next-leak 0.5.0 → 0.6.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.
- package/README.md +116 -18
- package/dist/build-report.d.ts +11 -0
- package/dist/build-run.d.ts +58 -0
- package/dist/build-target.d.ts +13 -0
- package/dist/build-verdict.d.ts +80 -0
- package/dist/{chunk-2FZXZLZW.js → chunk-22PBLCGO.js} +29 -1
- package/dist/{chunk-NNDGXIGQ.js → chunk-6CJU5TVV.js} +1 -1
- package/dist/{chunk-IDSNTUKC.js → chunk-AMSDGT3Y.js} +818 -411
- package/dist/cli-args.d.ts +9 -0
- package/dist/cli.js +508 -6
- package/dist/confidence.d.ts +6 -1
- package/dist/{html-report-X2LUOZRH.js → html-report-2UAOOZQO.js} +2 -2
- package/dist/index.js +3 -3
- package/dist/isr.d.ts +39 -0
- package/dist/launcher.d.ts +8 -0
- package/dist/manifests.d.ts +20 -0
- package/dist/process-tree.d.ts +44 -0
- package/dist/report.d.ts +0 -1
- package/dist/ritual.d.ts +22 -0
- package/dist/route-config.d.ts +19 -5
- package/dist/route-guidance.d.ts +21 -0
- package/dist/runner.d.ts +38 -0
- package/dist/target.d.ts +6 -1
- package/dist/unreclaimed-retention.d.ts +64 -0
- package/package.json +1 -1
|
@@ -7,7 +7,7 @@ import {
|
|
|
7
7
|
minGrowthFor,
|
|
8
8
|
resolveCycles,
|
|
9
9
|
warrantsIssueDraft
|
|
10
|
-
} from "./chunk-
|
|
10
|
+
} from "./chunk-22PBLCGO.js";
|
|
11
11
|
import {
|
|
12
12
|
assessPeakPressure,
|
|
13
13
|
describePeakPressure
|
|
@@ -161,7 +161,7 @@ var require_polyfills = __commonJS({
|
|
|
161
161
|
})(fs.rename);
|
|
162
162
|
}
|
|
163
163
|
fs.read = typeof fs.read !== "function" ? fs.read : (function(fs$read) {
|
|
164
|
-
function
|
|
164
|
+
function read2(fd, buffer, offset, length, position, callback_) {
|
|
165
165
|
var callback;
|
|
166
166
|
if (callback_ && typeof callback_ === "function") {
|
|
167
167
|
var eagCounter = 0;
|
|
@@ -175,8 +175,8 @@ var require_polyfills = __commonJS({
|
|
|
175
175
|
}
|
|
176
176
|
return fs$read.call(fs, fd, buffer, offset, length, position, callback);
|
|
177
177
|
}
|
|
178
|
-
if (Object.setPrototypeOf) Object.setPrototypeOf(
|
|
179
|
-
return
|
|
178
|
+
if (Object.setPrototypeOf) Object.setPrototypeOf(read2, fs$read);
|
|
179
|
+
return read2;
|
|
180
180
|
})(fs.read);
|
|
181
181
|
fs.readSync = typeof fs.readSync !== "function" ? fs.readSync : /* @__PURE__ */ (function(fs$readSync) {
|
|
182
182
|
return function(fd, buffer, offset, length, position) {
|
|
@@ -88693,7 +88693,7 @@ var require_buffer3 = __commonJS({
|
|
|
88693
88693
|
byteOffset /= 2;
|
|
88694
88694
|
}
|
|
88695
88695
|
}
|
|
88696
|
-
function
|
|
88696
|
+
function read2(buf, i2) {
|
|
88697
88697
|
if (indexSize === 1) {
|
|
88698
88698
|
return buf[i2];
|
|
88699
88699
|
} else {
|
|
@@ -88704,7 +88704,7 @@ var require_buffer3 = __commonJS({
|
|
|
88704
88704
|
if (dir) {
|
|
88705
88705
|
var foundIndex = -1;
|
|
88706
88706
|
for (i = byteOffset; i < arrLength; i++) {
|
|
88707
|
-
if (
|
|
88707
|
+
if (read2(arr, i) === read2(val, foundIndex === -1 ? 0 : i - foundIndex)) {
|
|
88708
88708
|
if (foundIndex === -1) foundIndex = i;
|
|
88709
88709
|
if (i - foundIndex + 1 === valLength) return foundIndex * indexSize;
|
|
88710
88710
|
} else {
|
|
@@ -88717,7 +88717,7 @@ var require_buffer3 = __commonJS({
|
|
|
88717
88717
|
for (i = byteOffset; i >= 0; i--) {
|
|
88718
88718
|
var found = true;
|
|
88719
88719
|
for (var j = 0; j < valLength; j++) {
|
|
88720
|
-
if (
|
|
88720
|
+
if (read2(arr, i + j) !== read2(val, j)) {
|
|
88721
88721
|
found = false;
|
|
88722
88722
|
break;
|
|
88723
88723
|
}
|
|
@@ -91969,7 +91969,374 @@ var require_autocannon = __commonJS({
|
|
|
91969
91969
|
}
|
|
91970
91970
|
});
|
|
91971
91971
|
|
|
91972
|
+
// src/manifests.ts
|
|
91973
|
+
import { z } from "zod";
|
|
91974
|
+
var appPathsManifestSchema = z.record(z.string(), z.string());
|
|
91975
|
+
var pagesManifestSchema = z.record(z.string(), z.string());
|
|
91976
|
+
var routeEntrySchema = z.looseObject({
|
|
91977
|
+
page: z.string(),
|
|
91978
|
+
regex: z.string()
|
|
91979
|
+
});
|
|
91980
|
+
var routesManifestSchema = z.looseObject({
|
|
91981
|
+
version: z.number(),
|
|
91982
|
+
basePath: z.string(),
|
|
91983
|
+
staticRoutes: z.array(routeEntrySchema),
|
|
91984
|
+
dynamicRoutes: z.array(routeEntrySchema)
|
|
91985
|
+
});
|
|
91986
|
+
var prerenderManifestSchema = z.looseObject({
|
|
91987
|
+
routes: z.record(
|
|
91988
|
+
z.string(),
|
|
91989
|
+
z.looseObject({
|
|
91990
|
+
// `false` on a route that is prerendered but never revalidates.
|
|
91991
|
+
initialRevalidateSeconds: z.union([z.number(), z.literal(false)]).optional(),
|
|
91992
|
+
srcRoute: z.string().nullable().optional()
|
|
91993
|
+
})
|
|
91994
|
+
).optional(),
|
|
91995
|
+
preview: z.looseObject({ previewModeId: z.string() }).optional()
|
|
91996
|
+
});
|
|
91997
|
+
var INTERNAL_PATHS = /* @__PURE__ */ new Set(["/_global-error", "/_not-found"]);
|
|
91998
|
+
var INTERCEPTING_SEGMENT = /^(\(\.{1,3}\))+/;
|
|
91999
|
+
var STATIC_ASSET_PATHS = /* @__PURE__ */ new Set(["/favicon.ico"]);
|
|
92000
|
+
function unaddressableReason(routePath) {
|
|
92001
|
+
if (routePath.split("/").some((segment) => INTERCEPTING_SEGMENT.test(segment))) {
|
|
92002
|
+
return "intercepting route \u2014 only reachable via client navigation";
|
|
92003
|
+
}
|
|
92004
|
+
if (STATIC_ASSET_PATHS.has(routePath)) {
|
|
92005
|
+
return "static asset served by a generated handler \u2014 no user code to leak";
|
|
92006
|
+
}
|
|
92007
|
+
return void 0;
|
|
92008
|
+
}
|
|
92009
|
+
function toRequestPath(manifestKey) {
|
|
92010
|
+
let kind;
|
|
92011
|
+
let withoutSuffix;
|
|
92012
|
+
if (manifestKey.endsWith("/page")) {
|
|
92013
|
+
kind = "page";
|
|
92014
|
+
withoutSuffix = manifestKey.slice(0, -"/page".length);
|
|
92015
|
+
} else if (manifestKey.endsWith("/route")) {
|
|
92016
|
+
kind = "route-handler";
|
|
92017
|
+
withoutSuffix = manifestKey.slice(0, -"/route".length);
|
|
92018
|
+
} else {
|
|
92019
|
+
return null;
|
|
92020
|
+
}
|
|
92021
|
+
const segments = withoutSuffix.split("/").filter((segment) => segment !== "").filter((segment) => !(segment.startsWith("(") && segment.endsWith(")"))).filter((segment) => !segment.startsWith("@"));
|
|
92022
|
+
return { path: `/${segments.join("/")}`, kind };
|
|
92023
|
+
}
|
|
92024
|
+
var PAGES_INTERNAL = /* @__PURE__ */ new Set(["/_app", "/_document", "/_error", "/404", "/500"]);
|
|
92025
|
+
function discoverPagesRoutes(pages) {
|
|
92026
|
+
const routes = [];
|
|
92027
|
+
for (const key of Object.keys(pages)) {
|
|
92028
|
+
if (PAGES_INTERNAL.has(key)) {
|
|
92029
|
+
continue;
|
|
92030
|
+
}
|
|
92031
|
+
routes.push({
|
|
92032
|
+
path: key,
|
|
92033
|
+
kind: key === "/api" || key.startsWith("/api/") ? "route-handler" : "page",
|
|
92034
|
+
dynamic: key.includes("[")
|
|
92035
|
+
});
|
|
92036
|
+
}
|
|
92037
|
+
return routes.sort((a, b) => a.path.localeCompare(b.path));
|
|
92038
|
+
}
|
|
92039
|
+
function discoverRoutes(appPaths) {
|
|
92040
|
+
const routes = /* @__PURE__ */ new Map();
|
|
92041
|
+
for (const key of Object.keys(appPaths)) {
|
|
92042
|
+
const parsed = toRequestPath(key);
|
|
92043
|
+
if (parsed === null || INTERNAL_PATHS.has(parsed.path) || routes.has(parsed.path)) {
|
|
92044
|
+
continue;
|
|
92045
|
+
}
|
|
92046
|
+
const reason = unaddressableReason(parsed.path);
|
|
92047
|
+
routes.set(parsed.path, {
|
|
92048
|
+
path: parsed.path,
|
|
92049
|
+
kind: parsed.kind,
|
|
92050
|
+
dynamic: parsed.path.includes("["),
|
|
92051
|
+
...reason !== void 0 && { unaddressableReason: reason }
|
|
92052
|
+
});
|
|
92053
|
+
}
|
|
92054
|
+
return [...routes.values()].sort((a, b) => a.path.localeCompare(b.path));
|
|
92055
|
+
}
|
|
92056
|
+
|
|
92057
|
+
// src/target.ts
|
|
92058
|
+
import { access, readFile } from "fs/promises";
|
|
92059
|
+
import path from "path";
|
|
92060
|
+
var TargetError = class extends Error {
|
|
92061
|
+
code;
|
|
92062
|
+
constructor(code, message) {
|
|
92063
|
+
super(message);
|
|
92064
|
+
this.name = "TargetError";
|
|
92065
|
+
this.code = code;
|
|
92066
|
+
}
|
|
92067
|
+
};
|
|
92068
|
+
async function exists(file) {
|
|
92069
|
+
try {
|
|
92070
|
+
await access(file);
|
|
92071
|
+
return true;
|
|
92072
|
+
} catch {
|
|
92073
|
+
return false;
|
|
92074
|
+
}
|
|
92075
|
+
}
|
|
92076
|
+
async function readManifest(file, parse2) {
|
|
92077
|
+
let raw;
|
|
92078
|
+
try {
|
|
92079
|
+
raw = JSON.parse(await readFile(file, "utf8"));
|
|
92080
|
+
} catch (cause) {
|
|
92081
|
+
throw new TargetError(
|
|
92082
|
+
"BAD_MANIFEST",
|
|
92083
|
+
`Could not read ${file}: ${cause instanceof Error ? cause.message : String(cause)}`
|
|
92084
|
+
);
|
|
92085
|
+
}
|
|
92086
|
+
try {
|
|
92087
|
+
return parse2(raw);
|
|
92088
|
+
} catch {
|
|
92089
|
+
throw new TargetError(
|
|
92090
|
+
"BAD_MANIFEST",
|
|
92091
|
+
`${file} does not match the expected Next.js manifest shape. This Next.js version may not be supported yet.`
|
|
92092
|
+
);
|
|
92093
|
+
}
|
|
92094
|
+
}
|
|
92095
|
+
async function validateTarget(appDir) {
|
|
92096
|
+
const nextDir = path.resolve(appDir, ".next");
|
|
92097
|
+
if (!await exists(nextDir)) {
|
|
92098
|
+
throw new TargetError(
|
|
92099
|
+
"NO_BUILD",
|
|
92100
|
+
`No .next directory in ${appDir}. Run "next build" first.`
|
|
92101
|
+
);
|
|
92102
|
+
}
|
|
92103
|
+
const standaloneServer = path.join(nextDir, "standalone", "server.js");
|
|
92104
|
+
if (!await exists(standaloneServer)) {
|
|
92105
|
+
throw new TargetError(
|
|
92106
|
+
"NO_STANDALONE",
|
|
92107
|
+
`No ${standaloneServer}.
|
|
92108
|
+
next-leak measures the standalone server bundle. Enable it once in next.config:
|
|
92109
|
+
|
|
92110
|
+
const nextConfig = {
|
|
92111
|
+
output: "standalone",
|
|
92112
|
+
// ...your existing config
|
|
92113
|
+
};
|
|
92114
|
+
|
|
92115
|
+
then rebuild: next build
|
|
92116
|
+
This only changes how the build is packaged \u2014 not how your app behaves.`
|
|
92117
|
+
);
|
|
92118
|
+
}
|
|
92119
|
+
const appPathsFile = path.join(nextDir, "server", "app-paths-manifest.json");
|
|
92120
|
+
const pagesFile = path.join(nextDir, "server", "pages-manifest.json");
|
|
92121
|
+
const appPaths = await exists(appPathsFile) ? await readManifest(appPathsFile, (raw) => appPathsManifestSchema.parse(raw)) : {};
|
|
92122
|
+
const pages = await exists(pagesFile) ? await readManifest(pagesFile, (raw) => pagesManifestSchema.parse(raw)) : {};
|
|
92123
|
+
if (Object.keys(appPaths).length === 0 && Object.keys(pages).length === 0) {
|
|
92124
|
+
throw new TargetError(
|
|
92125
|
+
"BAD_MANIFEST",
|
|
92126
|
+
`No routes found in ${nextDir}/server: neither app-paths-manifest.json nor pages-manifest.json is present. Rebuild the app with "next build".`
|
|
92127
|
+
);
|
|
92128
|
+
}
|
|
92129
|
+
const routesFile = path.join(nextDir, "routes-manifest.json");
|
|
92130
|
+
const routes = await exists(routesFile) ? await readManifest(routesFile, (raw) => routesManifestSchema.parse(raw)) : void 0;
|
|
92131
|
+
const prerenderFile = path.join(nextDir, "prerender-manifest.json");
|
|
92132
|
+
const prerender = await exists(prerenderFile) ? await readManifest(prerenderFile, (raw) => prerenderManifestSchema.parse(raw)) : void 0;
|
|
92133
|
+
return {
|
|
92134
|
+
appDir: path.resolve(appDir),
|
|
92135
|
+
standaloneServer,
|
|
92136
|
+
appPaths,
|
|
92137
|
+
pages,
|
|
92138
|
+
routes,
|
|
92139
|
+
prerender
|
|
92140
|
+
};
|
|
92141
|
+
}
|
|
92142
|
+
|
|
92143
|
+
// src/launcher.ts
|
|
92144
|
+
import { spawn } from "child_process";
|
|
92145
|
+
import { readdir, readFile as readFile2 } from "fs/promises";
|
|
92146
|
+
import path2 from "path";
|
|
92147
|
+
import { pathToFileURL } from "url";
|
|
92148
|
+
import { z as z2 } from "zod";
|
|
92149
|
+
var controlFileSchema = z2.object({ port: z2.number(), pid: z2.number() });
|
|
92150
|
+
var DEFAULT_MAX_OLD_SPACE_MB = 512;
|
|
92151
|
+
var LaunchError = class extends Error {
|
|
92152
|
+
constructor(message) {
|
|
92153
|
+
super(message);
|
|
92154
|
+
this.name = "LaunchError";
|
|
92155
|
+
}
|
|
92156
|
+
};
|
|
92157
|
+
var activeChildren = /* @__PURE__ */ new Set();
|
|
92158
|
+
var STDERR_WINDOW = 4096;
|
|
92159
|
+
function killActiveChildren() {
|
|
92160
|
+
for (const child of activeChildren) {
|
|
92161
|
+
child.kill("SIGKILL");
|
|
92162
|
+
}
|
|
92163
|
+
}
|
|
92164
|
+
function registerChild(child) {
|
|
92165
|
+
activeChildren.add(child);
|
|
92166
|
+
child.once("exit", () => activeChildren.delete(child));
|
|
92167
|
+
}
|
|
92168
|
+
function unregisterChild(child) {
|
|
92169
|
+
activeChildren.delete(child);
|
|
92170
|
+
}
|
|
92171
|
+
function explainStartupFailure(stderr) {
|
|
92172
|
+
const missingModule = /Cannot find module '([^']+)'/.exec(stderr);
|
|
92173
|
+
if (missingModule !== null) {
|
|
92174
|
+
return `the standalone build is missing a dependency (${missingModule[1]}). This is a build problem, not a measurement one: \`node .next/standalone/server.js\` fails the same way on its own. Rebuild, or copy the missing package into .next/standalone/node_modules.`;
|
|
92175
|
+
}
|
|
92176
|
+
if (/EADDRINUSE/.test(stderr)) {
|
|
92177
|
+
return "the port was taken by another process while starting.";
|
|
92178
|
+
}
|
|
92179
|
+
return `stderr:
|
|
92180
|
+
${stderr}`;
|
|
92181
|
+
}
|
|
92182
|
+
function explainRuntimeFailure(stderr, maxOldSpaceMb) {
|
|
92183
|
+
if (/heap out of memory|Reached heap limit|Ineffective mark-compacts/i.test(stderr)) {
|
|
92184
|
+
return `the measured process ran out of heap and was killed by V8 mid-run (limit in force: --max-old-space-size=${maxOldSpaceMb} MB). That is the measurement: this route does not fit in ${maxOldSpaceMb} MB under this load. Raise it with --max-old-space <mb> to match your deployment, or lower --requests/--connections to measure a lighter regime.`;
|
|
92185
|
+
}
|
|
92186
|
+
return `the measured process exited mid-run. ${explainStartupFailure(stderr)}`;
|
|
92187
|
+
}
|
|
92188
|
+
var sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
92189
|
+
async function pollUntil(deadline, what, probe, failed) {
|
|
92190
|
+
for (; ; ) {
|
|
92191
|
+
const failure = failed();
|
|
92192
|
+
if (failure !== void 0) {
|
|
92193
|
+
throw new LaunchError(failure);
|
|
92194
|
+
}
|
|
92195
|
+
const result = await probe();
|
|
92196
|
+
if (result !== void 0) {
|
|
92197
|
+
return result;
|
|
92198
|
+
}
|
|
92199
|
+
if (Date.now() > deadline) {
|
|
92200
|
+
throw new LaunchError(`timed out waiting for ${what}`);
|
|
92201
|
+
}
|
|
92202
|
+
await sleep(100);
|
|
92203
|
+
}
|
|
92204
|
+
}
|
|
92205
|
+
async function launchInstrumented(options) {
|
|
92206
|
+
const hostname = options.hostname ?? "127.0.0.1";
|
|
92207
|
+
const deadline = Date.now() + (options.readyTimeoutMs ?? 15e3);
|
|
92208
|
+
const child = spawn(
|
|
92209
|
+
process.execPath,
|
|
92210
|
+
[
|
|
92211
|
+
"--expose-gc",
|
|
92212
|
+
`--max-old-space-size=${options.maxOldSpaceMb ?? DEFAULT_MAX_OLD_SPACE_MB}`,
|
|
92213
|
+
"--import",
|
|
92214
|
+
pathToFileURL(options.bootstrapPath).href,
|
|
92215
|
+
options.serverPath
|
|
92216
|
+
],
|
|
92217
|
+
{
|
|
92218
|
+
cwd: path2.dirname(options.serverPath),
|
|
92219
|
+
env: {
|
|
92220
|
+
...process.env,
|
|
92221
|
+
...options.env,
|
|
92222
|
+
NODE_ENV: "production",
|
|
92223
|
+
PORT: String(options.appPort),
|
|
92224
|
+
HOSTNAME: hostname,
|
|
92225
|
+
NEXT_LEAK_DIR: options.workDir
|
|
92226
|
+
},
|
|
92227
|
+
stdio: ["ignore", "ignore", "pipe"]
|
|
92228
|
+
}
|
|
92229
|
+
);
|
|
92230
|
+
let stderrHead = "";
|
|
92231
|
+
let stderrTailBuffer = "";
|
|
92232
|
+
child.stderr?.on("data", (chunk) => {
|
|
92233
|
+
const text = chunk.toString();
|
|
92234
|
+
if (stderrHead.length < STDERR_WINDOW) {
|
|
92235
|
+
stderrHead = (stderrHead + text).slice(0, STDERR_WINDOW);
|
|
92236
|
+
}
|
|
92237
|
+
stderrTailBuffer = (stderrTailBuffer + text).slice(-STDERR_WINDOW);
|
|
92238
|
+
});
|
|
92239
|
+
const stderrWindow = () => stderrHead === stderrTailBuffer || stderrTailBuffer === "" ? stderrHead : `${stderrHead}
|
|
92240
|
+
[...]
|
|
92241
|
+
${stderrTailBuffer}`;
|
|
92242
|
+
let exited = false;
|
|
92243
|
+
activeChildren.add(child);
|
|
92244
|
+
child.once("exit", () => {
|
|
92245
|
+
exited = true;
|
|
92246
|
+
activeChildren.delete(child);
|
|
92247
|
+
});
|
|
92248
|
+
const failed = () => exited ? `server exited before becoming ready. ${explainStartupFailure(stderrWindow())}` : void 0;
|
|
92249
|
+
try {
|
|
92250
|
+
const controlPort = await pollUntil(
|
|
92251
|
+
deadline,
|
|
92252
|
+
`control channel in ${options.workDir}`,
|
|
92253
|
+
// Several processes may announce a channel (clustered servers); accept
|
|
92254
|
+
// the first one that actually answers instead of trusting a filename.
|
|
92255
|
+
async () => {
|
|
92256
|
+
let entries;
|
|
92257
|
+
try {
|
|
92258
|
+
entries = (await readdir(options.workDir)).filter(
|
|
92259
|
+
(entry) => entry.startsWith("control-") && entry.endsWith(".json")
|
|
92260
|
+
);
|
|
92261
|
+
} catch {
|
|
92262
|
+
return void 0;
|
|
92263
|
+
}
|
|
92264
|
+
for (const entry of entries) {
|
|
92265
|
+
try {
|
|
92266
|
+
const parsed = controlFileSchema.parse(
|
|
92267
|
+
JSON.parse(await readFile2(path2.join(options.workDir, entry), "utf8"))
|
|
92268
|
+
);
|
|
92269
|
+
const response = await fetch(`http://127.0.0.1:${parsed.port}/gc`);
|
|
92270
|
+
if (response.ok) {
|
|
92271
|
+
return parsed.port;
|
|
92272
|
+
}
|
|
92273
|
+
} catch {
|
|
92274
|
+
continue;
|
|
92275
|
+
}
|
|
92276
|
+
}
|
|
92277
|
+
return void 0;
|
|
92278
|
+
},
|
|
92279
|
+
failed
|
|
92280
|
+
);
|
|
92281
|
+
await pollUntil(
|
|
92282
|
+
deadline,
|
|
92283
|
+
`app on ${hostname}:${options.appPort}`,
|
|
92284
|
+
async () => {
|
|
92285
|
+
try {
|
|
92286
|
+
await fetch(`http://${hostname}:${options.appPort}/`, { method: "HEAD" });
|
|
92287
|
+
return true;
|
|
92288
|
+
} catch {
|
|
92289
|
+
return void 0;
|
|
92290
|
+
}
|
|
92291
|
+
},
|
|
92292
|
+
failed
|
|
92293
|
+
);
|
|
92294
|
+
return {
|
|
92295
|
+
pid: child.pid ?? -1,
|
|
92296
|
+
appPort: options.appPort,
|
|
92297
|
+
controlPort,
|
|
92298
|
+
explainExit: () => exited ? explainRuntimeFailure(stderrWindow(), options.maxOldSpaceMb ?? DEFAULT_MAX_OLD_SPACE_MB) : null,
|
|
92299
|
+
close: async () => {
|
|
92300
|
+
if (exited) {
|
|
92301
|
+
return;
|
|
92302
|
+
}
|
|
92303
|
+
const gone = new Promise((resolve) => child.once("exit", () => resolve()));
|
|
92304
|
+
child.kill("SIGTERM");
|
|
92305
|
+
const killTimer = setTimeout(() => child.kill("SIGKILL"), 3e3);
|
|
92306
|
+
await gone;
|
|
92307
|
+
clearTimeout(killTimer);
|
|
92308
|
+
}
|
|
92309
|
+
};
|
|
92310
|
+
} catch (cause) {
|
|
92311
|
+
child.kill("SIGKILL");
|
|
92312
|
+
throw cause;
|
|
92313
|
+
}
|
|
92314
|
+
}
|
|
92315
|
+
|
|
91972
92316
|
// src/cli-args.ts
|
|
92317
|
+
var BUILD_COMMAND = "build";
|
|
92318
|
+
var RUN_ONLY_FLAGS = [
|
|
92319
|
+
["routes", "--routes"],
|
|
92320
|
+
["cycles", "--cycles"],
|
|
92321
|
+
["requests", "--requests"],
|
|
92322
|
+
["connections", "--connections"],
|
|
92323
|
+
["idleSeconds", "--idle"],
|
|
92324
|
+
["warmupRequests", "--warmup"],
|
|
92325
|
+
["maxOldSpaceMb", "--max-old-space"],
|
|
92326
|
+
["quick", "--quick"],
|
|
92327
|
+
["noResolve", "--no-resolve"],
|
|
92328
|
+
["diffAll", "--diff-all"],
|
|
92329
|
+
["writeConfig", "--write-config"]
|
|
92330
|
+
];
|
|
92331
|
+
function runOnlyFlagUsed(options) {
|
|
92332
|
+
for (const [field, flag] of RUN_ONLY_FLAGS) {
|
|
92333
|
+
const value = options[field];
|
|
92334
|
+
if (value !== null && value !== false) {
|
|
92335
|
+
return flag;
|
|
92336
|
+
}
|
|
92337
|
+
}
|
|
92338
|
+
return null;
|
|
92339
|
+
}
|
|
91973
92340
|
var FLAGS = [
|
|
91974
92341
|
{
|
|
91975
92342
|
flag: "--routes",
|
|
@@ -91981,6 +92348,12 @@ var FLAGS = [
|
|
|
91981
92348
|
{ flag: "--requests", value: "int", argName: "<n>", help: "Requests per cycle (default 5000)" },
|
|
91982
92349
|
{ flag: "--connections", value: "int", argName: "<n>", help: "Concurrent connections (default 100)" },
|
|
91983
92350
|
{ flag: "--idle", value: "int", argName: "<seconds>", help: "Idle seconds before each sample (default 30)" },
|
|
92351
|
+
{
|
|
92352
|
+
flag: "--warmup",
|
|
92353
|
+
value: "int",
|
|
92354
|
+
argName: "<n>",
|
|
92355
|
+
help: "Requests before the baseline snapshot (default 200) \u2014 lower it on apps that cache per request"
|
|
92356
|
+
},
|
|
91984
92357
|
{
|
|
91985
92358
|
flag: "--max-old-space",
|
|
91986
92359
|
value: "int",
|
|
@@ -91999,6 +92372,11 @@ var FLAGS = [
|
|
|
91999
92372
|
help: "Do not re-measure inconclusive routes with more cycles (default: re-measure once)"
|
|
92000
92373
|
},
|
|
92001
92374
|
{ flag: "--output", value: "string", argName: "<dir>", help: "Where to write runs (default <app-dir>/.next-leak)" },
|
|
92375
|
+
{
|
|
92376
|
+
flag: "--write-config",
|
|
92377
|
+
value: "none",
|
|
92378
|
+
help: "Write next-leak.config.json for the routes that need sample params, then exit (never overwrites)"
|
|
92379
|
+
},
|
|
92002
92380
|
{ flag: "--help", alias: "-h", value: "none", help: "Show this help" },
|
|
92003
92381
|
{ flag: "--version", alias: "-v", value: "none", help: "Print the version" }
|
|
92004
92382
|
];
|
|
@@ -92015,7 +92393,8 @@ Find out whether your Next.js app actually leaks memory \u2014 how much, on whic
|
|
|
92015
92393
|
route, and whose fault it is.
|
|
92016
92394
|
|
|
92017
92395
|
Usage:
|
|
92018
|
-
next-leak <app-dir> [options]
|
|
92396
|
+
next-leak <app-dir> [options] measure a built server under load
|
|
92397
|
+
next-leak build <app-dir> measure the memory of "next build"
|
|
92019
92398
|
|
|
92020
92399
|
The app must be built with output: "standalone" (next build). For each
|
|
92021
92400
|
discovered route, next-leak boots a fresh instrumented process and runs:
|
|
@@ -92023,6 +92402,11 @@ warm-up \u2192 GC \u2192 baseline snapshot \u2192 [load \u2192 idle \u2192 GC \u
|
|
|
92023
92402
|
snapshot. Evidence (report.html, ISSUE drafts, raw snapshots, run.json) is
|
|
92024
92403
|
written under the output directory.
|
|
92025
92404
|
|
|
92405
|
+
The build command needs neither a previous build nor standalone output: it runs
|
|
92406
|
+
the build itself and samples the resident memory of each static-generation
|
|
92407
|
+
worker, which is where large sites run out of heap while prerendering. It takes
|
|
92408
|
+
--output only; the options below shape HTTP load and do not apply to it.
|
|
92409
|
+
|
|
92026
92410
|
Options:
|
|
92027
92411
|
${rows}
|
|
92028
92412
|
|
|
@@ -92035,6 +92419,7 @@ var LIMITS = {
|
|
|
92035
92419
|
"--requests": 1e6,
|
|
92036
92420
|
"--connections": 1e4,
|
|
92037
92421
|
"--idle": 3600,
|
|
92422
|
+
"--warmup": 1e6,
|
|
92038
92423
|
"--max-old-space": 65536
|
|
92039
92424
|
};
|
|
92040
92425
|
var MIN_MAX_OLD_SPACE_MB = 128;
|
|
@@ -92074,6 +92459,7 @@ function applyNumericFlag(flag, value, options) {
|
|
|
92074
92459
|
if (flag === "--requests") options.requests = parsed;
|
|
92075
92460
|
if (flag === "--connections") options.connections = parsed;
|
|
92076
92461
|
if (flag === "--idle") options.idleSeconds = parsed;
|
|
92462
|
+
if (flag === "--warmup") options.warmupRequests = parsed;
|
|
92077
92463
|
if (flag === "--max-old-space") options.maxOldSpaceMb = parsed;
|
|
92078
92464
|
return FLAG_OK;
|
|
92079
92465
|
}
|
|
@@ -92085,6 +92471,7 @@ function applyFlag(spec, value, options) {
|
|
|
92085
92471
|
case "--requests":
|
|
92086
92472
|
case "--connections":
|
|
92087
92473
|
case "--idle":
|
|
92474
|
+
case "--warmup":
|
|
92088
92475
|
case "--max-old-space":
|
|
92089
92476
|
return applyNumericFlag(spec.flag, value, options);
|
|
92090
92477
|
case "--quick":
|
|
@@ -92093,6 +92480,9 @@ function applyFlag(spec, value, options) {
|
|
|
92093
92480
|
case "--diff-all":
|
|
92094
92481
|
options.diffAll = true;
|
|
92095
92482
|
return FLAG_OK;
|
|
92483
|
+
case "--write-config":
|
|
92484
|
+
options.writeConfig = true;
|
|
92485
|
+
return FLAG_OK;
|
|
92096
92486
|
case "--no-resolve":
|
|
92097
92487
|
options.noResolve = true;
|
|
92098
92488
|
return FLAG_OK;
|
|
@@ -92126,17 +92516,19 @@ function parseFlagAt(argv, index, options) {
|
|
|
92126
92516
|
if (spec.flag === "--version") {
|
|
92127
92517
|
return { done: { kind: "version" } };
|
|
92128
92518
|
}
|
|
92129
|
-
const
|
|
92130
|
-
if ("error" in
|
|
92131
|
-
return { done: { kind: "error", message:
|
|
92519
|
+
const read2 = readFlagValue(spec, argv, index);
|
|
92520
|
+
if ("error" in read2) {
|
|
92521
|
+
return { done: { kind: "error", message: read2.error } };
|
|
92132
92522
|
}
|
|
92133
|
-
const outcome = applyFlag(spec,
|
|
92523
|
+
const outcome = applyFlag(spec, read2.value, options);
|
|
92134
92524
|
if (outcome.kind === "error") {
|
|
92135
92525
|
return { done: { kind: "error", message: outcome.message } };
|
|
92136
92526
|
}
|
|
92137
92527
|
return { consumed: spec.value === "none" ? 0 : 1 };
|
|
92138
92528
|
}
|
|
92139
92529
|
function parseCliArgs(argv) {
|
|
92530
|
+
const isBuild = argv[0] === BUILD_COMMAND;
|
|
92531
|
+
const rest = isBuild ? argv.slice(1) : argv;
|
|
92140
92532
|
const options = {
|
|
92141
92533
|
appDir: "",
|
|
92142
92534
|
routes: null,
|
|
@@ -92144,14 +92536,16 @@ function parseCliArgs(argv) {
|
|
|
92144
92536
|
requests: null,
|
|
92145
92537
|
connections: null,
|
|
92146
92538
|
idleSeconds: null,
|
|
92539
|
+
warmupRequests: null,
|
|
92147
92540
|
maxOldSpaceMb: null,
|
|
92148
92541
|
quick: false,
|
|
92149
92542
|
noResolve: false,
|
|
92150
92543
|
diffAll: false,
|
|
92544
|
+
writeConfig: false,
|
|
92151
92545
|
output: null
|
|
92152
92546
|
};
|
|
92153
|
-
for (let index = 0; index <
|
|
92154
|
-
const argument =
|
|
92547
|
+
for (let index = 0; index < rest.length; index += 1) {
|
|
92548
|
+
const argument = rest[index] ?? "";
|
|
92155
92549
|
if (!argument.startsWith("-")) {
|
|
92156
92550
|
if (options.appDir !== "") {
|
|
92157
92551
|
return { kind: "error", message: `unexpected extra argument "${argument}" \u2014 one app directory only` };
|
|
@@ -92159,7 +92553,7 @@ function parseCliArgs(argv) {
|
|
|
92159
92553
|
options.appDir = argument;
|
|
92160
92554
|
continue;
|
|
92161
92555
|
}
|
|
92162
|
-
const step = parseFlagAt(
|
|
92556
|
+
const step = parseFlagAt(rest, index, options);
|
|
92163
92557
|
if ("done" in step) {
|
|
92164
92558
|
return step.done;
|
|
92165
92559
|
}
|
|
@@ -92168,6 +92562,16 @@ function parseCliArgs(argv) {
|
|
|
92168
92562
|
if (options.appDir === "") {
|
|
92169
92563
|
return { kind: "help" };
|
|
92170
92564
|
}
|
|
92565
|
+
if (isBuild) {
|
|
92566
|
+
const misplaced = runOnlyFlagUsed(options);
|
|
92567
|
+
if (misplaced !== null) {
|
|
92568
|
+
return {
|
|
92569
|
+
kind: "error",
|
|
92570
|
+
message: `option "${misplaced}" shapes HTTP load and does not apply to "next-leak build" \u2014 see --help`
|
|
92571
|
+
};
|
|
92572
|
+
}
|
|
92573
|
+
return { kind: "build", options: { appDir: options.appDir, output: options.output } };
|
|
92574
|
+
}
|
|
92171
92575
|
return { kind: "run", options };
|
|
92172
92576
|
}
|
|
92173
92577
|
|
|
@@ -92188,173 +92592,234 @@ function checkRuntime(facts = currentFacts()) {
|
|
|
92188
92592
|
return null;
|
|
92189
92593
|
}
|
|
92190
92594
|
|
|
92191
|
-
// src/
|
|
92192
|
-
import {
|
|
92193
|
-
import
|
|
92194
|
-
import
|
|
92195
|
-
|
|
92196
|
-
|
|
92197
|
-
|
|
92198
|
-
|
|
92199
|
-
var
|
|
92595
|
+
// src/route-config.ts
|
|
92596
|
+
import { readFile as readFile3 } from "fs/promises";
|
|
92597
|
+
import path3 from "path";
|
|
92598
|
+
import { z as z3 } from "zod";
|
|
92599
|
+
var sampleValueSchema = z3.string().refine(
|
|
92600
|
+
(value) => !(value.includes("{n}") && /\{n%\d+\}/.test(value)),
|
|
92601
|
+
{ message: 'a sample value cannot carry both "{n}" and "{n%N}" \u2014 pick one' }
|
|
92602
|
+
);
|
|
92603
|
+
var routeConfigSchema = z3.object({
|
|
92604
|
+
params: z3.record(z3.string(), sampleValueSchema).optional(),
|
|
92605
|
+
routes: z3.record(z3.string(), z3.record(z3.string(), sampleValueSchema)).optional(),
|
|
92606
|
+
/**
|
|
92607
|
+
* Headers sent with every request. Real traffic is not header-less:
|
|
92608
|
+
* compression (`Accept-Encoding`), sessions and auth change which code
|
|
92609
|
+
* paths run, and some leaks only exist on those paths (zlib, per-session
|
|
92610
|
+
* caches). Measuring without them measures a different app.
|
|
92611
|
+
*/
|
|
92612
|
+
headers: z3.record(z3.string(), z3.string()).optional(),
|
|
92613
|
+
/** Query string appended per route template, e.g. `"weightKb=2048"`. */
|
|
92614
|
+
query: z3.record(z3.string(), z3.string()).optional(),
|
|
92615
|
+
/**
|
|
92616
|
+
* Simulates clients that give up before the response arrives (closed
|
|
92617
|
+
* tabs, load-balancer timeouts, bots). Several real leaks only exist on
|
|
92618
|
+
* that path — vercel/next.js#89091 traces `ServerResponse` retention to
|
|
92619
|
+
* an early disconnect — and a load generator that always waits politely
|
|
92620
|
+
* never reaches it.
|
|
92621
|
+
*
|
|
92622
|
+
* Counted **from the first byte of the response**, not from the request:
|
|
92623
|
+
* under load a server's first byte arrives long after any sane fixed
|
|
92624
|
+
* window, so a request-relative clock cuts before the stream starts and
|
|
92625
|
+
* tests the wrong path entirely.
|
|
92626
|
+
*/
|
|
92627
|
+
abandonAfterMs: z3.number().int().positive().optional()
|
|
92628
|
+
}).strict();
|
|
92629
|
+
var RouteConfigError = class extends Error {
|
|
92200
92630
|
constructor(message) {
|
|
92201
92631
|
super(message);
|
|
92202
|
-
this.name = "
|
|
92632
|
+
this.name = "RouteConfigError";
|
|
92203
92633
|
}
|
|
92204
92634
|
};
|
|
92205
|
-
var
|
|
92206
|
-
function
|
|
92207
|
-
|
|
92208
|
-
|
|
92635
|
+
var ROUTE_CONFIG_FILE = "next-leak.config.json";
|
|
92636
|
+
async function loadRouteConfig(appDir) {
|
|
92637
|
+
const file = path3.join(appDir, ROUTE_CONFIG_FILE);
|
|
92638
|
+
let raw;
|
|
92639
|
+
try {
|
|
92640
|
+
raw = await readFile3(file, "utf8");
|
|
92641
|
+
} catch {
|
|
92642
|
+
return {};
|
|
92643
|
+
}
|
|
92644
|
+
try {
|
|
92645
|
+
return routeConfigSchema.parse(JSON.parse(raw));
|
|
92646
|
+
} catch (cause) {
|
|
92647
|
+
throw new RouteConfigError(
|
|
92648
|
+
`${file} is invalid: ${cause instanceof Error ? cause.message : String(cause)}`
|
|
92649
|
+
);
|
|
92209
92650
|
}
|
|
92210
92651
|
}
|
|
92211
|
-
|
|
92212
|
-
|
|
92213
|
-
|
|
92214
|
-
|
|
92652
|
+
var SEGMENT_PATTERN = /^\[(\[)?(\.\.\.)?([^\]]+?)\]?\]$/;
|
|
92653
|
+
function dynamicSegmentsOf(routeTemplate) {
|
|
92654
|
+
const segments = [];
|
|
92655
|
+
for (const segment of routeTemplate.split("/")) {
|
|
92656
|
+
const match = SEGMENT_PATTERN.exec(segment);
|
|
92657
|
+
if (match === null) {
|
|
92658
|
+
continue;
|
|
92659
|
+
}
|
|
92660
|
+
segments.push({
|
|
92661
|
+
name: match[3] ?? "",
|
|
92662
|
+
catchAll: match[2] !== void 0,
|
|
92663
|
+
optional: match[1] !== void 0
|
|
92664
|
+
});
|
|
92215
92665
|
}
|
|
92216
|
-
|
|
92217
|
-
|
|
92666
|
+
return segments;
|
|
92667
|
+
}
|
|
92668
|
+
var UNIQUE_MARKER = "{n}";
|
|
92669
|
+
var BOUNDED_MARKER = /\{n%(\d+)\}/;
|
|
92670
|
+
function boundedMarkerOf(value) {
|
|
92671
|
+
const match = BOUNDED_MARKER.exec(value);
|
|
92672
|
+
const bound = Number(match?.[1]);
|
|
92673
|
+
if (match === null || !Number.isInteger(bound) || bound <= 0) {
|
|
92674
|
+
return null;
|
|
92218
92675
|
}
|
|
92219
|
-
return
|
|
92220
|
-
${stderr}`;
|
|
92676
|
+
return { marker: match[0], bound };
|
|
92221
92677
|
}
|
|
92222
|
-
|
|
92223
|
-
|
|
92224
|
-
|
|
92678
|
+
var ENCODED_MARKER = encodeURIComponent(UNIQUE_MARKER);
|
|
92679
|
+
var encodeSegment = (value) => restoreMarkers(encodeURIComponent(value).split(ENCODED_MARKER).join(UNIQUE_MARKER));
|
|
92680
|
+
var ENCODED_BOUNDED = /%7Bn%25(\d+)%7D/gi;
|
|
92681
|
+
var restoreMarkers = (value) => value.replace(ENCODED_BOUNDED, (_match, bound) => `{n%${bound}}`);
|
|
92682
|
+
var encodeCatchAll = (value) => value.split("/").map(encodeSegment).join("/");
|
|
92683
|
+
function resolveRoutePath(routeTemplate, config) {
|
|
92684
|
+
const resolved = [];
|
|
92685
|
+
for (const segment of routeTemplate.split("/")) {
|
|
92686
|
+
const match = SEGMENT_PATTERN.exec(segment);
|
|
92687
|
+
if (match === null) {
|
|
92688
|
+
resolved.push(encodeSegment(segment));
|
|
92689
|
+
continue;
|
|
92690
|
+
}
|
|
92691
|
+
const optional = match[1] !== void 0;
|
|
92692
|
+
const catchAll = match[2] !== void 0;
|
|
92693
|
+
const name = match[3] ?? "";
|
|
92694
|
+
const value = config.routes?.[routeTemplate]?.[name] ?? config.params?.[name];
|
|
92695
|
+
if (value !== void 0) {
|
|
92696
|
+
resolved.push(catchAll ? encodeCatchAll(value) : encodeSegment(value));
|
|
92697
|
+
} else if (!optional) {
|
|
92698
|
+
return null;
|
|
92699
|
+
}
|
|
92225
92700
|
}
|
|
92226
|
-
|
|
92701
|
+
const joined = resolved.join("/");
|
|
92702
|
+
const path7 = joined === "" ? "/" : joined;
|
|
92703
|
+
const query = config.query?.[routeTemplate];
|
|
92704
|
+
return query === void 0 || query === "" ? path7 : `${path7}?${query}`;
|
|
92227
92705
|
}
|
|
92228
|
-
|
|
92229
|
-
|
|
92230
|
-
|
|
92231
|
-
|
|
92232
|
-
|
|
92233
|
-
|
|
92706
|
+
|
|
92707
|
+
// src/route-guidance.ts
|
|
92708
|
+
var PLACEHOLDER = "REPLACE-ME";
|
|
92709
|
+
function sampleValuesFromManifest(manifest, routeTemplate) {
|
|
92710
|
+
const routes = manifest?.routes;
|
|
92711
|
+
if (routes === void 0) {
|
|
92712
|
+
return null;
|
|
92713
|
+
}
|
|
92714
|
+
const concrete = Object.entries(routes).find(([, entry]) => entry.srcRoute === routeTemplate)?.[0];
|
|
92715
|
+
if (concrete === void 0) {
|
|
92716
|
+
return null;
|
|
92717
|
+
}
|
|
92718
|
+
const templateSegments = routeTemplate.split("/");
|
|
92719
|
+
const concreteSegments = concrete.split("/");
|
|
92720
|
+
const values = {};
|
|
92721
|
+
for (const [index, segment] of templateSegments.entries()) {
|
|
92722
|
+
const dynamic = dynamicSegmentsOf(segment);
|
|
92723
|
+
const parameter = dynamic[0];
|
|
92724
|
+
if (parameter === void 0) {
|
|
92725
|
+
continue;
|
|
92234
92726
|
}
|
|
92235
|
-
const
|
|
92236
|
-
if (
|
|
92237
|
-
return
|
|
92727
|
+
const value = parameter.catchAll ? concreteSegments.slice(index).join("/") : concreteSegments[index];
|
|
92728
|
+
if (value === void 0 || value === "") {
|
|
92729
|
+
return null;
|
|
92238
92730
|
}
|
|
92239
|
-
|
|
92240
|
-
|
|
92731
|
+
values[parameter.name] = value;
|
|
92732
|
+
}
|
|
92733
|
+
return Object.keys(values).length === 0 ? null : values;
|
|
92734
|
+
}
|
|
92735
|
+
function renderConfigSkeleton(routeTemplates, manifest = void 0) {
|
|
92736
|
+
const routes = {};
|
|
92737
|
+
for (const template of routeTemplates) {
|
|
92738
|
+
const segments = dynamicSegmentsOf(template);
|
|
92739
|
+
if (segments.length === 0) {
|
|
92740
|
+
continue;
|
|
92241
92741
|
}
|
|
92242
|
-
|
|
92742
|
+
const sampled = sampleValuesFromManifest(manifest, template);
|
|
92743
|
+
const values = {};
|
|
92744
|
+
for (const segment of segments) {
|
|
92745
|
+
values[segment.name] = sampled?.[segment.name] ?? PLACEHOLDER;
|
|
92746
|
+
}
|
|
92747
|
+
routes[template] = values;
|
|
92243
92748
|
}
|
|
92749
|
+
return Object.keys(routes).length === 0 ? null : JSON.stringify({ routes }, null, 2);
|
|
92244
92750
|
}
|
|
92245
|
-
|
|
92246
|
-
|
|
92247
|
-
|
|
92248
|
-
|
|
92249
|
-
|
|
92250
|
-
|
|
92251
|
-
|
|
92252
|
-
|
|
92253
|
-
|
|
92254
|
-
|
|
92255
|
-
|
|
92256
|
-
|
|
92257
|
-
|
|
92258
|
-
|
|
92259
|
-
|
|
92260
|
-
|
|
92261
|
-
|
|
92262
|
-
|
|
92263
|
-
|
|
92264
|
-
|
|
92265
|
-
NEXT_LEAK_DIR: options.workDir
|
|
92266
|
-
},
|
|
92267
|
-
stdio: ["ignore", "ignore", "pipe"]
|
|
92751
|
+
function hasPlaceholders(skeleton) {
|
|
92752
|
+
return skeleton.includes(PLACEHOLDER);
|
|
92753
|
+
}
|
|
92754
|
+
|
|
92755
|
+
// src/unreclaimed-retention.ts
|
|
92756
|
+
var MB = 1024 * 1024;
|
|
92757
|
+
var CLASSES = ["heap", "external", "arrayBuffers"];
|
|
92758
|
+
var MIN_GAP_BYTES = 2 * MB;
|
|
92759
|
+
var MIN_GAP_RATIO = 0.35;
|
|
92760
|
+
var MIN_RETAINED_BYTES = MB;
|
|
92761
|
+
var read = (sample, memoryClass) => memoryClass === "heap" ? sample.heapUsed : memoryClass === "external" ? sample.external : sample.arrayBuffers;
|
|
92762
|
+
function largestGap(values) {
|
|
92763
|
+
return values.reduce((highest, value) => Math.max(highest, value), 0);
|
|
92764
|
+
}
|
|
92765
|
+
function gapsFor(input, memoryClass) {
|
|
92766
|
+
const gaps = [];
|
|
92767
|
+
for (const [index, held] of input.unreclaimedSamples.entries()) {
|
|
92768
|
+
const afterGc = input.memorySamples[index + 1];
|
|
92769
|
+
if (afterGc === void 0) {
|
|
92770
|
+
break;
|
|
92268
92771
|
}
|
|
92269
|
-
|
|
92270
|
-
|
|
92271
|
-
|
|
92272
|
-
|
|
92273
|
-
|
|
92274
|
-
|
|
92275
|
-
|
|
92276
|
-
|
|
92277
|
-
|
|
92278
|
-
|
|
92279
|
-
|
|
92280
|
-
|
|
92281
|
-
|
|
92282
|
-
|
|
92283
|
-
|
|
92284
|
-
|
|
92285
|
-
|
|
92286
|
-
|
|
92287
|
-
|
|
92288
|
-
|
|
92289
|
-
try {
|
|
92290
|
-
entries = (await readdir(options.workDir)).filter(
|
|
92291
|
-
(entry) => entry.startsWith("control-") && entry.endsWith(".json")
|
|
92292
|
-
);
|
|
92293
|
-
} catch {
|
|
92294
|
-
return void 0;
|
|
92295
|
-
}
|
|
92296
|
-
for (const entry of entries) {
|
|
92297
|
-
try {
|
|
92298
|
-
const parsed = controlFileSchema.parse(
|
|
92299
|
-
JSON.parse(await readFile(path.join(options.workDir, entry), "utf8"))
|
|
92300
|
-
);
|
|
92301
|
-
const response = await fetch(`http://127.0.0.1:${parsed.port}/gc`);
|
|
92302
|
-
if (response.ok) {
|
|
92303
|
-
return parsed.port;
|
|
92304
|
-
}
|
|
92305
|
-
} catch {
|
|
92306
|
-
continue;
|
|
92307
|
-
}
|
|
92308
|
-
}
|
|
92309
|
-
return void 0;
|
|
92310
|
-
},
|
|
92311
|
-
failed
|
|
92312
|
-
);
|
|
92313
|
-
await pollUntil(
|
|
92314
|
-
deadline,
|
|
92315
|
-
`app on ${hostname}:${options.appPort}`,
|
|
92316
|
-
async () => {
|
|
92317
|
-
try {
|
|
92318
|
-
await fetch(`http://${hostname}:${options.appPort}/`, { method: "HEAD" });
|
|
92319
|
-
return true;
|
|
92320
|
-
} catch {
|
|
92321
|
-
return void 0;
|
|
92322
|
-
}
|
|
92323
|
-
},
|
|
92324
|
-
failed
|
|
92772
|
+
gaps.push(read(held, memoryClass) - read(afterGc, memoryClass));
|
|
92773
|
+
}
|
|
92774
|
+
return gaps;
|
|
92775
|
+
}
|
|
92776
|
+
function assessUnreclaimedRetention(input) {
|
|
92777
|
+
if (input.verdict === "leak" && input.verdictIsWellSupported || input.unreclaimedSamples.length === 0) {
|
|
92778
|
+
return null;
|
|
92779
|
+
}
|
|
92780
|
+
let worst = null;
|
|
92781
|
+
for (const memoryClass of CLASSES) {
|
|
92782
|
+
const gaps = gapsFor(input, memoryClass);
|
|
92783
|
+
if (gaps.length === 0) {
|
|
92784
|
+
continue;
|
|
92785
|
+
}
|
|
92786
|
+
const largestGapBytes = largestGap(gaps);
|
|
92787
|
+
if (largestGapBytes < MIN_GAP_BYTES) {
|
|
92788
|
+
continue;
|
|
92789
|
+
}
|
|
92790
|
+
const retainedBytes = largestGap(
|
|
92791
|
+
input.memorySamples.slice(1).map((sample) => read(sample, memoryClass))
|
|
92325
92792
|
);
|
|
92326
|
-
|
|
92327
|
-
|
|
92328
|
-
|
|
92329
|
-
|
|
92330
|
-
|
|
92331
|
-
|
|
92332
|
-
|
|
92333
|
-
return;
|
|
92334
|
-
}
|
|
92335
|
-
const gone = new Promise((resolve) => child.once("exit", () => resolve()));
|
|
92336
|
-
child.kill("SIGTERM");
|
|
92337
|
-
const killTimer = setTimeout(() => child.kill("SIGKILL"), 3e3);
|
|
92338
|
-
await gone;
|
|
92339
|
-
clearTimeout(killTimer);
|
|
92340
|
-
}
|
|
92341
|
-
};
|
|
92342
|
-
} catch (cause) {
|
|
92343
|
-
child.kill("SIGKILL");
|
|
92344
|
-
throw cause;
|
|
92793
|
+
const ratio = largestGapBytes / Math.max(retainedBytes, MIN_RETAINED_BYTES);
|
|
92794
|
+
if (ratio < MIN_GAP_RATIO) {
|
|
92795
|
+
continue;
|
|
92796
|
+
}
|
|
92797
|
+
if (worst === null || ratio >= worst.ratio) {
|
|
92798
|
+
worst = { class: memoryClass, largestGapBytes, retainedBytes, ratio };
|
|
92799
|
+
}
|
|
92345
92800
|
}
|
|
92801
|
+
return worst;
|
|
92802
|
+
}
|
|
92803
|
+
var mb = (bytes) => `${(bytes / MB).toFixed(2)} MB`;
|
|
92804
|
+
var CLASS_LABEL = {
|
|
92805
|
+
heap: "heap",
|
|
92806
|
+
external: "external memory",
|
|
92807
|
+
arrayBuffers: "arrayBuffers"
|
|
92808
|
+
};
|
|
92809
|
+
function describeUnreclaimedRetention(retention) {
|
|
92810
|
+
return `held ${mb(retention.largestGapBytes)} of ${CLASS_LABEL[retention.class]} between collections that a GC took back (${retention.ratio.toFixed(1)}x what it retains) \u2014 a forced GC reclaims this, a long-running process may not run one often enough to; read seconds after load, so it also includes memory not yet collected`;
|
|
92346
92811
|
}
|
|
92347
92812
|
|
|
92348
92813
|
// src/report.ts
|
|
92349
|
-
var
|
|
92350
|
-
var formatMb = (bytes) => `${(bytes /
|
|
92814
|
+
var MB2 = 1024 * 1024;
|
|
92815
|
+
var formatMb = (bytes) => `${(bytes / MB2).toFixed(1)} MB`;
|
|
92351
92816
|
var formatGrowth = (bytes) => {
|
|
92352
92817
|
const sign = bytes >= 0 ? "+" : "";
|
|
92353
|
-
return `${sign}${(bytes /
|
|
92818
|
+
return `${sign}${(bytes / MB2).toFixed(2)} MB/1000 req`;
|
|
92354
92819
|
};
|
|
92355
92820
|
var VERDICT_ICON = { leak: "\u2716", stable: "\u2714", inconclusive: "?" };
|
|
92356
|
-
var RSS_MIN_GROWTH_PER_CYCLE = 16 *
|
|
92357
|
-
var RSS_MIN_TOTAL_GROWTH = 64 *
|
|
92821
|
+
var RSS_MIN_GROWTH_PER_CYCLE = 16 * MB2;
|
|
92822
|
+
var RSS_MIN_TOTAL_GROWTH = 64 * MB2;
|
|
92358
92823
|
function hasSustainedRssGrowth(memorySamples) {
|
|
92359
92824
|
const rss = memorySamples.map((sample) => sample.rss);
|
|
92360
92825
|
const first = rss[1];
|
|
@@ -92376,6 +92841,11 @@ function ownerLabel(attribution) {
|
|
|
92376
92841
|
return null;
|
|
92377
92842
|
}
|
|
92378
92843
|
}
|
|
92844
|
+
function revalidationLines(route) {
|
|
92845
|
+
return route.revalidatedEverySeconds === void 0 ? [] : [
|
|
92846
|
+
` driven through ISR revalidation (revalidates every ${route.revalidatedEverySeconds}s; without it the load would serve the cache)`
|
|
92847
|
+
];
|
|
92848
|
+
}
|
|
92379
92849
|
function confidenceLines(route) {
|
|
92380
92850
|
const lines = [];
|
|
92381
92851
|
if (route.confidence.supersededVerdict !== void 0) {
|
|
@@ -92442,6 +92912,15 @@ function peakPressureLines(route, parameters) {
|
|
|
92442
92912
|
});
|
|
92443
92913
|
return pressure === null ? [] : [` \u25B2 peak pressure: ${describePeakPressure(pressure)}`];
|
|
92444
92914
|
}
|
|
92915
|
+
function unreclaimedLines(route, verdict) {
|
|
92916
|
+
const retention = assessUnreclaimedRetention({
|
|
92917
|
+
unreclaimedSamples: route.unreclaimedSamples,
|
|
92918
|
+
memorySamples: route.memorySamples,
|
|
92919
|
+
verdict,
|
|
92920
|
+
verdictIsWellSupported: warrantsIssueDraft(route)
|
|
92921
|
+
});
|
|
92922
|
+
return retention === null ? [] : [` \u25B2 unreclaimed: ${describeUnreclaimedRetention(retention)}`];
|
|
92923
|
+
}
|
|
92445
92924
|
function routeLines(route, parameters) {
|
|
92446
92925
|
if (route.status === "skipped") {
|
|
92447
92926
|
return [` \u2013 ${route.route} skipped: ${route.reason}`];
|
|
@@ -92449,6 +92928,9 @@ function routeLines(route, parameters) {
|
|
|
92449
92928
|
if (route.status === "failed") {
|
|
92450
92929
|
return [` \u2716 ${route.route} failed: ${route.reason}`];
|
|
92451
92930
|
}
|
|
92931
|
+
if (route.status === "not-exercised") {
|
|
92932
|
+
return [` \u2013 ${route.route} not exercised: ${route.reason}`];
|
|
92933
|
+
}
|
|
92452
92934
|
const verdict = effectiveVerdict(route);
|
|
92453
92935
|
const curve = route.samples.map(formatMb).join(" \u2192 ");
|
|
92454
92936
|
const resolved = route.resolvedWithCycles === void 0 ? "" : ` (resolved at ${route.resolvedWithCycles} cycles)`;
|
|
@@ -92456,17 +92938,45 @@ function routeLines(route, parameters) {
|
|
|
92456
92938
|
` ${VERDICT_ICON[verdict]} ${route.route} ${verdict} (${formatGrowth(
|
|
92457
92939
|
route.growthPer1000Requests
|
|
92458
92940
|
)}) heap ${curve}${resolved}`,
|
|
92941
|
+
...revalidationLines(route),
|
|
92459
92942
|
...confidenceLines(route),
|
|
92460
92943
|
...memorySourceLines(route, verdict),
|
|
92461
92944
|
...peakPressureLines(route, parameters),
|
|
92945
|
+
...unreclaimedLines(route, verdict),
|
|
92462
92946
|
...findingLines(route)
|
|
92463
92947
|
];
|
|
92464
92948
|
}
|
|
92949
|
+
function skippedGuidanceLines(report) {
|
|
92950
|
+
const needConfig = report.routes.filter((route) => route.status === "skipped" && route.reason.includes("sample params")).map((route) => route.route);
|
|
92951
|
+
const skeleton = renderConfigSkeleton(needConfig, report.prerender);
|
|
92952
|
+
if (skeleton === null) {
|
|
92953
|
+
return [];
|
|
92954
|
+
}
|
|
92955
|
+
const editing = hasPlaceholders(skeleton) ? ` Replace each ${"REPLACE-ME"} with a value that exists in your app.` : ` The values come from paths your build already prerendered.`;
|
|
92956
|
+
return [
|
|
92957
|
+
"",
|
|
92958
|
+
`${needConfig.length} route(s) need sample params. Write this to next-leak.config.json in the app directory:${editing}`,
|
|
92959
|
+
...skeleton.split("\n").map((line) => ` ${line}`)
|
|
92960
|
+
];
|
|
92961
|
+
}
|
|
92962
|
+
function coverageLine(report) {
|
|
92963
|
+
const total = report.routes.length;
|
|
92964
|
+
const measured = report.routes.filter((route) => route.status === "measured").length;
|
|
92965
|
+
if (measured === total) {
|
|
92966
|
+
return `measured all ${total} discovered route(s)`;
|
|
92967
|
+
}
|
|
92968
|
+
const breakdown = ["skipped", "not-exercised", "failed"].map((status) => ({
|
|
92969
|
+
status,
|
|
92970
|
+
count: report.routes.filter((route) => route.status === status).length
|
|
92971
|
+
})).filter((entry) => entry.count > 0).map((entry) => `${entry.count} ${entry.status.replace("-", " ")}`).join(", ");
|
|
92972
|
+
return `measured ${measured} of ${total} discovered route(s) \u2014 ${breakdown}; the rest is not a verdict about your app`;
|
|
92973
|
+
}
|
|
92465
92974
|
function formatReport(report) {
|
|
92466
92975
|
const lines = [`next-leak \u2014 ${report.appDir}`, ""];
|
|
92467
92976
|
for (const route of report.routes) {
|
|
92468
92977
|
lines.push(...routeLines(route, report.parameters));
|
|
92469
92978
|
}
|
|
92979
|
+
lines.push(...skippedGuidanceLines(report));
|
|
92470
92980
|
const { minGrowthPerCycle, loadRequests, cycles, maxOldSpaceMb } = report.parameters;
|
|
92471
92981
|
const resolvedCycles = [
|
|
92472
92982
|
...new Set(
|
|
@@ -92476,8 +92986,8 @@ function formatReport(report) {
|
|
|
92476
92986
|
)
|
|
92477
92987
|
].sort((a, b) => a - b);
|
|
92478
92988
|
const cyclesLabel = resolvedCycles.length === 0 ? `${cycles} cycles` : `${cycles} cycles (${resolvedCycles.join(", ")} where resolved)`;
|
|
92989
|
+
lines.push("", coverageLine(report));
|
|
92479
92990
|
lines.push(
|
|
92480
|
-
"",
|
|
92481
92991
|
`judged over ${cyclesLabel} \xD7 ${loadRequests} requests, growth gate ${(minGrowthPerCycle / 1024).toFixed(0)} KiB/cycle (${formatGrowth(minGrowthPerCycle / loadRequests * 1e3)}), heap cap ${maxOldSpaceMb} MB`,
|
|
92482
92992
|
`snapshots and run.json: ${report.workDir}`,
|
|
92483
92993
|
`report: ${report.bundle.htmlReport}`,
|
|
@@ -92498,88 +93008,6 @@ function formatReport(report) {
|
|
|
92498
93008
|
return lines.join("\n");
|
|
92499
93009
|
}
|
|
92500
93010
|
|
|
92501
|
-
// src/route-config.ts
|
|
92502
|
-
import { readFile as readFile2 } from "fs/promises";
|
|
92503
|
-
import path2 from "path";
|
|
92504
|
-
import { z as z2 } from "zod";
|
|
92505
|
-
var routeConfigSchema = z2.object({
|
|
92506
|
-
params: z2.record(z2.string(), z2.string()).optional(),
|
|
92507
|
-
routes: z2.record(z2.string(), z2.record(z2.string(), z2.string())).optional(),
|
|
92508
|
-
/**
|
|
92509
|
-
* Headers sent with every request. Real traffic is not header-less:
|
|
92510
|
-
* compression (`Accept-Encoding`), sessions and auth change which code
|
|
92511
|
-
* paths run, and some leaks only exist on those paths (zlib, per-session
|
|
92512
|
-
* caches). Measuring without them measures a different app.
|
|
92513
|
-
*/
|
|
92514
|
-
headers: z2.record(z2.string(), z2.string()).optional(),
|
|
92515
|
-
/** Query string appended per route template, e.g. `"weightKb=2048"`. */
|
|
92516
|
-
query: z2.record(z2.string(), z2.string()).optional(),
|
|
92517
|
-
/**
|
|
92518
|
-
* Simulates clients that give up before the response arrives (closed
|
|
92519
|
-
* tabs, load-balancer timeouts, bots). Several real leaks only exist on
|
|
92520
|
-
* that path — vercel/next.js#89091 traces `ServerResponse` retention to
|
|
92521
|
-
* an early disconnect — and a load generator that always waits politely
|
|
92522
|
-
* never reaches it.
|
|
92523
|
-
*
|
|
92524
|
-
* Counted **from the first byte of the response**, not from the request:
|
|
92525
|
-
* under load a server's first byte arrives long after any sane fixed
|
|
92526
|
-
* window, so a request-relative clock cuts before the stream starts and
|
|
92527
|
-
* tests the wrong path entirely.
|
|
92528
|
-
*/
|
|
92529
|
-
abandonAfterMs: z2.number().int().positive().optional()
|
|
92530
|
-
}).strict();
|
|
92531
|
-
var RouteConfigError = class extends Error {
|
|
92532
|
-
constructor(message) {
|
|
92533
|
-
super(message);
|
|
92534
|
-
this.name = "RouteConfigError";
|
|
92535
|
-
}
|
|
92536
|
-
};
|
|
92537
|
-
var ROUTE_CONFIG_FILE = "next-leak.config.json";
|
|
92538
|
-
async function loadRouteConfig(appDir) {
|
|
92539
|
-
const file = path2.join(appDir, ROUTE_CONFIG_FILE);
|
|
92540
|
-
let raw;
|
|
92541
|
-
try {
|
|
92542
|
-
raw = await readFile2(file, "utf8");
|
|
92543
|
-
} catch {
|
|
92544
|
-
return {};
|
|
92545
|
-
}
|
|
92546
|
-
try {
|
|
92547
|
-
return routeConfigSchema.parse(JSON.parse(raw));
|
|
92548
|
-
} catch (cause) {
|
|
92549
|
-
throw new RouteConfigError(
|
|
92550
|
-
`${file} is invalid: ${cause instanceof Error ? cause.message : String(cause)}`
|
|
92551
|
-
);
|
|
92552
|
-
}
|
|
92553
|
-
}
|
|
92554
|
-
var SEGMENT_PATTERN = /^\[(\[)?(\.\.\.)?([^\]]+?)\]?\]$/;
|
|
92555
|
-
var UNIQUE_MARKER = "{n}";
|
|
92556
|
-
var ENCODED_MARKER = encodeURIComponent(UNIQUE_MARKER);
|
|
92557
|
-
var encodeSegment = (value) => encodeURIComponent(value).split(ENCODED_MARKER).join(UNIQUE_MARKER);
|
|
92558
|
-
var encodeCatchAll = (value) => value.split("/").map(encodeSegment).join("/");
|
|
92559
|
-
function resolveRoutePath(routeTemplate, config) {
|
|
92560
|
-
const resolved = [];
|
|
92561
|
-
for (const segment of routeTemplate.split("/")) {
|
|
92562
|
-
const match = SEGMENT_PATTERN.exec(segment);
|
|
92563
|
-
if (match === null) {
|
|
92564
|
-
resolved.push(encodeSegment(segment));
|
|
92565
|
-
continue;
|
|
92566
|
-
}
|
|
92567
|
-
const optional = match[1] !== void 0;
|
|
92568
|
-
const catchAll = match[2] !== void 0;
|
|
92569
|
-
const name = match[3] ?? "";
|
|
92570
|
-
const value = config.routes?.[routeTemplate]?.[name] ?? config.params?.[name];
|
|
92571
|
-
if (value !== void 0) {
|
|
92572
|
-
resolved.push(catchAll ? encodeCatchAll(value) : encodeSegment(value));
|
|
92573
|
-
} else if (!optional) {
|
|
92574
|
-
return null;
|
|
92575
|
-
}
|
|
92576
|
-
}
|
|
92577
|
-
const joined = resolved.join("/");
|
|
92578
|
-
const path7 = joined === "" ? "/" : joined;
|
|
92579
|
-
const query = config.query?.[routeTemplate];
|
|
92580
|
-
return query === void 0 || query === "" ? path7 : `${path7}?${query}`;
|
|
92581
|
-
}
|
|
92582
|
-
|
|
92583
93011
|
// src/attribution.ts
|
|
92584
93012
|
var UNATTRIBUTED = { owner: "unattributed", source: null, packageName: null };
|
|
92585
93013
|
function classifySource(rawSource) {
|
|
@@ -92845,6 +93273,7 @@ var SNAPSHOT_HEAD = '{"snapshot"';
|
|
|
92845
93273
|
var PROBE_BYTES = 512;
|
|
92846
93274
|
async function assertReadableSnapshot(file) {
|
|
92847
93275
|
const { open, stat } = await import("fs/promises");
|
|
93276
|
+
const { constants } = await import("buffer");
|
|
92848
93277
|
let size;
|
|
92849
93278
|
try {
|
|
92850
93279
|
size = (await stat(file)).size;
|
|
@@ -92854,6 +93283,11 @@ async function assertReadableSnapshot(file) {
|
|
|
92854
93283
|
if (size === 0) {
|
|
92855
93284
|
throw new SnapshotError(`heap snapshot is empty: ${file}`);
|
|
92856
93285
|
}
|
|
93286
|
+
if (size > constants.MAX_STRING_LENGTH) {
|
|
93287
|
+
throw new SnapshotError(
|
|
93288
|
+
`heap snapshot is ${(size / (1024 * 1024)).toFixed(0)} MB, past the ${(constants.MAX_STRING_LENGTH / (1024 * 1024)).toFixed(0)} MB a single string can hold, so it cannot be parsed: ${file}. The measurement itself is unaffected \u2014 only the snapshot diff, which names the retaining object, is unavailable. Lower --requests or --cycles to keep the heap smaller if you need it`
|
|
93289
|
+
);
|
|
93290
|
+
}
|
|
92857
93291
|
const handle = await open(file, "r");
|
|
92858
93292
|
try {
|
|
92859
93293
|
const head = Buffer.alloc(Math.min(PROBE_BYTES, size));
|
|
@@ -92885,80 +93319,6 @@ async function diffSnapshotFiles(baselineFile, afterFile, options = {}, loadHeap
|
|
|
92885
93319
|
return diffAgainstBaseline(summary, after, options);
|
|
92886
93320
|
}
|
|
92887
93321
|
|
|
92888
|
-
// src/manifests.ts
|
|
92889
|
-
import { z as z3 } from "zod";
|
|
92890
|
-
var appPathsManifestSchema = z3.record(z3.string(), z3.string());
|
|
92891
|
-
var pagesManifestSchema = z3.record(z3.string(), z3.string());
|
|
92892
|
-
var routeEntrySchema = z3.looseObject({
|
|
92893
|
-
page: z3.string(),
|
|
92894
|
-
regex: z3.string()
|
|
92895
|
-
});
|
|
92896
|
-
var routesManifestSchema = z3.looseObject({
|
|
92897
|
-
version: z3.number(),
|
|
92898
|
-
basePath: z3.string(),
|
|
92899
|
-
staticRoutes: z3.array(routeEntrySchema),
|
|
92900
|
-
dynamicRoutes: z3.array(routeEntrySchema)
|
|
92901
|
-
});
|
|
92902
|
-
var INTERNAL_PATHS = /* @__PURE__ */ new Set(["/_global-error", "/_not-found"]);
|
|
92903
|
-
var INTERCEPTING_SEGMENT = /^(\(\.{1,3}\))+/;
|
|
92904
|
-
var STATIC_ASSET_PATHS = /* @__PURE__ */ new Set(["/favicon.ico"]);
|
|
92905
|
-
function unaddressableReason(routePath) {
|
|
92906
|
-
if (routePath.split("/").some((segment) => INTERCEPTING_SEGMENT.test(segment))) {
|
|
92907
|
-
return "intercepting route \u2014 only reachable via client navigation";
|
|
92908
|
-
}
|
|
92909
|
-
if (STATIC_ASSET_PATHS.has(routePath)) {
|
|
92910
|
-
return "static asset served by a generated handler \u2014 no user code to leak";
|
|
92911
|
-
}
|
|
92912
|
-
return void 0;
|
|
92913
|
-
}
|
|
92914
|
-
function toRequestPath(manifestKey) {
|
|
92915
|
-
let kind;
|
|
92916
|
-
let withoutSuffix;
|
|
92917
|
-
if (manifestKey.endsWith("/page")) {
|
|
92918
|
-
kind = "page";
|
|
92919
|
-
withoutSuffix = manifestKey.slice(0, -"/page".length);
|
|
92920
|
-
} else if (manifestKey.endsWith("/route")) {
|
|
92921
|
-
kind = "route-handler";
|
|
92922
|
-
withoutSuffix = manifestKey.slice(0, -"/route".length);
|
|
92923
|
-
} else {
|
|
92924
|
-
return null;
|
|
92925
|
-
}
|
|
92926
|
-
const segments = withoutSuffix.split("/").filter((segment) => segment !== "").filter((segment) => !(segment.startsWith("(") && segment.endsWith(")"))).filter((segment) => !segment.startsWith("@"));
|
|
92927
|
-
return { path: `/${segments.join("/")}`, kind };
|
|
92928
|
-
}
|
|
92929
|
-
var PAGES_INTERNAL = /* @__PURE__ */ new Set(["/_app", "/_document", "/_error", "/404", "/500"]);
|
|
92930
|
-
function discoverPagesRoutes(pages) {
|
|
92931
|
-
const routes = [];
|
|
92932
|
-
for (const key of Object.keys(pages)) {
|
|
92933
|
-
if (PAGES_INTERNAL.has(key)) {
|
|
92934
|
-
continue;
|
|
92935
|
-
}
|
|
92936
|
-
routes.push({
|
|
92937
|
-
path: key,
|
|
92938
|
-
kind: key === "/api" || key.startsWith("/api/") ? "route-handler" : "page",
|
|
92939
|
-
dynamic: key.includes("[")
|
|
92940
|
-
});
|
|
92941
|
-
}
|
|
92942
|
-
return routes.sort((a, b) => a.path.localeCompare(b.path));
|
|
92943
|
-
}
|
|
92944
|
-
function discoverRoutes(appPaths) {
|
|
92945
|
-
const routes = /* @__PURE__ */ new Map();
|
|
92946
|
-
for (const key of Object.keys(appPaths)) {
|
|
92947
|
-
const parsed = toRequestPath(key);
|
|
92948
|
-
if (parsed === null || INTERNAL_PATHS.has(parsed.path) || routes.has(parsed.path)) {
|
|
92949
|
-
continue;
|
|
92950
|
-
}
|
|
92951
|
-
const reason = unaddressableReason(parsed.path);
|
|
92952
|
-
routes.set(parsed.path, {
|
|
92953
|
-
path: parsed.path,
|
|
92954
|
-
kind: parsed.kind,
|
|
92955
|
-
dynamic: parsed.path.includes("["),
|
|
92956
|
-
...reason !== void 0 && { unaddressableReason: reason }
|
|
92957
|
-
});
|
|
92958
|
-
}
|
|
92959
|
-
return [...routes.values()].sort((a, b) => a.path.localeCompare(b.path));
|
|
92960
|
-
}
|
|
92961
|
-
|
|
92962
93322
|
// src/vlq.ts
|
|
92963
93323
|
var BASE64 = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
|
|
92964
93324
|
var CHAR_TO_VALUE = new Map([...BASE64].map((char, index) => [char, index]));
|
|
@@ -93028,8 +93388,8 @@ function sourceIndexAt(lines, line, column) {
|
|
|
93028
93388
|
}
|
|
93029
93389
|
|
|
93030
93390
|
// src/module-registry.ts
|
|
93031
|
-
import { readdir as readdir2, readFile as
|
|
93032
|
-
import
|
|
93391
|
+
import { readdir as readdir2, readFile as readFile4 } from "fs/promises";
|
|
93392
|
+
import path4 from "path";
|
|
93033
93393
|
import { z as z4 } from "zod";
|
|
93034
93394
|
var flatMapSchema = z4.looseObject({
|
|
93035
93395
|
version: z4.number(),
|
|
@@ -93153,7 +93513,7 @@ async function jsFilesUnder(dir) {
|
|
|
93153
93513
|
}
|
|
93154
93514
|
for (const entry of entries) {
|
|
93155
93515
|
if (entry.isFile() && entry.name.endsWith(".js")) {
|
|
93156
|
-
out.push(
|
|
93516
|
+
out.push(path4.join(entry.parentPath, entry.name));
|
|
93157
93517
|
}
|
|
93158
93518
|
}
|
|
93159
93519
|
return out;
|
|
@@ -93164,8 +93524,8 @@ async function extractModuleRegistry(nextServerDir) {
|
|
|
93164
93524
|
let code;
|
|
93165
93525
|
let map;
|
|
93166
93526
|
try {
|
|
93167
|
-
code = await
|
|
93168
|
-
map = JSON.parse(await
|
|
93527
|
+
code = await readFile4(file, "utf8");
|
|
93528
|
+
map = JSON.parse(await readFile4(`${file}.map`, "utf8"));
|
|
93169
93529
|
} catch {
|
|
93170
93530
|
continue;
|
|
93171
93531
|
}
|
|
@@ -93200,15 +93560,44 @@ async function describeRedirect(url) {
|
|
|
93200
93560
|
return null;
|
|
93201
93561
|
}
|
|
93202
93562
|
}
|
|
93563
|
+
function boundedRequestSequence(fullUrl, bounded) {
|
|
93564
|
+
const parsed = new URL(fullUrl);
|
|
93565
|
+
const template = fullUrl.slice(parsed.origin.length);
|
|
93566
|
+
let counter = 0;
|
|
93567
|
+
return {
|
|
93568
|
+
origin: parsed.origin,
|
|
93569
|
+
requests: [
|
|
93570
|
+
{
|
|
93571
|
+
setupRequest: (request2) => {
|
|
93572
|
+
const value = String(counter % bounded.bound);
|
|
93573
|
+
counter += 1;
|
|
93574
|
+
return { ...request2, path: template.split(bounded.marker).join(value) };
|
|
93575
|
+
}
|
|
93576
|
+
}
|
|
93577
|
+
]
|
|
93578
|
+
};
|
|
93579
|
+
}
|
|
93580
|
+
function diagnoseFailure(result, connections) {
|
|
93581
|
+
if (result.timeouts > result.non2xx) {
|
|
93582
|
+
return ` \u2014 the failures are timeouts rather than error responses, so the load may simply be more than the app or something it calls can serve; lower --connections (currently ${connections}) and re-run before concluding anything about memory`;
|
|
93583
|
+
}
|
|
93584
|
+
if (result.non2xx > 0) {
|
|
93585
|
+
return ` \u2014 the app answered with errors under load, which is a fault in the route rather than too much traffic; fix that before measuring it`;
|
|
93586
|
+
}
|
|
93587
|
+
return "";
|
|
93588
|
+
}
|
|
93203
93589
|
async function runLoadPhase(options) {
|
|
93204
93590
|
const unique = options.url.includes(UNIQUE_MARKER);
|
|
93205
93591
|
const url = unique ? options.url.split(UNIQUE_MARKER).join("[<id>]") : options.url;
|
|
93592
|
+
const bounded = boundedMarkerOf(options.url);
|
|
93593
|
+
const boundedRequests = bounded === null ? void 0 : boundedRequestSequence(options.url, bounded);
|
|
93206
93594
|
const raw = await (0, import_autocannon.default)({
|
|
93207
|
-
url,
|
|
93595
|
+
url: boundedRequests === void 0 ? url : boundedRequests.origin,
|
|
93208
93596
|
amount: options.amount,
|
|
93209
93597
|
connections: options.connections,
|
|
93210
93598
|
...options.headers !== void 0 && { headers: options.headers },
|
|
93211
|
-
...unique && { idReplacement: true }
|
|
93599
|
+
...unique && { idReplacement: true },
|
|
93600
|
+
...boundedRequests !== void 0 && { requests: boundedRequests.requests }
|
|
93212
93601
|
});
|
|
93213
93602
|
const result = {
|
|
93214
93603
|
sent: raw.requests.sent,
|
|
@@ -93224,7 +93613,7 @@ async function runLoadPhase(options) {
|
|
|
93224
93613
|
const unanswered = failures - result.non2xx - result.errors - result.timeouts;
|
|
93225
93614
|
const redirect = result.non2xx > 0 ? await describeRedirect(options.url) : null;
|
|
93226
93615
|
throw new LoadError(
|
|
93227
|
-
`${failures} of ${options.amount} requests failed against ${options.url} (${result.non2xx} non-2xx, ${result.errors} errors, ${result.timeouts} timeouts` + (unanswered > 0 ? `, ${unanswered} with no recorded response` : "") + ")" + (redirect === null ?
|
|
93616
|
+
`${failures} of ${options.amount} requests failed against ${options.url} (${result.non2xx} non-2xx, ${result.errors} errors, ${result.timeouts} timeouts` + (unanswered > 0 ? `, ${unanswered} with no recorded response` : "") + ")" + (redirect === null ? diagnoseFailure(result, options.connections) : ` \u2014 ${redirect}`),
|
|
93228
93617
|
result
|
|
93229
93618
|
);
|
|
93230
93619
|
}
|
|
@@ -93432,6 +93821,10 @@ var MIN_POLLS_TO_JUDGE = 2;
|
|
|
93432
93821
|
var SETTLE_POLL_MS = 2e3;
|
|
93433
93822
|
var SETTLE_TOLERANCE = 0.01;
|
|
93434
93823
|
var PEAK_POLL_MS = 250;
|
|
93824
|
+
var UNRECLAIMED_SETTLE_MS = SETTLE_POLL_MS;
|
|
93825
|
+
function unreclaimedSettleFor(idleMs) {
|
|
93826
|
+
return Math.min(UNRECLAIMED_SETTLE_MS, Math.floor(idleMs / 4));
|
|
93827
|
+
}
|
|
93435
93828
|
function pollPeak(controlPort, phase, deps) {
|
|
93436
93829
|
const peak = {
|
|
93437
93830
|
phase,
|
|
@@ -93592,6 +93985,8 @@ async function runRitual(options, deps = defaultDeps) {
|
|
|
93592
93985
|
() => requestSnapshot(app.controlPort, "baseline")
|
|
93593
93986
|
);
|
|
93594
93987
|
const memorySamples = [baseline.sample];
|
|
93988
|
+
const unreclaimedSamples = [];
|
|
93989
|
+
let unreclaimedLost = false;
|
|
93595
93990
|
const peaks = [];
|
|
93596
93991
|
let afterSnapshot = "";
|
|
93597
93992
|
for (let cycle = 1; cycle <= cycles; cycle += 1) {
|
|
@@ -93603,9 +93998,18 @@ async function runRitual(options, deps = defaultDeps) {
|
|
|
93603
93998
|
peaks.push(await poller.stop());
|
|
93604
93999
|
}
|
|
93605
94000
|
});
|
|
94001
|
+
const unreclaimedSettleMs = unreclaimedSettleFor(idleMs);
|
|
94002
|
+
await timed(`cycle ${cycle} unreclaimed read`, async () => {
|
|
94003
|
+
await deps.sleep(unreclaimedSettleMs);
|
|
94004
|
+
try {
|
|
94005
|
+
unreclaimedSamples.push(await deps.readMemory(app.controlPort));
|
|
94006
|
+
} catch {
|
|
94007
|
+
unreclaimedLost = true;
|
|
94008
|
+
}
|
|
94009
|
+
});
|
|
93606
94010
|
const settle = await timed(
|
|
93607
94011
|
`cycle ${cycle} settle`,
|
|
93608
|
-
() => waitUntilSettled(app.controlPort, idleMs, deps)
|
|
94012
|
+
() => waitUntilSettled(app.controlPort, idleMs - unreclaimedSettleMs, deps)
|
|
93609
94013
|
);
|
|
93610
94014
|
settleOutcomes.push({ phase: `cycle ${cycle}`, ...settle });
|
|
93611
94015
|
if (cycle === cycles) {
|
|
@@ -93630,6 +94034,14 @@ async function runRitual(options, deps = defaultDeps) {
|
|
|
93630
94034
|
samples,
|
|
93631
94035
|
memorySamples,
|
|
93632
94036
|
peaks,
|
|
94037
|
+
unreclaimedSamples: unreclaimedLost ? [] : unreclaimedSamples,
|
|
94038
|
+
// The first cycle plays the part the baseline plays for the post-GC
|
|
94039
|
+
// series, so both trends drop a warm-up delta and stay comparable.
|
|
94040
|
+
unreclaimedTrend: unreclaimedLost ? { verdict: "inconclusive", growthPerCycle: 0, deltas: [] } : classifyMemoryTrend(
|
|
94041
|
+
unreclaimedSamples.map((sample) => sample.heapUsed),
|
|
94042
|
+
unreclaimedSamples.map((sample) => sample.external),
|
|
94043
|
+
{ minGrowthPerCycle }
|
|
94044
|
+
),
|
|
93633
94045
|
baselineSnapshot: baseline.file,
|
|
93634
94046
|
afterSnapshot,
|
|
93635
94047
|
trend: classifyMemoryTrend(samples, externalSamples, { minGrowthPerCycle }),
|
|
@@ -93648,8 +94060,8 @@ async function runRitual(options, deps = defaultDeps) {
|
|
|
93648
94060
|
}
|
|
93649
94061
|
|
|
93650
94062
|
// src/signatures.ts
|
|
93651
|
-
import { readFile as
|
|
93652
|
-
import
|
|
94063
|
+
import { readFile as readFile5 } from "fs/promises";
|
|
94064
|
+
import path5 from "path";
|
|
93653
94065
|
import semver from "semver";
|
|
93654
94066
|
import { z as z6 } from "zod";
|
|
93655
94067
|
var signatureSchema = z6.object({
|
|
@@ -93726,12 +94138,12 @@ function matchSignatures(diff, nextVersion, signatures = loadSignatures()) {
|
|
|
93726
94138
|
}
|
|
93727
94139
|
async function readNextVersion(appDir) {
|
|
93728
94140
|
const candidates = [
|
|
93729
|
-
|
|
93730
|
-
|
|
94141
|
+
path5.join(appDir, ".next", "standalone", "node_modules", "next", "package.json"),
|
|
94142
|
+
path5.join(appDir, "node_modules", "next", "package.json")
|
|
93731
94143
|
];
|
|
93732
94144
|
for (const candidate of candidates) {
|
|
93733
94145
|
try {
|
|
93734
|
-
const parsed = z6.object({ version: z6.string() }).parse(JSON.parse(await
|
|
94146
|
+
const parsed = z6.object({ version: z6.string() }).parse(JSON.parse(await readFile5(candidate, "utf8")));
|
|
93735
94147
|
return parsed.version;
|
|
93736
94148
|
} catch {
|
|
93737
94149
|
continue;
|
|
@@ -93740,88 +94152,54 @@ async function readNextVersion(appDir) {
|
|
|
93740
94152
|
return null;
|
|
93741
94153
|
}
|
|
93742
94154
|
|
|
93743
|
-
// src/
|
|
93744
|
-
import {
|
|
93745
|
-
import
|
|
93746
|
-
|
|
93747
|
-
|
|
93748
|
-
|
|
93749
|
-
|
|
93750
|
-
|
|
93751
|
-
|
|
93752
|
-
|
|
93753
|
-
|
|
93754
|
-
|
|
93755
|
-
try {
|
|
93756
|
-
await access(file);
|
|
93757
|
-
return true;
|
|
93758
|
-
} catch {
|
|
93759
|
-
return false;
|
|
94155
|
+
// src/runner.ts
|
|
94156
|
+
import { createHash as createHash3 } from "crypto";
|
|
94157
|
+
import { mkdir as mkdir2, writeFile } from "fs/promises";
|
|
94158
|
+
import { createServer } from "net";
|
|
94159
|
+
import path6 from "path";
|
|
94160
|
+
|
|
94161
|
+
// src/isr.ts
|
|
94162
|
+
var REVALIDATE_HEADER = "x-prerender-revalidate";
|
|
94163
|
+
function revalidateSecondsFor(manifest, route) {
|
|
94164
|
+
const routes = manifest?.routes;
|
|
94165
|
+
if (routes === void 0) {
|
|
94166
|
+
return null;
|
|
93760
94167
|
}
|
|
93761
|
-
|
|
93762
|
-
|
|
93763
|
-
|
|
93764
|
-
try {
|
|
93765
|
-
raw = JSON.parse(await readFile5(file, "utf8"));
|
|
93766
|
-
} catch (cause) {
|
|
93767
|
-
throw new TargetError(
|
|
93768
|
-
"BAD_MANIFEST",
|
|
93769
|
-
`Could not read ${file}: ${cause instanceof Error ? cause.message : String(cause)}`
|
|
93770
|
-
);
|
|
94168
|
+
const direct = routes[route]?.initialRevalidateSeconds;
|
|
94169
|
+
if (typeof direct === "number") {
|
|
94170
|
+
return direct;
|
|
93771
94171
|
}
|
|
93772
|
-
|
|
93773
|
-
|
|
93774
|
-
|
|
93775
|
-
|
|
93776
|
-
"BAD_MANIFEST",
|
|
93777
|
-
`${file} does not match the expected Next.js manifest shape. This Next.js version may not be supported yet.`
|
|
93778
|
-
);
|
|
94172
|
+
for (const entry of Object.values(routes)) {
|
|
94173
|
+
if (entry.srcRoute === route && typeof entry.initialRevalidateSeconds === "number") {
|
|
94174
|
+
return entry.initialRevalidateSeconds;
|
|
94175
|
+
}
|
|
93779
94176
|
}
|
|
94177
|
+
return null;
|
|
93780
94178
|
}
|
|
93781
|
-
|
|
93782
|
-
|
|
93783
|
-
|
|
93784
|
-
|
|
93785
|
-
|
|
93786
|
-
|
|
93787
|
-
|
|
94179
|
+
function revalidates(manifest, route) {
|
|
94180
|
+
return revalidateSecondsFor(manifest, route) !== null;
|
|
94181
|
+
}
|
|
94182
|
+
function planRevalidation(manifest, route, userHeaders) {
|
|
94183
|
+
const userSupplied = Object.keys(userHeaders ?? {}).some(
|
|
94184
|
+
(name) => name.toLowerCase() === REVALIDATE_HEADER
|
|
94185
|
+
);
|
|
94186
|
+
if (userSupplied) {
|
|
94187
|
+
return { kind: "drive", headers: {} };
|
|
93788
94188
|
}
|
|
93789
|
-
|
|
93790
|
-
|
|
93791
|
-
throw new TargetError(
|
|
93792
|
-
"NO_STANDALONE",
|
|
93793
|
-
`No ${standaloneServer}.
|
|
93794
|
-
next-leak measures the standalone server bundle. Enable it once in next.config:
|
|
93795
|
-
|
|
93796
|
-
const nextConfig = {
|
|
93797
|
-
output: "standalone",
|
|
93798
|
-
// ...your existing config
|
|
93799
|
-
};
|
|
93800
|
-
|
|
93801
|
-
then rebuild: next build
|
|
93802
|
-
This only changes how the build is packaged \u2014 not how your app behaves.`
|
|
93803
|
-
);
|
|
94189
|
+
if (!revalidates(manifest, route)) {
|
|
94190
|
+
return { kind: "not-isr" };
|
|
93804
94191
|
}
|
|
93805
|
-
const
|
|
93806
|
-
|
|
93807
|
-
|
|
93808
|
-
|
|
93809
|
-
|
|
93810
|
-
|
|
93811
|
-
"BAD_MANIFEST",
|
|
93812
|
-
`No routes found in ${nextDir}/server: neither app-paths-manifest.json nor pages-manifest.json is present. Rebuild the app with "next build".`
|
|
93813
|
-
);
|
|
94192
|
+
const previewModeId = manifest?.preview?.previewModeId;
|
|
94193
|
+
if (previewModeId === void 0 || previewModeId === "") {
|
|
94194
|
+
return {
|
|
94195
|
+
kind: "cannot-drive",
|
|
94196
|
+
reason: `revalidates every ${revalidateSecondsFor(manifest, route)}s, but the build's prerender-manifest.json carries no previewModeId \u2014 load would serve the cache and measure nothing. Set "${REVALIDATE_HEADER}" in next-leak.config.json to drive it.`
|
|
94197
|
+
};
|
|
93814
94198
|
}
|
|
93815
|
-
|
|
93816
|
-
const routes = await exists(routesFile) ? await readManifest(routesFile, (raw) => routesManifestSchema.parse(raw)) : void 0;
|
|
93817
|
-
return { appDir: path5.resolve(appDir), standaloneServer, appPaths, pages, routes };
|
|
94199
|
+
return { kind: "drive", headers: { [REVALIDATE_HEADER]: previewModeId } };
|
|
93818
94200
|
}
|
|
93819
94201
|
|
|
93820
94202
|
// src/runner.ts
|
|
93821
|
-
import { createHash as createHash3 } from "crypto";
|
|
93822
|
-
import { mkdir as mkdir2, writeFile } from "fs/promises";
|
|
93823
|
-
import { createServer } from "net";
|
|
93824
|
-
import path6 from "path";
|
|
93825
94203
|
var ESTIMATED_RPS = 250;
|
|
93826
94204
|
var PER_ROUTE_OVERHEAD_SECONDS = 10;
|
|
93827
94205
|
var MIN_SETTLE_SECONDS = 4;
|
|
@@ -93928,6 +94306,12 @@ function skipReason(route, requestPath) {
|
|
|
93928
94306
|
}
|
|
93929
94307
|
async function measureRoute(context, route, requestPath, index, pass = void 0) {
|
|
93930
94308
|
const { deps, options, target, workDir, routeConfig, registry, nextVersion, progress } = context;
|
|
94309
|
+
const plan = planRevalidation(target.prerender, route.path, routeConfig.headers);
|
|
94310
|
+
const revalidateSeconds = revalidateSecondsFor(target.prerender, route.path);
|
|
94311
|
+
const bounded = boundedMarkerOf(requestPath);
|
|
94312
|
+
const driven = plan.kind === "drive" ? plan.headers : {};
|
|
94313
|
+
const merged = { ...driven, ...routeConfig.headers ?? {} };
|
|
94314
|
+
const headers = Object.keys(merged).length === 0 ? void 0 : merged;
|
|
93931
94315
|
const result = await deps.ritual({
|
|
93932
94316
|
serverPath: target.standaloneServer,
|
|
93933
94317
|
route: requestPath,
|
|
@@ -93943,7 +94327,7 @@ async function measureRoute(context, route, requestPath, index, pass = void 0) {
|
|
|
93943
94327
|
...pass !== void 0 ? { cycles: pass.cycles } : options.cycles !== void 0 && { cycles: options.cycles },
|
|
93944
94328
|
...options.idleMs !== void 0 && { idleMs: options.idleMs },
|
|
93945
94329
|
...options.maxOldSpaceMb !== void 0 && { maxOldSpaceMb: options.maxOldSpaceMb },
|
|
93946
|
-
...
|
|
94330
|
+
...headers !== void 0 && { headers },
|
|
93947
94331
|
...routeConfig.abandonAfterMs !== void 0 && {
|
|
93948
94332
|
abandonAfterMs: routeConfig.abandonAfterMs
|
|
93949
94333
|
}
|
|
@@ -93957,6 +94341,7 @@ async function measureRoute(context, route, requestPath, index, pass = void 0) {
|
|
|
93957
94341
|
minGrowthPerCycle: result.minGrowthPerCycle,
|
|
93958
94342
|
memorySamples: result.memorySamples,
|
|
93959
94343
|
maxOldSpaceMb: options.maxOldSpaceMb ?? DEFAULT_MAX_OLD_SPACE_MB,
|
|
94344
|
+
warmupRequests: options.warmupRequests ?? RITUAL_DEFAULTS.warmupRequests,
|
|
93960
94345
|
...routeConfig.abandonAfterMs !== void 0 && {
|
|
93961
94346
|
abandonAfterMs: routeConfig.abandonAfterMs
|
|
93962
94347
|
}
|
|
@@ -93968,7 +94353,13 @@ async function measureRoute(context, route, requestPath, index, pass = void 0) {
|
|
|
93968
94353
|
let diff = null;
|
|
93969
94354
|
if (verdict !== "stable" || options.diffAll === true) {
|
|
93970
94355
|
progress(`diffing snapshots for ${route.path}`);
|
|
93971
|
-
|
|
94356
|
+
try {
|
|
94357
|
+
diff = await deps.diff(result.baselineSnapshot, result.afterSnapshot);
|
|
94358
|
+
} catch (cause) {
|
|
94359
|
+
progress(
|
|
94360
|
+
`snapshot diff unavailable for ${route.path}: ${cause instanceof Error ? cause.message : String(cause)}`
|
|
94361
|
+
);
|
|
94362
|
+
}
|
|
93972
94363
|
}
|
|
93973
94364
|
return {
|
|
93974
94365
|
route: route.path,
|
|
@@ -93978,6 +94369,11 @@ async function measureRoute(context, route, requestPath, index, pass = void 0) {
|
|
|
93978
94369
|
samples: result.samples,
|
|
93979
94370
|
memorySamples: result.memorySamples,
|
|
93980
94371
|
peaks: result.peaks,
|
|
94372
|
+
...revalidateSeconds !== null && { revalidatedEverySeconds: revalidateSeconds },
|
|
94373
|
+
...bounded !== null && { keyCardinality: bounded.bound },
|
|
94374
|
+
unreclaimedSamples: result.unreclaimedSamples,
|
|
94375
|
+
unreclaimedTrend: result.unreclaimedTrend,
|
|
94376
|
+
requestsPerCycle: result.requestsPerCycle,
|
|
93981
94377
|
timings: result.timings,
|
|
93982
94378
|
loadOutcomes: result.loadOutcomes,
|
|
93983
94379
|
settleOutcomes: result.settleOutcomes,
|
|
@@ -93993,7 +94389,7 @@ async function measureRoute(context, route, requestPath, index, pass = void 0) {
|
|
|
93993
94389
|
};
|
|
93994
94390
|
}
|
|
93995
94391
|
async function writeEvidenceBundle(report, workDir) {
|
|
93996
|
-
const { renderHtmlReport } = await import("./html-report-
|
|
94392
|
+
const { renderHtmlReport } = await import("./html-report-2UAOOZQO.js");
|
|
93997
94393
|
const { renderIssueMarkdown } = await import("./issue-report-HKA26WNV.js");
|
|
93998
94394
|
for (const route of report.routes) {
|
|
93999
94395
|
if (route.status === "measured" && warrantsIssueDraft(route)) {
|
|
@@ -94013,6 +94409,11 @@ async function routeReportFor(context, route, index, total) {
|
|
|
94013
94409
|
progress(`skipping ${label}: ${reason ?? "needs sample params"}`);
|
|
94014
94410
|
return { route: route.path, status: "skipped", reason: reason ?? "needs sample params" };
|
|
94015
94411
|
}
|
|
94412
|
+
const plan = planRevalidation(context.target.prerender, route.path, routeConfig.headers);
|
|
94413
|
+
if (plan.kind === "cannot-drive") {
|
|
94414
|
+
progress(`not measuring ${label}: ${plan.reason}`);
|
|
94415
|
+
return { route: route.path, status: "not-exercised", reason: plan.reason };
|
|
94416
|
+
}
|
|
94016
94417
|
try {
|
|
94017
94418
|
const asPath = requestPath === route.path ? "" : ` as ${requestPath}`;
|
|
94018
94419
|
progress(`measuring ${label}${asPath}`);
|
|
@@ -94108,6 +94509,7 @@ async function runMeasurement(options, deps = defaultDeps2) {
|
|
|
94108
94509
|
appDir: target.appDir,
|
|
94109
94510
|
startedAt: startedAt.toISOString(),
|
|
94110
94511
|
workDir,
|
|
94512
|
+
...target.prerender !== void 0 && { prerender: target.prerender },
|
|
94111
94513
|
environment: captureEnvironment(nextVersion),
|
|
94112
94514
|
parameters,
|
|
94113
94515
|
routes: reports,
|
|
@@ -94132,17 +94534,27 @@ async function runMeasurement(options, deps = defaultDeps2) {
|
|
|
94132
94534
|
}
|
|
94133
94535
|
|
|
94134
94536
|
export {
|
|
94135
|
-
|
|
94136
|
-
|
|
94137
|
-
|
|
94537
|
+
appPathsManifestSchema,
|
|
94538
|
+
routesManifestSchema,
|
|
94539
|
+
discoverPagesRoutes,
|
|
94540
|
+
discoverRoutes,
|
|
94541
|
+
TargetError,
|
|
94542
|
+
validateTarget,
|
|
94138
94543
|
LaunchError,
|
|
94139
94544
|
killActiveChildren,
|
|
94545
|
+
registerChild,
|
|
94546
|
+
unregisterChild,
|
|
94140
94547
|
launchInstrumented,
|
|
94141
|
-
|
|
94548
|
+
helpText,
|
|
94549
|
+
parseCliArgs,
|
|
94550
|
+
checkRuntime,
|
|
94142
94551
|
RouteConfigError,
|
|
94143
94552
|
ROUTE_CONFIG_FILE,
|
|
94144
94553
|
loadRouteConfig,
|
|
94145
94554
|
resolveRoutePath,
|
|
94555
|
+
renderConfigSkeleton,
|
|
94556
|
+
hasPlaceholders,
|
|
94557
|
+
formatReport,
|
|
94146
94558
|
classifySource,
|
|
94147
94559
|
attributeFinding,
|
|
94148
94560
|
attributeDiff,
|
|
@@ -94153,9 +94565,6 @@ export {
|
|
|
94153
94565
|
SnapshotError,
|
|
94154
94566
|
assertReadableSnapshot,
|
|
94155
94567
|
diffSnapshotFiles,
|
|
94156
|
-
appPathsManifestSchema,
|
|
94157
|
-
routesManifestSchema,
|
|
94158
|
-
discoverRoutes,
|
|
94159
94568
|
decodeVlqLine,
|
|
94160
94569
|
decodeMappings,
|
|
94161
94570
|
sourceIndexAt,
|
|
@@ -94167,8 +94576,6 @@ export {
|
|
|
94167
94576
|
loadSignatures,
|
|
94168
94577
|
matchSignatures,
|
|
94169
94578
|
readNextVersion,
|
|
94170
|
-
TargetError,
|
|
94171
|
-
validateTarget,
|
|
94172
94579
|
freePort,
|
|
94173
94580
|
routeSlug,
|
|
94174
94581
|
runMeasurement
|