Skip to content
Open
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
24 changes: 23 additions & 1 deletion tools/egg-bundler/src/lib/EntryGenerator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Comment on lines +282 to +296
};
Comment on lines +281 to +297
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The __restoreBundleManifest function is executed at application startup. Using JSON.parse(JSON.stringify(manifest)) to deep clone the entire manifest can be quite inefficient, especially for large applications where the manifest might be several megabytes.

Since we only need to modify specific paths within the tegg extension, we can optimize this by using an early return if the extension is missing and performing a shallow clone of the top-level structure and the tegg extension specifically. This avoids the overhead of stringifying and re-parsing the whole object.

const __restoreBundleManifest = (manifest: typeof MANIFEST_DATA) => {
  const tegg = manifest.extensions?.tegg;
  if (!tegg) return manifest;
  const restoredTegg = { ...tegg };
  if (restoredTegg.moduleReferences) {
    restoredTegg.moduleReferences = restoredTegg.moduleReferences.map((ref: any) => ({
      ...ref,
      path: __restoreBundleRuntimePath(ref.path),
    }));
  }
  if (restoredTegg.moduleDescriptors) {
    restoredTegg.moduleDescriptors = restoredTegg.moduleDescriptors.map((desc: any) => ({
      ...desc,
      unitPath: __restoreBundleRuntimePath(desc.unitPath),
    }));
  }
  return {
    ...manifest,
    extensions: {
      ...manifest.extensions,
      tegg: restoredTegg,
    },
  };
};

Comment on lines +281 to +297
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Using JSON.parse(JSON.stringify(manifest)) to deep clone the entire manifest at runtime is inefficient, especially as the manifest (particularly fileDiscovery and resolveCache) can grow quite large in complex applications. Since only the tegg extension data requires path restoration, it is more performant to perform a shallow clone of the manifest and only update the specific tegg properties. This avoids the overhead of serializing and re-parsing the bulk of the manifest data during application startup.

const __restoreBundleManifest = (manifest: typeof MANIFEST_DATA) => {
  const tegg = manifest.extensions?.tegg as any;
  if (!tegg) return manifest;

  const restoredTegg = { ...tegg };
  if (tegg.moduleReferences) {
    restoredTegg.moduleReferences = tegg.moduleReferences.map((ref: any) => ({
      ...ref,
      path: __restoreBundleRuntimePath(ref.path),
    }));
  }
  if (tegg.moduleDescriptors) {
    restoredTegg.moduleDescriptors = tegg.moduleDescriptors.map((desc: any) => ({
      ...desc,
      unitPath: __restoreBundleRuntimePath(desc.unitPath),
    }));
  }

  return {
    ...manifest,
    extensions: {
      ...manifest.extensions,
      tegg: restoredTegg,
    },
  } as any;
};

Comment on lines +281 to +297
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Using JSON.parse(JSON.stringify(manifest)) for deep cloning is inefficient, especially for large manifests which can be several megabytes in size. This adds unnecessary CPU and memory overhead during application startup. Since only the tegg extension paths need to be restored, a more efficient approach is to use object spreading to create a shallow copy of the manifest and only deep-clone the tegg extension object.

const __restoreBundleManifest = (manifest: typeof MANIFEST_DATA) => {
  const tegg = (manifest.extensions as any)?.tegg;
  if (!tegg) return manifest;
  return {
    ...manifest,
    extensions: {
      ...manifest.extensions,
      tegg: {
        ...tegg,
        moduleReferences: tegg.moduleReferences?.map((ref: any) => ({
          ...ref,
          path: __restoreBundleRuntimePath(ref.path),
        })),
        moduleDescriptors: tegg.moduleDescriptors?.map((desc: any) => ({
          ...desc,
          unitPath: __restoreBundleRuntimePath(desc.unitPath),
        })),
      },
    },
  };
};


const __BUNDLE_MAP_REL: Record<string, unknown> = {
${mapLines.join('\n')}
Expand Down Expand Up @@ -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);
};
Expand Down
151 changes: 150 additions & 1 deletion tools/egg-bundler/test/EntryGenerator.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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');
Expand Down Expand Up @@ -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: {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,27 @@ const MANIFEST_DATA = {
} as const;
const __APP_ABSOLUTE_ALIASES: Array<[string, string]> = [["<appBaseDir>/app/controller/home.ts","app/controller/home.ts"],["<appBaseDir>/app/extend/context.ts","app/extend/context.ts"],["<appBaseDir>/app/service/user.ts","app/service/user.ts"],["<appBaseDir>/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]> = [["<appBaseDir>/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<string, unknown> = {
["app/controller/home.ts"]: __m0,
Expand Down Expand Up @@ -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);
};
Expand Down
Loading