Skip to content

Commit a28abce

Browse files
killaguclaude
andauthored
feat(egg-bin): add manifest CLI command (#5850)
## Summary - Add `egg-bin manifest` command with three actions: `generate`, `validate`, `clean` - `generate`: forks child process to boot app in `metadataOnly` mode, writes `.egg/manifest.json` - `validate`: in-process `ManifestStore.load()` check with metadata output - `clean`: removes `.egg/manifest.json` - Extract `buildRequiresExecArgv()` from `dev.ts` into `BaseCommand` for reuse - Add E2E verification script (`ecosystem-ci/scripts/verify-manifest.mjs`) — tests full manifest lifecycle including app boot with manifest + health check - Update `.github/workflows/e2e-test.yml` to run manifest E2E in hello-tegg ### Local benchmark (cnpmcore, purge before each run) | Metric | No manifest | With manifest | Improvement | |--------|------------|---------------|-------------| | appStart | ~980ms | ~780ms | **~20%** | | loadFiles | ~660ms | ~490ms | **~26%** | | Load app.js | ~280ms | ~150ms | **~46%** | | Load Config | ~10ms | ~5ms | **~50%** | ## Test plan - [x] `pnpm --filter=@eggjs/bin run typecheck` passes - [x] `pnpm --filter=@eggjs/bin run pretest` (tsdown build) passes - [x] Local E2E: manifest generate/validate/clean on helloworld-tegg - [x] Local E2E: manifest generate/validate/clean + boot on cnpmcore - [x] Startup benchmark with `sudo purge` confirms ~20% cold start improvement - [ ] CI E2E workflow with hello-tegg 🤖 Generated with [Claude Code](https://claude.com/claude-code) <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **New Features** * Added a manifest CLI to generate, validate, and clean startup manifests. * **Scripts** * New manifest generation and verification scripts to produce, assert, run, and clean manifest lifecycles. * **Tests** * End-to-end CI verification added to build, validate, start, health-check, and clean manifests. * **Documentation** * New English and Chinese docs and sidebar entry describing the startup manifest and workflows. * **Refactor** * Simplified construction of Node process arguments for consistent command execution. * **Chores** * CI matrix updated (Node 20 removed). <!-- end of auto-generated comment: release notes by coderabbit.ai --> --------- Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 957c693 commit a28abce

File tree

13 files changed

+663
-16
lines changed

13 files changed

+663
-16
lines changed

.github/workflows/ci.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@ jobs:
6262
fail-fast: false
6363
matrix:
6464
os: ['ubuntu-latest', 'macos-latest', 'windows-latest']
65-
node: ['20', '22', '24']
65+
node: ['22', '24']
6666

6767
name: Test (${{ matrix.os }}, ${{ matrix.node }})
6868
runs-on: ${{ matrix.os }}
@@ -170,7 +170,7 @@ jobs:
170170
run: pnpm run ci
171171

172172
- name: Run example tests
173-
if: ${{ matrix.node != '20' && matrix.os != 'windows-latest' }}
173+
if: ${{ matrix.os != 'windows-latest' }}
174174
run: |
175175
pnpm run example:test:all
176176

.github/workflows/e2e-test.yml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -132,6 +132,10 @@ jobs:
132132
npm run lint
133133
npm run test
134134
npm run prepublishOnly
135+
136+
# Manifest E2E: generate, validate, boot with manifest, clean
137+
node ../../scripts/verify-manifest.mjs
138+
cd ..
135139
steps:
136140
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6
137141
- uses: ./.github/actions/clone
Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
1+
#!/usr/bin/env node
2+
3+
/**
4+
* E2E verification script for the egg-bin manifest CLI.
5+
*
6+
* Run this inside a project directory that has egg-bin and egg installed.
7+
* It tests the full manifest lifecycle:
8+
* 1. generate — creates .egg/manifest.json via metadataOnly boot
9+
* 2. validate — verifies the manifest is structurally valid
10+
* 3. boot with manifest — starts the app using the manifest and health-checks it
11+
* 4. clean — removes .egg/manifest.json
12+
*/
13+
14+
import { execSync } from 'node:child_process';
15+
import { existsSync, readFileSync, rmSync } from 'node:fs';
16+
import { join } from 'node:path';
17+
import { setTimeout as sleep } from 'node:timers/promises';
18+
19+
const projectDir = process.cwd();
20+
const manifestPath = join(projectDir, '.egg', 'manifest.json');
21+
const env = process.env.MANIFEST_VERIFY_ENV || 'unittest';
22+
const healthPort = process.env.MANIFEST_VERIFY_PORT || '7002';
23+
const healthTimeout = parseInt(process.env.MANIFEST_VERIFY_TIMEOUT || '60', 10);
24+
25+
function run(cmd) {
26+
console.log(`\n$ ${cmd}`);
27+
execSync(cmd, { stdio: 'inherit', cwd: projectDir });
28+
}
29+
30+
function runCapture(cmd) {
31+
console.log(`\n$ ${cmd}`);
32+
return execSync(cmd, { cwd: projectDir, encoding: 'utf-8' });
33+
}
34+
35+
function assert(condition, message) {
36+
if (!condition) {
37+
console.error(`FAIL: ${message}`);
38+
process.exit(1);
39+
}
40+
console.log(`PASS: ${message}`);
41+
}
42+
43+
console.log('=== Manifest E2E Verification ===');
44+
console.log('Project: %s', projectDir);
45+
console.log('Env: %s', env);
46+
47+
// Step 1: Clean any pre-existing manifest
48+
if (existsSync(manifestPath)) {
49+
rmSync(manifestPath);
50+
console.log('Cleaned pre-existing manifest');
51+
}
52+
53+
// Step 2: Generate manifest
54+
console.log('\n--- Step 1: Generate manifest ---');
55+
run(`npx egg-bin manifest generate --env=${env}`);
56+
57+
// Step 3: Verify manifest file exists and has valid structure
58+
console.log('\n--- Step 2: Verify manifest structure ---');
59+
assert(existsSync(manifestPath), '.egg/manifest.json exists after generate');
60+
61+
const manifest = JSON.parse(readFileSync(manifestPath, 'utf-8'));
62+
assert(manifest.version === 1, 'manifest version is 1');
63+
assert(typeof manifest.generatedAt === 'string' && manifest.generatedAt.length > 0, 'manifest has generatedAt');
64+
assert(typeof manifest.invalidation === 'object', 'manifest has invalidation');
65+
assert(manifest.invalidation.serverEnv === env, `manifest serverEnv matches "${env}"`);
66+
assert(typeof manifest.resolveCache === 'object', 'manifest has resolveCache');
67+
assert(typeof manifest.fileDiscovery === 'object', 'manifest has fileDiscovery');
68+
assert(typeof manifest.extensions === 'object', 'manifest has extensions');
69+
70+
const resolveCacheCount = Object.keys(manifest.resolveCache).length;
71+
const fileDiscoveryCount = Object.keys(manifest.fileDiscovery).length;
72+
console.log(' resolveCache: %d entries', resolveCacheCount);
73+
console.log(' fileDiscovery: %d entries', fileDiscoveryCount);
74+
75+
// Step 4: Validate manifest via CLI
76+
console.log('\n--- Step 3: Validate manifest via CLI ---');
77+
run(`npx egg-bin manifest validate --env=${env}`);
78+
79+
// Step 5: Boot the app with manifest and verify it starts correctly
80+
console.log('\n--- Step 4: Boot app with manifest ---');
81+
try {
82+
run(`npx eggctl start --port=${healthPort} --env=${env} --daemon`);
83+
84+
const healthUrl = `http://127.0.0.1:${healthPort}/`;
85+
const startTime = Date.now();
86+
let ready = false;
87+
88+
console.log(`Waiting for app at ${healthUrl} (timeout: ${healthTimeout}s)...`);
89+
while (true) {
90+
try {
91+
const output = runCapture(`curl -s -o /dev/null -w "%{http_code}" "${healthUrl}"`);
92+
const status = output.trim();
93+
console.log(' Health check: status=%s', status);
94+
// Any HTTP response (not connection refused) means the app is up.
95+
// Not all apps have a route on `/`, so we accept any status code.
96+
if (status !== '000') {
97+
ready = true;
98+
break;
99+
}
100+
} catch {
101+
console.log(' Health check: connection refused, retrying...');
102+
}
103+
104+
const elapsed = (Date.now() - startTime) / 1000;
105+
if (elapsed >= healthTimeout) {
106+
console.log(' Health check timed out after %ds', elapsed);
107+
break;
108+
}
109+
110+
await sleep(2000);
111+
}
112+
113+
run(`npx eggctl stop`);
114+
assert(ready, 'App booted successfully with manifest');
115+
} catch (err) {
116+
// Try to stop if started
117+
try {
118+
run(`npx eggctl stop`);
119+
} catch {
120+
/* ignore */
121+
}
122+
console.error('Boot test failed:', err.message);
123+
process.exit(1);
124+
}
125+
126+
// Step 6: Clean manifest
127+
console.log('\n--- Step 5: Clean manifest ---');
128+
run(`npx egg-bin manifest clean`);
129+
assert(!existsSync(manifestPath), '.egg/manifest.json removed after clean');
130+
131+
console.log('\n=== All manifest E2E checks passed ===');

pnpm-lock.yaml

Lines changed: 3 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

site/.vitepress/config.mts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -312,6 +312,7 @@ function sidebarCore(): DefaultTheme.SidebarItem[] {
312312
{ text: 'Internationalization', link: 'i18n' },
313313
{ text: 'View Template', link: 'view' },
314314
{ text: 'Security', link: 'security' },
315+
{ text: 'Startup Manifest', link: 'manifest' },
315316
],
316317
},
317318
];
@@ -422,6 +423,7 @@ function sidebarCoreZhCN(): DefaultTheme.SidebarItem[] {
422423
{ text: '国际化', link: 'i18n' },
423424
{ text: '模板渲染', link: 'view' },
424425
{ text: '安全', link: 'security' },
426+
{ text: '启动清单', link: 'manifest' },
425427
],
426428
},
427429
];

site/docs/core/manifest.md

Lines changed: 141 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,141 @@
1+
# Startup Manifest
2+
3+
Egg provides a startup manifest mechanism that caches file discovery and module resolution results to accelerate application cold starts.
4+
5+
## How It Works
6+
7+
Every time an application starts, the framework performs extensive filesystem operations:
8+
9+
- **Module resolution**: Hundreds of `fs.existsSync` calls probing `.ts`, `.js`, `.mjs` extensions
10+
- **File discovery**: Multiple `globby.sync` scans across plugin, config, and extension directories
11+
- **tegg module scanning**: Traversing module directories and `import()`-ing decorator files to collect metadata
12+
13+
The manifest mechanism collects these results on the first startup and writes them to `.egg/manifest.json`. Subsequent startups read from this cache, skipping redundant file I/O.
14+
15+
## Performance Improvement
16+
17+
Measured on cnpmcore in a container cold-start scenario (no filesystem page cache):
18+
19+
| Metric | No Manifest | With Manifest | Improvement |
20+
| ----------- | ----------- | ------------- | ----------- |
21+
| App Start | ~980ms | ~780ms | **~20%** |
22+
| Load Files | ~660ms | ~490ms | **~26%** |
23+
| Load app.js | ~280ms | ~150ms | **~46%** |
24+
25+
> Note: In local development, the OS page cache makes file I/O nearly zero-cost, so the improvement is negligible. The manifest primarily optimizes container cold starts and CI/CD environments without warm caches.
26+
27+
## Usage
28+
29+
### CLI Management (Recommended)
30+
31+
`egg-bin` provides a `manifest` command to manage the startup manifest:
32+
33+
#### Generate
34+
35+
```bash
36+
# Generate for production
37+
$ egg-bin manifest generate --env=prod
38+
39+
# Specify environment and scope
40+
$ egg-bin manifest generate --env=prod --scope=aliyun
41+
42+
# Specify framework
43+
$ egg-bin manifest generate --env=prod --framework=yadan
44+
```
45+
46+
The generation process boots the app in `metadataOnly` mode (skipping lifecycle hooks, only collecting metadata), then writes the results to `.egg/manifest.json`.
47+
48+
#### Validate
49+
50+
```bash
51+
$ egg-bin manifest validate --env=prod
52+
```
53+
54+
Example output:
55+
56+
```
57+
[manifest] Manifest is valid
58+
[manifest] version: 1
59+
[manifest] generatedAt: 2026-03-29T12:13:18.039Z
60+
[manifest] serverEnv: prod
61+
[manifest] serverScope:
62+
[manifest] resolveCache entries: 416
63+
[manifest] fileDiscovery entries: 31
64+
[manifest] extension entries: 1
65+
```
66+
67+
If the manifest is invalid or missing, the command exits with a non-zero code.
68+
69+
#### Clean
70+
71+
```bash
72+
$ egg-bin manifest clean
73+
```
74+
75+
### Automatic Generation
76+
77+
After a normal startup, the framework automatically generates a manifest during the `ready` phase (via `dumpManifest`). On the next startup, if the manifest is valid, it is automatically used.
78+
79+
## Invalidation
80+
81+
The manifest includes fingerprint data and is automatically invalidated when:
82+
83+
- **Lockfile changes**: `pnpm-lock.yaml`, `package-lock.json`, or `yarn.lock` mtime or size changes
84+
- **Config directory changes**: Files in `config/` change (MD5 fingerprint)
85+
- **Environment mismatch**: `serverEnv` or `serverScope` differs from the manifest
86+
- **TypeScript state change**: `EGG_TYPESCRIPT` enabled/disabled state changes
87+
- **Version mismatch**: Manifest format version differs
88+
89+
When the manifest is invalid, the framework falls back to normal file discovery — startup is never blocked.
90+
91+
## Environment Variables
92+
93+
| Variable | Description | Default |
94+
| -------------- | ---------------------------- | ----------------------------------------------------- |
95+
| `EGG_MANIFEST` | Enable manifest in local env | `false` (manifest not loaded in local env by default) |
96+
97+
> Local development (`serverEnv=local`) does not load the manifest by default, since files change frequently. Set `EGG_MANIFEST=true` to force-enable.
98+
99+
## Deployment Recommendations
100+
101+
### Container Deployment
102+
103+
Generate the manifest in your Dockerfile after building:
104+
105+
```dockerfile
106+
# Install dependencies and build
107+
RUN npm install --production
108+
RUN npm run build
109+
110+
# Generate startup manifest
111+
RUN npx egg-bin manifest generate --env=prod
112+
113+
# Start the app (manifest is used automatically)
114+
CMD ["npm", "start"]
115+
```
116+
117+
### CI/CD Pipelines
118+
119+
Generate the manifest during the build stage and deploy it with the artifact:
120+
121+
```bash
122+
# Build
123+
npm run build
124+
125+
# Generate manifest
126+
npx egg-bin manifest generate --env=prod
127+
128+
# Validate manifest
129+
npx egg-bin manifest validate --env=prod
130+
131+
# Package (includes .egg/manifest.json)
132+
tar -zcvf release.tgz .
133+
```
134+
135+
## Important Notes
136+
137+
1. **Environment-bound**: The manifest is bound to `serverEnv` and `serverScope` (deployment type, e.g. `aliyun`). Different environments or deployment types require separate manifests.
138+
2. **Regenerate after dependency changes**: Installing or updating dependencies changes the lockfile, which automatically invalidates the manifest.
139+
3. **`.egg` directory**: The manifest is stored at `.egg/manifest.json`. Consider adding `.egg/` to `.gitignore`.
140+
4. **Safe fallback**: A missing or invalid manifest causes the framework to fall back to normal discovery — startup is never broken.
141+
5. **metadataOnly mode**: `manifest generate` does not run the full application lifecycle — no database connections or external services are started.

0 commit comments

Comments
 (0)