mirror of
https://github.com/openclaw/openclaw.git
synced 2026-10-03 01:29:56 +00:00
fix(cli): defer CLI-only module imports after version fast path (#162311)
Defers the CLI lifecycle modules until after the `--version` fast path, so `openclaw --version` no longer imports them. Proof: built base/candidate in secretless Docker, median of 15 runs. Package-root `--version` 110.8 -> 97.6 ms with 312 -> 253 synchronously imported modules; all six lifecycle families no longer load there. The profiled launcher path changes within noise (114.3 -> 113.8 ms). `--version`, `-V`, `--help` and `status --json` produce byte-identical output on both, and the deferred modules still load for commands that need them. Regression tests fail before and pass after. Co-authored-by: Ayaan Zaidi <hi@obviy.us>
This commit is contained in:
parent
1b896e6327
commit
ec8df3aa0f
3 changed files with 83 additions and 31 deletions
10
src/entry.ts
10
src/entry.ts
|
|
@ -7,7 +7,6 @@ import { format } from "node:util";
|
|||
import { resolveCliArgvInvocation } from "./cli/argv-invocation.js";
|
||||
import { isRootHelpInvocation } from "./cli/argv.js";
|
||||
import { parseCliContainerArgs, resolveCliContainerTarget } from "./cli/container-target.js";
|
||||
import { requestExitAfterOneShotOutput, runCliWithExitFinalization } from "./cli/one-shot-exit.js";
|
||||
import {
|
||||
tryOutputPrecomputedCommandHelp,
|
||||
type PrecomputedCommandHelpDeps,
|
||||
|
|
@ -19,7 +18,6 @@ import {
|
|||
isUpdateAdmissionInvocation,
|
||||
tryRunUpdateAdmissionBeforeStartup,
|
||||
} from "./cli/run-main-update-admission.js";
|
||||
import { withCliProcessScope } from "./cli/runtime-cleanup-scope.js";
|
||||
import {
|
||||
configureGatewayStartupTraceConsoleFormatting,
|
||||
createGatewayDispatchStartupTrace,
|
||||
|
|
@ -240,8 +238,10 @@ if (
|
|||
gatewayEntryStartupTrace.mark("argv");
|
||||
|
||||
if (!tryHandleRootVersionFastPath(process.argv)) {
|
||||
const run = (finalize?: () => Promise<void>) =>
|
||||
withCliProcessScope(() => runMainOrRootHelp(process.argv, { finalize }));
|
||||
const run = async (finalize?: () => Promise<void>) => {
|
||||
const { withCliProcessScope } = await import("./cli/runtime-cleanup-scope.js");
|
||||
return withCliProcessScope(() => runMainOrRootHelp(process.argv, { finalize }));
|
||||
};
|
||||
const managedNodeStatePath = getManagedNodeHostStatePath();
|
||||
if (managedNodeStatePath) {
|
||||
const { withExistingOpenClawStateSchema } =
|
||||
|
|
@ -371,6 +371,8 @@ export async function runMainOrRootHelp(
|
|||
// mode so the envelope is written here. Only failures before runCli are startup failures.
|
||||
let commandStarted = false;
|
||||
let failureHandler: Awaited<ReturnType<typeof prepareCliFailureHandler>> | undefined;
|
||||
const { runCliWithExitFinalization, requestExitAfterOneShotOutput } =
|
||||
await import("./cli/one-shot-exit.js");
|
||||
await runCliWithExitFinalization({
|
||||
finalize: deps.finalize,
|
||||
run: async () => {
|
||||
|
|
|
|||
|
|
@ -13,9 +13,19 @@ vi.mock("node:fs", async (importOriginal) => ({
|
|||
vi.mock("./cli/run-main.js", () => ({
|
||||
runCli: vi.fn(async () => undefined),
|
||||
}));
|
||||
const lifecycleImports = vi.hoisted(() => ({ failureOutput: vi.fn() }));
|
||||
|
||||
vi.mock("./cli/one-shot-exit.js", () => ({
|
||||
runCliWithExitFinalization: vi.fn(),
|
||||
}));
|
||||
vi.mock("./cli/failure-output.js", () => {
|
||||
lifecycleImports.failureOutput();
|
||||
return {
|
||||
formatCliFailureLines: vi.fn(() => []),
|
||||
formatCliJsonFailure: vi.fn(),
|
||||
isExpectedCliError: vi.fn(() => false),
|
||||
};
|
||||
});
|
||||
vi.mock("./entry.version-fast-path.js", () => ({
|
||||
tryHandleRootVersionFastPath: vi.fn(() => false),
|
||||
}));
|
||||
|
|
@ -25,6 +35,26 @@ vi.mock("./infra/is-main.js", () => ({
|
|||
vi.mock("./infra/package-lifecycle.js", () => ({
|
||||
completePendingPackageLifecycle: vi.fn(async () => true),
|
||||
}));
|
||||
vi.mock("./library.js", () => ({
|
||||
applyTemplate: vi.fn(),
|
||||
createDefaultDeps: vi.fn(),
|
||||
deriveSessionKey: vi.fn(),
|
||||
describePortOwner: vi.fn(),
|
||||
ensureBinary: vi.fn(),
|
||||
ensurePortAvailable: vi.fn(),
|
||||
getReplyFromConfig: vi.fn(),
|
||||
handlePortError: vi.fn(),
|
||||
loadConfig: vi.fn(),
|
||||
monitorWebChannel: vi.fn(),
|
||||
normalizeE164: vi.fn(),
|
||||
PortInUseError: class PortInUseError extends Error {},
|
||||
promptYesNo: vi.fn(),
|
||||
resolveSessionKey: vi.fn(),
|
||||
resolveStorePath: vi.fn(),
|
||||
runCommandWithTimeout: vi.fn(),
|
||||
runExec: vi.fn(),
|
||||
waitForever: vi.fn(),
|
||||
}));
|
||||
|
||||
const originalArgv = process.argv;
|
||||
const originalExitCode = process.exitCode;
|
||||
|
|
@ -37,6 +67,7 @@ describe("legacy package executable entrypoint", () => {
|
|||
vi.mocked(tryHandleRootVersionFastPath).mockReturnValue(false);
|
||||
vi.mocked(existsSync).mockReturnValue(false);
|
||||
vi.mocked(completePendingPackageLifecycle).mockResolvedValue(true);
|
||||
lifecycleImports.failureOutput.mockClear();
|
||||
process.argv = ["node", "dist/index.js", "status"];
|
||||
});
|
||||
|
||||
|
|
@ -58,6 +89,24 @@ describe("legacy package executable entrypoint", () => {
|
|||
expect(tryHandleRootVersionFastPath).toHaveBeenCalledWith(process.argv);
|
||||
expect(runMain.runCli).not.toHaveBeenCalled();
|
||||
expect(exitFinalization.runCliWithExitFinalization).not.toHaveBeenCalled();
|
||||
expect(lifecycleImports.failureOutput).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("loads CLI failure modules only after the version fast path declines", async () => {
|
||||
process.argv = ["node", "dist/index.js", "status"];
|
||||
|
||||
await import("./index.js?legacy-cli-start" as "./index.js");
|
||||
|
||||
expect(lifecycleImports.failureOutput).toHaveBeenCalledOnce();
|
||||
});
|
||||
|
||||
it("keeps library imports free of CLI failure modules", async () => {
|
||||
vi.mocked(isMainModule).mockReturnValue(false);
|
||||
|
||||
const entry = await import("./index.js?legacy-library-entry" as "./index.js");
|
||||
|
||||
expect(typeof entry.loadConfig).toBe("function");
|
||||
expect(lifecycleImports.failureOutput).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("completes pending lifecycle before loading the CLI entry graph", async () => {
|
||||
|
|
|
|||
55
src/index.ts
55
src/index.ts
|
|
@ -6,6 +6,7 @@ import process from "node:process";
|
|||
import { fileURLToPath } from "node:url";
|
||||
import { resolveCliArgvInvocation } from "./cli/argv-invocation.js";
|
||||
import { tryRunUpdateAdmissionBeforeStartup } from "./cli/run-main-update-admission.js";
|
||||
import { tryHandleRootVersionFastPath } from "./entry.version-fast-path.js";
|
||||
import { isMainModule } from "./infra/is-main.js";
|
||||
|
||||
const isMain = isMainModule({
|
||||
|
|
@ -31,27 +32,8 @@ if (
|
|||
}
|
||||
}
|
||||
|
||||
const [
|
||||
{ formatCliFailureLines, formatCliJsonFailure, isExpectedCliError },
|
||||
{ isJsonOutputModeActive },
|
||||
{ runCliWithExitFinalization },
|
||||
{ withCliProcessScope },
|
||||
{ installDistEsmResolveFastPath },
|
||||
{ tryHandleRootVersionFastPath },
|
||||
{ formatUncaughtError },
|
||||
{ runFatalErrorHooks },
|
||||
{ installUnhandledRejectionHandler, isBenignUncaughtExceptionError, isUncaughtExceptionHandled },
|
||||
] = await Promise.all([
|
||||
import("./cli/failure-output.js"),
|
||||
import("./cli/json-output-mode.js"),
|
||||
import("./cli/one-shot-exit.js"),
|
||||
import("./cli/runtime-cleanup-scope.js"),
|
||||
import("./entry.esm-resolve-fast-path.js"),
|
||||
import("./entry.version-fast-path.js"),
|
||||
import("./infra/errors.js"),
|
||||
import("./infra/fatal-error-hooks.js"),
|
||||
import("./infra/unhandled-rejections.js"),
|
||||
]);
|
||||
const handledRootVersion =
|
||||
isMain && !handledAdmission && tryHandleRootVersionFastPath(process.argv);
|
||||
|
||||
type LegacyCliDeps = {
|
||||
runCli: (
|
||||
|
|
@ -102,12 +84,6 @@ export async function runLegacyCliEntry(
|
|||
await runCli(argv, options);
|
||||
}
|
||||
|
||||
if (isMain && !handledAdmission) {
|
||||
installDistEsmResolveFastPath(import.meta.url);
|
||||
}
|
||||
const handledRootVersion =
|
||||
isMain && !handledAdmission && tryHandleRootVersionFastPath(process.argv);
|
||||
|
||||
if (!isMain) {
|
||||
({
|
||||
applyTemplate,
|
||||
|
|
@ -132,6 +108,31 @@ if (!isMain) {
|
|||
}
|
||||
|
||||
if (isMain && !handledRootVersion && !handledAdmission) {
|
||||
const [
|
||||
{ formatCliFailureLines, formatCliJsonFailure, isExpectedCliError },
|
||||
{ isJsonOutputModeActive },
|
||||
{ runCliWithExitFinalization },
|
||||
{ withCliProcessScope },
|
||||
{ installDistEsmResolveFastPath: installFastPath },
|
||||
{ formatUncaughtError },
|
||||
{ runFatalErrorHooks },
|
||||
{
|
||||
installUnhandledRejectionHandler,
|
||||
isBenignUncaughtExceptionError,
|
||||
isUncaughtExceptionHandled,
|
||||
},
|
||||
] = await Promise.all([
|
||||
import("./cli/failure-output.js"),
|
||||
import("./cli/json-output-mode.js"),
|
||||
import("./cli/one-shot-exit.js"),
|
||||
import("./cli/runtime-cleanup-scope.js"),
|
||||
import("./entry.esm-resolve-fast-path.js"),
|
||||
import("./infra/errors.js"),
|
||||
import("./infra/fatal-error-hooks.js"),
|
||||
import("./infra/unhandled-rejections.js"),
|
||||
]);
|
||||
installFastPath(import.meta.url);
|
||||
|
||||
const { defaultRuntime, restoreRuntimeTerminalState } = await import("./runtime.js");
|
||||
|
||||
// Global error handlers to prevent silent crashes from unhandled rejections/exceptions.
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue