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
4 changes: 2 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ jobs:
fail-fast: false
matrix:
os: ['ubuntu-latest', 'macos-latest', 'windows-latest']
node: ['20', '22', '24']
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.

为啥把这个去掉了?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

四月生命周期就结束了。

node: ['22', '24']

name: Test (${{ matrix.os }}, ${{ matrix.node }})
runs-on: ${{ matrix.os }}
Expand Down Expand Up @@ -170,7 +170,7 @@ jobs:
run: pnpm run ci

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

Expand Down
4 changes: 4 additions & 0 deletions .github/workflows/e2e-test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,10 @@ jobs:
npm run lint
npm run test
npm run prepublishOnly

# Manifest E2E: generate, validate, boot with manifest, clean
node ../../scripts/verify-manifest.mjs
cd ..
steps:
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6
- uses: ./.github/actions/clone
Expand Down
131 changes: 131 additions & 0 deletions ecosystem-ci/scripts/verify-manifest.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
#!/usr/bin/env node

/**
* E2E verification script for the egg-bin manifest CLI.
*
* Run this inside a project directory that has egg-bin and egg installed.
* It tests the full manifest lifecycle:
* 1. generate — creates .egg/manifest.json via metadataOnly boot
* 2. validate — verifies the manifest is structurally valid
* 3. boot with manifest — starts the app using the manifest and health-checks it
* 4. clean — removes .egg/manifest.json
*/

import { execSync } from 'node:child_process';
import { existsSync, readFileSync, rmSync } from 'node:fs';
import { join } from 'node:path';
import { setTimeout as sleep } from 'node:timers/promises';

const projectDir = process.cwd();
const manifestPath = join(projectDir, '.egg', 'manifest.json');
const env = process.env.MANIFEST_VERIFY_ENV || 'unittest';
const healthPort = process.env.MANIFEST_VERIFY_PORT || '7002';
const healthTimeout = parseInt(process.env.MANIFEST_VERIFY_TIMEOUT || '60', 10);

function run(cmd) {
console.log(`\n$ ${cmd}`);
execSync(cmd, { stdio: 'inherit', cwd: projectDir });

Check warning

Code scanning / CodeQL

Indirect uncontrolled command line Medium

This command depends on an unsanitized
environment variable
.
This command depends on an unsanitized
environment variable
.

Copilot Autofix

AI 2 days ago

In general, the right fix is to avoid passing a single concatenated command string to execSync, and instead use an API that does not invoke a shell and takes the executable plus its arguments as an array, e.g. execFileSync. This inherently avoids shell parsing, so environment variables like MANIFEST_VERIFY_ENV and MANIFEST_VERIFY_PORT are passed as literal arguments and cannot alter the shell command structure.

For this file, the simplest change without altering functionality is:

  • Replace the generic run(cmd) and runCapture(cmd) helpers with argument-based wrappers that:
    • Accept command and args separately.
    • Log a reconstructed “pretty” shell-style command for human readability (using proper quoting when desired).
    • Call execFileSync (or execSync with { shell: false }, but execFileSync is clearer) with the command and argument array.
  • Update the call site that currently passes a string built with template interpolation:
    • Replace run(\npx egg-bin manifest generate --env=${env}`);`
    • With something like run('npx', ['egg-bin', 'manifest', 'generate', --env=${env}]);
  • Optionally, introduce a small helper to format the logged command (formatCommandForLog) to mimic shell syntax in logs, but that helper should not actually invoke a shell—only build a string for console.log.

All changes are confined to ecosystem-ci/scripts/verify-manifest.mjs. We can reuse the existing execSync import by switching to execFileSync (also exported from node:child_process), which requires a small import change. No change in observable behavior occurs other than removing the possibility of shell interpretation.

Suggested changeset 1
ecosystem-ci/scripts/verify-manifest.mjs

Autofix patch

Autofix patch
Run the following command in your local git repository to apply this patch
cat << 'EOF' | git apply
diff --git a/ecosystem-ci/scripts/verify-manifest.mjs b/ecosystem-ci/scripts/verify-manifest.mjs
--- a/ecosystem-ci/scripts/verify-manifest.mjs
+++ b/ecosystem-ci/scripts/verify-manifest.mjs
@@ -11,7 +11,7 @@
  *   4. clean — removes .egg/manifest.json
  */
 
-import { execSync } from 'node:child_process';
+import { execFileSync } from 'node:child_process';
 import { existsSync, readFileSync, rmSync } from 'node:fs';
 import { join } from 'node:path';
 import { setTimeout as sleep } from 'node:timers/promises';
@@ -22,16 +22,21 @@
 const healthPort = process.env.MANIFEST_VERIFY_PORT || '7002';
 const healthTimeout = parseInt(process.env.MANIFEST_VERIFY_TIMEOUT || '60', 10);
 
-function run(cmd) {
-  console.log(`\n$ ${cmd}`);
-  execSync(cmd, { stdio: 'inherit', cwd: projectDir });
+function formatCommandForLog(command, args) {
+  const renderedArgs = (args || []).map(a => JSON.stringify(String(a)));
+  return [command].concat(renderedArgs).join(' ');
 }
 
-function runCapture(cmd) {
-  console.log(`\n$ ${cmd}`);
-  return execSync(cmd, { cwd: projectDir, encoding: 'utf-8' });
+function run(command, args = []) {
+  console.log(`\n$ ${formatCommandForLog(command, args)}`);
+  execFileSync(command, args, { stdio: 'inherit', cwd: projectDir });
 }
 
+function runCapture(command, args = []) {
+  console.log(`\n$ ${formatCommandForLog(command, args)}`);
+  return execFileSync(command, args, { cwd: projectDir, encoding: 'utf-8' });
+}
+
 function assert(condition, message) {
   if (!condition) {
     console.error(`FAIL: ${message}`);
@@ -52,7 +53,7 @@
 
 // Step 2: Generate manifest
 console.log('\n--- Step 1: Generate manifest ---');
-run(`npx egg-bin manifest generate --env=${env}`);
+run('npx', ['egg-bin', 'manifest', 'generate', `--env=${env}`]);
 
 // Step 3: Verify manifest file exists and has valid structure
 console.log('\n--- Step 2: Verify manifest structure ---');
EOF
@@ -11,7 +11,7 @@
* 4. clean removes .egg/manifest.json
*/

import { execSync } from 'node:child_process';
import { execFileSync } from 'node:child_process';
import { existsSync, readFileSync, rmSync } from 'node:fs';
import { join } from 'node:path';
import { setTimeout as sleep } from 'node:timers/promises';
@@ -22,16 +22,21 @@
const healthPort = process.env.MANIFEST_VERIFY_PORT || '7002';
const healthTimeout = parseInt(process.env.MANIFEST_VERIFY_TIMEOUT || '60', 10);

function run(cmd) {
console.log(`\n$ ${cmd}`);
execSync(cmd, { stdio: 'inherit', cwd: projectDir });
function formatCommandForLog(command, args) {
const renderedArgs = (args || []).map(a => JSON.stringify(String(a)));
return [command].concat(renderedArgs).join(' ');
}

function runCapture(cmd) {
console.log(`\n$ ${cmd}`);
return execSync(cmd, { cwd: projectDir, encoding: 'utf-8' });
function run(command, args = []) {
console.log(`\n$ ${formatCommandForLog(command, args)}`);
execFileSync(command, args, { stdio: 'inherit', cwd: projectDir });
}

function runCapture(command, args = []) {
console.log(`\n$ ${formatCommandForLog(command, args)}`);
return execFileSync(command, args, { cwd: projectDir, encoding: 'utf-8' });
}

function assert(condition, message) {
if (!condition) {
console.error(`FAIL: ${message}`);
@@ -52,7 +53,7 @@

// Step 2: Generate manifest
console.log('\n--- Step 1: Generate manifest ---');
run(`npx egg-bin manifest generate --env=${env}`);
run('npx', ['egg-bin', 'manifest', 'generate', `--env=${env}`]);

// Step 3: Verify manifest file exists and has valid structure
console.log('\n--- Step 2: Verify manifest structure ---');
Copilot is powered by AI and may make mistakes. Always verify output.
}

function runCapture(cmd) {
console.log(`\n$ ${cmd}`);
return execSync(cmd, { cwd: projectDir, encoding: 'utf-8' });

Check warning

Code scanning / CodeQL

Indirect uncontrolled command line Medium

This command depends on an unsanitized
environment variable
.

Copilot Autofix

AI 2 days ago

General fix: avoid executing tainted data via a shell. Prefer execFileSync / spawnSync with an argument array, and validate any environment-derived values (like ports) before using them. Where shell commands must remain string-based, perform strict validation/whitelisting.

Best minimal fix here:

  1. Keep the existing run/runCapture APIs for the rest of the file to avoid behavior changes.
  2. For the health-check curl command, stop going through the generic runCapture wrapper that uses execSync with a shell string.
  3. Instead, import execFileSync from node:child_process and call it directly with a fixed executable (curl) and an explicit argument array, passing healthUrl as a plain argument so it is not interpreted by the shell.
  4. Additionally, validate healthPort when it is read from the environment to ensure it is a reasonable numeric TCP port, falling back to a safe default (7002) if invalid. This removes the ability to inject shell metacharacters via the port value and also improves robustness.

Concretely:

  • Update the import on line 14 to also import execFileSync.
  • After healthTimeout is defined, parse and validate healthPort into a numeric healthPortNumber within 1–65535, then derive healthPort from that numeric value (ensuring it contains only digits).
  • Replace the runCapture call at line 91 with a direct execFileSync('curl', [...]) call that returns the HTTP status code without invoking a shell.

All changes are confined to ecosystem-ci/scripts/verify-manifest.mjs.

Suggested changeset 1
ecosystem-ci/scripts/verify-manifest.mjs

Autofix patch

Autofix patch
Run the following command in your local git repository to apply this patch
cat << 'EOF' | git apply
diff --git a/ecosystem-ci/scripts/verify-manifest.mjs b/ecosystem-ci/scripts/verify-manifest.mjs
--- a/ecosystem-ci/scripts/verify-manifest.mjs
+++ b/ecosystem-ci/scripts/verify-manifest.mjs
@@ -11,7 +11,7 @@
  *   4. clean — removes .egg/manifest.json
  */
 
-import { execSync } from 'node:child_process';
+import { execSync, execFileSync } from 'node:child_process';
 import { existsSync, readFileSync, rmSync } from 'node:fs';
 import { join } from 'node:path';
 import { setTimeout as sleep } from 'node:timers/promises';
@@ -19,7 +19,15 @@
 const projectDir = process.cwd();
 const manifestPath = join(projectDir, '.egg', 'manifest.json');
 const env = process.env.MANIFEST_VERIFY_ENV || 'unittest';
-const healthPort = process.env.MANIFEST_VERIFY_PORT || '7002';
+const rawHealthPort = process.env.MANIFEST_VERIFY_PORT || '7002';
+const healthPortNumber = (() => {
+  const n = Number(rawHealthPort);
+  if (!Number.isInteger(n) || n < 1 || n > 65535) {
+    return 7002;
+  }
+  return n;
+})();
+const healthPort = String(healthPortNumber);
 const healthTimeout = parseInt(process.env.MANIFEST_VERIFY_TIMEOUT || '60', 10);
 
 function run(cmd) {
@@ -88,7 +96,7 @@
   console.log(`Waiting for app at ${healthUrl} (timeout: ${healthTimeout}s)...`);
   while (true) {
     try {
-      const output = runCapture(`curl -s -o /dev/null -w "%{http_code}" "${healthUrl}"`);
+      const output = execFileSync('curl', ['-s', '-o', '/dev/null', '-w', '%{http_code}', healthUrl], { cwd: projectDir, encoding: 'utf-8' });
       const status = output.trim();
       console.log('  Health check: status=%s', status);
       // Any HTTP response (not connection refused) means the app is up.
EOF
@@ -11,7 +11,7 @@
* 4. clean removes .egg/manifest.json
*/

import { execSync } from 'node:child_process';
import { execSync, execFileSync } from 'node:child_process';
import { existsSync, readFileSync, rmSync } from 'node:fs';
import { join } from 'node:path';
import { setTimeout as sleep } from 'node:timers/promises';
@@ -19,7 +19,15 @@
const projectDir = process.cwd();
const manifestPath = join(projectDir, '.egg', 'manifest.json');
const env = process.env.MANIFEST_VERIFY_ENV || 'unittest';
const healthPort = process.env.MANIFEST_VERIFY_PORT || '7002';
const rawHealthPort = process.env.MANIFEST_VERIFY_PORT || '7002';
const healthPortNumber = (() => {
const n = Number(rawHealthPort);
if (!Number.isInteger(n) || n < 1 || n > 65535) {
return 7002;
}
return n;
})();
const healthPort = String(healthPortNumber);
const healthTimeout = parseInt(process.env.MANIFEST_VERIFY_TIMEOUT || '60', 10);

function run(cmd) {
@@ -88,7 +96,7 @@
console.log(`Waiting for app at ${healthUrl} (timeout: ${healthTimeout}s)...`);
while (true) {
try {
const output = runCapture(`curl -s -o /dev/null -w "%{http_code}" "${healthUrl}"`);
const output = execFileSync('curl', ['-s', '-o', '/dev/null', '-w', '%{http_code}', healthUrl], { cwd: projectDir, encoding: 'utf-8' });
const status = output.trim();
console.log(' Health check: status=%s', status);
// Any HTTP response (not connection refused) means the app is up.
Copilot is powered by AI and may make mistakes. Always verify output.
}

function assert(condition, message) {
if (!condition) {
console.error(`FAIL: ${message}`);
process.exit(1);
}
console.log(`PASS: ${message}`);
}

console.log('=== Manifest E2E Verification ===');
console.log('Project: %s', projectDir);
console.log('Env: %s', env);

// Step 1: Clean any pre-existing manifest
if (existsSync(manifestPath)) {
rmSync(manifestPath);
console.log('Cleaned pre-existing manifest');
}

// Step 2: Generate manifest
console.log('\n--- Step 1: Generate manifest ---');
run(`npx egg-bin manifest generate --env=${env}`);

// Step 3: Verify manifest file exists and has valid structure
console.log('\n--- Step 2: Verify manifest structure ---');
assert(existsSync(manifestPath), '.egg/manifest.json exists after generate');

const manifest = JSON.parse(readFileSync(manifestPath, 'utf-8'));
assert(manifest.version === 1, 'manifest version is 1');
assert(typeof manifest.generatedAt === 'string' && manifest.generatedAt.length > 0, 'manifest has generatedAt');
assert(typeof manifest.invalidation === 'object', 'manifest has invalidation');
assert(manifest.invalidation.serverEnv === env, `manifest serverEnv matches "${env}"`);
assert(typeof manifest.resolveCache === 'object', 'manifest has resolveCache');
assert(typeof manifest.fileDiscovery === 'object', 'manifest has fileDiscovery');
assert(typeof manifest.extensions === 'object', 'manifest has extensions');

const resolveCacheCount = Object.keys(manifest.resolveCache).length;
const fileDiscoveryCount = Object.keys(manifest.fileDiscovery).length;
console.log(' resolveCache: %d entries', resolveCacheCount);
console.log(' fileDiscovery: %d entries', fileDiscoveryCount);

// Step 4: Validate manifest via CLI
console.log('\n--- Step 3: Validate manifest via CLI ---');
run(`npx egg-bin manifest validate --env=${env}`);

// Step 5: Boot the app with manifest and verify it starts correctly
console.log('\n--- Step 4: Boot app with manifest ---');
try {
run(`npx eggctl start --port=${healthPort} --env=${env} --daemon`);
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.

high

Using eggctl start --daemon followed by a generic eggctl stop might not reliably stop the specific process started by this script, especially if multiple eggctl instances are running. This could lead to orphaned processes or interfere with subsequent test runs. Consider capturing the PID of the daemonized process and using it for a targeted kill command or ensuring eggctl stop has a mechanism to stop the specific instance started by the script.


const healthUrl = `http://127.0.0.1:${healthPort}/`;
const startTime = Date.now();
let ready = false;

console.log(`Waiting for app at ${healthUrl} (timeout: ${healthTimeout}s)...`);
while (true) {
try {
const output = runCapture(`curl -s -o /dev/null -w "%{http_code}" "${healthUrl}"`);
const status = output.trim();
console.log(' Health check: status=%s', status);
// Any HTTP response (not connection refused) means the app is up.
// Not all apps have a route on `/`, so we accept any status code.
if (status !== '000') {
ready = true;
break;
}
} catch {
console.log(' Health check: connection refused, retrying...');
}
Comment on lines +100 to +102
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 broad catch block (catch { ... }) for the curl command silently ignores any error that might occur during the health check, not just connection refused. This can hide other issues that prevent the app from starting correctly. It's better to log the error object (err) to provide more context for debugging.

Suggested change
} catch {
console.log(' Health check: connection refused, retrying...');
}
} catch (err) {
console.log(' Health check: connection refused, retrying... Error: %s', err.message);
}


const elapsed = (Date.now() - startTime) / 1000;
if (elapsed >= healthTimeout) {
console.log(' Health check timed out after %ds', elapsed);
break;
}

await sleep(2000);
}
Comment on lines +107 to +111
Copy link

Copilot AI Mar 30, 2026

Choose a reason for hiding this comment

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

The retry loop uses execSync('sleep 2'), which relies on an external shell command and will fail on Windows/non-POSIX environments. Consider replacing this with an in-process delay (e.g., await new Promise(r => setTimeout(r, 2000))) so the script is portable and doesn’t depend on sleep being available.

Copilot uses AI. Check for mistakes.

run(`npx eggctl stop`);
assert(ready, 'App booted successfully with manifest');
} catch (err) {
// Try to stop if started
try {
run(`npx eggctl stop`);
} catch {
/* ignore */
}
Comment on lines +119 to +121
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 catch block for eggctl stop during error handling is too broad and silently ignores any error. If eggctl stop fails for a reason other than the app not being started, this error will be hidden, making debugging difficult. Consider logging the error object (err) to provide more context.

Suggested change
} catch {
/* ignore */
}
} catch (err) {
console.error('Failed to stop eggctl during cleanup:', err.message);
}

console.error('Boot test failed:', err.message);
process.exit(1);
}

// Step 6: Clean manifest
console.log('\n--- Step 5: Clean manifest ---');
run(`npx egg-bin manifest clean`);
assert(!existsSync(manifestPath), '.egg/manifest.json removed after clean');

console.log('\n=== All manifest E2E checks passed ===');
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.

2 changes: 2 additions & 0 deletions site/.vitepress/config.mts
Original file line number Diff line number Diff line change
Expand Up @@ -312,6 +312,7 @@ function sidebarCore(): DefaultTheme.SidebarItem[] {
{ text: 'Internationalization', link: 'i18n' },
{ text: 'View Template', link: 'view' },
{ text: 'Security', link: 'security' },
{ text: 'Startup Manifest', link: 'manifest' },
],
},
];
Expand Down Expand Up @@ -422,6 +423,7 @@ function sidebarCoreZhCN(): DefaultTheme.SidebarItem[] {
{ text: '国际化', link: 'i18n' },
{ text: '模板渲染', link: 'view' },
{ text: '安全', link: 'security' },
{ text: '启动清单', link: 'manifest' },
],
},
];
Expand Down
141 changes: 141 additions & 0 deletions site/docs/core/manifest.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
# Startup Manifest

Egg provides a startup manifest mechanism that caches file discovery and module resolution results to accelerate application cold starts.

## How It Works

Every time an application starts, the framework performs extensive filesystem operations:

- **Module resolution**: Hundreds of `fs.existsSync` calls probing `.ts`, `.js`, `.mjs` extensions
- **File discovery**: Multiple `globby.sync` scans across plugin, config, and extension directories
- **tegg module scanning**: Traversing module directories and `import()`-ing decorator files to collect metadata

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.

## Performance Improvement

Measured on cnpmcore in a container cold-start scenario (no filesystem page cache):

| Metric | No Manifest | With Manifest | Improvement |
| ----------- | ----------- | ------------- | ----------- |
| App Start | ~980ms | ~780ms | **~20%** |
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.

优化后也只降了200ms嘛,剩下还有哪些优化方向呢

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

看下 snapshot 那个 wip 的 PR。

| Load Files | ~660ms | ~490ms | **~26%** |
| Load app.js | ~280ms | ~150ms | **~46%** |

> 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.
## Usage

### CLI Management (Recommended)

`egg-bin` provides a `manifest` command to manage the startup manifest:

#### Generate

```bash
# Generate for production
$ egg-bin manifest generate --env=prod

# Specify environment and scope
$ egg-bin manifest generate --env=prod --scope=aliyun

# Specify framework
$ egg-bin manifest generate --env=prod --framework=yadan
```

The generation process boots the app in `metadataOnly` mode (skipping lifecycle hooks, only collecting metadata), then writes the results to `.egg/manifest.json`.

#### Validate

```bash
$ egg-bin manifest validate --env=prod
```

Example output:

```
[manifest] Manifest is valid
[manifest] version: 1
[manifest] generatedAt: 2026-03-29T12:13:18.039Z
[manifest] serverEnv: prod
[manifest] serverScope:
[manifest] resolveCache entries: 416
[manifest] fileDiscovery entries: 31
[manifest] extension entries: 1
```
Comment on lines +56 to +65
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.

⚠️ Potential issue | 🟡 Minor

Add language specifier to fenced code block.

The example output block is missing a language specifier, which triggers a markdown linting warning. Consider adding text or leaving it empty:

-```
+```text
 [manifest] Manifest is valid
🧰 Tools
🪛 markdownlint-cli2 (0.22.0)

[warning] 56-56: Fenced code blocks should have a language specified

(MD040, fenced-code-language)

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@site/docs/core/manifest.md` around lines 56 - 65, The fenced code block
showing the manifest output lacks a language specifier which triggers markdown
linting; update the triple-backtick fence around the manifest example (the
fenced code block containing "[manifest] Manifest is valid" etc.) to include a
language tag such as text (e.g., ```text) so the block is explicitly marked and
the linter warning is resolved.


If the manifest is invalid or missing, the command exits with a non-zero code.

#### Clean

```bash
$ egg-bin manifest clean
```

### Automatic Generation

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.

## Invalidation

The manifest includes fingerprint data and is automatically invalidated when:

- **Lockfile changes**: `pnpm-lock.yaml`, `package-lock.json`, or `yarn.lock` mtime or size changes
- **Config directory changes**: Files in `config/` change (MD5 fingerprint)
- **Environment mismatch**: `serverEnv` or `serverScope` differs from the manifest
- **TypeScript state change**: `EGG_TYPESCRIPT` enabled/disabled state changes
- **Version mismatch**: Manifest format version differs

When the manifest is invalid, the framework falls back to normal file discovery — startup is never blocked.

## Environment Variables

| Variable | Description | Default |
| -------------- | ---------------------------- | ----------------------------------------------------- |
| `EGG_MANIFEST` | Enable manifest in local env | `false` (manifest not loaded in local env by default) |

> Local development (`serverEnv=local`) does not load the manifest by default, since files change frequently. Set `EGG_MANIFEST=true` to force-enable.
## Deployment Recommendations

### Container Deployment

Generate the manifest in your Dockerfile after building:

```dockerfile
# Install dependencies and build
RUN npm install --production
RUN npm run build

# Generate startup manifest
RUN npx egg-bin manifest generate --env=prod

# Start the app (manifest is used automatically)
CMD ["npm", "start"]
```

### CI/CD Pipelines

Generate the manifest during the build stage and deploy it with the artifact:

```bash
# Build
npm run build

# Generate manifest
npx egg-bin manifest generate --env=prod

# Validate manifest
npx egg-bin manifest validate --env=prod

# Package (includes .egg/manifest.json)
tar -zcvf release.tgz .
```

## Important Notes

1. **Environment-bound**: The manifest is bound to `serverEnv` and `serverScope` (deployment type, e.g. `aliyun`). Different environments or deployment types require separate manifests.
2. **Regenerate after dependency changes**: Installing or updating dependencies changes the lockfile, which automatically invalidates the manifest.
3. **`.egg` directory**: The manifest is stored at `.egg/manifest.json`. Consider adding `.egg/` to `.gitignore`.
4. **Safe fallback**: A missing or invalid manifest causes the framework to fall back to normal discovery — startup is never broken.
5. **metadataOnly mode**: `manifest generate` does not run the full application lifecycle — no database connections or external services are started.
Loading
Loading