diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..3c3629e --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +node_modules diff --git a/entrypoints.ts b/entrypoints.ts new file mode 100644 index 0000000..febd429 --- /dev/null +++ b/entrypoints.ts @@ -0,0 +1,89 @@ +/** The kind of an entrypoint in an extension manifest. */ +enum ManifestEntrypointKind { + /** Javascript run as part of a content script */ + CONTENT_SCRIPT_JS, + /** CSS applied as part of a content script */ + CONTENT_SCRIPT_CSS, + /** Javascript run in the background page of an MV2 extension */ + BACKGROUND_SCRIPT, + /** Javascript run the service worker of an MV3 extension */ + BACKGROUND_SERVICE_WORKER, + /** Image file loaded as the extension's icon at a particular size */ + ICON, + /** Path listed in the `web_accessible_resources` of an MV2 extension */ + WEB_ACCESSIBLE_RESOURCE_V2, + /** Path listed in the `web_accessible_resources` of an MV3 extension */ + WEB_ACCESSIBLE_RESOURCE_V3, +} + +/** + * A handle to a single entrypoint from a manifest, which allows for rewriting + * the path of the asset in the manifest. + */ +interface ManifestEntrypoint { + type: ManifestEntrypointKind; + /** The full path to the entrypoint file. */ + path: string; + /** + * Replaces this entry path with the given new path. This is an in-place + * operation and directly affects the original manifest object. + */ + replacePath: (path: string) => void; +} + +/** + * 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. + */ +export const getScriptEntrypoints = (manifest: chrome.runtime.Manifest): ManifestEntrypoint[] => [ + ...(manifest.content_scripts ?? []).flatMap(script => (script.js ?? []).map((path, i) => ({ + type: ManifestEntrypointKind.CONTENT_SCRIPT_JS, + path, + replacePath: newPath => script.js!.splice(i, 1, newPath), + }))), + ...(manifest.manifest_version === 2 + ? (manifest.background?.scripts || []).map((path, i) => ({ + type: ManifestEntrypointKind.BACKGROUND_SCRIPT, + path, + replacePath: newPath => manifest.background!.scripts!.splice(i, 1, newPath), + })) + : (manifest.background?.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. + */ +export const getAssetEntrypoints = (manifest: chrome.runtime.Manifest): ManifestEntrypoint[] => [ + ...(manifest.content_scripts ?? []).flatMap(script => (script.css ?? []).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 + ? (manifest.web_accessible_resources ?? []).map((path, i) => ({ + type: ManifestEntrypointKind.WEB_ACCESSIBLE_RESOURCE_V2, + path, + replacePath: newPath => manifest.web_accessible_resources![i] = newPath, + })) + : (manifest.web_accessible_resources ?? []).flatMap(entry => entry.resources.map((path, i) => ({ + type: ManifestEntrypointKind.WEB_ACCESSIBLE_RESOURCE_V3, + path, + replacePath: newPath => entry.resources.splice(i, 1, newPath), + }))) + ) +]; diff --git a/index.mjs b/index.mjs index bfe0d1d..1791d7f 100644 --- a/index.mjs +++ b/index.mjs @@ -2,6 +2,7 @@ import {readFileSync} from 'node:fs'; import {resolve, basename, extname, dirname, relative, join} from 'node:path'; +import { getAssetEntrypoints, getScriptEntrypoints } from './entrypoints'; export function buildConfig ({ manifest: manifestPathRelative, @@ -47,81 +48,31 @@ export function buildConfig ({ return join(base, uniqueFileNameSegment(entryPath, ext)); } - /** Scans a manifest for entrypoints */ - function getEntryPointsFromManifest (manifestContent) { - const scriptEntrypointAbsolutePaths = []; - const styleEntrypointAbsolutePathss = []; - const otherAssetAbsolutePaths = []; - // Gather all JS entry points specified in the manifest - (manifestContent.content_scripts || []).forEach(({css, js}) => { - css && css.forEach((filename, i) => { - const id = resolve(manifestDirname, filename); - styleEntrypointAbsolutePathss.push(id); - css.splice(i, 1, getOutputFilename(id, 'css')); - }); - js && js.forEach((filename, i) => { - const id = resolve(manifestDirname, filename); - scriptEntrypointAbsolutePaths.push(id); - js.splice(i, 1, getOutputFilename(id, 'js')); - }); - }); - manifestContent.background?.scripts && manifestContent.background.scripts.forEach((filename, i) => { - const id = resolve(manifestDirname, filename); - scriptEntrypointAbsolutePaths.push(id); - manifestContent.background.scripts.splice(i, 1, getOutputFilename(id)); - }); - if (manifestContent.background?.service_worker) { - const id = resolve(manifestDirname, manifestContent.background.service_worker); - scriptEntrypointAbsolutePaths.push(id); - manifestContent.background.service_worker = getOutputFilename(id); - } - manifestContent.icons && Object.entries(manifestContent.icons).forEach(([size, filename]) => { - const id = resolve(manifestDirname, filename); - // console.log('icon:', size, filename, getOutputFilename(id)); - otherAssetAbsolutePaths.push(id); - manifestContent.icons[size] = getOutputFilename(id); - }); - (manifestContent.web_accessible_resources || []).forEach((entry, i) => { - if (typeof entry === 'string') { - // mv2 - single top-level array of items - const id = resolve(manifestDirname, entry); - otherAssetAbsolutePaths.push(id); - manifestContent.web_accessible_resources.splice(i, 1, getOutputFilename(id)); - } else { - // mv3 - array of objects with `resources` keys - const {resources} = entry; - resources && resources.forEach((filename, j) => { - const id = resolve(manifestDirname, filename); - otherAssetAbsolutePaths.push(id); - resources.splice(j, 1, getOutputFilename(id)); - }); - } - }); - - return { - scripts: scriptEntrypointAbsolutePaths, - assets: [ - ...styleEntrypointAbsolutePathss, - ...otherAssetAbsolutePaths, - ], - }; - } - - const {scripts, assets} = getEntryPointsFromManifest(manifestContent); + const scripts = getScriptEntrypoints(manifestContent); + const assets = getAssetEntrypoints(manifestContent); return [ - ...scripts.map(entrypointPath => ({ - input: relative(process.cwd(), entrypointPath), - output: { - file: join(outDir, getOutputFilename(entrypointPath, 'js')), - format: 'iife', - sourcemap, - }, - plugins: scriptPlugins, - })), + // Process each script entrypoint independently + ...scripts.map(({path, replacePath}) => { + // Figure out where this bundle will live in the output + const outPath = getOutputFilename(path, 'js'); - // A special step that processes the manifest and copies over non-JS - // assets in the meantime + // Rewrite the manifest with that path + replacePath(outPath); + + // Build the bundle + return { + input: relative(process.cwd(), path), + output: { + file: join(outDir, outPath), + format: 'iife', + sourcemap, + }, + plugins: scriptPlugins, + }; + }), + + // Special step that processes the manifest and injects other assets { input: manifestPathRelative, output: { @@ -131,11 +82,20 @@ export function buildConfig ({ { // emit other assets buildStart () { - assets.forEach(absolutePath => this.emitFile({ - type: 'asset', - fileName: getOutputFilename(absolutePath), - source: readFileSync(absolutePath), - })); + assets.forEach(({path, replacePath}) => { + // Figure out where the asset will live in output + const outPath = getOutputFilename(path); + + // Rewrite the manifest with that path + replacePath(outPath); + + // Emit the asset as part of the build step + this.emitFile({ + type: 'asset', + fileName: getOutputFilename(absolutePath), + source: readFileSync(absolutePath), + }); + }); }, // hacks to make sure the manifest is emitted as bare JSON diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..ec0ced3 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,46 @@ +{ + "name": "rollup-create-webext-config", + "version": "0.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "rollup-create-webext-config", + "version": "0.0.0", + "devDependencies": { + "@types/chrome": "^0.0.242" + } + }, + "node_modules/@types/chrome": { + "version": "0.0.242", + "resolved": "https://registry.npmjs.org/@types/chrome/-/chrome-0.0.242.tgz", + "integrity": "sha512-SeMXBSfcAGX9ezTz7Pro7n/AiNdIH3cetkdbM+Kfg3zD24jmbnm0IAEIxzx8ccqrnJenLCfD7fR+4WIYAbeQHw==", + "dev": true, + "dependencies": { + "@types/filesystem": "*", + "@types/har-format": "*" + } + }, + "node_modules/@types/filesystem": { + "version": "0.0.32", + "resolved": "https://registry.npmjs.org/@types/filesystem/-/filesystem-0.0.32.tgz", + "integrity": "sha512-Yuf4jR5YYMR2DVgwuCiP11s0xuVRyPKmz8vo6HBY3CGdeMj8af93CFZX+T82+VD1+UqHOxTq31lO7MI7lepBtQ==", + "dev": true, + "dependencies": { + "@types/filewriter": "*" + } + }, + "node_modules/@types/filewriter": { + "version": "0.0.29", + "resolved": "https://registry.npmjs.org/@types/filewriter/-/filewriter-0.0.29.tgz", + "integrity": "sha512-BsPXH/irW0ht0Ji6iw/jJaK8Lj3FJemon2gvEqHKpCdDCeemHa+rI3WBGq5z7cDMZgoLjY40oninGxqk+8NzNQ==", + "dev": true + }, + "node_modules/@types/har-format": { + "version": "1.2.11", + "resolved": "https://registry.npmjs.org/@types/har-format/-/har-format-1.2.11.tgz", + "integrity": "sha512-T232/TneofqK30AD1LRrrf8KnjLvzrjWDp7eWST5KoiSzrBfRsLrWDPk4STQPW4NZG6v2MltnduBVmakbZOBIQ==", + "dev": true + } + } +} diff --git a/package.json b/package.json index 9e16993..f9f2442 100644 --- a/package.json +++ b/package.json @@ -1,5 +1,8 @@ { "name": "rollup-create-webext-config", "version": "0.0.0", - "main": "index.mjs" + "main": "index.mjs", + "devDependencies": { + "@types/chrome": "^0.0.242" + } }