next-leak 0.1.0 → 0.1.2

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.
package/dist/cli.js CHANGED
@@ -9,9 +9,9 @@ import {
9
9
  killActiveChildren,
10
10
  parseCliArgs,
11
11
  runMeasurement
12
- } from "./chunk-UZTZWHKH.js";
13
- import "./chunk-K5PZFVJH.js";
14
- import "./chunk-OSZ6ND6E.js";
12
+ } from "./chunk-WHC6S57X.js";
13
+ import "./chunk-2BQZCZ4Z.js";
14
+ import "./chunk-PF7KYD5N.js";
15
15
 
16
16
  // src/cli.ts
17
17
  import { spawn } from "child_process";
@@ -36,45 +36,55 @@ function reexecWithHeadroom() {
36
36
  process.exit(code ?? (signal === "SIGINT" ? 130 : 1));
37
37
  });
38
38
  }
39
- async function main() {
40
- const parsed = parseCliArgs(process.argv.slice(2));
39
+ function installInterruptHandlers() {
40
+ const aborter = new AbortController();
41
+ let interrupts = 0;
42
+ for (const signalName of ["SIGINT", "SIGTERM"]) {
43
+ process.on(signalName, () => {
44
+ interrupts += 1;
45
+ if (interrupts > 1) {
46
+ process.exit(130);
47
+ }
48
+ console.error("\n\xB7 interrupted \u2014 stopping the measured process and writing a partial run.json");
49
+ aborter.abort();
50
+ killActiveChildren();
51
+ });
52
+ }
53
+ return aborter;
54
+ }
55
+ function handleNonRunCommand(parsed) {
41
56
  if (parsed.kind === "version") {
42
57
  console.log(version);
43
- return;
58
+ return true;
44
59
  }
45
60
  if (parsed.kind === "help") {
46
61
  console.log(helpText(version));
47
62
  process.exitCode = process.argv.length > 2 ? 0 : 1;
48
- return;
63
+ return true;
49
64
  }
50
65
  if (parsed.kind === "error") {
51
66
  console.error(`error: ${parsed.message}`);
52
67
  process.exitCode = 1;
53
- return;
68
+ return true;
54
69
  }
55
70
  const guardFailure = checkRuntime();
56
71
  if (guardFailure !== null) {
57
72
  console.error(`error: ${guardFailure}`);
58
73
  process.exitCode = 1;
74
+ return true;
75
+ }
76
+ return false;
77
+ }
78
+ async function main() {
79
+ const parsed = parseCliArgs(process.argv.slice(2));
80
+ if (handleNonRunCommand(parsed) || parsed.kind !== "run") {
59
81
  return;
60
82
  }
61
83
  if (!hasHeapHeadroom()) {
62
84
  reexecWithHeadroom();
63
85
  return;
64
86
  }
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
- }
87
+ const aborter = installInterruptHandlers();
78
88
  const { options } = parsed;
79
89
  const quickPreset = options.quick ? { loadRequests: 2e3, cycles: 4, idleMs: 8e3 } : {};
80
90
  const report = await runMeasurement({
@@ -96,11 +106,13 @@ async function main() {
96
106
  process.exitCode = 130;
97
107
  }
98
108
  }
99
- main().catch((cause) => {
109
+ try {
110
+ await main();
111
+ } catch (cause) {
100
112
  if (cause instanceof TargetError || cause instanceof RouteConfigError) {
101
113
  console.error(`error: ${cause.message}`);
102
114
  } else {
103
115
  console.error(cause);
104
116
  }
105
117
  process.exitCode = 1;
106
- });
118
+ }
@@ -1,7 +1,4 @@
1
1
  import type { HeapSample } from "./control-server.js";
2
- export declare class ControlError extends Error {
3
- constructor(message: string);
4
- }
5
2
  /** Forces GC in the measured process and returns a settled memory sample. */
6
3
  export declare function requestGc(port: number): Promise<HeapSample>;
7
4
  /** Forces GC, writes a named heap snapshot, and returns its path and sample. */
@@ -12,7 +12,6 @@ export type HeapSample = {
12
12
  external: number;
13
13
  arrayBuffers: number;
14
14
  };
15
- export declare function sampleMemory(gcExposed: boolean): HeapSample;
16
15
  export type ControlServerOptions = {
17
16
  /** Directory where heap snapshots are written. */
18
17
  snapshotDir: string;
@@ -1,9 +1,9 @@
1
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
2
  import {
3
3
  renderHtmlReport
4
- } from "./chunk-4BMPC45G.js";
5
- import "./chunk-K5PZFVJH.js";
6
- import "./chunk-OSZ6ND6E.js";
4
+ } from "./chunk-C3KH5Z2W.js";
5
+ import "./chunk-2BQZCZ4Z.js";
6
+ import "./chunk-PF7KYD5N.js";
7
7
  export {
8
8
  renderHtmlReport
9
9
  };
package/dist/index.d.ts CHANGED
@@ -26,7 +26,7 @@ export { captureEnvironment } from "./environment.js";
26
26
  export type { MeasurementEnvironment } from "./environment.js";
27
27
  export { renderHtmlReport } from "./html-report.js";
28
28
  export { renderIssueMarkdown } from "./issue-report.js";
29
- export type { MeasuredRoute } from "./issue-report.js";
29
+ export type { MeasuredRoute } from "./runner.js";
30
30
  export { routeSlug } from "./runner.js";
31
31
  export type { RunParameters } from "./runner.js";
32
32
  export { loadRouteConfig, resolveRoutePath, RouteConfigError, ROUTE_CONFIG_FILE } from "./route-config.js";
package/dist/index.js CHANGED
@@ -40,15 +40,15 @@ import {
40
40
  sourceIndexAt,
41
41
  summarizeBaseline,
42
42
  validateTarget
43
- } from "./chunk-UZTZWHKH.js";
43
+ } from "./chunk-WHC6S57X.js";
44
44
  import {
45
45
  renderHtmlReport
46
- } from "./chunk-4BMPC45G.js";
47
- import "./chunk-K5PZFVJH.js";
46
+ } from "./chunk-C3KH5Z2W.js";
47
+ import "./chunk-2BQZCZ4Z.js";
48
48
  import {
49
49
  renderIssueMarkdown
50
- } from "./chunk-5K4WEMIS.js";
51
- import "./chunk-OSZ6ND6E.js";
50
+ } from "./chunk-5ZYZW2BL.js";
51
+ import "./chunk-PF7KYD5N.js";
52
52
  export {
53
53
  LaunchError,
54
54
  LoadError,
@@ -1,8 +1,8 @@
1
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
2
  import {
3
3
  renderIssueMarkdown
4
- } from "./chunk-5K4WEMIS.js";
5
- import "./chunk-OSZ6ND6E.js";
4
+ } from "./chunk-5ZYZW2BL.js";
5
+ import "./chunk-PF7KYD5N.js";
6
6
  export {
7
7
  renderIssueMarkdown
8
8
  };
@@ -1,7 +1,4 @@
1
- import type { RouteReport, RunReport } from "./runner.js";
2
- export type MeasuredRoute = Extract<RouteReport, {
3
- status: "measured";
4
- }>;
1
+ import type { MeasuredRoute, RunReport } from "./runner.js";
5
2
  /**
6
3
  * Issue draft in the Next.js bug-template section order. The attribution
7
4
  * gates the preamble: app/dependency-owned leaks warn against filing
@@ -1,19 +1,5 @@
1
- import { type LineMapping } from "./vlq.js";
2
1
  /** Bundler module id → source path (e.g. `[project]/src/app/leaky/page.tsx`). */
3
2
  export type ModuleRegistry = ReadonlyMap<number, string>;
4
- export type NormalizedSourceMap = {
5
- sources: string[];
6
- lines: LineMapping[];
7
- };
8
- /** Accepts flat or sectioned sourcemaps; null when it is neither. */
9
- export declare function normalizeSourceMap(raw: unknown): NormalizedSourceMap | null;
10
- /**
11
- * Extracts `[id, factory, ...]` pairs from a Turbopack CJS chunk by walking
12
- * the sourcemap instead of lexing JavaScript: each factory is a contiguous
13
- * region mapped to one source, so a change of source index marks a factory
14
- * boundary, and the module id is the last integer literal in the unmapped
15
- * gap right before it. Non-Turbopack chunks contribute nothing.
16
- */
17
3
  export declare function extractChunkModules(code: string, rawMap: unknown): Map<number, string>;
18
4
  /**
19
5
  * Builds the module registry by scanning every server chunk that has an
@@ -4,7 +4,7 @@ import { z } from "zod";
4
4
  * route params. `params` applies globally; `routes` overrides per route
5
5
  * template (keys as discovered, e.g. `/[lang]/candidate/[candidateId]`).
6
6
  */
7
- export declare const routeConfigSchema: z.ZodObject<{
7
+ declare const routeConfigSchema: z.ZodObject<{
8
8
  params: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodString>>;
9
9
  routes: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodRecord<z.ZodString, z.ZodString>>>;
10
10
  headers: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodString>>;
@@ -34,3 +34,4 @@ export declare const UNIQUE_MARKER = "{n}";
34
34
  * requests that never completed — both verified against a real server.
35
35
  */
36
36
  export declare function resolveRoutePath(routeTemplate: string, config: RouteConfig): string | null;
37
+ export {};
@@ -1,3 +1,2 @@
1
1
  import type { RunReport } from "./runner.js";
2
- /** Canonical report fixture shared by report/html/issue renderer tests. */
3
2
  export declare function makeRunReport(): RunReport;
package/dist/runner.d.ts CHANGED
@@ -106,18 +106,6 @@ export type RunnerDeps = {
106
106
  nextVersion: typeof readNextVersion;
107
107
  };
108
108
  export declare function freePort(): Promise<number>;
109
- /**
110
- * Filesystem-safe label for a route. Distinct routes MUST get distinct slugs:
111
- * `/a/b` and `/a_b` used to collapse onto the same `ISSUE-a_b.md`, and any
112
- * all-non-ASCII path (`/ñ`) became "root", colliding with `/`. A short digest
113
- * disambiguates whenever the sanitized form loses information.
114
- */
115
- /**
116
- * Mean RSS growth per cycle, excluding the warm-up cycle exactly like the
117
- * heap verdict does. Reported alongside the heap so a flat heap with growing
118
- * RSS is visible instead of invisible.
119
- */
120
- export declare function rssTrend(memorySamples: readonly HeapSample[]): number;
121
109
  export declare function routeSlug(route: string): string;
122
110
  /**
123
111
  * Full measurement run: validate the target, discover routes, run the ritual
package/dist/vlq.d.ts CHANGED
@@ -1,4 +1,3 @@
1
- /** Decodes one line of base64-VLQ segments from a sourcemap `mappings` string. */
2
1
  export declare function decodeVlqLine(line: string): number[][];
3
2
  export type LineMapping = {
4
3
  /** Generated column → source index, sorted by column. */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "next-leak",
3
- "version": "0.1.0",
3
+ "version": "0.1.2",
4
4
  "description": "Find out whether your Next.js app actually leaks memory — how much, on which route, and whose fault it is.",
5
5
  "keywords": [
6
6
  "nextjs",
@@ -33,8 +33,7 @@
33
33
  "prepublishOnly": "npm run typecheck && npm run test && npm run build",
34
34
  "pack:smoke": "node scripts/pack-smoke.mjs",
35
35
  "test:mutation": "stryker run",
36
- "prepare": "husky >/dev/null 2>&1 || true",
37
- "release": "release-it"
36
+ "prepare": "husky >/dev/null 2>&1 || true"
38
37
  },
39
38
  "pnpm": {
40
39
  "overrides": {
@@ -46,21 +45,19 @@
46
45
  "@commitlint/config-conventional": "^21.2.0",
47
46
  "@memlab/core": "^2.0.4",
48
47
  "@memlab/heap-analysis": "^2.0.4",
49
- "@release-it/conventional-changelog": "^11.0.1",
50
48
  "@stryker-mutator/core": "^9.6.1",
51
49
  "@stryker-mutator/vitest-runner": "^9.6.1",
52
50
  "@types/autocannon": "^7.12.7",
53
51
  "@types/node": "^26.1.1",
54
52
  "@types/semver": "^7.7.1",
55
53
  "@vitest/coverage-v8": "^4.1.10",
54
+ "autocannon": "^8.0.0",
56
55
  "husky": "^9.1.7",
57
- "release-it": "^20.2.1",
58
56
  "tsup": "^8.5.1",
59
57
  "typescript": "^7.0.2",
60
58
  "vitest": "^4.1.10"
61
59
  },
62
60
  "dependencies": {
63
- "autocannon": "^8.0.0",
64
61
  "semver": "^7.8.5",
65
62
  "zod": "^4.4.3"
66
63
  },
@@ -1,103 +0,0 @@
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
-
3
- // src/confidence.ts
4
- function effectiveVerdict(report) {
5
- return report.confidence.supersededVerdict ?? report.trend.verdict;
6
- }
7
- var VERDICT_WEAKENING = /* @__PURE__ */ new Set([
8
- "near-threshold",
9
- "spiky-growth"
10
- ]);
11
- function warrantsIssueDraft(report) {
12
- return effectiveVerdict(report) === "leak" && !report.confidence.warnings.some((warning) => VERDICT_WEAKENING.has(warning.code));
13
- }
14
- var DEFAULT_MIN_GROWTH = 256 * 1024;
15
- var LOAD_COMPLETION_FLOOR = 0.99;
16
- var ABANDON_EFFECTIVE_FLOOR = 0.9;
17
- var MID_STREAM_FLOOR = 0.1;
18
- var SPIKE_RATIO = 4;
19
- var NOISE_FLOOR_MULTIPLE = 2;
20
- var mb = (bytes) => `${(bytes / (1024 * 1024)).toFixed(2)} MB`;
21
- var pct = (part, whole) => `${(part / whole * 100).toFixed(1)}%`;
22
- function assessConfidence(input) {
23
- const warnings = [];
24
- const minGrowth = input.minGrowthPerCycle ?? DEFAULT_MIN_GROWTH;
25
- const isLeak = input.trend.verdict === "leak";
26
- const moving = input.settleOutcomes.filter((outcome) => outcome.status === "moving");
27
- if (moving.length > 0) {
28
- const phases = moving.map((outcome) => outcome.phase).join(", ");
29
- warnings.push({
30
- code: "unsettled",
31
- detail: `the heap never held steady before sampling on ${phases} \u2014 raise --idle-ms so post-load transients finish draining`
32
- });
33
- }
34
- const unverified = input.settleOutcomes.filter((outcome) => outcome.status === "unknown");
35
- if (unverified.length > 0) {
36
- const phases = unverified.map((outcome) => outcome.phase).join(", ");
37
- warnings.push({
38
- code: "settle-unverified",
39
- detail: `the idle budget was too short to check whether the heap had settled on ${phases} \u2014 the samples may include post-load transients`
40
- });
41
- }
42
- for (const outcome of input.loadOutcomes) {
43
- if (input.abandonAfterMs !== void 0) {
44
- const abandoned = outcome.abandoned ?? 0;
45
- if (outcome.sent > 0 && abandoned < outcome.sent * ABANDON_EFFECTIVE_FLOOR) {
46
- warnings.push({
47
- code: "abandon-ineffective",
48
- detail: `${outcome.phase} disconnected early on only ${abandoned} of ${outcome.sent} requests (${pct(abandoned, outcome.sent)}) \u2014 the early-disconnect path was largely not exercised`
49
- });
50
- continue;
51
- }
52
- const midStream = outcome.abandonedMidStream ?? 0;
53
- if (abandoned > 0 && midStream < abandoned * MID_STREAM_FLOOR) {
54
- warnings.push({
55
- code: "abandon-before-response",
56
- detail: `${outcome.phase} cut ${abandoned} requests before the server sent anything (${midStream} mid-stream) \u2014 this tested pre-response disconnects, not mid-stream teardown; raise abandonAfterMs above the route's time-to-first-byte`
57
- });
58
- }
59
- continue;
60
- }
61
- const landed = outcome.ok2xx ?? 0;
62
- if (outcome.sent > 0 && landed < outcome.sent * LOAD_COMPLETION_FLOOR) {
63
- warnings.push({
64
- code: "load-incomplete",
65
- detail: `${outcome.phase} landed ${landed} of ${outcome.sent} requests (${pct(landed, outcome.sent)}) \u2014 the route saw less traffic than reported`
66
- });
67
- }
68
- }
69
- const deltas = input.trend.deltas;
70
- if ((isLeak || input.trend.verdict === "inconclusive") && deltas.length >= 2) {
71
- const positive = deltas.filter((delta) => delta > 0);
72
- if (positive.length === deltas.length) {
73
- const smallest = Math.min(...positive);
74
- const largest = Math.max(...positive);
75
- if (largest > smallest * SPIKE_RATIO) {
76
- warnings.push({
77
- code: "spiky-growth",
78
- detail: `one cycle grew ${mb(largest)} and another ${mb(smallest)} \u2014 the mean of ${mb(input.trend.growthPerCycle)}/cycle summarizes an uneven series; measure more cycles before quoting it`
79
- });
80
- }
81
- }
82
- }
83
- if (isLeak && input.trend.growthPerCycle < minGrowth * NOISE_FLOOR_MULTIPLE) {
84
- warnings.push({
85
- code: "near-threshold",
86
- detail: `growth of ${mb(input.trend.growthPerCycle)}/cycle barely clears the ${mb(minGrowth)} threshold \u2014 raise --load-requests so the signal outgrows the noise`
87
- });
88
- }
89
- const neverSettled = input.settleOutcomes.length > 0 && moving.length === input.settleOutcomes.length;
90
- const abandonedNothing = input.abandonAfterMs !== void 0 && input.loadOutcomes.length > 0 && input.loadOutcomes.every((outcome) => (outcome.abandoned ?? 0) === 0);
91
- const invalid = isLeak && (neverSettled || abandonedNothing);
92
- return {
93
- level: warnings.length === 0 ? "high" : "low",
94
- warnings,
95
- ...invalid && { supersededVerdict: "inconclusive" }
96
- };
97
- }
98
-
99
- export {
100
- effectiveVerdict,
101
- warrantsIssueDraft,
102
- assessConfidence
103
- };