From d3bf48c0ef9a4b5fe922ab5043b472c8ca7594fd Mon Sep 17 00:00:00 2001 From: grypez <143971198+grypez@users.noreply.github.com> Date: Wed, 21 Jan 2026 19:31:09 -0600 Subject: [PATCH 01/10] build: Support bundling with vite --- packages/cli/package.json | 5 +- packages/cli/src/app.ts | 3 +- packages/cli/src/commands/bundle.ts | 20 +- packages/cli/src/commands/watch.ts | 5 +- .../cli/src/vite/export-metadata-plugin.ts | 48 ++++ packages/cli/src/vite/vat-bundler.ts | 63 +++++ packages/kernel-test/package.json | 1 + packages/kernel-test/src/supervisor.test.ts | 2 +- .../ocap-kernel/src/vats/VatSupervisor.ts | 6 +- .../ocap-kernel/src/vats/bundle-loader.ts | 72 ++++++ yarn.lock | 226 +++++++++++------- 11 files changed, 345 insertions(+), 106 deletions(-) create mode 100644 packages/cli/src/vite/export-metadata-plugin.ts create mode 100644 packages/cli/src/vite/vat-bundler.ts create mode 100644 packages/ocap-kernel/src/vats/bundle-loader.ts diff --git a/packages/cli/package.json b/packages/cli/package.json index 93ba4196a..54361c106 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -36,8 +36,6 @@ "dependencies": { "@chainsafe/libp2p-noise": "^16.1.3", "@chainsafe/libp2p-yamux": "patch:@chainsafe/libp2p-yamux@npm%3A7.0.4#~/.yarn/patches/@chainsafe-libp2p-yamux-npm-7.0.4-284c2f6812.patch", - "@endo/bundle-source": "^4.1.2", - "@endo/init": "^1.1.12", "@endo/promise-kit": "^1.1.13", "@libp2p/autonat": "2.0.38", "@libp2p/circuit-relay-v2": "3.2.24", @@ -57,6 +55,7 @@ "glob": "^11.0.0", "libp2p": "2.10.0", "serve-handler": "^6.1.6", + "vite": "^7.3.0", "yargs": "^17.7.2" }, "devDependencies": { @@ -86,12 +85,12 @@ "jsdom": "^27.4.0", "prettier": "^3.5.3", "rimraf": "^6.0.1", + "rollup": "^4.55.3", "ses": "^1.14.0", "turbo": "^2.5.6", "typedoc": "^0.28.1", "typescript": "~5.8.2", "typescript-eslint": "^8.29.0", - "vite": "^7.3.0", "vitest": "^4.0.16" }, "engines": { diff --git a/packages/cli/src/app.ts b/packages/cli/src/app.ts index 121e86a9c..0ce8b70ec 100755 --- a/packages/cli/src/app.ts +++ b/packages/cli/src/app.ts @@ -1,5 +1,4 @@ -import '@endo/init'; - +import '@metamask/kernel-shims/endoify'; import { Logger } from '@metamask/logger'; import path from 'node:path'; import yargs from 'yargs'; diff --git a/packages/cli/src/commands/bundle.ts b/packages/cli/src/commands/bundle.ts index 8c4813870..d073ba7e1 100644 --- a/packages/cli/src/commands/bundle.ts +++ b/packages/cli/src/commands/bundle.ts @@ -1,15 +1,21 @@ -import '@endo/init'; -import endoBundleSource from '@endo/bundle-source'; -import { Logger } from '@metamask/logger'; import { glob } from 'glob'; import { writeFile } from 'node:fs/promises'; import { resolve, join } from 'node:path'; import { isDirectory } from '../file.ts'; import { resolveBundlePath } from '../path.ts'; +import { bundleVat } from '../vite/vat-bundler.ts'; + +/** + * Minimal logger interface for bundle operations. + */ +type BundleLogger = { + info: (message: string, ...args: unknown[]) => void; + error?: (message: string, ...args: unknown[]) => void; +}; type BundleFileOptions = { - logger: Logger; + logger: BundleLogger; targetPath?: string; }; @@ -30,7 +36,7 @@ export async function bundleFile( const { logger, targetPath } = options; const sourceFullPath = resolve(sourcePath); const bundlePath = targetPath ?? resolveBundlePath(sourceFullPath); - const bundle = await endoBundleSource(sourceFullPath); + const bundle = await bundleVat(sourceFullPath); const bundleContent = JSON.stringify(bundle); await writeFile(bundlePath, bundleContent); logger.info(`Wrote ${bundlePath}: ${new Blob([bundleContent]).size} bytes`); @@ -46,7 +52,7 @@ export async function bundleFile( */ export async function bundleDir( sourceDir: string, - options: { logger: Logger }, + options: { logger: BundleLogger }, ): Promise { const { logger } = options; logger.info('Bundling directory:', sourceDir); @@ -67,7 +73,7 @@ export async function bundleDir( */ export async function bundleSource( target: string, - logger: Logger, + logger: BundleLogger, ): Promise { const targetIsDirectory = await isDirectory(target); await (targetIsDirectory ? bundleDir : bundleFile)(target, { logger }); diff --git a/packages/cli/src/commands/watch.ts b/packages/cli/src/commands/watch.ts index 71dd6cde7..ca13361c8 100644 --- a/packages/cli/src/commands/watch.ts +++ b/packages/cli/src/commands/watch.ts @@ -1,4 +1,5 @@ import { makePromiseKit } from '@endo/promise-kit'; +import type { PromiseKit } from '@endo/promise-kit'; import { Logger } from '@metamask/logger'; import { watch } from 'chokidar'; import type { FSWatcher, MatchFunction } from 'chokidar'; @@ -17,8 +18,8 @@ type WatchDirReturn = { export const makeWatchEvents = ( watcher: FSWatcher, - readyResolve: ReturnType>['resolve'], - throwError: ReturnType>['reject'], + readyResolve: PromiseKit['resolve'], + throwError: PromiseKit['reject'], logger: Logger, ): { ready: () => void; diff --git a/packages/cli/src/vite/export-metadata-plugin.ts b/packages/cli/src/vite/export-metadata-plugin.ts new file mode 100644 index 000000000..c28b6f279 --- /dev/null +++ b/packages/cli/src/vite/export-metadata-plugin.ts @@ -0,0 +1,48 @@ +import type { Plugin, RenderedModule } from 'rollup'; + +export type BundleMetadata = { + exports: string[]; + modules: Record< + string, + { renderedExports: string[]; removedExports: string[] } + >; +}; + +/** + * Rollup plugin that captures export metadata from the bundle. + * + * Uses the `generateBundle` hook to extract the exports array and + * per-module metadata (renderedExports and removedExports) from the + * entry chunk. + * + * @returns A plugin with an additional `getMetadata()` method. + */ +export function exportMetadataPlugin(): Plugin & { + getMetadata: () => BundleMetadata; +} { + const metadata: BundleMetadata = { exports: [], modules: {} }; + + return { + name: 'export-metadata', + generateBundle(_, bundle) { + for (const chunk of Object.values(bundle)) { + if (chunk.type === 'chunk' && chunk.isEntry) { + const outputChunk = chunk; + metadata.exports = outputChunk.exports; + metadata.modules = Object.fromEntries( + Object.entries(outputChunk.modules).map( + ([id, info]: [string, RenderedModule]) => [ + id, + { + renderedExports: info.renderedExports, + removedExports: info.removedExports, + }, + ], + ), + ); + } + } + }, + getMetadata: () => metadata, + }; +} diff --git a/packages/cli/src/vite/vat-bundler.ts b/packages/cli/src/vite/vat-bundler.ts new file mode 100644 index 000000000..581aa99b6 --- /dev/null +++ b/packages/cli/src/vite/vat-bundler.ts @@ -0,0 +1,63 @@ +import { build } from 'vite'; +import type { Rollup, PluginOption } from 'vite'; + +import { exportMetadataPlugin } from './export-metadata-plugin.ts'; +import type { BundleMetadata } from './export-metadata-plugin.ts'; + +export type VatBundle = BundleMetadata & { + moduleFormat: 'vite-iife'; + code: string; +}; + +/** + * Bundle a vat source file using vite. + * + * Produces an IIFE bundle that assigns exports to a `__vatExports__` global, + * along with metadata about the bundle's exports and modules. + * + * @param sourcePath - Absolute path to the vat entry point. + * @returns The bundle object containing code and metadata. + */ +export async function bundleVat(sourcePath: string): Promise { + const metadataPlugin = exportMetadataPlugin(); + + const result = await build({ + configFile: false, + logLevel: 'silent', + build: { + write: false, + lib: { + entry: sourcePath, + formats: ['iife'], + name: '__vatExports__', + }, + rollupOptions: { + output: { + exports: 'named', + inlineDynamicImports: true, + }, + plugins: [metadataPlugin as unknown as PluginOption], + }, + minify: false, + }, + }); + + const output = Array.isArray(result) ? result[0] : result; + const chunk = (output as Rollup.RollupOutput).output.find( + (item): item is Rollup.OutputChunk => item.type === 'chunk' && item.isEntry, + ); + + if (!chunk) { + throw new Error(`Failed to produce bundle for ${sourcePath}`); + } + + // SES rejects code containing `import(` patterns, even in comments. + // Replace them with a safe alternative that won't trigger detection. + const sanitizedCode = chunk.code.replace(/\bimport\s*\(/gu, 'IMPORT('); + + return { + moduleFormat: 'vite-iife', + code: sanitizedCode, + ...metadataPlugin.getMetadata(), + }; +} diff --git a/packages/kernel-test/package.json b/packages/kernel-test/package.json index c4acfa7e2..d8f387b27 100644 --- a/packages/kernel-test/package.json +++ b/packages/kernel-test/package.json @@ -58,6 +58,7 @@ "@metamask/kernel-utils": "workspace:^", "@metamask/logger": "workspace:^", "@metamask/ocap-kernel": "workspace:^", + "@ocap/kernel-agents": "workspace:^", "@ocap/kernel-language-model-service": "workspace:^", "@ocap/nodejs": "workspace:^", "@ocap/nodejs-test-workers": "workspace:^", diff --git a/packages/kernel-test/src/supervisor.test.ts b/packages/kernel-test/src/supervisor.test.ts index a6b18fa00..cd9a08cdb 100644 --- a/packages/kernel-test/src/supervisor.test.ts +++ b/packages/kernel-test/src/supervisor.test.ts @@ -38,7 +38,7 @@ const makeVatSupervisor = async ({ const bundleContent = await readFile(bundlePath, 'utf-8'); return { ok: true, - json: async () => JSON.parse(bundleContent), + text: async () => bundleContent, // eslint-disable-next-line n/no-unsupported-features/node-builtins } as Response; }, diff --git a/packages/ocap-kernel/src/vats/VatSupervisor.ts b/packages/ocap-kernel/src/vats/VatSupervisor.ts index e0adbdf13..90aa39b20 100644 --- a/packages/ocap-kernel/src/vats/VatSupervisor.ts +++ b/packages/ocap-kernel/src/vats/VatSupervisor.ts @@ -4,7 +4,6 @@ import type { VatSyscallObject, VatSyscallResult, } from '@agoric/swingset-liveslots'; -import { importBundle } from '@endo/import-bundle'; import { makeMarshal } from '@endo/marshal'; import type { CapData } from '@endo/marshal'; import { @@ -24,6 +23,7 @@ import type { DuplexStream } from '@metamask/streams'; import { isJsonRpcRequest, isJsonRpcResponse } from '@metamask/utils'; import type { PlatformFactory } from '@ocap/kernel-platforms'; +import { loadBundle } from './bundle-loader.ts'; import { makeGCAndFinalize } from '../garbage-collection/gc-finalize.ts'; import { makeDummyMeterControl } from '../liveslots/meter-control.ts'; import { makeSupervisorSyscall } from '../liveslots/syscall.ts'; @@ -308,7 +308,7 @@ export class VatSupervisor { if (!fetched.ok) { throw Error(`fetch of user code ${bundleSpec} failed: ${fetched.status}`); } - const bundle = await fetched.json(); + const bundleContent = await fetched.text(); const buildVatNamespace = async ( lsEndowments: Record, inescapableGlobalProperties: object, @@ -333,7 +333,7 @@ export class VatSupervisor { // Otherwise, just rethrow the error. throw error; } - const vatNS = await importBundle(bundle, { + const vatNS = await loadBundle(bundleContent, { filePrefix: `vat-${this.id}/...`, endowments, inescapableGlobalProperties, diff --git a/packages/ocap-kernel/src/vats/bundle-loader.ts b/packages/ocap-kernel/src/vats/bundle-loader.ts new file mode 100644 index 000000000..f0cfce8bb --- /dev/null +++ b/packages/ocap-kernel/src/vats/bundle-loader.ts @@ -0,0 +1,72 @@ +import { importBundle } from '@endo/import-bundle'; + +type EndoBundle = { + moduleFormat: 'endoZipBase64'; + endoZipBase64: string; + endoZipBase64Sha512: string; +}; + +type ViteBundle = { + moduleFormat: 'vite-iife'; + code: string; + exports: string[]; + modules: Record< + string, + { renderedExports: string[]; removedExports: string[] } + >; +}; + +type Bundle = EndoBundle | ViteBundle; + +export type LoadBundleOptions = { + filePrefix?: string; + endowments?: object; + inescapableGlobalProperties?: object; +}; + +/** + * Load a bundle and return its namespace. + * + * Supports two bundle formats: + * - `endoZipBase64`: Legacy format using `importBundle()` + * - `vite-iife`: New format using `Compartment.evaluate()` + * + * @param content - The bundle content as a JSON string. + * @param options - Options for loading the bundle. + * @returns The namespace exported by the bundle. + */ +export async function loadBundle( + content: string, + options: LoadBundleOptions = {}, +): Promise> { + const parsed = JSON.parse(content) as Bundle; + const { endowments = {}, inescapableGlobalProperties = {} } = options; + + if (parsed.moduleFormat === 'endoZipBase64') { + return await importBundle(parsed, { + filePrefix: options.filePrefix, + endowments, + inescapableGlobalProperties, + }); + } + + if (parsed.moduleFormat === 'vite-iife') { + const compartment = new Compartment({ + // SES globals that may be used by bundled code + harden: globalThis.harden, + assert: globalThis.assert, + ...endowments, + ...inescapableGlobalProperties, + }); + // The code declares `var __vatExports__ = (function(){...})({});` + // We wrap it in an IIFE to capture and return the result. + const vatExports = compartment.evaluate( + `(function() { ${parsed.code}; return __vatExports__; })()`, + ); + return vatExports as Record; + } + + throw new Error( + `Unknown bundle format: ${(parsed as { moduleFormat: string }).moduleFormat}`, + ); +} diff --git a/yarn.lock b/yarn.lock index 3e8e2e1f9..90d506f3e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3355,8 +3355,6 @@ __metadata: "@arethetypeswrong/cli": "npm:^0.17.4" "@chainsafe/libp2p-noise": "npm:^16.1.3" "@chainsafe/libp2p-yamux": "patch:@chainsafe/libp2p-yamux@npm%3A7.0.4#~/.yarn/patches/@chainsafe-libp2p-yamux-npm-7.0.4-284c2f6812.patch" - "@endo/bundle-source": "npm:^4.1.2" - "@endo/init": "npm:^1.1.12" "@endo/promise-kit": "npm:^1.1.13" "@libp2p/autonat": "npm:2.0.38" "@libp2p/circuit-relay-v2": "npm:3.2.24" @@ -3400,6 +3398,7 @@ __metadata: libp2p: "npm:2.10.0" prettier: "npm:^3.5.3" rimraf: "npm:^6.0.1" + rollup: "npm:^4.55.3" serve-handler: "npm:^6.1.6" ses: "npm:^1.14.0" turbo: "npm:^2.5.6" @@ -3738,6 +3737,7 @@ __metadata: "@metamask/logger": "workspace:^" "@metamask/ocap-kernel": "workspace:^" "@ocap/cli": "workspace:^" + "@ocap/kernel-agents": "workspace:^" "@ocap/kernel-language-model-service": "workspace:^" "@ocap/nodejs": "workspace:^" "@ocap/nodejs-test-workers": "workspace:^" @@ -4619,142 +4619,177 @@ __metadata: languageName: node linkType: hard -"@rollup/rollup-android-arm-eabi@npm:4.46.3": - version: 4.46.3 - resolution: "@rollup/rollup-android-arm-eabi@npm:4.46.3" +"@rollup/rollup-android-arm-eabi@npm:4.55.3": + version: 4.55.3 + resolution: "@rollup/rollup-android-arm-eabi@npm:4.55.3" conditions: os=android & cpu=arm languageName: node linkType: hard -"@rollup/rollup-android-arm64@npm:4.46.3": - version: 4.46.3 - resolution: "@rollup/rollup-android-arm64@npm:4.46.3" +"@rollup/rollup-android-arm64@npm:4.55.3": + version: 4.55.3 + resolution: "@rollup/rollup-android-arm64@npm:4.55.3" conditions: os=android & cpu=arm64 languageName: node linkType: hard -"@rollup/rollup-darwin-arm64@npm:4.46.3": - version: 4.46.3 - resolution: "@rollup/rollup-darwin-arm64@npm:4.46.3" +"@rollup/rollup-darwin-arm64@npm:4.55.3": + version: 4.55.3 + resolution: "@rollup/rollup-darwin-arm64@npm:4.55.3" conditions: os=darwin & cpu=arm64 languageName: node linkType: hard -"@rollup/rollup-darwin-x64@npm:4.46.3": - version: 4.46.3 - resolution: "@rollup/rollup-darwin-x64@npm:4.46.3" +"@rollup/rollup-darwin-x64@npm:4.55.3": + version: 4.55.3 + resolution: "@rollup/rollup-darwin-x64@npm:4.55.3" conditions: os=darwin & cpu=x64 languageName: node linkType: hard -"@rollup/rollup-freebsd-arm64@npm:4.46.3": - version: 4.46.3 - resolution: "@rollup/rollup-freebsd-arm64@npm:4.46.3" +"@rollup/rollup-freebsd-arm64@npm:4.55.3": + version: 4.55.3 + resolution: "@rollup/rollup-freebsd-arm64@npm:4.55.3" conditions: os=freebsd & cpu=arm64 languageName: node linkType: hard -"@rollup/rollup-freebsd-x64@npm:4.46.3": - version: 4.46.3 - resolution: "@rollup/rollup-freebsd-x64@npm:4.46.3" +"@rollup/rollup-freebsd-x64@npm:4.55.3": + version: 4.55.3 + resolution: "@rollup/rollup-freebsd-x64@npm:4.55.3" conditions: os=freebsd & cpu=x64 languageName: node linkType: hard -"@rollup/rollup-linux-arm-gnueabihf@npm:4.46.3": - version: 4.46.3 - resolution: "@rollup/rollup-linux-arm-gnueabihf@npm:4.46.3" +"@rollup/rollup-linux-arm-gnueabihf@npm:4.55.3": + version: 4.55.3 + resolution: "@rollup/rollup-linux-arm-gnueabihf@npm:4.55.3" conditions: os=linux & cpu=arm & libc=glibc languageName: node linkType: hard -"@rollup/rollup-linux-arm-musleabihf@npm:4.46.3": - version: 4.46.3 - resolution: "@rollup/rollup-linux-arm-musleabihf@npm:4.46.3" +"@rollup/rollup-linux-arm-musleabihf@npm:4.55.3": + version: 4.55.3 + resolution: "@rollup/rollup-linux-arm-musleabihf@npm:4.55.3" conditions: os=linux & cpu=arm & libc=musl languageName: node linkType: hard -"@rollup/rollup-linux-arm64-gnu@npm:4.46.3": - version: 4.46.3 - resolution: "@rollup/rollup-linux-arm64-gnu@npm:4.46.3" +"@rollup/rollup-linux-arm64-gnu@npm:4.55.3": + version: 4.55.3 + resolution: "@rollup/rollup-linux-arm64-gnu@npm:4.55.3" conditions: os=linux & cpu=arm64 & libc=glibc languageName: node linkType: hard -"@rollup/rollup-linux-arm64-musl@npm:4.46.3": - version: 4.46.3 - resolution: "@rollup/rollup-linux-arm64-musl@npm:4.46.3" +"@rollup/rollup-linux-arm64-musl@npm:4.55.3": + version: 4.55.3 + resolution: "@rollup/rollup-linux-arm64-musl@npm:4.55.3" conditions: os=linux & cpu=arm64 & libc=musl languageName: node linkType: hard -"@rollup/rollup-linux-loongarch64-gnu@npm:4.46.3": - version: 4.46.3 - resolution: "@rollup/rollup-linux-loongarch64-gnu@npm:4.46.3" +"@rollup/rollup-linux-loong64-gnu@npm:4.55.3": + version: 4.55.3 + resolution: "@rollup/rollup-linux-loong64-gnu@npm:4.55.3" conditions: os=linux & cpu=loong64 & libc=glibc languageName: node linkType: hard -"@rollup/rollup-linux-ppc64-gnu@npm:4.46.3": - version: 4.46.3 - resolution: "@rollup/rollup-linux-ppc64-gnu@npm:4.46.3" +"@rollup/rollup-linux-loong64-musl@npm:4.55.3": + version: 4.55.3 + resolution: "@rollup/rollup-linux-loong64-musl@npm:4.55.3" + conditions: os=linux & cpu=loong64 & libc=musl + languageName: node + linkType: hard + +"@rollup/rollup-linux-ppc64-gnu@npm:4.55.3": + version: 4.55.3 + resolution: "@rollup/rollup-linux-ppc64-gnu@npm:4.55.3" conditions: os=linux & cpu=ppc64 & libc=glibc languageName: node linkType: hard -"@rollup/rollup-linux-riscv64-gnu@npm:4.46.3": - version: 4.46.3 - resolution: "@rollup/rollup-linux-riscv64-gnu@npm:4.46.3" +"@rollup/rollup-linux-ppc64-musl@npm:4.55.3": + version: 4.55.3 + resolution: "@rollup/rollup-linux-ppc64-musl@npm:4.55.3" + conditions: os=linux & cpu=ppc64 & libc=musl + languageName: node + linkType: hard + +"@rollup/rollup-linux-riscv64-gnu@npm:4.55.3": + version: 4.55.3 + resolution: "@rollup/rollup-linux-riscv64-gnu@npm:4.55.3" conditions: os=linux & cpu=riscv64 & libc=glibc languageName: node linkType: hard -"@rollup/rollup-linux-riscv64-musl@npm:4.46.3": - version: 4.46.3 - resolution: "@rollup/rollup-linux-riscv64-musl@npm:4.46.3" +"@rollup/rollup-linux-riscv64-musl@npm:4.55.3": + version: 4.55.3 + resolution: "@rollup/rollup-linux-riscv64-musl@npm:4.55.3" conditions: os=linux & cpu=riscv64 & libc=musl languageName: node linkType: hard -"@rollup/rollup-linux-s390x-gnu@npm:4.46.3": - version: 4.46.3 - resolution: "@rollup/rollup-linux-s390x-gnu@npm:4.46.3" +"@rollup/rollup-linux-s390x-gnu@npm:4.55.3": + version: 4.55.3 + resolution: "@rollup/rollup-linux-s390x-gnu@npm:4.55.3" conditions: os=linux & cpu=s390x & libc=glibc languageName: node linkType: hard -"@rollup/rollup-linux-x64-gnu@npm:4.46.3": - version: 4.46.3 - resolution: "@rollup/rollup-linux-x64-gnu@npm:4.46.3" +"@rollup/rollup-linux-x64-gnu@npm:4.55.3": + version: 4.55.3 + resolution: "@rollup/rollup-linux-x64-gnu@npm:4.55.3" conditions: os=linux & cpu=x64 & libc=glibc languageName: node linkType: hard -"@rollup/rollup-linux-x64-musl@npm:4.46.3": - version: 4.46.3 - resolution: "@rollup/rollup-linux-x64-musl@npm:4.46.3" +"@rollup/rollup-linux-x64-musl@npm:4.55.3": + version: 4.55.3 + resolution: "@rollup/rollup-linux-x64-musl@npm:4.55.3" conditions: os=linux & cpu=x64 & libc=musl languageName: node linkType: hard -"@rollup/rollup-win32-arm64-msvc@npm:4.46.3": - version: 4.46.3 - resolution: "@rollup/rollup-win32-arm64-msvc@npm:4.46.3" +"@rollup/rollup-openbsd-x64@npm:4.55.3": + version: 4.55.3 + resolution: "@rollup/rollup-openbsd-x64@npm:4.55.3" + conditions: os=openbsd & cpu=x64 + languageName: node + linkType: hard + +"@rollup/rollup-openharmony-arm64@npm:4.55.3": + version: 4.55.3 + resolution: "@rollup/rollup-openharmony-arm64@npm:4.55.3" + conditions: os=openharmony & cpu=arm64 + languageName: node + linkType: hard + +"@rollup/rollup-win32-arm64-msvc@npm:4.55.3": + version: 4.55.3 + resolution: "@rollup/rollup-win32-arm64-msvc@npm:4.55.3" conditions: os=win32 & cpu=arm64 languageName: node linkType: hard -"@rollup/rollup-win32-ia32-msvc@npm:4.46.3": - version: 4.46.3 - resolution: "@rollup/rollup-win32-ia32-msvc@npm:4.46.3" +"@rollup/rollup-win32-ia32-msvc@npm:4.55.3": + version: 4.55.3 + resolution: "@rollup/rollup-win32-ia32-msvc@npm:4.55.3" conditions: os=win32 & cpu=ia32 languageName: node linkType: hard -"@rollup/rollup-win32-x64-msvc@npm:4.46.3": - version: 4.46.3 - resolution: "@rollup/rollup-win32-x64-msvc@npm:4.46.3" +"@rollup/rollup-win32-x64-gnu@npm:4.55.3": + version: 4.55.3 + resolution: "@rollup/rollup-win32-x64-gnu@npm:4.55.3" + conditions: os=win32 & cpu=x64 + languageName: node + linkType: hard + +"@rollup/rollup-win32-x64-msvc@npm:4.55.3": + version: 4.55.3 + resolution: "@rollup/rollup-win32-x64-msvc@npm:4.55.3" conditions: os=win32 & cpu=x64 languageName: node linkType: hard @@ -13179,30 +13214,35 @@ __metadata: languageName: node linkType: hard -"rollup@npm:^4.43.0": - version: 4.46.3 - resolution: "rollup@npm:4.46.3" - dependencies: - "@rollup/rollup-android-arm-eabi": "npm:4.46.3" - "@rollup/rollup-android-arm64": "npm:4.46.3" - "@rollup/rollup-darwin-arm64": "npm:4.46.3" - "@rollup/rollup-darwin-x64": "npm:4.46.3" - "@rollup/rollup-freebsd-arm64": "npm:4.46.3" - "@rollup/rollup-freebsd-x64": "npm:4.46.3" - "@rollup/rollup-linux-arm-gnueabihf": "npm:4.46.3" - "@rollup/rollup-linux-arm-musleabihf": "npm:4.46.3" - "@rollup/rollup-linux-arm64-gnu": "npm:4.46.3" - "@rollup/rollup-linux-arm64-musl": "npm:4.46.3" - "@rollup/rollup-linux-loongarch64-gnu": "npm:4.46.3" - "@rollup/rollup-linux-ppc64-gnu": "npm:4.46.3" - "@rollup/rollup-linux-riscv64-gnu": "npm:4.46.3" - "@rollup/rollup-linux-riscv64-musl": "npm:4.46.3" - "@rollup/rollup-linux-s390x-gnu": "npm:4.46.3" - "@rollup/rollup-linux-x64-gnu": "npm:4.46.3" - "@rollup/rollup-linux-x64-musl": "npm:4.46.3" - "@rollup/rollup-win32-arm64-msvc": "npm:4.46.3" - "@rollup/rollup-win32-ia32-msvc": "npm:4.46.3" - "@rollup/rollup-win32-x64-msvc": "npm:4.46.3" +"rollup@npm:^4.43.0, rollup@npm:^4.55.3": + version: 4.55.3 + resolution: "rollup@npm:4.55.3" + dependencies: + "@rollup/rollup-android-arm-eabi": "npm:4.55.3" + "@rollup/rollup-android-arm64": "npm:4.55.3" + "@rollup/rollup-darwin-arm64": "npm:4.55.3" + "@rollup/rollup-darwin-x64": "npm:4.55.3" + "@rollup/rollup-freebsd-arm64": "npm:4.55.3" + "@rollup/rollup-freebsd-x64": "npm:4.55.3" + "@rollup/rollup-linux-arm-gnueabihf": "npm:4.55.3" + "@rollup/rollup-linux-arm-musleabihf": "npm:4.55.3" + "@rollup/rollup-linux-arm64-gnu": "npm:4.55.3" + "@rollup/rollup-linux-arm64-musl": "npm:4.55.3" + "@rollup/rollup-linux-loong64-gnu": "npm:4.55.3" + "@rollup/rollup-linux-loong64-musl": "npm:4.55.3" + "@rollup/rollup-linux-ppc64-gnu": "npm:4.55.3" + "@rollup/rollup-linux-ppc64-musl": "npm:4.55.3" + "@rollup/rollup-linux-riscv64-gnu": "npm:4.55.3" + "@rollup/rollup-linux-riscv64-musl": "npm:4.55.3" + "@rollup/rollup-linux-s390x-gnu": "npm:4.55.3" + "@rollup/rollup-linux-x64-gnu": "npm:4.55.3" + "@rollup/rollup-linux-x64-musl": "npm:4.55.3" + "@rollup/rollup-openbsd-x64": "npm:4.55.3" + "@rollup/rollup-openharmony-arm64": "npm:4.55.3" + "@rollup/rollup-win32-arm64-msvc": "npm:4.55.3" + "@rollup/rollup-win32-ia32-msvc": "npm:4.55.3" + "@rollup/rollup-win32-x64-gnu": "npm:4.55.3" + "@rollup/rollup-win32-x64-msvc": "npm:4.55.3" "@types/estree": "npm:1.0.8" fsevents: "npm:~2.3.2" dependenciesMeta: @@ -13226,10 +13266,14 @@ __metadata: optional: true "@rollup/rollup-linux-arm64-musl": optional: true - "@rollup/rollup-linux-loongarch64-gnu": + "@rollup/rollup-linux-loong64-gnu": + optional: true + "@rollup/rollup-linux-loong64-musl": optional: true "@rollup/rollup-linux-ppc64-gnu": optional: true + "@rollup/rollup-linux-ppc64-musl": + optional: true "@rollup/rollup-linux-riscv64-gnu": optional: true "@rollup/rollup-linux-riscv64-musl": @@ -13240,17 +13284,23 @@ __metadata: optional: true "@rollup/rollup-linux-x64-musl": optional: true + "@rollup/rollup-openbsd-x64": + optional: true + "@rollup/rollup-openharmony-arm64": + optional: true "@rollup/rollup-win32-arm64-msvc": optional: true "@rollup/rollup-win32-ia32-msvc": optional: true + "@rollup/rollup-win32-x64-gnu": + optional: true "@rollup/rollup-win32-x64-msvc": optional: true fsevents: optional: true bin: rollup: dist/bin/rollup - checksum: 10/671eaf282f102fa7f99c4db95b7652cf895a923f8c372bd0fe037a77e20f25dea472879f14a7429018bbd5eba2ac3b8dcbafa48b4c275c52825a9965d079bc6a + checksum: 10/9abdb43e96e24309cad838aeb0a97055d93c1b6820cb1d55041cdf414a07e3fe377564b711476e511f2d373484b04dd68b6129c42efc201deb8f761bf4c2de7e languageName: node linkType: hard From b674425ebc59b5d16feb97664c48522534a4a216 Mon Sep 17 00:00:00 2001 From: grypez <143971198+grypez@users.noreply.github.com> Date: Wed, 21 Jan 2026 20:23:40 -0600 Subject: [PATCH 02/10] build: Remove @endo/import-bundle support Drop support for the legacy endoZipBase64 bundle format and remove the @endo/import-bundle dependency. All vat bundles now use the vite-iife format loaded via Compartment.evaluate(). - Remove @endo/import-bundle from ocap-kernel dependencies - Simplify bundle-loader.ts to only support vite-iife format - Update VatSupervisor to use synchronous loadBundle - Update CLI tests to mock bundleVat instead of @endo/bundle-source - Update serve integration test to check vite-iife format Co-Authored-By: Claude Opus 4.5 --- packages/cli/README.md | 2 +- packages/cli/src/commands/bundle.test.ts | 30 +++++---- packages/cli/test/integration/serve.test.ts | 32 +++++----- packages/ocap-kernel/package.json | 1 - .../ocap-kernel/src/vats/VatSupervisor.ts | 3 +- .../ocap-kernel/src/vats/bundle-loader.ts | 64 ++++++------------- 6 files changed, 55 insertions(+), 77 deletions(-) diff --git a/packages/cli/README.md b/packages/cli/README.md index 2c442f584..a2fe6fad1 100644 --- a/packages/cli/README.md +++ b/packages/cli/README.md @@ -6,7 +6,7 @@ Ocap Kernel cli. ### `ocap bundle ` -Bundle the supplied file or directory targets. Expects each target to be a `.js` file or a directory containing `.js` files. Each `.js` file will be bundled using `@endo/bundle-source` and written to an associated `.bundle`. +Bundle the supplied file or directory targets. Expects each target to be a `.js` file or a directory containing `.js` files. Each `.js` file will be bundled using vite and written to an associated `.bundle`. ### `ocap watch ` diff --git a/packages/cli/src/commands/bundle.test.ts b/packages/cli/src/commands/bundle.test.ts index e83fec3c7..b1e97f7d9 100644 --- a/packages/cli/src/commands/bundle.test.ts +++ b/packages/cli/src/commands/bundle.test.ts @@ -12,7 +12,7 @@ import { fileExists } from '../file.ts'; const mocks = vi.hoisted(() => { return { - endoBundleSource: vi.fn(), + bundleVat: vi.fn(), Logger: vi.fn( () => ({ @@ -25,12 +25,10 @@ const mocks = vi.hoisted(() => { }; }); -vi.mock('@endo/bundle-source', () => ({ - default: mocks.endoBundleSource, +vi.mock('../vite/vat-bundler.ts', () => ({ + bundleVat: mocks.bundleVat, })); -vi.mock('@endo/init', () => ({})); - vi.mock('@metamask/logger', () => ({ Logger: mocks.Logger, })); @@ -68,8 +66,13 @@ describe('bundle', async () => { async ({ source, bundle }) => { expect(await fileExists(bundle)).toBe(false); - const testContent = { source: 'test-content' }; - mocks.endoBundleSource.mockImplementationOnce(() => testContent); + const testContent = { + moduleFormat: 'vite-iife', + code: 'test-code', + exports: [], + modules: {}, + }; + mocks.bundleVat.mockImplementationOnce(() => testContent); await bundleFile(source, { logger }); @@ -84,7 +87,7 @@ describe('bundle', async () => { ); it('throws if bundling fails', async () => { - mocks.endoBundleSource.mockImplementationOnce(() => { + mocks.bundleVat.mockImplementationOnce(() => { throw new Error('test error'); }); await expect( @@ -97,9 +100,12 @@ describe('bundle', async () => { it('bundles a directory', async () => { expect(await globBundles()).toStrictEqual([]); - mocks.endoBundleSource.mockImplementation(() => { - return 'test content'; - }); + mocks.bundleVat.mockImplementation(() => ({ + moduleFormat: 'vite-iife', + code: 'test content', + exports: [], + modules: {}, + })); await bundleDir(testBundleRoot, { logger }); @@ -111,7 +117,7 @@ describe('bundle', async () => { }); it('throws if bundling fails', async () => { - mocks.endoBundleSource.mockImplementationOnce(() => { + mocks.bundleVat.mockImplementationOnce(() => { throw new Error('test error'); }); await expect(bundleDir(testBundleRoot, { logger })).rejects.toThrow( diff --git a/packages/cli/test/integration/serve.test.ts b/packages/cli/test/integration/serve.test.ts index 2f2a4d200..54fbd7cc4 100644 --- a/packages/cli/test/integration/serve.test.ts +++ b/packages/cli/test/integration/serve.test.ts @@ -1,8 +1,6 @@ import '@metamask/kernel-shims/endoify'; -import type { BundleSourceResult } from '@endo/bundle-source'; import { makeCounter, stringify } from '@metamask/kernel-utils'; import { isObject, hasProperty } from '@metamask/utils'; -import { createHash } from 'node:crypto'; import { readFile } from 'node:fs/promises'; import { join, resolve } from 'node:path'; import { describe, it, expect, vi, beforeEach } from 'vitest'; @@ -12,16 +10,19 @@ import { defaultConfig } from '../../src/config.ts'; import { withTimeout } from '../../src/utils.ts'; import { makeTestBundleStage, validTestBundleNames } from '../bundles.ts'; -const isBundleSourceResult = ( - value: unknown, -): value is BundleSourceResult<'endoZipBase64'> => +type ViteBundle = { + moduleFormat: 'vite-iife'; + code: string; + exports: string[]; + modules: Record; +}; + +const isViteBundle = (value: unknown): value is ViteBundle => isObject(value) && hasProperty(value, 'moduleFormat') && - value.moduleFormat === 'endoZipBase64' && - hasProperty(value, 'endoZipBase64') && - typeof value.endoZipBase64 === 'string' && - hasProperty(value, 'endoZipBase64Sha512') && - typeof value.endoZipBase64Sha512 === 'string'; + value.moduleFormat === 'vite-iife' && + hasProperty(value, 'code') && + typeof value.code === 'string'; describe('serve', async () => { beforeEach(() => { @@ -86,7 +87,7 @@ describe('serve', async () => { try { const bundleData = await readFile(bundlePath); const expectedBundleContent = JSON.parse(bundleData.toString()); - if (!isBundleSourceResult(expectedBundleContent)) { + if (!isViteBundle(expectedBundleContent)) { throw new Error( [ `Could not read expected bundle ${bundlePath}`, @@ -94,19 +95,16 @@ describe('serve', async () => { ].join('\n'), ); } - const expectedBundleHash = expectedBundleContent.endoZipBase64Sha512; + const expectedCode = expectedBundleContent.code; const receivedBundleContent = await requestBundle(bundleName); - if (!isBundleSourceResult(receivedBundleContent)) { + if (!isViteBundle(receivedBundleContent)) { throw new Error( `Received unexpected response from server: ${stringify(receivedBundleContent)}`, ); } - const receivedBundleHash = createHash('sha512') - .update(Buffer.from(receivedBundleContent.endoZipBase64)) - .digest('hex'); - expect(receivedBundleHash).toStrictEqual(expectedBundleHash); + expect(receivedBundleContent.code).toStrictEqual(expectedCode); } finally { await withTimeout(close(), 400).catch(console.error); } diff --git a/packages/ocap-kernel/package.json b/packages/ocap-kernel/package.json index 10f251a7b..00d278769 100644 --- a/packages/ocap-kernel/package.json +++ b/packages/ocap-kernel/package.json @@ -72,7 +72,6 @@ "@chainsafe/libp2p-noise": "^16.1.3", "@chainsafe/libp2p-yamux": "patch:@chainsafe/libp2p-yamux@npm%3A7.0.4#~/.yarn/patches/@chainsafe-libp2p-yamux-npm-7.0.4-284c2f6812.patch", "@endo/errors": "^1.2.13", - "@endo/import-bundle": "^1.5.2", "@endo/marshal": "^1.8.0", "@endo/pass-style": "^1.6.3", "@endo/promise-kit": "^1.1.13", diff --git a/packages/ocap-kernel/src/vats/VatSupervisor.ts b/packages/ocap-kernel/src/vats/VatSupervisor.ts index 90aa39b20..6bff69d11 100644 --- a/packages/ocap-kernel/src/vats/VatSupervisor.ts +++ b/packages/ocap-kernel/src/vats/VatSupervisor.ts @@ -333,8 +333,7 @@ export class VatSupervisor { // Otherwise, just rethrow the error. throw error; } - const vatNS = await loadBundle(bundleContent, { - filePrefix: `vat-${this.id}/...`, + const vatNS = loadBundle(bundleContent, { endowments, inescapableGlobalProperties, }); diff --git a/packages/ocap-kernel/src/vats/bundle-loader.ts b/packages/ocap-kernel/src/vats/bundle-loader.ts index f0cfce8bb..cdf5b92f8 100644 --- a/packages/ocap-kernel/src/vats/bundle-loader.ts +++ b/packages/ocap-kernel/src/vats/bundle-loader.ts @@ -1,11 +1,3 @@ -import { importBundle } from '@endo/import-bundle'; - -type EndoBundle = { - moduleFormat: 'endoZipBase64'; - endoZipBase64: string; - endoZipBase64Sha512: string; -}; - type ViteBundle = { moduleFormat: 'vite-iife'; code: string; @@ -16,57 +8,41 @@ type ViteBundle = { >; }; -type Bundle = EndoBundle | ViteBundle; - export type LoadBundleOptions = { - filePrefix?: string; endowments?: object; inescapableGlobalProperties?: object; }; /** - * Load a bundle and return its namespace. - * - * Supports two bundle formats: - * - `endoZipBase64`: Legacy format using `importBundle()` - * - `vite-iife`: New format using `Compartment.evaluate()` + * Load a vite-iife bundle and return its namespace. * * @param content - The bundle content as a JSON string. * @param options - Options for loading the bundle. * @returns The namespace exported by the bundle. */ -export async function loadBundle( +export function loadBundle( content: string, options: LoadBundleOptions = {}, -): Promise> { - const parsed = JSON.parse(content) as Bundle; +): Record { + const parsed = JSON.parse(content) as Record; const { endowments = {}, inescapableGlobalProperties = {} } = options; - if (parsed.moduleFormat === 'endoZipBase64') { - return await importBundle(parsed, { - filePrefix: options.filePrefix, - endowments, - inescapableGlobalProperties, - }); - } - - if (parsed.moduleFormat === 'vite-iife') { - const compartment = new Compartment({ - // SES globals that may be used by bundled code - harden: globalThis.harden, - assert: globalThis.assert, - ...endowments, - ...inescapableGlobalProperties, - }); - // The code declares `var __vatExports__ = (function(){...})({});` - // We wrap it in an IIFE to capture and return the result. - const vatExports = compartment.evaluate( - `(function() { ${parsed.code}; return __vatExports__; })()`, - ); - return vatExports as Record; + if (parsed.moduleFormat !== 'vite-iife') { + throw new Error(`Unknown bundle format: ${String(parsed.moduleFormat)}`); } - - throw new Error( - `Unknown bundle format: ${(parsed as { moduleFormat: string }).moduleFormat}`, + const bundle = parsed as unknown as ViteBundle; + + const compartment = new Compartment({ + // SES globals that may be used by bundled code + harden: globalThis.harden, + assert: globalThis.assert, + ...endowments, + ...inescapableGlobalProperties, + }); + // The code declares `var __vatExports__ = (function(){...})({});` + // We wrap it in an IIFE to capture and return the result. + const vatExports = compartment.evaluate( + `(function() { ${bundle.code}; return __vatExports__; })()`, ); + return vatExports as Record; } From 7bfa20d42941233eb86681a7bfba5248ece529b9 Mon Sep 17 00:00:00 2001 From: grypez <143971198+grypez@users.noreply.github.com> Date: Thu, 22 Jan 2026 09:01:54 -0600 Subject: [PATCH 03/10] respin yarn --- packages/kernel-test/package.json | 1 - yarn.lock | 15 --------------- 2 files changed, 16 deletions(-) diff --git a/packages/kernel-test/package.json b/packages/kernel-test/package.json index d8f387b27..c4acfa7e2 100644 --- a/packages/kernel-test/package.json +++ b/packages/kernel-test/package.json @@ -58,7 +58,6 @@ "@metamask/kernel-utils": "workspace:^", "@metamask/logger": "workspace:^", "@metamask/ocap-kernel": "workspace:^", - "@ocap/kernel-agents": "workspace:^", "@ocap/kernel-language-model-service": "workspace:^", "@ocap/nodejs": "workspace:^", "@ocap/nodejs-test-workers": "workspace:^", diff --git a/yarn.lock b/yarn.lock index 90d506f3e..34abd75c5 100644 --- a/yarn.lock +++ b/yarn.lock @@ -755,19 +755,6 @@ __metadata: languageName: node linkType: hard -"@endo/import-bundle@npm:^1.5.2": - version: 1.5.2 - resolution: "@endo/import-bundle@npm:1.5.2" - dependencies: - "@endo/base64": "npm:^1.0.12" - "@endo/compartment-mapper": "npm:^1.6.3" - "@endo/errors": "npm:^1.2.13" - "@endo/where": "npm:^1.0.11" - ses: "npm:^1.14.0" - checksum: 10/521a4bc3a0e7b75626df996e5f965e9bedc8e463586b8dee57d90958371731617a205b98d825afaeeb920b64a1fc105a1acc1f3716742100097068e114169b43 - languageName: node - linkType: hard - "@endo/init@npm:^1.1.12, @endo/init@npm:^1.1.9": version: 1.1.12 resolution: "@endo/init@npm:1.1.12" @@ -2700,7 +2687,6 @@ __metadata: "@chainsafe/libp2p-noise": "npm:^16.1.3" "@chainsafe/libp2p-yamux": "patch:@chainsafe/libp2p-yamux@npm%3A7.0.4#~/.yarn/patches/@chainsafe-libp2p-yamux-npm-7.0.4-284c2f6812.patch" "@endo/errors": "npm:^1.2.13" - "@endo/import-bundle": "npm:^1.5.2" "@endo/marshal": "npm:^1.8.0" "@endo/pass-style": "npm:^1.6.3" "@endo/promise-kit": "npm:^1.1.13" @@ -3737,7 +3723,6 @@ __metadata: "@metamask/logger": "workspace:^" "@metamask/ocap-kernel": "workspace:^" "@ocap/cli": "workspace:^" - "@ocap/kernel-agents": "workspace:^" "@ocap/kernel-language-model-service": "workspace:^" "@ocap/nodejs": "workspace:^" "@ocap/nodejs-test-workers": "workspace:^" From 94482c222e1e0f3d59e563a54ebf78bc71a4c5ce Mon Sep 17 00:00:00 2001 From: grypez <143971198+grypez@users.noreply.github.com> Date: Thu, 22 Jan 2026 09:16:59 -0600 Subject: [PATCH 04/10] fix test mock --- packages/cli/test/test.bundle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/cli/test/test.bundle b/packages/cli/test/test.bundle index cb26f3a08..de8e2eda9 100644 --- a/packages/cli/test/test.bundle +++ b/packages/cli/test/test.bundle @@ -1 +1 @@ -{"moduleFormat":"endoZipBase64","endoZipBase64":"This is merely a test bundle!","endoZipBase64Sha512":"d0ef05812fb8a7cce57dcb8d6f6fbe5677f175e2ef966df1d81dec9868a8f56b0e89b691fdb3d43a3902962adcf37d1eee6902a113476cca85f8d76957bdf154"} \ No newline at end of file +{"moduleFormat":"vite-iife","code":"var __vatExports__ = (function(exports) { exports.test = 'This is merely a test bundle!'; return exports; })({});","exports":["test"],"modules":{}} From eccd27fab7392b0cbf56ec50a56bb7a885b4dadf Mon Sep 17 00:00:00 2001 From: grypez <143971198+grypez@users.noreply.github.com> Date: Thu, 22 Jan 2026 16:40:02 -0500 Subject: [PATCH 05/10] Apply some suggestions from code review Co-authored-by: Erik Marks <25517051+rekmarks@users.noreply.github.com> --- packages/cli/README.md | 2 +- packages/ocap-kernel/src/vats/bundle-loader.ts | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/cli/README.md b/packages/cli/README.md index a2fe6fad1..d7b7b1ac9 100644 --- a/packages/cli/README.md +++ b/packages/cli/README.md @@ -6,7 +6,7 @@ Ocap Kernel cli. ### `ocap bundle ` -Bundle the supplied file or directory targets. Expects each target to be a `.js` file or a directory containing `.js` files. Each `.js` file will be bundled using vite and written to an associated `.bundle`. +Bundle the supplied file or directory targets. Expects each target to be a `.js` file or a directory containing `.js` files. Each `.js` file will be bundled using `vite` and written to an associated `.bundle`. ### `ocap watch ` diff --git a/packages/ocap-kernel/src/vats/bundle-loader.ts b/packages/ocap-kernel/src/vats/bundle-loader.ts index cdf5b92f8..bfd4b08ae 100644 --- a/packages/ocap-kernel/src/vats/bundle-loader.ts +++ b/packages/ocap-kernel/src/vats/bundle-loader.ts @@ -1,5 +1,5 @@ -type ViteBundle = { - moduleFormat: 'vite-iife'; +type VatBundle = { + moduleFormat: 'iife'; code: string; exports: string[]; modules: Record< From ebbfee68616889ee007ac6d5e3b681e514b6281a Mon Sep 17 00:00:00 2001 From: grypez <143971198+grypez@users.noreply.github.com> Date: Thu, 22 Jan 2026 19:30:10 -0600 Subject: [PATCH 06/10] address remaining review comments --- .../cli/src/vite/strip-comments-plugin.ts | 73 +++++++++++++++++++ packages/cli/src/vite/vat-bundler.ts | 21 +++--- packages/cli/test/integration/serve.test.ts | 21 +----- packages/cli/test/test.bundle | 2 +- packages/kernel-utils/src/index.ts | 2 + packages/kernel-utils/src/vat-bundle.ts | 30 ++++++++ .../ocap-kernel/src/vats/bundle-loader.ts | 16 +--- 7 files changed, 122 insertions(+), 43 deletions(-) create mode 100644 packages/cli/src/vite/strip-comments-plugin.ts create mode 100644 packages/kernel-utils/src/vat-bundle.ts diff --git a/packages/cli/src/vite/strip-comments-plugin.ts b/packages/cli/src/vite/strip-comments-plugin.ts new file mode 100644 index 000000000..b5d1d11e2 --- /dev/null +++ b/packages/cli/src/vite/strip-comments-plugin.ts @@ -0,0 +1,73 @@ +import type { Plugin } from 'rollup'; + +/** + * Rollup plugin that strips comments from bundled code. + * + * SES rejects code containing `import(` patterns, even when they appear + * in comments. This plugin removes all comments to avoid triggering + * that detection. + * + * Uses the `renderChunk` hook to process the final output. + * + * @returns A Rollup plugin. + */ +export function stripCommentsPlugin(): Plugin { + return { + name: 'strip-comments', + renderChunk(code) { + // Remove single-line comments (// ...) + // Remove multi-line comments (/* ... */) + // Be careful not to remove comments inside strings + let result = ''; + let i = 0; + while (i < code.length) { + const char = code[i] as string; + const nextChar = code[i + 1]; + + // Check for string literals + if (char === '"' || char === "'" || char === '`') { + const quote = char; + result += quote; + i += 1; + // Copy string content including escape sequences + while (i < code.length) { + const strChar = code[i] as string; + if (strChar === '\\' && i + 1 < code.length) { + result += strChar + (code[i + 1] as string); + i += 2; + } else if (strChar === quote) { + result += quote; + i += 1; + break; + } else { + result += strChar; + i += 1; + } + } + } + // Check for single-line comment + else if (char === '/' && nextChar === '/') { + // Skip until end of line + while (i < code.length && code[i] !== '\n') { + i += 1; + } + } + // Check for multi-line comment + else if (char === '/' && nextChar === '*') { + i += 2; + // Skip until */ + while (i < code.length && !(code[i - 1] === '*' && code[i] === '/')) { + i += 1; + } + i += 1; // Skip the closing / + } + // Regular character + else { + result += char; + i += 1; + } + } + return result; + }, + }; +} diff --git a/packages/cli/src/vite/vat-bundler.ts b/packages/cli/src/vite/vat-bundler.ts index 581aa99b6..4a526b2f6 100644 --- a/packages/cli/src/vite/vat-bundler.ts +++ b/packages/cli/src/vite/vat-bundler.ts @@ -1,13 +1,11 @@ +import type { VatBundle } from '@metamask/kernel-utils'; import { build } from 'vite'; import type { Rollup, PluginOption } from 'vite'; import { exportMetadataPlugin } from './export-metadata-plugin.ts'; -import type { BundleMetadata } from './export-metadata-plugin.ts'; +import { stripCommentsPlugin } from './strip-comments-plugin.ts'; -export type VatBundle = BundleMetadata & { - moduleFormat: 'vite-iife'; - code: string; -}; +export type { VatBundle }; /** * Bundle a vat source file using vite. @@ -36,7 +34,10 @@ export async function bundleVat(sourcePath: string): Promise { exports: 'named', inlineDynamicImports: true, }, - plugins: [metadataPlugin as unknown as PluginOption], + plugins: [ + stripCommentsPlugin() as unknown as PluginOption, + metadataPlugin as unknown as PluginOption, + ], }, minify: false, }, @@ -51,13 +52,9 @@ export async function bundleVat(sourcePath: string): Promise { throw new Error(`Failed to produce bundle for ${sourcePath}`); } - // SES rejects code containing `import(` patterns, even in comments. - // Replace them with a safe alternative that won't trigger detection. - const sanitizedCode = chunk.code.replace(/\bimport\s*\(/gu, 'IMPORT('); - return { - moduleFormat: 'vite-iife', - code: sanitizedCode, + moduleFormat: 'iife', + code: chunk.code, ...metadataPlugin.getMetadata(), }; } diff --git a/packages/cli/test/integration/serve.test.ts b/packages/cli/test/integration/serve.test.ts index 54fbd7cc4..84cc14111 100644 --- a/packages/cli/test/integration/serve.test.ts +++ b/packages/cli/test/integration/serve.test.ts @@ -1,6 +1,5 @@ import '@metamask/kernel-shims/endoify'; -import { makeCounter, stringify } from '@metamask/kernel-utils'; -import { isObject, hasProperty } from '@metamask/utils'; +import { isVatBundle, makeCounter, stringify } from '@metamask/kernel-utils'; import { readFile } from 'node:fs/promises'; import { join, resolve } from 'node:path'; import { describe, it, expect, vi, beforeEach } from 'vitest'; @@ -10,20 +9,6 @@ import { defaultConfig } from '../../src/config.ts'; import { withTimeout } from '../../src/utils.ts'; import { makeTestBundleStage, validTestBundleNames } from '../bundles.ts'; -type ViteBundle = { - moduleFormat: 'vite-iife'; - code: string; - exports: string[]; - modules: Record; -}; - -const isViteBundle = (value: unknown): value is ViteBundle => - isObject(value) && - hasProperty(value, 'moduleFormat') && - value.moduleFormat === 'vite-iife' && - hasProperty(value, 'code') && - typeof value.code === 'string'; - describe('serve', async () => { beforeEach(() => { vi.resetModules(); @@ -87,7 +72,7 @@ describe('serve', async () => { try { const bundleData = await readFile(bundlePath); const expectedBundleContent = JSON.parse(bundleData.toString()); - if (!isViteBundle(expectedBundleContent)) { + if (!isVatBundle(expectedBundleContent)) { throw new Error( [ `Could not read expected bundle ${bundlePath}`, @@ -98,7 +83,7 @@ describe('serve', async () => { const expectedCode = expectedBundleContent.code; const receivedBundleContent = await requestBundle(bundleName); - if (!isViteBundle(receivedBundleContent)) { + if (!isVatBundle(receivedBundleContent)) { throw new Error( `Received unexpected response from server: ${stringify(receivedBundleContent)}`, ); diff --git a/packages/cli/test/test.bundle b/packages/cli/test/test.bundle index de8e2eda9..5dce507f1 100644 --- a/packages/cli/test/test.bundle +++ b/packages/cli/test/test.bundle @@ -1 +1 @@ -{"moduleFormat":"vite-iife","code":"var __vatExports__ = (function(exports) { exports.test = 'This is merely a test bundle!'; return exports; })({});","exports":["test"],"modules":{}} +{"moduleFormat":"iife","code":"var __vatExports__ = (function(exports) { exports.test = 'This is merely a test bundle!'; return exports; })({});","exports":["test"],"modules":{}} diff --git a/packages/kernel-utils/src/index.ts b/packages/kernel-utils/src/index.ts index d573379ce..934437874 100644 --- a/packages/kernel-utils/src/index.ts +++ b/packages/kernel-utils/src/index.ts @@ -26,6 +26,8 @@ export { export { waitUntilQuiescent } from './wait-quiescent.ts'; export { fromHex, toHex } from './hex.ts'; export { mergeDisjointRecords } from './merge-disjoint-records.ts'; +export type { VatBundle } from './vat-bundle.ts'; +export { isVatBundle } from './vat-bundle.ts'; export { retry, retryWithBackoff, diff --git a/packages/kernel-utils/src/vat-bundle.ts b/packages/kernel-utils/src/vat-bundle.ts new file mode 100644 index 000000000..855ced019 --- /dev/null +++ b/packages/kernel-utils/src/vat-bundle.ts @@ -0,0 +1,30 @@ +import { isObject, hasProperty } from '@metamask/utils'; + +/** + * A bundle produced by the vat bundler. + * + * Contains the bundled code as an IIFE that assigns exports to `__vatExports__`, + * along with metadata about the bundle's exports and modules. + */ +export type VatBundle = { + moduleFormat: 'iife'; + code: string; + exports: string[]; + modules: Record< + string, + { renderedExports: string[]; removedExports: string[] } + >; +}; + +/** + * Type guard to check if a value is a VatBundle. + * + * @param value - The value to check. + * @returns True if the value is a VatBundle. + */ +export const isVatBundle = (value: unknown): value is VatBundle => + isObject(value) && + hasProperty(value, 'moduleFormat') && + value.moduleFormat === 'iife' && + hasProperty(value, 'code') && + typeof value.code === 'string'; diff --git a/packages/ocap-kernel/src/vats/bundle-loader.ts b/packages/ocap-kernel/src/vats/bundle-loader.ts index bfd4b08ae..b12812413 100644 --- a/packages/ocap-kernel/src/vats/bundle-loader.ts +++ b/packages/ocap-kernel/src/vats/bundle-loader.ts @@ -1,12 +1,4 @@ -type VatBundle = { - moduleFormat: 'iife'; - code: string; - exports: string[]; - modules: Record< - string, - { renderedExports: string[]; removedExports: string[] } - >; -}; +import type { VatBundle } from '@metamask/kernel-utils'; export type LoadBundleOptions = { endowments?: object; @@ -14,7 +6,7 @@ export type LoadBundleOptions = { }; /** - * Load a vite-iife bundle and return its namespace. + * Load an iife bundle and return its namespace. * * @param content - The bundle content as a JSON string. * @param options - Options for loading the bundle. @@ -27,10 +19,10 @@ export function loadBundle( const parsed = JSON.parse(content) as Record; const { endowments = {}, inescapableGlobalProperties = {} } = options; - if (parsed.moduleFormat !== 'vite-iife') { + if (parsed.moduleFormat !== 'iife') { throw new Error(`Unknown bundle format: ${String(parsed.moduleFormat)}`); } - const bundle = parsed as unknown as ViteBundle; + const bundle = parsed as unknown as VatBundle; const compartment = new Compartment({ // SES globals that may be used by bundled code From bf74145cead9e1cb6fb990597c7c23ddcfb8ff97 Mon Sep 17 00:00:00 2001 From: grypez <143971198+grypez@users.noreply.github.com> Date: Thu, 22 Jan 2026 19:58:32 -0600 Subject: [PATCH 07/10] small fixes --- packages/cli/src/commands/bundle.test.ts | 4 ++-- packages/cli/src/commands/bundle.ts | 15 ++++----------- 2 files changed, 6 insertions(+), 13 deletions(-) diff --git a/packages/cli/src/commands/bundle.test.ts b/packages/cli/src/commands/bundle.test.ts index b1e97f7d9..a4a3608bc 100644 --- a/packages/cli/src/commands/bundle.test.ts +++ b/packages/cli/src/commands/bundle.test.ts @@ -67,7 +67,7 @@ describe('bundle', async () => { expect(await fileExists(bundle)).toBe(false); const testContent = { - moduleFormat: 'vite-iife', + moduleFormat: 'iife', code: 'test-code', exports: [], modules: {}, @@ -101,7 +101,7 @@ describe('bundle', async () => { expect(await globBundles()).toStrictEqual([]); mocks.bundleVat.mockImplementation(() => ({ - moduleFormat: 'vite-iife', + moduleFormat: 'iife', code: 'test content', exports: [], modules: {}, diff --git a/packages/cli/src/commands/bundle.ts b/packages/cli/src/commands/bundle.ts index d073ba7e1..79162f980 100644 --- a/packages/cli/src/commands/bundle.ts +++ b/packages/cli/src/commands/bundle.ts @@ -1,3 +1,4 @@ +import type { Logger } from '@metamask/logger'; import { glob } from 'glob'; import { writeFile } from 'node:fs/promises'; import { resolve, join } from 'node:path'; @@ -6,16 +7,8 @@ import { isDirectory } from '../file.ts'; import { resolveBundlePath } from '../path.ts'; import { bundleVat } from '../vite/vat-bundler.ts'; -/** - * Minimal logger interface for bundle operations. - */ -type BundleLogger = { - info: (message: string, ...args: unknown[]) => void; - error?: (message: string, ...args: unknown[]) => void; -}; - type BundleFileOptions = { - logger: BundleLogger; + logger: Logger; targetPath?: string; }; @@ -52,7 +45,7 @@ export async function bundleFile( */ export async function bundleDir( sourceDir: string, - options: { logger: BundleLogger }, + options: { logger: Logger }, ): Promise { const { logger } = options; logger.info('Bundling directory:', sourceDir); @@ -73,7 +66,7 @@ export async function bundleDir( */ export async function bundleSource( target: string, - logger: BundleLogger, + logger: Logger, ): Promise { const targetIsDirectory = await isDirectory(target); await (targetIsDirectory ? bundleDir : bundleFile)(target, { logger }); From 7e466033abfe3104122a9080f5528682caabc5e9 Mon Sep 17 00:00:00 2001 From: grypez <143971198+grypez@users.noreply.github.com> Date: Fri, 23 Jan 2026 13:47:41 -0600 Subject: [PATCH 08/10] add isVatBundle to index test --- packages/kernel-utils/src/index.test.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/kernel-utils/src/index.test.ts b/packages/kernel-utils/src/index.test.ts index 2b84f0ea6..67cef419d 100644 --- a/packages/kernel-utils/src/index.test.ts +++ b/packages/kernel-utils/src/index.test.ts @@ -20,6 +20,7 @@ describe('index', () => { 'isPrimitive', 'isTypedArray', 'isTypedObject', + 'isVatBundle', 'makeCounter', 'makeDefaultExo', 'makeDefaultInterface', From 84cebfa73647264cac3445f4cc01a3f1ffd34e41 Mon Sep 17 00:00:00 2001 From: grypez <143971198+grypez@users.noreply.github.com> Date: Fri, 23 Jan 2026 17:08:01 -0500 Subject: [PATCH 09/10] build: fix bundle import scrubber (#772) Reuses the acorn parsing dependency from `@ocap/kernel-agents-repl` to authoritatively scrub comments from vat bundles. Refs #770 --- > [!NOTE] > Replaces the comment scrubber with an AST-based implementation to reliably remove comments (including those containing `import(`) from bundled code. > > - Refactors `vite/strip-comments-plugin` to use Acorn (`parse` with `onComment`) and return unchanged code when no comments are found > - Adds unit tests for `strip-comments-plugin` covering single/multi-line comments, strings, regex, templates, and empty input > - Adds `acorn` dependency in `@ocap/cli` and updates `yarn.lock` > > Written by [Cursor Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit 3a59ba87e178c05bdf56b92735a213f9ea2bd7c7. This will update automatically on new commits. Configure [here](https://cursor.com/dashboard?tab=bugbot). --- packages/cli/package.json | 1 + .../src/vite/strip-comments-plugin.test.ts | 51 ++++++++++++ .../cli/src/vite/strip-comments-plugin.ts | 77 ++++++------------- yarn.lock | 1 + 4 files changed, 78 insertions(+), 52 deletions(-) create mode 100644 packages/cli/src/vite/strip-comments-plugin.test.ts diff --git a/packages/cli/package.json b/packages/cli/package.json index 54361c106..eb367c130 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -51,6 +51,7 @@ "@metamask/snaps-utils": "^11.7.1", "@metamask/utils": "^11.9.0", "@types/node": "^22.13.1", + "acorn": "^8.15.0", "chokidar": "^4.0.1", "glob": "^11.0.0", "libp2p": "2.10.0", diff --git a/packages/cli/src/vite/strip-comments-plugin.test.ts b/packages/cli/src/vite/strip-comments-plugin.test.ts new file mode 100644 index 000000000..951f8d8eb --- /dev/null +++ b/packages/cli/src/vite/strip-comments-plugin.test.ts @@ -0,0 +1,51 @@ +import { describe, it, expect } from 'vitest'; + +import { stripCommentsPlugin } from './strip-comments-plugin.ts'; + +describe('stripCommentsPlugin', () => { + const plugin = stripCommentsPlugin(); + const renderChunk = plugin.renderChunk as (code: string) => string | null; + + it.each([ + [ + 'single-line comment', + 'const x = 1; // comment\nconst y = 2;', + 'const x = 1; \nconst y = 2;', + ], + [ + 'multi-line comment', + 'const x = 1; /* comment */ const y = 2;', + 'const x = 1; const y = 2;', + ], + [ + 'multiple comments', + '/* a */ const x = 1; // b\n/* c */', + ' const x = 1; \n', + ], + [ + 'comment containing import()', + 'const x = 1; // import("module")\nconst y = 2;', + 'const x = 1; \nconst y = 2;', + ], + [ + 'comment with string content preserved', + 'const x = "// in string"; // real comment', + 'const x = "// in string"; ', + ], + ['code that is only a comment', '// just a comment', ''], + ])('removes %s', (_name, code, expected) => { + expect(renderChunk(code)).toBe(expected); + }); + + it.each([ + ['string with // pattern', 'const x = "// not a comment";'], + ['string with /* */ pattern', 'const x = "/* not a comment */";'], + ['regex literal like //', 'const re = /\\/\\//;'], + ['template literal with // pattern', 'const x = `// not a comment`;'], + ['nested quotes in string', 'const x = "a \\"// not comment\\" b";'], + ['no comments', 'const x = 1;'], + ['empty code', ''], + ])('returns null for %s', (_name, code) => { + expect(renderChunk(code)).toBeNull(); + }); +}); diff --git a/packages/cli/src/vite/strip-comments-plugin.ts b/packages/cli/src/vite/strip-comments-plugin.ts index b5d1d11e2..c8bd03b35 100644 --- a/packages/cli/src/vite/strip-comments-plugin.ts +++ b/packages/cli/src/vite/strip-comments-plugin.ts @@ -1,11 +1,13 @@ +import type { Comment } from 'acorn'; +import { parse } from 'acorn'; import type { Plugin } from 'rollup'; /** - * Rollup plugin that strips comments from bundled code. + * Rollup plugin that strips comments from bundled code using AST parsing. * * SES rejects code containing `import(` patterns, even when they appear - * in comments. This plugin removes all comments to avoid triggering - * that detection. + * in comments. This plugin uses Acorn to definitively identify comment nodes + * and removes them to avoid triggering that detection. * * Uses the `renderChunk` hook to process the final output. * @@ -15,58 +17,29 @@ export function stripCommentsPlugin(): Plugin { return { name: 'strip-comments', renderChunk(code) { - // Remove single-line comments (// ...) - // Remove multi-line comments (/* ... */) - // Be careful not to remove comments inside strings + const comments: Comment[] = []; + + parse(code, { + ecmaVersion: 'latest', + sourceType: 'module', + onComment: comments, + }); + + if (comments.length === 0) { + return null; + } + + // Build result by copying non-comment ranges. + // Comments are sorted by position since acorn parses linearly. let result = ''; - let i = 0; - while (i < code.length) { - const char = code[i] as string; - const nextChar = code[i + 1]; + let position = 0; - // Check for string literals - if (char === '"' || char === "'" || char === '`') { - const quote = char; - result += quote; - i += 1; - // Copy string content including escape sequences - while (i < code.length) { - const strChar = code[i] as string; - if (strChar === '\\' && i + 1 < code.length) { - result += strChar + (code[i + 1] as string); - i += 2; - } else if (strChar === quote) { - result += quote; - i += 1; - break; - } else { - result += strChar; - i += 1; - } - } - } - // Check for single-line comment - else if (char === '/' && nextChar === '/') { - // Skip until end of line - while (i < code.length && code[i] !== '\n') { - i += 1; - } - } - // Check for multi-line comment - else if (char === '/' && nextChar === '*') { - i += 2; - // Skip until */ - while (i < code.length && !(code[i - 1] === '*' && code[i] === '/')) { - i += 1; - } - i += 1; // Skip the closing / - } - // Regular character - else { - result += char; - i += 1; - } + for (const comment of comments) { + result += code.slice(position, comment.start); + position = comment.end; } + + result += code.slice(position); return result; }, }; diff --git a/yarn.lock b/yarn.lock index 34abd75c5..c2bb72a5c 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3369,6 +3369,7 @@ __metadata: "@typescript-eslint/parser": "npm:^8.29.0" "@typescript-eslint/utils": "npm:^8.29.0" "@vitest/eslint-plugin": "npm:^1.6.5" + acorn: "npm:^8.15.0" chokidar: "npm:^4.0.1" depcheck: "npm:^1.4.7" eslint: "npm:^9.23.0" From cc26d6898400249e16620efcad14a38fc9ab3eb3 Mon Sep 17 00:00:00 2001 From: grypez <143971198+grypez@users.noreply.github.com> Date: Fri, 23 Jan 2026 16:54:40 -0600 Subject: [PATCH 10/10] test: reproduce bugbot claims --- packages/kernel-utils/src/vat-bundle.test.ts | 106 ++++++++++++++++++ .../src/vats/bundle-loader.test.ts | 79 +++++++++++++ 2 files changed, 185 insertions(+) create mode 100644 packages/kernel-utils/src/vat-bundle.test.ts create mode 100644 packages/ocap-kernel/src/vats/bundle-loader.test.ts diff --git a/packages/kernel-utils/src/vat-bundle.test.ts b/packages/kernel-utils/src/vat-bundle.test.ts new file mode 100644 index 000000000..fd16ca075 --- /dev/null +++ b/packages/kernel-utils/src/vat-bundle.test.ts @@ -0,0 +1,106 @@ +import { describe, it, expect } from 'vitest'; + +import { isVatBundle } from './vat-bundle.ts'; + +describe('isVatBundle', () => { + describe('valid bundles', () => { + it('accepts complete VatBundle with all properties', () => { + const bundle = { + moduleFormat: 'iife', + code: 'var __vatExports__ = {};', + exports: ['foo', 'bar'], + modules: { + './module.js': { renderedExports: ['a'], removedExports: ['b'] }, + }, + }; + expect(isVatBundle(bundle)).toBe(true); + }); + + it('accepts VatBundle with empty exports and modules', () => { + const bundle = { + moduleFormat: 'iife', + code: 'var __vatExports__ = {};', + exports: [], + modules: {}, + }; + expect(isVatBundle(bundle)).toBe(true); + }); + }); + + describe('invalid bundles - missing required properties', () => { + it('rejects object missing moduleFormat', () => { + const bundle = { + code: 'var __vatExports__ = {};', + exports: [], + modules: {}, + }; + expect(isVatBundle(bundle)).toBe(false); + }); + + it('rejects object missing code', () => { + const bundle = { + moduleFormat: 'iife', + exports: [], + modules: {}, + }; + expect(isVatBundle(bundle)).toBe(false); + }); + + // BUG: isVatBundle does not check for exports property + // See PR #763 bugbot claim #7 + it.fails('rejects object missing exports', () => { + const bundle = { + moduleFormat: 'iife', + code: 'var __vatExports__ = {};', + modules: {}, + }; + expect(isVatBundle(bundle)).toBe(false); + }); + + // BUG: isVatBundle does not check for modules property + // See PR #763 bugbot claim #7 + it.fails('rejects object missing modules', () => { + const bundle = { + moduleFormat: 'iife', + code: 'var __vatExports__ = {};', + exports: [], + }; + expect(isVatBundle(bundle)).toBe(false); + }); + }); + + describe('invalid bundles - wrong property types', () => { + it('rejects wrong moduleFormat value', () => { + const bundle = { + moduleFormat: 'cjs', + code: 'var __vatExports__ = {};', + exports: [], + modules: {}, + }; + expect(isVatBundle(bundle)).toBe(false); + }); + + it('rejects non-string code property', () => { + const bundle = { + moduleFormat: 'iife', + code: 123, + exports: [], + modules: {}, + }; + expect(isVatBundle(bundle)).toBe(false); + }); + }); + + describe('invalid bundles - non-objects', () => { + it.each` + label | value + ${'null'} | ${null} + ${'undefined'} | ${undefined} + ${'string'} | ${'not a bundle'} + ${'number'} | ${42} + ${'array'} | ${[]} + `('rejects $label', ({ value }) => { + expect(isVatBundle(value)).toBe(false); + }); + }); +}); diff --git a/packages/ocap-kernel/src/vats/bundle-loader.test.ts b/packages/ocap-kernel/src/vats/bundle-loader.test.ts new file mode 100644 index 000000000..e741e6660 --- /dev/null +++ b/packages/ocap-kernel/src/vats/bundle-loader.test.ts @@ -0,0 +1,79 @@ +import { describe, it, expect } from 'vitest'; + +import { loadBundle } from './bundle-loader.ts'; + +describe('loadBundle', () => { + describe('input validation', () => { + it('throws on unknown bundle format', () => { + const content = JSON.stringify({ moduleFormat: 'unknown' }); + expect(() => loadBundle(content)).toThrow( + 'Unknown bundle format: unknown', + ); + }); + + it('throws on missing moduleFormat', () => { + const content = JSON.stringify({}); + expect(() => loadBundle(content)).toThrow( + 'Unknown bundle format: undefined', + ); + }); + + // BUG: loadBundle does not validate code property before using it + // See PR #763 bugbot claim #2 + it.fails('throws on missing code property', () => { + const content = JSON.stringify({ moduleFormat: 'iife' }); + expect(() => loadBundle(content)).toThrow('Invalid bundle: missing code'); + }); + + // BUG: loadBundle does not validate code property before using it + // See PR #763 bugbot claim #2 + it.fails('throws on non-string code property', () => { + const content = JSON.stringify({ moduleFormat: 'iife', code: 123 }); + expect(() => loadBundle(content)).toThrow( + 'Invalid bundle: code must be a string', + ); + }); + }); + + describe('bundle evaluation', () => { + it('evaluates valid iife bundle and returns exports', () => { + const content = JSON.stringify({ + moduleFormat: 'iife', + code: 'var __vatExports__ = { foo: "bar" };', + }); + const result = loadBundle(content); + expect(result).toStrictEqual({ foo: 'bar' }); + }); + + it('provides harden global to compartment', () => { + const content = JSON.stringify({ + moduleFormat: 'iife', + code: 'var __vatExports__ = { hasHarden: typeof harden === "function" };', + }); + const result = loadBundle(content); + expect(result).toStrictEqual({ hasHarden: true }); + }); + + it('passes endowments to compartment', () => { + const content = JSON.stringify({ + moduleFormat: 'iife', + code: 'var __vatExports__ = { customValue: customEndowment };', + }); + const result = loadBundle(content, { + endowments: { customEndowment: 42 }, + }); + expect(result).toStrictEqual({ customValue: 42 }); + }); + + it('passes inescapableGlobalProperties to compartment', () => { + const content = JSON.stringify({ + moduleFormat: 'iife', + code: 'var __vatExports__ = { inescapableValue: globalProp };', + }); + const result = loadBundle(content, { + inescapableGlobalProperties: { globalProp: 'test' }, + }); + expect(result).toStrictEqual({ inescapableValue: 'test' }); + }); + }); +});