From ff276f992f87f1a6976cf37768c225e9a12d9950 Mon Sep 17 00:00:00 2001 From: grypez <143971198+grypez@users.noreply.github.com> Date: Mon, 26 Aug 2024 12:36:32 -0500 Subject: [PATCH 01/15] build(extension): Add *-trusted-header externalization logic. --- .../src/background-trusted-header.js | 2 + packages/extension/src/background.ts | 4 +- packages/extension/tsconfig.build.json | 6 +- packages/extension/tsconfig.json | 12 ++- packages/extension/vite.config.ts | 97 +++++++++++++++++-- 5 files changed, 108 insertions(+), 13 deletions(-) create mode 100644 packages/extension/src/background-trusted-header.js diff --git a/packages/extension/src/background-trusted-header.js b/packages/extension/src/background-trusted-header.js new file mode 100644 index 000000000..19405f97b --- /dev/null +++ b/packages/extension/src/background-trusted-header.js @@ -0,0 +1,2 @@ +import './dev-console.js'; +import './endoify.js'; diff --git a/packages/extension/src/background.ts b/packages/extension/src/background.ts index ddf252964..723c2a566 100644 --- a/packages/extension/src/background.ts +++ b/packages/extension/src/background.ts @@ -1,6 +1,4 @@ -import './dev-console.js'; -import './endoify.js'; - +import './background-trusted-header.js'; import type { ExtensionMessage } from './shared.js'; import { Command, makeHandledCallback } from './shared.js'; diff --git a/packages/extension/tsconfig.build.json b/packages/extension/tsconfig.build.json index ab2ce37fb..29dc3a663 100644 --- a/packages/extension/tsconfig.build.json +++ b/packages/extension/tsconfig.build.json @@ -13,5 +13,9 @@ { "path": "../shims/tsconfig.build.json" }, { "path": "../streams/tsconfig.build.json" } ], - "include": ["./src/**/*.ts", "./src/dev-console.js"] + "include": [ + "./src/**/*.ts", + "./src/**/*-trusted-header.js", + "./src/dev-console.js" + ] } diff --git a/packages/extension/tsconfig.json b/packages/extension/tsconfig.json index 060acd4ab..9cb17a29d 100644 --- a/packages/extension/tsconfig.json +++ b/packages/extension/tsconfig.json @@ -9,6 +9,14 @@ "skipLibCheck": true, "types": ["chrome", "ses", "vitest", "vitest/jsdom"] }, - "references": [{ "path": "../streams" }, { "path": "../test-utils" }], - "include": ["./src/**/*.ts", "./src/dev-console.js"] + "references": [ + { "path": "../streams" }, + { "path": "../test-utils" }, + { "path": "../shims" } + ], + "include": [ + "./src/**/*.ts", + "./src/**/*-trusted-header.js", + "./src/dev-console.js" + ] } diff --git a/packages/extension/vite.config.ts b/packages/extension/vite.config.ts index e50397ff1..6172932e4 100644 --- a/packages/extension/vite.config.ts +++ b/packages/extension/vite.config.ts @@ -10,12 +10,6 @@ import { viteStaticCopy } from 'vite-plugin-static-copy'; const projectRoot = './src'; -/** - * Module specifiers that will be ignored by Rollup if imported, and therefore - * not transformed. **Only applies to JavaScript and TypeScript files.** - */ -const externalModules: readonly string[] = ['./dev-console.js', './endoify.js']; - /** * Files that need to be statically copied to the destination directory. * Paths are relative from the project root directory. @@ -26,6 +20,7 @@ const staticCopyTargets: readonly string[] = [ // External modules 'dev-console.js', '../../shims/dist/endoify.js', + 'background-trusted-header.js', ]; // https://vitejs.dev/config/ @@ -36,7 +31,6 @@ export default defineConfig({ emptyOutDir: true, outDir: path.resolve(projectRoot, '../dist'), rollupOptions: { - external: [...externalModules], input: { background: path.resolve(projectRoot, 'background.ts'), offscreen: path.resolve(projectRoot, 'offscreen.html'), @@ -56,6 +50,7 @@ export default defineConfig({ targets: staticCopyTargets.map((src) => ({ src, dest: './' })), watch: { reloadPageOnChange: true }, }), + endoifyTrustedHeaderPlugin(), ], }); @@ -101,3 +96,91 @@ function endoifyHtmlFilesPlugin(): Plugin { }, }; } + +/** + * Vite plugin to ensure that the following are true: + * - Every entrypoint contains at most one import from a *trusted-header file. + * - The import statement, if it exists, is the first line of the bundled output. + * + * @returns A rollup plugin for automatically externalizing trusted headers and checking they are imported first in the files that import them. + */ +function endoifyTrustedHeaderPlugin(): Plugin { + const trustedHeaderImporters = new Map(); + const isTrustedHeader = (value: string): boolean => + value.match(/-trusted-header\./u) !== null; + const makeExpectedPrefix = (moduleId: string): RegExp => { + const headerName = `${path.basename( + moduleId, + path.extname(moduleId), + )}-trusted-header.`; + const expectedPrefix = new RegExp( + `^import\\s*['"]\\./${headerName}js['"];`, + 'u', + ); + console.log(expectedPrefix); + return expectedPrefix; + }; + return { + name: 'ocap-kernel:trusted-header', + + resolveId: { + order: 'pre', + handler(source, importer) { + if (isTrustedHeader(source) && importer !== undefined) { + if (trustedHeaderImporters.has(importer)) { + this.error( + `MultipleTrustedHeaders: Module "${importer}" attempted to import trusted-header "${source}" ` + + `but already imported trusted-header "${trustedHeaderImporters.get( + importer, + )}".`, + ); + } + trustedHeaderImporters.set(importer, source); + this.info( + `Module "${source}" has been externalized because it was identified as a trusted-header.`, + ); + return { id: source, external: true }; + } + return null; + }, + }, + + buildEnd: { + order: 'post', + handler(error) { + if (error !== undefined) { + return; + } + const trustedHeaders = Array.from(this.getModuleIds()).filter( + (moduleId) => isTrustedHeader(moduleId), + ); + const importers = trustedHeaders.map((trustedHeader) => + this.getModuleInfo(trustedHeader)?.importers.at(0), + ); + importers.forEach((moduleId: string | undefined) => { + if (moduleId === undefined) { + this.warn( + `UnusedTrustedHeader: Module ${moduleId} was identified as a trusted header but no modules import it.`, + ); + return; + } + const code = this.getModuleInfo(moduleId)?.code; + if (code === null || code === undefined) { + this.error( + `NoCode: Module ${moduleId} was identified as a trusted header importer but has no code at buildEnd.`, + ); + } + const prefix = makeExpectedPrefix(moduleId); + if (code.match(prefix) === null) { + this.error( + `MissingTrustedHeaderImport: Module ${moduleId} was identified as a trusted header importer, ` + + `but does not begin with trusted header import.\n` + + `ExpectedPrefix: ${prefix}\n` + + `ObservedCode: ${code.split(';').at(0)}`, + ); + } + }); + }, + }, + }; +} From 6aeef36117a615fca39d21d37fd7f9e32087e7d0 Mon Sep 17 00:00:00 2001 From: grypez <143971198+grypez@users.noreply.github.com> Date: Tue, 3 Sep 2024 15:09:24 -0500 Subject: [PATCH 02/15] chore(extension): Granted trusted header lint exceptions. --- packages/extension/.eslintrc.cjs | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/packages/extension/.eslintrc.cjs b/packages/extension/.eslintrc.cjs index c747e0b88..9f07ad10e 100644 --- a/packages/extension/.eslintrc.cjs +++ b/packages/extension/.eslintrc.cjs @@ -19,5 +19,14 @@ module.exports = { project: ['./tsconfig.scripts.json'], }, }, + + { + files: ['src/**/*-trusted-header.js'], + rules: { + 'import-x/extensions': 'off', + 'import-x/no-unassigned-import': 'off', + 'import-x/no-unresolved': 'off', + }, + }, ], }; From 7761df50ed1f7959b57a8ea9d825d78904bbba53 Mon Sep 17 00:00:00 2001 From: grypez <143971198+grypez@users.noreply.github.com> Date: Tue, 3 Sep 2024 15:29:01 -0500 Subject: [PATCH 03/15] refactor(extension): Modularize build plugins. --- .../rollup-plugin-endoify-trusted-header.ts | 90 ++++++++++++ .../plugins/vite-plugin-endoify-html-files.ts | 46 ++++++ packages/extension/vite.config.ts | 137 +----------------- 3 files changed, 139 insertions(+), 134 deletions(-) create mode 100644 packages/extension/plugins/rollup-plugin-endoify-trusted-header.ts create mode 100644 packages/extension/plugins/vite-plugin-endoify-html-files.ts diff --git a/packages/extension/plugins/rollup-plugin-endoify-trusted-header.ts b/packages/extension/plugins/rollup-plugin-endoify-trusted-header.ts new file mode 100644 index 000000000..9d77937c0 --- /dev/null +++ b/packages/extension/plugins/rollup-plugin-endoify-trusted-header.ts @@ -0,0 +1,90 @@ +import path from 'path'; +import type { Plugin } from 'vite'; + +/** + * Vite plugin to ensure that the following are true: + * - Every entrypoint contains at most one import from a *trusted-header file. + * - The import statement, if it exists, is the first line of the bundled output. + * + * @returns A rollup plugin for automatically externalizing trusted headers and checking they are imported first in the files that import them. + */ +export function endoifyTrustedHeaderPlugin(): Plugin { + const trustedHeaderImporters = new Map(); + const isTrustedHeader = (value: string): boolean => + value.match(/-trusted-header\./u) !== null; + const makeExpectedPrefix = (moduleId: string): RegExp => { + const headerName = `${path.basename( + moduleId, + path.extname(moduleId), + )}-trusted-header.`; + const expectedPrefix = new RegExp( + `^import\\s*['"]\\./${headerName}js['"];`, + 'u', + ); + console.log(expectedPrefix); + return expectedPrefix; + }; + return { + name: 'ocap-kernel:endoify-trusted-header', + + resolveId: { + order: 'pre', + handler(source, importer) { + if (isTrustedHeader(source) && importer !== undefined) { + if (trustedHeaderImporters.has(importer)) { + this.error( + `MultipleTrustedHeaders: Module "${importer}" attempted to import trusted-header "${source}" ` + + `but already imported trusted-header "${trustedHeaderImporters.get( + importer, + )}".`, + ); + } + trustedHeaderImporters.set(importer, source); + this.info( + `Module "${source}" has been externalized because it was identified as a trusted-header.`, + ); + return { id: source, external: true }; + } + return null; + }, + }, + + buildEnd: { + order: 'post', + handler(error) { + if (error !== undefined) { + return; + } + const trustedHeaders = Array.from(this.getModuleIds()).filter( + (moduleId) => isTrustedHeader(moduleId), + ); + const importers = trustedHeaders.map((trustedHeader) => + this.getModuleInfo(trustedHeader)?.importers.at(0), + ); + importers.forEach((moduleId: string | undefined) => { + if (moduleId === undefined) { + this.warn( + `UnusedTrustedHeader: Module ${moduleId} was identified as a trusted header but no modules import it.`, + ); + return; + } + const code = this.getModuleInfo(moduleId)?.code; + if (code === null || code === undefined) { + this.error( + `NoCode: Module ${moduleId} was identified as a trusted header importer but has no code at buildEnd.`, + ); + } + const prefix = makeExpectedPrefix(moduleId); + if (code.match(prefix) === null) { + this.error( + `MissingTrustedHeaderImport: Module ${moduleId} was identified as a trusted header importer, ` + + `but does not begin with trusted header import.\n` + + `ExpectedPrefix: ${prefix}\n` + + `ObservedCode: ${code.split(';').at(0)}`, + ); + } + }); + }, + }, + }; +} diff --git a/packages/extension/plugins/vite-plugin-endoify-html-files.ts b/packages/extension/plugins/vite-plugin-endoify-html-files.ts new file mode 100644 index 000000000..0f63028dd --- /dev/null +++ b/packages/extension/plugins/vite-plugin-endoify-html-files.ts @@ -0,0 +1,46 @@ +import { load as loadHtml } from 'cheerio'; +import { format as prettierFormat } from 'prettier'; +import type { Plugin } from 'vite'; + +/** + * Vite plugin to insert the endoify script before the first script in the head element. + * + * @throws If the HTML document already references the endoify script or lacks the expected + * structure. + * @returns The Vite plugin. + */ +export function endoifyHtmlFilesPlugin(): Plugin { + const endoifyElement = ''; + + return { + name: 'ocap-kernel:endoify-html-files', + async transformIndexHtml(htmlString): Promise { + const htmlDoc = loadHtml(htmlString); + + if (htmlDoc('script[src="endoify.ts"]').length > 0) { + throw new Error( + `HTML document should not reference "endoify.ts" directly:\n${htmlString}`, + ); + } + + if (htmlDoc('script[src="endoify.js"]').length > 0) { + throw new Error( + `HTML document already references endoify script:\n${htmlString}`, + ); + } + + if (htmlDoc('head').length !== 1 || htmlDoc('head > script').length < 1) { + throw new Error( + `Expected HTML document with a single containing at least one '; - - return { - name: 'externalize-plugin', - async transformIndexHtml(htmlString): Promise { - const htmlDoc = loadHtml(htmlString); - - if (htmlDoc('script[src="endoify.ts"]').length > 0) { - throw new Error( - `HTML document should not reference "endoify.ts" directly:\n${htmlString}`, - ); - } - - if (htmlDoc('script[src="endoify.js"]').length > 0) { - throw new Error( - `HTML document already references endoify script:\n${htmlString}`, - ); - } - - if (htmlDoc('head').length !== 1 || htmlDoc('head > script').length < 1) { - throw new Error( - `Expected HTML document with a single containing at least one '; return { - name: 'ocap-kernel:endoify-html-files', + name: 'ocap-kernel:html-trusted-prelude', async transformIndexHtml(htmlString): Promise { const htmlDoc = loadHtml(htmlString); diff --git a/packages/extension/plugins/rollup-plugin-endoify-trusted-header.ts b/packages/extension/vite-plugins/js-trusted-prelude.ts similarity index 53% rename from packages/extension/plugins/rollup-plugin-endoify-trusted-header.ts rename to packages/extension/vite-plugins/js-trusted-prelude.ts index e3fc10a3a..394e4d2ed 100644 --- a/packages/extension/plugins/rollup-plugin-endoify-trusted-header.ts +++ b/packages/extension/vite-plugins/js-trusted-prelude.ts @@ -3,56 +3,56 @@ import type { Plugin } from 'vite'; /** * Vite plugin to ensure that the following are true: - * - Every bundle contains at most one import from a *trusted-header file. + * - Every bundle contains at most one import from a trusted prelude file. * - The import statement, if it exists, is the first line of the bundled output. * - * @returns A vite plugin for automatically externalizing trusted headers and checking they are imported first in the files that import them. + * @returns A vite plugin for automatically externalizing trusted preludes and checking they are imported first in the files that import them. */ -export function endoifyTrustedHeaderPlugin(): Plugin { - const trustedHeaderImporters = new Map(); - const isTrustedHeader = (value: string): boolean => - value.match(/-trusted-header\./u) !== null; +export function jsTrustedPreludePlugin(): Plugin { + const trustedPreludeImporters = new Map(); + const isTrustedPrelude = (value: string): boolean => + value.match(/-trusted-prelude\./u) !== null; const makeExpectedPrefix = (moduleId: string): RegExp => { - const headerName = `${path.basename( + const preludeName = `${path.basename( moduleId, path.extname(moduleId), - )}-trusted-header.`; + )}-trusted-prelude.`; const expectedPrefix = new RegExp( - `^import\\s*['"]\\./${headerName}js['"];`, + `^import\\s*['"]\\./${preludeName}js['"];`, 'u', ); console.log(expectedPrefix); return expectedPrefix; }; return { - name: 'ocap-kernel:endoify-trusted-header', + name: 'ocap-kernel:js-trusted-prelude', resolveId: { order: 'pre', /** - * Automatically externalize files with names `*-trusted-header.*`, and ensure no source imports more than one such file. + * Automatically externalize files with names `*-trusted-prelude.*`, and ensure no source imports more than one such file. * * @param source - The module which is doing the importing. * @param importer - The module being imported. * @returns A ResolveIdResult indicating how vite should resolve this source. - * @throws If a source attempts to import more than one trusted header. + * @throws If a source attempts to import more than one trusted prelude. */ handler(source, importer) { - if (isTrustedHeader(source) && importer !== undefined) { - // Check if this importer has already imported another trusted header. - if (trustedHeaderImporters.has(importer)) { + if (isTrustedPrelude(source) && importer !== undefined) { + // Check if this importer has already imported another trusted prelude. + if (trustedPreludeImporters.has(importer)) { this.error( - `Module "${importer}" attempted to import trusted-header "${source}" ` + - `but already imported trusted-header "${trustedHeaderImporters.get( + `Module "${importer}" attempted to import trusted prelude "${source}" ` + + `but already imported trusted prelude "${trustedPreludeImporters.get( importer, )}".`, ); } - trustedHeaderImporters.set(importer, source); + trustedPreludeImporters.set(importer, source); // Tell vite to externalize this source. this.info( - `Module "${source}" has been externalized because it was identified as a trusted-header.`, + `Module "${source}" has been externalized because it was identified as a trusted prelude.`, ); return { id: source, external: true }; } @@ -63,39 +63,39 @@ export function endoifyTrustedHeaderPlugin(): Plugin { buildEnd: { order: 'post', /** - * Check that identified trusted-headers are their importers' first import in the output bundle. + * Check that identified trusted preludes are their importers' first import in the output bundle. * * @param error - The error that caused the build to end, undefined if none (yet) occured. - * @throws If an identified trusted-header importer does not import its trusted header at buildEnd. + * @throws If an identified trusted prelude importer does not import its trusted prelude at buildEnd. */ handler(error): void { if (error !== undefined) { return; } - const trustedHeaders = Array.from(this.getModuleIds()).filter( - (moduleId) => isTrustedHeader(moduleId), + const trustedPreludes = Array.from(this.getModuleIds()).filter( + (moduleId) => isTrustedPrelude(moduleId), ); - const importers = trustedHeaders.map((trustedHeader) => - this.getModuleInfo(trustedHeader)?.importers.at(0), + const importers = trustedPreludes.map((trustedPrelude) => + this.getModuleInfo(trustedPrelude)?.importers.at(0), ); importers.forEach((moduleId: string | undefined) => { if (moduleId === undefined) { this.warn( - `Module ${moduleId} was identified as a trusted header but no modules import it.`, + `Module ${moduleId} was identified as a trusted prelude but no modules import it.`, ); return; } const code = this.getModuleInfo(moduleId)?.code; if (code === null || code === undefined) { this.error( - `Module ${moduleId} was identified as a trusted header importer but has no code at buildEnd.`, + `Module ${moduleId} was identified as a trusted prelude importer but has no code at buildEnd.`, ); } const prefix = makeExpectedPrefix(moduleId); if (code.match(prefix) === null) { this.error( - `Module ${moduleId} was identified as a trusted header importer, ` + - `but does not begin with trusted header import.\n` + + `Module ${moduleId} was identified as a trusted prelude importer, ` + + `but does not begin with trusted prelude import.\n` + `Expected prefix: ${prefix}\n` + `Observed code: ${code.split(';').at(0)}`, ); diff --git a/packages/extension/vite.config.ts b/packages/extension/vite.config.ts index 89e30af6e..15445b1ac 100644 --- a/packages/extension/vite.config.ts +++ b/packages/extension/vite.config.ts @@ -5,8 +5,8 @@ import path from 'path'; import { defineConfig } from 'vite'; import { viteStaticCopy } from 'vite-plugin-static-copy'; -import { endoifyTrustedHeaderPlugin } from './plugins/rollup-plugin-endoify-trusted-header'; -import { endoifyHtmlFilesPlugin } from './plugins/vite-plugin-endoify-html-files'; +import { htmlTrustedPreludePlugin } from './vite-plugins/html-trusted-prelude'; +import { jsTrustedPreludePlugin } from './vite-plugins/js-trusted-prelude'; const projectRoot = './src'; @@ -20,7 +20,7 @@ const staticCopyTargets: readonly string[] = [ // External modules 'dev-console.js', '../../shims/dist/endoify.js', - 'background-trusted-header.js', + 'background-trusted-prelude.js', ]; // https://vitejs.dev/config/ @@ -45,11 +45,11 @@ export default defineConfig({ }, plugins: [ - endoifyHtmlFilesPlugin(), + htmlTrustedPreludePlugin(), viteStaticCopy({ targets: staticCopyTargets.map((src) => ({ src, dest: './' })), watch: { reloadPageOnChange: true }, }), - endoifyTrustedHeaderPlugin(), + jsTrustedPreludePlugin(), ], }); From deca52e48bd089471030707ed92c82cc0906fb7d Mon Sep 17 00:00:00 2001 From: grypez <143971198+grypez@users.noreply.github.com> Date: Thu, 5 Sep 2024 15:38:27 -0500 Subject: [PATCH 08/15] fix(extension): Tidy the vite-plugins. --- packages/extension/vite-plugins/js-trusted-prelude.ts | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/packages/extension/vite-plugins/js-trusted-prelude.ts b/packages/extension/vite-plugins/js-trusted-prelude.ts index 394e4d2ed..c320d59a7 100644 --- a/packages/extension/vite-plugins/js-trusted-prelude.ts +++ b/packages/extension/vite-plugins/js-trusted-prelude.ts @@ -21,7 +21,6 @@ export function jsTrustedPreludePlugin(): Plugin { `^import\\s*['"]\\./${preludeName}js['"];`, 'u', ); - console.log(expectedPrefix); return expectedPrefix; }; return { @@ -73,7 +72,7 @@ export function jsTrustedPreludePlugin(): Plugin { return; } const trustedPreludes = Array.from(this.getModuleIds()).filter( - (moduleId) => isTrustedPrelude(moduleId), + isTrustedPrelude, ); const importers = trustedPreludes.map((trustedPrelude) => this.getModuleInfo(trustedPrelude)?.importers.at(0), @@ -86,7 +85,7 @@ export function jsTrustedPreludePlugin(): Plugin { return; } const code = this.getModuleInfo(moduleId)?.code; - if (code === null || code === undefined) { + if (!code) { this.error( `Module ${moduleId} was identified as a trusted prelude importer but has no code at buildEnd.`, ); From 5a1de6a00e6e263c723e4991a863029f3aa067bc Mon Sep 17 00:00:00 2001 From: grypez <143971198+grypez@users.noreply.github.com> Date: Thu, 5 Sep 2024 16:34:39 -0500 Subject: [PATCH 09/15] refactor(build): Remove Plugin suffix from exported Plugin maker functions. --- packages/extension/vite-plugins/html-trusted-prelude.ts | 2 +- packages/extension/vite-plugins/js-trusted-prelude.ts | 2 +- packages/extension/vite.config.ts | 8 ++++---- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/packages/extension/vite-plugins/html-trusted-prelude.ts b/packages/extension/vite-plugins/html-trusted-prelude.ts index dedc10007..89ebb3385 100644 --- a/packages/extension/vite-plugins/html-trusted-prelude.ts +++ b/packages/extension/vite-plugins/html-trusted-prelude.ts @@ -9,7 +9,7 @@ import type { Plugin } from 'vite'; * structure. * @returns The Vite plugin. */ -export function htmlTrustedPreludePlugin(): Plugin { +export function htmlTrustedPrelude(): Plugin { const endoifyElement = ''; return { diff --git a/packages/extension/vite-plugins/js-trusted-prelude.ts b/packages/extension/vite-plugins/js-trusted-prelude.ts index c320d59a7..d3c1e5f85 100644 --- a/packages/extension/vite-plugins/js-trusted-prelude.ts +++ b/packages/extension/vite-plugins/js-trusted-prelude.ts @@ -8,7 +8,7 @@ import type { Plugin } from 'vite'; * * @returns A vite plugin for automatically externalizing trusted preludes and checking they are imported first in the files that import them. */ -export function jsTrustedPreludePlugin(): Plugin { +export function jsTrustedPrelude(): Plugin { const trustedPreludeImporters = new Map(); const isTrustedPrelude = (value: string): boolean => value.match(/-trusted-prelude\./u) !== null; diff --git a/packages/extension/vite.config.ts b/packages/extension/vite.config.ts index 15445b1ac..91e8f2d81 100644 --- a/packages/extension/vite.config.ts +++ b/packages/extension/vite.config.ts @@ -5,8 +5,8 @@ import path from 'path'; import { defineConfig } from 'vite'; import { viteStaticCopy } from 'vite-plugin-static-copy'; -import { htmlTrustedPreludePlugin } from './vite-plugins/html-trusted-prelude'; -import { jsTrustedPreludePlugin } from './vite-plugins/js-trusted-prelude'; +import { htmlTrustedPrelude } from './vite-plugins/html-trusted-prelude'; +import { jsTrustedPrelude } from './vite-plugins/js-trusted-prelude'; const projectRoot = './src'; @@ -45,11 +45,11 @@ export default defineConfig({ }, plugins: [ - htmlTrustedPreludePlugin(), + htmlTrustedPrelude(), viteStaticCopy({ targets: staticCopyTargets.map((src) => ({ src, dest: './' })), watch: { reloadPageOnChange: true }, }), - jsTrustedPreludePlugin(), + jsTrustedPrelude(), ], }); From 48f768ccf65c7b1652d589745a093885dba98f59 Mon Sep 17 00:00:00 2001 From: grypez <143971198+grypez@users.noreply.github.com> Date: Mon, 9 Sep 2024 14:09:54 -0500 Subject: [PATCH 10/15] build(extension): Cooperatively correct jsTrustedPrelude implementation. The implementation throws errors only if its configuration assumptions are violated. Otherwise, it proactively externalizes trusted preludes and attempts to correct any correctness-breaking import permutations that may have occurred during bundling, merely warning that some corrective action was necessary but not breaking the build. The configuration assumptions are as follows. - If a file has a declared trusted prelude, the file must be declared an entry point. - If a file has a declared trusted prelude, it must not transitively import more than one declared trusted prelude. The result is a plugin which ensures that a declared trusted prelude file is never imported after any other code, bandaging bundles which unintentionally violate this constraint and rejecting source code which intentionally violates this constaint. --- .../vite-plugins/js-trusted-prelude.ts | 267 +++++++++++++----- packages/extension/vite.config.ts | 11 +- 2 files changed, 203 insertions(+), 75 deletions(-) diff --git a/packages/extension/vite-plugins/js-trusted-prelude.ts b/packages/extension/vite-plugins/js-trusted-prelude.ts index d3c1e5f85..40d61dc7d 100644 --- a/packages/extension/vite-plugins/js-trusted-prelude.ts +++ b/packages/extension/vite-plugins/js-trusted-prelude.ts @@ -1,105 +1,226 @@ import path from 'path'; import type { Plugin } from 'vite'; +type PluginContext = { + warn: (message: string) => void; + error: (message: string) => never; +}; + +/** + * Resolve trusted prelude file names as their basename only, rewriting `.[mc]?ts` files as `.[mc]?js`. + * + * @param fileName - The trusted prelude fileName to resolve. + * @returns The simple filename '[basename].[ext]', with '*ts' extensions converted to '*js' extensions. + */ +const resolvePreludeFileName = (fileName: string): string => + path.basename(fileName).replace(/ts$/u, 'js'); + /** - * Vite plugin to ensure that the following are true: - * - Every bundle contains at most one import from a trusted prelude file. - * - The import statement, if it exists, is the first line of the bundled output. + * Check if the given code begins by importing the given trusted prelude. * - * @returns A vite plugin for automatically externalizing trusted preludes and checking they are imported first in the files that import them. + * @param code - The code to evaluate. + * @param preludeFileName - The file name of the trusted prelude. + * @returns True if the code begins by importing the trusted prelude file, false otherwise. */ -export function jsTrustedPrelude(): Plugin { - const trustedPreludeImporters = new Map(); - const isTrustedPrelude = (value: string): boolean => - value.match(/-trusted-prelude\./u) !== null; - const makeExpectedPrefix = (moduleId: string): RegExp => { - const preludeName = `${path.basename( - moduleId, - path.extname(moduleId), - )}-trusted-prelude.`; - const expectedPrefix = new RegExp( - `^import\\s*['"]\\./${preludeName}js['"];`, +const importsTrustedPreludeFirst = ( + code: string, + preludeFileName: string, +): boolean => + code.match( + new RegExp( + `^import\\s*['"]\\./${resolvePreludeFileName(preludeFileName)}['"];`, 'u', - ); - return expectedPrefix; + ), + ) !== null; + +/** + * A Vite plugin to ensure the following. + * - Every declared trusted prelude is handled externally (automatically merged into rollupOptions config). + * - Every declared trusted prelude importer: + * - Begins by importing its declared trusted header (prepended at bundle write time if missing). + * - Imports at most one declared trusted prelude (throws otherwise). + * - Is a declared entry point (throws otherwise). + * + * @param pluginConfig - The config options bag. + * @param pluginConfig.trustedPreludes - A mapping from the keys of rollupOptions.input to the file names of trusted preludes for the corresponding entry point. + * @returns The Vite plugin. + */ +export function jsTrustedPrelude(pluginConfig: { + trustedPreludes: { + [key: string]: string; }; +}): Plugin { + const { trustedPreludes } = pluginConfig; + + // Plugin state transferred between rollup stages. + let configError: ((context: PluginContext) => never) | undefined; + let isTrustedPrelude: (source: string) => boolean; + let isTrustingImporter: (importer: string) => boolean; + /** + * Given the name of a trusted prelude importer, return the resolved file name of its trusted prelude. + * + * @param context - The calling plugin context, whose context.error will be called if necessary. + * @param importer - The name of the trusted prelude importer. + * @throws If importer was not declared as a trusted prelude importer. + */ + let getTrustedPrelude: (context: PluginContext, importer: string) => string; + return { name: 'ocap-kernel:js-trusted-prelude', - resolveId: { - order: 'pre', + /** + * Append declared trusted preludes to rollup's 'external' declaration. + * + * @param _ - Unused. + * @param __ - Unused. + * @returns Changes to be deeply merged into the declared vite config file. + */ + // eslint-disable-next-line @typescript-eslint/naming-convention + config(_, __) { + return { + build: { + rollupOptions: { + external: Object.values(trustedPreludes), + }, + }, + }; + }, - /** - * Automatically externalize files with names `*-trusted-prelude.*`, and ensure no source imports more than one such file. - * - * @param source - The module which is doing the importing. - * @param importer - The module being imported. - * @returns A ResolveIdResult indicating how vite should resolve this source. - * @throws If a source attempts to import more than one trusted prelude. - */ - handler(source, importer) { - if (isTrustedPrelude(source) && importer !== undefined) { - // Check if this importer has already imported another trusted prelude. - if (trustedPreludeImporters.has(importer)) { - this.error( - `Module "${importer}" attempted to import trusted prelude "${source}" ` + - `but already imported trusted prelude "${trustedPreludeImporters.get( - importer, - )}".`, - ); - } - trustedPreludeImporters.set(importer, source); - // Tell vite to externalize this source. - this.info( - `Module "${source}" has been externalized because it was identified as a trusted prelude.`, + /** + * Whenever the config changes, update config dependent functions and collect configuration errors to be thrown at buildStart. + * + * @param viteConfig - The resolved vite config file after all plugins have had a change to modify it. + */ + configResolved(viteConfig) { + // Collect entry points. + const entryPoints = new Map( + Object.entries(viteConfig.build.rollupOptions.input ?? {}), + ); + + // Parse trusted prelude mappings. + const misconfiguredKeys: string[] = []; + const resolvedTrustingImporters = new Map(); + const resolvedTrustedPreludes = new Set(); + for (const [key, source] of Object.entries(trustedPreludes)) { + // If this trusting importer isn't declared an entry point, add it to misconfigured keys. + if (!entryPoints.has(key)) { + misconfiguredKeys.push(key); + continue; + } + const preludeOutputFileName = resolvePreludeFileName(source); + resolvedTrustingImporters.set(key, preludeOutputFileName); + resolvedTrustedPreludes.add(preludeOutputFileName); + } + + // Set trusted prelude functions for use in generateBundle phase. + isTrustedPrelude = (source: string) => + resolvedTrustedPreludes.has(resolvePreludeFileName(source)); + isTrustingImporter = (importer: string) => + resolvedTrustingImporters.has(importer); + getTrustedPrelude = (context: PluginContext, importer: string) => { + const trustedPrelude = resolvedTrustingImporters.get(importer); + // Ensure importer was declared and recognized as a trusting importer. + if (!trustedPrelude) { + // Shouldn't be possible without heavy interference from other plugins. + context.error( + `Module "${importer}" was identified as but not declared as a trusted prelude importer.`, ); - return { id: source, external: true }; } - return null; - }, + return trustedPrelude; + }; + + // If misconfigured, prepare error for buildStart phase. + configError = + misconfiguredKeys.length === 0 + ? undefined + : (context): never => { + const errorLine = `Configured trusted prelude importers ${JSON.stringify( + misconfiguredKeys, + )} must be declared entry points.`; + context.warn(errorLine); + context.warn( + `Declared entry points: ${JSON.stringify( + Array.from(entryPoints.keys()), + )}`, + ); + return context.error(errorLine); + }; }, - buildEnd: { + // Throw configuration errors at build start to utilize rollup's `this.error`. + buildStart(_) { + configError?.(this); + }, + + generateBundle: { order: 'post', /** - * Check that identified trusted preludes are their importers' first import in the output bundle. + * At write time, ensure the following are true. + * - Every js entry point contains at most one import from a trusted prelude file. + * - The trusted prelude import statement is the first line of the bundled output. * - * @param error - The error that caused the build to end, undefined if none (yet) occured. - * @throws If an identified trusted prelude importer does not import its trusted prelude at buildEnd. + * @param _ - The NormalizedOutputOptions. + * @param bundle - The OutputBundle being generated. + * @param isWrite - Whether bundle is being written. */ - handler(error): void { - if (error !== undefined) { + async handler(_, bundle, isWrite) { + if (!isWrite) { return; } - const trustedPreludes = Array.from(this.getModuleIds()).filter( - isTrustedPrelude, - ); - const importers = trustedPreludes.map((trustedPrelude) => - this.getModuleInfo(trustedPrelude)?.importers.at(0), - ); - importers.forEach((moduleId: string | undefined) => { - if (moduleId === undefined) { + + // The relevant properties of the OutputChunk type, declared here because it is not exposed by Vite. + type TrustingChunk = { + imports: [string, ...string[]]; + fileName: string; + code: string; + name: string; + isEntry: boolean; + }; + + // Collect chunks which import a trusted prelude. + const trustingChunks: TrustingChunk[] = Object.values(bundle).filter( + (output) => + output.type === 'chunk' && isTrustingImporter(output.name), + ) as unknown as TrustingChunk[]; + + // Validate trusted prelude assumptions for chunks that import them and prepend the import if necessary. + for (const chunk of trustingChunks) { + // Ensure trusted prelude importer was declared an entry point. + if (!chunk.isEntry) { + // Shouldn't be possible without interference from other plugins. this.warn( - `Module ${moduleId} was identified as a trusted prelude but no modules import it.`, + `Identified a trusting chunk ${chunk.name} which was not declared an entry point.`, ); - return; } - const code = this.getModuleInfo(moduleId)?.code; - if (!code) { - this.error( - `Module ${moduleId} was identified as a trusted prelude importer but has no code at buildEnd.`, + + // Check if this chunk has imported more than one trusted prelude. + const chunkTrustedPreludes = chunk.imports.filter(isTrustedPrelude); + if (chunkTrustedPreludes.length > 1) { + // This should only occur due to transitive imports. + const errorLine = `Module "${chunk.fileName}" attempted to import multiple trusted preludes (perhaps transitively), but no more than one is allowed.`; + this.warn(errorLine); + this.warn( + `Imported preludes: ${JSON.stringify(chunkTrustedPreludes)}`, ); + this.error(errorLine); } - const prefix = makeExpectedPrefix(moduleId); - if (code.match(prefix) === null) { - this.error( - `Module ${moduleId} was identified as a trusted prelude importer, ` + - `but does not begin with trusted prelude import.\n` + - `Expected prefix: ${prefix}\n` + - `Observed code: ${code.split(';').at(0)}`, + + // Add the trusted prelude import to the beginning of the file if it is missing. + const declaredTrustedPrelude = getTrustedPrelude(this, chunk.name); + if (!importsTrustedPreludeFirst(chunk.code, declaredTrustedPrelude)) { + this.warn( + `Module "${chunk.name}" was declared as a trusted prelude importer but its first import was not the declared trusted prelude.`, + ); + const prefix = `import"./${declaredTrustedPrelude}";`; + // TODO: Maybe remove redundant import statements from code body. + // - Note: Not functionally necessary due to idempotency of import statements. + // - Implementation tip: this.parse(chunk.code).body [...]; + chunk.code = prefix + chunk.code; + this.warn( + `Automatically prepended prefix '${prefix}' to code for module "${chunk.name}".`, ); } - }); + } }, }, }; diff --git a/packages/extension/vite.config.ts b/packages/extension/vite.config.ts index 91e8f2d81..df3ca71a2 100644 --- a/packages/extension/vite.config.ts +++ b/packages/extension/vite.config.ts @@ -10,6 +10,10 @@ import { jsTrustedPrelude } from './vite-plugins/js-trusted-prelude'; const projectRoot = './src'; +const jsTrustedPreludes = { + background: path.resolve(projectRoot, 'background-trusted-prelude.js'), +}; + /** * Files that need to be statically copied to the destination directory. * Paths are relative from the project root directory. @@ -20,7 +24,8 @@ const staticCopyTargets: readonly string[] = [ // External modules 'dev-console.js', '../../shims/dist/endoify.js', - 'background-trusted-prelude.js', + // Trusted preludes + ...new Set(Object.values(jsTrustedPreludes)), ]; // https://vitejs.dev/config/ @@ -50,6 +55,8 @@ export default defineConfig({ targets: staticCopyTargets.map((src) => ({ src, dest: './' })), watch: { reloadPageOnChange: true }, }), - jsTrustedPrelude(), + jsTrustedPrelude({ + trustedPreludes: jsTrustedPreludes, + }), ], }); From 25f6e5816b84684da9b4c4603d891ddc80c11acf Mon Sep 17 00:00:00 2001 From: grypez <143971198+grypez@users.noreply.github.com> Date: Mon, 9 Sep 2024 22:02:16 -0500 Subject: [PATCH 11/15] docs(extension): Annotate jsTrustedPrelude. --- .../vite-plugins/js-trusted-prelude.ts | 48 +++++++++++-------- 1 file changed, 28 insertions(+), 20 deletions(-) diff --git a/packages/extension/vite-plugins/js-trusted-prelude.ts b/packages/extension/vite-plugins/js-trusted-prelude.ts index 40d61dc7d..9cc459d21 100644 --- a/packages/extension/vite-plugins/js-trusted-prelude.ts +++ b/packages/extension/vite-plugins/js-trusted-prelude.ts @@ -1,5 +1,9 @@ import path from 'path'; -import type { Plugin } from 'vite'; +import type { Plugin, ResolvedConfig } from 'vite'; + +// This type is referenced in JSDoc strings in this file. +// eslint-disable-next-line @typescript-eslint/no-unused-vars +type RollupOptions = ResolvedConfig['build']['rollupOptions']; type PluginContext = { warn: (message: string) => void; @@ -35,14 +39,14 @@ const importsTrustedPreludeFirst = ( /** * A Vite plugin to ensure the following. - * - Every declared trusted prelude is handled externally (automatically merged into rollupOptions config). + * - Every declared trusted prelude is handled externally (automatically merged into {@link RollupOptions.external}). * - Every declared trusted prelude importer: - * - Begins by importing its declared trusted header (prepended at bundle write time if missing). - * - Imports at most one declared trusted prelude (throws otherwise). - * - Is a declared entry point (throws otherwise). + * - Begins by importing its declared trusted header (prepended during {@link Plugin.generateBundle} if missing). + * - Imports at most one declared trusted prelude (throws during {@link Plugin.generateBundle} otherwise). + * - Is a declared entry point (throws during {@link Plugin.buildStart} otherwise). * * @param pluginConfig - The config options bag. - * @param pluginConfig.trustedPreludes - A mapping from the keys of rollupOptions.input to the file names of trusted preludes for the corresponding entry point. + * @param pluginConfig.trustedPreludes - A mapping from the keys of {@link RollupOptions.input} to the file names of trusted preludes for the corresponding entry point. * @returns The Vite plugin. */ export function jsTrustedPrelude(pluginConfig: { @@ -59,7 +63,7 @@ export function jsTrustedPrelude(pluginConfig: { /** * Given the name of a trusted prelude importer, return the resolved file name of its trusted prelude. * - * @param context - The calling plugin context, whose context.error will be called if necessary. + * @param context - The calling plugin context which provides `.warn` and `.error` methods. * @param importer - The name of the trusted prelude importer. * @throws If importer was not declared as a trusted prelude importer. */ @@ -69,14 +73,11 @@ export function jsTrustedPrelude(pluginConfig: { name: 'ocap-kernel:js-trusted-prelude', /** - * Append declared trusted preludes to rollup's 'external' declaration. + * Append declared trusted preludes to the {@link RollupOptions.external} declaration. * - * @param _ - Unused. - * @param __ - Unused. * @returns Changes to be deeply merged into the declared vite config file. */ - // eslint-disable-next-line @typescript-eslint/naming-convention - config(_, __) { + config() { return { build: { rollupOptions: { @@ -87,11 +88,11 @@ export function jsTrustedPrelude(pluginConfig: { }, /** - * Whenever the config changes, update config dependent functions and collect configuration errors to be thrown at buildStart. + * Whenever the config changes, update config dependent functions and collect configuration errors to be thrown during {@link Plugin.buildStart}. * * @param viteConfig - The resolved vite config file after all plugins have had a change to modify it. */ - configResolved(viteConfig) { + configResolved(viteConfig: ResolvedConfig) { // Collect entry points. const entryPoints = new Map( Object.entries(viteConfig.build.rollupOptions.input ?? {}), @@ -147,21 +148,28 @@ export function jsTrustedPrelude(pluginConfig: { }; }, - // Throw configuration errors at build start to utilize rollup's `this.error`. - buildStart(_) { + /** + * Throw configuration errors if there were any. + * Wait until buildStart to throw configuration errors to utilize {@link PluginContext}'s `warn` and `error`. + * + * @throws If a declared trusted prelude importer was not a declared entry point. + */ + buildStart() { configError?.(this); }, generateBundle: { order: 'post', /** - * At write time, ensure the following are true. - * - Every js entry point contains at most one import from a trusted prelude file. - * - The trusted prelude import statement is the first line of the bundled output. + * At write time, ensure the following. + * Every declared trusted prelude importer: + * - Imports at most one declared trusted prelude (throws otherwise). + * - Begins by importing its declared trusted header (prepended if missing). * - * @param _ - The NormalizedOutputOptions. + * @param _ - Unused. * @param bundle - The OutputBundle being generated. * @param isWrite - Whether bundle is being written. + * @throws If a declared trusted prelude importer imports more than one declared trusted prelude. */ async handler(_, bundle, isWrite) { if (!isWrite) { From aae768aac8b034830a62585dc442608d8ad869df Mon Sep 17 00:00:00 2001 From: grypez <143971198+grypez@users.noreply.github.com> Date: Mon, 9 Sep 2024 22:14:16 -0500 Subject: [PATCH 12/15] docs(extension): Permute plugin doc to align temporally. --- packages/extension/vite-plugins/js-trusted-prelude.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/extension/vite-plugins/js-trusted-prelude.ts b/packages/extension/vite-plugins/js-trusted-prelude.ts index 9cc459d21..099771269 100644 --- a/packages/extension/vite-plugins/js-trusted-prelude.ts +++ b/packages/extension/vite-plugins/js-trusted-prelude.ts @@ -41,9 +41,9 @@ const importsTrustedPreludeFirst = ( * A Vite plugin to ensure the following. * - Every declared trusted prelude is handled externally (automatically merged into {@link RollupOptions.external}). * - Every declared trusted prelude importer: - * - Begins by importing its declared trusted header (prepended during {@link Plugin.generateBundle} if missing). - * - Imports at most one declared trusted prelude (throws during {@link Plugin.generateBundle} otherwise). * - Is a declared entry point (throws during {@link Plugin.buildStart} otherwise). + * - Imports at most one declared trusted prelude (throws during {@link Plugin.generateBundle} otherwise). + * - Begins by importing its declared trusted header (prepended during {@link Plugin.generateBundle} if missing). * * @param pluginConfig - The config options bag. * @param pluginConfig.trustedPreludes - A mapping from the keys of {@link RollupOptions.input} to the file names of trusted preludes for the corresponding entry point. From a939b925a8930fa2e888cdfb199f7d60496b7245 Mon Sep 17 00:00:00 2001 From: grypez <143971198+grypez@users.noreply.github.com> Date: Tue, 10 Sep 2024 12:35:53 -0500 Subject: [PATCH 13/15] docs(extension): s/header/prelude/g --- packages/extension/vite-plugins/js-trusted-prelude.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/extension/vite-plugins/js-trusted-prelude.ts b/packages/extension/vite-plugins/js-trusted-prelude.ts index 099771269..cf30ceca3 100644 --- a/packages/extension/vite-plugins/js-trusted-prelude.ts +++ b/packages/extension/vite-plugins/js-trusted-prelude.ts @@ -43,7 +43,7 @@ const importsTrustedPreludeFirst = ( * - Every declared trusted prelude importer: * - Is a declared entry point (throws during {@link Plugin.buildStart} otherwise). * - Imports at most one declared trusted prelude (throws during {@link Plugin.generateBundle} otherwise). - * - Begins by importing its declared trusted header (prepended during {@link Plugin.generateBundle} if missing). + * - Begins by importing its declared trusted prelude (prepended during {@link Plugin.generateBundle} if missing). * * @param pluginConfig - The config options bag. * @param pluginConfig.trustedPreludes - A mapping from the keys of {@link RollupOptions.input} to the file names of trusted preludes for the corresponding entry point. @@ -164,7 +164,7 @@ export function jsTrustedPrelude(pluginConfig: { * At write time, ensure the following. * Every declared trusted prelude importer: * - Imports at most one declared trusted prelude (throws otherwise). - * - Begins by importing its declared trusted header (prepended if missing). + * - Begins by importing its declared trusted prelude (prepended if missing). * * @param _ - Unused. * @param bundle - The OutputBundle being generated. From 92ce904c1830821846bb0e57a059a6af0e1992b6 Mon Sep 17 00:00:00 2001 From: grypez <143971198+grypez@users.noreply.github.com> Date: Tue, 10 Sep 2024 12:38:03 -0500 Subject: [PATCH 14/15] docs(extension): Resolve TODO comment. --- packages/extension/vite-plugins/js-trusted-prelude.ts | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/packages/extension/vite-plugins/js-trusted-prelude.ts b/packages/extension/vite-plugins/js-trusted-prelude.ts index cf30ceca3..29f65e33d 100644 --- a/packages/extension/vite-plugins/js-trusted-prelude.ts +++ b/packages/extension/vite-plugins/js-trusted-prelude.ts @@ -220,9 +220,8 @@ export function jsTrustedPrelude(pluginConfig: { `Module "${chunk.name}" was declared as a trusted prelude importer but its first import was not the declared trusted prelude.`, ); const prefix = `import"./${declaredTrustedPrelude}";`; - // TODO: Maybe remove redundant import statements from code body. - // - Note: Not functionally necessary due to idempotency of import statements. - // - Implementation tip: this.parse(chunk.code).body [...]; + // Due to idempotency of ESM import statements, it is not necessary to remove duplicate imports. + // It is only necessary to ensure the trusted prelude import is the first. chunk.code = prefix + chunk.code; this.warn( `Automatically prepended prefix '${prefix}' to code for module "${chunk.name}".`, From 6808a589af4966d2a175f8db1b058a91043b47e9 Mon Sep 17 00:00:00 2001 From: grypez <143971198+grypez@users.noreply.github.com> Date: Tue, 10 Sep 2024 18:10:29 -0500 Subject: [PATCH 15/15] docs(extension): Clarify some plugin internal names. --- .../vite-plugins/js-trusted-prelude.ts | 29 +++++++++---------- 1 file changed, 14 insertions(+), 15 deletions(-) diff --git a/packages/extension/vite-plugins/js-trusted-prelude.ts b/packages/extension/vite-plugins/js-trusted-prelude.ts index 29f65e33d..6cabb4b02 100644 --- a/packages/extension/vite-plugins/js-trusted-prelude.ts +++ b/packages/extension/vite-plugins/js-trusted-prelude.ts @@ -67,7 +67,10 @@ export function jsTrustedPrelude(pluginConfig: { * @param importer - The name of the trusted prelude importer. * @throws If importer was not declared as a trusted prelude importer. */ - let getTrustedPrelude: (context: PluginContext, importer: string) => string; + let getTrustedPreludeFileName: ( + context: PluginContext, + importer: string, + ) => string; return { name: 'ocap-kernel:js-trusted-prelude', @@ -118,17 +121,13 @@ export function jsTrustedPrelude(pluginConfig: { resolvedTrustedPreludes.has(resolvePreludeFileName(source)); isTrustingImporter = (importer: string) => resolvedTrustingImporters.has(importer); - getTrustedPrelude = (context: PluginContext, importer: string) => { - const trustedPrelude = resolvedTrustingImporters.get(importer); + getTrustedPreludeFileName = (context: PluginContext, importer: string) => // Ensure importer was declared and recognized as a trusting importer. - if (!trustedPrelude) { + resolvedTrustingImporters.get(importer) ?? + context.error( // Shouldn't be possible without heavy interference from other plugins. - context.error( - `Module "${importer}" was identified as but not declared as a trusted prelude importer.`, - ); - } - return trustedPrelude; - }; + `Module "${importer}" was identified as but not declared as a trusted prelude importer.`, + ); // If misconfigured, prepare error for buildStart phase. configError = @@ -214,17 +213,17 @@ export function jsTrustedPrelude(pluginConfig: { } // Add the trusted prelude import to the beginning of the file if it is missing. - const declaredTrustedPrelude = getTrustedPrelude(this, chunk.name); - if (!importsTrustedPreludeFirst(chunk.code, declaredTrustedPrelude)) { + const declaredPrelude = getTrustedPreludeFileName(this, chunk.name); + if (!importsTrustedPreludeFirst(chunk.code, declaredPrelude)) { this.warn( `Module "${chunk.name}" was declared as a trusted prelude importer but its first import was not the declared trusted prelude.`, ); - const prefix = `import"./${declaredTrustedPrelude}";`; + const trustedPreludeImportStatement = `import"./${declaredPrelude}";`; // Due to idempotency of ESM import statements, it is not necessary to remove duplicate imports. // It is only necessary to ensure the trusted prelude import is the first. - chunk.code = prefix + chunk.code; + chunk.code = trustedPreludeImportStatement + chunk.code; this.warn( - `Automatically prepended prefix '${prefix}' to code for module "${chunk.name}".`, + `Automatically prepended prefix "${trustedPreludeImportStatement}" to code for module "${chunk.name}".`, ); } }