import { type ChildProcess } from "node:child_process"; /** * Old-space cap for measured processes, when the caller does not pick one. * * Small on purpose: a leak that would take a production container an hour to * kill reaches a 512 MB ceiling in minutes. It is a default, not a constant — * an app whose legitimate working set is larger cannot be measured under it, * so `--max-old-space` exists and the chosen value is recorded in `run.json`. */ export declare const DEFAULT_MAX_OLD_SPACE_MB = 512; /** * How long each startup wait gets before the route is called failed. * * It was 15 s, shared between waiting for the control channel and waiting for * the app to listen — so a slow channel spent the app's budget too. Both * numbers were also invisible: a real app that needs longer to boot than the * tool was willing to wait had no way to say so, and got * `timed out waiting for app` on every route (#71). 60 s is what a cold * standalone bundle of a couple of thousand modules takes on a laptop with * a busy disk, with room to spare; `--ready-timeout` moves it. */ export declare const DEFAULT_READY_TIMEOUT_MS = 60000; export type LaunchOptions = { /** Absolute path to the standalone `server.js` (or any PORT/HOSTNAME-honoring server). */ serverPath: string; /** Directory for `control.json` and heap snapshots (`NEXT_LEAK_DIR`). */ workDir: string; /** Port the measured app should listen on. */ appPort: number; /** Path to the built bootstrap module loaded with `--import`. */ bootstrapPath: string; hostname?: string; /** * Path the readiness probe asks for. Defaults to `/`, but a run should pass * the route it is about to measure: an app can serve that route perfectly * and still fail on `/` — an i18n redirect, an auth wall, a rewrite — and * the wait would then be judging a page nobody asked about (#74). */ readyPath?: string; maxOldSpaceMb?: number; /** * Budget for each of the two waits below, not for the pair. Default: * `DEFAULT_READY_TIMEOUT_MS`. */ readyTimeoutMs?: number; env?: Record; }; export type LaunchedApp = { pid: number; appPort: number; controlPort: number; /** * Why the measured process is gone, or null while it is alive. Without it * a child that died mid-run surfaces as "fetch failed", which reads like a * bug in the tool and hides the finding — most often that the app blew * through the heap limit the run configured. * * `heapExhausted` separates that one death from every other, because it is * the only one the run is allowed to call a verdict rather than a failure. */ explainExit: () => RuntimeDeath | null; /** SIGTERM, then SIGKILL after a grace period. Resolves when the child exited. */ close: () => Promise; }; /** * How the measured process died. `heapExhausted` is the one death that is a * measurement: the app did not fit in the limit the run gave it, which is the * finding the tool exists to produce. Every other death is a failure. */ export type RuntimeDeath = { reason: string; heapExhausted: boolean; }; export declare class LaunchError extends Error { constructor(message: string); } /** Interrupt safety: no measured-app process may outlive the CLI. */ export declare function killActiveChildren(): void; /** * Puts a process this tool spawned under the same interrupt safety as a * measured app — a build launched by `next-leak build` must not outlive a * Ctrl+C either. */ export declare function registerChild(child: ChildProcess): void; export declare function unregisterChild(child: ChildProcess): void; /** * Turns a stack dump into a sentence when the cause is recognisable. Seen in * the wild: a webpack `output: standalone` build that ships without * `@swc/helpers`, which fails identically when started by hand — the tool is * the messenger, and should say so instead of printing 20 lines of trace. */ export declare function explainStartupFailure(stderr: string): string; /** * Same idea as `explainStartupFailure`, for a process that died *during* a * run. Heap exhaustion is the one death this tool can name outright, and it * is a finding rather than an accident: the app did not fit in the limit the * run gave it. */ /** * Whether a stderr window carries V8's own fatal heap message. The build path * asks the same question of build output in `build-verdict.ts`; the two are * deliberately not shared yet, because `launcher` importing the build verdict * would couple the runtime path to it for one regex. */ export declare function stderrShowsHeapExhaustion(stderr: string): boolean; export declare function explainRuntimeFailure(stderr: string, maxOldSpaceMb: number): string; /** * What to say when the process is alive and answering on its control channel, * but never opened the app port. The old message named the port and stopped * there, which reads like the tool failed to connect to something that was * running. The process is up: what did not happen is the listen. */ export declare function appNeverListened(hostname: string, port: number, budgetMs: number, stderr: string, lastFailure?: string | undefined): string; /** * `fetch` rejects with a bare `TypeError: fetch failed`; what happened is one * level down, and not always as a `code` — a redirect loop arrives as a * message with none. */ export declare function probeFailure(cause: unknown): string; /** Whether a probe failure means the port is not open yet. */ export declare function meansNotListening(failure: string): boolean; /** * Spawns the measured server in a fresh child process with GC exposed and the * control-channel bootstrap preloaded, and waits until both the app port and * the control channel respond. */ export declare function launchInstrumented(options: LaunchOptions): Promise;