Skip to content
Open
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
103 changes: 103 additions & 0 deletions packages/core/src/loader/egg_loader.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import { type FileLoaderOptions, CaseStyle, FULLPATH, FileLoader } from './file_
import { ManifestStore, type StartupManifest } from './manifest.ts';

const debug = debuglog('egg/core/loader/egg_loader');
const LOADER_MANIFEST_EXTENSION = 'eggLoader';
Comment thread
killagu marked this conversation as resolved.
const CONVENTIONAL_MANIFEST_LOADS = [
{ type: 'resolve', path: ['agent'] },
{ type: 'resolve', path: ['app'] },
Expand Down Expand Up @@ -66,6 +67,20 @@ export interface EggDirInfo {
type: EggDirInfoType;
}

interface LoaderManifestPluginInfo {
path?: string;
package?: string;
dependencies?: string[];
optionalDependencies?: string[];
env?: string[];
version?: string;
}

interface LoaderManifestExtension {
eggPaths?: string[];
plugins?: Record<string, LoaderManifestPluginInfo>;
}

export class EggLoader {
#requiredCount = 0;
readonly options: EggLoaderOptions;
Expand Down Expand Up @@ -379,6 +394,20 @@ export class EggLoader {
eggPaths.unshift(realpath);
}
}

const bundleStore = ManifestStore.getBundleStore();
const extension =
bundleStore?.baseDir === this.options.baseDir
? (bundleStore.getExtension(LOADER_MANIFEST_EXTENSION) as LoaderManifestExtension | undefined)
: undefined;
if (extension?.eggPaths?.length) {
return Array.from(
new Set([
...extension.eggPaths.map((eggPath) => this.#toManifestAbsolute(eggPath)),
...eggPaths.filter((eggPath) => !this.#isBundleOutputRootPath(eggPath)),
]),
);
}
return eggPaths;
}

Expand Down Expand Up @@ -450,6 +479,7 @@ export class EggLoader {
this.#extendPlugins(this.allPlugins, this.eggPlugins);
this.#extendPlugins(this.allPlugins, this.appPlugins);
this.#extendPlugins(this.allPlugins, this.customPlugins);
this.#applyManifestPluginInfo(this.allPlugins);

const enabledPluginNames: string[] = []; // enabled plugins that configured explicitly
const plugins: Record<string, EggPluginInfo> = {};
Expand Down Expand Up @@ -498,6 +528,7 @@ export class EggLoader {
* @since 1.0.0
*/
this.plugins = enablePlugins;
this.#collectLoaderManifestExtension();
this.timing.end('Load Plugin');
}

Expand Down Expand Up @@ -924,6 +955,78 @@ export class EggLoader {
}
}
}

#applyManifestPluginInfo(allPlugins: Record<string, EggPluginInfo>): void {
// getEggPaths reads ManifestStore.getBundleStore in the constructor before this.manifest is assigned.
const extension = this.manifest.getExtension(LOADER_MANIFEST_EXTENSION) as LoaderManifestExtension | undefined;
const plugins = extension?.plugins;
if (!plugins) return;

for (const [name, manifestPlugin] of Object.entries(plugins)) {
const plugin = allPlugins[name];
if (!plugin) continue;

if (manifestPlugin.path && (!plugin.path || this.#isBundleOutputRootPath(plugin.path))) {
plugin.path = this.#toManifestAbsolute(manifestPlugin.path);
}
if (manifestPlugin.package && !plugin.package) {
plugin.package = manifestPlugin.package;
}
for (const key of ['dependencies', 'optionalDependencies', 'env'] as const) {
const values = manifestPlugin[key];
if (Array.isArray(values) && !plugin[key]?.length) {
plugin[key] = [...values];
}
}
if (manifestPlugin.version && !plugin.version) {
plugin.version = manifestPlugin.version;
}
}
}

#collectLoaderManifestExtension(): void {
if (this.manifest === ManifestStore.getBundleStore()) return;

const plugins: Record<string, LoaderManifestPluginInfo> = {};
for (const [name, plugin] of Object.entries(this.allPlugins)) {
plugins[name] = {
path: plugin.path ? this.#toManifestRelative(plugin.path) : undefined,
package: plugin.package,
dependencies: plugin.dependencies ? [...plugin.dependencies] : undefined,
optionalDependencies: plugin.optionalDependencies ? [...plugin.optionalDependencies] : undefined,
env: plugin.env ? [...plugin.env] : undefined,
version: plugin.version,
};
}
this.manifest.setExtension(LOADER_MANIFEST_EXTENSION, {
eggPaths: this.eggPaths.map((eggPath) => this.#toManifestRelative(eggPath)),
plugins,
} satisfies LoaderManifestExtension);
}

#toManifestAbsolute(filepath: string): string {
return path.isAbsolute(filepath) ? filepath : path.join(this.options.baseDir, filepath);
}

#toManifestRelative(filepath: string): string {
return path.isAbsolute(filepath)
? path.relative(this.options.baseDir, filepath).replaceAll(path.sep, '/')
: filepath;
}

#toRealpath(filepath: string): string {
try {
return fs.realpathSync(filepath);
} catch {
return filepath;
}
}

#isBundleOutputRootPath(filepath: string): boolean {
const resolvedBaseDir = path.resolve(this.options.baseDir);
const resolvedFilepath = path.resolve(this.options.baseDir, filepath);
return this.#toRealpath(resolvedFilepath) === this.#toRealpath(resolvedBaseDir);
}
Comment thread
killagu marked this conversation as resolved.
/** end Plugin loader */

/** start Config loader */
Expand Down
54 changes: 53 additions & 1 deletion packages/core/test/loader/mixin/load_plugin.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import path from 'node:path';
import { mm } from 'mm';
import { describe, it, afterEach } from 'vitest';

import { EggCore, EggLoader } from '../../../src/index.js';
import { EggCore, EggLoader, ManifestStore, type StartupManifest } from '../../../src/index.js';
import { createApp, getFilepath, type Application } from '../../helper.js';

// windows path is case-insensitive, the equal assert will fail
Expand All @@ -14,6 +14,7 @@ describe.skipIf(process.platform === 'win32')('test/loader/mixin/load_plugin.tes

afterEach(async () => {
mm.restore();
ManifestStore.setBundleStore(undefined);
if (app) {
await app.close();
}
Expand Down Expand Up @@ -41,6 +42,57 @@ describe.skipIf(process.platform === 'win32')('test/loader/mixin/load_plugin.tes
assert(loader.plugins.a.enable);
});

it('should apply bundled loader manifest eggPaths and plugin paths', async () => {
const baseDir = getFilepath('plugin');
const manifest: StartupManifest = {
version: 1,
generatedAt: '2026-01-01T00:00:00.000Z',
invalidation: {
lockfileFingerprint: 'bundle-test',
configFingerprint: 'bundle-test',
serverEnv: 'unittest',
serverScope: '',
typescriptEnabled: true,
},
extensions: {
eggLoader: {
eggPaths: ['node_modules/egg'],
plugins: {
virtual: {
path: 'node_modules/virtual-plugin',
},
},
},
},
resolveCache: {},
fileDiscovery: {},
};
const bundleStore = ManifestStore.fromBundle(manifest, baseDir);
ManifestStore.setBundleStore(bundleStore);

app = createApp('plugin');
const loader = app.loader;
assert.deepEqual(loader.eggPaths, [path.join(baseDir, 'node_modules/egg'), getFilepath('egg-esm')]);
mm(loader, 'loadEggPlugins', async () => ({
virtual: {
enable: true,
name: 'virtual',
path: baseDir,
dependencies: [],
optionalDependencies: [],
env: [],
from: path.join(baseDir, 'config/plugin.js'),
},
}));
mm(loader, 'loadAppPlugins', async () => ({}));
mm(loader, 'loadCustomPlugins', () => ({}));

await loader.loadPlugin();

assert.equal(loader.allPlugins.virtual.path, path.join(baseDir, 'node_modules/virtual-plugin'));
assert.deepEqual(bundleStore.getExtension('eggLoader'), manifest.extensions.eggLoader);
});

it('should loadConfig all plugins', async () => {
const baseDir = getFilepath('plugin');
app = createApp('plugin');
Expand Down
31 changes: 31 additions & 0 deletions tegg/core/loader/test/LoaderFactoryManifest.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -87,4 +87,35 @@ describe('core/loader/test/LoaderFactoryManifest.test.ts', () => {
assert.deepStrictEqual(secondNames, firstNames);
}
});

it('should use restored bundled manifest paths before matching module descriptors', async () => {
const baseDir = path.dirname(repoModulePath);
const bundledModulePath = path.relative(baseDir, repoModulePath);
const manifestRef = { name: 'module-for-loader', path: bundledModulePath };
const manifest: LoadAppManifest = {
moduleDescriptors: [
{
name: 'module-for-loader',
unitPath: bundledModulePath,
decoratedFiles: [],
},
],
};
const restoredRef = { ...manifestRef, path: path.join(baseDir, manifestRef.path) };
const restoredManifest: LoadAppManifest = {
moduleDescriptors: manifest.moduleDescriptors.map((desc) => ({
...desc,
unitPath: path.join(baseDir, desc.unitPath),
})),
};

assert.notEqual(manifestRef.path, repoModulePath);
assert.notEqual(manifest.moduleDescriptors[0].unitPath, repoModulePath);
assert.equal(restoredRef.path, repoModulePath);
assert.equal(restoredManifest.moduleDescriptors[0].unitPath, repoModulePath);

const manifestDescs = await LoaderFactory.loadApp([restoredRef], restoredManifest);
assert.equal(manifestDescs[0].unitPath, repoModulePath);
assert.deepStrictEqual(manifestDescs[0].clazzList, []);
});
});
2 changes: 1 addition & 1 deletion tegg/plugin/aop/test/aop.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ describe('plugin/aop/test/aop.test.ts', () => {
baseDir: path.join(import.meta.dirname, 'fixtures/apps/aop-app'),
});
await app.ready();
});
}, 30000);

it('module aop should work', async () => {
app.mockCsrf();
Expand Down
24 changes: 22 additions & 2 deletions tegg/plugin/config/src/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,23 @@ import { ModuleScanner } from './lib/ModuleScanner.ts';

const debug = debuglog('egg/tegg/plugin/config/app');

function restoreManifestModulePath(modulePath: string, baseDir: string): string {
return path.isAbsolute(modulePath) ? modulePath : path.join(baseDir, modulePath);
}

function restoreTeggManifestExtension(manifest: TeggManifestExtension, baseDir: string): TeggManifestExtension {
return {
moduleReferences: (manifest.moduleReferences ?? []).map((ref) => ({
...ref,
path: restoreManifestModulePath(ref.path, baseDir),
})),
moduleDescriptors: (manifest.moduleDescriptors ?? []).map((desc) => ({
...desc,
unitPath: restoreManifestModulePath(desc.unitPath, baseDir),
})),
Comment thread
killagu marked this conversation as resolved.
};
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

export default class App implements ILifecycleBoot {
private readonly app: Application;

Expand Down Expand Up @@ -40,7 +57,7 @@ export default class App implements ILifecycleBoot {

let moduleReferences: readonly ModuleReference[];
if (manifestTegg?.moduleReferences?.length) {
moduleReferences = manifestTegg.moduleReferences;
moduleReferences = restoreTeggManifestExtension(manifestTegg, this.app.baseDir).moduleReferences;
debug('load moduleReferences from manifest: %o', moduleReferences);
} else {
// Auto-exclude outDir (e.g. dist/) from module scanning to avoid
Expand Down Expand Up @@ -68,8 +85,11 @@ export default class App implements ILifecycleBoot {
#loadModuleConfigs(): void {
this.app.moduleConfigs = {};
for (const reference of this.app.moduleReferences) {
const modulePath = path.isAbsolute(reference.path)
? reference.path
: ModuleConfigUtil.resolveModuleDir(reference.path, this.app.baseDir);
const absoluteRef: ModuleReference = {
path: ModuleConfigUtil.resolveModuleDir(reference.path, this.app.baseDir),
path: modulePath,
name: reference.name,
optional: reference.optional,
};
Expand Down
Loading
Loading