Skip to content

feat(scripts): salvage single-mode, IPC readiness, framework resolver and esbuild plugins#5862

Open
killagu wants to merge 1 commit intonextfrom
refactor/remove-snapshot-build
Open

feat(scripts): salvage single-mode, IPC readiness, framework resolver and esbuild plugins#5862
killagu wants to merge 1 commit intonextfrom
refactor/remove-snapshot-build

Conversation

@killagu
Copy link
Copy Markdown
Contributor

@killagu killagu commented Apr 11, 2026

Summary

Salvage the independent-value infrastructure from the abandoned `feat/snapshot-scripts` branch:

  • `--single` mode start — `start-single.mjs/cjs` launchers with `--import=tsx/esm` for TypeScript framework sources
  • IPC-based `waitForReady` — replaces 1s polling `checkStatus` with an `egg-ready` IPC message listener, eliminating the busy-wait
  • `egg-server` process detection — `stop.ts` and `helper.ts` match `egg-server-*` process titles for single-mode and any future bundle/snapshot title-based processes
  • `resolveFrameworkEntry` helper — reads `package.json` `exports['.']`/`main` recursively, supports worktree, installed CJS, and ESM conditional-exports layouts
  • `esmPolyfillPlugin` — esbuild plugin that rewrites `import.meta.{dirname,url,filename}` and top-level `__dirname` in ESM sources, with opt-out guard for files that locally declare `__dirname`
  • `fileUrlResolverPlugin` — esbuild plugin that resolves `file://` URL imports to absolute paths
  • `tsx` dependency — needed by single-mode to load framework TypeScript sources at runtime

Context

The `feat/snapshot-scripts` branch pursued a `node --build-snapshot` based approach that was abandoned after multiple POCs revealed architectural dead-ends:

  1. `buildSnapshot()` POC — proved the API cannot work under `--build-snapshot` due to async hook stack corruption from libuv timer queue (removed in refactor(egg): remove buildSnapshot/restoreSnapshot that never worked under --build-snapshot #5861)
  2. Module-cache bundle POC — showed `class extends` declaration-time constraints that lazy proxies cannot work around (egg's own `HttpClient extends RawHttpClient` via urllib/undici WebAssembly)
  3. `NODE_COMPILE_CACHE` POC — ~0% speedup for egg. CPU profile showed parse+compile is only 17% of startup; egg is I/O (32%) and top-level module execution (43%) heavy, not parse-bound
  4. Runtime bundle POC69% module load time reduction (`import('egg')` 353ms → 110ms), ~55% projected full `startEgg` savings. This is the new primary track (separate PR, in progress)

The pieces in this PR have value independent of any snapshot/bundle strategy:

  • `--single` mode is useful on its own
  • IPC readiness is a bugfix (eliminates 1s polling sleep)
  • `resolveFrameworkEntry` is a generic helper
  • The esbuild plugins are reusable by the upcoming runtime bundle work

Dropped (not in this PR)

  • `snapshot-build.ts` command
  • `http`/`http2`/`urllib`/`undici` defer plugins (all `--build-snapshot` workarounds)
  • `optional-deps-stub`, `lazy-require-proxy`
  • `--snapshot` / `--snapshot-blob` flags

Test plan

  • `pnpm --filter=@eggjs/scripts run typecheck`
  • `pnpm --filter=@eggjs/scripts run test` — 31 active passed (15 esm-polyfill + 7 resolve-framework-entry + 8 stop + 1 egg-scripts)
  • `pnpm -w run build`

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Added --single flag to start apps in single-process mode.
    • Improved startup readiness detection via IPC messaging.
    • Exposed three new package entrypoints for ESM tooling and framework resolution.
  • Bug Fixes

    • Broader process detection for stop/cleanup flows to better identify running instances.
  • Chores

    • Updated build/config and runtime tooling to support single-process startup and ESM workflows.

Copilot AI review requested due to automatic review settings April 11, 2026 04:18
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Apr 11, 2026

📝 Walkthrough

Walkthrough

Adds single-process startup support and ESM/tooling utilities to tools/scripts: new start-single scripts (CJS/ESM), CLI flag and readiness IPC, framework entry resolution, ESM polyfill and file-url resolver esbuild plugins, tests, and package export/runtime updates.

Changes

Cohort / File(s) Summary
Package & build config
tools/scripts/package.json, tools/scripts/tsdown.config.ts
Added new exports (./esm-polyfill, ./file-url-resolver, ./resolve-framework-entry) and runtime deps (esbuild, tsx via catalog); added tsdown config with externals and unused diagnostics.
Single-process startup scripts
tools/scripts/scripts/start-single.cjs, tools/scripts/scripts/start-single.mjs
New CJS/ESM entrypoints that parse options, load framework, invoke start/startEgg, create HTTP server, send egg-ready IPC, and implement graceful shutdown with timeouts.
CLI start command
tools/scripts/src/commands/start.ts
Added --single flag, altered server binary selection and execArgv for TS loading in single mode, replaced polling readiness with waitForReady() that awaits egg-ready IPC and handles stderr tailing on failure.
Framework entry resolution
tools/scripts/src/resolve-framework-entry.ts, tools/scripts/test/resolve-framework-entry.test.ts
New resolveFrameworkEntry(frameworkPath) implementing prioritized resolution (worktree src → exports → main → index.js) and returning registry keys; comprehensive Vitest coverage for resolution scenarios and error cases.
ESM/tooling plugins & tests
tools/scripts/src/esm-polyfill.ts, tools/scripts/src/file-url-resolver.ts, tools/scripts/test/esm-polyfill.test.ts
Added ESM polyfill transformer and esbuild plugin plus a file:// resolver plugin; tests validate detection, substitutions, and opt-out semantics.
Process management & helpers
tools/scripts/src/commands/stop.ts, tools/scripts/src/helper.ts, tools/scripts/test/utils.ts
Expanded process matching to include start-single and egg-server, updated line filtering and cleanup signal selection for single-mode processes.

Sequence Diagram(s)

sequenceDiagram
  participant CLI as CLI (start command)
  participant Launcher as Launcher (start-single script)
  participant Framework as Framework module
  participant Server as HTTP Server
  participant Parent as Parent process (daemon manager)

  CLI->>Launcher: spawn with JSON options (port, baseDir, framework,...)
  Launcher->>Framework: dynamic import and resolve start/startEgg
  Framework-->>Launcher: returns app (with callback(), close())
  Launcher->>Server: create server from app.callback() and listen
  Server-->>Launcher: 'listening' (address, port)
  Launcher->>Parent: process.send({ type: "egg-ready", address, port })
  Parent-->>CLI: receives readiness
  Note over Launcher,Framework: On SIGTERM/SIGINT/SIGQUIT\nclose server -> app.close() -> exit
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Suggested reviewers

  • fengmk2
  • jerryliang64
  • gxkl

Poem

🐰 I hopped through scripts, both new and old,
Brought single-mode eggs out from the fold,
I bounced on imports, resolved each path,
Sent a tiny “egg-ready” note with a laugh,
Now one soft process wakes — behold! 🥚✨

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 40.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely summarizes the three main changes: adding single-mode startup capability, introducing IPC-based readiness signaling, and implementing framework entry point resolution.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch refactor/remove-snapshot-build

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@cloudflare-workers-and-pages
Copy link
Copy Markdown

cloudflare-workers-and-pages bot commented Apr 11, 2026

Deploying egg-v3 with  Cloudflare Pages  Cloudflare Pages

Latest commit: 959e604
Status: ✅  Deploy successful!
Preview URL: https://20c69538.egg-v3.pages.dev
Branch Preview URL: https://refactor-remove-snapshot-bui.egg-v3.pages.dev

View logs

Copy link
Copy Markdown
Contributor

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

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

Code Review

This pull request introduces a single-process mode for starting applications, complementing the existing cluster mode. Key changes include the addition of start-single scripts (CJS and ESM), a new --single flag for the start command, and a resolveFrameworkEntry utility to handle various framework layouts. The startup synchronization logic was refactored from polling to an IPC-based waitForReady mechanism. Review feedback suggests ensuring the ESM start script supports default exports for consistency, improving child process error and exit handling during startup, and using the local child reference instead of the class member for better reliability.

Comment on lines +12 to +16
const framework = await importModule(options.framework);
const startEgg = framework.start ?? framework.startEgg;
if (typeof startEgg !== 'function') {
throw new Error(`Cannot find start/startEgg function from framework: ${options.framework}`);
}
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

The ESM version of the start script should also check for the .default export to be consistent with the CJS version and to support frameworks that use default exports.

  const framework = await importModule(options.framework);
  let startEgg = framework.start ?? framework.startEgg;
  if (typeof startEgg !== 'function') {
    startEgg = framework.default?.start ?? framework.default?.startEgg;
  }
  if (typeof startEgg !== 'function') {
    throw new Error("Cannot find start/startEgg function from framework: " + options.framework);
  }

Comment on lines +332 to +337
child.on('exit', (code) => {
if (code) {
clearTimeout(timer);
reject(new Error(`Child process exited with code ${code}`));
}
});
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 waitForReady function should handle the error event on the child process and should also reject if the process exits (even with code 0) before the egg-ready message is received, as any exit during startup is considered a failure.

        child.on('exit', (code, signal) => {
          clearTimeout(timer);
          const message = signal ? "Child process killed by signal " + signal : "Child process exited with code " + code;
          reject(new Error(message));
        });

        child.on('error', (err) => {
          clearTimeout(timer);
          reject(err);
        });

}

if (!isSuccess) {
this.#child.kill('SIGTERM');
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

Use the child parameter passed to the function instead of the private member this.#child for better consistency and to avoid potential issues if the member is reassigned.

Suggested change
this.#child.kill('SIGTERM');
child.kill('SIGTERM');

@codecov
Copy link
Copy Markdown

codecov bot commented Apr 11, 2026

Codecov Report

❌ Patch coverage is 78.72340% with 10 lines in your changes missing coverage. Please review.
✅ Project coverage is 85.97%. Comparing base (09222b7) to head (959e604).

Files with missing lines Patch % Lines
tools/scripts/src/esm-polyfill.ts 47.36% 8 Missing and 2 partials ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             next    #5862      +/-   ##
==========================================
- Coverage   86.00%   85.97%   -0.04%     
==========================================
  Files         667      669       +2     
  Lines       18945    18991      +46     
  Branches     3652     3667      +15     
==========================================
+ Hits        16294    16327      +33     
- Misses       2297     2308      +11     
- Partials      354      356       +2     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This PR enhances @eggjs/scripts production lifecycle tooling by adding a single-process start path, improving startup readiness detection via IPC, broadening process detection for stop/cleanup flows, and introducing a framework entry resolver helper.

Changes:

  • Add --single start mode with new start-single.{mjs,cjs} launchers and tsx runtime loading for TypeScript framework sources.
  • Replace polling-based startup readiness with an IPC egg-ready listener (waitForReady).
  • Add resolveFrameworkEntry() helper with tests; update process matching to recognize egg-server* and start-single.

Reviewed changes

Copilot reviewed 13 out of 13 changed files in this pull request and generated 4 comments.

Show a summary per file
File Description
tools/scripts/tsdown.config.ts Adds tsdown build config and marks tsx as an unused-exports ignore due to runtime usage.
tools/scripts/test/utils.ts Updates test cleanup logic to recognize and terminate start-single processes.
tools/scripts/test/resolve-framework-entry.test.ts Adds unit tests for framework entry resolution across layouts/exports shapes.
tools/scripts/src/resolve-framework-entry.ts Implements recursive package.json exports/main entry resolution with worktree preference.
tools/scripts/src/helper.ts Broadens process scanning to include egg-server-titled processes.
tools/scripts/src/commands/stop.ts Expands stop-process detection to include start-single and egg-server* titles.
tools/scripts/src/commands/start.ts Adds --single, injects --import=tsx/esm for single mode, and switches readiness to IPC-based waitForReady.
tools/scripts/scripts/start-single.mjs New ESM single-mode server launcher that emits egg-ready over IPC.
tools/scripts/scripts/start-single.cjs New CJS single-mode server launcher that emits egg-ready over IPC.
tools/scripts/package.json Exposes resolve-framework-entry export and adds tsx as a runtime dependency.

Comment on lines +316 to +337
const timer = setTimeout(() => {
reject(new Error(`Start failed, ${timeoutMs / 1000}s timeout`));
}, timeoutMs);

child.on('message', (msg: any) => {
// https://github.com/eggjs/cluster/blob/master/src/master.ts#L119
if (msg && msg.action === 'egg-ready') {
clearTimeout(timer);
this.isReady = true;
this.log('%s started on %s', readyLabel, msg.data.address);
child.unref();
child.disconnect();
resolve();
}
});

child.on('exit', (code) => {
if (code) {
clearTimeout(timer);
reject(new Error(`Child process exited with code ${code}`));
}
});
Copy link

Copilot AI Apr 11, 2026

Choose a reason for hiding this comment

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

In waitForReady(), the child 'exit' handler only rejects when code is truthy. If the child exits before sending egg-ready with code === 0 (or exits via a signal where code is null), the promise will wait until timeout and report a misleading timeout error. Consider rejecting on any exit-before-ready (capturing both code and signal) and clearing listeners/timer accordingly.

Suggested change
const timer = setTimeout(() => {
reject(new Error(`Start failed, ${timeoutMs / 1000}s timeout`));
}, timeoutMs);
child.on('message', (msg: any) => {
// https://github.com/eggjs/cluster/blob/master/src/master.ts#L119
if (msg && msg.action === 'egg-ready') {
clearTimeout(timer);
this.isReady = true;
this.log('%s started on %s', readyLabel, msg.data.address);
child.unref();
child.disconnect();
resolve();
}
});
child.on('exit', (code) => {
if (code) {
clearTimeout(timer);
reject(new Error(`Child process exited with code ${code}`));
}
});
let settled = false;
const cleanup = () => {
clearTimeout(timer);
child.off('message', onMessage);
child.off('exit', onExit);
};
const finishResolve = () => {
if (settled) return;
settled = true;
cleanup();
resolve();
};
const finishReject = (err: Error) => {
if (settled) return;
settled = true;
cleanup();
reject(err);
};
const timer = setTimeout(() => {
finishReject(new Error(`Start failed, ${timeoutMs / 1000}s timeout`));
}, timeoutMs);
const onMessage = (msg: any) => {
// https://github.com/eggjs/cluster/blob/master/src/master.ts#L119
if (msg && msg.action === 'egg-ready') {
this.isReady = true;
this.log('%s started on %s', readyLabel, msg.data.address);
child.unref();
child.disconnect();
finishResolve();
}
};
const onExit = (code: number | null, signal: NodeJS.Signals | null) => {
const reason = signal ? `signal ${signal}` : `code ${code}`;
finishReject(new Error(`Child process exited before ready with ${reason}`));
};
child.on('message', onMessage);
child.on('exit', onExit);

Copilot uses AI. Check for mistakes.
Comment on lines 84 to +189
@@ -104,8 +108,9 @@ export default class Start<T extends typeof Start> extends BaseCommand<T> {
return name;
}

protected async getServerBin(): Promise<string> {
const serverBinName = this.isESM ? 'start-cluster.mjs' : 'start-cluster.cjs';
protected async getServerBin(single?: boolean): Promise<string> {
const prefix = single ? 'start-single' : 'start-cluster';
const serverBinName = this.isESM ? `${prefix}.mjs` : `${prefix}.cjs`;
return path.join(import.meta.dirname, '../../scripts', serverBinName);
}

@@ -145,8 +150,12 @@ export default class Start<T extends typeof Start> extends BaseCommand<T> {
// normalize env
this.env.HOME = HOME;
this.env.NODE_ENV = 'production';
// disable ts file loader
this.env.EGG_TS_ENABLE = 'false';
// Disable ts file loader in cluster mode.
// In single mode, Node.js 22.18+ native type stripping handles .ts files.
const isSingle = flags.single;
if (!isSingle) {
this.env.EGG_TS_ENABLE = 'false';
}

// it makes env big but more robust
this.env.PATH = this.env.Path = [
@@ -173,6 +182,11 @@ export default class Start<T extends typeof Start> extends BaseCommand<T> {

// additional execArgv
const execArgv: string[] = ['--no-deprecation', '--trace-warnings'];
// Single mode loads framework .ts source directly; use tsx for full TypeScript
// support including decorators (Node.js native type stripping can't handle them).
if (isSingle) {
execArgv.push('--import=tsx/esm');
}
Copy link

Copilot AI Apr 11, 2026

Choose a reason for hiding this comment

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

New --single mode (including tsx-based execArgv and different env/flag normalization) isn’t covered by the existing Vitest suite. Please add tests that start in single mode (daemon + non-daemon if supported) and verify readiness (egg-ready), stop behavior, and log output so regressions are caught.

Copilot uses AI. Check for mistakes.
const matched = flags.title
? cmd.includes('start-cluster') && cmd.includes(format(osRelated.titleTemplate, flags.title))
: cmd.includes('start-cluster');
? isEggProcess && cmd.includes(format(osRelated.titleTemplate, flags.title))
Copy link

Copilot AI Apr 11, 2026

Choose a reason for hiding this comment

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

stop --title currently requires the command line to include the serialized JSON "title":"...". For processes detected via cmd.startsWith('egg-server') (process-title based), that JSON substring may not exist, so --title won’t match even though the process title does. Consider also matching cmd.startsWith(flags.title) (or an equivalent title-prefix match) when cmd.startsWith('egg-server') is used.

Suggested change
? isEggProcess && cmd.includes(format(osRelated.titleTemplate, flags.title))
? isEggProcess &&
(cmd.includes(format(osRelated.titleTemplate, flags.title)) || cmd.startsWith(flags.title))

Copilot uses AI. Check for mistakes.

// Installed layout: read package.json
const pkgPath = path.join(frameworkPath, 'package.json');
const pkg = JSON.parse(readFileSync(pkgPath, 'utf-8'));
Copy link

Copilot AI Apr 11, 2026

Choose a reason for hiding this comment

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

resolveFrameworkEntry() reads/parses package.json without guarding for missing/invalid files, so callers will get low-level ENOENT/JSON parse errors rather than a consistent, actionable message like the later "Framework entry not found" error. Consider wrapping the package.json read/parse in a try/catch and throwing an Error that includes frameworkPath and pkgPath with next-step guidance.

Suggested change
const pkg = JSON.parse(readFileSync(pkgPath, 'utf-8'));
let pkg: Record<string, any>;
try {
pkg = JSON.parse(readFileSync(pkgPath, 'utf-8'));
} catch (error) {
const reason = error instanceof Error ? error.message : String(error);
throw new Error(
`Failed to read framework package metadata for ${frameworkPath} from ${pkgPath}: ${reason}. ` +
'Ensure the framework package exists, is installed or built, and has a valid package.json with usable "main" or "exports" fields.',
);
}

Copilot uses AI. Check for mistakes.
@cloudflare-workers-and-pages
Copy link
Copy Markdown

cloudflare-workers-and-pages bot commented Apr 11, 2026

Deploying egg with  Cloudflare Pages  Cloudflare Pages

Latest commit: 959e604
Status: ✅  Deploy successful!
Preview URL: https://d756f730.egg-cci.pages.dev
Branch Preview URL: https://refactor-remove-snapshot-bui.egg-cci.pages.dev

View logs

… and esbuild plugins

Independent-value infrastructure salvaged from the abandoned feat/snapshot-scripts branch:

- --single mode start + start-single.mjs/cjs launchers with --import=tsx/esm
- IPC-based waitForReady replacing 1s polling checkStatus
- egg-server process title matching in stop.ts / helper.ts for single-mode
  and any future bundle/snapshot title-based processes
- resolveFrameworkEntry generic helper (reads package.json exports[.]/main)
  supporting worktree, installed CJS, and ESM conditional-exports layouts
- esmPolyfillPlugin (import.meta.{dirname,url,filename} + __dirname) and
  fileUrlResolverPlugin esbuild plugins, exported for reuse

Context: --build-snapshot based approach abandoned after multiple POCs
revealed architectural dead-ends. These pieces have value independent of
any snapshot strategy and are salvaged into a standalone PR.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@killagu killagu force-pushed the refactor/remove-snapshot-build branch from 509c449 to 959e604 Compare April 11, 2026 04:23
@killagu killagu changed the title feat(scripts): add single-mode start, IPC readiness, and framework entry resolution feat(scripts): salvage single-mode, IPC readiness, framework resolver and esbuild plugins Apr 11, 2026
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 4

🧹 Nitpick comments (4)
tools/scripts/tsdown.config.ts (1)

3-12: Use a typed intermediate config object before export.

Please align this config with the repo’s typed-config convention.

Suggested refactor
 import { defineConfig } from 'tsdown';
 
-export default defineConfig({
+const config = {
   entry: 'src/**/*.ts',
   unbundle: true,
   external: [/^@eggjs\//, 'egg'],
   unused: {
     level: 'error',
     // tsx is used at runtime via --import=tsx/esm in spawned processes, not directly imported
     ignore: ['tsx', 'runscript'],
   },
-});
+} satisfies Parameters<typeof defineConfig>[0];
+
+export default defineConfig(config);

Based on learnings: Applies to {tsdown.config.ts,vitest.config.ts} : Use typed intermediate variables with appropriate types (UserConfig, UserWorkspaceConfig) for tsdown.config.ts and vitest.config.ts configuration files.

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

In `@tools/scripts/tsdown.config.ts` around lines 3 - 12, The exported default
should be a typed intermediate config object instead of an inline literal;
create a const (e.g., const config: UserConfig) with the same properties (entry,
unbundle, external, unused) and then export default defineConfig(config). Update
imports/types if needed so UserConfig (or the correct tsdown config type) is
referenced, and keep defineConfig(config) to preserve existing behavior; apply
the same pattern to related files like vitest.config.ts.
tools/scripts/test/resolve-framework-entry.test.ts (1)

10-123: Please add orchestration regressions for both CJS and TS example apps.

Given this PR changes startup/readiness/stop behavior, add integration tests that exercise start --single + egg-ready + stop in both CommonJS and TypeScript fixtures.

Based on learnings: Add regression cases that exercise both CommonJS and TypeScript example apps when features affect HTTP or process orchestration.

tools/scripts/src/commands/start.ts (2)

185-189: Please cover start --single with real-app regressions.

This path now depends on both tsx/esm loading and the egg-ready IPC handshake. The unit coverage mentioned in the PR is useful, but it will not catch the integration breakage this change can introduce; please boot one CommonJS example and one TypeScript example through the new single-process path, ideally in daemon mode too.

Based on learnings: Add regression cases that exercise both CommonJS and TypeScript example apps when features affect HTTP or process orchestration

Also applies to: 288-289

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

In `@tools/scripts/src/commands/start.ts` around lines 185 - 189, Add integration
regression tests that exercise the new single-process path triggered by isSingle
(where execArgv.push('--import=tsx/esm') is applied) and the egg-ready IPC
handshake: create two end-to-end test cases that launch the CLI command used in
start.ts with --single (and also run the same tests in daemon mode) — one
booting a CommonJS example app and one booting a TypeScript example app — and
assert that the app performs basic HTTP behavior and completes the egg-ready
handshake (process lifecycle/IPC) successfully; place these tests alongside
existing CLI/integration tests and reuse the same start/stop helpers so they run
in CI and catch regressions affecting process orchestration or HTTP endpoints.

320-325: Define a type for the IPC message payload instead of using any.

Line 320 uses msg: any for the egg-ready IPC handler, removing compile-time type safety. The senders (start-single.mjs, start-single.cjs, mock-egg/index.ts) consistently use { action: 'egg-ready', data: { address, port } }. A shared type would catch sender/receiver contract drift at compile time.

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

In `@tools/scripts/src/commands/start.ts` around lines 320 - 325, Introduce a
concrete IPC message type for the egg-ready payload (e.g., interface
EggReadyMessage { action: 'egg-ready'; data: { address: string; port: number }
}) and use it instead of any in the child.on('message', ...) handler so
TypeScript enforces the sender/receiver contract; add the type to a shared
module/exports used by the senders
(start-single.mjs/start-single.cjs/mock-egg/index.ts) and update the handler
signature from (msg: any) to (msg: EggReadyMessage | unknown) with a type guard
that narrows to EggReadyMessage before accessing msg.action and msg.data.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@tools/scripts/scripts/start-single.mjs`:
- Around line 13-16: The ESM launcher currently only checks framework.start ??
framework.startEgg and can fail for CJS-wrapped imports; update the logic that
assigns startEgg (and similarly start if used) to also fall back to
framework.default?.start and framework.default?.startEgg when
framework.start/startEgg are missing, so resolve startEgg from framework.start
?? framework.startEgg ?? framework.default?.start ?? framework.default?.startEgg
and then validate typeof startEgg === 'function' before throwing the error.

In `@tools/scripts/src/commands/start.ts`:
- Around line 351-366: The current block returns early when
this.flags['ignore-stderr'] is true, which turns a failed readiness into a
successful exit; instead, do not return success—either throw an Error or call
the command failure helper so the process exits non-zero. Locate the block that
checks hasStderrContent and stderrFile, which uses execFile and logToStderr, and
replace the final "return" under this.flags['ignore-stderr'] with logic that
logs a warning but then fails the command (e.g. throw new Error('startup failed:
non-empty stderr') or call the command's error/exit helper) so startup failures
remain failures even when --ignore-stderr is set.

In `@tools/scripts/src/commands/stop.ts`:
- Around line 50-54: The current filtering logic for matched requires the
JSON-formatted title even when the process command is a raw egg-server-* string;
update the matched expression so when flags.title is set it accepts either the
formatted title or raw egg-server variants. Concretely, in the matched
assignment (using isEggProcess, cmd, flags.title, format and
osRelated.titleTemplate) change the ternary branch to check isEggProcess &&
(cmd.includes(format(osRelated.titleTemplate, flags.title)) ||
cmd.startsWith(`egg-server-${flags.title}`) ||
cmd.includes(`egg-server-${flags.title}`) ), so raw egg-server-* process-title
commands with the provided title are also matched.

In `@tools/scripts/test/utils.ts`:
- Line 39: The process.kill call uses an invalid empty string signal which
throws; update the call that references process.kill(pid, type === 'master' ||
type === 'single' ? '' : 'SIGKILL') to use an explicit graceful signal (e.g.
'SIGTERM') for the master/single branch so it becomes process.kill(pid, type ===
'master' || type === 'single' ? 'SIGTERM' : 'SIGKILL'); keep the existing
try/catch behavior around this call so errors are still handled but no longer
triggered by an invalid signal, and ensure the variables pid and type referenced
in this change are the same ones used in the surrounding function.

---

Nitpick comments:
In `@tools/scripts/src/commands/start.ts`:
- Around line 185-189: Add integration regression tests that exercise the new
single-process path triggered by isSingle (where
execArgv.push('--import=tsx/esm') is applied) and the egg-ready IPC handshake:
create two end-to-end test cases that launch the CLI command used in start.ts
with --single (and also run the same tests in daemon mode) — one booting a
CommonJS example app and one booting a TypeScript example app — and assert that
the app performs basic HTTP behavior and completes the egg-ready handshake
(process lifecycle/IPC) successfully; place these tests alongside existing
CLI/integration tests and reuse the same start/stop helpers so they run in CI
and catch regressions affecting process orchestration or HTTP endpoints.
- Around line 320-325: Introduce a concrete IPC message type for the egg-ready
payload (e.g., interface EggReadyMessage { action: 'egg-ready'; data: { address:
string; port: number } }) and use it instead of any in the child.on('message',
...) handler so TypeScript enforces the sender/receiver contract; add the type
to a shared module/exports used by the senders
(start-single.mjs/start-single.cjs/mock-egg/index.ts) and update the handler
signature from (msg: any) to (msg: EggReadyMessage | unknown) with a type guard
that narrows to EggReadyMessage before accessing msg.action and msg.data.

In `@tools/scripts/tsdown.config.ts`:
- Around line 3-12: The exported default should be a typed intermediate config
object instead of an inline literal; create a const (e.g., const config:
UserConfig) with the same properties (entry, unbundle, external, unused) and
then export default defineConfig(config). Update imports/types if needed so
UserConfig (or the correct tsdown config type) is referenced, and keep
defineConfig(config) to preserve existing behavior; apply the same pattern to
related files like vitest.config.ts.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: a28d1fbe-3a5b-440b-b231-980f5caf510f

📥 Commits

Reviewing files that changed from the base of the PR and between 09222b7 and 58156bf.

📒 Files selected for processing (10)
  • tools/scripts/package.json
  • tools/scripts/scripts/start-single.cjs
  • tools/scripts/scripts/start-single.mjs
  • tools/scripts/src/commands/start.ts
  • tools/scripts/src/commands/stop.ts
  • tools/scripts/src/helper.ts
  • tools/scripts/src/resolve-framework-entry.ts
  • tools/scripts/test/resolve-framework-entry.test.ts
  • tools/scripts/test/utils.ts
  • tools/scripts/tsdown.config.ts

Comment on lines +351 to +366
if (hasStderrContent) {
try {
const args = ['-n', '100', stderrFile];
this.logToStderr('tail %s', args.join(' '));
const { stdout: headStdout } = await execFile('head', args);
const { stdout: tailStdout } = await execFile('tail', args);
this.logToStderr('Got error when startup: ');
this.logToStderr(headStdout);
this.logToStderr('...');
this.logToStderr(tailStdout);
} catch (tailErr) {
this.logToStderr('ignore tail error: %s', tailErr);
}
if (this.flags['ignore-stderr']) {
return; // User opted to ignore stderr errors
}
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 | 🟠 Major

Don't let --ignore-stderr turn a startup failure into success.

By the time execution reaches this block, readiness has already failed. Returning on Line 365 makes start --daemon exit successfully whenever the stderr file is non-empty, even though no egg-ready message was received.

Suggested adjustment
-        if (this.flags['ignore-stderr']) {
-          return; // User opted to ignore stderr errors
-        }
-        this.logToStderr('Start got error, see %o', stderrFile);
-        this.logToStderr('Or use `--ignore-stderr` to ignore stderr at startup.');
+        if (!this.flags['ignore-stderr']) {
+          this.logToStderr('Start got error, see %o', stderrFile);
+          this.logToStderr('Or use `--ignore-stderr` to suppress stderr output at startup.');
+        }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tools/scripts/src/commands/start.ts` around lines 351 - 366, The current
block returns early when this.flags['ignore-stderr'] is true, which turns a
failed readiness into a successful exit; instead, do not return success—either
throw an Error or call the command failure helper so the process exits non-zero.
Locate the block that checks hasStderrContent and stderrFile, which uses
execFile and logToStderr, and replace the final "return" under
this.flags['ignore-stderr'] with logic that logs a warning but then fails the
command (e.g. throw new Error('startup failed: non-empty stderr') or call the
command's error/exit helper) so startup failures remain failures even when
--ignore-stderr is set.


try {
process.kill(pid, type === 'master' ? '' : 'SIGKILL');
process.kill(pid, type === 'master' || type === 'single' ? '' : 'SIGKILL');
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 | 🟠 Major

🧩 Analysis chain

🌐 Web query:

In Node.js, is an empty string ('') a valid signal for process.kill(pid, signal)? How does it behave compared with omitting signal or using 'SIGTERM'?

💡 Result:

No, an empty string ('') is not a valid signal for process.kill(pid, signal) in Node.js. It will throw a TypeError [ERR_UNKNOWN_SIGNAL]: Unknown signal: '' because Node.js validates the signal parameter as a string or number and checks it against known signal names (like 'SIGTERM', 'SIGINT') or the special case of 0. An empty string does not match any valid signal name. Omitting the signal (process.kill(pid)) or explicitly using 'SIGTERM' both send the default signal SIGTERM, which requests a graceful termination of the target process (though the process may handle it differently). They behave identically. Using '' differs by throwing an error before any signal is sent, preventing the operation entirely.

Citations:


🏁 Script executed:

cat -n tools/scripts/test/utils.ts | sed -n '30,50p'

Repository: eggjs/egg

Length of output: 811


Use an explicit signal in process.kill() on Line 39.

An empty string '' is not a valid signal in Node.js and will throw TypeError [ERR_UNKNOWN_SIGNAL]. When type === 'master' || type === 'single', the process kill always fails with an error. Although the error is caught by the try-catch block, this masks the actual failure and prevents proper cleanup. Use 'SIGTERM' (the default graceful termination signal) instead.

Suggested fix
-        process.kill(pid, type === 'master' || type === 'single' ? '' : 'SIGKILL');
+        process.kill(pid, type === 'master' || type === 'single' ? 'SIGTERM' : 'SIGKILL');
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
process.kill(pid, type === 'master' || type === 'single' ? '' : 'SIGKILL');
process.kill(pid, type === 'master' || type === 'single' ? 'SIGTERM' : 'SIGKILL');
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tools/scripts/test/utils.ts` at line 39, The process.kill call uses an
invalid empty string signal which throws; update the call that references
process.kill(pid, type === 'master' || type === 'single' ? '' : 'SIGKILL') to
use an explicit graceful signal (e.g. 'SIGTERM') for the master/single branch so
it becomes process.kill(pid, type === 'master' || type === 'single' ? 'SIGTERM'
: 'SIGKILL'); keep the existing try/catch behavior around this call so errors
are still handled but no longer triggered by an invalid signal, and ensure the
variables pid and type referenced in this change are the same ones used in the
surrounding function.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

♻️ Duplicate comments (1)
tools/scripts/src/commands/start.ts (1)

351-366: ⚠️ Potential issue | 🟠 Major

Don’t let --ignore-stderr turn a failed start into success.

By the time Line 365 runs, readiness has already failed. Returning there makes start --daemon --ignore-stderr exit successfully even though no egg-ready message arrived.

Suggested adjustment
-        if (this.flags['ignore-stderr']) {
-          return; // User opted to ignore stderr errors
-        }
-        this.logToStderr('Start got error, see %o', stderrFile);
-        this.logToStderr('Or use `--ignore-stderr` to ignore stderr at startup.');
+        if (!this.flags['ignore-stderr']) {
+          this.logToStderr('Start got error, see %o', stderrFile);
+          this.logToStderr('Or use `--ignore-stderr` to ignore stderr at startup.');
+        }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tools/scripts/src/commands/start.ts` around lines 351 - 366, The current
early return when this.flags['ignore-stderr'] is true causes a failed readiness
to be treated as success; after handling hasStderrContent, do not simply
"return" when ignore-stderr is set — instead propagate the failure (throw a
specific Error or call process.exit(1)) so the CLI exits non‑zero when
startup/readiness did not complete (i.e. replace the return in the
hasStderrContent block with throwing an error or exiting non‑zero), ensuring the
failure condition detected after logging (hasStderrContent handling) cannot be
masked by this.flags['ignore-stderr'].
🧹 Nitpick comments (1)
tools/scripts/src/commands/start.ts (1)

320-325: Avoid any for the IPC payload.

Please switch this to unknown and narrow the message shape before reading action/data. As per coding guidelines "Avoid 'any' type in TypeScript; use 'unknown' when type is truly unknown".

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

In `@tools/scripts/src/commands/start.ts` around lines 320 - 325, Change the
child.on('message', (msg: any) => { ... }) handler to accept msg: unknown and
perform a type-narrowing guard before accessing properties: check msg is a
non-null object, that it has a string 'action' property and an object 'data'
with an 'address' string, then only inside that guarded branch perform
clearTimeout(timer), set this.isReady = true and call this.log('%s started on
%s', readyLabel, msg.data.address); you can create a small type predicate or
inline checks and then assert msg as the narrowed interface (e.g., { action:
string; data: { address: string } }) to avoid using any while keeping the same
logic in the child.on callback.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@tools/scripts/scripts/start-single.cjs`:
- Around line 12-18: The ESM launcher is missing the same fallback that the CJS
launcher uses: when resolving the start function it should check both top-level
named exports and the default export. In the ESM entry (where you currently read
framework/startEgg from the imported module), change the resolution to first try
framework.start ?? framework.startEgg and if that isn't a function then try
framework.default?.start ?? framework.default?.startEgg (ensuring the variable
name startEgg is reused or reassigned only after type-checking). This mirrors
the CJS logic (exports, startEgg) and keeps getServerBin() behavior consistent
across ESM/CJS.

In `@tools/scripts/src/commands/start.ts`:
- Around line 315-337: waitForReady currently only listens for 'message' and
non-zero exit codes, so spawn errors and clean exits before receiving
'egg-ready' hang until timeout and msg is typed as any; add a child.on('error',
(err) => { clearTimeout(timer); cleanup listeners; reject(err); }) and change
the child.on('exit', (code) => { if (!this.isReady) { clearTimeout(timer);
cleanup listeners; reject(new Error(`Child process exited with code ${code}`));
} }) to reject on any pre-ready termination, ensure you clear the timer and
remove/unbind listeners in a shared cleanup helper, and tighten the message
typing by replacing msg: any with a proper type or unknown and using a
type-guard to check msg.action === 'egg-ready' before accessing msg.data.

---

Duplicate comments:
In `@tools/scripts/src/commands/start.ts`:
- Around line 351-366: The current early return when this.flags['ignore-stderr']
is true causes a failed readiness to be treated as success; after handling
hasStderrContent, do not simply "return" when ignore-stderr is set — instead
propagate the failure (throw a specific Error or call process.exit(1)) so the
CLI exits non‑zero when startup/readiness did not complete (i.e. replace the
return in the hasStderrContent block with throwing an error or exiting
non‑zero), ensuring the failure condition detected after logging
(hasStderrContent handling) cannot be masked by this.flags['ignore-stderr'].

---

Nitpick comments:
In `@tools/scripts/src/commands/start.ts`:
- Around line 320-325: Change the child.on('message', (msg: any) => { ... })
handler to accept msg: unknown and perform a type-narrowing guard before
accessing properties: check msg is a non-null object, that it has a string
'action' property and an object 'data' with an 'address' string, then only
inside that guarded branch perform clearTimeout(timer), set this.isReady = true
and call this.log('%s started on %s', readyLabel, msg.data.address); you can
create a small type predicate or inline checks and then assert msg as the
narrowed interface (e.g., { action: string; data: { address: string } }) to
avoid using any while keeping the same logic in the child.on callback.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: f0f4be1a-6d59-4513-b812-b43e96f4ef6e

📥 Commits

Reviewing files that changed from the base of the PR and between 58156bf and 959e604.

📒 Files selected for processing (13)
  • tools/scripts/package.json
  • tools/scripts/scripts/start-single.cjs
  • tools/scripts/scripts/start-single.mjs
  • tools/scripts/src/commands/start.ts
  • tools/scripts/src/commands/stop.ts
  • tools/scripts/src/esm-polyfill.ts
  • tools/scripts/src/file-url-resolver.ts
  • tools/scripts/src/helper.ts
  • tools/scripts/src/resolve-framework-entry.ts
  • tools/scripts/test/esm-polyfill.test.ts
  • tools/scripts/test/resolve-framework-entry.test.ts
  • tools/scripts/test/utils.ts
  • tools/scripts/tsdown.config.ts
✅ Files skipped from review due to trivial changes (4)
  • tools/scripts/src/helper.ts
  • tools/scripts/src/file-url-resolver.ts
  • tools/scripts/test/resolve-framework-entry.test.ts
  • tools/scripts/scripts/start-single.mjs
🚧 Files skipped from review as they are similar to previous changes (5)
  • tools/scripts/test/utils.ts
  • tools/scripts/src/commands/stop.ts
  • tools/scripts/tsdown.config.ts
  • tools/scripts/package.json
  • tools/scripts/src/resolve-framework-entry.ts

Comment on lines +12 to +18
const exports = await importModule(options.framework);
let startEgg = exports.start ?? exports.startEgg;
if (typeof startEgg !== 'function') {
startEgg = exports.default?.start ?? exports.default?.startEgg;
}
if (typeof startEgg !== 'function') {
throw new Error(`Cannot find start/startEgg function from framework: ${options.framework}`);
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 | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

fd -t f "start-single\.(cjs|mjs)" tools/

Repository: eggjs/egg

Length of output: 130


🏁 Script executed:

cat -n tools/scripts/scripts/start-single.cjs

Repository: eggjs/egg

Length of output: 2872


🏁 Script executed:

cat -n tools/scripts/scripts/start-single.mjs

Repository: eggjs/egg

Length of output: 2732


🏁 Script executed:

fd -t f "start\.ts" tools/scripts/src/commands/ | head -5

Repository: eggjs/egg

Length of output: 88


🏁 Script executed:

cat -n tools/scripts/src/commands/start.ts | head -100

Repository: eggjs/egg

Length of output: 3912


🏁 Script executed:

cat -n tools/scripts/src/commands/start.ts | sed -n '84,200p'

Repository: eggjs/egg

Length of output: 4863


🏁 Script executed:

rg -A 20 "single" tools/scripts/src/commands/start.ts | head -80

Repository: eggjs/egg

Length of output: 2704


Align framework-entry fallback behavior between CJS and ESM launchers.

The .cjs launcher checks exports.default?.start and exports.default?.startEgg as a fallback (lines 15), but the .mjs launcher skips this step entirely and throws if the named export is not found. Since getServerBin() selects the launcher based on this.isESM at runtime, single-process startup will behave differently depending on which binary is used. Update start-single.mjs to mirror the fallback logic:

const startEgg = framework.start ?? framework.startEgg;
if (typeof startEgg !== 'function') {
  startEgg = framework.default?.start ?? framework.default?.startEgg;
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tools/scripts/scripts/start-single.cjs` around lines 12 - 18, The ESM
launcher is missing the same fallback that the CJS launcher uses: when resolving
the start function it should check both top-level named exports and the default
export. In the ESM entry (where you currently read framework/startEgg from the
imported module), change the resolution to first try framework.start ??
framework.startEgg and if that isn't a function then try
framework.default?.start ?? framework.default?.startEgg (ensuring the variable
name startEgg is reused or reassigned only after type-checking). This mirrors
the CJS logic (exports, startEgg) and keeps getServerBin() behavior consistent
across ESM/CJS.

Comment on lines +315 to +337
await new Promise<void>((resolve, reject) => {
const timer = setTimeout(() => {
reject(new Error(`Start failed, ${timeoutMs / 1000}s timeout`));
}, timeoutMs);

child.on('message', (msg: any) => {
// https://github.com/eggjs/cluster/blob/master/src/master.ts#L119
if (msg && msg.action === 'egg-ready') {
clearTimeout(timer);
this.isReady = true;
this.log('%s started on %s', readyLabel, msg.data.address);
child.unref();
child.disconnect();
resolve();
}
});

child.on('exit', (code) => {
if (code) {
clearTimeout(timer);
reject(new Error(`Child process exited with code ${code}`));
}
});
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 | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

# First, let's examine the actual code in the file
cat -n tools/scripts/src/commands/start.ts | sed -n '310,340p'

Repository: eggjs/egg

Length of output: 1328


🏁 Script executed:

# Also check the broader context around waitForReady function
cat -n tools/scripts/src/commands/start.ts | sed -n '295,380p'

Repository: eggjs/egg

Length of output: 3408


🏁 Script executed:

# Check the TypeScript configuration to verify strict mode settings
find . -name "tsconfig.json" | head -5

Repository: eggjs/egg

Length of output: 272


🏁 Script executed:

# Find the tsconfig.json for the tools/scripts package
find . -name "tsconfig.json" -path "*/tools/*" | xargs cat

Repository: eggjs/egg

Length of output: 5159


🏁 Script executed:

# Let's also check the Node.js documentation behavior for ChildProcess events
# Check if there are any other similar error handling patterns in the codebase
rg -A 5 -B 5 "child\.on\('error'" --type ts

Repository: eggjs/egg

Length of output: 35


🏁 Script executed:

# Check if there are patterns using child.once for exit handlers
rg -A 5 -B 5 "child\.once\('exit'" --type ts

Repository: eggjs/egg

Length of output: 874


🏁 Script executed:

# Verify the message type by checking how msg is used elsewhere
rg "msg\.action === 'egg-ready'" -A 3 -B 3 --type ts

Repository: eggjs/egg

Length of output: 1779


🏁 Script executed:

# Check the tools/scripts tsconfig more carefully
cat -n tools/scripts/tsconfig.json

Repository: eggjs/egg

Length of output: 394


🏁 Script executed:

# Check the root tsconfig that tools/scripts extends
cat -n tsconfig.json | head -30

Repository: eggjs/egg

Length of output: 949


🏁 Script executed:

# Verify Node.js ChildProcess spawn behavior - check if there are any spawn error handlers elsewhere
rg "spawn\(" -A 10 tools/scripts/src --type ts | head -40

Repository: eggjs/egg

Length of output: 1347


🏁 Script executed:

# Check how messages are typed in the codebase - look for interface definitions
rg "msg\.data\.address" -B 10 -A 2 --type ts | head -50

Repository: eggjs/egg

Length of output: 3019


🏁 Script executed:

# Let's check the `@eggjs/tsconfig` to understand what "strict" mode includes
npm info `@eggjs/tsconfig` 2>/dev/null || cat -n packages/*/tsconfig.json | grep -A 5 "strict"

Repository: eggjs/egg

Length of output: 914


🏁 Script executed:

# Look at how the process spawn is called to understand stdio setup
cat -n tools/scripts/src/commands/start.ts | sed -n '275,295p'

Repository: eggjs/egg

Length of output: 1229


🏁 Script executed:

# Check if child.unref() and child.disconnect() are called correctly
cat -n tools/scripts/src/commands/start.ts | sed -n '320,330p'

Repository: eggjs/egg

Length of output: 545


🏁 Script executed:

# Verify the handling of exit code 0 - check the non-waitForReady exit handler
cat -n tools/scripts/src/commands/start.ts | sed -n '288,298p'

Repository: eggjs/egg

Length of output: 555


🏁 Script executed:

# Check if there's any existing error event handling for spawn in the codebase
rg "\.on\('error'" -B 3 -A 3 tools/scripts/src --type ts

Repository: eggjs/egg

Length of output: 35


🏁 Script executed:

# Check Node.js child_process documentation behavior - verify if error events are possible during spawn
rg "spawn.*error" --type ts | head -20

Repository: eggjs/egg

Length of output: 392


🏁 Script executed:

# Look at the structure to understand spawn behavior and when errors can occur
cat -n tools/scripts/src/commands/start.ts | sed -n '1,30p'

Repository: eggjs/egg

Length of output: 1306


🏁 Script executed:

# Check if there are any TypeScript type definitions for the spawn return to understand expected behavior
rg "ChildProcess" tools/scripts/src/commands/start.ts -B 2 -A 2

Repository: eggjs/egg

Length of output: 560


🏁 Script executed:

# Look at the test case that mentions spawn error
cat -n tools/scripts/test/start-without-demon-2.test.ts | grep -A 15 "should emit spawn error"

Repository: eggjs/egg

Length of output: 576


🏁 Script executed:

# Check the test for spawn error handling
cat -n tools/scripts/test/start-without-demon-2.test.ts

Repository: eggjs/egg

Length of output: 5165


🏁 Script executed:

# Also check the master should not die if spawn error test
cat -n packages/cluster/test/agent_worker.test.ts | grep -B 5 -A 15 "master should not die if spawn error"

Repository: eggjs/egg

Length of output: 916


🏁 Script executed:

# Let's understand the actual node.js ChildProcess behavior for spawn errors
# Check if there's documentation in the code or comments about spawn errors
rg "spawn error|spawn.*error" -B 5 -A 5 tools/scripts --type ts

Repository: eggjs/egg

Length of output: 912


🏁 Script executed:

# Let's check what message structure should be used instead of any
rg "interface.*Message|type.*Message" -A 5 --type ts packages/cluster/src | head -40

Repository: eggjs/egg

Length of output: 2932


🏁 Script executed:

# Check if there's a defined message type in egg framework
rg "egg-ready.*data" -B 5 -A 5 --type ts | head -50

Repository: eggjs/egg

Length of output: 35


🏁 Script executed:

# Verify if MessageBody is exported from a location accessible to tools/scripts
rg "export.*MessageBody" --type ts | grep -E "(packages/cluster|packages/core)"

Repository: eggjs/egg

Length of output: 123


🏁 Script executed:

# Check the imports in start.ts to see what's already available
head -20 tools/scripts/src/commands/start.ts

Repository: eggjs/egg

Length of output: 783


🏁 Script executed:

# Check if MessageBody is exported from packages/cluster package.json exports
cat packages/cluster/package.json | grep -A 20 "exports"

Repository: eggjs/egg

Length of output: 845


🏁 Script executed:

# Alternative: check what's available to import from packages/cluster
rg "export.*from.*messenger" packages/cluster/src --type ts

Repository: eggjs/egg

Length of output: 35


🏁 Script executed:

# Check what's exported from packages/cluster/src/index.ts
cat packages/cluster/src/index.ts | grep -E "export|import.*MessageBody"

Repository: eggjs/egg

Length of output: 278


🏁 Script executed:

# Check if tools/scripts depends on `@egg/cluster` or if it can only import from public exports
cat tools/scripts/package.json | grep -E "dependencies|devDependencies" -A 20

Repository: eggjs/egg

Length of output: 776


🏁 Script executed:

# Check if packages/cluster is in the workspace and could be imported
cat packages/cluster/src/utils/messenger.ts | head -40

Repository: eggjs/egg

Length of output: 1176


🏁 Script executed:

# Check if there's a way to access MessageBody through the egg package
cat packages/egg/src/index.ts | grep -i "message" | head -20

Repository: eggjs/egg

Length of output: 35


🏁 Script executed:

# Confirm the actual tsconfig for tools/scripts and what strict mode is enabled
cat tools/scripts/tsconfig.json

Repository: eggjs/egg

Length of output: 331


🏁 Script executed:

# Check if there's a way to import from packages/cluster in tools/scripts context
rg "from '@egg" tools/scripts/src --type ts | head -10

Repository: eggjs/egg

Length of output: 152


🏁 Script executed:

# Let's verify what the actual strict mode settings are for tools/scripts
cat tsconfig.json | grep -E "strict|noImplicitAny"

Repository: eggjs/egg

Length of output: 35


🏁 Script executed:

# Check the full root tsconfig to understand strict mode inheritance
cat tsconfig.json | head -20

Repository: eggjs/egg

Length of output: 558


🏁 Script executed:

# Check what `@eggjs/tsconfig` includes
npm info `@eggjs/tsconfig` 2>&1 | grep -A 50 "README"

Repository: eggjs/egg

Length of output: 35


🏁 Script executed:

# Let's see if we can find the actual `@eggjs/tsconfig` definition or its source
find . -name "@eggjs" -o -name "eggjs" -type d 2>/dev/null | head -5

Repository: eggjs/egg

Length of output: 119


🏁 Script executed:

# Check the packages/tsconfig directory
ls -la packages/tsconfig/

Repository: eggjs/egg

Length of output: 657


🏁 Script executed:

# See what's in the tsconfig package
cat packages/tsconfig/src/index.json 2>/dev/null || cat packages/tsconfig/index.json 2>/dev/null || find packages/tsconfig -name "*.json" -type f

Repository: eggjs/egg

Length of output: 174


🏁 Script executed:

# Let's check if strict mode is enabled in the `@eggjs/tsconfig` by looking at the actual source
find packages -name "tsconfig" -type d | head -5

Repository: eggjs/egg

Length of output: 70


🏁 Script executed:

# Check the tsconfig.json in packages/tsconfig - this is the `@eggjs/tsconfig`
cat packages/tsconfig/tsconfig.json

Repository: eggjs/egg

Length of output: 1827


🏁 Script executed:

# Verify that MessageBody is indeed not part of the public exports
grep -r "export.*MessageBody" packages/cluster/src/index.ts

Repository: eggjs/egg

Length of output: 35


Handle spawn errors and reject on any pre-ready termination.

waitForReady() lacks an error event listener and only rejects on non-zero exit codes. Spawn failures (e.g., command not found) and clean exits before egg-ready both hang until timeout. Additionally, msg is typed as any on line 320, violating strict TypeScript mode.

Suggested adjustment
       await new Promise<void>((resolve, reject) => {
         const timer = setTimeout(() => {
           reject(new Error(`Start failed, ${timeoutMs / 1000}s timeout`));
         }, timeoutMs);

         child.on('message', (msg: any) => {
           // https://github.com/eggjs/cluster/blob/master/src/master.ts#L119
           if (msg && msg.action === 'egg-ready') {
             clearTimeout(timer);
             this.isReady = true;
             this.log('%s started on %s', readyLabel, msg.data.address);
             child.unref();
             child.disconnect();
             resolve();
           }
         });

-        child.on('exit', (code) => {
-          if (code) {
-            clearTimeout(timer);
-            reject(new Error(`Child process exited with code ${code}`));
-          }
-        });
+        child.once('error', err => {
+          clearTimeout(timer);
+          reject(err);
+        });
+        child.once('exit', (code, signal) => {
+          clearTimeout(timer);
+          reject(
+            new Error(
+              code === null
+                ? `Child process exited before ready via ${signal}`
+                : `Child process exited before ready with code ${code}`,
+            ),
+          );
+        });
       });
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tools/scripts/src/commands/start.ts` around lines 315 - 337, waitForReady
currently only listens for 'message' and non-zero exit codes, so spawn errors
and clean exits before receiving 'egg-ready' hang until timeout and msg is typed
as any; add a child.on('error', (err) => { clearTimeout(timer); cleanup
listeners; reject(err); }) and change the child.on('exit', (code) => { if
(!this.isReady) { clearTimeout(timer); cleanup listeners; reject(new
Error(`Child process exited with code ${code}`)); } }) to reject on any
pre-ready termination, ensure you clear the timer and remove/unbind listeners in
a shared cleanup helper, and tighten the message typing by replacing msg: any
with a proper type or unknown and using a type-guard to check msg.action ===
'egg-ready' before accessing msg.data.

@fengmk2
Copy link
Copy Markdown
Member

fengmk2 commented Apr 11, 2026

@codex review

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 959e604ac8

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +322 to +328
if (msg && msg.action === 'egg-ready') {
clearTimeout(timer);
this.isReady = true;
this.log('%s started on %s', readyLabel, msg.data.address);
child.unref();
child.disconnect();
resolve();
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Fail daemon start when startup writes stderr before ready

waitForReady() now resolves as soon as it receives egg-ready, but that success path never inspects master-stderr.log. As a result, boot-time errors that still allow readiness are silently accepted (exit code 0) even when --ignore-stderr is not set, which regresses the prior startup contract and makes --ignore-stderr ineffective in this common case.

Useful? React with 👍 / 👎.

Comment on lines +13 to +15
const startEgg = framework.start ?? framework.startEgg;
if (typeof startEgg !== 'function') {
throw new Error(`Cannot find start/startEgg function from framework: ${options.framework}`);
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Support default-exported frameworks in ESM single launcher

The ESM single-process launcher only checks framework.start/framework.startEgg, but unlike the CJS launcher it does not fall back to framework.default. Frameworks that expose their start API only via default export (common in CJS/interop builds) will incorrectly throw Cannot find start/startEgg in ESM apps.

Useful? React with 👍 / 👎.

Comment on lines +52 to +55
.replace(/\bimport\.meta\.dirname\b/g, JSON.stringify(dirname))
.replace(/\bimport\.meta\.url\b/g, JSON.stringify(url))
.replace(/\bimport\.meta\.filename\b/g, JSON.stringify(absPath))
.replace(/\b__dirname\b/g, JSON.stringify(dirname));
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Restrict ESM polyfill rewrites to actual identifiers

The polyfill uses global regex replacements on raw source text, so tokens are rewritten even inside string literals (and comments), not just executable identifiers. For example, a literal like "__dirname" is mutated into an absolute path string, which can corrupt behavior or emitted bundles in modules that mention these tokens textually.

Useful? React with 👍 / 👎.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants