next-leak 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,24 @@
1
+ export type CliRunOptions = {
2
+ appDir: string;
3
+ routes: string[] | null;
4
+ cycles: number | null;
5
+ requests: number | null;
6
+ connections: number | null;
7
+ idleSeconds: number | null;
8
+ quick: boolean;
9
+ diffAll: boolean;
10
+ output: string | null;
11
+ };
12
+ export type ParsedCli = {
13
+ kind: "run";
14
+ options: CliRunOptions;
15
+ } | {
16
+ kind: "help";
17
+ } | {
18
+ kind: "version";
19
+ } | {
20
+ kind: "error";
21
+ message: string;
22
+ };
23
+ export declare function helpText(version: string): string;
24
+ export declare function parseCliArgs(argv: string[]): ParsedCli;
package/dist/cli.d.ts ADDED
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
package/dist/cli.js ADDED
@@ -0,0 +1,106 @@
1
+ #!/usr/bin/env node
2
+ import { createRequire as __nextLeakCreateRequire } from 'node:module';import { fileURLToPath as __nextLeakFileURLToPath } from 'node:url';import { dirname as __nextLeakDirname } from 'node:path';const require = __nextLeakCreateRequire(import.meta.url);const __filename = __nextLeakFileURLToPath(import.meta.url);const __dirname = __nextLeakDirname(__filename);
3
+ import {
4
+ RouteConfigError,
5
+ TargetError,
6
+ checkRuntime,
7
+ formatReport,
8
+ helpText,
9
+ killActiveChildren,
10
+ parseCliArgs,
11
+ runMeasurement
12
+ } from "./chunk-UZTZWHKH.js";
13
+ import "./chunk-K5PZFVJH.js";
14
+ import "./chunk-OSZ6ND6E.js";
15
+
16
+ // src/cli.ts
17
+ import { spawn } from "child_process";
18
+ import { createRequire } from "module";
19
+ import { fileURLToPath } from "url";
20
+ var require2 = createRequire(import.meta.url);
21
+ var { version } = require2("../package.json");
22
+ var HEADROOM_SENTINEL = "NEXT_LEAK_WITH_HEADROOM";
23
+ var HEADROOM_MB = 8192;
24
+ function hasHeapHeadroom() {
25
+ return process.execArgv.some((argument) => argument.includes("--max-old-space-size")) || (process.env.NODE_OPTIONS ?? "").includes("--max-old-space-size") || process.env[HEADROOM_SENTINEL] === "1";
26
+ }
27
+ function reexecWithHeadroom() {
28
+ const child = spawn(process.execPath, [`--max-old-space-size=${HEADROOM_MB}`, ...process.argv.slice(1)], {
29
+ stdio: "inherit",
30
+ env: { ...process.env, [HEADROOM_SENTINEL]: "1" }
31
+ });
32
+ for (const signalName of ["SIGINT", "SIGTERM"]) {
33
+ process.on(signalName, () => child.kill(signalName));
34
+ }
35
+ child.once("exit", (code, signal) => {
36
+ process.exit(code ?? (signal === "SIGINT" ? 130 : 1));
37
+ });
38
+ }
39
+ async function main() {
40
+ const parsed = parseCliArgs(process.argv.slice(2));
41
+ if (parsed.kind === "version") {
42
+ console.log(version);
43
+ return;
44
+ }
45
+ if (parsed.kind === "help") {
46
+ console.log(helpText(version));
47
+ process.exitCode = process.argv.length > 2 ? 0 : 1;
48
+ return;
49
+ }
50
+ if (parsed.kind === "error") {
51
+ console.error(`error: ${parsed.message}`);
52
+ process.exitCode = 1;
53
+ return;
54
+ }
55
+ const guardFailure = checkRuntime();
56
+ if (guardFailure !== null) {
57
+ console.error(`error: ${guardFailure}`);
58
+ process.exitCode = 1;
59
+ return;
60
+ }
61
+ if (!hasHeapHeadroom()) {
62
+ reexecWithHeadroom();
63
+ return;
64
+ }
65
+ const aborter = new AbortController();
66
+ let interrupts = 0;
67
+ for (const signalName of ["SIGINT", "SIGTERM"]) {
68
+ process.on(signalName, () => {
69
+ interrupts += 1;
70
+ if (interrupts > 1) {
71
+ process.exit(130);
72
+ }
73
+ console.error("\n\xB7 interrupted \u2014 stopping the measured process and writing a partial run.json");
74
+ aborter.abort();
75
+ killActiveChildren();
76
+ });
77
+ }
78
+ const { options } = parsed;
79
+ const quickPreset = options.quick ? { loadRequests: 2e3, cycles: 4, idleMs: 8e3 } : {};
80
+ const report = await runMeasurement({
81
+ appDir: options.appDir,
82
+ bootstrapPath: fileURLToPath(new URL("./bootstrap.js", import.meta.url)),
83
+ signal: aborter.signal,
84
+ ...quickPreset,
85
+ ...options.routes !== null && { routeFilter: options.routes },
86
+ ...options.cycles !== null && { cycles: options.cycles },
87
+ ...options.requests !== null && { loadRequests: options.requests },
88
+ ...options.connections !== null && { connections: options.connections },
89
+ ...options.idleSeconds !== null && { idleMs: options.idleSeconds * 1e3 },
90
+ ...options.diffAll && { diffAll: true },
91
+ ...options.output !== null && { outputDir: options.output },
92
+ onProgress: (message) => console.error(`\xB7 ${message}`)
93
+ });
94
+ console.log(formatReport(report));
95
+ if (aborter.signal.aborted) {
96
+ process.exitCode = 130;
97
+ }
98
+ }
99
+ main().catch((cause) => {
100
+ if (cause instanceof TargetError || cause instanceof RouteConfigError) {
101
+ console.error(`error: ${cause.message}`);
102
+ } else {
103
+ console.error(cause);
104
+ }
105
+ process.exitCode = 1;
106
+ });
@@ -0,0 +1,78 @@
1
+ import type { LoadOutcome, SettleOutcome } from "./ritual.js";
2
+ import type { TrendResult, TrendVerdict } from "./trend.js";
3
+ /**
4
+ * Why a measurement may not support its own verdict.
5
+ *
6
+ * `unsettled` — the heap was still moving when sampled
7
+ * `settle-unverified` — the idle budget was too short to check
8
+ * `load-incomplete` — fewer requests landed than were asked for
9
+ * `abandon-ineffective` — early-disconnect run that disconnected nothing
10
+ * `abandon-before-response` — cut before the server sent a byte, so the
11
+ * mid-stream teardown path was never reached
12
+ * `spiky-growth` — one cycle dominates, so the mean describes little
13
+ * `near-threshold` — growth barely clears the noise floor
14
+ */
15
+ export type WarningCode = "unsettled" | "settle-unverified" | "load-incomplete" | "abandon-ineffective" | "abandon-before-response" | "spiky-growth" | "near-threshold";
16
+ export type MeasurementWarning = {
17
+ code: WarningCode;
18
+ detail: string;
19
+ };
20
+ export type ConfidenceReport = {
21
+ level: "high" | "low";
22
+ warnings: MeasurementWarning[];
23
+ /**
24
+ * Verdict the evidence actually supports, when the measurement is not merely
25
+ * noisy but invalid. Only ever downgrades `leak`: accusing an app of leaking
26
+ * on evidence that does not hold is the expensive error — it sends someone
27
+ * chasing a ghost and ends as an issue against this tool.
28
+ */
29
+ supersededVerdict?: TrendVerdict;
30
+ };
31
+ export type ConfidenceInput = {
32
+ trend: TrendResult;
33
+ loadOutcomes: readonly LoadOutcome[];
34
+ settleOutcomes: readonly SettleOutcome[];
35
+ /** Set when the run asked for early disconnects. */
36
+ abandonAfterMs?: number;
37
+ /** Threshold the verdict used, for the noise-floor check. */
38
+ minGrowthPerCycle?: number;
39
+ };
40
+ /**
41
+ * The verdict a route's evidence actually supports.
42
+ *
43
+ * `trend.verdict` stays exactly as measured — the raw record must survive — so
44
+ * every consumer that shows a verdict to a human reads it through here
45
+ * instead, or it will report a leak the audit already withdrew.
46
+ *
47
+ * Structurally typed on purpose: it lives here, next to the audit, so the
48
+ * reporters can reach it without importing the runner (and, through it,
49
+ * memlab) just to render a line of text.
50
+ */
51
+ export declare function effectiveVerdict(report: {
52
+ trend: TrendResult;
53
+ confidence: ConfidenceReport;
54
+ }): TrendVerdict;
55
+ /**
56
+ * Whether a route's evidence is solid enough to draft an issue for.
57
+ *
58
+ * Stricter than the verdict on purpose: a draft is written to be pasted into
59
+ * someone else's tracker, so it needs a leak that is plain, not one that
60
+ * merely cleared the threshold. Measuring a healthy route on a real app
61
+ * (`/server-plp`, 4 cycles × 2000 requests) produced deltas of
62
+ * [0.9, 0.25, 0.33] MB and a draft; at 8 cycles × 5000 the same route
63
+ * oscillated around a flat 39 MB and was plainly stable.
64
+ */
65
+ export declare function warrantsIssueDraft(report: {
66
+ trend: TrendResult;
67
+ confidence: ConfidenceReport;
68
+ }): boolean;
69
+ /**
70
+ * Audits a route measurement against its own evidence.
71
+ *
72
+ * A leak detector is an instrument, and a miscalibrated instrument does not
73
+ * fail loudly — it reports confident, wrong numbers. Two implementations of
74
+ * early disconnects shipped in this repo that abandoned nothing, and both
75
+ * produced a verdict indistinguishable from the correct one; only the audit
76
+ * trail caught them. This turns that trail into a check that runs every time.
77
+ */
78
+ export declare function assessConfidence(input: ConfidenceInput): ConfidenceReport;
@@ -0,0 +1,11 @@
1
+ import type { HeapSample } from "./control-server.js";
2
+ export declare class ControlError extends Error {
3
+ constructor(message: string);
4
+ }
5
+ /** Forces GC in the measured process and returns a settled memory sample. */
6
+ export declare function requestGc(port: number): Promise<HeapSample>;
7
+ /** Forces GC, writes a named heap snapshot, and returns its path and sample. */
8
+ export declare function requestSnapshot(port: number, name: string): Promise<{
9
+ file: string;
10
+ sample: HeapSample;
11
+ }>;
@@ -0,0 +1,33 @@
1
+ /**
2
+ * Forces garbage collection. A single pass is not enough to settle the heap;
3
+ * phase-0 measurements used 3 passes separated by event-loop ticks so that
4
+ * finalizers and pending callbacks can release references between passes.
5
+ * Returns false when the process was not started with `--expose-gc`.
6
+ */
7
+ export declare function forceGc(passes?: number): Promise<boolean>;
8
+ export type HeapSample = {
9
+ gcExposed: boolean;
10
+ heapUsed: number;
11
+ rss: number;
12
+ external: number;
13
+ arrayBuffers: number;
14
+ };
15
+ export declare function sampleMemory(gcExposed: boolean): HeapSample;
16
+ export type ControlServerOptions = {
17
+ /** Directory where heap snapshots are written. */
18
+ snapshotDir: string;
19
+ /** Injectable for tests; defaults to `v8.writeHeapSnapshot`. */
20
+ writeSnapshot?: (file: string) => string;
21
+ };
22
+ export type ControlServer = {
23
+ port: number;
24
+ close: () => Promise<void>;
25
+ };
26
+ /**
27
+ * Internal control channel booted inside the measured app's process.
28
+ *
29
+ * - `GET /gc` — force GC, respond with a memory sample.
30
+ * - `GET /snapshot?name=<label>` — force GC, write `<label>.heapsnapshot`
31
+ * into `snapshotDir`, respond `{ file, sample }` only once fully written.
32
+ */
33
+ export declare function startControlServer(options: ControlServerOptions): Promise<ControlServer>;
@@ -0,0 +1,11 @@
1
+ export type MeasurementEnvironment = {
2
+ nodeVersion: string;
3
+ platform: string;
4
+ arch: string;
5
+ cpuModel: string | null;
6
+ totalMemoryBytes: number;
7
+ nextVersion: string | null;
8
+ nextLeakVersion: string;
9
+ };
10
+ /** A report without environment info is not reproducible — capture it once per run. */
11
+ export declare function captureEnvironment(nextVersion: string | null): MeasurementEnvironment;
@@ -0,0 +1,9 @@
1
+ export type RuntimeFacts = {
2
+ nodeMajor: number;
3
+ platform: NodeJS.Platform;
4
+ };
5
+ /**
6
+ * Startup guards — fail with an actionable message before any process is
7
+ * spawned, instead of failing mid-run with an unrelated error.
8
+ */
9
+ export declare function checkRuntime(facts?: RuntimeFacts): string | null;
@@ -0,0 +1,96 @@
1
+ /**
2
+ * Minimal structural view of a parsed heap snapshot. `@memlab`'s
3
+ * IHeapSnapshot satisfies it; tests use small hand-built graphs.
4
+ */
5
+ export type HeapEdgeLike = {
6
+ type: string;
7
+ name_or_index: string | number;
8
+ fromNode: HeapNodeLike;
9
+ };
10
+ export type HeapNodeLike = {
11
+ id: number;
12
+ type: string;
13
+ name: string;
14
+ self_size: number;
15
+ retainedSize: number;
16
+ referrers: HeapEdgeLike[];
17
+ /** Outgoing edges; optional because test fixtures rarely need them. */
18
+ references?: Array<{
19
+ name_or_index: string | number;
20
+ }>;
21
+ };
22
+ export type HeapLike = {
23
+ nodes: {
24
+ forEach(callback: (node: HeapNodeLike) => void): void;
25
+ };
26
+ };
27
+ export type DiffOptions = {
28
+ /** Type-level deltas smaller than this are dropped. Default 20 KiB. */
29
+ minTypeDeltaBytes?: number;
30
+ /** Retained-size growth for an existing node to be reported. Default 100 KiB. */
31
+ grownThresholdBytes?: number;
32
+ /** Retained size for a new node to be considered. Default 2 KiB. */
33
+ newThresholdBytes?: number;
34
+ /**
35
+ * Baseline nodes with retained size below this floor are not tracked for
36
+ * growth — the memory guard that keeps the baseline summary small.
37
+ * Default 50 KiB.
38
+ */
39
+ bigRetainedFloorBytes?: number;
40
+ maxFindings?: number;
41
+ chainDepth?: number;
42
+ };
43
+ export type TypeDelta = {
44
+ type: string;
45
+ deltaBytes: number;
46
+ };
47
+ export type NodeFinding = {
48
+ kind: "grown" | "new";
49
+ nodeType: string;
50
+ name: string;
51
+ /** Retained-size delta for grown nodes; absolute retained size for new ones. */
52
+ retainedBytes: number;
53
+ retainerChain: string;
54
+ /** Bundler module ids seen along the chain (needs `resolveNumeric`). */
55
+ moduleIds: number[];
56
+ };
57
+ export type HeapDiff = {
58
+ typeDeltas: TypeDelta[];
59
+ grownNodes: NodeFinding[];
60
+ newNodes: NodeFinding[];
61
+ };
62
+ /**
63
+ * Compact summary of a baseline heap. This is all that stays resident after
64
+ * the baseline snapshot is parsed — never the heap itself.
65
+ */
66
+ export type BaselineSummary = {
67
+ nodeIds: Set<number>;
68
+ bigRetained: Map<number, number>;
69
+ typeSelfSizes: Map<string, number>;
70
+ };
71
+ export declare function summarizeBaseline(heap: HeapLike, options?: DiffOptions): BaselineSummary;
72
+ /**
73
+ * Walks referrers upward preferring strong, non-synthetic edges and refusing
74
+ * to revisit nodes, producing a single human-readable ownership chain.
75
+ */
76
+ export declare function retainerChain(node: HeapNodeLike, depth: number): string;
77
+ export declare function diffAgainstBaseline(baseline: BaselineSummary, after: HeapLike, options?: DiffOptions): HeapDiff;
78
+ export declare class SnapshotError extends Error {
79
+ constructor(message: string);
80
+ }
81
+ /**
82
+ * Cheap structural check before handing a file to memlab.
83
+ *
84
+ * memlab does not throw on malformed input — it calls `process.exit(1)`,
85
+ * which no try/catch can intercept. Without this guard a truncated snapshot
86
+ * (disk full, process killed mid-write) killed the CLI outright and took a
87
+ * multi-hour run's results with it. Reads O(1) bytes, not the whole file.
88
+ */
89
+ export declare function assertReadableSnapshot(file: string): Promise<void>;
90
+ export type HeapLoader = (file: string) => Promise<HeapLike>;
91
+ /**
92
+ * Diffs two snapshot files parsing them strictly sequentially: the baseline
93
+ * heap is reduced to its compact summary and released before the after heap
94
+ * is parsed, so at most one full heap graph is resident at any time.
95
+ */
96
+ export declare function diffSnapshotFiles(baselineFile: string, afterFile: string, options?: DiffOptions, loadHeap?: HeapLoader): Promise<HeapDiff>;
@@ -0,0 +1,9 @@
1
+ import { createRequire as __nextLeakCreateRequire } from 'node:module';import { fileURLToPath as __nextLeakFileURLToPath } from 'node:url';import { dirname as __nextLeakDirname } from 'node:path';const require = __nextLeakCreateRequire(import.meta.url);const __filename = __nextLeakFileURLToPath(import.meta.url);const __dirname = __nextLeakDirname(__filename);
2
+ import {
3
+ renderHtmlReport
4
+ } from "./chunk-4BMPC45G.js";
5
+ import "./chunk-K5PZFVJH.js";
6
+ import "./chunk-OSZ6ND6E.js";
7
+ export {
8
+ renderHtmlReport
9
+ };
@@ -0,0 +1,3 @@
1
+ import type { RunReport } from "./runner.js";
2
+ /** Self-contained report page: renders offline, from file://, no requests. */
3
+ export declare function renderHtmlReport(run: RunReport): string;
@@ -0,0 +1,35 @@
1
+ export { classifyTrend } from "./trend.js";
2
+ export type { TrendOptions, TrendResult, TrendVerdict } from "./trend.js";
3
+ export { discoverRoutes, appPathsManifestSchema, routesManifestSchema } from "./manifests.js";
4
+ export type { AppPathsManifest, DiscoveredRoute, RouteKind, RoutesManifest } from "./manifests.js";
5
+ export { validateTarget, TargetError } from "./target.js";
6
+ export type { TargetErrorCode, ValidatedTarget } from "./target.js";
7
+ export { runLoadPhase, LoadError } from "./load.js";
8
+ export type { LoadPhaseOptions, LoadPhaseResult } from "./load.js";
9
+ export { runRitual } from "./ritual.js";
10
+ export type { RitualDeps, RitualOptions, RitualResult } from "./ritual.js";
11
+ export { launchInstrumented, LaunchError } from "./launcher.js";
12
+ export type { LaunchedApp, LaunchOptions } from "./launcher.js";
13
+ export { diffSnapshotFiles, diffAgainstBaseline, summarizeBaseline, retainerChain, assertReadableSnapshot, SnapshotError } from "./heap-diff.js";
14
+ export type { DiffOptions, HeapDiff, HeapLike, NodeFinding, TypeDelta } from "./heap-diff.js";
15
+ export { runMeasurement, freePort } from "./runner.js";
16
+ export type { RouteReport, RunOptions, RunnerDeps, RunReport } from "./runner.js";
17
+ export { formatReport } from "./report.js";
18
+ export { extractModuleRegistry, extractChunkModules } from "./module-registry.js";
19
+ export type { ModuleRegistry } from "./module-registry.js";
20
+ export { decodeMappings, decodeVlqLine, sourceIndexAt } from "./vlq.js";
21
+ export { attributeDiff, attributeFinding, classifySource } from "./attribution.js";
22
+ export type { AttributedDiff, FindingAttribution, Owner, RouteAttribution } from "./attribution.js";
23
+ export { loadSignatures, matchSignatures, readNextVersion } from "./signatures.js";
24
+ export type { MatchedSignature, Signature } from "./signatures.js";
25
+ export { captureEnvironment } from "./environment.js";
26
+ export type { MeasurementEnvironment } from "./environment.js";
27
+ export { renderHtmlReport } from "./html-report.js";
28
+ export { renderIssueMarkdown } from "./issue-report.js";
29
+ export type { MeasuredRoute } from "./issue-report.js";
30
+ export { routeSlug } from "./runner.js";
31
+ export type { RunParameters } from "./runner.js";
32
+ export { loadRouteConfig, resolveRoutePath, RouteConfigError, ROUTE_CONFIG_FILE } from "./route-config.js";
33
+ export type { RouteConfig } from "./route-config.js";
34
+ export { checkRuntime } from "./guards.js";
35
+ export { parseCliArgs, helpText } from "./cli-args.js";
package/dist/index.js ADDED
@@ -0,0 +1,95 @@
1
+ import { createRequire as __nextLeakCreateRequire } from 'node:module';import { fileURLToPath as __nextLeakFileURLToPath } from 'node:url';import { dirname as __nextLeakDirname } from 'node:path';const require = __nextLeakCreateRequire(import.meta.url);const __filename = __nextLeakFileURLToPath(import.meta.url);const __dirname = __nextLeakDirname(__filename);
2
+ import {
3
+ LaunchError,
4
+ LoadError,
5
+ ROUTE_CONFIG_FILE,
6
+ RouteConfigError,
7
+ SnapshotError,
8
+ TargetError,
9
+ appPathsManifestSchema,
10
+ assertReadableSnapshot,
11
+ attributeDiff,
12
+ attributeFinding,
13
+ captureEnvironment,
14
+ checkRuntime,
15
+ classifySource,
16
+ classifyTrend,
17
+ decodeMappings,
18
+ decodeVlqLine,
19
+ diffAgainstBaseline,
20
+ diffSnapshotFiles,
21
+ discoverRoutes,
22
+ extractChunkModules,
23
+ extractModuleRegistry,
24
+ formatReport,
25
+ freePort,
26
+ helpText,
27
+ launchInstrumented,
28
+ loadRouteConfig,
29
+ loadSignatures,
30
+ matchSignatures,
31
+ parseCliArgs,
32
+ readNextVersion,
33
+ resolveRoutePath,
34
+ retainerChain,
35
+ routeSlug,
36
+ routesManifestSchema,
37
+ runLoadPhase,
38
+ runMeasurement,
39
+ runRitual,
40
+ sourceIndexAt,
41
+ summarizeBaseline,
42
+ validateTarget
43
+ } from "./chunk-UZTZWHKH.js";
44
+ import {
45
+ renderHtmlReport
46
+ } from "./chunk-4BMPC45G.js";
47
+ import "./chunk-K5PZFVJH.js";
48
+ import {
49
+ renderIssueMarkdown
50
+ } from "./chunk-5K4WEMIS.js";
51
+ import "./chunk-OSZ6ND6E.js";
52
+ export {
53
+ LaunchError,
54
+ LoadError,
55
+ ROUTE_CONFIG_FILE,
56
+ RouteConfigError,
57
+ SnapshotError,
58
+ TargetError,
59
+ appPathsManifestSchema,
60
+ assertReadableSnapshot,
61
+ attributeDiff,
62
+ attributeFinding,
63
+ captureEnvironment,
64
+ checkRuntime,
65
+ classifySource,
66
+ classifyTrend,
67
+ decodeMappings,
68
+ decodeVlqLine,
69
+ diffAgainstBaseline,
70
+ diffSnapshotFiles,
71
+ discoverRoutes,
72
+ extractChunkModules,
73
+ extractModuleRegistry,
74
+ formatReport,
75
+ freePort,
76
+ helpText,
77
+ launchInstrumented,
78
+ loadRouteConfig,
79
+ loadSignatures,
80
+ matchSignatures,
81
+ parseCliArgs,
82
+ readNextVersion,
83
+ renderHtmlReport,
84
+ renderIssueMarkdown,
85
+ resolveRoutePath,
86
+ retainerChain,
87
+ routeSlug,
88
+ routesManifestSchema,
89
+ runLoadPhase,
90
+ runMeasurement,
91
+ runRitual,
92
+ sourceIndexAt,
93
+ summarizeBaseline,
94
+ validateTarget
95
+ };
@@ -0,0 +1,8 @@
1
+ import { createRequire as __nextLeakCreateRequire } from 'node:module';import { fileURLToPath as __nextLeakFileURLToPath } from 'node:url';import { dirname as __nextLeakDirname } from 'node:path';const require = __nextLeakCreateRequire(import.meta.url);const __filename = __nextLeakFileURLToPath(import.meta.url);const __dirname = __nextLeakDirname(__filename);
2
+ import {
3
+ renderIssueMarkdown
4
+ } from "./chunk-5K4WEMIS.js";
5
+ import "./chunk-OSZ6ND6E.js";
6
+ export {
7
+ renderIssueMarkdown
8
+ };
@@ -0,0 +1,10 @@
1
+ import type { RouteReport, RunReport } from "./runner.js";
2
+ export type MeasuredRoute = Extract<RouteReport, {
3
+ status: "measured";
4
+ }>;
5
+ /**
6
+ * Issue draft in the Next.js bug-template section order. The attribution
7
+ * gates the preamble: app/dependency-owned leaks warn against filing
8
+ * upstream; framework/unattributed leaks read as an upstream-ready draft.
9
+ */
10
+ export declare function renderIssueMarkdown(route: MeasuredRoute, run: RunReport): string;
@@ -0,0 +1,39 @@
1
+ export type LaunchOptions = {
2
+ /** Absolute path to the standalone `server.js` (or any PORT/HOSTNAME-honoring server). */
3
+ serverPath: string;
4
+ /** Directory for `control.json` and heap snapshots (`NEXT_LEAK_DIR`). */
5
+ workDir: string;
6
+ /** Port the measured app should listen on. */
7
+ appPort: number;
8
+ /** Path to the built bootstrap module loaded with `--import`. */
9
+ bootstrapPath: string;
10
+ hostname?: string;
11
+ maxOldSpaceMb?: number;
12
+ readyTimeoutMs?: number;
13
+ env?: Record<string, string>;
14
+ };
15
+ export type LaunchedApp = {
16
+ pid: number;
17
+ appPort: number;
18
+ controlPort: number;
19
+ /** SIGTERM, then SIGKILL after a grace period. Resolves when the child exited. */
20
+ close: () => Promise<void>;
21
+ };
22
+ export declare class LaunchError extends Error {
23
+ constructor(message: string);
24
+ }
25
+ /** Interrupt safety: no measured-app process may outlive the CLI. */
26
+ export declare function killActiveChildren(): void;
27
+ /**
28
+ * Turns a stack dump into a sentence when the cause is recognisable. Seen in
29
+ * the wild: a webpack `output: standalone` build that ships without
30
+ * `@swc/helpers`, which fails identically when started by hand — the tool is
31
+ * the messenger, and should say so instead of printing 20 lines of trace.
32
+ */
33
+ export declare function explainStartupFailure(stderr: string): string;
34
+ /**
35
+ * Spawns the measured server in a fresh child process with GC exposed and the
36
+ * control-channel bootstrap preloaded, and waits until both the app port and
37
+ * the control channel respond.
38
+ */
39
+ export declare function launchInstrumented(options: LaunchOptions): Promise<LaunchedApp>;
package/dist/load.d.ts ADDED
@@ -0,0 +1,34 @@
1
+ export type LoadPhaseOptions = {
2
+ url: string;
3
+ /** Total number of requests to send. */
4
+ amount: number;
5
+ connections: number;
6
+ /** Sent with every request (compression, cookies, auth). */
7
+ headers?: Record<string, string>;
8
+ /**
9
+ * Give up on each request after this many milliseconds, emulating clients
10
+ * that disconnect before the response arrives. Abandoned requests are
11
+ * expected, so they do not count against the error budget.
12
+ */
13
+ abandonAfterMs?: number;
14
+ /**
15
+ * Maximum tolerated ratio of non-2xx responses plus socket errors before
16
+ * the phase fails. A route that errors under load must fail the run, not
17
+ * silently measure garbage. Default: 0.01 (1%).
18
+ */
19
+ maxErrorRatio?: number;
20
+ };
21
+ export type LoadPhaseResult = {
22
+ sent: number;
23
+ ok2xx: number;
24
+ non2xx: number;
25
+ errors: number;
26
+ timeouts: number;
27
+ durationSeconds: number;
28
+ };
29
+ export declare class LoadError extends Error {
30
+ readonly result: LoadPhaseResult;
31
+ constructor(message: string, result: LoadPhaseResult);
32
+ }
33
+ /** Runs one bounded load phase and fails when the error budget is exceeded. */
34
+ export declare function runLoadPhase(options: LoadPhaseOptions): Promise<LoadPhaseResult>;
@@ -0,0 +1,44 @@
1
+ import { z } from "zod";
2
+ /** `.next/server/app-paths-manifest.json`: app path → server bundle. */
3
+ export declare const appPathsManifestSchema: z.ZodRecord<z.ZodString, z.ZodString>;
4
+ export type AppPathsManifest = z.infer<typeof appPathsManifestSchema>;
5
+ /** `.next/server/pages-manifest.json`: page path → server bundle. */
6
+ export declare const pagesManifestSchema: z.ZodRecord<z.ZodString, z.ZodString>;
7
+ export type PagesManifest = z.infer<typeof pagesManifestSchema>;
8
+ /** `.next/routes-manifest.json` — only the fields the tool relies on. */
9
+ export declare const routesManifestSchema: z.ZodObject<{
10
+ version: z.ZodNumber;
11
+ basePath: z.ZodString;
12
+ staticRoutes: z.ZodArray<z.ZodObject<{
13
+ page: z.ZodString;
14
+ regex: z.ZodString;
15
+ }, z.core.$loose>>;
16
+ dynamicRoutes: z.ZodArray<z.ZodObject<{
17
+ page: z.ZodString;
18
+ regex: z.ZodString;
19
+ }, z.core.$loose>>;
20
+ }, z.core.$loose>;
21
+ export type RoutesManifest = z.infer<typeof routesManifestSchema>;
22
+ export type RouteKind = "page" | "route-handler";
23
+ export type DiscoveredRoute = {
24
+ /** Request path, e.g. "/" or "/products/[id]". */
25
+ path: string;
26
+ kind: RouteKind;
27
+ /** True when the path needs sample param values before it can be requested. */
28
+ dynamic: boolean;
29
+ /** Set when the route exists but cannot be requested directly. */
30
+ unaddressableReason?: string;
31
+ };
32
+ /**
33
+ * Routes from a Pages Router build.
34
+ *
35
+ * Server-side leaks are not an App Router exclusive — vercel/next.js#95094
36
+ * leaks through Pages middleware — and refusing to read this manifest made
37
+ * next-leak fail with a raw ENOENT on any Pages-only app.
38
+ *
39
+ * Statically prerendered pages (`.html` bundles) are kept deliberately: they
40
+ * still travel the server's request path, so a leak there is a real finding,
41
+ * and dropping routes silently is worse than measuring a cheap one.
42
+ */
43
+ export declare function discoverPagesRoutes(pages: PagesManifest): DiscoveredRoute[];
44
+ export declare function discoverRoutes(appPaths: AppPathsManifest): DiscoveredRoute[];