Skip to content
Closed
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
3 changes: 2 additions & 1 deletion tegg/core/loader/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -42,10 +42,11 @@
},
"dependencies": {
"@eggjs/core-decorator": "workspace:*",
"@eggjs/loader-fs": "workspace:*",
"@eggjs/metadata": "workspace:*",
"@eggjs/tegg-types": "workspace:*",
"@eggjs/typings": "workspace:*",
"globby": "catalog:",
"@eggjs/utils": "workspace:*",
"is-type-of": "catalog:"
},
"devDependencies": {
Expand Down
16 changes: 11 additions & 5 deletions tegg/core/loader/src/LoaderFactory.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { PrototypeUtil } from '@eggjs/core-decorator';
import { type LoaderFS } from '@eggjs/loader-fs';
import type { ModuleDescriptor } from '@eggjs/metadata';
import {
EggLoadUnitType,
Expand All @@ -8,7 +9,11 @@ import {
type ModuleReference,
} from '@eggjs/tegg-types';

export type LoaderCreator = (unitPath: string) => Loader;
export interface LoaderOptions {
loaderFS?: LoaderFS;
}

export type LoaderCreator = (unitPath: string, options?: LoaderOptions) => Loader;

export interface ManifestModuleReference {
name: string;
Expand Down Expand Up @@ -40,12 +45,12 @@ export interface LoadAppManifest {
export class LoaderFactory {
private static loaderCreatorMap: Map<EggLoadUnitTypeLike, LoaderCreator> = new Map();

static createLoader(unitPath: string, type: EggLoadUnitTypeLike): Loader {
static createLoader(unitPath: string, type: EggLoadUnitTypeLike, options?: LoaderOptions): Loader {
const creator = this.loaderCreatorMap.get(type);
if (!creator) {
throw new Error(`not find creator for loader type ${type}`);
}
return creator(unitPath);
return creator(unitPath, options);
}

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

let loader: Loader;
if (manifestDesc && ModuleLoaderClass && loaderType === EggLoadUnitType.MODULE) {
loader = new ModuleLoaderClass(moduleReference.path, manifestDesc.decoratedFiles);
loader = new ModuleLoaderClass(moduleReference.path, manifestDesc.decoratedFiles, options?.loaderFS);
} else {
loader = LoaderFactory.createLoader(moduleReference.path, loaderType);
loader = LoaderFactory.createLoader(moduleReference.path, loaderType, options);
}

const res: ModuleDescriptor = {
Expand Down
35 changes: 27 additions & 8 deletions tegg/core/loader/src/LoaderUtil.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { pathToFileURL } from 'node:url';
import { PrototypeUtil } from '@eggjs/core-decorator';
import type { EggProtoImplClass } from '@eggjs/tegg-types';
import type {} from '@eggjs/typings/global';
import { importModule } from '@eggjs/utils';
import { isClass } from 'is-type-of';

// Guard against poorly mocked module constructors.
Expand Down Expand Up @@ -79,21 +80,20 @@ export class LoaderUtil {
throw createLoadError(originalFilePath, e);
}
if (exports == null) {
if (process.platform === 'win32') {
// convert to file:// url
// 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:'
filePath = pathToFileURL(filePath).toString();
}
try {
exports = await import(filePath);
exports =
globalThis.__EGG_BUNDLE_MODULE_LOADER__ === undefined
? await importModule(filePath, { importDefaultOnly: false })
: await importFallback(filePath);
Comment thread
coderabbitai[bot] marked this conversation as resolved.
} catch (e: unknown) {
throw createLoadError(filePath, e);
}
}
const normalizedExports = normalizeExports(exports);
const clazzList: EggProtoImplClass[] = [];
const exportNames = Object.keys(exports);
const exportNames = Object.keys(normalizedExports);
for (const exportName of exportNames) {
const clazz = exports[exportName];
const clazz = normalizedExports[exportName];
const isEggProto =
isClass(clazz) && (PrototypeUtil.isEggPrototype(clazz) || PrototypeUtil.isEggMultiInstancePrototype(clazz));
if (!isEggProto) {
Expand All @@ -108,3 +108,22 @@ export class LoaderUtil {
return clazzList;
}
}

async function importFallback(filePath: string): Promise<unknown> {
if (process.platform === 'win32') {
// convert to file:// url
// 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:'
filePath = pathToFileURL(filePath).toString();
}
return await import(filePath);
}

function normalizeExports(exports: unknown): Record<string, unknown> {
if (exports !== null && (typeof exports === 'object' || typeof exports === 'function')) {
if (isClass(exports)) {
return { default: exports };
}
return exports as Record<string, unknown>;
}
return {};
}
12 changes: 7 additions & 5 deletions tegg/core/loader/src/impl/ModuleLoader.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import path from 'node:path';
import { debuglog } from 'node:util';

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

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

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

constructor(moduleDir: string, precomputedFiles?: string[]) {
constructor(moduleDir: string, precomputedFiles?: string[], loaderFS: LoaderFS = new RealLoaderFS()) {
this.moduleDir = moduleDir;
this.precomputedFiles = precomputedFiles;
this.loaderFS = loaderFS;
}

async load(): Promise<EggProtoImplClass[]> {
Expand All @@ -33,7 +35,7 @@ export class ModuleLoader implements Loader {
debug('load from manifest, files: %o, moduleDir: %o', files, this.moduleDir);
} else {
const filePattern = LoaderUtil.filePattern();
files = await globby(filePattern, { cwd: this.moduleDir });
files = this.loaderFS.glob(filePattern, { cwd: this.moduleDir });
debug('load files: %o, filePattern: %o, moduleDir: %o', files, filePattern, this.moduleDir);
}
for (const file of files) {
Expand All @@ -47,8 +49,8 @@ export class ModuleLoader implements Loader {
return this.protoClazzList;
}

static createModuleLoader(path: string): ModuleLoader {
return new ModuleLoader(path);
static createModuleLoader(path: string, options?: { loaderFS?: LoaderFS }): ModuleLoader {
return new ModuleLoader(path, undefined, options?.loaderFS);
}
}

Expand Down
11 changes: 11 additions & 0 deletions tegg/core/loader/test/Loader.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,17 @@ describe('core/loader/test/Loader.test.ts', () => {
);
});

it('should load cjs default exported prototype class', async () => {
const cjsDefaultRepoFile = path.join(__dirname, './fixtures/modules/module-with-cjs-default/CjsDefaultRepo.cjs');

const prototypes = await LoaderUtil.loadFile(cjsDefaultRepoFile);

assert.deepEqual(
prototypes.map((proto) => proto.name),
['CjsDefaultRepo'],
);
});

it('should wrap bundle module loader errors', async () => {
const bundledFile = '/bundle/app/service.ts';
globalThis.__EGG_BUNDLE_MODULE_LOADER__ = () => {
Expand Down
20 changes: 20 additions & 0 deletions tegg/core/loader/test/LoaderFactoryManifest.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import assert from 'node:assert/strict';
import path from 'node:path';

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

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

it('should pass loaderFS to module loaders', async () => {
const loaderFS = new RecordingLoaderFS();

const descriptors = await LoaderFactory.loadApp([moduleRef], undefined, { loaderFS });

assert.equal(descriptors.length, 1);
assert(descriptors[0].clazzList.length > 0);
assert.deepEqual(loaderFS.globCalls, [{ cwd: repoModulePath }]);
});
});

class RecordingLoaderFS extends RealLoaderFS {
readonly globCalls: Array<{ cwd: string | undefined }> = [];

glob(patterns: string | string[], options?: LoaderFSGlobOptions): string[] {
this.globCalls.push({ cwd: options?.cwd ? String(options.cwd) : undefined });
return super.glob(patterns, options);
}
}
57 changes: 56 additions & 1 deletion tegg/core/loader/test/ModuleLoaderManifest.test.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,22 @@
import assert from 'node:assert/strict';
import path from 'node:path';

import { Prototype, PrototypeUtil, SingletonProto } from '@eggjs/core-decorator';
import { RealLoaderFS, type LoaderFSGlobOptions } from '@eggjs/loader-fs';
import { EggLoadUnitType } from '@eggjs/metadata';
import { describe, it } from 'vitest';
import type {} from '@eggjs/typings/global';
import { afterEach, describe, it } from 'vitest';

import { ModuleLoader } from '../src/impl/ModuleLoader.ts';
import { LoaderFactory } from '../src/index.ts';

describe('core/loader/test/ModuleLoaderManifest.test.ts', () => {
const repoModulePath = path.join(__dirname, './fixtures/modules/module-for-loader');
const toBundlePath = (file: string) => file.split('\\').join('/');

afterEach(() => {
globalThis.__EGG_BUNDLE_MODULE_LOADER__ = undefined;
});

it('should load only precomputed files when provided', async () => {
const loader = new ModuleLoader(repoModulePath, ['AppRepo.ts']);
Expand Down Expand Up @@ -49,4 +57,51 @@ describe('core/loader/test/ModuleLoaderManifest.test.ts', () => {
const second = await loader.load();
assert.strictEqual(first, second);
});

it('should use loaderFS for file discovery', async () => {
const loaderFS = new RecordingLoaderFS();
const loader = new ModuleLoader(repoModulePath, undefined, loaderFS);

const prototypes = await loader.load();

assert.equal(prototypes.length, 4);
assert.deepEqual(loaderFS.globCalls, [{ cwd: repoModulePath }]);
});

it('should load bundled service and repository files from manifest paths', async () => {
class BundledService {}
SingletonProto()(BundledService);
class BundledRepo {}
Prototype()(BundledRepo);

const bundledModuleDir = '/bundle/modules/order';
const serviceFile = path.join(bundledModuleDir, 'BundledService.ts');
const repoFile = path.join(bundledModuleDir, 'repository/BundledRepo.ts');
const normalizedServiceFile = toBundlePath(serviceFile);
const normalizedRepoFile = toBundlePath(repoFile);
const requestedFiles: string[] = [];
globalThis.__EGG_BUNDLE_MODULE_LOADER__ = (filepath: string) => {
requestedFiles.push(filepath);
if (filepath === normalizedServiceFile) return { BundledService };
if (filepath === normalizedRepoFile) return { BundledRepo };
return undefined;
};
const loader = new ModuleLoader(bundledModuleDir, ['BundledService.ts', 'repository/BundledRepo.ts']);

const prototypes = await loader.load();

assert.deepEqual(prototypes.map((proto) => proto.name).sort(), ['BundledRepo', 'BundledService']);
assert.deepEqual(requestedFiles, [normalizedServiceFile, normalizedRepoFile]);
assert.equal(PrototypeUtil.getFilePath(BundledService), serviceFile);
assert.equal(PrototypeUtil.getFilePath(BundledRepo), repoFile);
});
});

class RecordingLoaderFS extends RealLoaderFS {
readonly globCalls: Array<{ cwd: string | undefined }> = [];

glob(patterns: string | string[], options?: LoaderFSGlobOptions): string[] {
this.globCalls.push({ cwd: options?.cwd ? String(options.cwd) : undefined });
return super.glob(patterns, options);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
const { Prototype } = require('@eggjs/core-decorator');

class CjsDefaultRepo {}

Prototype()(CjsDefaultRepo);

module.exports = CjsDefaultRepo;
2 changes: 1 addition & 1 deletion tegg/plugin/controller/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,7 @@
"@eggjs/controller-decorator": "workspace:*",
"@eggjs/core-decorator": "workspace:*",
"@eggjs/lifecycle": "workspace:*",
"@eggjs/loader-fs": "workspace:*",
"@eggjs/metadata": "workspace:*",
"@eggjs/module-common": "workspace:*",
"@eggjs/router": "workspace:*",
Expand All @@ -107,7 +108,6 @@
"await-event": "catalog:",
"content-type": "catalog:",
"egg-errors": "catalog:",
"globby": "catalog:",
"koa-compose": "catalog:",
"path-to-regexp": "catalog:path-to-regexp1",
"raw-body": "^2.5.2",
Expand Down
5 changes: 4 additions & 1 deletion tegg/plugin/controller/src/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,10 @@ export default class ControllerAppBootHook implements ILifecycleBoot {
this.app.eggPrototypeLifecycleUtil.registerLifecycle(this.controllerPrototypeHook);
this.app.eggObjectFactory.registerEggObjectCreateMethod(AgentControllerProto, AgentControllerObject.createObject);
this.app.loaderFactory.registerLoader(CONTROLLER_LOAD_UNIT, (unitPath) => {
return new EggControllerLoader(unitPath);
return new EggControllerLoader(unitPath, {
loaderFS: this.app.loader.loaderFS,
manifest: this.app.loader.manifest,
});
});
this.controllerRegisterFactory.registerControllerRegister(ControllerType.HTTP, HTTPControllerRegister.create);
this.app.loadUnitFactory.registerLoadUnitCreator(
Expand Down
22 changes: 17 additions & 5 deletions tegg/plugin/controller/src/lib/EggControllerLoader.ts
Original file line number Diff line number Diff line change
@@ -1,24 +1,36 @@
import path from 'node:path';

import type { EggProtoImplClass } from '@eggjs/core-decorator';
import { RealLoaderFS, type LoaderFS } from '@eggjs/loader-fs';
import { LoaderUtil } from '@eggjs/tegg-loader';
import type { Loader } from '@eggjs/tegg-types';
import globby from 'globby';

interface FileDiscoveryManifest {
globFiles?: (directory: string, fallback: () => string[]) => string[];
}

export class EggControllerLoader implements Loader {
private readonly controllerDir: string;
private readonly loaderFS: LoaderFS;
private readonly manifest?: FileDiscoveryManifest;

constructor(controllerDir: string) {
constructor(controllerDir: string, options?: { loaderFS?: LoaderFS; manifest?: FileDiscoveryManifest }) {
this.controllerDir = controllerDir;
this.loaderFS = options?.loaderFS ?? new RealLoaderFS();
this.manifest = options?.manifest;
}

async load(): Promise<EggProtoImplClass[]> {
const filePattern = LoaderUtil.filePattern();
let files: string[];
try {
const httpControllers = (await globby(filePattern, { cwd: this.controllerDir })).map((file) =>
path.join(this.controllerDir, file),
);
const discovered =
typeof this.manifest?.globFiles === 'function'
? this.manifest.globFiles(this.controllerDir, () =>
this.loaderFS.glob(filePattern, { cwd: this.controllerDir }),
)
: this.loaderFS.glob(filePattern, { cwd: this.controllerDir });
const httpControllers = discovered.map((file) => path.join(this.controllerDir, file));
files = httpControllers;
} catch {
files = [];
Expand Down
Loading
Loading