Skip to content

Commit 513a179

Browse files
sirtimidclaude
andcommitted
fix: address PR review findings for Vite v8 migration
- Export and add unit tests for removeDynamicImportsPlugin (16 tests) - Add this.warn() when dynamic imports cannot be replaced - Replace definite-assignment mutex with makePromiseKit - Move exports: {} after endowment spreads to prevent accidental override - Fix inaccurate comments (remove self, clarify ESM interop trigger) - Add bundle-loader endowment tests for exports, globalThis, global Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 59f8fcc commit 513a179

5 files changed

Lines changed: 128 additions & 16 deletions

File tree

packages/kernel-utils/src/vite-plugins/bundle-vat.ts

Lines changed: 14 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { makePromiseKit } from '@endo/promise-kit';
12
import type {
23
OutputChunk,
34
Plugin as RolldownPlugin,
@@ -21,14 +22,18 @@ export type { VatBundle } from '../vat-bundle.ts';
2122
* dynamic imports from the code so that Rolldown accepts the IIFE format.
2223
*
2324
* Pattern transformed:
24-
* `import('specifier')` → `Promise.resolve({})`
25+
* `import('specifier')` / `import("specifier")` / `` import(`specifier`) ``
26+
* → `Promise.resolve({})`
27+
*
28+
* Only replaces calls with literal string specifiers. Dynamic expressions
29+
* like `import(variable)` are left untouched.
2530
*
2631
* @returns A Rolldown plugin.
2732
*/
28-
function removeDynamicImportsPlugin(): RolldownPlugin {
33+
export function removeDynamicImportsPlugin(): RolldownPlugin {
2934
return {
3035
name: 'ocap-kernel:remove-dynamic-imports',
31-
transform(code) {
36+
transform(code, id) {
3237
if (!/\bimport\s*\(/u.test(code)) {
3338
return null;
3439
}
@@ -39,6 +44,10 @@ function removeDynamicImportsPlugin(): RolldownPlugin {
3944
);
4045

4146
if (transformed === code) {
47+
this.warn(
48+
`Module "${id}" contains dynamic import() expressions that could not ` +
49+
`be replaced (e.g. computed specifiers). Rolldown may reject IIFE output.`,
50+
);
4251
return null;
4352
}
4453

@@ -66,10 +75,8 @@ export async function bundleVat(sourcePath: string): Promise<VatBundle> {
6675
// Wait for any in-flight build to finish before starting ours, then
6776
// register a slot so the next caller waits for us.
6877
const prevQueue = buildQueue;
69-
let releaseLock!: () => void;
70-
buildQueue = new Promise<void>((resolve) => {
71-
releaseLock = resolve;
72-
});
78+
const { promise, resolve: releaseLock } = makePromiseKit<void>();
79+
buildQueue = promise;
7380

7481
let result: Awaited<ReturnType<typeof build>>;
7582
try {

packages/kernel-utils/src/vite-plugins/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import type { Plugin } from 'vite';
22

33
import { bundleVat } from './bundle-vat.ts';
44

5-
export { bundleVat } from './bundle-vat.ts';
5+
export { bundleVat, removeDynamicImportsPlugin } from './bundle-vat.ts';
66
export type { VatBundle } from './bundle-vat.ts';
77

88
type VatEntry = {
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
import { describe, it, expect, vi } from 'vitest';
2+
3+
import { removeDynamicImportsPlugin } from './bundle-vat.ts';
4+
5+
type TransformFn = (
6+
code: string,
7+
id: string,
8+
) => { code: string; map: null } | null;
9+
10+
describe('removeDynamicImportsPlugin', () => {
11+
const plugin = removeDynamicImportsPlugin();
12+
const transform = (plugin.transform as TransformFn).bind({
13+
warn: vi.fn(),
14+
});
15+
16+
it.each([
17+
['single-quoted import', `import('module')`, `Promise.resolve({})`],
18+
['double-quoted import', `import("module")`, `Promise.resolve({})`],
19+
['backtick-quoted import', 'import(`module`)', `Promise.resolve({})`],
20+
[
21+
'import with whitespace before paren',
22+
`import ('module')`,
23+
`Promise.resolve({})`,
24+
],
25+
[
26+
'import with whitespace around specifier',
27+
`import( 'module' )`,
28+
`Promise.resolve({})`,
29+
],
30+
[
31+
'multiple dynamic imports in one file',
32+
`const a = import('foo');\nconst b = import("bar");`,
33+
`const a = Promise.resolve({});\nconst b = Promise.resolve({});`,
34+
],
35+
[
36+
'dynamic import surrounded by other code',
37+
`const x = 1;\nawait import('lazy-util');\nconst y = 2;`,
38+
`const x = 1;\nawait Promise.resolve({});\nconst y = 2;`,
39+
],
40+
])('replaces %s', (_name, code, expected) => {
41+
const result = transform(code, 'test.ts');
42+
expect(result).toStrictEqual({ code: expected, map: null });
43+
});
44+
45+
it.each([
46+
['no import at all', 'const x = 1;'],
47+
['ESM import statement', `import foo from 'bar';`],
48+
['ESM named import', `import { foo } from 'bar';`],
49+
['empty code', ''],
50+
])('returns null for %s (fast-path miss)', (_name, code) => {
51+
expect(transform(code, 'test.ts')).toBeNull();
52+
});
53+
54+
it.each([
55+
['computed specifier (variable)', `import(variable)`],
56+
['computed specifier (expression)', `import(getPath())`],
57+
['computed specifier (concatenation)', `import('./locales/' + lang)`],
58+
])('returns null for %s (no literal match)', (_name, code) => {
59+
expect(transform(code, 'test.ts')).toBeNull();
60+
});
61+
62+
it('does not match reimport or similar substrings', () => {
63+
const code = `reimport('foo')`;
64+
expect(transform(code, 'test.ts')).toBeNull();
65+
});
66+
67+
it('warns when fast-path matches but replacement regex does not', () => {
68+
const warn = vi.fn();
69+
const boundTransform = (plugin.transform as TransformFn).bind({
70+
warn,
71+
});
72+
73+
boundTransform(`import(variable)`, 'test.ts');
74+
75+
expect(warn).toHaveBeenCalledWith(
76+
expect.stringContaining('could not be replaced'),
77+
);
78+
});
79+
});

packages/ocap-kernel/src/vats/bundle-loader.test.ts

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,5 +85,32 @@ describe('loadBundle', () => {
8585
});
8686
expect(result).toStrictEqual({ inescapableValue: 'test' });
8787
});
88+
89+
it('provides exports object to compartment', () => {
90+
const content = JSON.stringify({
91+
moduleFormat: 'iife',
92+
code: 'Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" }); var __vatExports__ = { ok: true };',
93+
});
94+
const result = loadBundle(content);
95+
expect(result).toStrictEqual({ ok: true });
96+
});
97+
98+
it('provides globalThis on compartment global', () => {
99+
const content = JSON.stringify({
100+
moduleFormat: 'iife',
101+
code: 'var __vatExports__ = { hasGlobalThis: typeof globalThis === "object" && globalThis !== undefined };',
102+
});
103+
const result = loadBundle(content);
104+
expect(result).toStrictEqual({ hasGlobalThis: true });
105+
});
106+
107+
it('provides global on compartment global', () => {
108+
const content = JSON.stringify({
109+
moduleFormat: 'iife',
110+
code: 'var __vatExports__ = { hasGlobal: typeof global === "object" && global !== undefined };',
111+
});
112+
const result = loadBundle(content);
113+
expect(result).toStrictEqual({ hasGlobal: true });
114+
});
88115
});
89116
});

packages/ocap-kernel/src/vats/bundle-loader.ts

Lines changed: 7 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -38,19 +38,18 @@ export function loadBundle(
3838
const compartment = new Compartment({
3939
// SES globals that may be used by bundled code
4040
harden: globalThis.harden,
41-
// Rolldown adds `Object.defineProperty(exports, Symbol.toStringTag, ...)` to
42-
// IIFE bundles for modules with no named exports. Provide an empty object so
43-
// that call does not throw in the Compartment.
44-
exports: {},
4541
...endowments,
4642
...inescapableGlobalProperties,
43+
// Rolldown adds `Object.defineProperty(exports, Symbol.toStringTag, ...)` to
44+
// IIFE bundles as part of ESM interop. This MUST come after endowment spreads
45+
// to ensure it is not accidentally overridden.
46+
exports: {},
4747
});
4848
// Rolldown-generated CJS helpers use `var localThis = globalThis`, and
4949
// some bundled libraries (e.g. lodash) detect the global via
50-
// `typeof global == "object" && global` or `Function("return this")()`.
51-
// None of these work in a SES Compartment out of the box:
52-
// - `global` / `self` are not compartment bindings
53-
// - `Function("return this")()` returns `undefined` in strict mode
50+
// `typeof global == "object" && global`.
51+
// Neither `globalThis` nor `global` are available in a SES Compartment by
52+
// default — only hardened intrinsics are exposed as named bindings.
5453
// Inject these properties on the compartment's own global so that both
5554
// patterns resolve to the compartment's global (which has all the intrinsics).
5655
const cg = compartment.globalThis as Record<string, unknown>;

0 commit comments

Comments
 (0)