import { readFileSync } from 'node:fs'; import { resolve, dirname, relative, join, basename, extname } from 'node:path'; /** The kind of an entrypoint in an extension manifest. */ var ManifestEntrypointKind; (function (ManifestEntrypointKind) { /** Javascript run as part of a content script */ ManifestEntrypointKind[ManifestEntrypointKind["CONTENT_SCRIPT_JS"] = 0] = "CONTENT_SCRIPT_JS"; /** CSS applied as part of a content script */ ManifestEntrypointKind[ManifestEntrypointKind["CONTENT_SCRIPT_CSS"] = 1] = "CONTENT_SCRIPT_CSS"; /** Javascript run in the background page of an MV2 extension */ ManifestEntrypointKind[ManifestEntrypointKind["BACKGROUND_SCRIPT"] = 2] = "BACKGROUND_SCRIPT"; /** Javascript run the service worker of an MV3 extension */ ManifestEntrypointKind[ManifestEntrypointKind["BACKGROUND_SERVICE_WORKER"] = 3] = "BACKGROUND_SERVICE_WORKER"; /** Image file loaded as the extension's icon at a particular size */ ManifestEntrypointKind[ManifestEntrypointKind["ICON"] = 4] = "ICON"; /** Path listed in the `web_accessible_resources` of an MV2 extension */ ManifestEntrypointKind[ManifestEntrypointKind["WEB_ACCESSIBLE_RESOURCE_V2"] = 5] = "WEB_ACCESSIBLE_RESOURCE_V2"; /** Path listed in the `web_accessible_resources` of an MV3 extension */ ManifestEntrypointKind[ManifestEntrypointKind["WEB_ACCESSIBLE_RESOURCE_V3"] = 6] = "WEB_ACCESSIBLE_RESOURCE_V3"; })(ManifestEntrypointKind || (ManifestEntrypointKind = {})); /** * Gets all script entrypoints from a manifest, returning them as * {@link ManifestEntrypoint} objects which allow for rewriting the path of each * individual entrypoint in place. * @param manifest Parsed `manifest.json` data. */ const getScriptEntrypoints = (manifest) => { var _a, _b, _c; return [ ...((_a = manifest.content_scripts) !== null && _a !== void 0 ? _a : []).flatMap(script => { var _a; return ((_a = script.js) !== null && _a !== void 0 ? _a : []).map((path, i) => ({ type: ManifestEntrypointKind.CONTENT_SCRIPT_JS, path, replacePath: (newPath) => script.js.splice(i, 1, newPath), })); }), ...(manifest.manifest_version === 2 ? (((_b = manifest.background) === null || _b === void 0 ? void 0 : _b.scripts) || []).map((path, i) => ({ type: ManifestEntrypointKind.BACKGROUND_SCRIPT, path, replacePath: (newPath) => manifest.background.scripts.splice(i, 1, newPath), })) : (((_c = manifest.background) === null || _c === void 0 ? void 0 : _c.service_worker) ? [{ type: ManifestEntrypointKind.BACKGROUND_SERVICE_WORKER, path: manifest.background.service_worker, replacePath: (newPath) => manifest.background.service_worker = newPath, }] : [])), ]; }; /** * Gets all asset entrypoints from a manifest, returning them as * {@link ManifestEntrypoint} objects which allow for rewriting the path of each * individual entrypoint in place. * @param manifest Parsed `manifest.json` data. */ const getAssetEntrypoints = (manifest) => { var _a, _b, _c; return [ ...((_a = manifest.content_scripts) !== null && _a !== void 0 ? _a : []).flatMap(script => { var _a; return ((_a = script.css) !== null && _a !== void 0 ? _a : []).map((path, i) => ({ type: ManifestEntrypointKind.CONTENT_SCRIPT_CSS, path, replacePath: (newPath) => script.css.splice(i, 1, newPath), })); }), ...Object.entries(manifest.icons || {}).map(([iconSize, path]) => ({ type: ManifestEntrypointKind.ICON, path, replacePath: (newPath) => manifest.icons[iconSize] = newPath, })), ...(manifest.manifest_version === 2 ? ((_b = manifest.web_accessible_resources) !== null && _b !== void 0 ? _b : []).map((path, i) => ({ type: ManifestEntrypointKind.WEB_ACCESSIBLE_RESOURCE_V2, path, replacePath: (newPath) => manifest.web_accessible_resources[i] = newPath, })) : ((_c = manifest.web_accessible_resources) !== null && _c !== void 0 ? _c : []).flatMap(entry => entry.resources.map((path, i) => ({ type: ManifestEntrypointKind.WEB_ACCESSIBLE_RESOURCE_V3, path, replacePath: (newPath) => entry.resources.splice(i, 1, newPath), })))) ]; }; /* eslint-env node */ function buildConfig({ manifest: manifestPathRelative, outDir, scriptPlugins = [], sourcemap, }) { const manifestPath = resolve(process.cwd(), manifestPathRelative); const manifestDirname = dirname(manifestPath); // Load the manifest let manifestContent; try { manifestContent = JSON.parse(readFileSync(manifestPath, { encoding: 'utf-8' })); } catch (error) { throw new Error('Failed to load manifest'); } // reserve manifest.json at the root of the build output - needs to be the // actual manifest file we generate const uniqueFileNameSegmentCache = new Map([['\0', 'manifest.json']]); function ensureUniquePath(filepath, ext = extname(filepath).slice(1)) { // console.log('getting segment for', filepath); const cached = uniqueFileNameSegmentCache.get(filepath); if (cached) { return cached; } const idealName = join(dirname(filepath), basename(filepath, extname(filepath))); const buildName = (str, n) => n ? `${str}_${n}.${ext}` : `${str}.${ext}`; let uniquenessNum = 0; const existingNames = [...uniqueFileNameSegmentCache.values()]; while (existingNames.some(existingName => existingName.toLowerCase() === buildName(idealName, uniquenessNum).toLowerCase())) { uniquenessNum += 1; } const finalName = buildName(idealName, uniquenessNum); uniqueFileNameSegmentCache.set(filepath, finalName); return finalName; } const scripts = getScriptEntrypoints(manifestContent); const assets = getAssetEntrypoints(manifestContent); return [ // Process each script entrypoint independently ...scripts.map(({ path, replacePath }) => { // Figure out where this bundle will live in the output const outPath = ensureUniquePath(path, 'js'); // Rewrite the manifest with that path replacePath(outPath); // Build the bundle return { input: relative(process.cwd(), join(manifestDirname, path)), output: { file: join(outDir, outPath), format: 'iife', sourcemap, }, plugins: scriptPlugins, }; }), // Special step that processes the manifest and injects other assets { input: manifestPathRelative, output: { file: join(outDir, 'manifest.json'), }, plugins: [ { name: '_manifest-asset-processing', // emit other assets buildStart() { assets.forEach(({ path, replacePath }) => { // Figure out where the asset will live in output const outPath = ensureUniquePath(path); // Rewrite the manifest with that path replacePath(outPath); // Emit the asset as part of the build step this.emitFile({ type: 'asset', fileName: outPath, source: readFileSync(join(manifestDirname, path)), }); }); }, // hacks to make sure the manifest is emitted as bare JSON load: id => id === manifestPath ? 'debugger;' : null, renderChunk: (_, chunk) => chunk.facadeModuleId === manifestPath ? JSON.stringify(manifestContent, null, '\t') : null, }, ], }, ]; } export { buildConfig };