@esmx/import 3.0.0-rc.117 → 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/src/import-vm.ts CHANGED
@@ -1,12 +1,73 @@
1
1
  import fs from 'node:fs';
2
- import { isBuiltin } from 'node:module';
2
+ import { createRequire, isBuiltin } from 'node:module';
3
3
  import path from 'node:path';
4
4
  import { fileURLToPath } from 'node:url';
5
5
  import vm from 'node:vm';
6
- import { CircularDependencyError, FileReadError } from './error';
6
+ import { FileReadError } from './error';
7
7
  import { createImportMapResolver } from './import-map-resolve';
8
8
  import type { ImportMap } from './types';
9
9
 
10
+ const requireSync = createRequire(import.meta.url);
11
+
12
+ /**
13
+ * Cache value type. Style-asset imports (`*.css`, `*.scss`, etc.) materialize
14
+ * as a `SyntheticModule` exposing the asset URL; everything else parses through
15
+ * `SourceTextModule`. Both inherit from the (internal) base `vm.Module` and
16
+ * expose the `namespace` getter the public API ultimately returns.
17
+ */
18
+ type CachedVmModule = vm.SourceTextModule | vm.SyntheticModule;
19
+
20
+ /**
21
+ * Extensions that the esmx federation contract treats as STYLE ASSETS rather
22
+ * than executable JS modules. On the server they materialize as no-op synthetic
23
+ * modules exposing the asset URL — they have no observable side effect there,
24
+ * because there is no document. Browsers receive the stylesheet via a
25
+ * `<link rel="stylesheet">` that the host injects from the federation manifest
26
+ * (`chunks[*].css[]`), not by evaluating these modules. See
27
+ * `docs/design/principles.md` P1 and the G section of the redesign plan.
28
+ *
29
+ * Query strings (`?inline`, `?url`) are tolerated — bundlers sometimes attach
30
+ * them when rewriting CSS imports.
31
+ */
32
+ const STYLE_ASSET_RE =
33
+ /\.(?:css|scss|sass|less|stylus|styl|pcss|postcss)(?:\?.*)?$/i;
34
+
35
+ function isStyleAsset(url: string): boolean {
36
+ return STYLE_ASSET_RE.test(url);
37
+ }
38
+
39
+ let lazyGlobalsMaterialized = false;
40
+
41
+ /**
42
+ * Materializes Node's lazily-defined `DOMException` global in the main realm
43
+ * before a vm SSR context is created.
44
+ *
45
+ * Node installs `DOMException` as a lazy getter on the global object. When that
46
+ * getter is first invoked from inside a vm context — which happens when an SSR
47
+ * library copies/enumerates `globalThis` during rendering (e.g. `@lit-labs/ssr`
48
+ * runs `Object.assign(globalThis, window)`) — Node hits an `isolate_data`
49
+ * native assertion and aborts the entire process, because vm contexts do not
50
+ * carry Node's per-context data.
51
+ *
52
+ * This is a robustness property of esmx's own vm execution sandbox, not a shim
53
+ * for any specific framework: reading the global here, in the main realm,
54
+ * replaces the lazy getter with a plain data property on the shared global so
55
+ * code running inside the vm only ever observes a concrete value. It runs once
56
+ * and has no observable effect beyond eagerly instantiating a global that would
57
+ * otherwise be created on first access anyway.
58
+ */
59
+ function materializeLazyGlobals(): void {
60
+ if (lazyGlobalsMaterialized) {
61
+ return;
62
+ }
63
+ lazyGlobalsMaterialized = true;
64
+ void globalThis.DOMException;
65
+ }
66
+
67
+ export interface VmImportOptions {
68
+ readFileSync?: (path: string) => string;
69
+ }
70
+
10
71
  async function importBuiltinModule(specifier: string, context: vm.Context) {
11
72
  const nodeModule = await import(specifier);
12
73
  const keys = Object.keys(nodeModule);
@@ -29,7 +90,13 @@ async function importBuiltinModule(specifier: string, context: vm.Context) {
29
90
  return module;
30
91
  }
31
92
 
32
- export function createVmImport(baseURL: URL, importMap: ImportMap = {}) {
93
+ export function createVmImport(
94
+ baseURL: URL,
95
+ importMap: ImportMap = {},
96
+ options?: VmImportOptions
97
+ ) {
98
+ const readFileSync =
99
+ options?.readFileSync ?? ((path) => fs.readFileSync(path, 'utf-8'));
33
100
  const importMapResolver = createImportMapResolver(baseURL.href, importMap);
34
101
  const buildMeta = (specifier: string, parent: string): ImportMeta => {
35
102
  const result = importMapResolver(specifier, parent);
@@ -46,95 +113,194 @@ export function createVmImport(baseURL: URL, importMap: ImportMap = {}) {
46
113
  }
47
114
  };
48
115
  };
49
- async function moduleLinker(
116
+
117
+ function syncLinker(
50
118
  specifier: string,
51
119
  parent: string,
52
120
  context: vm.Context,
53
- cache: Map<string, Promise<vm.SourceTextModule>>,
121
+ cache: Map<string, CachedVmModule>,
122
+ linkStatus: Map<string, Promise<void>>,
54
123
  moduleIds: string[]
55
- ) {
124
+ ): CachedVmModule {
56
125
  if (isBuiltin(specifier)) {
57
- return importBuiltinModule(specifier, context);
126
+ const nodeModule = requireSync(specifier);
127
+ const keys = Object.keys(nodeModule);
128
+ const hasDefault = keys.includes('default');
129
+ if (!hasDefault) {
130
+ keys.push('default');
131
+ }
132
+ const module = new vm.SyntheticModule(
133
+ keys,
134
+ function evaluateCallback() {
135
+ keys.forEach((key) => {
136
+ this.setExport(
137
+ key,
138
+ key === 'default' && !hasDefault
139
+ ? nodeModule
140
+ : nodeModule[key]
141
+ );
142
+ });
143
+ },
144
+ {
145
+ identifier: specifier,
146
+ context: context
147
+ }
148
+ );
149
+ const linkResult = module.link(() => {
150
+ throw new TypeError(`Native modules should not be linked`);
151
+ });
152
+ const linkPromise =
153
+ linkResult && typeof linkResult.then === 'function'
154
+ ? linkResult.then(() => module.evaluate())
155
+ : module.evaluate();
156
+ linkStatus.set(specifier, linkPromise);
157
+ return module;
58
158
  }
59
159
  const meta = buildMeta(specifier, parent);
60
160
 
61
- if (moduleIds.includes(meta.url)) {
62
- throw new CircularDependencyError(
63
- 'Circular dependency detected',
64
- moduleIds,
65
- meta.filename
161
+ const cachedModule = cache.get(meta.url);
162
+ if (cachedModule) {
163
+ return cachedModule;
164
+ }
165
+
166
+ // Style assets (.css and friends) are part of esmx's federation
167
+ // contract but carry no observable server-side behaviour — the host
168
+ // emits a `<link rel="stylesheet">` from the manifest, not by
169
+ // evaluating the file. Return a SyntheticModule exposing the asset
170
+ // URL so user code that writes `import sheet from './x.css'`
171
+ // typechecks and runs without the VM trying to parse CSS as JS.
172
+ if (isStyleAsset(meta.url)) {
173
+ const exportNames = ['default', 'href'];
174
+ const styleModule = new vm.SyntheticModule(
175
+ exportNames,
176
+ function evaluateCallback() {
177
+ this.setExport('default', meta.url);
178
+ this.setExport('href', meta.url);
179
+ },
180
+ { identifier: specifier, context }
66
181
  );
182
+ cache.set(meta.url, styleModule);
183
+ const linkResult = styleModule.link(() => {
184
+ throw new TypeError(
185
+ `Style modules should not be linked: ${specifier}`
186
+ );
187
+ });
188
+ const linkPromise =
189
+ linkResult && typeof linkResult.then === 'function'
190
+ ? linkResult.then(() => styleModule.evaluate())
191
+ : styleModule.evaluate();
192
+ linkStatus.set(meta.url, linkPromise);
193
+ return styleModule;
67
194
  }
68
195
 
69
- const module = cache.get(meta.url);
70
- if (module) {
71
- return module;
196
+ let text: string;
197
+ try {
198
+ text = readFileSync(meta.filename);
199
+ } catch (error) {
200
+ throw new FileReadError(
201
+ `Failed to read module: ${meta.filename}`,
202
+ moduleIds,
203
+ meta.filename,
204
+ error as Error
205
+ );
72
206
  }
73
- const modulePromise = new Promise<vm.SourceTextModule>((resolve) => {
74
- process.nextTick(() => {
75
- moduleBuild().then(resolve);
76
- });
77
- });
78
207
 
79
- cache.set(meta.url, modulePromise);
80
- return modulePromise;
81
-
82
- async function moduleBuild(): Promise<vm.SourceTextModule> {
83
- let text: string;
84
- try {
85
- text = fs.readFileSync(meta.filename, 'utf-8');
86
- } catch (error) {
87
- throw new FileReadError(
88
- `Failed to read module: ${meta.filename}`,
89
- moduleIds,
90
- meta.filename,
91
- error as Error
208
+ const module = new vm.SourceTextModule(text, {
209
+ initializeImportMeta: (importMeta) => {
210
+ Object.assign(importMeta, meta);
211
+ },
212
+ identifier: specifier,
213
+ context: context,
214
+ importModuleDynamically: (specifier, referrer) => {
215
+ return moduleLinker(
216
+ specifier,
217
+ meta.url,
218
+ referrer.context,
219
+ cache,
220
+ linkStatus,
221
+ [...moduleIds, meta.filename]
92
222
  );
93
223
  }
94
- const module = new vm.SourceTextModule(text, {
95
- initializeImportMeta: (importMeta) => {
96
- Object.assign(importMeta, meta);
97
- },
98
- identifier: specifier,
99
- context: context,
100
- importModuleDynamically: (specifier, referrer) => {
101
- return moduleLinker(
102
- specifier,
103
- meta.url,
104
- referrer.context,
105
- cache,
106
- [...moduleIds, meta.filename]
107
- );
108
- }
109
- });
110
- await module.link((specifier: string, referrer) => {
111
- return moduleLinker(
224
+ });
225
+
226
+ cache.set(meta.url, module);
227
+
228
+ const linkPromise = module
229
+ .link((specifier: string, referrer) => {
230
+ return syncLinker(
112
231
  specifier,
113
232
  meta.url,
114
233
  referrer.context,
115
234
  cache,
235
+ linkStatus,
116
236
  [...moduleIds, meta.filename]
117
237
  );
118
- });
119
- await module.evaluate();
120
- return module;
238
+ })
239
+ .then(() => module.evaluate());
240
+
241
+ linkStatus.set(meta.url, linkPromise);
242
+
243
+ return module;
244
+ }
245
+
246
+ async function moduleLinker(
247
+ specifier: string,
248
+ parent: string,
249
+ context: vm.Context,
250
+ cache: Map<string, CachedVmModule>,
251
+ linkStatus: Map<string, Promise<void>>,
252
+ moduleIds: string[]
253
+ ) {
254
+ if (isBuiltin(specifier)) {
255
+ return importBuiltinModule(specifier, context);
256
+ }
257
+ const meta = buildMeta(specifier, parent);
258
+
259
+ const cachedModule = cache.get(meta.url);
260
+ if (cachedModule) {
261
+ const status = linkStatus.get(meta.url);
262
+ if (status) {
263
+ await status;
264
+ }
265
+ return cachedModule;
121
266
  }
267
+
268
+ const module = syncLinker(
269
+ specifier,
270
+ parent,
271
+ context,
272
+ cache,
273
+ linkStatus,
274
+ moduleIds
275
+ );
276
+
277
+ const status = linkStatus.get(meta.url);
278
+ if (status) {
279
+ await status;
280
+ }
281
+
282
+ return module;
122
283
  }
284
+
123
285
  return async (
124
286
  specifier: string,
125
287
  parent: string,
126
288
  sandbox?: vm.Context,
127
289
  options?: vm.CreateContextOptions
128
290
  ) => {
291
+ materializeLazyGlobals();
129
292
  const context = vm.createContext(sandbox, options);
293
+ const cache = new Map<string, CachedVmModule>();
294
+ const linkStatus = new Map<string, Promise<void>>();
130
295
  const module = await moduleLinker(
131
296
  specifier,
132
297
  parent,
133
298
  context,
134
- new Map(),
299
+ cache,
300
+ linkStatus,
135
301
  []
136
302
  );
137
303
 
138
- return module.namespace;
304
+ return module.namespace as Record<string, any>;
139
305
  };
140
306
  }
package/src/index.ts CHANGED
@@ -6,5 +6,10 @@ export {
6
6
  ModuleLoadingError
7
7
  } from './error';
8
8
  export { createLoaderImport } from './import-loader';
9
- export { createVmImport } from './import-vm';
10
- export type { ImportMap, ScopesMap, SpecifierMap } from './types';
9
+ export { createVmImport, type VmImportOptions } from './import-vm';
10
+ export type {
11
+ ImportMap,
12
+ IntegrityMap,
13
+ ScopesMap,
14
+ SpecifierMap
15
+ } from './types';
package/src/types.ts CHANGED
@@ -2,9 +2,17 @@ export type SpecifierMap = Record<string, string>;
2
2
 
3
3
  export type ScopesMap = Record<string, SpecifierMap>;
4
4
 
5
+ export type IntegrityMap = Record<string, string>;
6
+
5
7
  export interface ImportMap {
6
8
  imports?: SpecifierMap;
7
9
  scopes?: ScopesMap;
10
+ /**
11
+ * Subresource Integrity metadata, mapping module URLs to integrity hashes.
12
+ * Part of the Import Maps specification; ignored by browsers that do not
13
+ * support it.
14
+ */
15
+ integrity?: IntegrityMap;
8
16
  }
9
17
 
10
18
  export type ImportMapResolver = (