Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions tools/scripts/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,10 @@
"./baseCommand": "./src/baseCommand.ts",
"./commands/start": "./src/commands/start.ts",
"./commands/stop": "./src/commands/stop.ts",
"./esm-polyfill": "./src/esm-polyfill.ts",
"./file-url-resolver": "./src/file-url-resolver.ts",
"./helper": "./src/helper.ts",
"./resolve-framework-entry": "./src/resolve-framework-entry.ts",
"./types": "./src/types.ts",
"./package.json": "./package.json"
},
Expand All @@ -42,7 +45,10 @@
"./baseCommand": "./dist/baseCommand.js",
"./commands/start": "./dist/commands/start.js",
"./commands/stop": "./dist/commands/stop.js",
"./esm-polyfill": "./dist/esm-polyfill.js",
"./file-url-resolver": "./dist/file-url-resolver.js",
"./helper": "./dist/helper.js",
"./resolve-framework-entry": "./dist/resolve-framework-entry.js",
"./types": "./dist/types.js",
"./package.json": "./package.json"
}
Expand All @@ -56,9 +62,11 @@
"dependencies": {
"@eggjs/utils": "workspace:*",
"@oclif/core": "catalog:",
"esbuild": "catalog:",
"node-homedir": "catalog:",
"runscript": "catalog:",
"source-map-support": "catalog:",
"tsx": "catalog:",
"utility": "catalog:"
},
"devDependencies": {
Expand Down
83 changes: 83 additions & 0 deletions tools/scripts/scripts/start-single.cjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
const http = require('node:http');
const { debuglog } = require('node:util');

const { importModule } = require('@eggjs/utils');

const debug = debuglog('egg/scripts/start-single/cjs');

async function main() {
debug('argv: %o', process.argv);
const options = JSON.parse(process.argv[2]);
debug('start single options: %o', options);
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}`);
Comment on lines +12 to +18
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.

}
const app = await startEgg({
baseDir: options.baseDir,
framework: options.framework,
env: options.env,
mode: 'single',
});

const port = options.port ?? 7001;
const server = http.createServer(app.callback());
app.emit('server', server);

await new Promise((resolve, reject) => {
server.listen(port, () => {
resolve(undefined);
});
server.once('error', reject);
});

const address = server.address();
const url = typeof address === 'string' ? address : `http://127.0.0.1:${address.port}`;

debug('server started on %s', url);

// notify parent process (daemon mode)
if (process.send) {
process.send({
action: 'egg-ready',
data: { address: url, port: address.port ?? port },
});
}

// graceful shutdown
const shutdown = async (signal) => {
debug('receive signal %s, closing server', signal);
server.close(() => {
debug('server closed');
if (typeof app.close === 'function') {
app
.close()
.then(() => {
process.exit(0);
})
.catch(() => {
process.exit(1);
});
} else {
process.exit(0);
}
});
// force exit after timeout
setTimeout(() => {
process.exit(1);
}, 10000).unref();
};

process.once('SIGTERM', () => shutdown('SIGTERM'));
process.once('SIGINT', () => shutdown('SIGINT'));
process.once('SIGQUIT', () => shutdown('SIGQUIT'));
}

main().catch((err) => {
console.error(err);
process.exit(1);
});
80 changes: 80 additions & 0 deletions tools/scripts/scripts/start-single.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
import http from 'node:http';
import { debuglog } from 'node:util';

import { importModule } from '@eggjs/utils';

const debug = debuglog('egg/scripts/start-single/esm');

async function main() {
debug('argv: %o', process.argv);
const options = JSON.parse(process.argv[2]);
debug('start single options: %o', options);
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}`);
Comment on lines +13 to +15
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 +12 to +16
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);
  }

const app = await startEgg({
baseDir: options.baseDir,
framework: options.framework,
env: options.env,
mode: 'single',
});

const port = options.port ?? 7001;
const server = http.createServer(app.callback());
app.emit('server', server);

await new Promise((resolve, reject) => {
server.listen(port, () => {
resolve(undefined);
});
server.once('error', reject);
});

const address = server.address();
const url = typeof address === 'string' ? address : `http://127.0.0.1:${address.port}`;

debug('server started on %s', url);

// notify parent process (daemon mode)
if (process.send) {
process.send({
action: 'egg-ready',
data: { address: url, port: address.port ?? port },
});
}

// graceful shutdown
const shutdown = async (signal) => {
debug('receive signal %s, closing server', signal);
server.close(() => {
debug('server closed');
if (typeof app.close === 'function') {
app
.close()
.then(() => {
process.exit(0);
})
.catch(() => {
process.exit(1);
});
} else {
process.exit(0);
}
});
// force exit after timeout
setTimeout(() => {
process.exit(1);
}, 10000).unref();
};

process.once('SIGTERM', () => shutdown('SIGTERM'));
process.once('SIGINT', () => shutdown('SIGINT'));
process.once('SIGQUIT', () => shutdown('SIGQUIT'));
}

main().catch((err) => {
console.error(err);
process.exit(1);
});
Loading
Loading