diff --git a/build/index.js b/build/index.js new file mode 100644 index 0000000..c54e73f --- /dev/null +++ b/build/index.js @@ -0,0 +1,178 @@ +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'); + } + const uniqueFileNameSegmentCache = new Map(); + function uniqueFileNameSegment(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; + } + function getOutputFilename(entryPath, ext) { + // return join(dirname(entryPath), uniqueFileNameSegment(entryPath, + // ext)); + return uniqueFileNameSegment(entryPath, ext); + } + 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 = getOutputFilename(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 = getOutputFilename(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 }; diff --git a/entrypoints.ts b/entrypoints.ts index febd429..4d51425 100644 --- a/entrypoints.ts +++ b/entrypoints.ts @@ -41,18 +41,18 @@ export const getScriptEntrypoints = (manifest: chrome.runtime.Manifest): Manifes ...(manifest.content_scripts ?? []).flatMap(script => (script.js ?? []).map((path, i) => ({ type: ManifestEntrypointKind.CONTENT_SCRIPT_JS, path, - replacePath: newPath => script.js!.splice(i, 1, newPath), + replacePath: (newPath: string) => 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), + replacePath: (newPath: string) => 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, + replacePath: (newPath: string) => manifest.background!.service_worker = newPath, }] : []) ), ]; @@ -67,23 +67,23 @@ export const getAssetEntrypoints = (manifest: chrome.runtime.Manifest): Manifest ...(manifest.content_scripts ?? []).flatMap(script => (script.css ?? []).map((path, i) => ({ type: ManifestEntrypointKind.CONTENT_SCRIPT_CSS, path, - replacePath: newPath => script.css!.splice(i, 1, newPath), + replacePath: (newPath: string) => script.css!.splice(i, 1, newPath), }))), ...Object.entries(manifest.icons || {}).map(([iconSize, path]) => ({ type: ManifestEntrypointKind.ICON, path, - replacePath: newPath => manifest.icons![iconSize] = newPath, + replacePath: (newPath: string) => manifest.icons![iconSize as unknown as number] = 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, + replacePath: (newPath: string) => 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), + replacePath: (newPath: string) => entry.resources.splice(i, 1, newPath), }))) ) ]; diff --git a/index.ts b/index.ts index 5798937..43556a1 100644 --- a/index.ts +++ b/index.ts @@ -2,19 +2,27 @@ import {readFileSync} from 'node:fs'; import {resolve, basename, extname, dirname, relative, join} from 'node:path'; -import { getAssetEntrypoints, getScriptEntrypoints } from './entrypoints'; + +import {getAssetEntrypoints, getScriptEntrypoints} from './entrypoints'; + +import {type RollupOptions, type Plugin} from 'rollup'; export function buildConfig ({ manifest: manifestPathRelative, outDir, scriptPlugins = [], sourcemap, -}) { +}: { + manifest: string; + outDir: string; + scriptPlugins: Plugin[]; + sourcemap: boolean | 'inline' | 'hidden'; +}): RollupOptions[] { const manifestPath = resolve(process.cwd(), manifestPathRelative); const manifestDirname = dirname(manifestPath); // Load the manifest - let manifestContent; + let manifestContent: chrome.runtime.Manifest; try { manifestContent = JSON.parse(readFileSync(manifestPath, {encoding: 'utf-8'})); } catch (error) { @@ -22,15 +30,15 @@ export function buildConfig ({ } const uniqueFileNameSegmentCache = new Map(); - function uniqueFileNameSegment (filepath, ext = extname(filepath).slice(1)) { + function uniqueFileNameSegment (filepath: string, ext = extname(filepath).slice(1)) { // console.log('getting segment for', filepath); const cached = uniqueFileNameSegmentCache.get(filepath); if (cached) { return cached; } - const idealName = basename(filepath, extname(filepath)); + const idealName = join(dirname(filepath), basename(filepath, extname(filepath))); - const buildName = (str, n) => n ? `${str}_${n}.${ext}` : `${str}.${ext}`; + const buildName = (str: string, n: number) => n ? `${str}_${n}.${ext}` : `${str}.${ext}`; let uniquenessNum = 0; const existingNames = [...uniqueFileNameSegmentCache.values()]; @@ -43,9 +51,10 @@ export function buildConfig ({ return finalName; } - function getOutputFilename (entryPath, ext?) { - const base = relative(manifestDirname, dirname(entryPath)); - return join(base, uniqueFileNameSegment(entryPath, ext)); + function getOutputFilename (entryPath: string, ext?: string) { + // return join(dirname(entryPath), uniqueFileNameSegment(entryPath, + // ext)); + return uniqueFileNameSegment(entryPath, ext); } const scripts = getScriptEntrypoints(manifestContent); @@ -62,10 +71,10 @@ export function buildConfig ({ // Build the bundle return { - input: relative(process.cwd(), path), + input: relative(process.cwd(), join(manifestDirname, path)), output: { file: join(outDir, outPath), - format: 'iife', + format: 'iife' as const, sourcemap, }, plugins: scriptPlugins, @@ -80,6 +89,7 @@ export function buildConfig ({ }, plugins: [ { + name: '_manifest-asset-processing', // emit other assets buildStart () { assets.forEach(({path, replacePath}) => { @@ -93,7 +103,7 @@ export function buildConfig ({ this.emitFile({ type: 'asset', fileName: outPath, - source: readFileSync(path), + source: readFileSync(join(manifestDirname, path)), }); }); }, diff --git a/package-lock.json b/package-lock.json index ec0ced3..facc821 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,9 +8,66 @@ "name": "rollup-create-webext-config", "version": "0.0.0", "devDependencies": { - "@types/chrome": "^0.0.242" + "@rollup/plugin-typescript": "^11.1.2", + "@tsconfig/recommended": "^1.0.2", + "@types/chrome": "^0.0.242", + "rollup": "^3.26.3" } }, + "node_modules/@rollup/plugin-typescript": { + "version": "11.1.2", + "resolved": "https://registry.npmjs.org/@rollup/plugin-typescript/-/plugin-typescript-11.1.2.tgz", + "integrity": "sha512-0ghSOCMcA7fl1JM+0gYRf+Q/HWyg+zg7/gDSc+fRLmlJWcW5K1I+CLRzaRhXf4Y3DRyPnnDo4M2ktw+a6JcDEg==", + "dev": true, + "dependencies": { + "@rollup/pluginutils": "^5.0.1", + "resolve": "^1.22.1" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "rollup": "^2.14.0||^3.0.0", + "tslib": "*", + "typescript": ">=3.7.0" + }, + "peerDependenciesMeta": { + "rollup": { + "optional": true + }, + "tslib": { + "optional": true + } + } + }, + "node_modules/@rollup/pluginutils": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-5.0.2.tgz", + "integrity": "sha512-pTd9rIsP92h+B6wWwFbW8RkZv4hiR/xKsqre4SIuAOaOEQRxi0lqLke9k2/7WegC85GgUs9pjmOjCUi3In4vwA==", + "dev": true, + "dependencies": { + "@types/estree": "^1.0.0", + "estree-walker": "^2.0.2", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "rollup": "^1.20.0||^2.0.0||^3.0.0" + }, + "peerDependenciesMeta": { + "rollup": { + "optional": true + } + } + }, + "node_modules/@tsconfig/recommended": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@tsconfig/recommended/-/recommended-1.0.2.tgz", + "integrity": "sha512-dbHBtbWBOjq0/otpopAE02NT2Cm05Qe2JsEKeCf/wjSYbI2hz8nCqnpnOJWHATgjDz4fd3dchs3Wy1gQGjfN6w==", + "dev": true + }, "node_modules/@types/chrome": { "version": "0.0.242", "resolved": "https://registry.npmjs.org/@types/chrome/-/chrome-0.0.242.tgz", @@ -21,6 +78,12 @@ "@types/har-format": "*" } }, + "node_modules/@types/estree": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.1.tgz", + "integrity": "sha512-LG4opVs2ANWZ1TJoKc937iMmNstM/d0ae1vNbnBvBhqCSezgVUOzcLCqbI5elV8Vy6WKwKjaqR+zO9VKirBBCA==", + "dev": true + }, "node_modules/@types/filesystem": { "version": "0.0.32", "resolved": "https://registry.npmjs.org/@types/filesystem/-/filesystem-0.0.32.tgz", @@ -41,6 +104,133 @@ "resolved": "https://registry.npmjs.org/@types/har-format/-/har-format-1.2.11.tgz", "integrity": "sha512-T232/TneofqK30AD1LRrrf8KnjLvzrjWDp7eWST5KoiSzrBfRsLrWDPk4STQPW4NZG6v2MltnduBVmakbZOBIQ==", "dev": true + }, + "node_modules/estree-walker": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz", + "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==", + "dev": true + }, + "node_modules/fsevents": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "dev": true, + "hasInstallScript": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", + "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==", + "dev": true + }, + "node_modules/has": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", + "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", + "dev": true, + "dependencies": { + "function-bind": "^1.1.1" + }, + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/is-core-module": { + "version": "2.12.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.12.1.tgz", + "integrity": "sha512-Q4ZuBAe2FUsKtyQJoQHlvP8OvBERxO3jEmy1I7hcRXcJBGGHFh/aJBswbXuS9sgrDH2QUO8ilkwNPHvHMd8clg==", + "dev": true, + "dependencies": { + "has": "^1.0.3" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true + }, + "node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/resolve": { + "version": "1.22.2", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.2.tgz", + "integrity": "sha512-Sb+mjNHOULsBv818T40qSPeRiuWLyaGMa5ewydRLFimneixmVy2zdivRl+AF6jaYPC8ERxGDmFSiqui6SfPd+g==", + "dev": true, + "dependencies": { + "is-core-module": "^2.11.0", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/rollup": { + "version": "3.26.3", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-3.26.3.tgz", + "integrity": "sha512-7Tin0C8l86TkpcMtXvQu6saWH93nhG3dGQ1/+l5V2TDMceTxO7kDiK6GzbfLWNNxqJXm591PcEZUozZm51ogwQ==", + "dev": true, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=14.18.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "dev": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/typescript": { + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.1.6.tgz", + "integrity": "sha512-zaWCozRZ6DLEWAWFrVDz1H6FVXzUSfTy5FUMWsQlU8Ym5JP9eO4xkTIROFCQvhQf61z6O/G6ugw3SgAnvvm+HA==", + "dev": true, + "peer": true, + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } } } } diff --git a/package.json b/package.json index f9f2442..241c3cf 100644 --- a/package.json +++ b/package.json @@ -1,8 +1,12 @@ { "name": "rollup-create-webext-config", "version": "0.0.0", - "main": "index.mjs", + "main": "build/index.js", + "type": "module", "devDependencies": { - "@types/chrome": "^0.0.242" + "@rollup/plugin-typescript": "^11.1.2", + "@tsconfig/recommended": "^1.0.2", + "@types/chrome": "^0.0.242", + "rollup": "^3.26.3" } } diff --git a/rollup.config.js b/rollup.config.js new file mode 100644 index 0000000..a098d2d --- /dev/null +++ b/rollup.config.js @@ -0,0 +1,17 @@ +import {defineConfig} from 'rollup'; +import typescript from '@rollup/plugin-typescript'; + +export default defineConfig({ + input: 'index.ts', + output: { + format: 'es', + dir: 'build', + }, + external: [ + 'node:fs', + 'node:path', + ], + plugins: [ + typescript(), + ], +}); diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..ff8085a --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,3 @@ +{ + "extends": "@tsconfig/recommended/tsconfig.json" +}