Skip to content

fix(utils): resolve bundle-inlined modules in importResolve#5971

Merged
killagu merged 2 commits into
nextfrom
fix-utils-import-resolve-bundle
Jun 20, 2026
Merged

fix(utils): resolve bundle-inlined modules in importResolve#5971
killagu merged 2 commits into
nextfrom
fix-utils-import-resolve-bundle

Conversation

@killagu

@killagu killagu commented Jun 19, 2026

Copy link
Copy Markdown
Contributor

Motivation

In bundle mode a module's source may not exist on disk — it is inlined into the bundle. importResolve runs through its on-disk resolution paths (absolute / relative / node_modules), and on failure falls into import.meta.resolve, which is unavailable in the bundled runtime. As a result, resolving a bundle-only module throws even though importModule itself already knows how to serve it via the registered bundle module loader.

Scope

Add a bundle fallback to importResolve, symmetric with the existing one in importModule:

  • It runs after on-disk resolution has failed and before import.meta.resolve.
  • When globalThis.__EGG_BUNDLE_MODULE_LOADER__ is registered and recognizes normalizeBundleModulePath(filepath), the path is treated as already resolved and returned unchanged as the canonical key.
  • Paths are POSIX-normalized before the loader lookup, matching importModule.

The fallback only takes effect when a loader is registered. When no loader is registered, behavior is unchanged (the unregistered case still throws / falls back exactly as before) — covered by an explicit assertion.

Test evidence

Added importResolve coverage to packages/utils/test/bundle-import.test.ts:

  • returns the path as the canonical key for a bundle-only (not-on-disk) module
  • normalizes Windows-style paths before the bundle lookup
  • does not consult the loader once on-disk resolution succeeds
  • still throws for missing modules when no loader is registered
npx vitest run packages/utils/  →  70 passed | 13 skipped, 0 failed
npx oxlint --type-aware packages/utils → clean
tsgo --noEmit (packages/utils) → clean

This PR has no dependency on the other bundle-startup PRs and can land first.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Added a bundle-mode resolution step for module imports, enabling support for virtual modules available only in bundled environments.
  • Tests

    • Added coverage for bundle module resolution, including canonical key behavior, Windows-style path normalization, correct skipping when on-disk resolution succeeds, and error handling when no loader is registered.

Copilot AI review requested due to automatic review settings June 19, 2026 13:08

Copilot AI left a comment

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.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@coderabbitai

coderabbitai Bot commented Jun 19, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: ddbf3be8-edea-4244-aff9-2d0f47f87bea

📥 Commits

Reviewing files that changed from the base of the PR and between ffc6286 and b6724b3.

📒 Files selected for processing (1)
  • packages/utils/test/bundle-import.test.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • packages/utils/test/bundle-import.test.ts

📝 Walkthrough

Walkthrough

importResolve() in packages/utils/src/import.ts gains an early bundle-mode interception: it reads globalThis.__EGG_BUNDLE_MODULE_LOADER__, calls it with a POSIX-normalized filepath, and returns the original filepath immediately when the loader recognizes it. Four new test cases in bundle-import.test.ts cover the main branches of this behavior.

Changes

Bundle-mode interception in importResolve

Layer / File(s) Summary
Bundle-mode early exit in importResolve and test coverage
packages/utils/src/import.ts, packages/utils/test/bundle-import.test.ts
importResolve() checks globalThis.__EGG_BUNDLE_MODULE_LOADER__ with a POSIX-normalized path and returns filepath as the canonical key on a loader hit. Four new vitest cases validate virtual bundle specifiers, Windows path normalization, on-disk bypass of the loader, and error behavior with no loader registered.

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~10 minutes

Possibly related PRs

  • eggjs/egg#5867: Adds the same globalThis.__EGG_BUNDLE_MODULE_LOADER__ hook infrastructure in packages/utils/src/import.ts, which this PR extends to importResolve().
  • eggjs/egg#5932: Integrates globalThis.__EGG_BUNDLE_MODULE_LOADER__ with normalized paths in the tegg loader layer, mirroring the normalization pattern used in this PR's importResolve() interception.

Suggested reviewers

  • jerryliang64

Poem

🐇 Hopping through bundles, no disk to seek,
A virtual path found at the loader's peek.
Windows or POSIX, I normalize the way,
Return the key early — no resolve delay!
Carrots for caches, the tests all pass,
Bundle mode magic achieved at last! 🥕

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main change: adding bundle-inlined module resolution to importResolve.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

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

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix-utils-import-resolve-bundle

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.

@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Jun 19, 2026

Copy link
Copy Markdown

Deploying egg with  Cloudflare Pages  Cloudflare Pages

Latest commit: b6724b3
Status: ✅  Deploy successful!
Preview URL: https://ac704799.egg-cci.pages.dev
Branch Preview URL: https://fix-utils-import-resolve-bun.egg-cci.pages.dev

View logs

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

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.

Code Review

This pull request updates importResolve in packages/utils/src/import.ts to support bundle-only modules by checking a registered bundle module loader when on-disk resolution fails, returning the path as the canonical key. It also adds corresponding unit tests. The review feedback highlights a key limitation: the current implementation does not correctly resolve relative paths for bundle-only modules because it only checks the unresolved path against the loader. The reviewer suggests resolving relative paths against the provided paths option, including checking common file extensions, and provides code suggestions and test cases to address this issue.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

Comment on lines +375 to +379
const bundleModuleLoader = globalThis.__EGG_BUNDLE_MODULE_LOADER__;
if (bundleModuleLoader && bundleModuleLoader(normalizeBundleModulePath(filepath)) !== undefined) {
debug('[importResolve:bundle] %o => %o', filepath, filepath);
return filepath;
}

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

When resolving a relative path (e.g., ./foo) to a bundle-only module, the current implementation only checks the unresolved filepath against the bundle loader. Since the bundle loader typically indexes modules by their absolute canonical paths, this check will fail. We should resolve the relative path against the provided paths option and check those resolved absolute paths (along with common extensions) against the bundle loader to ensure correct resolution.

  const bundleModuleLoader = globalThis.__EGG_BUNDLE_MODULE_LOADER__;
  if (bundleModuleLoader) {
    const normalizedFilepath = normalizeBundleModulePath(filepath);
    if (bundleModuleLoader(normalizedFilepath) !== undefined) {
      debug('[importResolve:bundle] %o => %o', filepath, filepath);
      return filepath;
    }
    if (isRelativePath(filepath)) {
      for (const p of paths) {
        const resolvedPath = path.resolve(p, filepath);
        const normalizedResolved = normalizeBundleModulePath(resolvedPath);
        if (bundleModuleLoader(normalizedResolved) !== undefined) {
          debug('[importResolve:bundle:relative] %o => %o', filepath, resolvedPath);
          return resolvedPath;
        }
        for (const ext of [".js", ".ts", ".mjs", ".cjs", "/index.js", "/index.ts"]) {
          if (bundleModuleLoader(normalizedResolved + ext) !== undefined) {
            debug('[importResolve:bundle:relative] %o => %o', filepath, resolvedPath + ext);
            return resolvedPath + ext;
          }
        }
      }
    }
  }

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Thanks, but I am intentionally not resolving relative paths here. This fallback is deliberately symmetric with importModule, which keys the loader on normalizeBundleModulePath(filepath) using the raw (unresolved) filepath. importResolve returns that same filepath as the canonical key precisely so a subsequent importModule(filepath) looks the loader up with an identical key.

Resolving ./foo to an absolute path before the lookup would make importResolve return a different key than the one importModule keys on, breaking that contract. The bundle loader itself owns the path namespace and decides what it recognizes; both functions hand it the same path-as-passed. So this is by design rather than an oversight, and I am leaving the relative-path test out for the same reason.

Comment on lines +139 to +173
it('importResolve returns the path as canonical key for bundle-only modules', () => {
const seen: string[] = [];
setBundleModuleLoader((p) => {
seen.push(p);
return p === 'virtual/not-on-disk' ? { virtual: true } : undefined;
});

// The module is inlined into the bundle and has no source on disk, but the
// loader recognizes it, so importResolve hands it back unchanged.
assert.equal(importResolve('virtual/not-on-disk'), 'virtual/not-on-disk');
assert.ok(seen.includes('virtual/not-on-disk'));
});

it('importResolve normalizes Windows-style paths before the bundle lookup', () => {
setBundleModuleLoader((p) => (p === 'virtual/win/mod' ? { windows: true } : undefined));

const filepath = 'virtual/win/mod'.split(path.posix.sep).join(path.win32.sep);
assert.equal(importResolve(filepath), filepath);
});

it('importResolve does not consult the loader once on-disk resolution succeeds', () => {
let called = false;
setBundleModuleLoader(() => {
called = true;
return { hit: true };
});

const resolved = importResolve(getFilepath('esm'));
assert.ok(resolved.endsWith('/fixtures/esm/index.js'));
assert.equal(called, false);
});

it('importResolve still throws for missing modules when no loader is registered', () => {
assert.throws(() => importResolve('virtual/not-on-disk'));
});

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

Add a test case to verify that importResolve can correctly resolve relative paths for bundle-only modules against the paths option.

  it('importResolve returns the path as canonical key for bundle-only modules', () => {
    const seen: string[] = [];
    setBundleModuleLoader((p) => {
      seen.push(p);
      return p === 'virtual/not-on-disk' ? { virtual: true } : undefined;
    });

    // The module is inlined into the bundle and has no source on disk, but the
    // loader recognizes it, so importResolve hands it back unchanged.
    assert.equal(importResolve('virtual/not-on-disk'), 'virtual/not-on-disk');
    assert.ok(seen.includes('virtual/not-on-disk'));
  });

  it('importResolve resolves relative paths for bundle-only modules against paths option', () => {
    const absolutePath = path.resolve(process.cwd(), 'virtual/relative/mod.js');
    const normalizedAbsolute = absolutePath.split(path.win32.sep).join(path.posix.sep);
    setBundleModuleLoader((p) => (p === normalizedAbsolute ? { relative: true } : undefined));

    assert.equal(importResolve('./relative/mod', { paths: [path.resolve(process.cwd(), 'virtual')] }), absolutePath);
  });

  it('importResolve normalizes Windows-style paths before the bundle lookup', () => {
    setBundleModuleLoader((p) => (p === 'virtual/win/mod' ? { windows: true } : undefined));

    const filepath = 'virtual/win/mod'.split(path.posix.sep).join(path.win32.sep);
    assert.equal(importResolve(filepath), filepath);
  });

  it('importResolve does not consult the loader once on-disk resolution succeeds', () => {
    let called = false;
    setBundleModuleLoader(() => {
      called = true;
      return { hit: true };
    });

    const resolved = importResolve(getFilepath('esm'));
    assert.ok(resolved.endsWith('/fixtures/esm/index.js'));
    assert.equal(called, false);
  });

  it('importResolve still throws for missing modules when no loader is registered', () => {
    assert.throws(() => importResolve('virtual/not-on-disk'));
  });

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Skipping the relative-path test for the reason explained on the import.ts thread: the fallback is intentionally symmetric with importModule and keys the loader on the raw path-as-passed, so resolving relatives here would break the canonical-key contract. The other suggested cases are already covered by the existing tests.

@codecov

codecov Bot commented Jun 19, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 85.57%. Comparing base (7b062db) to head (b6724b3).

Additional details and impacted files
@@           Coverage Diff           @@
##             next    #5971   +/-   ##
=======================================
  Coverage   85.57%   85.57%           
=======================================
  Files         669      669           
  Lines       19849    19853    +4     
  Branches     3923     3924    +1     
=======================================
+ Hits        16985    16990    +5     
+ Misses       2475     2474    -1     
  Partials      389      389           

☔ View full report in Codecov by Harness.
📢 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.

@coderabbitai coderabbitai Bot left a comment

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.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@packages/utils/test/bundle-import.test.ts`:
- Around line 166-168: The assertion on the resolved path ending with
'/fixtures/esm/index.js' uses hardcoded POSIX separators which causes test
failures on Windows. Fix this by building the expected path string dynamically
using path utilities (such as path.sep or path.join) to ensure the correct path
separator is used for the current platform, or by normalizing both the resolved
and expected paths before comparison.
🪄 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: 387c3420-32e3-4738-ab79-161b5a055568

📥 Commits

Reviewing files that changed from the base of the PR and between 0333c73 and 30f2d5e.

📒 Files selected for processing (2)
  • packages/utils/src/import.ts
  • packages/utils/test/bundle-import.test.ts

Comment thread packages/utils/test/bundle-import.test.ts
@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Jun 19, 2026

Copy link
Copy Markdown

Deploying egg-v3 with  Cloudflare Pages  Cloudflare Pages

Latest commit: b6724b3
Status: ✅  Deploy successful!
Preview URL: https://7475084e.egg-v3.pages.dev
Branch Preview URL: https://fix-utils-import-resolve-bun.egg-v3.pages.dev

View logs

In bundle mode a module's source may not exist on disk, so on-disk
resolution fails and importResolve falls into import.meta.resolve, which
is unavailable in the bundled runtime. Add a fallback that runs after
disk resolution fails and before import.meta.resolve: when a bundle
module loader is registered and recognizes the normalized path, return
the path unchanged as the canonical key, mirroring importModule.

The fallback only takes effect when __EGG_BUNDLE_MODULE_LOADER__ is
registered; behavior is unchanged otherwise. Adds importResolve coverage
for the registered, Windows-path, disk-hit, and unregistered cases.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@killagu killagu force-pushed the fix-utils-import-resolve-bundle branch from 30f2d5e to ffc6286 Compare June 20, 2026 13:44
Use a separator-agnostic regex instead of a hardcoded POSIX-separator
endsWith() so the test holds on Windows too.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings June 20, 2026 13:46

Copilot AI left a comment

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.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@killagu killagu merged commit dfc15a3 into next Jun 20, 2026
36 of 37 checks passed
@killagu killagu deleted the fix-utils-import-resolve-bundle branch June 20, 2026 14:49
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