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
11 changes: 3 additions & 8 deletions packages/utils/src/import.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { pathToFileURL, fileURLToPath } from 'node:url';
import { debuglog } from 'node:util';

import type { BundleModuleLoader } from '@eggjs/typings';
import type {} from '@eggjs/typings/global';

import { ImportResolveError } from './error/index.ts';

Expand Down Expand Up @@ -422,12 +423,6 @@ export function setSnapshotModuleLoader(loader: SnapshotModuleLoader): void {

export type { BundleModuleLoader } from '@eggjs/typings';

type BundleModuleGlobalThis = typeof globalThis & {
__EGG_BUNDLE_MODULE_LOADER__: BundleModuleLoader | undefined;
};

const bundleModuleGlobalThis = globalThis as BundleModuleGlobalThis;

function normalizeBundleModulePath(filepath: string): string {
return filepath.split(path.win32.sep).join(path.posix.sep);
}
Expand All @@ -443,11 +438,11 @@ function normalizeBundleModulePath(filepath: string): string {
* compatibility.
*/
export function setBundleModuleLoader(loader: BundleModuleLoader | undefined): void {
bundleModuleGlobalThis.__EGG_BUNDLE_MODULE_LOADER__ = loader;
globalThis.__EGG_BUNDLE_MODULE_LOADER__ = loader;
}

export async function importModule(filepath: string, options?: ImportModuleOptions): Promise<any> {
const _bundleModuleLoader = bundleModuleGlobalThis.__EGG_BUNDLE_MODULE_LOADER__;
const _bundleModuleLoader = globalThis.__EGG_BUNDLE_MODULE_LOADER__;
if (_bundleModuleLoader) {
const hit = _bundleModuleLoader(normalizeBundleModulePath(filepath));
if (hit !== undefined) {
Expand Down
2 changes: 1 addition & 1 deletion plugins/mock/test/mock_service_cluster.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ describe('test/mock_service_cluster.test.ts', () => {
baseDir: getFixtures('demo_mock_service_cluster'),
});
await app.ready();
});
}, 60000);
afterAll(() => app.close());

afterEach(mm.restore);
Expand Down
1 change: 1 addition & 0 deletions tegg/core/loader/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@
"@eggjs/core-decorator": "workspace:*",
"@eggjs/metadata": "workspace:*",
"@eggjs/tegg-types": "workspace:*",
"@eggjs/typings": "workspace:*",
"globby": "catalog:",
"is-type-of": "catalog:"
},
Expand Down
37 changes: 24 additions & 13 deletions tegg/core/loader/src/LoaderUtil.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,19 @@ 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 { isClass } from 'is-type-of';

// Guard against poorly mocked module constructors.
const Module = globalThis.module?.constructor?.length > 1 ? globalThis.module.constructor : BuiltinModule;

function createLoadError(filePath: string, e: unknown): Error {
const message = e instanceof Error ? e.message : String(e);
return new Error(`[tegg/loader] load ${filePath} failed: ${message}`, {
cause: e,
});
}

interface LoaderUtilConfig {
extraFilePattern?: string[];
}
Expand Down Expand Up @@ -64,20 +72,23 @@ export class LoaderUtil {

static async loadFile(filePath: string): Promise<EggProtoImplClass[]> {
const originalFilePath = filePath;
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();
}
let exports;
let exports: any;
try {
exports = await import(filePath);
} catch (e: any) {
console.trace('[tegg/loader] loadFile %s error:', filePath);
console.error(e);
throw new Error(`[tegg/loader] load ${filePath} failed: ${e.message}`, {
cause: e,
});
exports = globalThis.__EGG_BUNDLE_MODULE_LOADER__?.(originalFilePath.split('\\').join('/'));
} catch (e: unknown) {
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);
} catch (e: unknown) {
throw createLoadError(filePath, e);
Comment thread
coderabbitai[bot] marked this conversation as resolved.
}
}
const clazzList: EggProtoImplClass[] = [];
const exportNames = Object.keys(exports);
Expand Down
57 changes: 56 additions & 1 deletion tegg/core/loader/test/Loader.test.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,19 @@
import assert from 'node:assert/strict';
import path from 'node:path';

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

import { LoaderFactory, LoaderUtil } from '../src/index.ts';

describe('core/loader/test/Loader.test.ts', () => {
afterEach(() => {
Comment thread
killagu marked this conversation as resolved.
globalThis.__EGG_BUNDLE_MODULE_LOADER__ = undefined;
LoaderUtil.setConfig({});
});

describe('module loader', () => {
it('should load module', async () => {
const repoModulePath = path.join(__dirname, './fixtures/modules/module-for-loader');
Expand Down Expand Up @@ -37,6 +44,54 @@ describe('core/loader/test/Loader.test.ts', () => {
const prototypes = await loader.load();
assert.equal(prototypes.length, 1);
});

it('should load pre-bundled files through the bundle module loader', async () => {
class BundledService {}
SingletonProto()(BundledService);
const bundledFile = '/bundle/app/port/manager/UserRoleManager.ts';
globalThis.__EGG_BUNDLE_MODULE_LOADER__ = (filepath: string) => {
assert.equal(filepath, bundledFile);
return { BundledService };
};

const prototypes = await LoaderUtil.loadFile(bundledFile);

assert.deepEqual(
prototypes.map((proto) => proto.name),
['BundledService'],
);
assert.equal(PrototypeUtil.getFilePath(BundledService), bundledFile);
});

it('should fall back to dynamic import when the bundle module loader returns null', async () => {
const appRepoFile = path.join(__dirname, './fixtures/modules/module-for-loader/AppRepo.ts');
globalThis.__EGG_BUNDLE_MODULE_LOADER__ = () => null;

const prototypes = await LoaderUtil.loadFile(appRepoFile);

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

it('should wrap bundle module loader errors', async () => {
const bundledFile = '/bundle/app/service.ts';
globalThis.__EGG_BUNDLE_MODULE_LOADER__ = () => {
throw 'bundle loader failed';
};

await assert.rejects(
async () => {
await LoaderUtil.loadFile(bundledFile);
},
(err: Error & { cause?: unknown }) => {
assert.equal(err.message, '[tegg/loader] load /bundle/app/service.ts failed: bundle loader failed');
assert.equal(err.cause, 'bundle loader failed');
return true;
},
);
});
});

describe('file has tsc error', () => {
Expand Down
1 change: 1 addition & 0 deletions tools/egg-bundler/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,7 @@
},
"dependencies": {
"@eggjs/core": "workspace:*",
"@eggjs/typings": "workspace:*",
"@utoo/pack": "catalog:",
"execa": "catalog:",
"js-yaml": "catalog:",
Expand Down
6 changes: 2 additions & 4 deletions tools/egg-bundler/src/lib/EntryGenerator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -259,6 +259,7 @@ for (const [key, spec] of __EXTERNAL_SPECS) {
import path from 'node:path';

import { ManifestStore } from '@eggjs/core';
import type {} from '@eggjs/typings/global';
import { startEgg } from ${frameworkSpec};
import * as __frameworkModule from ${frameworkSpec};

Expand Down Expand Up @@ -314,11 +315,8 @@ for (const [appAbsRequest, targetRel] of __APP_RESOLVE_CACHE_ALIASES) {
}
}

const __bundleGlobalThis = globalThis as typeof globalThis & {
__EGG_BUNDLE_MODULE_LOADER__?: (filepath: string) => unknown;
};
ManifestStore.setBundleStore(ManifestStore.fromBundle(MANIFEST_DATA as any, __outputDir));
__bundleGlobalThis.__EGG_BUNDLE_MODULE_LOADER__ = (filepath) => {
globalThis.__EGG_BUNDLE_MODULE_LOADER__ = (filepath) => {
return __getBundleMap(filepath);
};

Expand Down
71 changes: 46 additions & 25 deletions tools/egg-bundler/src/lib/ManifestLoader.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,15 @@ interface TeggModuleDescriptor {
decoratedFiles?: string[];
}

interface TeggModuleReference {
name: string;
path: string;
optional?: boolean;
loaderType?: string;
}

interface TeggManifestExtension {
moduleReferences?: TeggModuleReference[];
moduleDescriptors?: TeggModuleDescriptor[];
}

Expand Down Expand Up @@ -401,35 +409,48 @@ export class ManifestLoader {
): Promise<Record<string, unknown>> {
const result: Record<string, unknown> = { ...extensions };
const tegg = extensions?.tegg as TeggManifestExtension | undefined;
if (tegg?.moduleDescriptors) {
result.tegg = {
...tegg,
moduleDescriptors: await Promise.all(
tegg.moduleDescriptors.map(async (desc) => {
if (!path.isAbsolute(desc.unitPath)) return desc;
const real = await this.#realpath(desc.unitPath);
let best: ModuleMapEntry | undefined;
for (const entry of moduleMap) {
if (real === entry.realDir || real.startsWith(entry.realDir + path.sep)) {
best = entry;
break;
}
}
if (!best) {
// keep as relative-to-baseDir form so runtime can resolve via #resolveFromBase
const rel = path.relative(this.#baseDir, real).replaceAll(path.sep, '/');
return { ...desc, unitPath: rel };
}
const rest = real === best.realDir ? '' : real.slice(best.realDir.length + 1);
const unitPath = [best.normalizedDir, rest].filter(Boolean).join('/').replaceAll(path.sep, '/');
return { ...desc, unitPath };
}),
),
};
if (tegg?.moduleReferences || tegg?.moduleDescriptors) {
const normalizedTegg: TeggManifestExtension = { ...tegg };
if (tegg.moduleReferences) {
normalizedTegg.moduleReferences = await Promise.all(
tegg.moduleReferences.map(async (ref) => ({
...ref,
path: await this.#normalizeTeggUnitPath(ref.path, moduleMap),
})),
);
}
if (tegg.moduleDescriptors) {
normalizedTegg.moduleDescriptors = await Promise.all(
tegg.moduleDescriptors.map(async (desc) => ({
...desc,
unitPath: await this.#normalizeTeggUnitPath(desc.unitPath, moduleMap),
})),
);
}
result.tegg = normalizedTegg;
}
return result;
}

async #normalizeTeggUnitPath(unitPath: string, moduleMap: ModuleMapEntry[]): Promise<string> {
if (!path.isAbsolute(unitPath)) return unitPath;
const real = await this.#realpath(unitPath);
let best: ModuleMapEntry | undefined;
for (const entry of moduleMap) {
if (real === entry.realDir || real.startsWith(entry.realDir + path.sep)) {
best = entry;
break;
}
}
if (!best) {
// Keep local app modules relative to baseDir so bundled runtime can
// resolve them under outputDir, and keep descriptor/reference keys equal.
return path.relative(this.#baseDir, real).replaceAll(path.sep, '/');
}
const rest = real === best.realDir ? '' : real.slice(best.realDir.length + 1);
return [best.normalizedDir, rest].filter(Boolean).join('/').replaceAll(path.sep, '/');
}

async #findPackageJsonFromNodeModules(name: string, startDir: string): Promise<string | undefined> {
let dir = startDir;
const nameSegments = name.split('/');
Expand Down
32 changes: 32 additions & 0 deletions tools/egg-bundler/test/EntryGenerator.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,38 @@ describe('EntryGenerator', () => {
]);
});

it('includes app/port controller decorated files from tegg manifest descriptors', async () => {
const manifest = makeManifest({
extensions: {
tegg: {
moduleReferences: [
{
name: 'appPort',
path: 'app/port',
},
],
moduleDescriptors: [
{
unitPath: 'app/port',
decoratedFiles: ['controller/HomeController.ts', 'manager/UserRoleManager.ts'],
},
],
},
},
});

const gen = new EntryGenerator({ baseDir: tmpDir, manifestLoader: createFakeLoader(manifest) });
const result = await gen.generate();
const worker = await fs.readFile(result.workerEntry, 'utf8');

expect(extractImports(worker).map((i) => i.specifier)).toEqual([
'../../app/port/controller/HomeController.ts',
'../../app/port/manager/UserRoleManager.ts',
]);
expect(worker).toContain('"moduleReferences"');
expect(worker).toContain('"path": "app/port"');
});

it('skips resolveCache entries whose value is null', async () => {
const manifest = makeManifest({
resolveCache: {
Expand Down
Loading
Loading