Skip to content
Open
Show file tree
Hide file tree
Changes from 3 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
93 changes: 93 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,68 @@ 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;
}

#isBundleOutputRootPath(filepath: string): boolean {
return path.resolve(this.options.baseDir, filepath) === path.resolve(this.options.baseDir);
}
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
19 changes: 19 additions & 0 deletions tegg/core/loader/src/LoaderFactory.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import path from 'node:path';

import { PrototypeUtil } from '@eggjs/core-decorator';
import type { ModuleDescriptor } from '@eggjs/metadata';
import {
Expand Down Expand Up @@ -37,6 +39,23 @@ export interface LoadAppManifest {
moduleDescriptors: ManifestModuleDescriptor[];
}

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

export 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),
})),
};
}

export class LoaderFactory {
private static loaderCreatorMap: Map<EggLoadUnitTypeLike, LoaderCreator> = new Map();

Expand Down
31 changes: 30 additions & 1 deletion tegg/core/loader/test/LoaderFactoryManifest.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import path from 'node:path';
import { ModuleDescriptorDumper } from '@eggjs/metadata';
import { describe, it } from 'vitest';

import { LoaderFactory } from '../src/index.ts';
import { LoaderFactory, restoreTeggManifestExtension } from '../src/index.ts';
import type { LoadAppManifest, ManifestModuleDescriptor } from '../src/index.ts';

describe('core/loader/test/LoaderFactoryManifest.test.ts', () => {
Expand Down Expand Up @@ -87,4 +87,33 @@ describe('core/loader/test/LoaderFactoryManifest.test.ts', () => {
assert.deepStrictEqual(secondNames, firstNames);
}
});

it('should restore bundled manifest paths before matching module descriptors', async () => {
const baseDir = path.dirname(repoModulePath);
const normalDescs = await LoaderFactory.loadApp([moduleRef]);
const decoratedFiles = ModuleDescriptorDumper.getDecoratedFiles(normalDescs[0]);
const manifest = restoreTeggManifestExtension(
{
moduleReferences: [{ name: 'module-for-loader', path: path.basename(repoModulePath) }],
moduleDescriptors: [
{
name: 'module-for-loader',
unitPath: path.basename(repoModulePath),
decoratedFiles,
},
],
},
baseDir,
);

assert.equal(manifest.moduleReferences[0].path, repoModulePath);
assert.equal(manifest.moduleDescriptors[0].unitPath, repoModulePath);

const manifestDescs = await LoaderFactory.loadApp([moduleRef], manifest);
assert.equal(manifestDescs[0].unitPath, repoModulePath);
assert.deepStrictEqual(
manifestDescs[0].clazzList.map((c) => c.name).sort(),
normalDescs[0].clazzList.map((c) => c.name).sort(),
);
});
});
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
6 changes: 3 additions & 3 deletions tegg/plugin/config/src/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { debuglog } from 'node:util';

import { ModuleConfigUtil } from '@eggjs/tegg-common-util';
import type { ModuleReference } from '@eggjs/tegg-common-util';
import { TEGG_MANIFEST_KEY } from '@eggjs/tegg-loader';
import { restoreManifestModulePath, restoreTeggManifestExtension, TEGG_MANIFEST_KEY } from '@eggjs/tegg-loader';
import type { TeggManifestExtension } from '@eggjs/tegg-loader';
import type { Application, ILifecycleBoot } from 'egg';

Expand Down Expand Up @@ -40,7 +40,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 @@ -69,7 +69,7 @@ export default class App implements ILifecycleBoot {
this.app.moduleConfigs = {};
for (const reference of this.app.moduleReferences) {
const absoluteRef: ModuleReference = {
path: ModuleConfigUtil.resolveModuleDir(reference.path, this.app.baseDir),
path: restoreManifestModulePath(reference.path, this.app.baseDir),
name: reference.name,
optional: reference.optional,
};
Expand Down
52 changes: 52 additions & 0 deletions tegg/plugin/config/test/ReadModule.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
import path from 'node:path';

import { mm, type MockApplication } from '@eggjs/mock';
import { describe, it, afterAll, beforeAll, expect } from 'vitest';

import AppBootHook from '../src/app.ts';
import { getFixtures } from './utils.ts';

describe('plugin/config/test/ReadModule.test.ts', () => {
Expand Down Expand Up @@ -42,4 +45,53 @@ describe('plugin/config/test/ReadModule.test.ts', () => {
expect(app.moduleConfigs).toBeDefined();
expect(app.moduleReferences).toBeDefined();
});

it('should restore manifest module references against runtime baseDir', async () => {
const baseDir = getFixtures('apps/app-with-modules');
const fakeApp = {
baseDir,
config: {
tegg: {
readModuleOptions: {},
},
},
loader: {
getTypeFiles() {
return ['module'];
},
manifest: {
getExtension() {
return {
moduleReferences: [
{
optional: undefined,
name: 'moduleA',
path: 'app/module-a',
},
],
moduleDescriptors: [
{
name: 'moduleA',
unitPath: 'app/module-a',
decoratedFiles: [],
},
],
};
},
},
},
};

await new AppBootHook(fakeApp as any).loadMetadata();

const modulePath = path.join(baseDir, 'app/module-a');
expect((fakeApp as any).moduleReferences).toEqual([
{
optional: undefined,
name: 'moduleA',
path: modulePath,
},
]);
expect((fakeApp as any).moduleConfigs.moduleA.reference.path).toBe(modulePath);
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ export class ControllerLoadUnitHandler extends Base {
}

async _init(): Promise<void> {
const controllerDir = path.join(this.app.config.baseDir, 'app/controller');
const controllerDir = path.join(this.app.config.baseDir ?? this.app.baseDir, 'app/controller');
const loader = this.app.loaderFactory.createLoader(controllerDir, CONTROLLER_LOAD_UNIT as EggLoadUnitType);
this.controllerLoadUnit = await this.app.loadUnitFactory.createLoadUnit(
controllerDir,
Expand Down
Loading
Loading