/* eslint-env node */ import {readFileSync} from 'node:fs'; import {resolve, basename, extname, dirname, relative, join} from 'node:path'; export function buildConfig ({ manifest: manifestPathRelative, outDir = 'build/[platform]', 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 = 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) { const base = relative(manifestDirname, dirname(entryPath)); 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); return [ ...scripts.map(entrypointPath => ({ input: relative(process.cwd(), entrypointPath), output: { file: join(outDir, getOutputFilename(entrypointPath, 'js')), format: 'iife', sourcemap, }, plugins: scriptPlugins, })), // A special step that processes the manifest and copies over non-JS // assets in the meantime { input: manifestPathRelative, output: { file: join(outDir, 'manifest.json'), }, plugins: [ { // emit other assets buildStart () { assets.forEach(absolutePath => this.emitFile({ type: 'asset', fileName: getOutputFilename(absolutePath), source: readFileSync(absolutePath), })); }, // 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, }, ], }, ]; }