diff --git a/tools/egg-bundler/src/lib/EntryGenerator.ts b/tools/egg-bundler/src/lib/EntryGenerator.ts index b533f100ef..d0bec3dd33 100644 --- a/tools/egg-bundler/src/lib/EntryGenerator.ts +++ b/tools/egg-bundler/src/lib/EntryGenerator.ts @@ -274,6 +274,27 @@ const __framework = ${frameworkSpec}; const MANIFEST_DATA = ${manifestJson} as const; const __APP_ABSOLUTE_ALIASES: Array<[string, string]> = ${appAbsoluteAliases}; const __APP_RESOLVE_CACHE_ALIASES: Array<[string, string]> = ${appResolveCacheAliases}; +const __restoreBundleRuntimePath = (filepath: string) => { + if (!filepath || path.isAbsolute(filepath)) return filepath; + return path.resolve(__outputDir, filepath); +}; +const __restoreBundleManifest = (manifest: typeof MANIFEST_DATA) => { + const restored = JSON.parse(JSON.stringify(manifest)); + const tegg = restored.extensions?.tegg; + if (tegg?.moduleReferences) { + tegg.moduleReferences = tegg.moduleReferences.map((ref) => ({ + ...ref, + path: __restoreBundleRuntimePath(ref.path), + })); + } + if (tegg?.moduleDescriptors) { + tegg.moduleDescriptors = tegg.moduleDescriptors.map((desc) => ({ + ...desc, + unitPath: __restoreBundleRuntimePath(desc.unitPath), + })); + } + return restored; +}; const __BUNDLE_MAP_REL: Record = { ${mapLines.join('\n')} @@ -315,7 +336,8 @@ for (const [appAbsRequest, targetRel] of __APP_RESOLVE_CACHE_ALIASES) { } } -ManifestStore.setBundleStore(ManifestStore.fromBundle(MANIFEST_DATA as any, __outputDir)); +const __RUNTIME_MANIFEST_DATA = __restoreBundleManifest(MANIFEST_DATA); +ManifestStore.setBundleStore(ManifestStore.fromBundle(__RUNTIME_MANIFEST_DATA as any, __outputDir)); globalThis.__EGG_BUNDLE_MODULE_LOADER__ = (filepath) => { return __getBundleMap(filepath); }; diff --git a/tools/egg-bundler/test/EntryGenerator.test.ts b/tools/egg-bundler/test/EntryGenerator.test.ts index 6d3560c1d2..2ae74e507d 100644 --- a/tools/egg-bundler/test/EntryGenerator.test.ts +++ b/tools/egg-bundler/test/EntryGenerator.test.ts @@ -226,7 +226,7 @@ describe('EntryGenerator', () => { expect(worker).toContain("import { ManifestStore } from '@eggjs/core'"); expect(worker).toContain('import { startEgg } from "egg"'); - expect(worker).toContain('ManifestStore.setBundleStore(ManifestStore.fromBundle(MANIFEST_DATA'); + expect(worker).toContain('ManifestStore.setBundleStore(ManifestStore.fromBundle(__RUNTIME_MANIFEST_DATA'); expect(worker).toContain('__EGG_BUNDLE_MODULE_LOADER__'); expect(worker).toContain('__setBundleMap(__framework, __frameworkModule)'); expect(worker).not.toContain('__frameworkImport'); @@ -366,6 +366,155 @@ export async function startEgg(options) { expect(runtimeResult.controllerResolved).toBe('bundled-controller'); }); + it('restores tegg manifest paths so bundled controller/service/repository modules load by runtime path', async () => { + const resultFile = path.join(tmpDir, 'runtime-result.json'); + const moduleDir = path.join(tmpDir, 'modules/foo'); + await fs.writeFile(path.join(tmpDir, 'package.json'), JSON.stringify({ type: 'module' })); + await fs.mkdir(moduleDir, { recursive: true }); + await fs.writeFile( + path.join(moduleDir, 'package.json'), + JSON.stringify({ type: 'module', eggModule: { name: 'foo' } }), + ); + await fs.writeFile( + path.join(moduleDir, 'FooRepository.ts'), + ` +export class FooRepository { + static publicPrototype = { kind: 'repository', accessLevel: 'PUBLIC' }; +} +`, + ); + await fs.writeFile( + path.join(moduleDir, 'FooService.ts'), + ` +import { FooRepository } from './FooRepository.ts'; + +export class FooService { + static publicPrototype = { kind: 'service', accessLevel: 'PUBLIC' }; + static dependencies = [FooRepository.publicPrototype.kind]; +} +`, + ); + await fs.writeFile( + path.join(moduleDir, 'FooController.ts'), + ` +import { FooService } from './FooService.ts'; + +export class FooController { + static decoratedController = { path: '/foo', serviceAccess: FooService.publicPrototype.accessLevel }; +} +`, + ); + await writePackage( + path.join(tmpDir, 'node_modules/@eggjs/core'), + ` +export const ManifestStore = { + fromBundle(manifest, baseDir) { + return { manifest, baseDir }; + }, + setBundleStore(store) { + globalThis.__manifestStore = store; + }, +}; +`, + ); + await writePackage( + path.join(tmpDir, 'node_modules/@runtime/framework'), + ` +import fs from 'node:fs/promises'; +import path from 'node:path'; + +export async function startEgg() { + const loader = globalThis.__EGG_BUNDLE_MODULE_LOADER__; + const manifest = globalThis.__manifestStore.manifest; + const tegg = manifest.extensions.tegg; + const desc = tegg.moduleDescriptors[0]; + const runtimeFiles = desc.decoratedFiles.map((file) => path.join(desc.unitPath, file)); + const runtimeModules = runtimeFiles.map((file) => loader(file)); + const originalServiceFile = path.join(process.env.ORIGINAL_BASE_DIR, 'modules/foo/FooService.ts'); + const originalServiceModule = loader(originalServiceFile); + await fs.writeFile(process.env.EGG_RUNTIME_RESULT, JSON.stringify({ + manifestBaseDir: globalThis.__manifestStore.baseDir, + moduleReferencePath: tegg.moduleReferences[0].path, + descriptorUnitPath: desc.unitPath, + decoratedFiles: desc.decoratedFiles, + runtimeModuleNames: runtimeModules.flatMap((mod) => Object.keys(mod)), + serviceAccessLevel: runtimeModules[1].FooService.publicPrototype.accessLevel, + repositoryDependency: runtimeModules[1].FooService.dependencies[0], + controllerServiceAccess: runtimeModules[2].FooController.decoratedController.serviceAccess, + originalAbsoluteAliasAccessLevel: originalServiceModule.FooService.publicPrototype.accessLevel, + }, null, 2)); + return { + config: { cluster: { listen: { port: 0 } } }, + listen(_port, callback) { + callback(); + }, + }; +} +`, + ); + + const manifest = makeManifest({ + extensions: { + tegg: { + moduleReferences: [ + { + name: 'foo', + path: 'modules/foo', + }, + ], + moduleDescriptors: [ + { + name: 'foo', + unitPath: 'modules/foo', + decoratedFiles: ['FooRepository.ts', 'FooService.ts', 'FooController.ts'], + }, + ], + }, + }, + }); + const outputDir = path.join(tmpDir, 'dist'); + const gen = new EntryGenerator({ + baseDir: tmpDir, + outputDir, + framework: '@runtime/framework', + manifestLoader: createFakeLoader(manifest), + }); + const result = await gen.generate(); + + await execaNode(result.workerEntry, [], { + cwd: tmpDir, + env: { + ...process.env, + EGG_RUNTIME_RESULT: resultFile, + ORIGINAL_BASE_DIR: tmpDir, + }, + nodeOptions: ['--experimental-strip-types'], + timeout: 5_000, + }); + + const runtimeResult = JSON.parse(await fs.readFile(resultFile, 'utf8')) as { + manifestBaseDir: string; + moduleReferencePath: string; + descriptorUnitPath: string; + decoratedFiles: string[]; + runtimeModuleNames: string[]; + serviceAccessLevel: string; + repositoryDependency: string; + controllerServiceAccess: string; + originalAbsoluteAliasAccessLevel: string; + }; + const runtimeModuleDir = path.join(outputDir, 'modules/foo'); + expect(runtimeResult.manifestBaseDir).toBe(outputDir); + expect(runtimeResult.moduleReferencePath).toBe(runtimeModuleDir); + expect(runtimeResult.descriptorUnitPath).toBe(runtimeModuleDir); + expect(runtimeResult.decoratedFiles).toEqual(['FooRepository.ts', 'FooService.ts', 'FooController.ts']); + expect(runtimeResult.runtimeModuleNames).toEqual(['FooRepository', 'FooService', 'FooController']); + expect(runtimeResult.serviceAccessLevel).toBe('PUBLIC'); + expect(runtimeResult.repositoryDependency).toBe('repository'); + expect(runtimeResult.controllerServiceAccess).toBe('PUBLIC'); + expect(runtimeResult.originalAbsoluteAliasAccessLevel).toBe('PUBLIC'); + }); + it('loads externalized package files via createRequire instead of static imports', async () => { const manifest = makeManifest({ fileDiscovery: { diff --git a/tools/egg-bundler/test/__snapshots__/EntryGenerator.worker.canonical.snap b/tools/egg-bundler/test/__snapshots__/EntryGenerator.worker.canonical.snap index 4b2f8e79e3..ac598bc99c 100644 --- a/tools/egg-bundler/test/__snapshots__/EntryGenerator.worker.canonical.snap +++ b/tools/egg-bundler/test/__snapshots__/EntryGenerator.worker.canonical.snap @@ -55,6 +55,27 @@ const MANIFEST_DATA = { } as const; const __APP_ABSOLUTE_ALIASES: Array<[string, string]> = [["/app/controller/home.ts","app/controller/home.ts"],["/app/extend/context.ts","app/extend/context.ts"],["/app/service/user.ts","app/service/user.ts"],["/node_modules/@eggjs/fake-module/app/service/UserService.ts","node_modules/@eggjs/fake-module/app/service/UserService.ts"]]; const __APP_RESOLVE_CACHE_ALIASES: Array<[string, string]> = [["/app/extend/context.ts","app/extend/context.ts"]]; +const __restoreBundleRuntimePath = (filepath: string) => { + if (!filepath || path.isAbsolute(filepath)) return filepath; + return path.resolve(__outputDir, filepath); +}; +const __restoreBundleManifest = (manifest: typeof MANIFEST_DATA) => { + const restored = JSON.parse(JSON.stringify(manifest)); + const tegg = restored.extensions?.tegg; + if (tegg?.moduleReferences) { + tegg.moduleReferences = tegg.moduleReferences.map((ref) => ({ + ...ref, + path: __restoreBundleRuntimePath(ref.path), + })); + } + if (tegg?.moduleDescriptors) { + tegg.moduleDescriptors = tegg.moduleDescriptors.map((desc) => ({ + ...desc, + unitPath: __restoreBundleRuntimePath(desc.unitPath), + })); + } + return restored; +}; const __BUNDLE_MAP_REL: Record = { ["app/controller/home.ts"]: __m0, @@ -99,7 +120,8 @@ for (const [appAbsRequest, targetRel] of __APP_RESOLVE_CACHE_ALIASES) { } } -ManifestStore.setBundleStore(ManifestStore.fromBundle(MANIFEST_DATA as any, __outputDir)); +const __RUNTIME_MANIFEST_DATA = __restoreBundleManifest(MANIFEST_DATA); +ManifestStore.setBundleStore(ManifestStore.fromBundle(__RUNTIME_MANIFEST_DATA as any, __outputDir)); globalThis.__EGG_BUNDLE_MODULE_LOADER__ = (filepath) => { return __getBundleMap(filepath); };