rollup-build-webext-config/index.mjs
2023-07-28 05:28:06 -04:00

152 lines
4.9 KiB
JavaScript

/* 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,
},
],
},
];
}