Skip to content

feat(utils): add setBundleModuleLoader runtime hook#5867

Open
killagu wants to merge 1 commit intoeggjs:nextfrom
killagu:split/01-utils-bundle-module-loader
Open

feat(utils): add setBundleModuleLoader runtime hook#5867
killagu wants to merge 1 commit intoeggjs:nextfrom
killagu:split/01-utils-bundle-module-loader

Conversation

@killagu
Copy link
Copy Markdown
Contributor

@killagu killagu commented Apr 14, 2026

Summary

Adds setBundleModuleLoader(loader) to @eggjs/utils so a bundled Egg application can intercept importModule() and short-circuit module lookups to an in-bundle map. Reuses the existing __esModule double-default and importDefaultOnly handling.

State is stored on globalThis.__EGG_BUNDLE_MODULE_LOADER__ so the bundled entry and an external egg framework instance — which may be different module instances under turbopack — share the same hook.

Why

This is batch 1, part of a 19-PR split of #5863 (a 4812-line monolith adding @eggjs/egg-bundler). #5863 is kept open as a tracking reference. This PR is independent (base = next) and can land in any order with the other batch-1 PRs.

The runtime hook is a prerequisite for the upcoming @eggjs/egg-bundler package: the generated worker entry calls it before `new Application()` to redirect every loader `importModule()` to the bundled namespace, eliminating filesystem scanning at runtime.

Test plan

  • `pnpm --filter=@eggjs/utils test` — 60 passed + 13 skipped (6 new `bundle-import.test.ts` cases)
  • typecheck clean

Stack context

Other batch-1 PRs (independent, can land in any order):

  • `refactor(onerror): inline error page template as string constant`
  • `refactor(development): inline loader trace template as string constant`
  • `refactor(watcher): use direct class imports for event sources`

Subsequent batches (will be opened once batch-1 merges):

  • Group A continued: `feat(core): add ManifestStore.fromBundle`, `feat(core): add ManifestStore.setBundleStore hook`
  • Group C: 10 PRs adding `@eggjs/egg-bundler` package incrementally
  • Group D: `feat(egg-bin): add bundle subcommand`
  • Group E: `remove @eggjs/* from auto-externals`, `post-process turbopack output`

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Added support for registering custom bundle module loaders that intercept module import operations
    • Enables loading modules from virtual or bundled sources without requiring filesystem access
    • Gracefully falls through to standard import behavior when no loader is registered or returns undefined
  • Tests

    • Added comprehensive test suite validating bundle module loading functionality and edge cases

Add `setBundleModuleLoader()` which intercepts `importModule()` BEFORE
`importResolve()` so bundled apps can redirect module lookups to an
in-bundle map without the source files existing on disk. Reuses the
existing `__esModule` double-default and `importDefaultOnly` handling.

State is stored on `globalThis.__EGG_BUNDLE_MODULE_LOADER__` so that
bundled and external copies of @eggjs/utils share the same loader —
required because a bundled entry and the external egg framework may
resolve to different module instances.

Required by the upcoming @eggjs/egg-bundler to bootstrap a bundled
Egg app without filesystem discovery.
Copilot AI review requested due to automatic review settings April 14, 2026 03:59
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Apr 14, 2026

📝 Walkthrough

Walkthrough

Added a bundle module loader mechanism to the import system. New BundleModuleLoader type and setBundleModuleLoader function allow registering a global loader that intercepts module imports. The importModule function now consults this loader first (with posix path normalization) before proceeding to standard resolution, with consistent interop handling applied to loader results.

Changes

Cohort / File(s) Summary
Bundle Loader Implementation
packages/utils/src/import.ts
Added BundleModuleLoader type and setBundleModuleLoader() function to register a global module loader. Updated importModule() to consult the registered loader first (with backslash-to-forward-slash path normalization), applying the same default-unwrapping interop rules before falling through to normal resolution.
Bundle Loader Test Suite
packages/utils/test/bundle-import.test.ts
Added comprehensive test suite verifying loader registration, interception of module imports, return of fake payloads, unwrapping of nested defaults, fallthrough to normal imports when loader returns undefined, and resolution of virtual/non-existent paths.

Sequence Diagram

sequenceDiagram
    participant Client
    participant importModule
    participant BundleLoader
    participant StandardResolver

    Client->>importModule: importModule(filepath)
    importModule->>importModule: normalize filepath (backslash→forward slash)
    importModule->>BundleLoader: check if loader registered?
    alt Loader Registered
        BundleLoader->>BundleLoader: loader(normalized_filepath)
        BundleLoader-->>importModule: module object (or undefined)
        alt Loader returned value
            importModule->>importModule: apply interop rules (unwrap defaults)
            importModule-->>Client: processed module
        else Loader returned undefined
            importModule->>StandardResolver: fall through to normal resolution
            StandardResolver-->>Client: resolved module
        end
    else No Loader Registered
        importModule->>StandardResolver: proceed to normal resolution
        StandardResolver-->>Client: resolved module
    end
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related PRs

Suggested reviewers

  • jerryliang64

Poem

🐰 A loader hops in, straight and true,
Bundle paths unwrap, old defaults anew,
Through posix-fied trails it bounces with glee,
Intercepts modules with interop spree! 📦✨

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 66.67% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately describes the main change: adding a setBundleModuleLoader runtime hook to the utils package.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@codecov
Copy link
Copy Markdown

codecov bot commented Apr 14, 2026

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 86.02%. Comparing base (490f849) to head (acb9a67).

Additional details and impacted files
@@            Coverage Diff             @@
##             next    #5867      +/-   ##
==========================================
+ Coverage   86.00%   86.02%   +0.01%     
==========================================
  Files         667      667              
  Lines       18945    18956      +11     
  Branches     3652     3657       +5     
==========================================
+ Hits        16294    16306      +12     
+ Misses       2297     2295       -2     
- Partials      354      355       +1     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

Copy link
Copy Markdown
Contributor

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces a BundleModuleLoader to allow bundled applications to serve modules that may not exist on disk. It adds a global registration mechanism via setBundleModuleLoader and updates importModule to intercept requests using the registered loader. Feedback highlights potential consistency issues with the isESM flag when multiple package instances exist and the need to restore the original isESM state when the loader is unset to ensure test isolation.

Comment on lines +415 to +417
const _bundleModuleLoader: BundleModuleLoader | undefined = (globalThis as any).__EGG_BUNDLE_MODULE_LOADER__;
if (_bundleModuleLoader) {
const hit = _bundleModuleLoader(filepath.replaceAll('\\', '/'));
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

There is a consistency issue when multiple instances of @eggjs/utils are present in the same process (e.g., under Turbopack). While the bundle loader is shared via globalThis, the isESM = false override in setBundleModuleLoader only affects the current module instance. Other instances will still have isESM = true, leading to inconsistent behavior in importModule and importResolve if the loader falls through. Consider synchronizing this state globally or checking the global loader presence dynamically when evaluating ESM vs CJS logic.

  const _bundleModuleLoader: BundleModuleLoader | undefined = (globalThis as any).__EGG_BUNDLE_MODULE_LOADER__;
  if (_bundleModuleLoader) {
    isESM = false;
    const hit = _bundleModuleLoader(filepath.replaceAll('\\', '/'));

Comment on lines +409 to +412
export function setBundleModuleLoader(loader: BundleModuleLoader | undefined): void {
(globalThis as any).__EGG_BUNDLE_MODULE_LOADER__ = loader;
if (loader) isESM = false;
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The isESM variable is set to false when a loader is provided, but it is not restored to its original state when setBundleModuleLoader(undefined) is called. This can lead to persistent side effects in the current process, which is particularly problematic for test isolation (as seen in the afterEach cleanup in the new tests). Consider capturing the initial detected value of isESM and restoring it when the loader is unset.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@packages/utils/src/import.ts`:
- Around line 410-415: The code uses (globalThis as
any).__EGG_BUNDLE_MODULE_LOADER__ which weakens typing; declare a typed global
interface for __EGG_BUNDLE_MODULE_LOADER__ (e.g. interface Global {
__EGG_BUNDLE_MODULE_LOADER__?: BundleModuleLoader }) via declare global so you
can replace both (globalThis as any).__EGG_BUNDLE_MODULE_LOADER__ occurrences
with properly typed accesses, and keep the existing logic in importModule and
the loader assignment (references: __EGG_BUNDLE_MODULE_LOADER__,
BundleModuleLoader, importModule, loader, isESM).
- Around line 409-412: setBundleModuleLoader currently sets (globalThis as
any).__EGG_BUNDLE_MODULE_LOADER__ and flips isESM = false on registration but
never restores isESM when loader is unset, causing importModule to incorrectly
use require(); update setBundleModuleLoader(loader) to set
__EGG_BUNDLE_MODULE_LOADER__ and set isESM = loader ? false : true (or restore
previous ESM state) so unregistering (loader === undefined) resets isESM; also
remove the (globalThis as any) casts by adding a globalThis augmentation that
declares __EGG_BUNDLE_MODULE_LOADER__?: BundleModuleLoader and use that typed
property instead of any to access the global.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: a64667b7-dc8d-4ad0-9fcc-ba23be486dc5

📥 Commits

Reviewing files that changed from the base of the PR and between 490f849 and acb9a67.

⛔ Files ignored due to path filters (1)
  • packages/utils/test/__snapshots__/index.test.ts.snap is excluded by !**/*.snap
📒 Files selected for processing (2)
  • packages/utils/src/import.ts
  • packages/utils/test/bundle-import.test.ts

Comment on lines +409 to +412
export function setBundleModuleLoader(loader: BundleModuleLoader | undefined): void {
(globalThis as any).__EGG_BUNDLE_MODULE_LOADER__ = loader;
if (loader) isESM = false;
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Verify there is no unregister branch restoring isESM today.
rg -n -A12 -B2 'export function setBundleModuleLoader' packages/utils/src/import.ts
rg -n 'isESM\s*=' packages/utils/src/import.ts

Repository: eggjs/egg

Length of output: 1118


🏁 Script executed:

# Find all calls to setBundleModuleLoader to understand usage pattern
rg -n 'setBundleModuleLoader' --type ts --type js

Repository: eggjs/egg

Length of output: 876


🏁 Script executed:

# Check what _snapshotModuleLoader is and where it's defined
rg -n '_snapshotModuleLoader' packages/utils/src/import.ts

Repository: eggjs/egg

Length of output: 247


🏁 Script executed:

# Look at more context around the isESM initialization and snapshot logic
sed -n '1,100p' packages/utils/src/import.ts | cat -n

Repository: eggjs/egg

Length of output: 4089


🏁 Script executed:

# Examine the test file to understand the impact of isESM state
cat -n packages/utils/test/bundle-import.test.ts

Repository: eggjs/egg

Length of output: 2702


🏁 Script executed:

# Check if isESM is used in importModule logic to understand the actual impact
sed -n '414,450p' packages/utils/src/import.ts | cat -n

Repository: eggjs/egg

Length of output: 1728


🏁 Script executed:

# Check if there are any test failures related to isESM state between tests
cd packages/utils && npm test 2>&1 | head -100

Repository: eggjs/egg

Length of output: 273


🏁 Script executed:

# Look at how snapshot loader is set to understand the state management pattern
sed -n '375,405p' packages/utils/src/import.ts | cat -n

Repository: eggjs/egg

Length of output: 1465


🏁 Script executed:

# Check if there's any cleanup/reset of isESM in test setup/teardown
rg -n 'isESM|beforeEach|afterEach|before|after' packages/utils/test/bundle-import.test.ts

Repository: eggjs/egg

Length of output: 126


🏁 Script executed:

# Check setSnapshotModuleLoader for similar isESM restoration issue
sed -n '388,402p' packages/utils/src/import.ts | cat -n

Repository: eggjs/egg

Length of output: 705


🏁 Script executed:

# Verify all `any` casts in this file to understand full scope
rg -n 'as any' packages/utils/src/import.ts

Repository: eggjs/egg

Length of output: 265


🏁 Script executed:

# Verify that _snapshotModuleLoader is in scope and accessible where proposed fix would apply
sed -n '375,430p' packages/utils/src/import.ts | cat -n

Repository: eggjs/egg

Length of output: 2817


🏁 Script executed:

# Check if there are any type definitions for globalThis to use instead of 'as any'
rg -n 'globalThis|declare' packages/utils/src/import.ts | head -20

Repository: eggjs/egg

Length of output: 309


Restore isESM when unregistering the bundle loader.

Line 411 only sets isESM = false on registration. When setBundleModuleLoader(undefined) is called (as in the afterEach in tests), isESM stays false, causing subsequent importModule() calls to incorrectly route through require() instead of import(), breaking imports of ESM-only modules.

Additionally, replace as any casts on globalThis.

Lines 410 and 415 use (globalThis as any) which violates the TypeScript guideline against any types. Define a proper type or augmentation for the globalThis properties instead.

🛠️ Proposed fix for state restoration
+const detectedIsESM = isESM;
+
 export function setBundleModuleLoader(loader: BundleModuleLoader | undefined): void {
   (globalThis as any).__EGG_BUNDLE_MODULE_LOADER__ = loader;
-  if (loader) isESM = false;
+  if (loader) {
+    isESM = false;
+  } else if (!_snapshotModuleLoader) {
+    isESM = detectedIsESM;
+  }
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/utils/src/import.ts` around lines 409 - 412, setBundleModuleLoader
currently sets (globalThis as any).__EGG_BUNDLE_MODULE_LOADER__ and flips isESM
= false on registration but never restores isESM when loader is unset, causing
importModule to incorrectly use require(); update setBundleModuleLoader(loader)
to set __EGG_BUNDLE_MODULE_LOADER__ and set isESM = loader ? false : true (or
restore previous ESM state) so unregistering (loader === undefined) resets
isESM; also remove the (globalThis as any) casts by adding a globalThis
augmentation that declares __EGG_BUNDLE_MODULE_LOADER__?: BundleModuleLoader and
use that typed property instead of any to access the global.

Comment on lines +410 to +415
(globalThis as any).__EGG_BUNDLE_MODULE_LOADER__ = loader;
if (loader) isESM = false;
}

export async function importModule(filepath: string, options?: ImportModuleOptions): Promise<any> {
const _bundleModuleLoader: BundleModuleLoader | undefined = (globalThis as any).__EGG_BUNDLE_MODULE_LOADER__;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Verify current any-casts on bundle loader global access.
rg -n '\(globalThis as any\)\.__EGG_BUNDLE_MODULE_LOADER__' packages/utils/src/import.ts

Repository: eggjs/egg

Length of output: 233


🏁 Script executed:

#!/bin/bash
# Find all occurrences of __EGG_BUNDLE_MODULE_LOADER__ in the codebase
rg -n '__EGG_BUNDLE_MODULE_LOADER__' --type ts --type tsx

Repository: eggjs/egg

Length of output: 80


🏁 Script executed:

#!/bin/bash
# Show the context around the current usage in import.ts (lines 400-420)
sed -n '400,420p' packages/utils/src/import.ts | cat -n

Repository: eggjs/egg

Length of output: 1158


🏁 Script executed:

#!/bin/bash
# Check the beginning of import.ts to see current exports and type definitions
head -50 packages/utils/src/import.ts | cat -n

Repository: eggjs/egg

Length of output: 1968


🏁 Script executed:

#!/bin/bash
# Search all files for __EGG_BUNDLE_MODULE_LOADER__ without file type filter
rg -n '__EGG_BUNDLE_MODULE_LOADER__'

Repository: eggjs/egg

Length of output: 291


🏁 Script executed:

#!/bin/bash
# Get the exact line numbers from the import.ts file (use wc to check file length first)
wc -l packages/utils/src/import.ts

Repository: eggjs/egg

Length of output: 85


🏁 Script executed:

#!/bin/bash
# Get lines around 410-415 from import.ts
sed -n '405,420p' packages/utils/src/import.ts | cat -n

Repository: eggjs/egg

Length of output: 940


🏁 Script executed:

#!/bin/bash
# Get context around the BundleModuleLoader type definition (should be earlier in file)
sed -n '390,425p' packages/utils/src/import.ts | cat -n

Repository: eggjs/egg

Length of output: 1762


🏁 Script executed:

#!/bin/bash
# Search for any existing declare global blocks in the file
rg -n 'declare global' packages/utils/src/import.ts

Repository: eggjs/egg

Length of output: 35


🏁 Script executed:

#!/bin/bash
# Get the section with BundleModuleLoader export (likely around line 400 or earlier)
rg -B 5 -A 2 'export type BundleModuleLoader' packages/utils/src/import.ts

Repository: eggjs/egg

Length of output: 390


🏁 Script executed:

#!/bin/bash
# Double-check for any usages in plugins directory
rg -n '__EGG_BUNDLE_MODULE_LOADER__' plugins/ 2>/dev/null || echo "No matches in plugins/"

Repository: eggjs/egg

Length of output: 75


🏁 Script executed:

#!/bin/bash
# Verify the exact structure - show where BundleModuleLoader export appears
sed -n '400,420p' packages/utils/src/import.ts | cat -n

Repository: eggjs/egg

Length of output: 1158


Replace globalThis as any with a typed global declaration.

Lines 410 and 415 use as any casts, which weakens strict typing. The __EGG_BUNDLE_MODULE_LOADER__ global is only used within this file and can be safely typed via declare global.

♻️ Proposed refactor
 export type BundleModuleLoader = (filepath: string) => unknown;
 
+declare global {
+  // shared runtime hook across duplicated package instances
+  var __EGG_BUNDLE_MODULE_LOADER__: BundleModuleLoader | undefined;
+}
+
 /**
  * Register a bundle module loader. Uses globalThis so that bundled and
  * external copies of `@eggjs/utils` share the same loader.
  */
 export function setBundleModuleLoader(loader: BundleModuleLoader | undefined): void {
-  (globalThis as any).__EGG_BUNDLE_MODULE_LOADER__ = loader;
+  globalThis.__EGG_BUNDLE_MODULE_LOADER__ = loader;
   if (loader) isESM = false;
 }
 
 export async function importModule(filepath: string, options?: ImportModuleOptions): Promise<any> {
-  const _bundleModuleLoader: BundleModuleLoader | undefined = (globalThis as any).__EGG_BUNDLE_MODULE_LOADER__;
+  const _bundleModuleLoader = globalThis.__EGG_BUNDLE_MODULE_LOADER__;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/utils/src/import.ts` around lines 410 - 415, The code uses
(globalThis as any).__EGG_BUNDLE_MODULE_LOADER__ which weakens typing; declare a
typed global interface for __EGG_BUNDLE_MODULE_LOADER__ (e.g. interface Global {
__EGG_BUNDLE_MODULE_LOADER__?: BundleModuleLoader }) via declare global so you
can replace both (globalThis as any).__EGG_BUNDLE_MODULE_LOADER__ occurrences
with properly typed accesses, and keep the existing logic in importModule and
the loader assignment (references: __EGG_BUNDLE_MODULE_LOADER__,
BundleModuleLoader, importModule, loader, isESM).

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds a bundle-aware runtime hook to @eggjs/utils so a bundled Egg application can intercept importModule() calls and serve modules from an in-bundle map (avoiding disk-based resolution/scanning).

Changes:

  • Add BundleModuleLoader + setBundleModuleLoader() and consult the loader at the start of importModule().
  • Add Vitest coverage for bundle-loader behavior (hit, miss/fallthrough, default unwrapping, virtual paths).
  • Update the “export all” snapshot to include the new API.

Reviewed changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated 1 comment.

File Description
packages/utils/src/import.ts Introduces setBundleModuleLoader() (stored on globalThis) and adds a pre-importResolve interception path in importModule().
packages/utils/test/bundle-import.test.ts New tests validating bundle interception, default handling, fallthrough behavior, and virtual module paths.
packages/utils/test/snapshots/index.test.ts.snap Updates export snapshot to include setBundleModuleLoader.

Comment on lines +409 to +422
export function setBundleModuleLoader(loader: BundleModuleLoader | undefined): void {
(globalThis as any).__EGG_BUNDLE_MODULE_LOADER__ = loader;
if (loader) isESM = false;
}

export async function importModule(filepath: string, options?: ImportModuleOptions): Promise<any> {
const _bundleModuleLoader: BundleModuleLoader | undefined = (globalThis as any).__EGG_BUNDLE_MODULE_LOADER__;
if (_bundleModuleLoader) {
const hit = _bundleModuleLoader(filepath.replaceAll('\\', '/'));
if (hit !== undefined) {
let obj = hit as any;
if (obj?.default?.__esModule === true && 'default' in obj.default) obj = obj.default;
if (options?.importDefaultOnly && obj && typeof obj === 'object' && 'default' in obj) obj = obj.default;
return obj;
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants