@eggjs/utils 5.0.2-beta.2 → 5.0.2-beta.22

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/README.md CHANGED
@@ -42,6 +42,65 @@ npm i @eggjs/utils
42
42
  - {String} baseDir - the current directory of application
43
43
  - {String} framework - the directory of framework
44
44
 
45
+ ### `setBundleModuleLoader(loader)`
46
+
47
+ Register a module loader hook for bundled Egg apps. The hook runs before the
48
+ normal `importModule()` resolution path.
49
+
50
+ - {Function | undefined} loader - a synchronous function that receives the
51
+ original `filepath` argument passed to `importModule()` after POSIX separator
52
+ normalization, or a virtual specifier. It does not receive the resolved
53
+ absolute file path from `importResolve()`. Return `undefined` to fall back to
54
+ the normal import path.
55
+
56
+ The bundle loader is stored on `globalThis`, so bundled and external copies of
57
+ `@eggjs/utils` share the same loader. Non-`undefined` results follow the same
58
+ default export unwrapping rules as `importModule()`, including
59
+ `importDefaultOnly`.
60
+
61
+ ### Bundle / snapshot module-loading hooks
62
+
63
+ `importModule()` resolves a module through the following hooks, in order. The
64
+ first one that produces a value wins; otherwise it falls back to the native
65
+ `import()` / `require()` path:
66
+
67
+ 1. **`globalThis.__EGG_BUNDLE_MODULE_LOADER__`** — set via
68
+ `setBundleModuleLoader()`. Looks up a module already inlined into the bundle
69
+ (typically a static bundle map emitted by `egg-bundler`). Runs before on-disk
70
+ resolution and receives the POSIX-normalized `importModule()` filepath or a
71
+ virtual specifier. Return `undefined` to fall through.
72
+ 2. **Snapshot module loader** — set via `setSnapshotModuleLoader()`. This is a
73
+ module-local hook (not a `globalThis` global) used by the V8 snapshot entry
74
+ generator to serve pre-bundled modules synchronously, keyed by the resolved
75
+ path. Once registered it handles every load that reaches it, so the importer
76
+ below is not consulted while it is active.
77
+ 3. **`globalThis.__EGG_MODULE_IMPORTER__`** — an async (or sync, since the value
78
+ is awaited) importer that receives the resolved file path (the
79
+ `importResolve()` result, with OS-native separators — not normalized). When
80
+ set, and the two hooks above did not resolve the module, it replaces the
81
+ native `await import(filePath)`.
82
+
83
+ The bundle loader and importer globals are typed in `@eggjs/typings`
84
+ (`BundleModuleLoader` / `ModuleImporter`); import `@eggjs/typings/global` to pick
85
+ up the `declare global` augmentation. These hooks are the contract that
86
+ `egg-bundler`'s generated entry relies on. `@eggjs/core`'s `ManifestLoaderFS`
87
+ consults `__EGG_BUNDLE_MODULE_LOADER__` directly; its importer/native fallback is
88
+ reached through `@eggjs/loader-fs`, which calls back into `importModule()`. The
89
+ tegg loader (`LoaderUtil.loadFile`) consults both globals directly, passing the
90
+ loader filepath with separators normalized to POSIX.
91
+
92
+ `__EGG_MODULE_IMPORTER__` has two main uses:
93
+
94
+ - **Bundler-based test runners (e.g. Vitest):** route module loading through the
95
+ runner's own module graph so the loader and the test file share a single
96
+ module instance (otherwise `ctx.getEggObject(ClassRef)` fails with
97
+ "can not get proto").
98
+ - **V8 startup-snapshot restore:** the deserialized main function runs without a
99
+ host dynamic-import callback, so native `import()` throws. The snapshot entry
100
+ installs a synchronous `require()`-based importer (`createRequire()` over the
101
+ bundle output dir); `require()` can load ESM on Node >= 22, so modules resolve
102
+ without dynamic import.
103
+
45
104
  ## License
46
105
 
47
106
  [MIT](LICENSE)
package/dist/import.d.ts CHANGED
@@ -1,3 +1,5 @@
1
+ import { BundleModuleLoader, BundleModuleLoader as BundleModuleLoader$1 } from "@eggjs/typings";
2
+
1
3
  //#region src/import.d.ts
2
4
  interface ImportResolveOptions {
3
5
  paths?: string[];
@@ -10,6 +12,39 @@ declare function getRequire(): NodeRequire;
10
12
  declare function getExtensions(): NodeJS.RequireExtensions;
11
13
  declare function isSupportTypeScript(): boolean;
12
14
  declare function importResolve(filepath: string, options?: ImportResolveOptions): string;
15
+ /**
16
+ * Module loader function type for V8 snapshot support.
17
+ * Called with the resolved absolute file path, returns the module exports.
18
+ */
19
+ type SnapshotModuleLoader = (resolvedPath: string) => any;
20
+ /**
21
+ * Register a snapshot module loader that intercepts `importModule()` calls.
22
+ *
23
+ * When set, `importModule()` delegates to this loader instead of calling
24
+ * `import()` or `require()`. This is used by the V8 snapshot entry generator
25
+ * to provide pre-bundled modules — the bundler generates a static module map
26
+ * from the egg manifest and registers it via this API.
27
+ *
28
+ * Also sets `isESM = false` because the snapshot bundle is CJS and
29
+ * esbuild's `import.meta` polyfill causes incorrect ESM detection.
30
+ *
31
+ * Pass `undefined` to clear the loader and restore the auto-detected `isESM`
32
+ * value. Always clear it once snapshot mode is no longer needed (e.g. in test
33
+ * teardown) so the module-level state does not leak into other files when
34
+ * vitest runs with `isolate: false`.
35
+ */
36
+ declare function setSnapshotModuleLoader(loader: SnapshotModuleLoader | undefined): void;
37
+ /**
38
+ * Register a bundle module loader. Uses globalThis so that bundled and
39
+ * external copies of @eggjs/utils share the same loader.
40
+ *
41
+ * The loader receives a POSIX-normalized filepath or virtual specifier before
42
+ * normal resolution runs. Return `undefined` to fall through to the default
43
+ * import path. Non-undefined hits use the same default unwrapping semantics as
44
+ * normal imports, including `importDefaultOnly` and double-default `__esModule`
45
+ * compatibility.
46
+ */
47
+ declare function setBundleModuleLoader(loader: BundleModuleLoader | undefined): void;
13
48
  declare function importModule(filepath: string, options?: ImportModuleOptions): Promise<any>;
14
49
  //#endregion
15
- export { ImportModuleOptions, ImportResolveOptions, getExtensions, getRequire, importModule, importResolve, isESM, isSupportTypeScript };
50
+ export { type BundleModuleLoader$1 as BundleModuleLoader, ImportModuleOptions, ImportResolveOptions, SnapshotModuleLoader, getExtensions, getRequire, importModule, importResolve, isESM, isSupportTypeScript, setBundleModuleLoader, setSnapshotModuleLoader };
package/dist/import.js CHANGED
@@ -8,17 +8,30 @@ import { debuglog } from "node:util";
8
8
 
9
9
  //#region src/import.ts
10
10
  const debug = debuglog("egg/utils/import");
11
+ let nativeDynamicImport;
12
+ /* v8 ignore next -- covered by the spawned Node fixture; Vitest cannot instrument this opaque import. */
13
+ function getNativeDynamicImport() {
14
+ if (!nativeDynamicImport) try {
15
+ nativeDynamicImport = new Function("specifier", "return import(specifier);");
16
+ } catch (err) {
17
+ const error = /* @__PURE__ */ new Error("Native dynamic import fallback for bundled module loader misses requires code generation from strings.");
18
+ error.cause = err;
19
+ throw error;
20
+ }
21
+ return nativeDynamicImport;
22
+ }
11
23
  let isESM = true;
12
24
  try {
13
25
  if (typeof import.meta !== "undefined") isESM = true;
14
26
  } catch {
15
27
  isESM = false;
16
28
  }
29
+ const detectedIsESM = isESM;
17
30
  const nodeMajorVersion = parseInt(process.versions.node.split(".", 1)[0], 10);
18
- const supportImportMetaResolve = nodeMajorVersion >= 18;
31
+ const supportImportMetaResolve = nodeMajorVersion >= 18 && typeof import.meta !== "undefined" && typeof import.meta.resolve === "function";
19
32
  let _customRequire;
20
33
  function getRequire() {
21
- if (!_customRequire) if (typeof __require !== "undefined") _customRequire = __require;
34
+ if (!_customRequire) if (typeof __require !== "undefined" && __require.extensions) _customRequire = __require;
22
35
  else _customRequire = createRequire(process.cwd());
23
36
  return _customRequire;
24
37
  }
@@ -193,6 +206,11 @@ function importResolve(filepath, options) {
193
206
  }
194
207
  }
195
208
  }
209
+ const bundleModuleLoader = globalThis.__EGG_BUNDLE_MODULE_LOADER__;
210
+ if (bundleModuleLoader && bundleModuleLoader(normalizeBundleModulePath(filepath)) !== void 0) {
211
+ debug("[importResolve:bundle] %o => %o", filepath, filepath);
212
+ return filepath;
213
+ }
196
214
  const extname = path.extname(filepath);
197
215
  if (!isAbsolute && extname === ".json" || !isESM) moduleFilePath = getRequire().resolve(filepath, { paths });
198
216
  else if (supportImportMetaResolve) {
@@ -205,19 +223,95 @@ function importResolve(filepath, options) {
205
223
  if (moduleFilePath.startsWith("file://")) moduleFilePath = fileURLToPath(moduleFilePath);
206
224
  debug("[importResolve] import.meta.resolve %o => %o", filepath, moduleFilePath);
207
225
  if (!fs.statSync(moduleFilePath, { throwIfNoEntry: false })?.isFile()) throw new TypeError(`Cannot find module ${filepath}, because ${moduleFilePath} does not exists`);
208
- } else moduleFilePath = getRequire().resolve(filepath);
226
+ } else moduleFilePath = getRequire().resolve(filepath, paths ? { paths } : void 0);
209
227
  debug("[importResolve:success] %o, options: %o => %o, isESM: %s", filepath, options, moduleFilePath, isESM);
210
228
  return moduleFilePath;
211
229
  }
230
+ let _snapshotModuleLoader;
231
+ /**
232
+ * Register a snapshot module loader that intercepts `importModule()` calls.
233
+ *
234
+ * When set, `importModule()` delegates to this loader instead of calling
235
+ * `import()` or `require()`. This is used by the V8 snapshot entry generator
236
+ * to provide pre-bundled modules — the bundler generates a static module map
237
+ * from the egg manifest and registers it via this API.
238
+ *
239
+ * Also sets `isESM = false` because the snapshot bundle is CJS and
240
+ * esbuild's `import.meta` polyfill causes incorrect ESM detection.
241
+ *
242
+ * Pass `undefined` to clear the loader and restore the auto-detected `isESM`
243
+ * value. Always clear it once snapshot mode is no longer needed (e.g. in test
244
+ * teardown) so the module-level state does not leak into other files when
245
+ * vitest runs with `isolate: false`.
246
+ */
247
+ function setSnapshotModuleLoader(loader) {
248
+ _snapshotModuleLoader = loader;
249
+ isESM = loader ? false : detectedIsESM;
250
+ }
251
+ function normalizeBundleModulePath(filepath) {
252
+ return filepath.split(path.win32.sep).join(path.posix.sep);
253
+ }
254
+ /**
255
+ * Register a bundle module loader. Uses globalThis so that bundled and
256
+ * external copies of @eggjs/utils share the same loader.
257
+ *
258
+ * The loader receives a POSIX-normalized filepath or virtual specifier before
259
+ * normal resolution runs. Return `undefined` to fall through to the default
260
+ * import path. Non-undefined hits use the same default unwrapping semantics as
261
+ * normal imports, including `importDefaultOnly` and double-default `__esModule`
262
+ * compatibility.
263
+ */
264
+ function setBundleModuleLoader(loader) {
265
+ globalThis.__EGG_BUNDLE_MODULE_LOADER__ = loader;
266
+ }
267
+ const _inflightImports = /* @__PURE__ */ new Map();
212
268
  async function importModule(filepath, options) {
269
+ const _bundleModuleLoader = globalThis.__EGG_BUNDLE_MODULE_LOADER__;
270
+ if (_bundleModuleLoader) {
271
+ const hit = _bundleModuleLoader(normalizeBundleModulePath(filepath));
272
+ if (hit !== void 0) {
273
+ let obj$1 = hit;
274
+ if (obj$1?.default?.__esModule === true && "default" in obj$1.default) obj$1 = obj$1.default;
275
+ if (options?.importDefaultOnly && obj$1 && typeof obj$1 === "object" && "default" in obj$1) obj$1 = obj$1.default;
276
+ return obj$1;
277
+ }
278
+ }
213
279
  const moduleFilePath = importResolve(filepath, options);
280
+ if (_snapshotModuleLoader) {
281
+ let obj$1 = _snapshotModuleLoader(moduleFilePath);
282
+ if (obj$1 && typeof obj$1 === "object" && obj$1.default?.__esModule === true && obj$1.default && "default" in obj$1.default) obj$1 = obj$1.default;
283
+ if (options?.importDefaultOnly) {
284
+ if (obj$1 && typeof obj$1 === "object" && "default" in obj$1) obj$1 = obj$1.default;
285
+ }
286
+ return obj$1;
287
+ }
288
+ const _moduleImporter = globalThis.__EGG_MODULE_IMPORTER__;
289
+ if (_moduleImporter) {
290
+ let obj$1 = await _moduleImporter(moduleFilePath);
291
+ if (obj$1 && typeof obj$1 === "object" && obj$1.default?.__esModule === true && obj$1.default && "default" in obj$1.default) obj$1 = obj$1.default;
292
+ if (options?.importDefaultOnly && obj$1 && typeof obj$1 === "object" && "default" in obj$1) obj$1 = obj$1.default;
293
+ return obj$1;
294
+ }
214
295
  let obj;
215
296
  if (isESM) {
216
297
  const fileUrl = pathToFileURL(moduleFilePath).toString();
217
298
  debug("[importModule:start] await import fileUrl: %s, isESM: %s", fileUrl, isESM);
218
- obj = await import(fileUrl);
299
+ /* v8 ignore if -- covered by the spawned Node fixture; Vitest cannot instrument this opaque import. */
300
+ if (_bundleModuleLoader) obj = await getNativeDynamicImport()(fileUrl);
301
+ else {
302
+ let pending = _inflightImports.get(fileUrl);
303
+ if (pending === void 0) {
304
+ pending = import(fileUrl);
305
+ _inflightImports.set(fileUrl, pending);
306
+ const clearInflight = () => {
307
+ if (_inflightImports.get(fileUrl) === pending) _inflightImports.delete(fileUrl);
308
+ };
309
+ pending.then(clearInflight, clearInflight);
310
+ }
311
+ obj = await pending;
312
+ }
219
313
  debug("[importModule:success] await import %o", fileUrl);
220
- if (obj?.default?.__esModule === true && "default" in obj?.default) obj = obj.default;
314
+ if (obj?.default?.__esModule === true && obj.default && "default" in obj.default) obj = obj.default;
221
315
  if (options?.importDefaultOnly) {
222
316
  if ("default" in obj) obj = obj.default;
223
317
  }
@@ -231,4 +325,4 @@ async function importModule(filepath, options) {
231
325
  }
232
326
 
233
327
  //#endregion
234
- export { getExtensions, getRequire, importModule, importResolve, isESM, isSupportTypeScript };
328
+ export { getExtensions, getRequire, importModule, importResolve, isESM, isSupportTypeScript, setBundleModuleLoader, setSnapshotModuleLoader };
package/dist/index.d.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  import { getFrameworkOrEggPath } from "./deprecated.js";
2
2
  import { getFrameworkPath } from "./framework.js";
3
3
  import { findEggCore, getConfig, getLoadUnits, getLoader, getPlugins } from "./plugin.js";
4
- import { ImportModuleOptions, ImportResolveOptions, getExtensions, getRequire, importModule, importResolve, isESM, isSupportTypeScript } from "./import.js";
4
+ import { BundleModuleLoader, ImportModuleOptions, ImportResolveOptions, SnapshotModuleLoader, getExtensions, getRequire, importModule, importResolve, isESM, isSupportTypeScript, setBundleModuleLoader, setSnapshotModuleLoader } from "./import.js";
5
5
  import { ImportResolveError } from "./error/ImportResolveError.js";
6
6
 
7
7
  //#region src/index.d.ts
@@ -25,4 +25,4 @@ type EggType = (typeof EggType)[keyof typeof EggType];
25
25
  */
26
26
  declare function detectType(baseDir: string): Promise<keyof typeof EggType>;
27
27
  //#endregion
28
- export { EggType, ImportModuleOptions, ImportResolveError, ImportResolveOptions, _default as default, detectType, findEggCore, getConfig, getExtensions, getFrameworkOrEggPath, getFrameworkPath, getLoadUnits, getLoader, getPlugins, getRequire, importModule, importResolve, isESM, isSupportTypeScript };
28
+ export { BundleModuleLoader, EggType, ImportModuleOptions, ImportResolveError, ImportResolveOptions, SnapshotModuleLoader, _default as default, detectType, findEggCore, getConfig, getExtensions, getFrameworkOrEggPath, getFrameworkPath, getLoadUnits, getLoader, getPlugins, getRequire, importModule, importResolve, isESM, isSupportTypeScript, setBundleModuleLoader, setSnapshotModuleLoader };
package/dist/index.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import { getFrameworkOrEggPath } from "./deprecated.js";
2
2
  import { ImportResolveError } from "./error/ImportResolveError.js";
3
- import { getExtensions, getRequire, importModule, importResolve, isESM, isSupportTypeScript } from "./import.js";
3
+ import { getExtensions, getRequire, importModule, importResolve, isESM, isSupportTypeScript, setBundleModuleLoader, setSnapshotModuleLoader } from "./import.js";
4
4
  import { getFrameworkPath } from "./framework.js";
5
5
  import { findEggCore, getConfig, getLoadUnits, getLoader, getPlugins } from "./plugin.js";
6
6
  import fs from "node:fs/promises";
@@ -42,4 +42,4 @@ async function detectType(baseDir) {
42
42
  }
43
43
 
44
44
  //#endregion
45
- export { EggType, ImportResolveError, src_default as default, detectType, findEggCore, getConfig, getExtensions, getFrameworkOrEggPath, getFrameworkPath, getLoadUnits, getLoader, getPlugins, getRequire, importModule, importResolve, isESM, isSupportTypeScript };
45
+ export { EggType, ImportResolveError, src_default as default, detectType, findEggCore, getConfig, getExtensions, getFrameworkOrEggPath, getFrameworkPath, getLoadUnits, getLoader, getPlugins, getRequire, importModule, importResolve, isESM, isSupportTypeScript, setBundleModuleLoader, setSnapshotModuleLoader };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@eggjs/utils",
3
- "version": "5.0.2-beta.2",
3
+ "version": "5.0.2-beta.22",
4
4
  "description": "Utils for all egg projects",
5
5
  "keywords": [
6
6
  "egg",
@@ -26,9 +26,18 @@
26
26
  "./package.json": "./package.json"
27
27
  },
28
28
  "publishConfig": {
29
- "access": "public"
29
+ "access": "public",
30
+ "exports": {
31
+ ".": "./dist/index.js",
32
+ "./package.json": "./package.json"
33
+ }
34
+ },
35
+ "scripts": {
36
+ "typecheck": "tsgo --noEmit"
37
+ },
38
+ "dependencies": {
39
+ "@eggjs/typings": "4.1.2-beta.22"
30
40
  },
31
- "dependencies": {},
32
41
  "devDependencies": {
33
42
  "coffee": "5",
34
43
  "mm": "^4.0.2",
@@ -37,8 +46,5 @@
37
46
  },
38
47
  "engines": {
39
48
  "node": ">=22.18.0"
40
- },
41
- "scripts": {
42
- "typecheck": "tsgo --noEmit"
43
49
  }
44
- }
50
+ }