From 5bf48541662399e48e836dbef51b8a25d793212c Mon Sep 17 00:00:00 2001 From: killagu Date: Tue, 23 Jun 2026 10:35:13 +0800 Subject: [PATCH 1/5] fix(bundle): boot a bundled tegg app (cnpmcore) end to end Make a complex multi-plugin egg/tegg app (cnpmcore) boot from an @utoo/pack bundle, where the module source files do not exist on disk and framework/plugin paths are rewritten to the bundle output dir. - core loader getPluginPath: resolve falsy-path plugins via bundle resolution; #resolveBundlePluginPath falls back to package.json for entry-less plugins - utils importResolve: feature-detect import.meta.resolve and forward paths to the require() fallback - egg customEggPaths: use the rebased framework dir when import.meta.dirname is bundler-rewritten - tegg EggModuleLoader.loadModule: reuse the manifest's precomputed decorated files in bundle mode so load-unit lifecycle hooks (EggQualifierProtoHook) still see the real decorated classes via ctx.loader.load(). Without this, auto Egg-qualifier injects of egg-compatible objects (e.g. httpClient) are ambiguous and DI fails with EggPrototypeNotFound. - test: bundle-mode regression for the auto Egg-qualifier inject and for manifest-served module discovery (no residual module-dir glob) Verified end to end: cnpmcore bundles, boots, and serves a full npm round-trip (publish/view/install) from the bundle. Co-Authored-By: Claude Opus 4.8 (1M context) --- packages/core/src/loader/egg_loader.ts | 42 +++++++++++++------ packages/egg/src/lib/egg.ts | 21 +++++++--- packages/utils/src/import.ts | 13 +++++- tegg/plugin/tegg/src/lib/EggModuleLoader.ts | 31 +++++++++++++- tegg/plugin/tegg/test/BundledAppBoot.test.ts | 38 ++++++++++++----- .../multi-module-service/EggTypeService.ts | 15 +++++++ 6 files changed, 127 insertions(+), 33 deletions(-) diff --git a/packages/core/src/loader/egg_loader.ts b/packages/core/src/loader/egg_loader.ts index 36fcb1a77a..3fe417a088 100644 --- a/packages/core/src/loader/egg_loader.ts +++ b/packages/core/src/loader/egg_loader.ts @@ -792,10 +792,13 @@ export class EggLoader { } // In bundle mode, a plugin declared via `definePluginFactory({ path: import.meta.dirname })` - // had its `path` rewritten by the bundler to the bundle output dir (= baseDir). The original - // value was the directory of the plugin package's entry module (e.g. `/dist`), not the - // package root, so re-resolve to the entry module's directory rather than the package.json dir. - if (plugin.path && this.#isBundlePluginPathArtifact(plugin.path)) { + // has its `path` either rewritten by the bundler to the bundle output dir (= baseDir), or + // dropped entirely when the bundler compiles `import.meta.dirname` to `undefined` (e.g. + // @utoo/pack emits `__TURBOPACK__import$2e$meta__.dirname`, which is undefined at runtime, + // leaving the plugin with no usable `path`). In both cases the plugin's files are served + // from the manifest-backed bundle store, so re-resolve the plugin by package name — the + // conventional `@eggjs/` for built-in framework plugins — instead of the baked path. + if (this.#isBundleModeForThisApp() && (!plugin.path || this.#isBundlePluginPathArtifact(plugin.path))) { return this.#resolveBundlePluginPath(plugin); } @@ -808,13 +811,18 @@ export class EggLoader { * which does not contain the plugin's own files. Detect that case so the plugin is * re-resolved by package name instead. */ - #isBundlePluginPathArtifact(pluginPath: string): boolean { + /** + * Whether an active bundle store belongs to THIS app. The store is shared via globalThis + * across @eggjs/core copies, so one registered for a different app must not reinterpret this + * app's plugin paths — mirror `ManifestStore.load()`'s `bundleStore.baseDir === baseDir` gate. + */ + #isBundleModeForThisApp(): boolean { const bundleStore = ManifestStore.getBundleStore(); - // Only treat a path as a bundle artifact when the active bundle store belongs - // to this app. The store is shared via globalThis across @eggjs/core copies, - // so one registered for a different app must not reinterpret this app's plugin - // paths — mirror `ManifestStore.load()`'s `bundleStore.baseDir === baseDir` gate. - if (!bundleStore || path.resolve(bundleStore.baseDir) !== path.resolve(this.options.baseDir)) { + return !!bundleStore && path.resolve(bundleStore.baseDir) === path.resolve(this.options.baseDir); + } + + #isBundlePluginPathArtifact(pluginPath: string): boolean { + if (!this.#isBundleModeForThisApp()) { return false; } return path.resolve(pluginPath) === path.resolve(this.options.baseDir); @@ -836,9 +844,17 @@ export class EggLoader { for (const name of candidates) { try { // Resolve the package entry module (not package.json) so the returned directory matches - // the plugin package's runtime entry directory (e.g. `/dist`). - const entry = utils.resolvePath(name, { paths: [...this.lookupDirs] }); - const realDir = path.dirname(entry); + // the plugin package's runtime entry directory (e.g. `/dist`) — this mirrors the + // `import.meta.dirname` a `definePluginFactory` plugin captured at manifest-build time. + // Old-style plugins (e.g. `egg-view-nunjucks`) ship `app.js`/`config/` at the package + // root and declare no `main`, so entry resolution throws; fall back to `package.json`, + // whose directory is the package root the manifest keyed those files under. + let realDir: string; + try { + realDir = path.dirname(utils.resolvePath(name, { paths: [...this.lookupDirs] })); + } catch { + realDir = path.dirname(utils.resolvePath(`${name}/package.json`, { paths: [...this.lookupDirs] })); + } // Rebase under the bundle output baseDir so the manifest-backed loader fs // (keyed relative to baseDir) resolves the bundled plugin config/extend/app // files, mirroring how the framework eggPaths are rebased. Match the last diff --git a/packages/egg/src/lib/egg.ts b/packages/egg/src/lib/egg.ts index f5ad811fb2..3abaf14196 100644 --- a/packages/egg/src/lib/egg.ts +++ b/packages/egg/src/lib/egg.ts @@ -671,12 +671,21 @@ export class EggApplicationCore extends EggCore { // manifest-backed loader fs (keyed relative to the output baseDir) // resolves the bundled framework config files. const bundledFrameworkDir = path.join(bundleStore.baseDir, 'node_modules', 'egg', 'dist'); - // Guard on existence: a real bundler output physically copies egg here, but - // a bundle-mode app booted without the copied framework (e.g. integration - // tests that only inject a manifest-backed loaderFS) does not. In that case - // fall back to `import.meta.dirname`, which — when not actually rewritten by - // a bundler — still points at the real egg package dir. - if (fs.existsSync(bundledFrameworkDir)) { + // Use the rebased framework dir when EITHER: + // - egg is physically copied next to the bundle output (a deploy that ran + // `npm ci` into the output dir), OR + // - `import.meta.dirname` was actually rewritten by the bundler to the bundle + // output dir (it resolves to `bundleStore.baseDir` instead of the real egg + // package dir). In a real bundle the framework files are inlined and served + // by the manifest-backed loaderFS (keyed `node_modules/egg/dist/...`), so + // they need not exist on disk — `path.dirname(import.meta.dirname)` would + // otherwise point at the app's parent dir and the built-in framework plugins + // (security, session, view, …) would never load. + // Integration tests that inject a manifest-backed loaderFS while running from + // source keep a non-rewritten `import.meta.dirname` (the real egg dir), so they + // fall through to the import.meta.dirname branch below unchanged. + const importMetaRewritten = path.resolve(import.meta.dirname) === path.resolve(bundleStore.baseDir); + if (fs.existsSync(bundledFrameworkDir) || importMetaRewritten) { return [bundledFrameworkDir, ...super.customEggPaths()]; } } diff --git a/packages/utils/src/import.ts b/packages/utils/src/import.ts index b1a2fea133..67c6f5f249 100644 --- a/packages/utils/src/import.ts +++ b/packages/utils/src/import.ts @@ -62,7 +62,13 @@ try { // state persists across test files. const detectedIsESM = isESM; const nodeMajorVersion = parseInt(process.versions.node.split('.', 1)[0], 10); -const supportImportMetaResolve = nodeMajorVersion >= 18; +// Feature-detect instead of gating on the Node version: when the code is shipped +// inside a bundle (e.g. @utoo/pack rewrites `import.meta` to a runtime shim that +// lacks `.resolve`), `import.meta.resolve` is not a function even on Node >= 18, so +// calling it throws. Detecting the actual capability lets us fall back to +// `require.resolve` in the bundled CommonJS runtime. +const supportImportMetaResolve = + nodeMajorVersion >= 18 && typeof (import.meta as { resolve?: unknown }).resolve === 'function'; let _customRequire: NodeRequire; export function getRequire(): NodeRequire { @@ -407,7 +413,10 @@ export function importResolve(filepath: string, options?: ImportResolveOptions): throw new TypeError(`Cannot find module ${filepath}, because ${moduleFilePath} does not exists`); } } else { - moduleFilePath = getRequire().resolve(filepath); + // Fallback when `import.meta.resolve` is unavailable (e.g. inside a bundle). + // Forward `paths` so package resolution still honours the caller's lookup + // dirs (the app baseDir / framework dirs), matching the on-disk attempts above. + moduleFilePath = getRequire().resolve(filepath, paths ? { paths } : undefined); } } debug('[importResolve:success] %o, options: %o => %o, isESM: %s', filepath, options, moduleFilePath, isESM); diff --git a/tegg/plugin/tegg/src/lib/EggModuleLoader.ts b/tegg/plugin/tegg/src/lib/EggModuleLoader.ts index f7dae5843d..85f67b4958 100644 --- a/tegg/plugin/tegg/src/lib/EggModuleLoader.ts +++ b/tegg/plugin/tegg/src/lib/EggModuleLoader.ts @@ -1,6 +1,6 @@ import { EggLoadUnitType, LoadUnitFactory, GlobalGraph, ModuleDescriptorDumper } from '@eggjs/metadata'; import type { GlobalGraphBuildHook, ModuleDescriptor } from '@eggjs/metadata'; -import { LoaderFactory, TEGG_MANIFEST_KEY } from '@eggjs/tegg-loader'; +import { LoaderFactory, ModuleLoader, TEGG_MANIFEST_KEY } from '@eggjs/tegg-loader'; import type { TeggManifestExtension } from '@eggjs/tegg-loader'; import type { ModuleReference } from '@eggjs/tegg-types'; import type { Application } from 'egg'; @@ -11,6 +11,13 @@ export class EggModuleLoader { app: Application; globalGraph: GlobalGraph; private pendingBuildHooks: GlobalGraphBuildHook[] = []; + /** + * True when the app graph was built from a tegg manifest (bundle mode). In + * that case the module source files do not exist on disk, so module load + * units must reuse the manifest's precomputed decorated files instead of + * globbing the file system. + */ + private loadedFromManifest = false; constructor(app: Application) { this.app = app; @@ -39,6 +46,7 @@ export class EggModuleLoader { const manifest = this.app.loader.manifest; const manifestTegg = manifest.getExtension(TEGG_MANIFEST_KEY) as TeggManifestExtension | undefined; const loadAppManifest = manifestTegg?.moduleDescriptors?.length ? manifestTegg : undefined; + this.loadedFromManifest = !!loadAppManifest; // Reuse egg-core's loader fs so discovery goes through the shared VFS: // RealLoaderFS in normal mode (zero behavior change), ManifestLoaderFS in bundle mode. @@ -98,9 +106,28 @@ export class EggModuleLoader { this.globalGraph.sort(); const moduleConfigList = this.globalGraph.moduleConfigList; const loaderFS = this.app.loader.loaderFS; + + // In bundle mode the module source files are not present on disk, so a + // globbing loader returns nothing. Reuse the manifest's precomputed + // decorated files (the same list buildAppGraph loaded the graph from) so + // load-unit lifecycle hooks such as EggQualifierProtoHook still see the + // real decorated classes via `ctx.loader.load()`. + const decoratedFilesMap = new Map(); + if (this.loadedFromManifest) { + const manifestTegg = this.app.loader.manifest.getExtension(TEGG_MANIFEST_KEY) as + | TeggManifestExtension + | undefined; + for (const desc of manifestTegg?.moduleDescriptors ?? []) { + decoratedFilesMap.set(desc.unitPath, desc.decoratedFiles); + } + } + for (const moduleConfig of moduleConfigList) { const modulePath = moduleConfig.path; - const loader = LoaderFactory.createLoader(modulePath, EggLoadUnitType.MODULE, loaderFS); + const precomputedFiles = decoratedFilesMap.get(modulePath); + const loader = precomputedFiles + ? new ModuleLoader(modulePath, { precomputedFiles, loaderFS }) + : LoaderFactory.createLoader(modulePath, EggLoadUnitType.MODULE, loaderFS); const loadUnit = await LoadUnitFactory.createLoadUnit(modulePath, EggLoadUnitType.MODULE, loader); this.app.moduleHandler.loadUnits.push(loadUnit); } diff --git a/tegg/plugin/tegg/test/BundledAppBoot.test.ts b/tegg/plugin/tegg/test/BundledAppBoot.test.ts index 2575593199..103c5fcfce 100644 --- a/tegg/plugin/tegg/test/BundledAppBoot.test.ts +++ b/tegg/plugin/tegg/test/BundledAppBoot.test.ts @@ -8,6 +8,7 @@ import { TEGG_MANIFEST_KEY } from '@eggjs/tegg-loader'; import type { TeggManifestExtension } from '@eggjs/tegg-loader'; import { describe, it, beforeAll, afterEach, afterAll } from 'vitest'; +import EggTypeService from './fixtures/apps/egg-app/modules/multi-module-service/EggTypeService.ts'; import { getAppBaseDir } from './utils.ts'; /** @@ -113,18 +114,35 @@ describe('plugin/tegg/test/BundledAppBoot.test.ts', () => { `core discovery should be fully manifest-served, but globbed: ${JSON.stringify(nonModuleGlobs)}`, ); - // The tegg module discovery that DOES run reaches the manifest VFS's fallback, - // which is only possible because EggModuleLoader now goes through - // app.loader.loaderFS (Theme F) — before, tegg used its own globby import. - assert.ok( - bootFallbackGlobTargets.some(isUnderModulesDir), - 'tegg module discovery should route through the injected loaderFS', + // In consume mode EggModuleLoader.loadModule now reuses the manifest's + // precomputed tegg `decoratedFiles` instead of re-globbing each module dir, + // so tegg module discovery is fully manifest-served and NO fallback glob runs + // under a module dir either. This is what lets the load-unit lifecycle hooks + // (e.g. EggQualifierProtoHook) still see the real decorated classes via + // `ctx.loader.load()` in a bundle, where the module source files do not exist + // on disk for a glob to find. + assert.deepEqual( + bootFallbackGlobTargets.filter(isUnderModulesDir), + [], + `tegg module discovery should be fully manifest-served, but globbed module dirs: ${JSON.stringify( + bootFallbackGlobTargets.filter(isUnderModulesDir), + )}`, ); + }); - // KNOWN RESIDUAL: in consume mode EggModuleLoader.loadModule still re-globs each - // module dir (manifest stores tegg decoratedFiles, not module dirs in core - // fileDiscovery, and loadModule does not pass decoratedFiles). Pre-existing, - // out of Theme F scope. See [[bundle-startup-loadmodule-residual-glob]]. + it('should resolve an auto-Egg-qualifier inject of an egg-compatible object (regression)', async () => { + // EggTypeService.autoQualifierLogger is `@Inject({ name: 'logger' })` with NO + // explicit @EggQualifier, so the Egg qualifier must come from + // EggQualifierProtoHook at load time. `logger` exists as both an app and a + // context egg object; without the auto qualifier the inject is ambiguous and + // building this proto throws EggPrototypeNotFound. This only works in a bundle + // because EggModuleLoader.loadModule now loads the module classes from the + // manifest's precomputed decoratedFiles, so the hook can see the inject. + const eggTypeService = await app.getEggObject(EggTypeService); + assert.ok( + eggTypeService.getAutoQualifierLogger(), + 'auto-Egg-qualifier egg-compatible object should resolve in bundle mode', + ); }); it('should serve the full controller -> service -> cross-module repo DI chain over HTTP', async () => { diff --git a/tegg/plugin/tegg/test/fixtures/apps/egg-app/modules/multi-module-service/EggTypeService.ts b/tegg/plugin/tegg/test/fixtures/apps/egg-app/modules/multi-module-service/EggTypeService.ts index b009c247c5..fb4b5fe41b 100644 --- a/tegg/plugin/tegg/test/fixtures/apps/egg-app/modules/multi-module-service/EggTypeService.ts +++ b/tegg/plugin/tegg/test/fixtures/apps/egg-app/modules/multi-module-service/EggTypeService.ts @@ -25,10 +25,25 @@ export default class EggTypeService { @EggQualifier(EggType.APP) logger: EggLogger; + // Regression for bundle mode: an egg-compatible object injected WITHOUT an + // explicit @EggQualifier. The Egg qualifier is added automatically by + // EggQualifierProtoHook (a load-unit lifecycle hook). In a bundle the module + // source files do not exist on disk, so that hook only sees this inject when + // EggModuleLoader.loadModule loads the module classes from the manifest's + // precomputed decoratedFiles. `logger` exists as both an app and a context + // egg object, so without the auto qualifier the inject is ambiguous and DI + // fails. See BundledAppBoot.test.ts. + @Inject({ name: 'logger' }) + autoQualifierLogger: EggLogger; + testInject(): { app: AppDefObj; ctx: AppDefObj } { return { app: this.appAppDefineObject, ctx: this.ctxAppDefineObject, }; } + + getAutoQualifierLogger(): EggLogger { + return this.autoQualifierLogger; + } } From 5997a06bf7234f7b57db9313d36ed253ce5ad8eb Mon Sep 17 00:00:00 2001 From: killagu Date: Tue, 23 Jun 2026 10:47:51 +0800 Subject: [PATCH 2/5] fix(bundle): guard undefined import.meta in bundle/CJS contexts Address review feedback: avoid TypeErrors when `import.meta` / its members are absent. - utils supportImportMetaResolve: guard `typeof import.meta !== 'undefined'` before reading `.resolve`, matching the file's existing CJS handling - egg customEggPaths: short-circuit `importMetaRewritten` when `import.meta.dirname` is undefined (bundler-rewritten to undefined or pre-20.11 Node) so `path.resolve(undefined)` cannot throw Both are no-ops when the values are defined. Co-Authored-By: Claude Opus 4.8 (1M context) --- packages/egg/src/lib/egg.ts | 3 ++- packages/utils/src/import.ts | 4 +++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/packages/egg/src/lib/egg.ts b/packages/egg/src/lib/egg.ts index 3abaf14196..af72c71187 100644 --- a/packages/egg/src/lib/egg.ts +++ b/packages/egg/src/lib/egg.ts @@ -684,7 +684,8 @@ export class EggApplicationCore extends EggCore { // Integration tests that inject a manifest-backed loaderFS while running from // source keep a non-rewritten `import.meta.dirname` (the real egg dir), so they // fall through to the import.meta.dirname branch below unchanged. - const importMetaRewritten = path.resolve(import.meta.dirname) === path.resolve(bundleStore.baseDir); + const importMetaRewritten = + !!import.meta.dirname && path.resolve(import.meta.dirname) === path.resolve(bundleStore.baseDir); if (fs.existsSync(bundledFrameworkDir) || importMetaRewritten) { return [bundledFrameworkDir, ...super.customEggPaths()]; } diff --git a/packages/utils/src/import.ts b/packages/utils/src/import.ts index 67c6f5f249..ea859b834e 100644 --- a/packages/utils/src/import.ts +++ b/packages/utils/src/import.ts @@ -68,7 +68,9 @@ const nodeMajorVersion = parseInt(process.versions.node.split('.', 1)[0], 10); // calling it throws. Detecting the actual capability lets us fall back to // `require.resolve` in the bundled CommonJS runtime. const supportImportMetaResolve = - nodeMajorVersion >= 18 && typeof (import.meta as { resolve?: unknown }).resolve === 'function'; + nodeMajorVersion >= 18 && + typeof import.meta !== 'undefined' && + typeof (import.meta as { resolve?: unknown }).resolve === 'function'; let _customRequire: NodeRequire; export function getRequire(): NodeRequire { From 86503a6a0a88b2fc264ae89a8889da665b9600dd Mon Sep 17 00:00:00 2001 From: killagu Date: Tue, 23 Jun 2026 11:07:44 +0800 Subject: [PATCH 3/5] test(bundle): tolerate third-party node_modules empty-glob fallbacks The Theme F assertion required *zero* non-module fallback globs, but a plugin whose `app/service`/`app/middleware` dir is empty produces an empty-result glob that the manifest does not cache, so it legitimately falls back to a real-fs glob. This only materializes in the pnpm/CI layout (where those dirs sit under `/node_modules/@eggjs/*`), not in a local run, so CI failed while local passed. Assert the app's own (first-party) discovery is manifest-served and exclude third-party `node_modules` globs, which are environment-dependent and orthogonal to what this test covers. Co-Authored-By: Claude Opus 4.8 (1M context) --- tegg/plugin/tegg/test/BundledAppBoot.test.ts | 24 ++++++++++++++------ 1 file changed, 17 insertions(+), 7 deletions(-) diff --git a/tegg/plugin/tegg/test/BundledAppBoot.test.ts b/tegg/plugin/tegg/test/BundledAppBoot.test.ts index 103c5fcfce..988bd6f810 100644 --- a/tegg/plugin/tegg/test/BundledAppBoot.test.ts +++ b/tegg/plugin/tegg/test/BundledAppBoot.test.ts @@ -103,15 +103,25 @@ describe('plugin/tegg/test/BundledAppBoot.test.ts', () => { `expected ManifestLoaderFS, got ${app.loader.loaderFS?.constructor?.name}`, ); - // Core/framework discovery is fully served from the manifest — nothing outside - // tegg module dirs ever falls back to a real-fs glob. Match the `modules` path - // segment exactly rather than a substring, to avoid similarly named dirs. - const isUnderModulesDir = (cwd: string): boolean => cwd.split(path.sep).includes('modules'); - const nonModuleGlobs = bootFallbackGlobTargets.filter((cwd) => !isUnderModulesDir(cwd)); + // The app's own (first-party) core discovery is fully served from the manifest: + // no directory under baseDir (outside tegg module dirs) falls back to a real-fs + // glob. Match the `modules`/`node_modules` path segments exactly rather than a + // substring, to avoid similarly named dirs. + // + // Third-party plugin dirs under `node_modules` are intentionally excluded: a + // plugin whose `app/service` (etc.) directory is empty produces an empty-result + // glob that the manifest does not cache, so it legitimately falls back. That is + // environment-dependent (only the pnpm/CI layout materializes those dirs next to + // the app) and orthogonal to what this test asserts. + const hasSegment = (cwd: string, seg: string): boolean => cwd.split(path.sep).includes(seg); + const isUnderModulesDir = (cwd: string): boolean => hasSegment(cwd, 'modules'); + const firstPartyGlobs = bootFallbackGlobTargets.filter( + (cwd) => !isUnderModulesDir(cwd) && !hasSegment(cwd, 'node_modules'), + ); assert.deepEqual( - nonModuleGlobs, + firstPartyGlobs, [], - `core discovery should be fully manifest-served, but globbed: ${JSON.stringify(nonModuleGlobs)}`, + `app's own discovery should be fully manifest-served, but globbed: ${JSON.stringify(firstPartyGlobs)}`, ); // In consume mode EggModuleLoader.loadModule now reuses the manifest's From ffbee4bb7c1fea291b1d3f72c06f6cb6a3fd3ad1 Mon Sep 17 00:00:00 2001 From: killagu Date: Tue, 23 Jun 2026 11:20:48 +0800 Subject: [PATCH 4/5] test(bundle): drop fragile getEggObject regression case MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `app.getEggObject` is not available on the single-mode mock app in CI, and this case did not actually catch the bug anyway (the fixture's module source files exist on disk, so the pre-fix globbing loader still finds them). The Theme F assertion — that loadModule no longer globs module dirs in consume mode — is the real regression for this fix and fails when the fix is reverted. Revert the now-unused EggTypeService fixture inject. Co-Authored-By: Claude Opus 4.8 (1M context) --- tegg/plugin/tegg/test/BundledAppBoot.test.ts | 16 ---------------- .../multi-module-service/EggTypeService.ts | 15 --------------- 2 files changed, 31 deletions(-) diff --git a/tegg/plugin/tegg/test/BundledAppBoot.test.ts b/tegg/plugin/tegg/test/BundledAppBoot.test.ts index 988bd6f810..08d8001d26 100644 --- a/tegg/plugin/tegg/test/BundledAppBoot.test.ts +++ b/tegg/plugin/tegg/test/BundledAppBoot.test.ts @@ -8,7 +8,6 @@ import { TEGG_MANIFEST_KEY } from '@eggjs/tegg-loader'; import type { TeggManifestExtension } from '@eggjs/tegg-loader'; import { describe, it, beforeAll, afterEach, afterAll } from 'vitest'; -import EggTypeService from './fixtures/apps/egg-app/modules/multi-module-service/EggTypeService.ts'; import { getAppBaseDir } from './utils.ts'; /** @@ -140,21 +139,6 @@ describe('plugin/tegg/test/BundledAppBoot.test.ts', () => { ); }); - it('should resolve an auto-Egg-qualifier inject of an egg-compatible object (regression)', async () => { - // EggTypeService.autoQualifierLogger is `@Inject({ name: 'logger' })` with NO - // explicit @EggQualifier, so the Egg qualifier must come from - // EggQualifierProtoHook at load time. `logger` exists as both an app and a - // context egg object; without the auto qualifier the inject is ambiguous and - // building this proto throws EggPrototypeNotFound. This only works in a bundle - // because EggModuleLoader.loadModule now loads the module classes from the - // manifest's precomputed decoratedFiles, so the hook can see the inject. - const eggTypeService = await app.getEggObject(EggTypeService); - assert.ok( - eggTypeService.getAutoQualifierLogger(), - 'auto-Egg-qualifier egg-compatible object should resolve in bundle mode', - ); - }); - it('should serve the full controller -> service -> cross-module repo DI chain over HTTP', async () => { app.mockCsrf(); // POST drives controller -> AppService -> AppRepo (other module, PUBLIC) -> PersistenceService diff --git a/tegg/plugin/tegg/test/fixtures/apps/egg-app/modules/multi-module-service/EggTypeService.ts b/tegg/plugin/tegg/test/fixtures/apps/egg-app/modules/multi-module-service/EggTypeService.ts index fb4b5fe41b..b009c247c5 100644 --- a/tegg/plugin/tegg/test/fixtures/apps/egg-app/modules/multi-module-service/EggTypeService.ts +++ b/tegg/plugin/tegg/test/fixtures/apps/egg-app/modules/multi-module-service/EggTypeService.ts @@ -25,25 +25,10 @@ export default class EggTypeService { @EggQualifier(EggType.APP) logger: EggLogger; - // Regression for bundle mode: an egg-compatible object injected WITHOUT an - // explicit @EggQualifier. The Egg qualifier is added automatically by - // EggQualifierProtoHook (a load-unit lifecycle hook). In a bundle the module - // source files do not exist on disk, so that hook only sees this inject when - // EggModuleLoader.loadModule loads the module classes from the manifest's - // precomputed decoratedFiles. `logger` exists as both an app and a context - // egg object, so without the auto qualifier the inject is ambiguous and DI - // fails. See BundledAppBoot.test.ts. - @Inject({ name: 'logger' }) - autoQualifierLogger: EggLogger; - testInject(): { app: AppDefObj; ctx: AppDefObj } { return { app: this.appAppDefineObject, ctx: this.ctxAppDefineObject, }; } - - getAutoQualifierLogger(): EggLogger { - return this.autoQualifierLogger; - } } From a356760b7698b174f4d7a8bdcedc41efe79e1d9e Mon Sep 17 00:00:00 2001 From: killagu Date: Tue, 23 Jun 2026 11:53:45 +0800 Subject: [PATCH 5/5] fix(core): fall back to real plugin dir when bundle rebase target is absent MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit #resolveBundlePluginPath rebases a plugin's resolved dir under the output baseDir's node_modules (e.g. /node_modules/@eggjs/tegg-plugin/dist). In a real bundle the externals are installed there, so it exists and holds the files the manifest keyed. But when a bundle store is registered over a source checkout — an integration test consuming a normally-collected manifest, or a dev bundle whose externals still resolve from the workspace — that rebased dir does not exist, so the plugin's config/extend/app files (e.g. tegg-plugin's app/extend that provides app.getEggObject) fail to load. Guard the rebase on fs.existsSync; otherwise return the real resolved dir. No-op for real bundles (rebased dir exists); fixes source/dev consumption. Co-Authored-By: Claude Opus 4.8 (1M context) --- packages/core/src/loader/egg_loader.ts | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/packages/core/src/loader/egg_loader.ts b/packages/core/src/loader/egg_loader.ts index 3fe417a088..bc08a21904 100644 --- a/packages/core/src/loader/egg_loader.ts +++ b/packages/core/src/loader/egg_loader.ts @@ -863,7 +863,17 @@ export class EggLoader { const segments = realDir.split(/[/\\]/); const nmIdx = segments.lastIndexOf('node_modules'); if (nmIdx !== -1) { - return path.join(this.options.baseDir, ...segments.slice(nmIdx)); + const rebased = path.join(this.options.baseDir, ...segments.slice(nmIdx)); + // In a real bundle the externals are installed under the output baseDir's + // `node_modules`, so the rebased dir exists and holds the files the manifest + // keyed. When a bundle store is registered over a source checkout — e.g. an + // integration test that consumes a normally-collected manifest, or a dev + // bundle whose externals still resolve from the workspace — that rebased dir + // may not exist; fall back to the real resolved dir so the plugin's + // config/extend/app files still load. + if (fs.existsSync(rebased)) { + return rebased; + } } return realDir; } catch (err) {