From 34c749355a28ea9466e3a811d800e6efa864ecad Mon Sep 17 00:00:00 2001 From: killagu Date: Sat, 20 Jun 2026 22:32:25 +0800 Subject: [PATCH 1/2] fix(egg): rebase framework eggPaths under bundle output MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit In bundle mode the bundler rewrites `import.meta.dirname` to the bundle output directory rather than the egg package directory, so the framework can no longer locate its `config/*` files. When a bundle store for this app is registered, rebase the framework dir to `/node_modules/egg/dist` so the manifest-backed loader fs resolves the bundled framework config. The rebase is gated twice so it never disturbs non-bundle (or unrelated) apps: - Store ownership: the active bundle store is shared via globalThis across @eggjs/core copies, so only rebase when `bundleStore.baseDir` resolves to this app's `baseDir` (mirroring `ManifestStore.load()`'s gate). A store registered for a different app must not redirect this app's framework path. - Existence: a real bundler output physically copies egg to `/node_modules/egg/dist`, but a bundle-mode app booted without the copied framework (e.g. integration tests that only inject a manifest-backed loaderFS) does not. Fall back to `import.meta.dirname` when the rebased dir is absent — which, when not actually rewritten by a bundler, still points at the real egg package dir. Without a matching bundle store the original `import.meta.dirname` logic applies unchanged. Co-Authored-By: Claude Opus 4.8 (1M context) --- packages/egg/src/lib/egg.ts | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/packages/egg/src/lib/egg.ts b/packages/egg/src/lib/egg.ts index 0bd69e8362..f5ad811fb2 100644 --- a/packages/egg/src/lib/egg.ts +++ b/packages/egg/src/lib/egg.ts @@ -657,6 +657,29 @@ export class EggApplicationCore extends EggCore { } protected override customEggPaths(): string[] { + const bundleStore = ManifestStore.getBundleStore(); + // Only rebase when the active bundle store belongs to *this* app. A global + // bundle store (shared via globalThis across @eggjs/core copies) may have + // been registered for a different app; mirror `ManifestStore.load()`'s + // `bundleStore.baseDir === baseDir` gate so an unrelated store never + // redirects this app's framework paths. + if (bundleStore && path.resolve(bundleStore.baseDir) === path.resolve(this.baseDir)) { + // In bundle mode `import.meta.dirname` is rewritten by the bundler to the + // bundle output directory, not the egg package directory, so it can no + // longer locate the framework `config/*` files. Rebase the framework dir + // under the output baseDir (`/node_modules/egg/dist`) so the + // 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)) { + return [bundledFrameworkDir, ...super.customEggPaths()]; + } + } return [path.dirname(import.meta.dirname), ...super.customEggPaths()]; } From b18d14a3e7ea38237a52ca677da17a214915e2f7 Mon Sep 17 00:00:00 2001 From: killagu Date: Sat, 20 Jun 2026 22:32:25 +0800 Subject: [PATCH 2/2] fix(core): relocate bundle-mode plugin paths to output node_modules MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A plugin declared via `definePluginFactory({ path: import.meta.dirname })` has its `path` rewritten by the bundler to the bundle output dir (= baseDir), which does not contain the plugin's own files. Detect that artifact and re-resolve the plugin to its package entry directory, rebased under the output `node_modules`, so the manifest-backed loader fs finds the bundled plugin config/extend/app files — mirroring how the framework eggPaths are rebased. Built-in framework plugins carry only a `name` (no `package`), so the re-resolution falls back to the conventional `@eggjs/` package name in addition to the bare name. The output `node_modules` segment is matched by exact path segment (not a substring) so directories like `my_node_modules` are not mistaken for the package-root marker. Artifact detection is gated on store ownership: the bundle store is shared via globalThis across @eggjs/core copies, so a path is only treated as a bundle artifact when the active store's `baseDir` resolves to this app's `baseDir`. Without a matching bundle store the original `plugin.path` short-circuit and `#resolvePluginPath` logic apply verbatim. Adds a fixture with a built-in (name-only) plugin and a package-declared plugin under a bundle-output `node_modules`, with regression coverage asserting both resolve to the correct rebased directory, that a store registered for a different app does not reinterpret this app's paths, and that non-bundle paths are returned verbatim. Co-Authored-By: Claude Opus 4.8 (1M context) --- packages/core/src/loader/egg_loader.ts | 70 ++++++++++- .../@eggjs/bundle-builtin/dist/index.js | 5 + .../@eggjs/bundle-builtin/package.json | 9 ++ .../bundle-plugin-pkg/dist/index.js | 3 + .../bundle-plugin-pkg/package.json | 9 ++ .../fixtures/bundle-plugin-paths/package.json | 4 + .../test/loader/bundle_plugin_path.test.ts | 111 ++++++++++++++++++ 7 files changed, 210 insertions(+), 1 deletion(-) create mode 100644 packages/core/test/fixtures/bundle-plugin-paths/node_modules/@eggjs/bundle-builtin/dist/index.js create mode 100644 packages/core/test/fixtures/bundle-plugin-paths/node_modules/@eggjs/bundle-builtin/package.json create mode 100644 packages/core/test/fixtures/bundle-plugin-paths/node_modules/bundle-plugin-pkg/dist/index.js create mode 100644 packages/core/test/fixtures/bundle-plugin-paths/node_modules/bundle-plugin-pkg/package.json create mode 100644 packages/core/test/fixtures/bundle-plugin-paths/package.json create mode 100644 packages/core/test/loader/bundle_plugin_path.test.ts diff --git a/packages/core/src/loader/egg_loader.ts b/packages/core/src/loader/egg_loader.ts index 8e6eb3cd71..36fcb1a77a 100644 --- a/packages/core/src/loader/egg_loader.ts +++ b/packages/core/src/loader/egg_loader.ts @@ -780,7 +780,7 @@ export class EggLoader { // Get the real plugin path protected getPluginPath(plugin: EggPluginInfo): string { - if (plugin.path) { + if (plugin.path && !this.#isBundlePluginPathArtifact(plugin.path)) { return plugin.path; } @@ -790,9 +790,77 @@ export class EggLoader { `plugin ${plugin.name} invalid, use 'path' instead of package: "${plugin.package}"`, ); } + + // 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)) { + return this.#resolveBundlePluginPath(plugin); + } + return this.#resolvePluginPath(plugin); } + /** + * In bundle mode a plugin declared via `definePluginFactory({ path: import.meta.dirname })` + * carries a `path` rewritten by the bundler to the bundle output directory (= baseDir), + * 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 { + 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 false; + } + return path.resolve(pluginPath) === path.resolve(this.options.baseDir); + } + + /** + * Re-resolve a bundle-artifact plugin path to the directory of the plugin package's entry + * module — the same directory `definePluginFactory` captured via `import.meta.dirname` at + * build time. Built-in framework plugins only carry a `name` (no `package`), so fall back to + * the conventional `@eggjs/` package name in addition to the bare name. + */ + #resolveBundlePluginPath(plugin: EggPluginInfo): string { + const candidates = plugin.package + ? [plugin.package] + : plugin.name.includes('/') + ? [plugin.name] + : [plugin.name, `@eggjs/${plugin.name}`]; + let lastErr: unknown; + 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); + // 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 + // `node_modules` by path segment (not a substring) so directories like + // `my_node_modules` are not mistaken for the package root marker. + const segments = realDir.split(/[/\\]/); + const nmIdx = segments.lastIndexOf('node_modules'); + if (nmIdx !== -1) { + return path.join(this.options.baseDir, ...segments.slice(nmIdx)); + } + return realDir; + } catch (err) { + lastErr = err; + } + } + const name = plugin.package || plugin.name; + debug('[resolveBundlePluginPath] error: %o, plugin info: %o', lastErr, plugin); + throw new Error(`Can not find plugin ${name} in "${[...this.lookupDirs].join(', ')}"`, { + cause: lastErr, + }); + } + #resolvePluginPath(plugin: EggPluginInfo): string { const name = plugin.package || plugin.name; try { diff --git a/packages/core/test/fixtures/bundle-plugin-paths/node_modules/@eggjs/bundle-builtin/dist/index.js b/packages/core/test/fixtures/bundle-plugin-paths/node_modules/@eggjs/bundle-builtin/dist/index.js new file mode 100644 index 0000000000..abde2a8802 --- /dev/null +++ b/packages/core/test/fixtures/bundle-plugin-paths/node_modules/@eggjs/bundle-builtin/dist/index.js @@ -0,0 +1,5 @@ +// Bundle output entry for the built-in plugin fixture. The plugin is declared +// with only a `name`, so the loader must fall back to the `@eggjs/` +// package when re-resolving the bundle-rewritten path. +'use strict'; +module.exports = {}; diff --git a/packages/core/test/fixtures/bundle-plugin-paths/node_modules/@eggjs/bundle-builtin/package.json b/packages/core/test/fixtures/bundle-plugin-paths/node_modules/@eggjs/bundle-builtin/package.json new file mode 100644 index 0000000000..d2cb7019e2 --- /dev/null +++ b/packages/core/test/fixtures/bundle-plugin-paths/node_modules/@eggjs/bundle-builtin/package.json @@ -0,0 +1,9 @@ +{ + "name": "@eggjs/bundle-builtin", + "version": "1.0.0", + "main": "./dist/index.js", + "module": "./dist/index.js", + "eggPlugin": { + "name": "bundle-builtin" + } +} diff --git a/packages/core/test/fixtures/bundle-plugin-paths/node_modules/bundle-plugin-pkg/dist/index.js b/packages/core/test/fixtures/bundle-plugin-paths/node_modules/bundle-plugin-pkg/dist/index.js new file mode 100644 index 0000000000..489f950565 --- /dev/null +++ b/packages/core/test/fixtures/bundle-plugin-paths/node_modules/bundle-plugin-pkg/dist/index.js @@ -0,0 +1,3 @@ +// Bundle output entry for the `package`-declared plugin fixture. +'use strict'; +module.exports = {}; diff --git a/packages/core/test/fixtures/bundle-plugin-paths/node_modules/bundle-plugin-pkg/package.json b/packages/core/test/fixtures/bundle-plugin-paths/node_modules/bundle-plugin-pkg/package.json new file mode 100644 index 0000000000..06d98f18b6 --- /dev/null +++ b/packages/core/test/fixtures/bundle-plugin-paths/node_modules/bundle-plugin-pkg/package.json @@ -0,0 +1,9 @@ +{ + "name": "bundle-plugin-pkg", + "version": "1.0.0", + "main": "./dist/index.js", + "module": "./dist/index.js", + "eggPlugin": { + "name": "bundlePluginPkg" + } +} diff --git a/packages/core/test/fixtures/bundle-plugin-paths/package.json b/packages/core/test/fixtures/bundle-plugin-paths/package.json new file mode 100644 index 0000000000..075e320347 --- /dev/null +++ b/packages/core/test/fixtures/bundle-plugin-paths/package.json @@ -0,0 +1,4 @@ +{ + "name": "bundle-plugin-paths", + "version": "1.0.0" +} diff --git a/packages/core/test/loader/bundle_plugin_path.test.ts b/packages/core/test/loader/bundle_plugin_path.test.ts new file mode 100644 index 0000000000..9c9fb28682 --- /dev/null +++ b/packages/core/test/loader/bundle_plugin_path.test.ts @@ -0,0 +1,111 @@ +import assert from 'node:assert/strict'; +import path from 'node:path'; + +import { afterEach, describe, it } from 'vitest'; + +import { EggLoader, ManifestStore } from '../../src/index.ts'; +import { getFilepath } from '../helper.ts'; + +// Regression coverage for bundle-mode plugin path relocation. +// +// In bundle mode the bundler rewrites `import.meta.dirname` (used by +// `definePluginFactory({ path: import.meta.dirname })`) to the bundle output +// directory (= baseDir). The loader must detect that rewritten artifact and +// re-resolve the plugin to its package entry directory, rebased under the +// output `node_modules`, so the manifest-backed loader fs can find the bundled +// plugin files. Non-bundle behavior must stay byte-for-byte identical. +describe('test/loader/bundle_plugin_path.test.ts', () => { + const baseDir = getFilepath('bundle-plugin-paths'); + + function createLoader() { + const loader = new EggLoader({ + baseDir, + app: {}, + logger: console, + } as any); + // `lookupDirs` is normally populated by `loadPlugin()`; set it directly so + // `getPluginPath` can be exercised in isolation without a full plugin load. + (loader as any).lookupDirs = (loader as any).getLookupDirs(); + return loader; + } + + function registerBundleStore() { + const data = ManifestStore.createCollector(baseDir).generateManifest({ + serverEnv: 'prod', + serverScope: '', + typescriptEnabled: false, + }); + ManifestStore.setBundleStore(ManifestStore.fromBundle(data, baseDir)); + } + + afterEach(() => { + ManifestStore.setBundleStore(undefined); + }); + + describe('with a registered bundle store', () => { + it('should re-resolve a built-in plugin declared with only a name', () => { + registerBundleStore(); + const loader = createLoader(); + // The bundler rewrote the plugin path to the output baseDir; the plugin + // only carries `name`, so it falls back to the `@eggjs/` package. + const resolved = (loader as any).getPluginPath({ + name: 'bundle-builtin', + path: baseDir, + }); + assert.equal(resolved, path.join(baseDir, 'node_modules', '@eggjs', 'bundle-builtin', 'dist')); + }); + + it('should re-resolve a plugin declared with a package name', () => { + registerBundleStore(); + const loader = createLoader(); + const resolved = (loader as any).getPluginPath({ + name: 'bundlePluginPkg', + package: 'bundle-plugin-pkg', + path: baseDir, + }); + assert.equal(resolved, path.join(baseDir, 'node_modules', 'bundle-plugin-pkg', 'dist')); + }); + + it('should keep an explicit path that is not the bundle artifact', () => { + registerBundleStore(); + const loader = createLoader(); + // path !== baseDir => not a rewritten artifact, return verbatim. + const explicit = path.join(baseDir, 'node_modules', 'bundle-plugin-pkg'); + const resolved = (loader as any).getPluginPath({ + name: 'bundlePluginPkg', + path: explicit, + }); + assert.equal(resolved, explicit); + }); + }); + + describe('with a bundle store registered for a *different* app', () => { + it('should not treat this app’s paths as bundle artifacts', () => { + // The bundle store is shared via globalThis across @eggjs/core copies, so a + // store registered for another app must not redirect this app's plugin + // resolution. Register a store whose baseDir differs from this loader's. + const otherBaseDir = path.join(baseDir, '..', 'some-other-app'); + const data = ManifestStore.createCollector(otherBaseDir).generateManifest({ + serverEnv: 'prod', + serverScope: '', + typescriptEnabled: false, + }); + ManifestStore.setBundleStore(ManifestStore.fromBundle(data, otherBaseDir)); + const loader = createLoader(); + // path === this app's baseDir, but the store is for another app, so the + // artifact gate stays off and the explicit path is returned verbatim. + assert.equal((loader as any).getPluginPath({ name: 'x', path: baseDir }), baseDir); + }); + }); + + describe('without a bundle store (non-bundle, zero behavior change)', () => { + it('should return an explicit path verbatim, even when it equals baseDir', () => { + const loader = createLoader(); + // Without a bundle store the artifact gate is off, so the original + // `plugin.path` short-circuit applies unchanged. + assert.equal((loader as any).getPluginPath({ name: 'x', path: baseDir }), baseDir); + const other = path.join(baseDir, 'node_modules', 'bundle-plugin-pkg'); + assert.equal((loader as any).getPluginPath({ name: 'x', path: other }), other); + }); + }); +});