137 lines
4.2 KiB
TypeScript
137 lines
4.2 KiB
TypeScript
/* eslint-env node */
|
|
|
|
import {readFile} from 'node:fs/promises';
|
|
import {basename, dirname, extname, join, relative, resolve} from 'node:path';
|
|
|
|
import {type Plugin, type RollupOptions} from 'rollup';
|
|
|
|
import {getAssetEntrypoints, getScriptEntrypoints} from './entrypoints';
|
|
|
|
/**
|
|
* Looks through a `manifest.json` file for Javascript entrypoints and other
|
|
* assets, and returns an array of Rollup configuration. This configuration will
|
|
* have each entrypoint processed as its own bundle, and will additionally
|
|
* handle copying the manifest and other assets into the build output, as well
|
|
* as any necessary path rewriting in the manifest itself.
|
|
* @param options
|
|
* @param options.manifest Path to the manifest file to be processed, relative
|
|
* to the current working directory
|
|
* @param options.outDir Directory path where build output will go
|
|
* @param options.plugins Rollup plugins to be used when processing each
|
|
* Javascript entry point
|
|
* @param options.sourcemap Controls inclusion of sourcemaps in the output
|
|
*/
|
|
export async function buildConfig ({
|
|
manifest: manifestPathRelative,
|
|
outDir,
|
|
plugins = [],
|
|
sourcemap,
|
|
}: {
|
|
manifest: string;
|
|
outDir: string;
|
|
plugins: Plugin[];
|
|
sourcemap: boolean | 'inline' | 'hidden';
|
|
}): Promise<RollupOptions[]> {
|
|
const manifestPath = resolve(process.cwd(), manifestPathRelative);
|
|
const manifestDirname = dirname(manifestPath);
|
|
|
|
// Load the manifest
|
|
let manifestContent: chrome.runtime.Manifest;
|
|
try {
|
|
manifestContent = JSON.parse(await readFile(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: string, ext = extname(filepath).slice(1)) {
|
|
// TODO: this function is hell there's gotta be a better way to do literally
|
|
// all of this
|
|
const cached = uniqueFileNameSegmentCache.get(filepath);
|
|
if (cached) {
|
|
return cached;
|
|
}
|
|
const idealName = join(dirname(filepath), basename(filepath, extname(filepath)));
|
|
|
|
const buildName = (str: string, n: number) => 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' as const,
|
|
sourcemap,
|
|
},
|
|
plugins,
|
|
};
|
|
}),
|
|
|
|
// 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
|
|
async buildStart () {
|
|
await Promise.all(assets.map(async ({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: await readFile(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,
|
|
},
|
|
],
|
|
},
|
|
];
|
|
}
|