From e06769ede22e7336bf05b2c966faa4c377fa8fce Mon Sep 17 00:00:00 2001 From: killa Date: Sun, 10 May 2026 14:56:59 +0800 Subject: [PATCH 1/5] fix: route loader runtime through manifest fs --- packages/core/src/global.ts | 1 + packages/core/src/loader/egg_loader.ts | 50 ++++---- packages/core/src/loader/loader_fs.ts | 120 ++++++++++++++++++ packages/core/test/loader/egg_loader.test.ts | 23 ++++ packages/core/test/loader/loader_fs.test.ts | 45 ++++++- tools/egg-bundler/src/lib/EntryGenerator.ts | 53 +++++++- tools/egg-bundler/test/EntryGenerator.test.ts | 23 ++++ .../EntryGenerator.worker.canonical.snap | 22 ++++ 8 files changed, 312 insertions(+), 25 deletions(-) diff --git a/packages/core/src/global.ts b/packages/core/src/global.ts index 7c8b22972a..14ed6a6d46 100644 --- a/packages/core/src/global.ts +++ b/packages/core/src/global.ts @@ -2,6 +2,7 @@ import type { ManifestStore } from './loader/manifest.ts'; declare global { var __EGG_BUNDLE_STORE__: ManifestStore | undefined; + var __EGG_BUNDLE_FILE_LOADER__: ((filepath: string) => string | undefined) | undefined; } export {}; diff --git a/packages/core/src/loader/egg_loader.ts b/packages/core/src/loader/egg_loader.ts index 57b892ea55..b0ac9f3337 100644 --- a/packages/core/src/loader/egg_loader.ts +++ b/packages/core/src/loader/egg_loader.ts @@ -1,5 +1,4 @@ import assert from 'node:assert'; -import fs from 'node:fs'; import path from 'node:path'; import { debuglog, inspect } from 'node:util'; @@ -8,12 +7,11 @@ import { Request, Response, Application, Context as KoaContext } from '@eggjs/ko import { pathMatching, type PathMatchingOptions } from '@eggjs/path-matching'; import { isESM, isSupportTypeScript } from '@eggjs/utils'; import type { Logger } from 'egg-logger'; -import globby from 'globby'; import { isAsyncFunction, isClass, isGeneratorFunction, isObject, isPromise } from 'is-type-of'; import { homedir } from 'node-homedir'; import { now, diff } from 'performance-ms'; import { register as tsconfigPathsRegister } from 'tsconfig-paths'; -import { getParamNames, readJSONSync, readJSON, exists } from 'utility'; +import { getParamNames } from 'utility'; import type { BaseContextClass } from '../base_context_class.ts'; import type { Context, EggCore, MiddlewareFunc } from '../egg.ts'; @@ -24,7 +22,7 @@ import { sequencify } from '../utils/sequencify.ts'; import { Timing } from '../utils/timing.ts'; import { type ContextLoaderOptions, ContextLoader } from './context_loader.ts'; import { type FileLoaderOptions, CaseStyle, FULLPATH, FileLoader } from './file_loader.ts'; -import { RealLoaderFS, type LoaderFS } from './loader_fs.ts'; +import { ManifestLoaderFS, RealLoaderFS, type LoaderFS } from './loader_fs.ts'; import { ManifestStore, type StartupManifest } from './manifest.ts'; const debug = debuglog('egg/core/loader/egg_loader'); @@ -82,7 +80,7 @@ export class EggLoader { dirs?: EggDirInfo[]; /** Startup manifest — loaded from cache or collecting for generation */ readonly manifest: ManifestStore; - readonly loaderFS: LoaderFS; + loaderFS: LoaderFS; /** * @class @@ -96,7 +94,7 @@ export class EggLoader { constructor(options: EggLoaderOptions) { this.options = options; this.loaderFS = this.options.loaderFS ?? new RealLoaderFS(); - assert(fs.existsSync(this.options.baseDir), `${this.options.baseDir} not exists`); + assert(this.loaderFS.exists(this.options.baseDir), `${this.options.baseDir} not exists`); assert(this.options.app, 'options.app is required'); assert(this.options.logger, 'options.logger is required'); @@ -107,7 +105,7 @@ export class EggLoader { * @see {@link AppInfo#pkg} * @since 1.0.0 */ - this.pkg = readJSONSync(path.join(this.options.baseDir, 'package.json')); + this.pkg = this.loaderFS.readJSON(path.join(this.options.baseDir, 'package.json')); this.outDir = this.#resolveOutDir(); // auto require('tsconfig-paths/register') on typescript app @@ -115,7 +113,7 @@ export class EggLoader { if (process.env.EGG_TYPESCRIPT === 'true' || (this.pkg.egg && this.pkg.egg.typescript)) { // skip require tsconfig-paths if tsconfig.json not exists const tsConfigFile = path.join(this.options.baseDir, 'tsconfig.json'); - if (fs.existsSync(tsConfigFile)) { + if (this.loaderFS.exists(tsConfigFile)) { // @ts-expect-error only cwd is required tsconfigPathsRegister({ cwd: this.options.baseDir }); } else { @@ -176,6 +174,9 @@ export class EggLoader { this.manifest = ManifestStore.load(this.options.baseDir, this.serverEnv, this.serverScope) ?? ManifestStore.createCollector(this.options.baseDir); + if (!this.options.loaderFS) { + this.loaderFS = new ManifestLoaderFS(this.options.baseDir, this.manifest, this.loaderFS); + } } get app(): EggCore { @@ -201,8 +202,8 @@ export class EggLoader { let serverEnv = this.options.env; const envPath = path.join(this.options.baseDir, 'config/env'); - if (!serverEnv && fs.existsSync(envPath)) { - serverEnv = fs.readFileSync(envPath, 'utf8').trim(); + if (!serverEnv && this.loaderFS.exists(envPath)) { + serverEnv = this.loaderFS.readFile(envPath, 'utf8').trim(); } if (!serverEnv && process.env.EGG_SERVER_ENV) { @@ -378,8 +379,8 @@ export class EggLoader { ); } assert(typeof eggPath === 'string', "Symbol.for('egg#eggPath') should be string"); - assert(fs.existsSync(eggPath), `${eggPath} not exists`); - const realpath = fs.realpathSync(eggPath); + assert(this.loaderFS.exists(eggPath), `${eggPath} not exists`); + const realpath = this.loaderFS.realpath(eggPath); if (!eggPaths.includes(realpath)) { eggPaths.unshift(realpath); } @@ -594,7 +595,10 @@ export class EggLoader { continue; } - const config: Record = await utils.loadFile(filepath); + const config: Record = (await this.loaderFS.loadFile(filepath)) as Record< + string, + EggPluginInfo + >; for (const name in config) { this.#normalizePluginConfig(config, name, filepath); } @@ -643,9 +647,9 @@ export class EggLoader { async #mergePluginConfig(plugin: EggPluginInfo): Promise { let pkg: any; let eggPluginConfig: any; - const pluginPackage = path.join(plugin.path as string, 'package.json'); - if (await utils.existsPath(pluginPackage)) { - pkg = await readJSON(pluginPackage); + const pluginPackage = this.resolveModule(path.join(plugin.path as string, 'package.json')); + if (pluginPackage && this.loaderFS.exists(pluginPackage)) { + pkg = this.loaderFS.readJSON(pluginPackage); eggPluginConfig = pkg.eggPlugin; if (pkg.version) { plugin.version = pkg.version; @@ -848,7 +852,7 @@ export class EggLoader { } else if (exports.require) { realPluginPath = path.join(pluginPath, exports.require); } - if (exports.typescript && isSupportTypeScript() && !(await exists(realPluginPath))) { + if (exports.typescript && isSupportTypeScript() && !this.loaderFS.exists(realPluginPath)) { // if require/import path not exists, use typescript path for development stage realPluginPath = path.join(pluginPath, exports.typescript); debug('[formatPluginPathFromPackageJSON] use typescript path %o', realPluginPath); @@ -1585,7 +1589,7 @@ export class EggLoader { async requireFile(filepath: string): Promise { const timingKey = `Require(${this.#requiredCount++}) ${utils.getResolvedFilename(filepath, this.options.baseDir)}`; this.timing.start(timingKey); - const mod = await utils.loadFile(filepath); + const mod = await this.loaderFS.loadFile(filepath); this.timing.end(timingKey); return mod; } @@ -1746,9 +1750,9 @@ export class EggLoader { } // 2. Auto-detect from tsconfig.json compilerOptions.outDir const tsConfigFile = path.join(this.options.baseDir, 'tsconfig.json'); - if (fs.existsSync(tsConfigFile)) { + if (this.loaderFS.exists(tsConfigFile)) { try { - const tsConfig = JSON.parse(fs.readFileSync(tsConfigFile, 'utf-8')); + const tsConfig = JSON.parse(this.loaderFS.readFile(tsConfigFile, 'utf-8')); if (tsConfig.compilerOptions?.outDir) { debug('[resolveOutDir] use tsconfig.json compilerOptions.outDir: %o', tsConfig.compilerOptions.outDir); return tsConfig.compilerOptions.outDir; @@ -1766,7 +1770,7 @@ export class EggLoader { const relativePath = path.relative(baseDir, filepath); for (const ext of ['.js', '.mjs']) { const outDirPath = path.join(baseDir, this.outDir, relativePath + ext); - if (fs.existsSync(outDirPath)) { + if (this.loaderFS.exists(outDirPath)) { debug('[resolveModule:outDir] %o => %o', filepath, outDirPath); return outDirPath; } @@ -1832,8 +1836,8 @@ export class EggLoader { if (Object.hasOwn(manifest.fileDiscovery, dirKey)) return manifest.fileDiscovery[dirKey]; manifest.fileDiscovery[dirKey] = - fs.existsSync(directory) && fs.statSync(directory).isDirectory() - ? globby.sync(FileLoader.getDefaultMatch(), { cwd: directory }).sort() + this.loaderFS.exists(directory) && this.loaderFS.stat(directory).isDirectory() + ? this.loaderFS.glob(FileLoader.getDefaultMatch(), { cwd: directory }).sort() : []; return manifest.fileDiscovery[dirKey]; } diff --git a/packages/core/src/loader/loader_fs.ts b/packages/core/src/loader/loader_fs.ts index 8446c60c1a..e521c8a36a 100644 --- a/packages/core/src/loader/loader_fs.ts +++ b/packages/core/src/loader/loader_fs.ts @@ -1,9 +1,11 @@ import fs, { type Stats } from 'node:fs'; +import path from 'node:path'; import globby from 'globby'; import { readJSONSync } from 'utility'; import utils from '../utils/index.ts'; +import type { ManifestStore } from './manifest.ts'; export type LoaderFSGlobOptions = globby.GlobbyOptions; @@ -11,6 +13,8 @@ export interface LoaderFS { exists(filepath: string): boolean; stat(filepath: string): Stats; realpath(filepath: string): string; + readFile(filepath: string): Buffer; + readFile(filepath: string, encoding: BufferEncoding): string; readJSON(filepath: string): T; glob(patterns: string | string[], options?: LoaderFSGlobOptions): string[]; loadFile(filepath: string): Promise; @@ -29,6 +33,12 @@ export class RealLoaderFS implements LoaderFS { return fs.realpathSync(filepath); } + readFile(filepath: string): Buffer; + readFile(filepath: string, encoding: BufferEncoding): string; + readFile(filepath: string, encoding?: BufferEncoding): Buffer | string { + return encoding ? fs.readFileSync(filepath, encoding) : fs.readFileSync(filepath); + } + readJSON(filepath: string): T { return readJSONSync(filepath) as T; } @@ -41,3 +51,113 @@ export class RealLoaderFS implements LoaderFS { return utils.loadFile(filepath); } } + +export class ManifestLoaderFS implements LoaderFS { + readonly #baseDir: string; + readonly #manifest: ManifestStore; + readonly #delegate: LoaderFS; + + constructor(baseDir: string, manifest: ManifestStore, delegate: LoaderFS = new RealLoaderFS()) { + this.#baseDir = baseDir; + this.#manifest = manifest; + this.#delegate = delegate; + } + + exists(filepath: string): boolean { + if (this.#bundleFileText(filepath) !== undefined || this.#isManifestKnown(filepath)) return true; + return this.#delegate.exists(filepath); + } + + stat(filepath: string): Stats { + const type = this.#manifestEntryType(filepath); + if (type) return this.#syntheticStats(type); + return this.#delegate.stat(filepath); + } + + realpath(filepath: string): string { + if (this.#bundleFileText(filepath) !== undefined || this.#isManifestKnown(filepath)) return filepath; + return this.#delegate.realpath(filepath); + } + + readFile(filepath: string): Buffer; + readFile(filepath: string, encoding: BufferEncoding): string; + readFile(filepath: string, encoding?: BufferEncoding): Buffer | string { + const text = this.#bundleFileText(filepath); + if (text !== undefined) { + return encoding ? text : Buffer.from(text); + } + return encoding ? this.#delegate.readFile(filepath, encoding) : this.#delegate.readFile(filepath); + } + + readJSON(filepath: string): T { + const text = this.#bundleFileText(filepath); + if (text !== undefined) return JSON.parse(text) as T; + return this.#delegate.readJSON(filepath); + } + + glob(patterns: string | string[], options?: LoaderFSGlobOptions): string[] { + return this.#delegate.glob(patterns, options); + } + + async loadFile(filepath: string): Promise { + const normalized = this.#normalize(filepath); + const bundleHit = globalThis.__EGG_BUNDLE_MODULE_LOADER__?.(normalized); + if (bundleHit !== undefined) return this.#unwrapDefault(bundleHit); + + const text = this.#bundleFileText(filepath); + if (text !== undefined) return Buffer.from(text); + + return this.#delegate.loadFile(filepath); + } + + #bundleFileText(filepath: string): string | undefined { + const loader = globalThis.__EGG_BUNDLE_FILE_LOADER__; + if (!loader) return; + return loader(this.#normalize(filepath)); + } + + #isManifestKnown(filepath: string): boolean { + return this.#manifestEntryType(filepath) !== undefined; + } + + #manifestEntryType(filepath: string): 'file' | 'directory' | undefined { + const rel = this.#toRelative(filepath); + if (rel === '' || this.#manifest.data.fileDiscovery[rel]) return 'directory'; + + for (const [dir, files] of Object.entries(this.#manifest.data.fileDiscovery)) { + if (files.includes(path.posix.relative(dir, rel))) return 'file'; + } + + for (const value of Object.values(this.#manifest.data.resolveCache)) { + if (value && this.#normalize(value) === rel) return 'file'; + } + } + + #syntheticStats(type: 'file' | 'directory'): Stats { + return { + isFile: () => type === 'file', + isDirectory: () => type === 'directory', + isBlockDevice: () => false, + isCharacterDevice: () => false, + isSymbolicLink: () => false, + isFIFO: () => false, + isSocket: () => false, + } as Stats; + } + + #toRelative(filepath: string): string { + const rel = path.isAbsolute(filepath) ? path.relative(this.#baseDir, filepath) : filepath; + return this.#normalize(rel); + } + + #normalize(filepath: string): string { + return filepath.split(path.win32.sep).join(path.posix.sep); + } + + #unwrapDefault(obj: unknown): unknown { + if (obj && typeof obj === 'object' && 'default' in obj && (obj as { default?: unknown }).default !== undefined) { + return (obj as { default: unknown }).default; + } + return obj; + } +} diff --git a/packages/core/test/loader/egg_loader.test.ts b/packages/core/test/loader/egg_loader.test.ts index 13c7f6630b..2f841dfe6c 100644 --- a/packages/core/test/loader/egg_loader.test.ts +++ b/packages/core/test/loader/egg_loader.test.ts @@ -86,6 +86,29 @@ describe('test/loader/egg_loader.test.ts', () => { assert.equal(ret[0], 1); assert.equal(ret[1], 2); }); + + it('should load resolved files through loaderFS', async () => { + const baseDir = getFilepath('load_file'); + const calls: string[] = []; + class RecordingLoaderFS extends RealLoaderFS { + async loadFile(filepath: string) { + calls.push(filepath); + return super.loadFile(filepath); + } + } + const loader = new EggLoader({ + env: 'unittest', + baseDir, + app: {}, + logger: console, + loaderFS: new RecordingLoaderFS(), + } as any); + + const ret = await loader.loadFile(path.join(baseDir, 'function.js'), 1, 2); + + assert.deepEqual(ret, [1, 2]); + assert(calls.some((filepath) => filepath.endsWith(path.join('load_file', 'function.js')))); + }); }); it('should be loaded by loadToApp, support symbol property', async () => { diff --git a/packages/core/test/loader/loader_fs.test.ts b/packages/core/test/loader/loader_fs.test.ts index f029e433c0..25c2e7595f 100644 --- a/packages/core/test/loader/loader_fs.test.ts +++ b/packages/core/test/loader/loader_fs.test.ts @@ -5,7 +5,8 @@ import path from 'node:path'; import globby from 'globby'; import { describe, it } from 'vitest'; -import { RealLoaderFS } from '../../src/loader/loader_fs.ts'; +import { ManifestLoaderFS, RealLoaderFS } from '../../src/loader/loader_fs.ts'; +import { ManifestStore, type StartupManifest } from '../../src/loader/manifest.ts'; import utils from '../../src/utils/index.ts'; import { getFilepath } from '../helper.ts'; @@ -33,4 +34,46 @@ describe('test/loader/loader_fs.test.ts', () => { await utils.loadFile(path.join(baseDir, 'object.js')), ); }); + + it('should answer manifest-backed file stats without touching the delegate filesystem', () => { + const manifest: StartupManifest = { + version: 1, + generatedAt: '2026-05-10T00:00:00.000Z', + invalidation: { + lockfileFingerprint: '', + configFingerprint: '', + serverEnv: 'prod', + serverScope: '', + typescriptEnabled: false, + }, + extensions: {}, + resolveCache: { + 'config/plugin': 'config/plugin.js', + }, + fileDiscovery: { + 'app/controller': ['home.js'], + }, + }; + class NoStatLoaderFS extends RealLoaderFS { + stat(): fs.Stats { + throw new Error('delegate stat should not run for manifest-known paths'); + } + + exists(): boolean { + throw new Error('delegate exists should not run for manifest-known paths'); + } + } + const manifestFS = new ManifestLoaderFS( + '/bundle/app', + ManifestStore.fromBundle(manifest, '/bundle/app'), + new NoStatLoaderFS(), + ); + + assert.equal(manifestFS.exists('/bundle/app/app/controller'), true); + assert.equal(manifestFS.stat('/bundle/app/app/controller').isDirectory(), true); + assert.equal(manifestFS.exists('/bundle/app/app/controller/home.js'), true); + assert.equal(manifestFS.stat('/bundle/app/app/controller/home.js').isFile(), true); + assert.equal(manifestFS.exists('/bundle/app/config/plugin.js'), true); + assert.equal(manifestFS.stat('/bundle/app/config/plugin.js').isFile(), true); + }); }); diff --git a/tools/egg-bundler/src/lib/EntryGenerator.ts b/tools/egg-bundler/src/lib/EntryGenerator.ts index b533f100ef..effe2ddec3 100644 --- a/tools/egg-bundler/src/lib/EntryGenerator.ts +++ b/tools/egg-bundler/src/lib/EntryGenerator.ts @@ -1,3 +1,4 @@ +import { readFileSync } from 'node:fs'; import fs from 'node:fs/promises'; import { createRequire } from 'node:module'; import path from 'node:path'; @@ -35,6 +36,11 @@ interface BundleEntry { bareSpecifier?: string; } +interface BundleTextFile { + relKey: string; + text: string; +} + interface TeggModuleDescriptor { unitPath: string; decoratedFiles?: string[]; @@ -89,7 +95,7 @@ export class EntryGenerator { // 2. Every non-null resolveCache target (extensions, plugin app.ts, middlewares…) for (const value of Object.values(manifest.resolveCache)) { - if (value) this.#addEntry(map, value); + if (value && !this.#isTextFileEntry(value)) this.#addEntry(map, value); } // 3. Tegg decorated files (unitPath is either absolute or node_modules-normalized) @@ -110,6 +116,22 @@ export class EntryGenerator { }); } + #collectBundleTextFiles(manifest: StartupManifest): BundleTextFile[] { + const keys = new Set(); + for (const value of Object.values(manifest.resolveCache)) { + if (value && this.#isTextFileEntry(value)) keys.add(value.replaceAll(path.sep, '/')); + } + + return [...keys].sort().map((relKey) => ({ + relKey, + text: readFileSync(this.#absFromRelKey(relKey), 'utf8'), + })); + } + + #isTextFileEntry(relKey: string): boolean { + return relKey.endsWith('/package.json') || relKey === 'package.json'; + } + #addEntry(map: Map, relKey: string): void { const normalized = relKey.replaceAll(path.sep, '/'); if (map.has(normalized)) return; @@ -218,6 +240,7 @@ export class EntryGenerator { const importLines: string[] = []; const mapLines: string[] = []; const externalSpecs: Array<[string, string]> = []; + const textFiles = this.#collectBundleTextFiles(manifest); let internalIdx = 0; for (const entry of entries) { @@ -238,6 +261,12 @@ export class EntryGenerator { ), ); const appResolveCacheAliases = JSON.stringify(this.#collectResolveCacheAliases(manifest)); + const appTextFileAliases = JSON.stringify( + this.#uniqueAliasPairs( + textFiles.flatMap((file) => this.#absoluteAliasKeys(file.relKey).map((abs) => [abs, file.relKey])), + ), + ); + const textFileMap = JSON.stringify(Object.fromEntries(textFiles.map((file) => [file.relKey, file.text]))); const frameworkSpec = JSON.stringify(this.#framework); const externalBlock = @@ -274,17 +303,24 @@ const __framework = ${frameworkSpec}; const MANIFEST_DATA = ${manifestJson} as const; const __APP_ABSOLUTE_ALIASES: Array<[string, string]> = ${appAbsoluteAliases}; const __APP_RESOLVE_CACHE_ALIASES: Array<[string, string]> = ${appResolveCacheAliases}; +const __APP_TEXT_FILE_ALIASES: Array<[string, string]> = ${appTextFileAliases}; const __BUNDLE_MAP_REL: Record = { ${mapLines.join('\n')} }; +const __BUNDLE_TEXT_FILE_REL: Record = ${textFileMap}; ${externalBlock} const __BUNDLE_MAP: Record = {}; +const __BUNDLE_TEXT_FILE: Record = {}; const __normalizeBundleKey = (filepath: string) => filepath.split(path.sep).join('/'); const __setBundleMap = (filepath: string, mod: unknown) => { __BUNDLE_MAP[__normalizeBundleKey(filepath)] = mod; }; const __getBundleMap = (filepath: string) => __BUNDLE_MAP[__normalizeBundleKey(filepath)]; +const __setBundleTextFile = (filepath: string, text: string) => { + __BUNDLE_TEXT_FILE[__normalizeBundleKey(filepath)] = text; +}; +const __getBundleTextFile = (filepath: string) => __BUNDLE_TEXT_FILE[__normalizeBundleKey(filepath)]; const __setBundleAliases = (rel: string, mod: unknown) => { __setBundleMap(rel, mod); if (!path.isAbsolute(rel)) { @@ -295,6 +331,12 @@ __setBundleMap(__framework, __frameworkModule); for (const [rel, mod] of Object.entries(__BUNDLE_MAP_REL)) { __setBundleAliases(rel, mod); } +for (const [rel, text] of Object.entries(__BUNDLE_TEXT_FILE_REL)) { + __setBundleTextFile(rel, text); + if (!path.isAbsolute(rel)) { + __setBundleTextFile(path.resolve(__outputDir, rel), text); + } +} for (const [appAbs, targetRel] of __APP_ABSOLUTE_ALIASES) { const mod = __getBundleMap(targetRel); if (mod !== undefined) { @@ -314,8 +356,17 @@ for (const [appAbsRequest, targetRel] of __APP_RESOLVE_CACHE_ALIASES) { __setBundleMap(appAbsRequest, mod); } } +for (const [appAbs, targetRel] of __APP_TEXT_FILE_ALIASES) { + const text = __getBundleTextFile(targetRel); + if (text !== undefined) { + __setBundleTextFile(appAbs, text); + } +} ManifestStore.setBundleStore(ManifestStore.fromBundle(MANIFEST_DATA as any, __outputDir)); +globalThis.__EGG_BUNDLE_FILE_LOADER__ = (filepath) => { + return __getBundleTextFile(filepath); +}; globalThis.__EGG_BUNDLE_MODULE_LOADER__ = (filepath) => { return __getBundleMap(filepath); }; diff --git a/tools/egg-bundler/test/EntryGenerator.test.ts b/tools/egg-bundler/test/EntryGenerator.test.ts index 6d3560c1d2..3ecb985cb2 100644 --- a/tools/egg-bundler/test/EntryGenerator.test.ts +++ b/tools/egg-bundler/test/EntryGenerator.test.ts @@ -202,6 +202,29 @@ describe('EntryGenerator', () => { expect(imports.some((i) => i.specifier.includes('is-null-entry'))).toBe(false); }); + it('emits package.json resolve targets through the bundled text file loader', async () => { + await fs.mkdir(path.join(tmpDir, 'node_modules/fake-plugin'), { recursive: true }); + await fs.writeFile( + path.join(tmpDir, 'node_modules/fake-plugin/package.json'), + JSON.stringify({ name: 'fake-plugin', version: '1.0.0' }), + ); + const manifest = makeManifest({ + resolveCache: { + 'node_modules/fake-plugin/package.json': 'node_modules/fake-plugin/package.json', + }, + }); + + const gen = new EntryGenerator({ baseDir: tmpDir, manifestLoader: createFakeLoader(manifest) }); + const result = await gen.generate(); + const worker = await fs.readFile(result.workerEntry, 'utf8'); + + expect(extractImports(worker)).toEqual([]); + expect(worker).toContain('__EGG_BUNDLE_FILE_LOADER__'); + expect(worker).toContain('__BUNDLE_TEXT_FILE_REL'); + expect(worker).toContain('"node_modules/fake-plugin/package.json"'); + expect(worker).toContain('\\"version\\":\\"1.0.0\\"'); + }); + it('deduplicates entries that appear in both fileDiscovery and resolveCache', async () => { const manifest = makeManifest({ fileDiscovery: { 'app/controller': ['home.ts'] }, diff --git a/tools/egg-bundler/test/__snapshots__/EntryGenerator.worker.canonical.snap b/tools/egg-bundler/test/__snapshots__/EntryGenerator.worker.canonical.snap index 4b2f8e79e3..0689b91948 100644 --- a/tools/egg-bundler/test/__snapshots__/EntryGenerator.worker.canonical.snap +++ b/tools/egg-bundler/test/__snapshots__/EntryGenerator.worker.canonical.snap @@ -55,6 +55,7 @@ const MANIFEST_DATA = { } as const; const __APP_ABSOLUTE_ALIASES: Array<[string, string]> = [["/app/controller/home.ts","app/controller/home.ts"],["/app/extend/context.ts","app/extend/context.ts"],["/app/service/user.ts","app/service/user.ts"],["/node_modules/@eggjs/fake-module/app/service/UserService.ts","node_modules/@eggjs/fake-module/app/service/UserService.ts"]]; const __APP_RESOLVE_CACHE_ALIASES: Array<[string, string]> = [["/app/extend/context.ts","app/extend/context.ts"]]; +const __APP_TEXT_FILE_ALIASES: Array<[string, string]> = []; const __BUNDLE_MAP_REL: Record = { ["app/controller/home.ts"]: __m0, @@ -62,13 +63,19 @@ const __BUNDLE_MAP_REL: Record = { ["app/service/user.ts"]: __m2, ["node_modules/@eggjs/fake-module/app/service/UserService.ts"]: __m3, }; +const __BUNDLE_TEXT_FILE_REL: Record = {}; const __BUNDLE_MAP: Record = {}; +const __BUNDLE_TEXT_FILE: Record = {}; const __normalizeBundleKey = (filepath: string) => filepath.split(path.sep).join('/'); const __setBundleMap = (filepath: string, mod: unknown) => { __BUNDLE_MAP[__normalizeBundleKey(filepath)] = mod; }; const __getBundleMap = (filepath: string) => __BUNDLE_MAP[__normalizeBundleKey(filepath)]; +const __setBundleTextFile = (filepath: string, text: string) => { + __BUNDLE_TEXT_FILE[__normalizeBundleKey(filepath)] = text; +}; +const __getBundleTextFile = (filepath: string) => __BUNDLE_TEXT_FILE[__normalizeBundleKey(filepath)]; const __setBundleAliases = (rel: string, mod: unknown) => { __setBundleMap(rel, mod); if (!path.isAbsolute(rel)) { @@ -79,6 +86,12 @@ __setBundleMap(__framework, __frameworkModule); for (const [rel, mod] of Object.entries(__BUNDLE_MAP_REL)) { __setBundleAliases(rel, mod); } +for (const [rel, text] of Object.entries(__BUNDLE_TEXT_FILE_REL)) { + __setBundleTextFile(rel, text); + if (!path.isAbsolute(rel)) { + __setBundleTextFile(path.resolve(__outputDir, rel), text); + } +} for (const [appAbs, targetRel] of __APP_ABSOLUTE_ALIASES) { const mod = __getBundleMap(targetRel); if (mod !== undefined) { @@ -98,8 +111,17 @@ for (const [appAbsRequest, targetRel] of __APP_RESOLVE_CACHE_ALIASES) { __setBundleMap(appAbsRequest, mod); } } +for (const [appAbs, targetRel] of __APP_TEXT_FILE_ALIASES) { + const text = __getBundleTextFile(targetRel); + if (text !== undefined) { + __setBundleTextFile(appAbs, text); + } +} ManifestStore.setBundleStore(ManifestStore.fromBundle(MANIFEST_DATA as any, __outputDir)); +globalThis.__EGG_BUNDLE_FILE_LOADER__ = (filepath) => { + return __getBundleTextFile(filepath); +}; globalThis.__EGG_BUNDLE_MODULE_LOADER__ = (filepath) => { return __getBundleMap(filepath); }; From 15bbd470b98ae6741fa18851607fc92a86b8edd5 Mon Sep 17 00:00:00 2001 From: killa Date: Sun, 10 May 2026 15:02:22 +0800 Subject: [PATCH 2/5] test: update core export snapshot --- packages/core/test/__snapshots__/index.test.ts.snap | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/core/test/__snapshots__/index.test.ts.snap b/packages/core/test/__snapshots__/index.test.ts.snap index fc1467a86e..02ac30dd77 100644 --- a/packages/core/test/__snapshots__/index.test.ts.snap +++ b/packages/core/test/__snapshots__/index.test.ts.snap @@ -18,6 +18,7 @@ exports[`should expose properties 1`] = ` "KoaRequest", "KoaResponse", "Lifecycle", + "ManifestLoaderFS", "ManifestStore", "RealLoaderFS", "Request", From e65f8c6fc5e36ec6499ca43e71fa6011cbda4311 Mon Sep 17 00:00:00 2001 From: killa Date: Sun, 10 May 2026 15:05:43 +0800 Subject: [PATCH 3/5] fix: address manifest loader review feedback --- packages/core/src/loader/egg_loader.ts | 8 +- packages/core/src/loader/loader_fs.ts | 85 +++++++++++++++++--- packages/core/test/loader/egg_loader.test.ts | 42 ++++++++++ packages/core/test/loader/loader_fs.test.ts | 46 ++++++++++- 4 files changed, 167 insertions(+), 14 deletions(-) diff --git a/packages/core/src/loader/egg_loader.ts b/packages/core/src/loader/egg_loader.ts index b0ac9f3337..91bf6d9d19 100644 --- a/packages/core/src/loader/egg_loader.ts +++ b/packages/core/src/loader/egg_loader.ts @@ -94,6 +94,11 @@ export class EggLoader { constructor(options: EggLoaderOptions) { this.options = options; this.loaderFS = this.options.loaderFS ?? new RealLoaderFS(); + const bundleManifest = !this.options.loaderFS ? ManifestStore.getBundleStore() : undefined; + const earlyManifest = bundleManifest?.baseDir === this.options.baseDir ? bundleManifest : undefined; + if (earlyManifest) { + this.loaderFS = new ManifestLoaderFS(this.options.baseDir, earlyManifest, this.loaderFS); + } assert(this.loaderFS.exists(this.options.baseDir), `${this.options.baseDir} not exists`); assert(this.options.app, 'options.app is required'); assert(this.options.logger, 'options.logger is required'); @@ -172,9 +177,10 @@ export class EggLoader { // Load pre-computed manifest or create a collector for future generation this.manifest = + earlyManifest ?? ManifestStore.load(this.options.baseDir, this.serverEnv, this.serverScope) ?? ManifestStore.createCollector(this.options.baseDir); - if (!this.options.loaderFS) { + if (!this.options.loaderFS && !earlyManifest) { this.loaderFS = new ManifestLoaderFS(this.options.baseDir, this.manifest, this.loaderFS); } } diff --git a/packages/core/src/loader/loader_fs.ts b/packages/core/src/loader/loader_fs.ts index e521c8a36a..7dc73518f0 100644 --- a/packages/core/src/loader/loader_fs.ts +++ b/packages/core/src/loader/loader_fs.ts @@ -56,11 +56,14 @@ export class ManifestLoaderFS implements LoaderFS { readonly #baseDir: string; readonly #manifest: ManifestStore; readonly #delegate: LoaderFS; + readonly #knownFiles = new Set(); + readonly #knownDirs = new Set(); constructor(baseDir: string, manifest: ManifestStore, delegate: LoaderFS = new RealLoaderFS()) { this.#baseDir = baseDir; this.#manifest = manifest; this.#delegate = delegate; + this.#indexManifestPaths(); } exists(filepath: string): boolean { @@ -84,7 +87,7 @@ export class ManifestLoaderFS implements LoaderFS { readFile(filepath: string, encoding?: BufferEncoding): Buffer | string { const text = this.#bundleFileText(filepath); if (text !== undefined) { - return encoding ? text : Buffer.from(text); + return encoding ? Buffer.from(text).toString(encoding) : Buffer.from(text); } return encoding ? this.#delegate.readFile(filepath, encoding) : this.#delegate.readFile(filepath); } @@ -105,7 +108,9 @@ export class ManifestLoaderFS implements LoaderFS { if (bundleHit !== undefined) return this.#unwrapDefault(bundleHit); const text = this.#bundleFileText(filepath); - if (text !== undefined) return Buffer.from(text); + if (text !== undefined) { + return path.extname(filepath) === '.json' ? JSON.parse(text) : Buffer.from(text); + } return this.#delegate.loadFile(filepath); } @@ -113,7 +118,12 @@ export class ManifestLoaderFS implements LoaderFS { #bundleFileText(filepath: string): string | undefined { const loader = globalThis.__EGG_BUNDLE_FILE_LOADER__; if (!loader) return; - return loader(this.#normalize(filepath)); + const normalized = this.#normalize(filepath); + const direct = loader(normalized); + if (direct !== undefined) return direct; + + const rel = this.#toRelative(filepath); + return rel === normalized ? undefined : loader(rel); } #isManifestKnown(filepath: string): boolean { @@ -121,20 +131,35 @@ export class ManifestLoaderFS implements LoaderFS { } #manifestEntryType(filepath: string): 'file' | 'directory' | undefined { - const rel = this.#toRelative(filepath); - if (rel === '' || this.#manifest.data.fileDiscovery[rel]) return 'directory'; + if (this.#bundleFileText(filepath) !== undefined) return 'file'; - for (const [dir, files] of Object.entries(this.#manifest.data.fileDiscovery)) { - if (files.includes(path.posix.relative(dir, rel))) return 'file'; - } - - for (const value of Object.values(this.#manifest.data.resolveCache)) { - if (value && this.#normalize(value) === rel) return 'file'; - } + const rel = this.#toRelative(filepath); + if (this.#knownDirs.has(rel)) return 'directory'; + if (this.#knownFiles.has(rel)) return 'file'; } #syntheticStats(type: 'file' | 'directory'): Stats { + const mode = type === 'directory' ? 0o040755 : 0o100644; + const time = new Date(0); return { + dev: 0, + ino: 0, + mode, + nlink: 1, + uid: 0, + gid: 0, + rdev: 0, + size: 0, + blksize: 4096, + blocks: 0, + atimeMs: 0, + mtimeMs: 0, + ctimeMs: 0, + birthtimeMs: 0, + atime: time, + mtime: time, + ctime: time, + birthtime: time, isFile: () => type === 'file', isDirectory: () => type === 'directory', isBlockDevice: () => false, @@ -145,6 +170,42 @@ export class ManifestLoaderFS implements LoaderFS { } as Stats; } + #indexManifestPaths(): void { + this.#addKnownDir(''); + + for (const [dir, files] of Object.entries(this.#manifest.data.fileDiscovery)) { + const normalizedDir = this.#normalize(dir); + this.#addKnownDir(normalizedDir); + for (const file of files) { + this.#addKnownFile(path.posix.join(normalizedDir, this.#normalize(file))); + } + } + + for (const value of Object.values(this.#manifest.data.resolveCache)) { + if (value) this.#addKnownFile(this.#normalize(value)); + } + } + + #addKnownFile(rel: string): void { + this.#knownFiles.add(rel); + this.#addKnownDir(path.posix.dirname(rel)); + } + + #addKnownDir(rel: string): void { + if (rel === '.' || rel === '/') rel = ''; + + let current = rel; + while (true) { + this.#knownDirs.add(current); + const parent = path.posix.dirname(current); + if (parent === current || parent === '.') { + this.#knownDirs.add(''); + break; + } + current = parent; + } + } + #toRelative(filepath: string): string { const rel = path.isAbsolute(filepath) ? path.relative(this.#baseDir, filepath) : filepath; return this.#normalize(rel); diff --git a/packages/core/test/loader/egg_loader.test.ts b/packages/core/test/loader/egg_loader.test.ts index 2f841dfe6c..1102d21a8d 100644 --- a/packages/core/test/loader/egg_loader.test.ts +++ b/packages/core/test/loader/egg_loader.test.ts @@ -10,9 +10,11 @@ import { ContextLoader, EggLoader, FileLoader, + ManifestStore, RealLoaderFS, type EggLoaderOptions, type LoaderFS, + type StartupManifest, } from '../../src/index.js'; import { createApp, getFilepath, type Application } from '../helper.js'; @@ -109,6 +111,46 @@ describe('test/loader/egg_loader.test.ts', () => { assert.deepEqual(ret, [1, 2]); assert(calls.some((filepath) => filepath.endsWith(path.join('load_file', 'function.js')))); }); + + it('should initialize bundle manifest loader before package metadata reads', () => { + const baseDir = '/bundle/app'; + const originalStore = ManifestStore.getBundleStore(); + const originalFileLoader = globalThis.__EGG_BUNDLE_FILE_LOADER__; + const manifest: StartupManifest = { + version: 1, + generatedAt: '2026-05-10T00:00:00.000Z', + invalidation: { + lockfileFingerprint: '', + configFingerprint: '', + serverEnv: 'prod', + serverScope: '', + typescriptEnabled: false, + }, + extensions: {}, + resolveCache: {}, + fileDiscovery: {}, + }; + + ManifestStore.setBundleStore(ManifestStore.fromBundle(manifest, baseDir)); + globalThis.__EGG_BUNDLE_FILE_LOADER__ = (rel) => { + if (rel === 'package.json') return '{"name":"bundle-app"}'; + }; + + try { + const loader = new EggLoader({ + env: 'prod', + baseDir, + app: {}, + logger: console, + } as any); + + assert.equal(loader.pkg.name, 'bundle-app'); + assert.equal(loader.manifest.baseDir, baseDir); + } finally { + ManifestStore.setBundleStore(originalStore); + globalThis.__EGG_BUNDLE_FILE_LOADER__ = originalFileLoader; + } + }); }); it('should be loaded by loadToApp, support symbol property', async () => { diff --git a/packages/core/test/loader/loader_fs.test.ts b/packages/core/test/loader/loader_fs.test.ts index 25c2e7595f..b6a0751002 100644 --- a/packages/core/test/loader/loader_fs.test.ts +++ b/packages/core/test/loader/loader_fs.test.ts @@ -70,10 +70,54 @@ describe('test/loader/loader_fs.test.ts', () => { ); assert.equal(manifestFS.exists('/bundle/app/app/controller'), true); + assert.equal(manifestFS.exists('/bundle/app/app'), true); + assert.equal(manifestFS.stat('/bundle/app/app').isDirectory(), true); assert.equal(manifestFS.stat('/bundle/app/app/controller').isDirectory(), true); assert.equal(manifestFS.exists('/bundle/app/app/controller/home.js'), true); - assert.equal(manifestFS.stat('/bundle/app/app/controller/home.js').isFile(), true); + const fileStat = manifestFS.stat('/bundle/app/app/controller/home.js'); + assert.equal(fileStat.isFile(), true); + assert.equal(typeof fileStat.mode, 'number'); + assert(fileStat.mtime instanceof Date); assert.equal(manifestFS.exists('/bundle/app/config/plugin.js'), true); assert.equal(manifestFS.stat('/bundle/app/config/plugin.js').isFile(), true); }); + + it('should preserve bundled text file read/load behavior', async () => { + const originalFileLoader = globalThis.__EGG_BUNDLE_FILE_LOADER__; + globalThis.__EGG_BUNDLE_FILE_LOADER__ = (rel) => { + if (rel === 'config/app.json') return '{"name":"egg"}'; + }; + const manifestFS = new ManifestLoaderFS( + '/bundle/app', + ManifestStore.fromBundle( + { + version: 1, + generatedAt: '2026-05-10T00:00:00.000Z', + invalidation: { + lockfileFingerprint: '', + configFingerprint: '', + serverEnv: 'prod', + serverScope: '', + typescriptEnabled: false, + }, + extensions: {}, + resolveCache: {}, + fileDiscovery: {}, + }, + '/bundle/app', + ), + ); + + try { + assert.equal(manifestFS.exists('/bundle/app/config/app.json'), true); + assert.equal(manifestFS.stat('/bundle/app/config/app.json').isFile(), true); + assert.equal( + manifestFS.readFile('/bundle/app/config/app.json', 'base64'), + Buffer.from('{"name":"egg"}').toString('base64'), + ); + assert.deepEqual(await manifestFS.loadFile('/bundle/app/config/app.json'), { name: 'egg' }); + } finally { + globalThis.__EGG_BUNDLE_FILE_LOADER__ = originalFileLoader; + } + }); }); From 06f45651e8ab63d280d6752e372b62fc0c0bf822 Mon Sep 17 00:00:00 2001 From: killa Date: Sun, 10 May 2026 15:08:32 +0800 Subject: [PATCH 4/5] fix: preserve loader fs compatibility --- packages/core/src/loader/egg_loader.ts | 6 +++--- packages/core/src/loader/loader_fs.ts | 21 ++++++++++++++++--- .../test/__snapshots__/index.test.ts.snap | 1 + packages/core/test/loader/loader_fs.test.ts | 15 ++++++++++++- tools/egg-bundler/src/lib/EntryGenerator.ts | 19 +++++++++-------- 5 files changed, 46 insertions(+), 16 deletions(-) diff --git a/packages/core/src/loader/egg_loader.ts b/packages/core/src/loader/egg_loader.ts index 91bf6d9d19..63452dd2f7 100644 --- a/packages/core/src/loader/egg_loader.ts +++ b/packages/core/src/loader/egg_loader.ts @@ -22,7 +22,7 @@ import { sequencify } from '../utils/sequencify.ts'; import { Timing } from '../utils/timing.ts'; import { type ContextLoaderOptions, ContextLoader } from './context_loader.ts'; import { type FileLoaderOptions, CaseStyle, FULLPATH, FileLoader } from './file_loader.ts'; -import { ManifestLoaderFS, RealLoaderFS, type LoaderFS } from './loader_fs.ts'; +import { ManifestLoaderFS, RealLoaderFS, readFileWithLoaderFS, type LoaderFS } from './loader_fs.ts'; import { ManifestStore, type StartupManifest } from './manifest.ts'; const debug = debuglog('egg/core/loader/egg_loader'); @@ -209,7 +209,7 @@ export class EggLoader { const envPath = path.join(this.options.baseDir, 'config/env'); if (!serverEnv && this.loaderFS.exists(envPath)) { - serverEnv = this.loaderFS.readFile(envPath, 'utf8').trim(); + serverEnv = readFileWithLoaderFS(this.loaderFS, envPath, 'utf8').trim(); } if (!serverEnv && process.env.EGG_SERVER_ENV) { @@ -1758,7 +1758,7 @@ export class EggLoader { const tsConfigFile = path.join(this.options.baseDir, 'tsconfig.json'); if (this.loaderFS.exists(tsConfigFile)) { try { - const tsConfig = JSON.parse(this.loaderFS.readFile(tsConfigFile, 'utf-8')); + const tsConfig = JSON.parse(readFileWithLoaderFS(this.loaderFS, tsConfigFile, 'utf-8')); if (tsConfig.compilerOptions?.outDir) { debug('[resolveOutDir] use tsconfig.json compilerOptions.outDir: %o', tsConfig.compilerOptions.outDir); return tsConfig.compilerOptions.outDir; diff --git a/packages/core/src/loader/loader_fs.ts b/packages/core/src/loader/loader_fs.ts index 7dc73518f0..1a3283a164 100644 --- a/packages/core/src/loader/loader_fs.ts +++ b/packages/core/src/loader/loader_fs.ts @@ -13,13 +13,26 @@ export interface LoaderFS { exists(filepath: string): boolean; stat(filepath: string): Stats; realpath(filepath: string): string; - readFile(filepath: string): Buffer; - readFile(filepath: string, encoding: BufferEncoding): string; + readFile?: LoaderFSReadFile; readJSON(filepath: string): T; glob(patterns: string | string[], options?: LoaderFSGlobOptions): string[]; loadFile(filepath: string): Promise; } +export interface LoaderFSReadFile { + (filepath: string): Buffer; + (filepath: string, encoding: BufferEncoding): string; +} + +export function readFileWithLoaderFS(loaderFS: LoaderFS, filepath: string): Buffer; +export function readFileWithLoaderFS(loaderFS: LoaderFS, filepath: string, encoding: BufferEncoding): string; +export function readFileWithLoaderFS(loaderFS: LoaderFS, filepath: string, encoding?: BufferEncoding): Buffer | string { + if (loaderFS.readFile) { + return encoding ? loaderFS.readFile(filepath, encoding) : loaderFS.readFile(filepath); + } + return encoding ? fs.readFileSync(filepath, encoding) : fs.readFileSync(filepath); +} + export class RealLoaderFS implements LoaderFS { exists(filepath: string): boolean { return fs.existsSync(filepath); @@ -89,7 +102,9 @@ export class ManifestLoaderFS implements LoaderFS { if (text !== undefined) { return encoding ? Buffer.from(text).toString(encoding) : Buffer.from(text); } - return encoding ? this.#delegate.readFile(filepath, encoding) : this.#delegate.readFile(filepath); + return encoding + ? readFileWithLoaderFS(this.#delegate, filepath, encoding) + : readFileWithLoaderFS(this.#delegate, filepath); } readJSON(filepath: string): T { diff --git a/packages/core/test/__snapshots__/index.test.ts.snap b/packages/core/test/__snapshots__/index.test.ts.snap index 02ac30dd77..90e3d16ebb 100644 --- a/packages/core/test/__snapshots__/index.test.ts.snap +++ b/packages/core/test/__snapshots__/index.test.ts.snap @@ -26,6 +26,7 @@ exports[`should expose properties 1`] = ` "Router", "Singleton", "Timing", + "readFileWithLoaderFS", "sequencify", "utils", ] diff --git a/packages/core/test/loader/loader_fs.test.ts b/packages/core/test/loader/loader_fs.test.ts index b6a0751002..9f8fe556ac 100644 --- a/packages/core/test/loader/loader_fs.test.ts +++ b/packages/core/test/loader/loader_fs.test.ts @@ -5,7 +5,7 @@ import path from 'node:path'; import globby from 'globby'; import { describe, it } from 'vitest'; -import { ManifestLoaderFS, RealLoaderFS } from '../../src/loader/loader_fs.ts'; +import { ManifestLoaderFS, RealLoaderFS, readFileWithLoaderFS, type LoaderFS } from '../../src/loader/loader_fs.ts'; import { ManifestStore, type StartupManifest } from '../../src/loader/manifest.ts'; import utils from '../../src/utils/index.ts'; import { getFilepath } from '../helper.ts'; @@ -35,6 +35,19 @@ describe('test/loader/loader_fs.test.ts', () => { ); }); + it('should allow custom loaderFS implementations without readFile', () => { + const compatLoaderFS: LoaderFS = { + exists: loaderFS.exists.bind(loaderFS), + stat: loaderFS.stat.bind(loaderFS), + realpath: loaderFS.realpath.bind(loaderFS), + readJSON: loaderFS.readJSON.bind(loaderFS), + glob: loaderFS.glob.bind(loaderFS), + loadFile: loaderFS.loadFile.bind(loaderFS), + }; + + assert(readFileWithLoaderFS(compatLoaderFS, path.join(baseDir, 'package.json'), 'utf8').includes('"type"')); + }); + it('should answer manifest-backed file stats without touching the delegate filesystem', () => { const manifest: StartupManifest = { version: 1, diff --git a/tools/egg-bundler/src/lib/EntryGenerator.ts b/tools/egg-bundler/src/lib/EntryGenerator.ts index effe2ddec3..300d171348 100644 --- a/tools/egg-bundler/src/lib/EntryGenerator.ts +++ b/tools/egg-bundler/src/lib/EntryGenerator.ts @@ -1,4 +1,3 @@ -import { readFileSync } from 'node:fs'; import fs from 'node:fs/promises'; import { createRequire } from 'node:module'; import path from 'node:path'; @@ -75,7 +74,7 @@ export class EntryGenerator { const workerEntry = path.join(this.#outputDir, 'worker.entry.ts'); - await fs.writeFile(workerEntry, this.#renderWorkerEntry(entries, manifest)); + await fs.writeFile(workerEntry, await this.#renderWorkerEntry(entries, manifest)); return { workerEntry, @@ -116,16 +115,18 @@ export class EntryGenerator { }); } - #collectBundleTextFiles(manifest: StartupManifest): BundleTextFile[] { + async #collectBundleTextFiles(manifest: StartupManifest): Promise { const keys = new Set(); for (const value of Object.values(manifest.resolveCache)) { if (value && this.#isTextFileEntry(value)) keys.add(value.replaceAll(path.sep, '/')); } - return [...keys].sort().map((relKey) => ({ - relKey, - text: readFileSync(this.#absFromRelKey(relKey), 'utf8'), - })); + return Promise.all( + [...keys].sort().map(async (relKey) => ({ + relKey, + text: await fs.readFile(this.#absFromRelKey(relKey), 'utf8'), + })), + ); } #isTextFileEntry(relKey: string): boolean { @@ -236,11 +237,11 @@ export class EntryGenerator { return unique; } - #renderWorkerEntry(entries: BundleEntry[], manifest: StartupManifest): string { + async #renderWorkerEntry(entries: BundleEntry[], manifest: StartupManifest): Promise { const importLines: string[] = []; const mapLines: string[] = []; const externalSpecs: Array<[string, string]> = []; - const textFiles = this.#collectBundleTextFiles(manifest); + const textFiles = await this.#collectBundleTextFiles(manifest); let internalIdx = 0; for (const entry of entries) { From e9eff2bcb747c971408cceed2619f6d9f38f7fda Mon Sep 17 00:00:00 2001 From: killa Date: Sun, 10 May 2026 15:09:22 +0800 Subject: [PATCH 5/5] fix: declare bundle module loader global --- packages/core/src/global.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/core/src/global.ts b/packages/core/src/global.ts index 14ed6a6d46..193747b368 100644 --- a/packages/core/src/global.ts +++ b/packages/core/src/global.ts @@ -3,6 +3,7 @@ import type { ManifestStore } from './loader/manifest.ts'; declare global { var __EGG_BUNDLE_STORE__: ManifestStore | undefined; var __EGG_BUNDLE_FILE_LOADER__: ((filepath: string) => string | undefined) | undefined; + var __EGG_BUNDLE_MODULE_LOADER__: ((filepath: string) => unknown) | undefined; } export {};