@esmx/import 3.0.0-rc.116 → 3.0.0-rc.118
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +8 -1
- package/README.zh-CN.md +13 -6
- package/dist/error.test.mjs +38 -0
- package/dist/import-loader.mjs +5 -2
- package/dist/import-vm.d.ts +4 -1
- package/dist/import-vm.mjs +133 -55
- package/dist/import-vm.test.d.ts +1 -0
- package/dist/import-vm.test.mjs +653 -0
- package/dist/index.d.ts +2 -2
- package/dist/types.d.ts +7 -0
- package/package.json +23 -6
- package/src/error.test.ts +46 -0
- package/src/import-loader.ts +5 -2
- package/src/import-vm.test.ts +755 -0
- package/src/import-vm.ts +222 -56
- package/src/index.ts +7 -2
- package/src/types.ts +8 -0
|
@@ -0,0 +1,755 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import os from 'node:os';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
|
|
5
|
+
import { createVmImport } from './import-vm';
|
|
6
|
+
|
|
7
|
+
describe('createVmImport', () => {
|
|
8
|
+
let tempDir: string;
|
|
9
|
+
let vmImport: ReturnType<typeof createVmImport>;
|
|
10
|
+
|
|
11
|
+
beforeEach(() => {
|
|
12
|
+
tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'import-vm-test-'));
|
|
13
|
+
vmImport = createVmImport(new URL(`file://${tempDir}/`));
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
afterEach(() => {
|
|
17
|
+
fs.rmSync(tempDir, { recursive: true, force: true });
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
describe('basic module loading', () => {
|
|
21
|
+
it('should load a simple ES module', async () => {
|
|
22
|
+
const modulePath = path.join(tempDir, 'simple.mjs');
|
|
23
|
+
fs.writeFileSync(
|
|
24
|
+
modulePath,
|
|
25
|
+
`export const value = 42;\nexport default 'hello';`
|
|
26
|
+
);
|
|
27
|
+
|
|
28
|
+
const namespace = await vmImport(
|
|
29
|
+
'./simple.mjs',
|
|
30
|
+
`file://${tempDir}/entry.mjs`
|
|
31
|
+
);
|
|
32
|
+
|
|
33
|
+
expect(namespace.value).toBe(42);
|
|
34
|
+
expect(namespace.default).toBe('hello');
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it('should load a module with multiple exports', async () => {
|
|
38
|
+
const modulePath = path.join(tempDir, 'math.mjs');
|
|
39
|
+
fs.writeFileSync(
|
|
40
|
+
modulePath,
|
|
41
|
+
`export const add = (a, b) => a + b;\nexport const mul = (a, b) => a * b;`
|
|
42
|
+
);
|
|
43
|
+
|
|
44
|
+
const namespace = await vmImport(
|
|
45
|
+
'./math.mjs',
|
|
46
|
+
`file://${tempDir}/entry.mjs`
|
|
47
|
+
);
|
|
48
|
+
|
|
49
|
+
expect(namespace.add(2, 3)).toBe(5);
|
|
50
|
+
expect(namespace.mul(4, 5)).toBe(20);
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it('should provide correct import.meta', async () => {
|
|
54
|
+
const modulePath = path.join(tempDir, 'meta.mjs');
|
|
55
|
+
fs.writeFileSync(
|
|
56
|
+
modulePath,
|
|
57
|
+
`export const url = import.meta.url;\nexport const filename = import.meta.filename;`
|
|
58
|
+
);
|
|
59
|
+
|
|
60
|
+
const namespace = await vmImport(
|
|
61
|
+
'./meta.mjs',
|
|
62
|
+
`file://${tempDir}/entry.mjs`
|
|
63
|
+
);
|
|
64
|
+
|
|
65
|
+
// Windows uses file:///C:/... with forward slashes
|
|
66
|
+
expect(namespace.url).toMatch(/^file:\/\/\//);
|
|
67
|
+
expect(namespace.url).toContain('meta.mjs');
|
|
68
|
+
expect(namespace.filename).toBe(modulePath);
|
|
69
|
+
});
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
describe('module dependencies', () => {
|
|
73
|
+
it('should load a module that imports another module', async () => {
|
|
74
|
+
fs.writeFileSync(
|
|
75
|
+
path.join(tempDir, 'helper.mjs'),
|
|
76
|
+
`export const helper = () => 'helper-value';`
|
|
77
|
+
);
|
|
78
|
+
|
|
79
|
+
fs.writeFileSync(
|
|
80
|
+
path.join(tempDir, 'main.mjs'),
|
|
81
|
+
`import { helper } from './helper.mjs';\nexport const result = helper();`
|
|
82
|
+
);
|
|
83
|
+
|
|
84
|
+
const namespace = await vmImport(
|
|
85
|
+
'./main.mjs',
|
|
86
|
+
`file://${tempDir}/entry.mjs`
|
|
87
|
+
);
|
|
88
|
+
|
|
89
|
+
expect(namespace.result).toBe('helper-value');
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
it('should load deeply nested module chains', async () => {
|
|
93
|
+
fs.writeFileSync(
|
|
94
|
+
path.join(tempDir, 'a.mjs'),
|
|
95
|
+
`export const a = 'A';`
|
|
96
|
+
);
|
|
97
|
+
|
|
98
|
+
fs.writeFileSync(
|
|
99
|
+
path.join(tempDir, 'b.mjs'),
|
|
100
|
+
`import { a } from './a.mjs';\nexport const b = a + 'B';`
|
|
101
|
+
);
|
|
102
|
+
|
|
103
|
+
fs.writeFileSync(
|
|
104
|
+
path.join(tempDir, 'c.mjs'),
|
|
105
|
+
`import { b } from './b.mjs';\nexport const c = b + 'C';`
|
|
106
|
+
);
|
|
107
|
+
|
|
108
|
+
const namespace = await vmImport(
|
|
109
|
+
'./c.mjs',
|
|
110
|
+
`file://${tempDir}/entry.mjs`
|
|
111
|
+
);
|
|
112
|
+
|
|
113
|
+
expect(namespace.c).toBe('ABC');
|
|
114
|
+
});
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
describe('circular dependencies', () => {
|
|
118
|
+
it('should handle A -> B -> A circular reference', async () => {
|
|
119
|
+
fs.writeFileSync(
|
|
120
|
+
path.join(tempDir, 'a.mjs'),
|
|
121
|
+
`import * as b from './b.mjs';\nexport const a = 'A';\nexport const getB = () => b.b;`
|
|
122
|
+
);
|
|
123
|
+
|
|
124
|
+
fs.writeFileSync(
|
|
125
|
+
path.join(tempDir, 'b.mjs'),
|
|
126
|
+
`import * as a from './a.mjs';\nexport const b = 'B';\nexport const getA = () => a.a;`
|
|
127
|
+
);
|
|
128
|
+
|
|
129
|
+
const namespace = await vmImport(
|
|
130
|
+
'./a.mjs',
|
|
131
|
+
`file://${tempDir}/entry.mjs`
|
|
132
|
+
);
|
|
133
|
+
|
|
134
|
+
expect(namespace.a).toBe('A');
|
|
135
|
+
expect(namespace.getB()).toBe('B');
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
it('should handle self-reference (A -> A)', async () => {
|
|
139
|
+
fs.writeFileSync(
|
|
140
|
+
path.join(tempDir, 'self.mjs'),
|
|
141
|
+
`import * as self from './self.mjs';\nexport const foo = 'bar';\nexport const hasSelf = () => typeof self === 'object';`
|
|
142
|
+
);
|
|
143
|
+
|
|
144
|
+
const namespace = await vmImport(
|
|
145
|
+
'./self.mjs',
|
|
146
|
+
`file://${tempDir}/entry.mjs`
|
|
147
|
+
);
|
|
148
|
+
|
|
149
|
+
expect(namespace.foo).toBe('bar');
|
|
150
|
+
expect(namespace.hasSelf()).toBe(true);
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
it('should handle three-way circular reference', async () => {
|
|
154
|
+
fs.writeFileSync(
|
|
155
|
+
path.join(tempDir, 'x.mjs'),
|
|
156
|
+
`import * as y from './y.mjs';\nexport const x = 'X';\nexport const getY = () => y.y;`
|
|
157
|
+
);
|
|
158
|
+
|
|
159
|
+
fs.writeFileSync(
|
|
160
|
+
path.join(tempDir, 'y.mjs'),
|
|
161
|
+
`import * as z from './z.mjs';\nexport const y = 'Y';\nexport const getZ = () => z.z;`
|
|
162
|
+
);
|
|
163
|
+
|
|
164
|
+
fs.writeFileSync(
|
|
165
|
+
path.join(tempDir, 'z.mjs'),
|
|
166
|
+
`import * as x from './x.mjs';\nexport const z = 'Z';\nexport const getX = () => x.x;`
|
|
167
|
+
);
|
|
168
|
+
|
|
169
|
+
const namespace = await vmImport(
|
|
170
|
+
'./x.mjs',
|
|
171
|
+
`file://${tempDir}/entry.mjs`
|
|
172
|
+
);
|
|
173
|
+
|
|
174
|
+
expect(namespace.x).toBe('X');
|
|
175
|
+
expect(namespace.getY()).toBe('Y');
|
|
176
|
+
});
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
describe('builtin modules', () => {
|
|
180
|
+
it('should load node:path module', async () => {
|
|
181
|
+
const namespace = await vmImport(
|
|
182
|
+
'node:path',
|
|
183
|
+
`file://${tempDir}/entry.mjs`
|
|
184
|
+
);
|
|
185
|
+
|
|
186
|
+
expect(typeof namespace.join).toBe('function');
|
|
187
|
+
// Use path.join to ensure platform-independent comparison
|
|
188
|
+
expect(namespace.join('/a', 'b')).toBe(path.join('/a', 'b'));
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
it('should load node:fs module', async () => {
|
|
192
|
+
const namespace = await vmImport(
|
|
193
|
+
'node:fs',
|
|
194
|
+
`file://${tempDir}/entry.mjs`
|
|
195
|
+
);
|
|
196
|
+
|
|
197
|
+
expect(typeof namespace.readFileSync).toBe('function');
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
it('should load node:os module', async () => {
|
|
201
|
+
const namespace = await vmImport(
|
|
202
|
+
'node:os',
|
|
203
|
+
`file://${tempDir}/entry.mjs`
|
|
204
|
+
);
|
|
205
|
+
|
|
206
|
+
expect(typeof namespace.platform).toBe('function');
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
it('should load builtin module as dependency of user module', async () => {
|
|
210
|
+
fs.writeFileSync(
|
|
211
|
+
path.join(tempDir, 'uses-path.mjs'),
|
|
212
|
+
`import { join } from 'node:path';\nexport const result = join('/test', 'file.txt');`
|
|
213
|
+
);
|
|
214
|
+
|
|
215
|
+
const namespace = await vmImport(
|
|
216
|
+
'./uses-path.mjs',
|
|
217
|
+
`file://${tempDir}/entry.mjs`
|
|
218
|
+
);
|
|
219
|
+
|
|
220
|
+
expect(namespace.result).toBe(path.join('/test', 'file.txt'));
|
|
221
|
+
});
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
describe('caching', () => {
|
|
225
|
+
it('should cache loaded modules within a single vmImport call', async () => {
|
|
226
|
+
fs.writeFileSync(
|
|
227
|
+
path.join(tempDir, 'counter.mjs'),
|
|
228
|
+
`export let count = 0;\nexport const increment = () => ++count;`
|
|
229
|
+
);
|
|
230
|
+
|
|
231
|
+
fs.writeFileSync(
|
|
232
|
+
path.join(tempDir, 'consumer.mjs'),
|
|
233
|
+
`import { count, increment } from './counter.mjs';\nexport const getCount = () => count;\nexport const inc = increment;`
|
|
234
|
+
);
|
|
235
|
+
|
|
236
|
+
const ns = await vmImport(
|
|
237
|
+
'./consumer.mjs',
|
|
238
|
+
`file://${tempDir}/entry.mjs`
|
|
239
|
+
);
|
|
240
|
+
|
|
241
|
+
// counter.mjs is loaded once and cached within this vmImport call
|
|
242
|
+
expect(ns.getCount()).toBe(0);
|
|
243
|
+
ns.inc();
|
|
244
|
+
expect(ns.getCount()).toBe(1);
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
it('should not share cache across separate vmImport calls', async () => {
|
|
248
|
+
fs.writeFileSync(
|
|
249
|
+
path.join(tempDir, 'state.mjs'),
|
|
250
|
+
`export let value = Math.random();`
|
|
251
|
+
);
|
|
252
|
+
|
|
253
|
+
const ns1 = await vmImport(
|
|
254
|
+
'./state.mjs',
|
|
255
|
+
`file://${tempDir}/a.mjs`
|
|
256
|
+
);
|
|
257
|
+
|
|
258
|
+
const ns2 = await vmImport(
|
|
259
|
+
'./state.mjs',
|
|
260
|
+
`file://${tempDir}/b.mjs`
|
|
261
|
+
);
|
|
262
|
+
|
|
263
|
+
// Each vmImport creates a new VM context with fresh cache
|
|
264
|
+
// So the module is evaluated separately
|
|
265
|
+
expect(ns1.value).not.toBe(ns2.value);
|
|
266
|
+
});
|
|
267
|
+
|
|
268
|
+
it('should cache shared dependencies within single vmImport call', async () => {
|
|
269
|
+
fs.writeFileSync(
|
|
270
|
+
path.join(tempDir, 'shared-dep.mjs'),
|
|
271
|
+
`export let counter = 0;\nexport const inc = () => ++counter;`
|
|
272
|
+
);
|
|
273
|
+
|
|
274
|
+
fs.writeFileSync(
|
|
275
|
+
path.join(tempDir, 'user-a.mjs'),
|
|
276
|
+
`import { counter, inc } from './shared-dep.mjs';\nexport const getCount = () => counter;\nexport const increment = inc;`
|
|
277
|
+
);
|
|
278
|
+
|
|
279
|
+
fs.writeFileSync(
|
|
280
|
+
path.join(tempDir, 'user-b.mjs'),
|
|
281
|
+
`import { counter, inc } from './shared-dep.mjs';\nexport const getCount = () => counter;\nexport const increment = inc;`
|
|
282
|
+
);
|
|
283
|
+
|
|
284
|
+
fs.writeFileSync(
|
|
285
|
+
path.join(tempDir, 'main-cache.mjs'),
|
|
286
|
+
`import * as a from './user-a.mjs';\nimport * as b from './user-b.mjs';\nexport const aCount = a.getCount;\nexport const bCount = b.getCount;\nexport const aInc = a.increment;\nexport const bInc = b.increment;`
|
|
287
|
+
);
|
|
288
|
+
|
|
289
|
+
const ns = await vmImport(
|
|
290
|
+
'./main-cache.mjs',
|
|
291
|
+
`file://${tempDir}/entry.mjs`
|
|
292
|
+
);
|
|
293
|
+
|
|
294
|
+
// Both users share the same shared-dep module instance
|
|
295
|
+
expect(ns.aCount()).toBe(0);
|
|
296
|
+
expect(ns.bCount()).toBe(0);
|
|
297
|
+
|
|
298
|
+
ns.aInc();
|
|
299
|
+
expect(ns.aCount()).toBe(1);
|
|
300
|
+
expect(ns.bCount()).toBe(1);
|
|
301
|
+
|
|
302
|
+
ns.bInc();
|
|
303
|
+
expect(ns.aCount()).toBe(2);
|
|
304
|
+
expect(ns.bCount()).toBe(2);
|
|
305
|
+
});
|
|
306
|
+
});
|
|
307
|
+
|
|
308
|
+
describe('dynamic import', () => {
|
|
309
|
+
it('should handle dynamic import() inside VM module', async () => {
|
|
310
|
+
fs.writeFileSync(
|
|
311
|
+
path.join(tempDir, 'lazy.mjs'),
|
|
312
|
+
`export const lazy = 'lazy-value';`
|
|
313
|
+
);
|
|
314
|
+
|
|
315
|
+
fs.writeFileSync(
|
|
316
|
+
path.join(tempDir, 'dynamic.mjs'),
|
|
317
|
+
`export const result = await import('./lazy.mjs').then(m => m.lazy);`
|
|
318
|
+
);
|
|
319
|
+
|
|
320
|
+
const namespace = await vmImport(
|
|
321
|
+
'./dynamic.mjs',
|
|
322
|
+
`file://${tempDir}/entry.mjs`
|
|
323
|
+
);
|
|
324
|
+
|
|
325
|
+
expect(namespace.result).toBe('lazy-value');
|
|
326
|
+
});
|
|
327
|
+
|
|
328
|
+
it('should handle dynamic import of already cached module', async () => {
|
|
329
|
+
fs.writeFileSync(
|
|
330
|
+
path.join(tempDir, 'dep.mjs'),
|
|
331
|
+
`export const value = 'dep-value';`
|
|
332
|
+
);
|
|
333
|
+
|
|
334
|
+
fs.writeFileSync(
|
|
335
|
+
path.join(tempDir, 'dynamic-cache.mjs'),
|
|
336
|
+
`import { value } from './dep.mjs';\nexport const staticValue = value;\nexport const dynamicValue = await import('./dep.mjs').then(m => m.value);`
|
|
337
|
+
);
|
|
338
|
+
|
|
339
|
+
const namespace = await vmImport(
|
|
340
|
+
'./dynamic-cache.mjs',
|
|
341
|
+
`file://${tempDir}/entry.mjs`
|
|
342
|
+
);
|
|
343
|
+
|
|
344
|
+
expect(namespace.staticValue).toBe('dep-value');
|
|
345
|
+
expect(namespace.dynamicValue).toBe('dep-value');
|
|
346
|
+
});
|
|
347
|
+
});
|
|
348
|
+
|
|
349
|
+
describe('error handling', () => {
|
|
350
|
+
it('should throw FileReadError for non-existent module', async () => {
|
|
351
|
+
await expect(
|
|
352
|
+
vmImport('./non-existent.mjs', `file://${tempDir}/entry.mjs`)
|
|
353
|
+
).rejects.toThrow('Failed to read module');
|
|
354
|
+
});
|
|
355
|
+
|
|
356
|
+
it('should throw for syntax errors in module', async () => {
|
|
357
|
+
fs.writeFileSync(
|
|
358
|
+
path.join(tempDir, 'bad-syntax.mjs'),
|
|
359
|
+
`export const x = {` // 语法错误
|
|
360
|
+
);
|
|
361
|
+
|
|
362
|
+
await expect(
|
|
363
|
+
vmImport('./bad-syntax.mjs', `file://${tempDir}/entry.mjs`)
|
|
364
|
+
).rejects.toThrow();
|
|
365
|
+
});
|
|
366
|
+
|
|
367
|
+
it('should throw for runtime errors during module evaluation', async () => {
|
|
368
|
+
fs.writeFileSync(
|
|
369
|
+
path.join(tempDir, 'runtime-error.mjs'),
|
|
370
|
+
`throw new Error('Runtime error');`
|
|
371
|
+
);
|
|
372
|
+
|
|
373
|
+
await expect(
|
|
374
|
+
vmImport('./runtime-error.mjs', `file://${tempDir}/entry.mjs`)
|
|
375
|
+
).rejects.toThrow('Runtime error');
|
|
376
|
+
});
|
|
377
|
+
});
|
|
378
|
+
|
|
379
|
+
describe('sandbox', () => {
|
|
380
|
+
it('should create isolated context', async () => {
|
|
381
|
+
fs.writeFileSync(
|
|
382
|
+
path.join(tempDir, 'global.mjs'),
|
|
383
|
+
`if (typeof globalThis.customVar === 'undefined') {\n globalThis.customVar = 'set';\n}\nexport const hasVar = globalThis.customVar;`
|
|
384
|
+
);
|
|
385
|
+
|
|
386
|
+
const ns1 = await vmImport(
|
|
387
|
+
'./global.mjs',
|
|
388
|
+
`file://${tempDir}/entry.mjs`,
|
|
389
|
+
{}
|
|
390
|
+
);
|
|
391
|
+
|
|
392
|
+
expect(ns1.hasVar).toBe('set');
|
|
393
|
+
|
|
394
|
+
// Second import with fresh context
|
|
395
|
+
const ns2 = await vmImport(
|
|
396
|
+
'./global.mjs',
|
|
397
|
+
`file://${tempDir}/entry.mjs`,
|
|
398
|
+
{}
|
|
399
|
+
);
|
|
400
|
+
|
|
401
|
+
// Fresh context - variable should not exist
|
|
402
|
+
expect(ns2.hasVar).toBe('set'); // Actually it will be 'set' because module is cached
|
|
403
|
+
});
|
|
404
|
+
});
|
|
405
|
+
|
|
406
|
+
describe('import map', () => {
|
|
407
|
+
it('should resolve specifiers using import map', async () => {
|
|
408
|
+
fs.writeFileSync(
|
|
409
|
+
path.join(tempDir, 'lib.mjs'),
|
|
410
|
+
`export const lib = 'library';`
|
|
411
|
+
);
|
|
412
|
+
|
|
413
|
+
const importMap = {
|
|
414
|
+
imports: {
|
|
415
|
+
'#lib': `file://${tempDir}/lib.mjs`
|
|
416
|
+
}
|
|
417
|
+
};
|
|
418
|
+
|
|
419
|
+
const vmImportWithMap = createVmImport(
|
|
420
|
+
new URL(`file://${tempDir}/`),
|
|
421
|
+
importMap
|
|
422
|
+
);
|
|
423
|
+
|
|
424
|
+
fs.writeFileSync(
|
|
425
|
+
path.join(tempDir, 'app.mjs'),
|
|
426
|
+
`import { lib } from '#lib';\nexport const result = lib;`
|
|
427
|
+
);
|
|
428
|
+
|
|
429
|
+
const namespace = await vmImportWithMap(
|
|
430
|
+
'./app.mjs',
|
|
431
|
+
`file://${tempDir}/entry.mjs`
|
|
432
|
+
);
|
|
433
|
+
|
|
434
|
+
expect(namespace.result).toBe('library');
|
|
435
|
+
});
|
|
436
|
+
});
|
|
437
|
+
|
|
438
|
+
describe('in-memory filesystem', () => {
|
|
439
|
+
let memfs: Map<string, string>;
|
|
440
|
+
let memImport: ReturnType<typeof createVmImport>;
|
|
441
|
+
const memBase =
|
|
442
|
+
process.platform === 'win32' ? 'C:\\project' : '/project';
|
|
443
|
+
const memBaseURL = new URL(`file://${memBase}/`);
|
|
444
|
+
const memEntry = `file://${memBase}/entry.mjs`;
|
|
445
|
+
|
|
446
|
+
const memPath = (filename: string) => path.join(memBase, filename);
|
|
447
|
+
|
|
448
|
+
const createMemRead = () => (filepath: string) => {
|
|
449
|
+
const content = memfs.get(filepath);
|
|
450
|
+
if (content === undefined) {
|
|
451
|
+
const err = new Error(
|
|
452
|
+
`ENOENT: no such file or directory, open '${filepath}'`
|
|
453
|
+
);
|
|
454
|
+
(err as any).code = 'ENOENT';
|
|
455
|
+
throw err;
|
|
456
|
+
}
|
|
457
|
+
return content;
|
|
458
|
+
};
|
|
459
|
+
|
|
460
|
+
beforeEach(() => {
|
|
461
|
+
memfs = new Map();
|
|
462
|
+
memImport = createVmImport(
|
|
463
|
+
memBaseURL,
|
|
464
|
+
{},
|
|
465
|
+
{
|
|
466
|
+
readFileSync: createMemRead()
|
|
467
|
+
}
|
|
468
|
+
);
|
|
469
|
+
});
|
|
470
|
+
|
|
471
|
+
it('should load a simple ES module from memory', async () => {
|
|
472
|
+
memfs.set(memPath('simple.mjs'), `export const value = 42;`);
|
|
473
|
+
|
|
474
|
+
const namespace = await memImport('./simple.mjs', memEntry);
|
|
475
|
+
|
|
476
|
+
expect(namespace.value).toBe(42);
|
|
477
|
+
});
|
|
478
|
+
|
|
479
|
+
it('should load a module with multiple exports from memory', async () => {
|
|
480
|
+
memfs.set(
|
|
481
|
+
memPath('math.mjs'),
|
|
482
|
+
`export const add = (a, b) => a + b;\nexport const mul = (a, b) => a * b;`
|
|
483
|
+
);
|
|
484
|
+
|
|
485
|
+
const namespace = await memImport('./math.mjs', memEntry);
|
|
486
|
+
|
|
487
|
+
expect(namespace.add(2, 3)).toBe(5);
|
|
488
|
+
expect(namespace.mul(4, 5)).toBe(20);
|
|
489
|
+
});
|
|
490
|
+
|
|
491
|
+
it('should load a module that imports another module from memory', async () => {
|
|
492
|
+
memfs.set(
|
|
493
|
+
memPath('helper.mjs'),
|
|
494
|
+
`export const helper = () => 'helper-value';`
|
|
495
|
+
);
|
|
496
|
+
|
|
497
|
+
memfs.set(
|
|
498
|
+
memPath('main.mjs'),
|
|
499
|
+
`import { helper } from './helper.mjs';\nexport const result = helper();`
|
|
500
|
+
);
|
|
501
|
+
|
|
502
|
+
const namespace = await memImport('./main.mjs', memEntry);
|
|
503
|
+
|
|
504
|
+
expect(namespace.result).toBe('helper-value');
|
|
505
|
+
});
|
|
506
|
+
|
|
507
|
+
it('should handle deeply nested module chains from memory', async () => {
|
|
508
|
+
memfs.set(memPath('a.mjs'), `export const a = 'A';`);
|
|
509
|
+
|
|
510
|
+
memfs.set(
|
|
511
|
+
memPath('b.mjs'),
|
|
512
|
+
`import { a } from './a.mjs';\nexport const b = a + 'B';`
|
|
513
|
+
);
|
|
514
|
+
|
|
515
|
+
memfs.set(
|
|
516
|
+
memPath('c.mjs'),
|
|
517
|
+
`import { b } from './b.mjs';\nexport const c = b + 'C';`
|
|
518
|
+
);
|
|
519
|
+
|
|
520
|
+
const namespace = await memImport('./c.mjs', memEntry);
|
|
521
|
+
|
|
522
|
+
expect(namespace.c).toBe('ABC');
|
|
523
|
+
});
|
|
524
|
+
|
|
525
|
+
it('should handle A -> B -> A circular reference from memory', async () => {
|
|
526
|
+
memfs.set(
|
|
527
|
+
memPath('a.mjs'),
|
|
528
|
+
`import * as b from './b.mjs';\nexport const a = 'A';\nexport const getB = () => b.b;`
|
|
529
|
+
);
|
|
530
|
+
|
|
531
|
+
memfs.set(
|
|
532
|
+
memPath('b.mjs'),
|
|
533
|
+
`import * as a from './a.mjs';\nexport const b = 'B';\nexport const getA = () => a.a;`
|
|
534
|
+
);
|
|
535
|
+
|
|
536
|
+
const namespace = await memImport('./a.mjs', memEntry);
|
|
537
|
+
|
|
538
|
+
expect(namespace.a).toBe('A');
|
|
539
|
+
expect(namespace.getB()).toBe('B');
|
|
540
|
+
});
|
|
541
|
+
|
|
542
|
+
it('should cache loaded modules within a single memImport call', async () => {
|
|
543
|
+
memfs.set(
|
|
544
|
+
memPath('counter.mjs'),
|
|
545
|
+
`export let count = 0;\nexport const increment = () => ++count;`
|
|
546
|
+
);
|
|
547
|
+
|
|
548
|
+
memfs.set(
|
|
549
|
+
memPath('consumer.mjs'),
|
|
550
|
+
`import { count, increment } from './counter.mjs';\nexport const getCount = () => count;\nexport const inc = increment;`
|
|
551
|
+
);
|
|
552
|
+
|
|
553
|
+
const ns = await memImport('./consumer.mjs', memEntry);
|
|
554
|
+
|
|
555
|
+
expect(ns.getCount()).toBe(0);
|
|
556
|
+
ns.inc();
|
|
557
|
+
expect(ns.getCount()).toBe(1);
|
|
558
|
+
});
|
|
559
|
+
|
|
560
|
+
it('should handle dynamic import() inside VM module from memory', async () => {
|
|
561
|
+
memfs.set(memPath('lazy.mjs'), `export const lazy = 'lazy-value';`);
|
|
562
|
+
|
|
563
|
+
memfs.set(
|
|
564
|
+
memPath('dynamic.mjs'),
|
|
565
|
+
`export const result = await import('./lazy.mjs').then(m => m.lazy);`
|
|
566
|
+
);
|
|
567
|
+
|
|
568
|
+
const namespace = await memImport('./dynamic.mjs', memEntry);
|
|
569
|
+
|
|
570
|
+
expect(namespace.result).toBe('lazy-value');
|
|
571
|
+
});
|
|
572
|
+
|
|
573
|
+
it('should throw FileReadError for non-existent module in memory', async () => {
|
|
574
|
+
await expect(
|
|
575
|
+
memImport('./non-existent.mjs', memEntry)
|
|
576
|
+
).rejects.toThrow('Failed to read module');
|
|
577
|
+
});
|
|
578
|
+
|
|
579
|
+
it('should resolve specifiers using import map with memory fs', async () => {
|
|
580
|
+
memfs.set(memPath('lib.mjs'), `export const lib = 'library';`);
|
|
581
|
+
|
|
582
|
+
const importMap = {
|
|
583
|
+
imports: {
|
|
584
|
+
'#lib': `file://${memBase}/lib.mjs`
|
|
585
|
+
}
|
|
586
|
+
};
|
|
587
|
+
|
|
588
|
+
const vmImportWithMap = createVmImport(memBaseURL, importMap, {
|
|
589
|
+
readFileSync: createMemRead()
|
|
590
|
+
});
|
|
591
|
+
|
|
592
|
+
memfs.set(
|
|
593
|
+
memPath('app.mjs'),
|
|
594
|
+
`import { lib } from '#lib';\nexport const result = lib;`
|
|
595
|
+
);
|
|
596
|
+
|
|
597
|
+
const namespace = await vmImportWithMap('./app.mjs', memEntry);
|
|
598
|
+
|
|
599
|
+
expect(namespace.result).toBe('library');
|
|
600
|
+
});
|
|
601
|
+
|
|
602
|
+
it('should provide correct import.meta with memory fs', async () => {
|
|
603
|
+
memfs.set(
|
|
604
|
+
memPath('meta.mjs'),
|
|
605
|
+
`export const url = import.meta.url;\nexport const filename = import.meta.filename;`
|
|
606
|
+
);
|
|
607
|
+
|
|
608
|
+
const namespace = await memImport('./meta.mjs', memEntry);
|
|
609
|
+
|
|
610
|
+
expect(namespace.url).toBe(new URL('meta.mjs', memBaseURL).href);
|
|
611
|
+
expect(namespace.filename).toBe(memPath('meta.mjs'));
|
|
612
|
+
});
|
|
613
|
+
});
|
|
614
|
+
|
|
615
|
+
/**
|
|
616
|
+
* Style assets are part of esmx's federation contract (manifest's
|
|
617
|
+
* `chunks[*].css[]`). On the server they must NOT be parsed as JS — the
|
|
618
|
+
* VM linker intercepts them, returns a SyntheticModule exposing the asset
|
|
619
|
+
* URL, and lets `import './x.css'` succeed without side effects (the
|
|
620
|
+
* actual stylesheet reaches the browser via a `<link>` tag the host emits
|
|
621
|
+
* from the manifest, not by evaluating this module).
|
|
622
|
+
*/
|
|
623
|
+
describe('style asset imports', () => {
|
|
624
|
+
it('does not parse .css as JS — side-effect import resolves cleanly', async () => {
|
|
625
|
+
const cssPath = path.join(tempDir, 'styles.css');
|
|
626
|
+
fs.writeFileSync(
|
|
627
|
+
cssPath,
|
|
628
|
+
'/* contains a dot selector — would crash the JS parser */\n.btn { color: red; }'
|
|
629
|
+
);
|
|
630
|
+
const consumerPath = path.join(tempDir, 'consumer.mjs');
|
|
631
|
+
fs.writeFileSync(
|
|
632
|
+
consumerPath,
|
|
633
|
+
`import './styles.css';\nexport const ok = true;`
|
|
634
|
+
);
|
|
635
|
+
|
|
636
|
+
const namespace = await vmImport(
|
|
637
|
+
'./consumer.mjs',
|
|
638
|
+
`file://${tempDir}/entry.mjs`
|
|
639
|
+
);
|
|
640
|
+
|
|
641
|
+
expect(namespace.ok).toBe(true);
|
|
642
|
+
});
|
|
643
|
+
|
|
644
|
+
it('exposes the asset URL as default + href named export', async () => {
|
|
645
|
+
const cssPath = path.join(tempDir, 'theme.css');
|
|
646
|
+
fs.writeFileSync(cssPath, ':root { --x: 1; }');
|
|
647
|
+
const consumerPath = path.join(tempDir, 'consumer.mjs');
|
|
648
|
+
fs.writeFileSync(
|
|
649
|
+
consumerPath,
|
|
650
|
+
`import url, { href } from './theme.css';\nexport { url, href };`
|
|
651
|
+
);
|
|
652
|
+
|
|
653
|
+
const namespace = await vmImport(
|
|
654
|
+
'./consumer.mjs',
|
|
655
|
+
`file://${tempDir}/entry.mjs`
|
|
656
|
+
);
|
|
657
|
+
|
|
658
|
+
const expected = new URL('theme.css', `file://${tempDir}/`).href;
|
|
659
|
+
expect(namespace.url).toBe(expected);
|
|
660
|
+
expect(namespace.href).toBe(expected);
|
|
661
|
+
});
|
|
662
|
+
|
|
663
|
+
it.each([
|
|
664
|
+
'scss',
|
|
665
|
+
'sass',
|
|
666
|
+
'less',
|
|
667
|
+
'stylus',
|
|
668
|
+
'styl',
|
|
669
|
+
'pcss',
|
|
670
|
+
'postcss'
|
|
671
|
+
])('treats .%s as a style asset', async (ext) => {
|
|
672
|
+
const assetPath = path.join(tempDir, `styles.${ext}`);
|
|
673
|
+
fs.writeFileSync(
|
|
674
|
+
assetPath,
|
|
675
|
+
'.x { color: red; } // would crash JS parser'
|
|
676
|
+
);
|
|
677
|
+
const consumerPath = path.join(tempDir, `c-${ext}.mjs`);
|
|
678
|
+
fs.writeFileSync(
|
|
679
|
+
consumerPath,
|
|
680
|
+
`import './styles.${ext}';\nexport const ok = true;`
|
|
681
|
+
);
|
|
682
|
+
|
|
683
|
+
const namespace = await vmImport(
|
|
684
|
+
`./c-${ext}.mjs`,
|
|
685
|
+
`file://${tempDir}/entry.mjs`
|
|
686
|
+
);
|
|
687
|
+
|
|
688
|
+
expect(namespace.ok).toBe(true);
|
|
689
|
+
});
|
|
690
|
+
|
|
691
|
+
it('tolerates query-string suffixes (?inline, ?url)', async () => {
|
|
692
|
+
const cssPath = path.join(tempDir, 'q.css');
|
|
693
|
+
fs.writeFileSync(cssPath, '.q {}');
|
|
694
|
+
const consumerPath = path.join(tempDir, 'qc.mjs');
|
|
695
|
+
// The on-disk file is `q.css`; the import specifier carries a
|
|
696
|
+
// bundler-style suffix. Node's import.meta.resolve normalises this
|
|
697
|
+
// to a file URL with the query preserved.
|
|
698
|
+
fs.writeFileSync(
|
|
699
|
+
consumerPath,
|
|
700
|
+
`import './q.css?inline';\nexport const ok = true;`
|
|
701
|
+
);
|
|
702
|
+
|
|
703
|
+
const namespace = await vmImport(
|
|
704
|
+
'./qc.mjs',
|
|
705
|
+
`file://${tempDir}/entry.mjs`
|
|
706
|
+
);
|
|
707
|
+
|
|
708
|
+
expect(namespace.ok).toBe(true);
|
|
709
|
+
});
|
|
710
|
+
|
|
711
|
+
it('caches style modules — same URL imported twice yields one module', async () => {
|
|
712
|
+
const cssPath = path.join(tempDir, 'shared.css');
|
|
713
|
+
fs.writeFileSync(cssPath, '.s {}');
|
|
714
|
+
const aPath = path.join(tempDir, 'a.mjs');
|
|
715
|
+
const bPath = path.join(tempDir, 'b.mjs');
|
|
716
|
+
const rootPath = path.join(tempDir, 'root.mjs');
|
|
717
|
+
fs.writeFileSync(
|
|
718
|
+
aPath,
|
|
719
|
+
`import url from './shared.css';\nexport { url as a };`
|
|
720
|
+
);
|
|
721
|
+
fs.writeFileSync(
|
|
722
|
+
bPath,
|
|
723
|
+
`import url from './shared.css';\nexport { url as b };`
|
|
724
|
+
);
|
|
725
|
+
fs.writeFileSync(
|
|
726
|
+
rootPath,
|
|
727
|
+
`export { a } from './a.mjs';\nexport { b } from './b.mjs';`
|
|
728
|
+
);
|
|
729
|
+
|
|
730
|
+
const namespace = await vmImport(
|
|
731
|
+
'./root.mjs',
|
|
732
|
+
`file://${tempDir}/entry.mjs`
|
|
733
|
+
);
|
|
734
|
+
|
|
735
|
+
expect(namespace.a).toBeDefined();
|
|
736
|
+
expect(namespace.a).toBe(namespace.b);
|
|
737
|
+
});
|
|
738
|
+
|
|
739
|
+
it('does not affect non-style imports', async () => {
|
|
740
|
+
const jsPath = path.join(tempDir, 'normal.mjs');
|
|
741
|
+
fs.writeFileSync(
|
|
742
|
+
jsPath,
|
|
743
|
+
'export const value = 42;\nexport default "x";'
|
|
744
|
+
);
|
|
745
|
+
|
|
746
|
+
const namespace = await vmImport(
|
|
747
|
+
'./normal.mjs',
|
|
748
|
+
`file://${tempDir}/entry.mjs`
|
|
749
|
+
);
|
|
750
|
+
|
|
751
|
+
expect(namespace.value).toBe(42);
|
|
752
|
+
expect(namespace.default).toBe('x');
|
|
753
|
+
});
|
|
754
|
+
});
|
|
755
|
+
});
|