diff --git a/packages/utils/src/import.ts b/packages/utils/src/import.ts index 4bed1dedb3..b1a2fea133 100644 --- a/packages/utils/src/import.ts +++ b/packages/utils/src/import.ts @@ -373,6 +373,17 @@ export function importResolve(filepath: string, options?: ImportResolveOptions): } } + // In bundle mode a module's source may not exist on disk (it is inlined into the + // bundle). After on-disk resolution has failed, if the registered bundle module + // loader recognizes the path, treat it as already resolved and return it as the + // canonical key, mirroring importModule. This must run before import.meta.resolve + // (which is unavailable in the bundled runtime). + const bundleModuleLoader = globalThis.__EGG_BUNDLE_MODULE_LOADER__; + if (bundleModuleLoader && bundleModuleLoader(normalizeBundleModulePath(filepath)) !== undefined) { + debug('[importResolve:bundle] %o => %o', filepath, filepath); + return filepath; + } + const extname = path.extname(filepath); if ((!isAbsolute && extname === '.json') || !isESM) { moduleFilePath = getRequire().resolve(filepath, { diff --git a/packages/utils/test/bundle-import.test.ts b/packages/utils/test/bundle-import.test.ts index bca31a3c3d..3b2881b0c5 100644 --- a/packages/utils/test/bundle-import.test.ts +++ b/packages/utils/test/bundle-import.test.ts @@ -5,7 +5,7 @@ import path from 'node:path'; import coffee from 'coffee'; import { afterEach, describe, it } from 'vitest'; -import { importModule, setBundleModuleLoader } from '../src/import.ts'; +import { importModule, importResolve, setBundleModuleLoader } from '../src/import.ts'; import { getFilepath } from './helper.ts'; describe('test/bundle-import.test.ts', () => { @@ -135,4 +135,40 @@ describe('test/bundle-import.test.ts', () => { const result = await importModule(filepath); assert.deepEqual(result, fakeModule); }); + + 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.match(resolved, /[\\/]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')); + }); });