Skip to content

Commit 13fa530

Browse files
committed
feat(tegg): load modules through loader fs
1 parent b015dce commit 13fa530

11 files changed

Lines changed: 150 additions & 26 deletions

File tree

tegg/core/loader/package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,10 +42,11 @@
4242
},
4343
"dependencies": {
4444
"@eggjs/core-decorator": "workspace:*",
45+
"@eggjs/loader-fs": "workspace:*",
4546
"@eggjs/metadata": "workspace:*",
4647
"@eggjs/tegg-types": "workspace:*",
4748
"@eggjs/typings": "workspace:*",
48-
"globby": "catalog:",
49+
"@eggjs/utils": "workspace:*",
4950
"is-type-of": "catalog:"
5051
},
5152
"devDependencies": {

tegg/core/loader/src/LoaderFactory.ts

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { PrototypeUtil } from '@eggjs/core-decorator';
2+
import { type LoaderFS } from '@eggjs/loader-fs';
23
import type { ModuleDescriptor } from '@eggjs/metadata';
34
import {
45
EggLoadUnitType,
@@ -8,7 +9,11 @@ import {
89
type ModuleReference,
910
} from '@eggjs/tegg-types';
1011

11-
export type LoaderCreator = (unitPath: string) => Loader;
12+
export interface LoaderOptions {
13+
loaderFS?: LoaderFS;
14+
}
15+
16+
export type LoaderCreator = (unitPath: string, options?: LoaderOptions) => Loader;
1217

1318
export interface ManifestModuleReference {
1419
name: string;
@@ -40,12 +45,12 @@ export interface LoadAppManifest {
4045
export class LoaderFactory {
4146
private static loaderCreatorMap: Map<EggLoadUnitTypeLike, LoaderCreator> = new Map();
4247

43-
static createLoader(unitPath: string, type: EggLoadUnitTypeLike): Loader {
48+
static createLoader(unitPath: string, type: EggLoadUnitTypeLike, options?: LoaderOptions): Loader {
4449
const creator = this.loaderCreatorMap.get(type);
4550
if (!creator) {
4651
throw new Error(`not find creator for loader type ${type}`);
4752
}
48-
return creator(unitPath);
53+
return creator(unitPath, options);
4954
}
5055

5156
static registerLoader(type: EggLoadUnitTypeLike, creator: LoaderCreator): void {
@@ -55,6 +60,7 @@ export class LoaderFactory {
5560
static async loadApp(
5661
moduleReferences: readonly ModuleReference[],
5762
manifest?: LoadAppManifest,
63+
options?: LoaderOptions,
5864
): Promise<ModuleDescriptor[]> {
5965
const result: ModuleDescriptor[] = [];
6066
const multiInstanceClazzList: EggProtoImplClass[] = [];
@@ -79,9 +85,9 @@ export class LoaderFactory {
7985

8086
let loader: Loader;
8187
if (manifestDesc && ModuleLoaderClass && loaderType === EggLoadUnitType.MODULE) {
82-
loader = new ModuleLoaderClass(moduleReference.path, manifestDesc.decoratedFiles);
88+
loader = new ModuleLoaderClass(moduleReference.path, manifestDesc.decoratedFiles, options?.loaderFS);
8389
} else {
84-
loader = LoaderFactory.createLoader(moduleReference.path, loaderType);
90+
loader = LoaderFactory.createLoader(moduleReference.path, loaderType, options);
8591
}
8692

8793
const res: ModuleDescriptor = {

tegg/core/loader/src/LoaderUtil.ts

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { pathToFileURL } from 'node:url';
44
import { PrototypeUtil } from '@eggjs/core-decorator';
55
import type { EggProtoImplClass } from '@eggjs/tegg-types';
66
import type {} from '@eggjs/typings/global';
7+
import { importModule } from '@eggjs/utils';
78
import { isClass } from 'is-type-of';
89

910
// Guard against poorly mocked module constructors.
@@ -79,13 +80,11 @@ export class LoaderUtil {
7980
throw createLoadError(originalFilePath, e);
8081
}
8182
if (exports == null) {
82-
if (process.platform === 'win32') {
83-
// convert to file:// url
84-
// avoid windows path issue: Only URLs with a scheme in: file, data, and node are supported by the default ESM loader. On Windows, absolute paths must be valid file:// URLs. Received protocol 'd:'
85-
filePath = pathToFileURL(filePath).toString();
86-
}
8783
try {
88-
exports = await import(filePath);
84+
exports =
85+
globalThis.__EGG_BUNDLE_MODULE_LOADER__ === undefined
86+
? await importModule(filePath, { importDefaultOnly: false })
87+
: await importFallback(filePath);
8988
} catch (e: unknown) {
9089
throw createLoadError(filePath, e);
9190
}
@@ -108,3 +107,12 @@ export class LoaderUtil {
108107
return clazzList;
109108
}
110109
}
110+
111+
async function importFallback(filePath: string): Promise<unknown> {
112+
if (process.platform === 'win32') {
113+
// convert to file:// url
114+
// avoid windows path issue: Only URLs with a scheme in: file, data, and node are supported by the default ESM loader. On Windows, absolute paths must be valid file:// URLs. Received protocol 'd:'
115+
filePath = pathToFileURL(filePath).toString();
116+
}
117+
return await import(filePath);
118+
}

tegg/core/loader/src/impl/ModuleLoader.ts

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
import path from 'node:path';
22
import { debuglog } from 'node:util';
33

4+
import { RealLoaderFS, type LoaderFS } from '@eggjs/loader-fs';
45
import type { EggProtoImplClass, Loader } from '@eggjs/tegg-types';
5-
import globby from 'globby';
66

77
import { LoaderFactory } from '../LoaderFactory.ts';
88
import { LoaderUtil } from '../LoaderUtil.ts';
@@ -11,13 +11,15 @@ const debug = debuglog('egg/tegg/loader/impl/ModuleLoader');
1111

1212
export class ModuleLoader implements Loader {
1313
private readonly moduleDir: string;
14+
private readonly loaderFS: LoaderFS;
1415
private protoClazzList: EggProtoImplClass[];
1516
/** Pre-computed file list from manifest (only decorated files) */
1617
private readonly precomputedFiles?: string[];
1718

18-
constructor(moduleDir: string, precomputedFiles?: string[]) {
19+
constructor(moduleDir: string, precomputedFiles?: string[], loaderFS: LoaderFS = new RealLoaderFS()) {
1920
this.moduleDir = moduleDir;
2021
this.precomputedFiles = precomputedFiles;
22+
this.loaderFS = loaderFS;
2123
}
2224

2325
async load(): Promise<EggProtoImplClass[]> {
@@ -33,7 +35,7 @@ export class ModuleLoader implements Loader {
3335
debug('load from manifest, files: %o, moduleDir: %o', files, this.moduleDir);
3436
} else {
3537
const filePattern = LoaderUtil.filePattern();
36-
files = await globby(filePattern, { cwd: this.moduleDir });
38+
files = this.loaderFS.glob(filePattern, { cwd: this.moduleDir });
3739
debug('load files: %o, filePattern: %o, moduleDir: %o', files, filePattern, this.moduleDir);
3840
}
3941
for (const file of files) {
@@ -47,8 +49,8 @@ export class ModuleLoader implements Loader {
4749
return this.protoClazzList;
4850
}
4951

50-
static createModuleLoader(path: string): ModuleLoader {
51-
return new ModuleLoader(path);
52+
static createModuleLoader(path: string, options?: { loaderFS?: LoaderFS }): ModuleLoader {
53+
return new ModuleLoader(path, undefined, options?.loaderFS);
5254
}
5355
}
5456

tegg/core/loader/test/LoaderFactoryManifest.test.ts

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import assert from 'node:assert/strict';
22
import path from 'node:path';
33

4+
import { RealLoaderFS, type LoaderFSGlobOptions } from '@eggjs/loader-fs';
45
import { ModuleDescriptorDumper } from '@eggjs/metadata';
56
import { describe, it } from 'vitest';
67

@@ -87,4 +88,23 @@ describe('core/loader/test/LoaderFactoryManifest.test.ts', () => {
8788
assert.deepStrictEqual(secondNames, firstNames);
8889
}
8990
});
91+
92+
it('should pass loaderFS to module loaders', async () => {
93+
const loaderFS = new RecordingLoaderFS();
94+
95+
const descriptors = await LoaderFactory.loadApp([moduleRef], undefined, { loaderFS });
96+
97+
assert.equal(descriptors.length, 1);
98+
assert(descriptors[0].clazzList.length > 0);
99+
assert.deepEqual(loaderFS.globCalls, [{ cwd: repoModulePath }]);
100+
});
90101
});
102+
103+
class RecordingLoaderFS extends RealLoaderFS {
104+
readonly globCalls: Array<{ cwd: string | undefined }> = [];
105+
106+
glob(patterns: string | string[], options?: LoaderFSGlobOptions): string[] {
107+
this.globCalls.push({ cwd: options?.cwd ? String(options.cwd) : undefined });
108+
return super.glob(patterns, options);
109+
}
110+
}

tegg/core/loader/test/ModuleLoaderManifest.test.ts

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import assert from 'node:assert/strict';
22
import path from 'node:path';
33

4+
import { RealLoaderFS, type LoaderFSGlobOptions } from '@eggjs/loader-fs';
45
import { EggLoadUnitType } from '@eggjs/metadata';
56
import { describe, it } from 'vitest';
67

@@ -49,4 +50,23 @@ describe('core/loader/test/ModuleLoaderManifest.test.ts', () => {
4950
const second = await loader.load();
5051
assert.strictEqual(first, second);
5152
});
53+
54+
it('should use loaderFS for file discovery', async () => {
55+
const loaderFS = new RecordingLoaderFS();
56+
const loader = new ModuleLoader(repoModulePath, undefined, loaderFS);
57+
58+
const prototypes = await loader.load();
59+
60+
assert.equal(prototypes.length, 4);
61+
assert.deepEqual(loaderFS.globCalls, [{ cwd: repoModulePath }]);
62+
});
5263
});
64+
65+
class RecordingLoaderFS extends RealLoaderFS {
66+
readonly globCalls: Array<{ cwd: string | undefined }> = [];
67+
68+
glob(patterns: string | string[], options?: LoaderFSGlobOptions): string[] {
69+
this.globCalls.push({ cwd: options?.cwd ? String(options.cwd) : undefined });
70+
return super.glob(patterns, options);
71+
}
72+
}

tegg/plugin/controller/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,7 @@
9696
"@eggjs/controller-decorator": "workspace:*",
9797
"@eggjs/core-decorator": "workspace:*",
9898
"@eggjs/lifecycle": "workspace:*",
99+
"@eggjs/loader-fs": "workspace:*",
99100
"@eggjs/metadata": "workspace:*",
100101
"@eggjs/module-common": "workspace:*",
101102
"@eggjs/router": "workspace:*",
@@ -107,7 +108,6 @@
107108
"await-event": "catalog:",
108109
"content-type": "catalog:",
109110
"egg-errors": "catalog:",
110-
"globby": "catalog:",
111111
"koa-compose": "catalog:",
112112
"path-to-regexp": "catalog:path-to-regexp1",
113113
"raw-body": "^2.5.2",

tegg/plugin/controller/src/app.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,10 @@ export default class ControllerAppBootHook implements ILifecycleBoot {
5252
this.app.eggPrototypeLifecycleUtil.registerLifecycle(this.controllerPrototypeHook);
5353
this.app.eggObjectFactory.registerEggObjectCreateMethod(AgentControllerProto, AgentControllerObject.createObject);
5454
this.app.loaderFactory.registerLoader(CONTROLLER_LOAD_UNIT, (unitPath) => {
55-
return new EggControllerLoader(unitPath);
55+
return new EggControllerLoader(unitPath, {
56+
loaderFS: this.app.loader.loaderFS,
57+
manifest: this.app.loader.manifest,
58+
});
5659
});
5760
this.controllerRegisterFactory.registerControllerRegister(ControllerType.HTTP, HTTPControllerRegister.create);
5861
this.app.loadUnitFactory.registerLoadUnitCreator(

tegg/plugin/controller/src/lib/EggControllerLoader.ts

Lines changed: 17 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,36 @@
11
import path from 'node:path';
22

33
import type { EggProtoImplClass } from '@eggjs/core-decorator';
4+
import { RealLoaderFS, type LoaderFS } from '@eggjs/loader-fs';
45
import { LoaderUtil } from '@eggjs/tegg-loader';
56
import type { Loader } from '@eggjs/tegg-types';
6-
import globby from 'globby';
7+
8+
interface FileDiscoveryManifest {
9+
globFiles?: (directory: string, fallback: () => string[]) => string[];
10+
}
711

812
export class EggControllerLoader implements Loader {
913
private readonly controllerDir: string;
14+
private readonly loaderFS: LoaderFS;
15+
private readonly manifest?: FileDiscoveryManifest;
1016

11-
constructor(controllerDir: string) {
17+
constructor(controllerDir: string, options?: { loaderFS?: LoaderFS; manifest?: FileDiscoveryManifest }) {
1218
this.controllerDir = controllerDir;
19+
this.loaderFS = options?.loaderFS ?? new RealLoaderFS();
20+
this.manifest = options?.manifest;
1321
}
1422

1523
async load(): Promise<EggProtoImplClass[]> {
1624
const filePattern = LoaderUtil.filePattern();
1725
let files: string[];
1826
try {
19-
const httpControllers = (await globby(filePattern, { cwd: this.controllerDir })).map((file) =>
20-
path.join(this.controllerDir, file),
21-
);
27+
const discovered =
28+
typeof this.manifest?.globFiles === 'function'
29+
? this.manifest.globFiles(this.controllerDir, () =>
30+
this.loaderFS.glob(filePattern, { cwd: this.controllerDir }),
31+
)
32+
: this.loaderFS.glob(filePattern, { cwd: this.controllerDir });
33+
const httpControllers = discovered.map((file) => path.join(this.controllerDir, file));
2234
files = httpControllers;
2335
} catch {
2436
files = [];

tegg/plugin/controller/test/lib/EggControllerLoader.test.ts

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { RealLoaderFS, type LoaderFSGlobOptions } from '@eggjs/loader-fs';
12
import { ControllerMetadataUtil } from '@eggjs/tegg';
23
import { describe, it, expect } from 'vitest';
34

@@ -15,4 +16,51 @@ describe('plugin/controller/test/lib/EggModuleLoader.test.ts', () => {
1516
const metadata = ControllerMetadataUtil.getControllerMetadata(AppController);
1617
expect(metadata).toBeDefined();
1718
});
19+
20+
it('should use loaderFS for controller discovery', async () => {
21+
const controllerDir = getFixtures('apps/controller-app/app/controller');
22+
const loaderFS = new RecordingLoaderFS();
23+
const loader = new EggControllerLoader(controllerDir, { loaderFS });
24+
25+
const classes = await loader.load();
26+
27+
expect(classes.length).toBe(7);
28+
expect(loaderFS.globCalls).toEqual([{ cwd: controllerDir }]);
29+
});
30+
31+
it('should use manifest-backed controller file discovery when available', async () => {
32+
const controllerDir = getFixtures('apps/controller-app/app/controller');
33+
const loaderFS = new RecordingLoaderFS();
34+
const manifest = {
35+
globFiles() {
36+
return ['AppController.ts'];
37+
},
38+
};
39+
const loader = new EggControllerLoader(controllerDir, { loaderFS, manifest });
40+
41+
const classes = await loader.load();
42+
43+
expect(classes.map((clazz) => clazz.name)).toEqual(['AppController']);
44+
expect(loaderFS.globCalls).toEqual([]);
45+
});
46+
47+
it('should fall back to loaderFS when manifest has no globFiles method', async () => {
48+
const controllerDir = getFixtures('apps/controller-app/app/controller');
49+
const loaderFS = new RecordingLoaderFS();
50+
const loader = new EggControllerLoader(controllerDir, { loaderFS, manifest: {} });
51+
52+
const classes = await loader.load();
53+
54+
expect(classes.length).toBe(7);
55+
expect(loaderFS.globCalls).toEqual([{ cwd: controllerDir }]);
56+
});
1857
});
58+
59+
class RecordingLoaderFS extends RealLoaderFS {
60+
readonly globCalls: Array<{ cwd: string | undefined }> = [];
61+
62+
glob(patterns: string | string[], options?: LoaderFSGlobOptions): string[] {
63+
this.globCalls.push({ cwd: options?.cwd ? String(options.cwd) : undefined });
64+
return super.glob(patterns, options);
65+
}
66+
}

0 commit comments

Comments
 (0)