Move things to src

This commit is contained in:
Erin 2023-09-05 15:41:43 -04:00
parent 41a3ad8f6a
commit d86d678588
3 changed files with 1 additions and 1 deletions

116
src/entrypoints.ts Normal file
View file

@ -0,0 +1,116 @@
/** 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: string) {
this.path = newPath;
script.js!.splice(i, 1, newPath);
},
}))
),
...(manifest.manifest_version === 2
? (manifest.background?.scripts || []).map((path, i) => ({
type: ManifestEntrypointKind.BACKGROUND_SCRIPT,
path,
replacePath (newPath: string) {
this.path = newPath;
manifest.background!.scripts!.splice(i, 1, newPath);
},
}))
: (manifest.background?.service_worker
? [{
type: ManifestEntrypointKind.BACKGROUND_SERVICE_WORKER,
path: manifest.background.service_worker,
replacePath (newPath: string) {
this.path = 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: string) {
this.path = newPath;
script.css!.splice(i, 1, newPath);
},
}))
),
...Object.entries(manifest.icons || {}).map(([iconSize, path]) => ({
type: ManifestEntrypointKind.ICON,
path,
replacePath (newPath: string) {
this.path = newPath;
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: string) {
this.path = 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: string) {
this.path = newPath;
entry.resources.splice(i, 1, newPath);
},
}))
)),
];

136
src/index.ts Normal file
View file

@ -0,0 +1,136 @@
/* 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,
},
],
},
];
}