@eggjs/utils 5.0.2-beta.16 → 5.0.2-beta.17

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
@@ -58,6 +58,49 @@ The bundle loader is stored on `globalThis`, so bundled and external copies of
58
58
  default export unwrapping rules as `importModule()`, including
59
59
  `importDefaultOnly`.
60
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
+
61
104
  ## License
62
105
 
63
106
  [MIT](LICENSE)
package/dist/import.js CHANGED
@@ -264,6 +264,7 @@ function normalizeBundleModulePath(filepath) {
264
264
  function setBundleModuleLoader(loader) {
265
265
  globalThis.__EGG_BUNDLE_MODULE_LOADER__ = loader;
266
266
  }
267
+ const _inflightImports = /* @__PURE__ */ new Map();
267
268
  async function importModule(filepath, options) {
268
269
  const _bundleModuleLoader = globalThis.__EGG_BUNDLE_MODULE_LOADER__;
269
270
  if (_bundleModuleLoader) {
@@ -297,7 +298,18 @@ async function importModule(filepath, options) {
297
298
  debug("[importModule:start] await import fileUrl: %s, isESM: %s", fileUrl, isESM);
298
299
  /* v8 ignore if -- covered by the spawned Node fixture; Vitest cannot instrument this opaque import. */
299
300
  if (_bundleModuleLoader) obj = await getNativeDynamicImport()(fileUrl);
300
- else obj = await import(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
+ }
301
313
  debug("[importModule:success] await import %o", fileUrl);
302
314
  if (obj?.default?.__esModule === true && obj.default && "default" in obj.default) obj = obj.default;
303
315
  if (options?.importDefaultOnly) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@eggjs/utils",
3
- "version": "5.0.2-beta.16",
3
+ "version": "5.0.2-beta.17",
4
4
  "description": "Utils for all egg projects",
5
5
  "keywords": [
6
6
  "egg",
@@ -29,7 +29,7 @@
29
29
  "access": "public"
30
30
  },
31
31
  "dependencies": {
32
- "@eggjs/typings": "4.1.2-beta.16"
32
+ "@eggjs/typings": "4.1.2-beta.17"
33
33
  },
34
34
  "devDependencies": {
35
35
  "coffee": "5",