Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
70 changes: 69 additions & 1 deletion packages/core/src/loader/egg_loader.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}

Expand All @@ -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. `<pkg>/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/<name>` 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. `<pkg>/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 {
Expand Down

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 4 additions & 0 deletions packages/core/test/fixtures/bundle-plugin-paths/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"name": "bundle-plugin-paths",
"version": "1.0.0"
}
111 changes: 111 additions & 0 deletions packages/core/test/loader/bundle_plugin_path.test.ts
Original file line number Diff line number Diff line change
@@ -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/<name>` 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);
});
});
});
23 changes: 23 additions & 0 deletions packages/egg/src/lib/egg.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 (`<output>/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()];
}

Expand Down
Loading