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
3 changes: 1 addition & 2 deletions packages/core/src/loader/egg_loader.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1763,12 +1763,11 @@ export class EggLoader {
* Generate startup manifest from collected data.
* Should be called after all loading phases complete.
*/
generateManifest(extensions?: Record<string, unknown>): StartupManifest {
generateManifest(): StartupManifest {
return this.manifest.generateManifest({
serverEnv: this.serverEnv,
serverScope: this.serverScope,
typescriptEnabled: isSupportTypeScript(),
extensions,
});
}
}
Expand Down
11 changes: 9 additions & 2 deletions packages/core/src/loader/manifest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ export class ManifestStore {
// Collectors for manifest generation (populated during loading)
readonly #resolveCacheCollector: Record<string, string | null> = {};
readonly #fileDiscoveryCollector: Record<string, string[]> = {};
readonly #extensionCollector: Record<string, unknown> = {};

private constructor(data: StartupManifest, baseDir: string) {
this.data = data;
Expand Down Expand Up @@ -194,6 +195,13 @@ export class ManifestStore {
return this.data.extensions?.[name];
}

/**
* Register plugin extension data for manifest generation.
*/
setExtension(name: string, data: unknown): void {
this.#extensionCollector[name] = data;
}

// --- Generation APIs ---

/**
Expand All @@ -210,7 +218,7 @@ export class ManifestStore {
serverScope: options.serverScope,
typescriptEnabled: options.typescriptEnabled,
},
extensions: options.extensions ?? {},
extensions: this.#extensionCollector,
resolveCache: this.#resolveCacheCollector,
fileDiscovery: this.#fileDiscoveryCollector,
};
Expand Down Expand Up @@ -309,5 +317,4 @@ export interface ManifestGenerateOptions {
serverEnv: string;
serverScope: string;
typescriptEnabled: boolean;
extensions?: Record<string, unknown>;
}
84 changes: 80 additions & 4 deletions packages/core/test/loader/manifest.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,19 +64,19 @@ describe('ManifestStore', () => {
}
});

it('should preserve provided extensions', () => {
it('should preserve extensions set via setExtension', () => {
const baseDir = setupBaseDir();
try {
const extensions = { tegg: { moduleReferences: [{ name: 'mod', path: '/tmp/mod' }] } };
const teggData = { moduleReferences: [{ name: 'mod', path: '/tmp/mod' }] };
const collector = ManifestStore.createCollector(baseDir);
collector.setExtension('tegg', teggData);
const manifest = collector.generateManifest({
serverEnv: 'prod',
serverScope: '',
typescriptEnabled: true,
extensions,
});

assert.deepStrictEqual(manifest.extensions, extensions);
assert.deepStrictEqual(manifest.extensions, { tegg: teggData });
} finally {
fs.rmSync(baseDir, { recursive: true, force: true });
}
Expand Down Expand Up @@ -474,4 +474,80 @@ describe('ManifestStore', () => {
}
});
});

describe('setExtension()', () => {
it('should store and retrieve extension data', () => {
const baseDir = setupBaseDir();
try {
const collector = ManifestStore.createCollector(baseDir);
const data = { modules: ['a', 'b'] };
collector.setExtension('tegg', data);
// Not accessible via getExtension until generateManifest
// (getExtension reads from data, not collector)
const manifest = collector.generateManifest({
serverEnv: 'prod',
serverScope: '',
typescriptEnabled: true,
});
assert.deepStrictEqual(manifest.extensions.tegg, data);
} finally {
fs.rmSync(baseDir, { recursive: true, force: true });
}
});

it('should support multiple extension keys', () => {
const baseDir = setupBaseDir();
try {
const collector = ManifestStore.createCollector(baseDir);
collector.setExtension('tegg', { a: 1 });
collector.setExtension('custom', { b: 2 });
const manifest = collector.generateManifest({
serverEnv: 'prod',
serverScope: '',
typescriptEnabled: true,
});
assert.deepStrictEqual(manifest.extensions.tegg, { a: 1 });
assert.deepStrictEqual(manifest.extensions.custom, { b: 2 });
} finally {
fs.rmSync(baseDir, { recursive: true, force: true });
}
});

it('should overwrite previous value for same key', () => {
const baseDir = setupBaseDir();
try {
const collector = ManifestStore.createCollector(baseDir);
collector.setExtension('tegg', { old: true });
collector.setExtension('tegg', { new: true });
const manifest = collector.generateManifest({
serverEnv: 'prod',
serverScope: '',
typescriptEnabled: true,
});
assert.deepStrictEqual(manifest.extensions.tegg, { new: true });
} finally {
fs.rmSync(baseDir, { recursive: true, force: true });
}
});

it('should survive write → load roundtrip', async () => {
const baseDir = setupBaseDir();
try {
const collector = ManifestStore.createCollector(baseDir);
collector.setExtension('tegg', { roundtrip: true });
const manifest = collector.generateManifest({
serverEnv: 'prod',
serverScope: '',
typescriptEnabled: true,
});
await ManifestStore.write(baseDir, manifest);

const store = ManifestStore.load(baseDir, 'prod', '')!;
assert.ok(store);
assert.deepStrictEqual(store.getExtension('tegg'), { roundtrip: true });
} finally {
fs.rmSync(baseDir, { recursive: true, force: true });
}
});
});
});
6 changes: 3 additions & 3 deletions packages/core/test/loader/manifest_roundtrip.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,12 +29,12 @@ describe('ManifestStore roundtrip: generate → write → load', () => {
collector.resolveModule(path.join(baseDir, 'missing'), () => undefined);
collector.globFiles(path.join(baseDir, 'app/controller'), () => ['home.ts', 'user.ts']);

const extensions = { tegg: { moduleReferences: [{ name: 'foo', path: '/tmp/foo' }] } };
const teggData = { moduleReferences: [{ name: 'foo', path: '/tmp/foo' }] };
collector.setExtension('tegg', teggData);
const original = collector.generateManifest({
serverEnv: 'prod',
serverScope: '',
typescriptEnabled: true,
extensions,
});
await ManifestStore.write(baseDir, original);

Expand All @@ -46,7 +46,7 @@ describe('ManifestStore roundtrip: generate → write → load', () => {
assert.equal(original.resolveCache['some/path'], 'resolved/path');
assert.equal(original.resolveCache['missing'], null);
assert.deepStrictEqual(original.fileDiscovery['app/controller'], ['home.ts', 'user.ts']);
assert.deepStrictEqual(store.data.extensions, extensions);
assert.deepStrictEqual(store.data.extensions, { tegg: teggData });
assert.equal(store.data.version, original.version);
assert.equal(store.data.generatedAt, original.generatedAt);

Expand Down
28 changes: 26 additions & 2 deletions packages/egg/src/lib/egg.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import path from 'node:path';
import { performance } from 'node:perf_hooks';

import { Cookies as ContextCookies } from '@eggjs/cookies';
import { EggCore, Router } from '@eggjs/core';
import { EggCore, Router, ManifestStore } from '@eggjs/core';
import type { EggCoreOptions, Next, MiddlewareFunc as EggCoreMiddlewareFunc, ILifecycleBoot } from '@eggjs/core';
import { utils as eggUtils } from '@eggjs/core';
import { extend } from '@eggjs/extend2';
Expand Down Expand Up @@ -190,6 +190,7 @@ export class EggApplicationCore extends EggCore {
const dumpStartTime = Date.now();
this.dumpConfig();
this.dumpTiming();
this.dumpManifest();
this.coreLogger.info('[egg] dump config after ready, %sms', Date.now() - dumpStartTime);
}),
);
Expand All @@ -214,7 +215,7 @@ export class EggApplicationCore extends EggCore {

// single process mode will close agent before app close
if (this.type === 'application' && this.options.mode === 'single') {
await this.agent!.close();
await this.agent?.close();
}

for (const logger of this.loggers.values()) {
Expand Down Expand Up @@ -533,6 +534,29 @@ export class EggApplicationCore extends EggCore {
}
}

/**
* Generate and save startup manifest for faster subsequent startups.
* Only generates when no valid manifest was loaded (avoids overwriting during manifest-accelerated starts).
*/
dumpManifest(): void {
try {
// Skip in local env (manifest is not loaded there unless EGG_MANIFEST=true)
if (this.loader.serverEnv === 'local' && process.env.EGG_MANIFEST !== 'true') {
return;
}
// Skip if we loaded from a valid manifest (generatedAt is truthy)
if (this.loader.manifest.data.generatedAt) {
return;
}
const manifest = this.loader.generateManifest();
ManifestStore.write(this.baseDir, manifest).catch((err: Error) => {
this.coreLogger.warn('[egg] dumpManifest write error: %s', err.message);
});
} catch (err: any) {
this.coreLogger.warn('[egg] dumpManifest error: %s', err.message);
}
}

protected override customEggPaths(): string[] {
return [path.dirname(import.meta.dirname), ...super.customEggPaths()];
}
Expand Down
27 changes: 19 additions & 8 deletions packages/egg/src/lib/start.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ export interface StartEggOptions {
mode?: 'single';
env?: string;
plugins?: EggPlugin;
/** Skip lifecycle hooks, only trigger loadMetadata for manifest generation */
metadataOnly?: boolean;
}

export interface SingleModeApplication extends Application {
Expand Down Expand Up @@ -53,18 +55,27 @@ export async function startEgg(options: StartEggOptions = {}): Promise<SingleMod
ApplicationClass = framework.Application;
}

const agent = new AgentClass({
...options,
}) as SingleModeAgent;
await agent.ready();
// In metadataOnly mode, skip agent entirely — only app metadata is needed
let agent: SingleModeAgent | undefined;
if (!options.metadataOnly) {
agent = new AgentClass({
...options,
}) as SingleModeAgent;
await agent.ready();
}

const application = new ApplicationClass({
...options,
}) as SingleModeApplication;
application.agent = agent;
agent.application = application;
if (agent) {
application.agent = agent;
agent.application = application;
}
await application.ready();

// emit egg-ready message in agent and application
application.messenger.broadcast('egg-ready');
if (!options.metadataOnly) {
// emit egg-ready message in agent and application
application.messenger.broadcast('egg-ready');
}
return application;
}
3 changes: 3 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

61 changes: 56 additions & 5 deletions tegg/core/loader/src/LoaderFactory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,33 @@ import {

export type LoaderCreator = (unitPath: string) => Loader;

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

export interface ManifestModuleDescriptor {
name: string;
unitPath: string;
optional?: boolean;
/** Files containing decorated classes, relative to unitPath */
decoratedFiles: string[];
}

/** Shape of the 'tegg' manifest extension stored via ManifestStore.setExtension() */
export interface TeggManifestExtension {
moduleReferences: ManifestModuleReference[];
moduleDescriptors: ManifestModuleDescriptor[];
}

export const TEGG_MANIFEST_KEY = 'tegg';

export interface LoadAppManifest {
moduleDescriptors: ManifestModuleDescriptor[];
}

export class LoaderFactory {
private static loaderCreatorMap: Map<EggLoadUnitTypeLike, LoaderCreator> = new Map();

Expand All @@ -25,14 +52,38 @@ export class LoaderFactory {
this.loaderCreatorMap.set(type, creator);
}

static async loadApp(moduleReferences: readonly ModuleReference[]): Promise<ModuleDescriptor[]> {
static async loadApp(
moduleReferences: readonly ModuleReference[],
manifest?: LoadAppManifest,
): Promise<ModuleDescriptor[]> {
const result: ModuleDescriptor[] = [];
const multiInstanceClazzList: EggProtoImplClass[] = [];

const manifestMap = new Map<string, ManifestModuleDescriptor>();
if (manifest?.moduleDescriptors) {
for (const desc of manifest.moduleDescriptors) {
manifestMap.set(desc.unitPath, desc);
}
}

// Lazy-load ModuleLoader to avoid circular dependency
// (ModuleLoader.ts calls LoaderFactory.registerLoader at module scope)
let ModuleLoaderClass: (typeof import('./impl/ModuleLoader.ts'))['ModuleLoader'] | undefined;
if (manifestMap.size > 0) {
ModuleLoaderClass = (await import('./impl/ModuleLoader.ts')).ModuleLoader;
}

for (const moduleReference of moduleReferences) {
const loader = LoaderFactory.createLoader(
moduleReference.path,
moduleReference.loaderType || EggLoadUnitType.MODULE,
);
const manifestDesc = manifestMap.get(moduleReference.path);
const loaderType = moduleReference.loaderType || EggLoadUnitType.MODULE;

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

const res: ModuleDescriptor = {
name: moduleReference.name,
unitPath: moduleReference.path,
Expand Down
Loading
Loading