@mapled/cli 0.11.0 → 0.13.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/dist/live.js ADDED
@@ -0,0 +1,2346 @@
1
+ import { readdir, readFile, stat } from "node:fs/promises";
2
+ import path from "node:path";
3
+ import { appRoute, listFiles, tokenize } from "./scan.js";
4
+ /** Live updates (§18.8) as the site's code shows them. A Next.js site
5
+ follows Publish in open tabs with two pieces: `<MapledLive />` from
6
+ "@mapled/next/live" (or `watchRelease` from "@mapled/next") asks a
7
+ route of the site every few seconds, and that route answers with
8
+ `createReleaseHandler` from "@mapled/next/server". A listener without
9
+ the route polls a 404 forever and nothing says so — `mapled doctor`
10
+ reads both and says which is missing, and `mapled scan` records the
11
+ result in the manifest (`capabilities.realtime`, `releaseRoute`) so
12
+ MAPLED.md can tell the next agent. The code is read with the scanner's
13
+ tokenizer: comments and strings never count as a use, and neither
14
+ does an imported name the file binds again. */
15
+ /** Where `<MapledLive />` and `watchRelease` ask when no endpoint is given. */
16
+ export const RELEASE_ROUTE = "/api/mapled/release";
17
+ /** The `@mapled/next` from which that default follows the site's basePath —
18
+ before it, a listener with no endpoint asks RELEASE_ROUTE as written. */
19
+ export const BASE_PATH_SINCE = "0.10.0";
20
+ /** The extensions a route file may have when next.config doesn't set `pageExtensions`. */
21
+ export const PAGE_EXTENSIONS = ["tsx", "ts", "jsx", "js"];
22
+ // the config files Next reads, in its order
23
+ const NEXT_CONFIGS = ["next.config.js", "next.config.mjs", "next.config.ts", "next.config.mts", "next.config.cjs"];
24
+ // the three names as written, or an escape that may spell one: `import { w\u0061tchRelease as w }`
25
+ const WORDS = /MapledLive|watchRelease|createReleaseHandler|\\u/;
26
+ /** A member `object.name` as one name among the file's: no identifier
27
+ reads the same. A namespace's `ns.createReleaseHandler` is one — a
28
+ namespace can't be written to, so its member is the import wherever
29
+ `ns` itself is. */
30
+ const memberName = (object, name) => `${object}.${name}`;
31
+ /** The imports of the three exports, and of their modules as namespaces; type-only imports don't count. */
32
+ function importsOf(tokens) {
33
+ const found = { live: new Set(), watch: new Set(), handler: new Set(), ns: { live: new Set(), watch: new Set(), handler: new Set() } };
34
+ for (let i = 0; i < tokens.length; i++) {
35
+ const t = tokens[i];
36
+ if (t.kind !== "ident" || t.value !== "import")
37
+ continue;
38
+ if (tokens[i + 1]?.kind === "ident" && tokens[i + 1].value === "type")
39
+ continue;
40
+ const names = [];
41
+ let namespace = null;
42
+ let j = i + 1;
43
+ let source = null;
44
+ let inBraces = false;
45
+ for (; j < tokens.length && j < i + 200; j++) {
46
+ const u = tokens[j];
47
+ if (u.kind === "punct" && u.value === "{")
48
+ inBraces = true;
49
+ else if (u.kind === "punct" && u.value === "}")
50
+ inBraces = false;
51
+ else if (u.kind === "ident" && u.value === "from" && !inBraces) {
52
+ source = tokens[j + 1]?.kind === "string" ? tokens[j + 1].value : null;
53
+ break;
54
+ }
55
+ else if (u.kind === "punct" && u.value === ";")
56
+ break;
57
+ else if (!inBraces && u.kind === "punct" && u.value === "*" && tokens[j + 1]?.kind === "ident" && tokens[j + 1].value === "as" && tokens[j + 2]?.kind === "ident") {
58
+ // `import * as ns`, `import def, * as ns`
59
+ namespace = tokens[j + 2].value;
60
+ j += 2;
61
+ }
62
+ else if (inBraces && u.kind === "ident") {
63
+ if (u.value === "type")
64
+ continue;
65
+ const prev = tokens[j - 1];
66
+ if (prev?.kind === "ident" && prev.value === "as")
67
+ continue;
68
+ const local = tokens[j + 1]?.kind === "ident" && tokens[j + 1].value === "as" && tokens[j + 2]?.kind === "ident" ? tokens[j + 2].value : u.value;
69
+ names.push({ imported: u.value, local });
70
+ }
71
+ }
72
+ i = j;
73
+ for (const n of names) {
74
+ if (source === "@mapled/next/live" && n.imported === "MapledLive")
75
+ found.live.add(n.local);
76
+ if (source === "@mapled/next" && n.imported === "watchRelease")
77
+ found.watch.add(n.local);
78
+ if (source === "@mapled/next/server" && n.imported === "createReleaseHandler")
79
+ found.handler.add(n.local);
80
+ }
81
+ if (namespace !== null) {
82
+ if (source === "@mapled/next/live")
83
+ found.ns.live.add(namespace);
84
+ if (source === "@mapled/next")
85
+ found.ns.watch.add(namespace);
86
+ if (source === "@mapled/next/server")
87
+ found.ns.handler.add(namespace);
88
+ }
89
+ }
90
+ return found;
91
+ }
92
+ const isPunct = (t, value) => t?.kind === "punct" && t.value === value;
93
+ const isWord = (t, value) => t?.kind === "ident" && t.value === value;
94
+ const isSpread = (tokens, k) => isPunct(tokens[k], ".") && isPunct(tokens[k + 1], ".") && isPunct(tokens[k + 2], ".");
95
+ /** A string the code writes out: closed, and not a template with a substitution (its value reads `${}`). */
96
+ const literal = (t) => t?.kind === "string" && !t.unclosed && t.value !== "${}";
97
+ // how far an element's attributes or a call's options are read; past it the endpoint can't be told
98
+ const OPTIONS_TOKENS = 2000;
99
+ /** The endpoint of a JSX element's attributes, from just after its name.
100
+ The last of `endpoint=` and spread props wins, as in JSX; null when the
101
+ code decides it — an expression, or spread props that may carry one —
102
+ and undefined when the element gives none: the SDK's default. */
103
+ function jsxEndpoint(tokens, from) {
104
+ let endpoint = undefined;
105
+ let depth = 0;
106
+ for (let k = from; k < tokens.length; k++) {
107
+ if (k >= from + OPTIONS_TOKENS)
108
+ return null;
109
+ const t = tokens[k];
110
+ if (t.kind === "punct" && t.value === "{") {
111
+ if (depth === 0 && isSpread(tokens, k + 1))
112
+ endpoint = null;
113
+ depth++;
114
+ }
115
+ else if (t.kind === "punct" && t.value === "}") {
116
+ // the code around the element closes before it does
117
+ if (depth === 0)
118
+ return endpoint;
119
+ depth--;
120
+ }
121
+ else if (depth === 0 && t.kind === "punct" && (t.value === ">" || t.value === "<" || t.value === ";"))
122
+ return endpoint;
123
+ // the whole attribute name: `data-endpoint`, `x:endpoint` and `endpoint-x` are other attributes
124
+ else if (depth === 0 && isWord(t, "endpoint") && !isPunct(tokens[k - 1], "-") && !isPunct(tokens[k - 1], ":") && !isPunct(tokens[k + 1], "-") && !isPunct(tokens[k + 1], ":")) {
125
+ // `<MapledLive endpoint />` passes true, not the default; a JSX name takes no escapes, so an escaped one can't be read
126
+ if (!isPunct(tokens[k + 1], "=") || t.escaped === true) {
127
+ endpoint = null;
128
+ continue;
129
+ }
130
+ const v = tokens[k + 2];
131
+ // a string a line break cut off runs on in JSX: past it the element can't be read
132
+ if (v?.kind === "string" && v.unclosed)
133
+ return null;
134
+ // a JSX attribute's string reads no backslash escapes and decodes HTML entities: with either, it isn't the text the tokenizer read
135
+ if (v?.kind === "string")
136
+ endpoint = v.escaped || v.value.includes("&") ? null : v.value;
137
+ else if (isPunct(v, "{") && literal(tokens[k + 3]) && isPunct(tokens[k + 4], "}"))
138
+ endpoint = tokens[k + 3].value;
139
+ else
140
+ endpoint = null;
141
+ }
142
+ }
143
+ // the file ends after the attributes
144
+ return endpoint;
145
+ }
146
+ // words that may stand before a property's key: `get endpoint() {…}`, `async *endpoint() {…}`
147
+ const KEY_PREFIX = new Set(["get", "set", "async", "static"]);
148
+ /** The key of the object property that starts at `k` and the token after
149
+ it; `name` is null when the code decides the key — `[expr]` — and
150
+ undefined when no property starts there (`{}`, a trailing comma). */
151
+ function propertyKey(tokens, k) {
152
+ // a prefix word is the key itself when what follows ends it: `{ get: 1 }`, `{ async }`, `{ set() {} }`
153
+ const ends = (u) => isPunct(u, ":") || isPunct(u, "(") || isPunct(u, ",") || isPunct(u, "}") || isPunct(u, "=");
154
+ while ((isPunct(tokens[k], "*") || (tokens[k]?.kind === "ident" && KEY_PREFIX.has(tokens[k].value) && !ends(tokens[k + 1]))) && k < tokens.length)
155
+ k++;
156
+ const t = tokens[k];
157
+ if (isPunct(t, "[")) {
158
+ // a literal computed key reads as written; any other — a template with a substitution too — is the code's
159
+ if (literal(tokens[k + 1]) && isPunct(tokens[k + 2], "]"))
160
+ return { name: tokens[k + 1].value, next: k + 3 };
161
+ return { name: null, next: k + 1 };
162
+ }
163
+ if (t?.kind === "string" && !literal(t))
164
+ return { name: null, next: k + 1 };
165
+ if (t?.kind === "ident" || t?.kind === "string")
166
+ return { name: t.value, next: k + 1 };
167
+ // a numeric key: `{ 1: … }`
168
+ if (t?.kind === "punct" && /^\d$/.test(t.value))
169
+ return { name: t.value, next: k + 1 };
170
+ return { name: undefined, next: k };
171
+ }
172
+ /** The endpoint of a call's options object, from its opening parenthesis,
173
+ read property by property: the last `endpoint: "…"` or spread wins;
174
+ null when the code decides it — options from elsewhere, a spread, a
175
+ key the reading can't read (`[key]`), or an `endpoint` that isn't a
176
+ plain string: a shorthand, a method, a getter, an expression — and
177
+ undefined when the options give none: the SDK's default. */
178
+ function callEndpoint(tokens, paren) {
179
+ if (!isPunct(tokens[paren + 1], "{"))
180
+ return null;
181
+ let endpoint = undefined;
182
+ let depth = 0;
183
+ let property = false;
184
+ for (let k = paren + 1; k < tokens.length && k < paren + OPTIONS_TOKENS; k++) {
185
+ const t = tokens[k];
186
+ if (depth === 1 && property) {
187
+ property = false;
188
+ if (isSpread(tokens, k))
189
+ endpoint = null;
190
+ else {
191
+ const { name, next } = propertyKey(tokens, k);
192
+ if (name === null)
193
+ endpoint = null;
194
+ else if (name === "endpoint") {
195
+ const v = tokens[next + 1];
196
+ const after = tokens[next + 2];
197
+ endpoint = isPunct(tokens[next], ":") && literal(v) && (isPunct(after, ",") || isPunct(after, "}")) ? v.value : null;
198
+ }
199
+ }
200
+ }
201
+ if (t.kind === "punct" && (t.value === "{" || t.value === "(" || t.value === "[")) {
202
+ depth++;
203
+ if (depth === 1)
204
+ property = true;
205
+ }
206
+ else if (t.kind === "punct" && (t.value === "}" || t.value === ")" || t.value === "]")) {
207
+ depth--;
208
+ if (depth === 0)
209
+ return endpoint;
210
+ }
211
+ else if (depth === 1 && isPunct(t, ","))
212
+ property = true;
213
+ }
214
+ return null;
215
+ }
216
+ const DECLARE = new Set(["const", "let", "var"]);
217
+ // words that start a statement and never continue an expression
218
+ const STATEMENT = new Set(["export", "import", "const", "let", "var", "return", "if", "for", "while", "do", "throw", "try", "switch"]);
219
+ const BEFORE_FUNCTION_VALUE = new Set(["async", "new", "await", "return", "typeof", "void", "yield", "default"]);
220
+ function bracketsOf(tokens) {
221
+ const after = new Int32Array(tokens.length + 1).fill(tokens.length);
222
+ const open = new Int32Array(tokens.length).fill(-1);
223
+ const parent = new Int32Array(tokens.length).fill(-1);
224
+ const stack = [];
225
+ for (let k = 0; k < tokens.length; k++) {
226
+ const t = tokens[k];
227
+ if (t.kind === "punct" && (t.value === "}" || t.value === ")" || t.value === "]")) {
228
+ const o = stack.pop();
229
+ if (o !== undefined) {
230
+ after[o] = k + 1;
231
+ open[k] = o;
232
+ }
233
+ }
234
+ parent[k] = stack.length > 0 ? stack[stack.length - 1] : -1;
235
+ if (t.kind === "punct" && (t.value === "{" || t.value === "(" || t.value === "["))
236
+ stack.push(k);
237
+ }
238
+ return { after, open, parent };
239
+ }
240
+ /** The token after the bracket that `open` opens, or the end. */
241
+ function closing(tokens, open) {
242
+ let depth = 0;
243
+ for (let k = open; k < tokens.length; k++) {
244
+ const t = tokens[k];
245
+ if (t.kind !== "punct")
246
+ continue;
247
+ if (t.value === "{" || t.value === "(" || t.value === "[")
248
+ depth++;
249
+ else if (t.value === "}" || t.value === ")" || t.value === "]") {
250
+ depth--;
251
+ if (depth === 0)
252
+ return k + 1;
253
+ }
254
+ }
255
+ return tokens.length;
256
+ }
257
+ /** Where an initializer that starts at `from` ends: a `;` outside
258
+ brackets — or a `,`, between declarators; a return's value runs on
259
+ past a comma — the next statement, or the end of the block around it.
260
+ Brackets are stepped over whole (`after`, from `bracketsOf`), so
261
+ nested initializers don't make a deep file quadratic. */
262
+ function initializerEnd(tokens, after, from, commas = true) {
263
+ for (let k = from; k < tokens.length; k++) {
264
+ const t = tokens[k];
265
+ if (t.kind === "punct" && (t.value === "{" || t.value === "(" || t.value === "["))
266
+ k = after[k] - 1;
267
+ else if (t.kind === "punct" && (t.value === "}" || t.value === ")" || t.value === "]"))
268
+ return k;
269
+ else if (t.kind === "punct" && (t.value === ";" || (commas && t.value === ",")))
270
+ return k;
271
+ else if (k > from && t.kind === "ident" && !isPunct(tokens[k - 1], ".")) {
272
+ if (STATEMENT.has(t.value))
273
+ return k;
274
+ // a type's commas (`as Record<string, X>`) aren't the declarator's
275
+ if (t.value === "as" || t.value === "satisfies") {
276
+ const end = typeEnd(tokens, after, k + 1, tokens.length);
277
+ if (end > k + 1)
278
+ k = end - 1;
279
+ continue;
280
+ }
281
+ // a declaration after a line without a semicolon, not `= async function`
282
+ const prev = tokens[k - 1];
283
+ const valueFollows = prev.kind === "punct" ? !")]}".includes(prev.value) : prev.kind === "ident" && BEFORE_FUNCTION_VALUE.has(prev.value);
284
+ if ((t.value === "function" || t.value === "class") && !valueFollows)
285
+ return k;
286
+ }
287
+ }
288
+ return tokens.length;
289
+ }
290
+ /** Past a function's parameters and return type to the brace of its body. */
291
+ function bodyAfter(tokens, from) {
292
+ let depth = 0;
293
+ for (let k = from; k < tokens.length && k < from + 400; k++) {
294
+ const u = tokens[k];
295
+ if (depth === 0 && isPunct(u, "{"))
296
+ return k;
297
+ // an overload signature has no body
298
+ if (depth === 0 && isPunct(u, ";"))
299
+ return null;
300
+ if (u.kind === "punct" && (u.value === "(" || u.value === "["))
301
+ depth++;
302
+ else if (u.kind === "punct" && (u.value === ")" || u.value === "]"))
303
+ depth--;
304
+ }
305
+ return null;
306
+ }
307
+ /** `const|let|var name = …` and `function name(…) { … }`, each with the
308
+ tokens of its value: the initializer, or the function's body; a name
309
+ declared without a value is here too, with none. */
310
+ function declarationsOf(tokens, after) {
311
+ const out = [];
312
+ const exportedAt = (k) => {
313
+ const prev = tokens[k - 1];
314
+ if (prev?.kind === "ident" && prev.value === "async")
315
+ return tokens[k - 2]?.kind === "ident" && tokens[k - 2].value === "export";
316
+ return prev?.kind === "ident" && prev.value === "export";
317
+ };
318
+ for (let i = 0; i < tokens.length; i++) {
319
+ const t = tokens[i];
320
+ if (t.kind !== "ident")
321
+ continue;
322
+ if (DECLARE.has(t.value) && !isPunct(tokens[i - 1], ".")) {
323
+ const exported = exportedAt(i);
324
+ let k = i + 1;
325
+ // one declarator after another: `const a = …, b = …`
326
+ while (tokens[k]?.kind === "ident") {
327
+ const name = tokens[k].value;
328
+ let eq = k + 1;
329
+ if (isPunct(tokens[eq], ":")) {
330
+ // a type annotation up to the `=`
331
+ let depth = 0;
332
+ for (; eq < tokens.length && eq < k + 400; eq++) {
333
+ const u = tokens[eq];
334
+ if (u.kind !== "punct")
335
+ continue;
336
+ if (u.value === "{" || u.value === "(" || u.value === "[")
337
+ depth++;
338
+ else if (u.value === "}" || u.value === ")" || u.value === "]")
339
+ depth--;
340
+ else if (depth === 0 && (u.value === "=" || u.value === ";"))
341
+ break;
342
+ }
343
+ }
344
+ if (!isPunct(tokens[eq], "=")) {
345
+ // `let h;`, `let a, b`, `for (const h of …)`: bound, to nothing the reading can see
346
+ out.push({ name, at: k, exported, from: eq, to: eq, fn: false, async: false });
347
+ if (!isPunct(tokens[eq], ","))
348
+ break;
349
+ k = eq + 1;
350
+ continue;
351
+ }
352
+ const to = initializerEnd(tokens, after, eq + 1);
353
+ out.push({ name, at: k, exported, from: eq + 1, to, fn: false, async: false });
354
+ if (!isPunct(tokens[to], ","))
355
+ break;
356
+ k = to + 1;
357
+ }
358
+ }
359
+ else if (t.value === "function" && tokens[i + 1]?.kind === "ident") {
360
+ const name = tokens[i + 1].value;
361
+ let open = i + 2;
362
+ while (open < tokens.length && open < i + 50 && !isPunct(tokens[open], "("))
363
+ open++;
364
+ if (!isPunct(tokens[open], "("))
365
+ continue;
366
+ const body = bodyAfter(tokens, after[open]);
367
+ if (body === null)
368
+ continue;
369
+ out.push({ name, at: i + 1, exported: exportedAt(i), from: body, to: after[body], fn: true, async: isWord(tokens[i - 1], "async") });
370
+ }
371
+ }
372
+ return out;
373
+ }
374
+ /** The local names an `export { a as GET, b }` clause exports, by exported
375
+ name; re-exports `from` another module don't count — their code is elsewhere. */
376
+ function exportClauses(tokens) {
377
+ const out = new Map();
378
+ for (let i = 0; i < tokens.length; i++) {
379
+ const t = tokens[i];
380
+ if (t.kind !== "ident" || t.value !== "export" || !isPunct(tokens[i + 1], "{"))
381
+ continue;
382
+ const end = closing(tokens, i + 1);
383
+ const from = tokens[end];
384
+ if (from?.kind === "ident" && from.value === "from")
385
+ continue;
386
+ for (let k = i + 2; k < end - 1; k++) {
387
+ const u = tokens[k];
388
+ if (u.kind !== "ident" || u.value === "type")
389
+ continue;
390
+ const renamed = tokens[k + 1]?.kind === "ident" && tokens[k + 1].value === "as" && tokens[k + 2]?.kind === "ident";
391
+ const exported = renamed ? tokens[k + 2].value : u.value;
392
+ const list = out.get(exported);
393
+ if (list)
394
+ list.push(u.value);
395
+ else
396
+ out.set(exported, [u.value]);
397
+ if (renamed)
398
+ k += 2;
399
+ }
400
+ i = end - 1;
401
+ }
402
+ return out;
403
+ }
404
+ /** What a name is to the release, as a level: calling a `FACTORY` gives
405
+ a `HANDLER` (`createReleaseHandler` itself, or `make` in `function
406
+ make() { return createReleaseHandler(…) }`), calling a handler gives the
407
+ `RESPONSE` — the release the tab reads. A call goes one level down; a
408
+ function goes one level up from what it returns. */
409
+ const RESPONSE = 0;
410
+ const HANDLER = 1;
411
+ const FACTORY = 2;
412
+ const NOT_A_VALUE = new Set(["new", "typeof", "void", "delete", "function", "class", "this", "super", "null", "true", "false", "undefined", "async", "await", "yield", "return"]);
413
+ const CONTROL = new Set(["if", "for", "while", "switch", "catch", "with"]);
414
+ /** The term of the expression in [from, to), or null. A member `o.name`
415
+ is a term under `memberName`: a namespace's `ns.createReleaseHandler`
416
+ is seeded as the factory (`releaseHandlerOf`); any other member is a
417
+ name nothing gives a level. */
418
+ function termOf(tokens, after, from, to, up, cap = FACTORY) {
419
+ let k = from;
420
+ while (k < to && isWord(tokens[k], "await"))
421
+ k++;
422
+ const t = tokens[k];
423
+ if (k >= to || t?.kind !== "ident" || NOT_A_VALUE.has(t.value))
424
+ return null;
425
+ let name = t.value;
426
+ let j = k + 1;
427
+ if (j + 1 < to && isPunct(tokens[j], ".") && tokens[j + 1]?.kind === "ident") {
428
+ name = memberName(name, tokens[j + 1].value);
429
+ j += 2;
430
+ }
431
+ let calls = 0;
432
+ while (j < to && isPunct(tokens[j], "(")) {
433
+ j = after[j];
434
+ calls++;
435
+ }
436
+ if (j > to || !typesOnly(tokens, after, j, to))
437
+ return null;
438
+ return { name, calls, up, cap };
439
+ }
440
+ /** Whether [j, to) is nothing but `as Type` / `satisfies Type`, once or
441
+ more: a type doesn't change the value, an operator past it (`h as T ||
442
+ other`, `h as T, other`) does. */
443
+ function typesOnly(tokens, after, j, to) {
444
+ while (j < to) {
445
+ if (!isWord(tokens[j], "as") && !isWord(tokens[j], "satisfies"))
446
+ return false;
447
+ const end = typeEnd(tokens, after, j + 1, to);
448
+ if (end <= j + 1)
449
+ return false;
450
+ j = end;
451
+ }
452
+ return true;
453
+ }
454
+ // words a type goes on after: `keyof typeof x`, `readonly string[]`
455
+ const TYPE_PREFIX = new Set(["keyof", "typeof", "readonly", "unique", "infer", "extends"]);
456
+ /** The token after the type that starts at `j` and ends by `to` — `<…>`
457
+ held together with its commas, unions, intersections, function types
458
+ — or -1 when its brackets don't close by then. Two names in a row
459
+ outside `<…>` are a new statement, or the next `as`. */
460
+ function typeEnd(tokens, after, j, to) {
461
+ let angles = 0;
462
+ let named = false;
463
+ for (let k = j; k < to; k++) {
464
+ const u = tokens[k];
465
+ if (u.kind !== "punct") {
466
+ if (angles === 0 && named && !TYPE_PREFIX.has(tokens[k - 1].value))
467
+ return k;
468
+ named = true;
469
+ continue;
470
+ }
471
+ if (u.value === "(" || u.value === "[" || u.value === "{") {
472
+ if (after[k] > to)
473
+ return -1;
474
+ k = after[k] - 1;
475
+ named = true;
476
+ }
477
+ else if (u.value === "<") {
478
+ angles++;
479
+ named = false;
480
+ }
481
+ else if (u.value === ">" && angles > 0) {
482
+ angles--;
483
+ named = true;
484
+ }
485
+ else if (/^\d$/.test(u.value))
486
+ named = true;
487
+ else if (u.value === "=>" || u.value === ".")
488
+ named = false;
489
+ // a union or an intersection; `||` and `&&` are the value's
490
+ else if ((u.value === "|" || u.value === "&") && !isPunct(tokens[k + 1], u.value))
491
+ named = false;
492
+ // type arguments, and conditional types inside them
493
+ else if (angles > 0 && (u.value === "," || u.value === "?" || u.value === ":"))
494
+ named = false;
495
+ else
496
+ return angles === 0 ? k : -1;
497
+ }
498
+ return angles === 0 ? to : -1;
499
+ }
500
+ /** The function a value in [from, to) is — `function (…) {…}`, `async (…)
501
+ => …`, `x => …` — as its block (the braces) or its expression; null
502
+ when the whole value isn't a function. */
503
+ function functionOf(tokens, after, from, to) {
504
+ if (from >= to)
505
+ return null;
506
+ let k = from;
507
+ const async = isWord(tokens[k], "async");
508
+ if (async)
509
+ k++;
510
+ let arrow;
511
+ if (isWord(tokens[k], "function")) {
512
+ // a generator gives back an iterator, never a response
513
+ if (isPunct(tokens[k + 1], "*"))
514
+ return null;
515
+ let open = k + 1;
516
+ while (open < to && !isPunct(tokens[open], "("))
517
+ open++;
518
+ const body = open < to ? bodyAfter(tokens, after[open]) : null;
519
+ return body !== null && after[body] === to ? { block: true, from: body, to, async } : null;
520
+ }
521
+ if (tokens[k]?.kind === "ident" && isPunct(tokens[k + 1], "=>"))
522
+ arrow = k + 1;
523
+ else if (isPunct(tokens[k], "(")) {
524
+ arrow = after[k];
525
+ if (isPunct(tokens[arrow], ":")) {
526
+ // past a return type to the arrow
527
+ let depth = 0;
528
+ for (; arrow < to; arrow++) {
529
+ const u = tokens[arrow];
530
+ if (depth === 0 && isPunct(u, "=>"))
531
+ break;
532
+ if (u.kind === "punct" && (u.value === "(" || u.value === "[" || u.value === "{"))
533
+ depth++;
534
+ else if (u.kind === "punct" && (u.value === ")" || u.value === "]" || u.value === "}"))
535
+ depth--;
536
+ }
537
+ }
538
+ if (!isPunct(tokens[arrow], "=>"))
539
+ return null;
540
+ }
541
+ else
542
+ return null;
543
+ const body = arrow + 1;
544
+ if (isPunct(tokens[body], "{"))
545
+ return after[body] === to ? { block: true, from: body, to, async } : null;
546
+ return body < to ? { block: false, from: body, to, async } : null;
547
+ }
548
+ // words a block of the statements around it follows: `else { … }`, `try { … }`
549
+ const BLOCK_AFTER = new Set(["else", "try", "finally", "do", "catch"]);
550
+ // words whose condition a block of the statements around it follows: `if (…) { … }`
551
+ const BLOCK_AFTER_CONDITION = new Set(["if", "for", "while", "switch", "catch", "with"]);
552
+ /** Whether the `{` at `k`, among statements, opens a block of them — `if
553
+ (…) {`, `else {`, `try {`, `case …: {`, a bare `{` — and not a
554
+ function, a method under any name, a class or anything else whose
555
+ returns are its own. The list is of what's known to be a block, not
556
+ of what's known not to be: whatever it doesn't name keeps its returns. */
557
+ function blockAt(tokens, b, k) {
558
+ const prev = tokens[k - 1];
559
+ if (prev === undefined || isPunct(prev, ";") || isPunct(prev, "{") || isPunct(prev, ":"))
560
+ return true;
561
+ const word = (at, words) => tokens[at]?.kind === "ident" && words.has(tokens[at].value) && !isPunct(tokens[at - 1], ".");
562
+ if (word(k - 1, BLOCK_AFTER))
563
+ return true;
564
+ if (!isPunct(prev, ")"))
565
+ return false;
566
+ const open = b.open[k - 1];
567
+ return open > 0 && (word(open - 1, BLOCK_AFTER_CONDITION) || (isWord(tokens[open - 1], "await") && isWord(tokens[open - 2], "for")));
568
+ }
569
+ /** The returns of each function body in `bodies`, by its `{`, and the
570
+ bodies that throw. A return or a throw is the body's own when only
571
+ blocks of its statements (`blockAt`) stand between them; one inside
572
+ anything else — a nested function, a method under any name, a class —
573
+ belongs to that. */
574
+ function returnsIn(tokens, b, bodies) {
575
+ // for each bracket: whose statements it holds (-1: none known), and whether it holds statements
576
+ const owner = new Int32Array(tokens.length).fill(-1);
577
+ const statements = new Uint8Array(tokens.length);
578
+ const out = new Map();
579
+ const throwing = new Set();
580
+ for (let k = 0; k < tokens.length; k++) {
581
+ const t = tokens[k];
582
+ const p = b.parent[k];
583
+ if (t.kind === "punct" && (t.value === "{" || t.value === "(" || t.value === "[")) {
584
+ if (bodies.has(k)) {
585
+ owner[k] = k;
586
+ statements[k] = 1;
587
+ }
588
+ else if (t.value === "{" && (p === -1 || statements[p] === 1) && blockAt(tokens, b, k)) {
589
+ owner[k] = p === -1 ? -1 : owner[p];
590
+ statements[k] = 1;
591
+ }
592
+ }
593
+ else if (p !== -1 && (isWord(t, "return") || isWord(t, "throw")) && !isPunct(tokens[k - 1], ".") && !isPunct(tokens[k + 1], ":")) {
594
+ const body = owner[p];
595
+ if (body === -1)
596
+ continue;
597
+ if (t.value === "throw") {
598
+ throwing.add(body);
599
+ continue;
600
+ }
601
+ const list = out.get(body);
602
+ if (list)
603
+ list.push(k);
604
+ else
605
+ out.set(body, [k]);
606
+ }
607
+ }
608
+ return { returns: out, throwing };
609
+ }
610
+ /** Where the value a `return` at `k` gives ends: past commas; a line
611
+ break right after `return` ends the statement, and it returns nothing. */
612
+ function returnEnd(tokens, after, k) {
613
+ return tokens[k + 1]?.nl ? k + 1 : initializerEnd(tokens, after, k + 1, false);
614
+ }
615
+ /** What a declaration gives, one value per way out: its value, the
616
+ expression its arrow returns, or each return of its function body —
617
+ a name is what all of them agree on, so a GET that answers the
618
+ handler's response on one branch and anything else on another isn't
619
+ a handler's answer. A body that throws, or whose last statement isn't
620
+ a return of its own (it may run off its end), gives one value the
621
+ reading can't see. Null: a value that isn't a term. */
622
+ function valuesOf(tokens, b, d, fn, own) {
623
+ const after = b.after;
624
+ if (!fn)
625
+ return [termOf(tokens, after, d.from, d.to, 0)];
626
+ // an async function gives a promise: of a response that's still the answer a route awaits; of a handler it's no handler
627
+ const cap = fn.async ? HANDLER : FACTORY;
628
+ if (!fn.block)
629
+ return [termOf(tokens, after, fn.from, fn.to, 1, cap)];
630
+ const returns = own.returns.get(fn.from) ?? [];
631
+ const last = returns[returns.length - 1];
632
+ if (last === undefined || own.throwing.has(fn.from) || b.parent[last] !== fn.from)
633
+ return [null];
634
+ // the last return is a statement of the body, not the branch of an `if … else return`
635
+ const prev = tokens[last - 1];
636
+ const opener = isPunct(prev, ")") ? b.open[last - 1] : -1;
637
+ const head = (at) => (tokens[at]?.kind === "ident" && !isPunct(tokens[at - 1], ".") ? tokens[at].value : "");
638
+ const condition = opener > 0 && (BLOCK_AFTER_CONDITION.has(head(opener - 1)) || (head(opener - 1) === "await" && head(opener - 2) === "for"));
639
+ const statement = last - 1 === fn.from || isPunct(prev, ";") || isPunct(prev, "}") || (tokens[last].nl === true && prev.kind !== "ident" && !condition);
640
+ if (!statement)
641
+ return [null];
642
+ // nothing but `;` between the last return's value and the body's closing brace
643
+ let end = returnEnd(tokens, after, last);
644
+ while (isPunct(tokens[end], ";"))
645
+ end++;
646
+ if (end !== fn.to - 1)
647
+ return [null];
648
+ return returns.map((k) => termOf(tokens, after, k + 1, returnEnd(tokens, after, k), 1, cap));
649
+ }
650
+ /** `name = …`, `name += …` and every other assignment, `name++`, `--name`. */
651
+ function changes(tokens, k) {
652
+ const punct = (at) => (tokens[at]?.kind === "punct" ? tokens[at].value : "");
653
+ const twice = (a, c) => punct(a) === c && punct(a + 1) === c;
654
+ return twice(k - 2, "+") || twice(k - 2, "-") || assigned(tokens, k);
655
+ }
656
+ /** Whether what ends at `k` — a name, a member `o[k]` — is assigned to or updated after it: `= …`, `??= …`, `++`. */
657
+ function assigned(tokens, k) {
658
+ const punct = (at) => (tokens[at]?.kind === "punct" ? tokens[at].value : "");
659
+ const twice = (a, c) => punct(a) === c && punct(a + 1) === c;
660
+ if (twice(k + 1, "+") || twice(k + 1, "-"))
661
+ return true;
662
+ let j = k + 1;
663
+ let operator = "";
664
+ while (operator.length < 3 && punct(j).length === 1 && "+-*/%&|^?<>".includes(punct(j)))
665
+ operator += punct(j++);
666
+ if (punct(j) !== "=" || punct(j + 1) === "=")
667
+ return false;
668
+ // `<=` and `>=` compare; `<<=`, `**=`, `??=`, `||=` … assign
669
+ return operator !== "<" && operator !== ">" && operator !== "?";
670
+ }
671
+ /** Names the code binds or changes other than by a declaration the
672
+ reading follows: parameters, destructuring — in a declaration or an
673
+ assignment (`({ h } = other)`, `[h] = list`) —, `catch (e)`, a loop's
674
+ head, a class, an enum or a namespace, an assignment or an update.
675
+ Where such a name is read it may not be what its declaration says, so
676
+ the reading gives it no level, whichever function it's in: a GET that
677
+ answers through it is one doctor can't confirm. */
678
+ function reboundNames(tokens, b, declared) {
679
+ // spans all of whose names are bound, as a running count: nested spans stay linear
680
+ const marks = new Int32Array(tokens.length + 1);
681
+ const span = (from, to) => {
682
+ if (from >= to)
683
+ return;
684
+ marks[from]++;
685
+ marks[to]--;
686
+ };
687
+ const out = new Set();
688
+ for (let k = 0; k < tokens.length; k++) {
689
+ const t = tokens[k];
690
+ const prev = tokens[k - 1];
691
+ if (isPunct(t, "(")) {
692
+ const next = tokens[b.after[k]];
693
+ const head = prev?.kind === "ident" && !isPunct(tokens[k - 2], ".") ? prev.value : null;
694
+ const condition = head !== null && CONTROL.has(head) && head !== "catch" && head !== "for";
695
+ // `(…) =>`, `name(…) {`, `(…): Type`, `function (…)`; `catch (e)` and a loop's head
696
+ const binds = isPunct(next, "=>") || (!condition && (isPunct(next, "{") || isPunct(next, ":"))) || head === "function" || head === "catch" || head === "for" || isWord(tokens[k - 2], "function");
697
+ if (binds)
698
+ span(k + 1, b.after[k] - 1);
699
+ }
700
+ else if ((isPunct(t, "{") || isPunct(t, "[")) && isPunct(tokens[b.after[k]], "=") && !isPunct(tokens[b.after[k] + 1], "=")) {
701
+ // a pattern assigned to: every name in it may be bound (a member `o[k] = …` is taken as one too)
702
+ span(k + 1, b.after[k] - 1);
703
+ }
704
+ else if (t.kind === "ident" && !isPunct(prev, ".")) {
705
+ if (isPunct(tokens[k + 1], "=>") || isWord(prev, "class") || isWord(prev, "enum") || isWord(prev, "namespace"))
706
+ out.add(t.value);
707
+ else if (DECLARE.has(t.value) && (isPunct(tokens[k + 1], "{") || isPunct(tokens[k + 1], "[")))
708
+ span(k + 2, b.after[k + 1] - 1);
709
+ else if (!declared.has(k) && !isWord(prev, "type") && changes(tokens, k))
710
+ out.add(t.value);
711
+ }
712
+ }
713
+ let depth = 0;
714
+ for (let k = 0; k < tokens.length; k++) {
715
+ depth += marks[k];
716
+ if (depth > 0 && tokens[k].kind === "ident")
717
+ out.add(tokens[k].value);
718
+ }
719
+ return out;
720
+ }
721
+ function readingOf(tokens) {
722
+ const b = bracketsOf(tokens);
723
+ const declarations = declarationsOf(tokens, b.after);
724
+ const rebound = reboundNames(tokens, b, new Set(declarations.map((d) => d.at)));
725
+ const bound = new Set([...rebound, ...declarations.map((d) => d.name)]);
726
+ return { b, declarations, rebound, bound };
727
+ }
728
+ /** Whether the route's exported GET answers with the release handler:
729
+ GET is a handler — `export const GET = createReleaseHandler(…)`,
730
+ `export { release as GET }`, `const GET = make()` — or a function that
731
+ always returns a handler's response: `export async function GET(req) {
732
+ return release(req); }`. A GET that only mentions the handler, calls it
733
+ and drops the result, returns it uncalled (`() => release`), or answers
734
+ something else on another branch or by a throw doesn't. A name declared
735
+ more than once, or returned more than once, is what all its values agree on,
736
+ a name bound or changed any other way is nothing the reading knows,
737
+ and neither is `createReleaseHandler` where the file binds its name
738
+ again — or a namespace's `ns.createReleaseHandler` where it binds `ns` again. */
739
+ function releaseHandlerOf(tokens, names, read) {
740
+ const handlers = names.handler;
741
+ // `createReleaseHandler(` under a name it's imported as, or `ns.createReleaseHandler(` under a namespace's — not `a.ns.createReleaseHandler(`, a member of something else
742
+ const member = (k) => tokens[k].value === "createReleaseHandler" && isPunct(tokens[k - 1], ".") && tokens[k - 2]?.kind === "ident" && names.ns.handler.has(tokens[k - 2].value) && !isPunct(tokens[k - 3], ".");
743
+ const calls = (k) => (handlers.has(tokens[k].value) || member(k)) && isPunct(tokens[k + 1], "(");
744
+ if (!tokens.some((u, k) => u.kind === "ident" && calls(k)))
745
+ return null;
746
+ const { b, declarations, rebound, bound } = read();
747
+ const after = b.after;
748
+ const fns = declarations.map((d) => (d.fn ? { block: true, from: d.from, to: d.to, async: d.async } : functionOf(tokens, after, d.from, d.to)));
749
+ const own = returnsIn(tokens, b, new Set(fns.flatMap((fn) => (fn?.block ? [fn.from] : []))));
750
+ // every value a name is given — each declaration's, each return's — with the term it reads
751
+ const values = [];
752
+ const readers = new Map();
753
+ const declared = new Map();
754
+ declarations.forEach((d, i) => {
755
+ for (const term of valuesOf(tokens, b, d, fns[i], own)) {
756
+ const v = values.push(d.name) - 1;
757
+ declared.set(d.name, (declared.get(d.name) ?? 0) + 1);
758
+ if (term === null)
759
+ continue;
760
+ const list = readers.get(term.name);
761
+ if (list)
762
+ list.push([v, term]);
763
+ else
764
+ readers.set(term.name, [[v, term]]);
765
+ }
766
+ });
767
+ // levels as bits: of each name, of each value, and how many values of a name reach each level
768
+ const levels = new Map();
769
+ const ofValue = new Uint8Array(values.length);
770
+ const agree = new Map();
771
+ const queue = [];
772
+ const grow = (name, bits) => {
773
+ if (rebound.has(name))
774
+ return;
775
+ const had = levels.get(name) ?? 0;
776
+ if ((had | bits) === had)
777
+ return;
778
+ levels.set(name, had | bits);
779
+ queue.push([name, bits & ~had]);
780
+ };
781
+ for (const h of handlers)
782
+ if (!bound.has(h))
783
+ grow(h, 1 << FACTORY);
784
+ // a namespace's member is the factory wherever the namespace is the import; bound again, neither is anything the reading knows
785
+ for (const ns of names.ns.handler)
786
+ if (!bound.has(ns))
787
+ grow(memberName(ns, "createReleaseHandler"), 1 << FACTORY);
788
+ while (queue.length > 0) {
789
+ const [name, bits] = queue.pop();
790
+ for (const [i, term] of readers.get(name) ?? []) {
791
+ let reached = 0;
792
+ for (let level = RESPONSE; level <= FACTORY; level++) {
793
+ const to = level - term.calls + term.up;
794
+ if (bits & (1 << level) && to >= RESPONSE && to <= term.cap)
795
+ reached |= 1 << to;
796
+ }
797
+ const fresh = reached & ~ofValue[i];
798
+ if (fresh === 0)
799
+ continue;
800
+ ofValue[i] = ofValue[i] | fresh;
801
+ const d = values[i];
802
+ // a declaration under an import's name — the factory's or its namespace's — is nothing the reading knows either
803
+ if (handlers.has(d) || names.ns.handler.has(d))
804
+ continue;
805
+ const counts = agree.get(d) ?? [0, 0, 0];
806
+ agree.set(d, counts);
807
+ let all = 0;
808
+ for (let level = RESPONSE; level <= FACTORY; level++) {
809
+ if (!(fresh & (1 << level)))
810
+ continue;
811
+ counts[level] = counts[level] + 1;
812
+ if (counts[level] === declared.get(d))
813
+ all |= 1 << level;
814
+ }
815
+ if (all !== 0)
816
+ grow(d, all);
817
+ }
818
+ }
819
+ const serves = (name) => ((levels.get(name) ?? 0) & (1 << HANDLER)) !== 0;
820
+ const direct = serves("GET") && declarations.some((d) => d.name === "GET" && d.exported);
821
+ const clause = (exportClauses(tokens).get("GET") ?? []).some(serves);
822
+ return direct || clause ? "GET" : "other";
823
+ }
824
+ /** The listeners one file mounts and, for a route file, whether its GET
825
+ serves the release. An imported name the file binds again — a
826
+ parameter, a declaration, destructuring — may not be the import where
827
+ it's used, and the reading can't tell where: none of its uses count.
828
+ A namespace's name the same: its members count only where `ns` itself
829
+ is the import. */
830
+ export function liveInFile(file, text) {
831
+ const tokens = tokenize(text);
832
+ const names = importsOf(tokens);
833
+ let reading = null;
834
+ const read = () => (reading ??= readingOf(tokens));
835
+ /** Whether the name at `k` is a member of one of `namespaces` as imported:
836
+ `ns.name`, not `a.ns.name` — a member of something else — and `ns` not bound again. */
837
+ const ofNamespace = (k, namespaces) => {
838
+ const ns = tokens[k - 2];
839
+ return isPunct(tokens[k - 1], ".") && ns?.kind === "ident" && namespaces.has(ns.value) && !isPunct(tokens[k - 3], ".") && !read().bound.has(ns.value);
840
+ };
841
+ const listeners = [];
842
+ const listener = (via, endpoint) => endpoint === undefined ? { file, via, endpoint: RELEASE_ROUTE, byDefault: true } : { file, via, endpoint };
843
+ for (let k = 0; k < tokens.length; k++) {
844
+ const t = tokens[k];
845
+ if (t.kind !== "ident")
846
+ continue;
847
+ if (names.live.has(t.value) && isPunct(tokens[k - 1], "<")) {
848
+ if (read().bound.has(t.value))
849
+ continue;
850
+ listeners.push(listener("<MapledLive />", jsxEndpoint(tokens, k + 1)));
851
+ }
852
+ else if (t.value === "MapledLive" && isPunct(tokens[k - 3], "<") && ofNamespace(k, names.ns.live)) {
853
+ listeners.push(listener("<MapledLive />", jsxEndpoint(tokens, k + 1)));
854
+ }
855
+ else if (names.watch.has(t.value) && isPunct(tokens[k + 1], "(") && !isPunct(tokens[k - 1], ".")) {
856
+ const prev = tokens[k - 1];
857
+ if (prev?.kind === "ident" && prev.value === "function")
858
+ continue;
859
+ if (read().bound.has(t.value))
860
+ continue;
861
+ listeners.push(listener("watchRelease", callEndpoint(tokens, k + 1)));
862
+ }
863
+ else if (t.value === "watchRelease" && isPunct(tokens[k + 1], "(") && ofNamespace(k, names.ns.watch)) {
864
+ listeners.push(listener("watchRelease", callEndpoint(tokens, k + 1)));
865
+ }
866
+ }
867
+ // `dynamicParams` and the segment settings as the route exports them, or an export the reading can't list
868
+ const listing = tokens.some((u, k) => isWord(u, "exports") || (isWord(u, "export") && isPunct(tokens[k + 1], "*")));
869
+ const sets = (name, keeps) => (listing || tokens.some((u) => isName(u, name))) && setsExport(tokens, read().b.after, name, keeps);
870
+ const params = sets("dynamicParams", (t) => isWord(t ?? undefined, "true"));
871
+ // "force-dynamic" keeps the route only while the handler's read names its own window (keepsWindow)
872
+ const forced = () => sets("dynamic", (t) => literal(t ?? undefined) && t.value === "auto") && !windowsKept(tokens, names, read().b.after);
873
+ const segment = sets("dynamic", SEGMENT_KEEPS.dynamic) ? "dynamic" : forced() ? "force-dynamic" : sets("fetchCache", SEGMENT_KEEPS.fetchCache) ? "fetchCache" : undefined;
874
+ return { listeners, handler: releaseHandlerOf(tokens, names, read), params, ...(segment ? { segment } : {}) };
875
+ }
876
+ /** Whether every release handler the file makes keeps a window of its own
877
+ for the pointer's read (keepsWindow), with the factory only ever called
878
+ where it's named: `const make = createReleaseHandler`, a namespace
879
+ passed on or taken apart, make handlers the reading doesn't see. */
880
+ function windowsKept(tokens, names, after) {
881
+ // the import clauses name them without calling: `import … from`, never past a string — `import "server-only"` has no clause
882
+ const clause = new Uint8Array(tokens.length);
883
+ for (let i = 0; i < tokens.length; i++) {
884
+ if (!isWord(tokens[i], "import") || isPunct(tokens[i - 1], ".") || isPunct(tokens[i + 1], "("))
885
+ continue;
886
+ let j = i + 1;
887
+ while (j < tokens.length && j < i + 200 && !isWord(tokens[j], "from") && tokens[j].kind !== "string" && !isPunct(tokens[j], ";"))
888
+ j = isPunct(tokens[j], "{") ? after[j] : j + 1;
889
+ if (isWord(tokens[j], "from"))
890
+ clause.fill(1, i, j + 1);
891
+ }
892
+ for (let k = 0; k < tokens.length; k++) {
893
+ const t = tokens[k];
894
+ if (t.kind !== "ident" || clause[k] === 1)
895
+ continue;
896
+ const ns = names.ns.handler.has(t.value) && !isPunct(tokens[k - 1], ".");
897
+ if (ns && !isPunct(tokens[k + 1], "."))
898
+ return false;
899
+ const direct = names.handler.has(t.value) && !isPunct(tokens[k - 1], ".");
900
+ const member = t.value === "createReleaseHandler" && isPunct(tokens[k - 1], ".") && tokens[k - 2]?.kind === "ident" && names.ns.handler.has(tokens[k - 2].value) && !isPunct(tokens[k - 3], ".");
901
+ if ((direct || member) && !(isPunct(tokens[k + 1], "(") && keepsWindow(tokens, after, k + 1)))
902
+ return false;
903
+ }
904
+ return true;
905
+ }
906
+ /** Whether a `createReleaseHandler` call, from its opening parenthesis,
907
+ leaves the pointer's read a window of its own: options written out as
908
+ an object whose `revalidate` is absent (3600) or digits above 0. On a
909
+ `force-dynamic` route Next 15 and later fetch a read with no window —
910
+ `revalidate: false` counts as none — uncached, past the `mapled` tag,
911
+ and the release moves before the webhook refreshes the pages. False
912
+ where the reading can't tell: options from elsewhere, a spread, a key
913
+ the code decides, a `revalidate` that isn't plain digits. */
914
+ function keepsWindow(tokens, after, paren) {
915
+ const open = paren + 1;
916
+ if (!isPunct(tokens[open], "{") || !isPunct(tokens[after[open]], ")"))
917
+ return false;
918
+ let kept = true;
919
+ for (const [a, z] of itemsOf(tokens, after, open)) {
920
+ if (a === z)
921
+ continue;
922
+ if (isSpread(tokens, a))
923
+ return false;
924
+ const { name, next } = propertyKey(tokens, a);
925
+ if (name === null)
926
+ return false;
927
+ if (name !== "revalidate")
928
+ continue;
929
+ // the last one wins, as in the object: `revalidate: 60`, `revalidate: 0.5`
930
+ const digits = tokens.slice(next + 1, z).map((u) => (u.kind === "punct" ? u.value : " ")).join("");
931
+ kept = isPunct(tokens[next], ":") && /^(\d+\.?\d*|\.\d+)$/.test(digits) && Number(digits) > 0;
932
+ }
933
+ return kept;
934
+ }
935
+ /** Whether the token is the name — as an identifier or a string: `export { x as "name" }`. */
936
+ const isName = (u, name) => (u?.kind === "ident" || u?.kind === "string") && u.value === name;
937
+ /** The route segment settings that change how Next serves the release
938
+ route or reads what the handler reads, in the order doctor names them:
939
+ `dynamic` can make it a static copy built without the tab's request,
940
+ and `fetchCache` can make the handler's read skip the `mapled` cache
941
+ tag, so the release moves before the webhook refreshes the pages.
942
+ `revalidate` isn't one: it only fills in fetches that don't name their
943
+ own window, and the handler's names one (Next 14's fetch patch; the
944
+ live smoke with `revalidate = 1` on Next 15). "force-dynamic" is the
945
+ third: harmless while the handler keeps its read's window, and past
946
+ the tag when it doesn't (keepsWindow). */
947
+ export const SEGMENT_SETTINGS = ["dynamic", "fetchCache"];
948
+ /** For each, the values written out that leave the route as the handler
949
+ sets it up: the default, and for `dynamic` also "force-dynamic" — the
950
+ route reads its request, so it is dynamic anyway; what it does to the
951
+ handler's read is checked apart (windowsKept). */
952
+ const SEGMENT_KEEPS = {
953
+ dynamic: (t) => literal(t ?? undefined) && (t.value === "auto" || t.value === "force-dynamic"),
954
+ fetchCache: (t) => literal(t ?? undefined) && t.value === "auto",
955
+ };
956
+ /** Whether the route may export `name` as anything but a value `keeps`
957
+ accepts, written out on a `const`. Next reads only the module's
958
+ export: a local variable, a property or a string that says the name is
959
+ nothing to it. An export the reading can't list — `export *`,
960
+ CommonJS — may be it. */
961
+ function setsExport(tokens, after, name, keeps) {
962
+ for (let i = 0; i < tokens.length; i++) {
963
+ const t = tokens[i];
964
+ // CommonJS: `module.exports = { dynamicParams: false }`, `exports.dynamicParams = false`
965
+ if (isWord(t, "exports"))
966
+ return true;
967
+ if (!isWord(t, "export") || isPunct(tokens[i - 1], "."))
968
+ continue;
969
+ let j = i + 1;
970
+ // `export * from "./segment"` may carry it, and so may TypeScript's `export =`; `export * as ns` names only ns
971
+ if (isPunct(tokens[j], "="))
972
+ return true;
973
+ if (isPunct(tokens[j], "*")) {
974
+ if (!isWord(tokens[j + 1], "as"))
975
+ return true;
976
+ continue;
977
+ }
978
+ // a clause, with or without `from`: `export { x as dynamicParams }`, `export { dynamicParams }`, `export { x as "dynamicParams" }`
979
+ if (isPunct(tokens[j], "{")) {
980
+ for (let k = j + 1; k < after[j] - 1; k++)
981
+ if (isName(tokens[k], name) && !isWord(tokens[k + 1], "as"))
982
+ return true;
983
+ continue;
984
+ }
985
+ while (isWord(tokens[j], "declare") || isWord(tokens[j], "async") || isWord(tokens[j], "abstract"))
986
+ j++;
987
+ const declare = tokens[j];
988
+ if (declare?.kind !== "ident" || !DECLARE.has(declare.value)) {
989
+ // `export function dynamicParams`, `export class …`, `export import dynamicParams = …`
990
+ const named = isPunct(tokens[j + 1], "*") ? j + 2 : j + 1;
991
+ if (isName(tokens[named], name))
992
+ return true;
993
+ continue;
994
+ }
995
+ // each declarator: `export const a = 1, dynamicParams = false`, `export const { dynamicParams } = config`
996
+ for (let k = j + 1; k < tokens.length;) {
997
+ let next;
998
+ if (isPunct(tokens[k], "{") || isPunct(tokens[k], "[")) {
999
+ for (let p = k + 1; p < after[k] - 1; p++)
1000
+ if (isName(tokens[p], name))
1001
+ return true;
1002
+ next = after[k];
1003
+ }
1004
+ else if (tokens[k].kind === "ident") {
1005
+ // only a `const` keeps its value: a `let` or `var` may change it later
1006
+ if (isName(tokens[k], name) && (declare.value !== "const" || !keeps(writtenValue(tokens, after, k))))
1007
+ return true;
1008
+ next = k + 1;
1009
+ }
1010
+ else
1011
+ break;
1012
+ if (isPunct(tokens[next], ":"))
1013
+ next = typeEnd(tokens, after, next + 1, tokens.length);
1014
+ if (next < 0)
1015
+ break;
1016
+ if (isPunct(tokens[next], "="))
1017
+ next = initializerEnd(tokens, after, next + 1);
1018
+ if (!isPunct(tokens[next], ","))
1019
+ break;
1020
+ k = next + 1;
1021
+ }
1022
+ }
1023
+ return false;
1024
+ }
1025
+ /** The one token a declaration at `k` writes as its value: `name = true`,
1026
+ with or without a type (`: boolean`), the value in parentheses or
1027
+ followed by `as` / `satisfies` / `!`. Null when the value is anything
1028
+ else — an expression, more than one token — or there is none. */
1029
+ function writtenValue(tokens, after, k) {
1030
+ if (tokens[k].kind !== "ident")
1031
+ return null;
1032
+ const eq = isPunct(tokens[k + 1], ":") ? typeEnd(tokens, after, k + 2, tokens.length) : k + 1;
1033
+ if (eq < 0 || !isPunct(tokens[eq], "=") || isPunct(tokens[eq + 1], "="))
1034
+ return null;
1035
+ let from = eq + 1;
1036
+ let to = initializerEnd(tokens, after, from);
1037
+ for (;;) {
1038
+ // where the value ends: `x as T`, `x satisfies T` and `x!` are x, and a
1039
+ // line without a semicolon ends before a name that starts the next one
1040
+ let end = to;
1041
+ for (let j = from; j < end; j++) {
1042
+ const u = tokens[j];
1043
+ const p = tokens[j - 1];
1044
+ if (j > from && (isWord(u, "as") || isWord(u, "satisfies") || (u.nl === true && u.kind === "ident" && (p.kind !== "punct" || ")]}".includes(p.value)))))
1045
+ end = j;
1046
+ else if (u.kind === "punct" && "([{".includes(u.value))
1047
+ j = after[j] - 1;
1048
+ }
1049
+ while (end > from + 1 && isPunct(tokens[end - 1], "!"))
1050
+ end--;
1051
+ if (!isPunct(tokens[from], "(") || after[from] !== end)
1052
+ return end === from + 1 ? tokens[from] : null;
1053
+ from++;
1054
+ to = end - 1;
1055
+ }
1056
+ }
1057
+ /** The route files of the App Router with the path each answers. */
1058
+ export function routeFileOf(file, extensions = PAGE_EXTENSIONS) {
1059
+ const m = /^(?:src\/)?app\/(?:(.+)\/)?route\.(.+)$/.exec(file);
1060
+ if (!m || !extensions.includes(m[2]))
1061
+ return null;
1062
+ return appRoute(m[1] ? m[1].split("/") : []);
1063
+ }
1064
+ // Pages Router files that are no page of their own
1065
+ const PAGES_SPECIAL = new Set(["_app", "_document", "_error"]);
1066
+ /** The page files of both routers with the path each answers: an App
1067
+ Router `page.<ext>`, and any Pages Router file but `_app`, `_document`
1068
+ and `_error`, with the longest page extension its name ends in taken
1069
+ off, as Next does, and `index` naming its folder. */
1070
+ export function pageFileOf(file, extensions = PAGE_EXTENSIONS) {
1071
+ const app = /^(?:src\/)?app\/(?:(.+)\/)?page\.(.+)$/.exec(file);
1072
+ if (app)
1073
+ return extensions.includes(app[2]) ? appRoute(app[1] ? app[1].split("/") : []) : null;
1074
+ const pages = /^(?:src\/)?pages\/(.+)$/.exec(file);
1075
+ if (!pages)
1076
+ return null;
1077
+ const name = pages[1];
1078
+ const ext = extensions.filter((e) => name.endsWith(`.${e}`)).sort((a, b) => b.length - a.length)[0];
1079
+ if (ext === undefined || name.length === ext.length + 1)
1080
+ return null;
1081
+ const segments = name.slice(0, -(ext.length + 1)).split("/");
1082
+ if (segments.length === 1 && PAGES_SPECIAL.has(segments[0]))
1083
+ return null;
1084
+ if (segments[segments.length - 1] === "index")
1085
+ segments.pop();
1086
+ return "/" + segments.join("/");
1087
+ }
1088
+ /** Whether the site has the folder at its root: Next reads `app` and
1089
+ `pages` there, and under `src/` only when the root has none. */
1090
+ async function atRoot(dir, name) {
1091
+ try {
1092
+ return (await stat(path.join(dir, name))).isDirectory();
1093
+ }
1094
+ catch {
1095
+ return false;
1096
+ }
1097
+ }
1098
+ // the settings of next.config that move where a file answers
1099
+ const SETTINGS = ["pageExtensions", "basePath"];
1100
+ // calls that set a property of an object they're given, by a name or from another object, or give it a prototype that may carry one
1101
+ const COPIES = new Set(["assign", "defineProperty", "defineProperties", "Reflect", "setPrototypeOf", "__defineGetter__", "__defineSetter__", "__proto__"]);
1102
+ // Next's hooks: the functions a config may hold, which Next calls only once it has loaded the config — webpack's as it builds, the routes' as it collects them
1103
+ const HOOKS = new Set(["webpack", "headers", "redirects", "rewrites", "generateBuildId", "exportPathMap"]);
1104
+ // the names the subset reads without a binding, which the file may not bind
1105
+ const AMBIENT = new Set(["process", "require", "module", "exports", "URL", "undefined", "NaN", "Infinity", "globalThis", "eval", "arguments"]);
1106
+ // the words that start a statement of the subset; none goes on with the expression a line break comes after
1107
+ const STARTS = new Set(["import", "export", "const", "let", "var", "module"]);
1108
+ // words after which JavaScript reads a `/` as a regex, not a division — the contextual ones (`of`, `await`, `let`) either way
1109
+ const BEFORE_REGEX = new Set(["return", "typeof", "case", "delete", "do", "else", "in", "instanceof", "new", "of", "throw", "void", "yield", "await", "extends", "let"]);
1110
+ // words no value ends with: past a line break after one, the expression goes on
1111
+ const GOES_ON = new Set([...BEFORE_REGEX, "as", "satisfies", "keyof", "default", "export", "import", "const", "var"]);
1112
+ // brackets and what closes each
1113
+ const OPENS = new Map([
1114
+ ["(", ")"],
1115
+ ["[", "]"],
1116
+ ["{", "}"],
1117
+ ]);
1118
+ // how deep values may nest, so a deep file can't overflow the stack
1119
+ const DEPTH = 200;
1120
+ const isDigit = (t) => t?.kind === "punct" && /^\d$/.test(t.value);
1121
+ /** Whether every bracket closes with its own kind, and none closes nothing. */
1122
+ function balanced(tokens, b) {
1123
+ for (let k = 0; k < tokens.length; k++) {
1124
+ const t = tokens[k];
1125
+ if (t.kind !== "punct")
1126
+ continue;
1127
+ const close = OPENS.get(t.value);
1128
+ if (close !== undefined) {
1129
+ const at = b.after[k] - 1;
1130
+ if (!isPunct(tokens[at], close) || b.open[at] !== k)
1131
+ return false;
1132
+ }
1133
+ else if ((t.value === ")" || t.value === "]" || t.value === "}") && b.open[k] < 0)
1134
+ return false;
1135
+ }
1136
+ return true;
1137
+ }
1138
+ /** Whether each `/` of the tokens is what JavaScript reads there: a
1139
+ division after a value — a name, a literal, `]`, a `)` that closes no
1140
+ `if (…)` — and a regex anywhere else. The tokens read a regex only after
1141
+ punctuation that ends no value (and `return`, `typeof`), so they differ
1142
+ after `+`, after `else`, after `if (x)`: a brace in such a regex would
1143
+ move every bracket after it. After `}` JavaScript can read either. */
1144
+ function slashesAgree(tokens, b) {
1145
+ for (let k = 0; k < tokens.length; k++) {
1146
+ const t = tokens[k];
1147
+ const regex = t.kind === "regex";
1148
+ if (!regex && !isPunct(t, "/"))
1149
+ continue;
1150
+ const p = tokens[k - 1];
1151
+ let division;
1152
+ if (p === undefined)
1153
+ division = false;
1154
+ else if (p.kind === "ident")
1155
+ division = !BEFORE_REGEX.has(p.value) || isPunct(tokens[k - 2], ".");
1156
+ else if (p.kind === "string" || p.kind === "regex" || isDigit(p) || p.value === "]")
1157
+ division = true;
1158
+ else if (p.value === ")") {
1159
+ const open = b.open[k - 1];
1160
+ const head = open >= 0 ? tokens[open - 1] : undefined;
1161
+ const condition = head?.kind === "ident" && !isPunct(tokens[open - 2], ".") && (["if", "while", "for", "with"].includes(head.value) || (head.value === "await" && isWord(tokens[open - 2], "for")));
1162
+ division = !condition;
1163
+ }
1164
+ else if (p.value === "}")
1165
+ return false;
1166
+ else
1167
+ division = false;
1168
+ if (regex === division)
1169
+ return false;
1170
+ }
1171
+ return true;
1172
+ }
1173
+ /** The file as the declarative subset nextConfigOf reads (see there), or
1174
+ null when anything in it is outside the subset. */
1175
+ function declarative(tokens, b) {
1176
+ const bindings = new Map();
1177
+ const props = new Map();
1178
+ const configs = [];
1179
+ let exported = null;
1180
+ let exports = 0;
1181
+ let depth = 0;
1182
+ const guard = (fail, f) => {
1183
+ if (depth >= DEPTH)
1184
+ return fail;
1185
+ depth++;
1186
+ try {
1187
+ return f();
1188
+ }
1189
+ finally {
1190
+ depth--;
1191
+ }
1192
+ };
1193
+ // the items between the brackets that open at `open`: a trailing comma adds none, a hole makes it null
1194
+ const listOf = (open) => {
1195
+ const items = itemsOf(tokens, b.after, open);
1196
+ if (items.length === 1 && items[0][0] === items[0][1])
1197
+ return [];
1198
+ if (items.length > 1 && items[items.length - 1][0] === items[items.length - 1][1])
1199
+ items.pop();
1200
+ return items.some(([x, y]) => x === y) ? null : items;
1201
+ };
1202
+ // [from, to) split at its commas outside brackets
1203
+ const splitAt = (from, to) => {
1204
+ const out = [];
1205
+ let start = from;
1206
+ for (let k = from; k <= to; k++) {
1207
+ const t = tokens[k];
1208
+ if (k < to && t?.kind === "punct" && OPENS.has(t.value))
1209
+ k = b.after[k] - 1;
1210
+ else if (k === to || isPunct(t, ",")) {
1211
+ out.push([start, k]);
1212
+ start = k + 1;
1213
+ }
1214
+ }
1215
+ return out;
1216
+ };
1217
+ // a value's last token: a line break after it may end the statement
1218
+ const endsValue = (t) => t !== undefined && (t.kind === "string" || t.kind === "regex" || (t.kind === "ident" && !GOES_ON.has(t.value)) || isDigit(t) || isPunct(t, ")") || isPunct(t, "]") || isPunct(t, "}"));
1219
+ // where the statement at `k` ends: its `;`, or a line that starts another after a value
1220
+ const statementEnd = (k) => {
1221
+ for (let j = k; j < tokens.length; j++) {
1222
+ const t = tokens[j];
1223
+ if (isPunct(t, ";"))
1224
+ return j;
1225
+ if (j > k && t.nl === true && t.kind === "ident" && STARTS.has(t.value) && endsValue(tokens[j - 1]))
1226
+ return j;
1227
+ if (t.kind === "punct" && OPENS.has(t.value))
1228
+ j = b.after[j] - 1;
1229
+ }
1230
+ return tokens.length;
1231
+ };
1232
+ const bind = (k, binding) => {
1233
+ const t = tokens[k];
1234
+ if (t?.kind !== "ident" || AMBIENT.has(t.value) || bindings.has(t.value))
1235
+ return false;
1236
+ bindings.set(t.value, { ...binding, at: k, uses: 0 });
1237
+ return true;
1238
+ };
1239
+ // a type as next.config writes one: `NextConfig`, `import("next").NextConfig`, with type arguments of the same and `[]` after; `const`
1240
+ const simpleType = (j, to, level = 0) => {
1241
+ if (level > 8 || j >= to)
1242
+ return -1;
1243
+ if (isWord(tokens[j], "const"))
1244
+ return j + 1;
1245
+ if (isWord(tokens[j], "import")) {
1246
+ if (!(isPunct(tokens[j + 1], "(") && literal(tokens[j + 2]) && isPunct(tokens[j + 3], ")") && isPunct(tokens[j + 4], ".")))
1247
+ return -1;
1248
+ j += 5;
1249
+ }
1250
+ if (tokens[j]?.kind !== "ident")
1251
+ return -1;
1252
+ j++;
1253
+ while (isPunct(tokens[j], ".") && tokens[j + 1]?.kind === "ident")
1254
+ j += 2;
1255
+ if (isPunct(tokens[j], "<")) {
1256
+ do {
1257
+ j = simpleType(j + 1, to, level + 1);
1258
+ if (j < 0)
1259
+ return -1;
1260
+ } while (isPunct(tokens[j], ","));
1261
+ if (!isPunct(tokens[j], ">"))
1262
+ return -1;
1263
+ j++;
1264
+ }
1265
+ while (isPunct(tokens[j], "[") && isPunct(tokens[j + 1], "]"))
1266
+ j += 2;
1267
+ return j <= to ? j : -1;
1268
+ };
1269
+ // what may follow a value to the end of its range: `as T`, `satisfies T`, on its line
1270
+ const suffix = (j, to) => {
1271
+ while (j < to) {
1272
+ if (!(isWord(tokens[j], "as") || isWord(tokens[j], "satisfies")) || tokens[j].nl === true)
1273
+ return false;
1274
+ j = simpleType(j + 1, to);
1275
+ if (j < 0)
1276
+ return false;
1277
+ }
1278
+ return j === to;
1279
+ };
1280
+ // where the value in [from, to) ends before its types, or -1
1281
+ const valueEnd = (from, to) => {
1282
+ for (let k = from; k < to; k++) {
1283
+ const t = tokens[k];
1284
+ if (t.kind === "punct" && OPENS.has(t.value))
1285
+ k = b.after[k] - 1;
1286
+ else if ((isWord(t, "as") || isWord(t, "satisfies")) && k > from && !isPunct(tokens[k - 1], "."))
1287
+ return suffix(k, to) ? k : -1;
1288
+ }
1289
+ return to;
1290
+ };
1291
+ // a use of the name at `k` in a role, after its binding: the first settles what the binding's value is for that role
1292
+ const use = (k, role) => {
1293
+ const binding = bindings.get(tokens[k].value);
1294
+ if (binding === undefined || binding.at >= k || binding.busy === true)
1295
+ return false;
1296
+ if (binding.role !== undefined && binding.role !== role)
1297
+ return false;
1298
+ binding.role = role;
1299
+ binding.uses++;
1300
+ // the config is taken once: a second use would hand it to more code
1301
+ if (role === "config" && binding.uses > 1)
1302
+ return false;
1303
+ if (binding.uses > 1)
1304
+ return true;
1305
+ binding.busy = true;
1306
+ const fine = settle(binding, role);
1307
+ delete binding.busy;
1308
+ return fine;
1309
+ };
1310
+ const settle = (binding, role) => {
1311
+ switch (role) {
1312
+ case "config":
1313
+ return binding.kind === "value" && configValue(binding.from, binding.to);
1314
+ case "data":
1315
+ return binding.kind === "none" || (binding.kind === "value" && data(binding.from, binding.to));
1316
+ }
1317
+ };
1318
+ // the config in [from, to): the object, or a name of it — a plugin's call is code the reading doesn't run (block 348)
1319
+ const configValue = (from, to) => guard(false, () => {
1320
+ const end = valueEnd(from, to);
1321
+ if (end <= from)
1322
+ return false;
1323
+ const t = tokens[from];
1324
+ if (isPunct(t, "(") && b.after[from] === end)
1325
+ return configValue(from + 1, end - 1);
1326
+ if (isPunct(t, "{") && b.after[from] === end) {
1327
+ configs.push(from);
1328
+ return object(from, true);
1329
+ }
1330
+ return t.kind === "ident" && from + 1 === end && use(from, "config");
1331
+ });
1332
+ // an object literal's properties: values that are data, a shorthand's name; functions only as Next's hooks, at the config's top
1333
+ const object = (open, config) => guard(false, () => {
1334
+ const items = listOf(open);
1335
+ if (items === null)
1336
+ return false;
1337
+ const list = [];
1338
+ for (const [x, y] of items) {
1339
+ // `async` or `get` is a key itself when what follows ends one: `{ get: 1 }`, `{ async }`
1340
+ const endsKey = (j) => j === y || isPunct(tokens[j], ":") || isPunct(tokens[j], "(");
1341
+ let k = x;
1342
+ const async = isWord(tokens[k], "async") && !endsKey(k + 1);
1343
+ if (async)
1344
+ k++;
1345
+ // a generator, a spread, a getter or a setter: code the reading doesn't run, or properties from elsewhere
1346
+ if (isPunct(tokens[k], "*") || isPunct(tokens[k], ".") || ((isWord(tokens[k], "get") || isWord(tokens[k], "set")) && !endsKey(k + 1)))
1347
+ return false;
1348
+ const t = tokens[k];
1349
+ let key;
1350
+ let at = k;
1351
+ let next = k + 1;
1352
+ if (t?.kind === "ident" || literal(t))
1353
+ key = t.value;
1354
+ else if (isPunct(t, "[") && literal(tokens[k + 1]) && isPunct(tokens[k + 2], "]")) {
1355
+ key = tokens[k + 1].value;
1356
+ at = k + 1;
1357
+ next = k + 3;
1358
+ }
1359
+ else
1360
+ return false;
1361
+ // `__proto__: …` sets the object's prototype
1362
+ if (key === "__proto__")
1363
+ return false;
1364
+ const hook = config && HOOKS.has(key);
1365
+ if (next === y) {
1366
+ if (async || t?.kind !== "ident" || !use(k, "data"))
1367
+ return false;
1368
+ list.push({ key, at, kind: "shorthand", from: k, to: y });
1369
+ }
1370
+ else if (isPunct(tokens[next], ":") && !async) {
1371
+ if (functionOf(tokens, b.after, next + 1, y) !== null) {
1372
+ if (!hook)
1373
+ return false;
1374
+ list.push({ key, at, kind: "hook", from: next + 1, to: y });
1375
+ }
1376
+ else {
1377
+ if (!data(next + 1, y))
1378
+ return false;
1379
+ list.push({ key, at, kind: "value", from: next + 1, to: y });
1380
+ }
1381
+ }
1382
+ else if (isPunct(tokens[next], "(")) {
1383
+ // a method: `async headers() {…}`
1384
+ const body = bodyAfter(tokens, b.after[next]);
1385
+ if (!hook || body === null || b.after[body] !== y)
1386
+ return false;
1387
+ list.push({ key, at, kind: "hook", from: next, to: y });
1388
+ }
1389
+ else
1390
+ return false;
1391
+ }
1392
+ props.set(open, list);
1393
+ return true;
1394
+ });
1395
+ // data in [from, to), to its end but for types
1396
+ const data = (from, to) => guard(false, () => {
1397
+ const e = expr(from, to);
1398
+ return e > from && (e === to || suffix(e, to));
1399
+ });
1400
+ const expr = (k, to) => guard(-1, () => {
1401
+ let j = binary(k, to);
1402
+ if (j < 0 || j >= to || !isPunct(tokens[j], "?") || isPunct(tokens[j + 1], "?") || isPunct(tokens[j + 1], "."))
1403
+ return j;
1404
+ j = expr(j + 1, to);
1405
+ if (j < 0 || j >= to || !isPunct(tokens[j], ":"))
1406
+ return -1;
1407
+ return expr(j + 1, to);
1408
+ });
1409
+ const binary = (k, to) => {
1410
+ let j = unary(k, to);
1411
+ while (j >= 0 && j < to) {
1412
+ const n = operator(j);
1413
+ if (n <= 0)
1414
+ return n < 0 ? -1 : j;
1415
+ j = unary(j + n, to);
1416
+ }
1417
+ return j;
1418
+ };
1419
+ // the binary operator at `j`, by its tokens: 0 where the operand ends, -1 for an assignment or an operator the subset doesn't read
1420
+ const operator = (j) => {
1421
+ const t = tokens[j];
1422
+ const u = tokens[j + 1];
1423
+ const v = tokens[j + 2];
1424
+ if (t?.kind !== "punct")
1425
+ return 0;
1426
+ switch (t.value) {
1427
+ case "=":
1428
+ case "!":
1429
+ return isPunct(u, "=") ? (isPunct(v, "=") ? 3 : 2) : -1;
1430
+ case "&":
1431
+ case "|":
1432
+ case "?":
1433
+ if (!isPunct(u, t.value))
1434
+ return t.value === "?" ? 0 : -1;
1435
+ return isPunct(v, "=") ? -1 : 2;
1436
+ case "+":
1437
+ return isPunct(u, "+") || isPunct(u, "=") ? -1 : 1;
1438
+ }
1439
+ return 0;
1440
+ };
1441
+ const unary = (k, to) => {
1442
+ let j = k;
1443
+ while (j < to && j < k + 16 && (isPunct(tokens[j], "!") || (isPunct(tokens[j], "-") && !isPunct(tokens[j + 1], "-"))))
1444
+ j++;
1445
+ return primary(j, to);
1446
+ };
1447
+ const numberEnd = (k, to) => {
1448
+ let j = k;
1449
+ let point = false;
1450
+ while (j < to && (j === k || tokens[j].nl !== true)) {
1451
+ if (isDigit(tokens[j]))
1452
+ j++;
1453
+ else if (isPunct(tokens[j], ".") && !point && isDigit(tokens[j + 1])) {
1454
+ point = true;
1455
+ j++;
1456
+ }
1457
+ else
1458
+ break;
1459
+ }
1460
+ return j;
1461
+ };
1462
+ // the arguments of the call that opens at `open`, each data
1463
+ const args = (open, to) => {
1464
+ const close = b.after[open];
1465
+ const items = close <= to ? listOf(open) : null;
1466
+ return items !== null && items.every(([x, y]) => data(x, y)) ? close : -1;
1467
+ };
1468
+ const primary = (k, to) => {
1469
+ const t = tokens[k];
1470
+ if (t === undefined || k >= to)
1471
+ return -1;
1472
+ if (t.kind === "string")
1473
+ return literal(t) ? k + 1 : -1;
1474
+ if (t.kind === "regex")
1475
+ return k + 1;
1476
+ if (t.kind === "punct") {
1477
+ if (isDigit(t) || (t.value === "." && isDigit(tokens[k + 1])))
1478
+ return numberEnd(k, to);
1479
+ const close = b.after[k];
1480
+ if (!OPENS.has(t.value) || close > to)
1481
+ return -1;
1482
+ if (t.value === "(")
1483
+ return data(k + 1, close - 1) ? close : -1;
1484
+ if (t.value === "[")
1485
+ return listOf(k)?.every(([x, y]) => data(x, y)) ? close : -1;
1486
+ return object(k, false) ? close : -1;
1487
+ }
1488
+ const next = tokens[k + 1];
1489
+ switch (t.value) {
1490
+ case "true":
1491
+ case "false":
1492
+ case "null":
1493
+ case "undefined":
1494
+ case "NaN":
1495
+ case "Infinity":
1496
+ return k + 1;
1497
+ case "process":
1498
+ // `process.env.NAME`, `process.env["NAME"]`, `process.cwd()`
1499
+ if (!isPunct(next, "."))
1500
+ return -1;
1501
+ if (isWord(tokens[k + 2], "env") && isPunct(tokens[k + 3], ".") && tokens[k + 4]?.kind === "ident")
1502
+ return k + 5;
1503
+ if (isWord(tokens[k + 2], "env") && isPunct(tokens[k + 3], "[") && literal(tokens[k + 4]) && isPunct(tokens[k + 5], "]"))
1504
+ return k + 6;
1505
+ return isWord(tokens[k + 2], "cwd") && isPunct(tokens[k + 3], "(") && isPunct(tokens[k + 4], ")") ? k + 5 : -1;
1506
+ case "import":
1507
+ // `import.meta.dirname`, `.filename`, `.url`
1508
+ return isPunct(next, ".") && isWord(tokens[k + 2], "meta") && isPunct(tokens[k + 3], ".") && ["dirname", "filename", "url"].some((w) => isWord(tokens[k + 4], w)) ? k + 5 : -1;
1509
+ case "require":
1510
+ // `require.resolve("m")`: a path; nothing is loaded
1511
+ return isPunct(next, ".") && isWord(tokens[k + 2], "resolve") && isPunct(tokens[k + 3], "(") && literal(tokens[k + 4]) && isPunct(tokens[k + 5], ")") ? k + 6 : -1;
1512
+ case "new":
1513
+ // `new URL("…")`
1514
+ return isWord(next, "URL") && isPunct(tokens[k + 2], "(") ? args(k + 2, to) : -1;
1515
+ }
1516
+ if ((t.value === "__dirname" || t.value === "__filename") && !bindings.has(t.value))
1517
+ return k + 1;
1518
+ // a name declared with data — not a member of it or a call
1519
+ return !isPunct(next, ".") && !isPunct(next, "(") && use(k, "data") ? k + 1 : -1;
1520
+ };
1521
+ // statements
1522
+ const exportValue = (from, e) => {
1523
+ if (from >= e)
1524
+ return false;
1525
+ exports++;
1526
+ exported = { from, to: e };
1527
+ return true;
1528
+ };
1529
+ // `import type { NextConfig } from "next"`, `import type X from "m"`, `import type * as M from "m"`: types, erased — the only import, since a module
1530
+ // loaded runs its code, and which module a name loads the spelling doesn't show (Node's self-reference, a next.config.ts's tsconfig paths)
1531
+ const typeImport = (k, e) => {
1532
+ if (!isWord(tokens[k + 1], "type") || !(e >= k + 5 && isWord(tokens[e - 2], "from") && literal(tokens[e - 1])))
1533
+ return false;
1534
+ const stop = e - 2;
1535
+ const j = k + 2;
1536
+ if (isPunct(tokens[j], "{"))
1537
+ return b.after[j] === stop;
1538
+ if (isPunct(tokens[j], "*"))
1539
+ return isWord(tokens[j + 1], "as") && tokens[j + 2]?.kind === "ident" && j + 3 === stop;
1540
+ return tokens[j]?.kind === "ident" && !isWord(tokens[j], "from") && j + 1 === stop;
1541
+ };
1542
+ const exportStatement = (k, e) => {
1543
+ const t = tokens[k + 1];
1544
+ if (isWord(t, "default"))
1545
+ return exportValue(k + 2, e);
1546
+ // `export type { T }`, `export type { T } from "m"`: types, erased
1547
+ if (isWord(t, "type") && isPunct(tokens[k + 2], "{")) {
1548
+ const close = b.after[k + 2];
1549
+ return close === e || (close + 2 === e && isWord(tokens[close], "from") && literal(tokens[close + 1]));
1550
+ }
1551
+ // `export { config as default }`: the one export
1552
+ if (!isPunct(t, "{") || b.after[k + 1] !== e)
1553
+ return false;
1554
+ const items = listOf(k + 1);
1555
+ if (items === null || items.length !== 1)
1556
+ return false;
1557
+ const [x, y] = items[0];
1558
+ const named = isWord(tokens[x + 2], "default") || (literal(tokens[x + 2]) && tokens[x + 2].value === "default");
1559
+ if (!(y === x + 3 && tokens[x].kind === "ident" && isWord(tokens[x + 1], "as") && named))
1560
+ return false;
1561
+ exports++;
1562
+ exported = { name: x };
1563
+ return true;
1564
+ };
1565
+ const declaration = (j, e) => {
1566
+ if (j >= e)
1567
+ return false;
1568
+ for (const [x, y] of splitAt(j, e)) {
1569
+ if (x === y)
1570
+ return false;
1571
+ const t = tokens[x];
1572
+ if (t.kind === "ident") {
1573
+ let v = x + 1;
1574
+ // a type: `const nextConfig: NextConfig = …`
1575
+ if (isPunct(tokens[v], ":")) {
1576
+ v = simpleType(v + 1, y);
1577
+ if (v < 0)
1578
+ return false;
1579
+ }
1580
+ if (v === y) {
1581
+ if (!bind(x, { kind: "none" }))
1582
+ return false;
1583
+ continue;
1584
+ }
1585
+ if (!isPunct(tokens[v], "=") || isPunct(tokens[v + 1], "=") || v + 1 >= y || !bind(x, { kind: "value", from: v + 1, to: y }))
1586
+ return false;
1587
+ }
1588
+ else
1589
+ return false;
1590
+ }
1591
+ return true;
1592
+ };
1593
+ const statement = (k, e) => {
1594
+ const t = tokens[k];
1595
+ // a directive: "use strict"
1596
+ if (e === k + 1 && literal(t))
1597
+ return true;
1598
+ if (t.kind !== "ident")
1599
+ return false;
1600
+ switch (t.value) {
1601
+ case "import":
1602
+ return typeImport(k, e);
1603
+ case "export":
1604
+ return exportStatement(k, e);
1605
+ case "const":
1606
+ case "let":
1607
+ case "var":
1608
+ return declaration(k + 1, e);
1609
+ case "module":
1610
+ return isPunct(tokens[k + 1], ".") && isWord(tokens[k + 2], "exports") && isPunct(tokens[k + 3], "=") && !isPunct(tokens[k + 4], "=") && exportValue(k + 4, e);
1611
+ }
1612
+ return false;
1613
+ };
1614
+ for (let k = 0; k < tokens.length;) {
1615
+ if (isPunct(tokens[k], ";")) {
1616
+ k++;
1617
+ continue;
1618
+ }
1619
+ const e = statementEnd(k);
1620
+ if (e === k || !statement(k, e))
1621
+ return null;
1622
+ k = e;
1623
+ }
1624
+ if (exports > 1)
1625
+ return null;
1626
+ const out = exported;
1627
+ if (out !== null && !("name" in out ? use(out.name, "config") : configValue(out.from, out.to)))
1628
+ return null;
1629
+ // a name the load doesn't use still has its value computed: data, or a module loaded
1630
+ for (const binding of bindings.values()) {
1631
+ if (binding.uses > 0 || binding.kind !== "value")
1632
+ continue;
1633
+ binding.role = "data";
1634
+ binding.uses++;
1635
+ if (!data(binding.from, binding.to))
1636
+ return null;
1637
+ }
1638
+ return { configs, props };
1639
+ }
1640
+ /** next.config's `pageExtensions`, `basePath` and `output`, read only when
1641
+ the file is written in a declarative subset of JavaScript, where no code
1642
+ runs while Next loads it — neither the file's nor a module's:
1643
+
1644
+ - statements: type imports (`import type { NextConfig } from "next"`),
1645
+ declarations, and one export of the config object itself —
1646
+ `module.exports = {…}`, `export default nextConfig` or `export {
1647
+ config as default }`;
1648
+ - values: literals, and arrays and objects of them, `process.env.NAME`,
1649
+ `process.cwd()`, `__dirname`, `import.meta.dirname`, `new URL(…)`,
1650
+ `require.resolve("…")`, names declared so; `!`, `+`, comparisons,
1651
+ `&&`, `||`, `??` and `? :` over them; a type after `as` or
1652
+ `satisfies`;
1653
+ - functions only as Next's hooks (`webpack`, `headers`, `redirects`,
1654
+ `rewrites`, `generateBuildId`, `exportPathMap`) at the top of the
1655
+ exported object: Next calls them once it has loaded the config.
1656
+
1657
+ Anything else makes it null: a module loaded in any way (`import "…"`,
1658
+ `import x from "…"`, `require(…)`) — its code runs, and which module a
1659
+ name loads its spelling doesn't show (Node's self-reference, a
1660
+ next.config.ts's tsconfig `paths`); so a plugin's call around the
1661
+ config (`withMDX(nextConfig)`) and a `path` helper too (block 348). And
1662
+ any other statement, a function or class of the file, a getter, a
1663
+ spread, a member of a value, a call, a name bound twice or over one the
1664
+ subset reads (`require`, `process`), the config's name used twice. Each
1665
+ setting: a literal, once, in that object — `pageExtensions` or
1666
+ `basePath` named anywhere else in the file makes it null, and an
1667
+ `output` the reading can't take (an expression, a second one, one a
1668
+ hook's code writes) makes only `output` null. `live.test.ts` holds the
1669
+ reading to Next's own `loadConfig` over generated configs.
1670
+
1671
+ What the reading trusts: Next calls the hooks only after loading, and a
1672
+ hook leaves the settings it loaded alone. */
1673
+ export function nextConfigOf(config) {
1674
+ if (config === null)
1675
+ return { pageExtensions: PAGE_EXTENSIONS, basePath: "" };
1676
+ // what JavaScript reads apart from the tokens: a hashbang line, CommonJS's `<!--` and `-->` comments — `-->` may follow a `/* … */` on its line — anywhere
1677
+ if (config.startsWith("#!") || /<!--|-->/.test(config))
1678
+ return null;
1679
+ // every line terminator JavaScript knows ends a `//` comment: `\r`, U+2028 and U+2029 too
1680
+ config = config.replace(/\r\n?|[\u2028\u2029]/g, "\n");
1681
+ const hidden = [];
1682
+ const ambiguous = [];
1683
+ const tokens = tokenize(config, hidden, ambiguous, true);
1684
+ const b = bracketsOf(tokens);
1685
+ if (!balanced(tokens, b) || !slashesAgree(tokens, b))
1686
+ return null;
1687
+ // the code of each template's `${…}` — a hook's only, since data has no such template — read for the names it mentions
1688
+ const all = [...tokens];
1689
+ for (let i = 0; i < hidden.length; i++) {
1690
+ if (i >= 1000)
1691
+ return null;
1692
+ const part = tokenize(`;\n${hidden[i]}\n;`, hidden, ambiguous, true);
1693
+ if (!slashesAgree(part, bracketsOf(part)))
1694
+ return null;
1695
+ for (const t of part)
1696
+ all.push(t);
1697
+ }
1698
+ // a `/` right after `}`, in the file or a template's code, may start a regex that holds a brace
1699
+ if (ambiguous.length > 0)
1700
+ return null;
1701
+ // an identifier with an escape in its name, a backslash outside a string, a string a line break cut off
1702
+ if (all.some((t) => isPunct(t, "\\") || (t.kind === "ident" && t.escaped === true) || (t.kind === "string" && t.unclosed === true)))
1703
+ return null;
1704
+ const subset = declarative(tokens, b);
1705
+ if (subset === null)
1706
+ return null;
1707
+ // each setting: a literal, once, in the config object
1708
+ const settings = {};
1709
+ const accepted = new Set();
1710
+ // `output` never makes the config unreadable: only its value is in doubt — undefined while unset, null when it can't be read
1711
+ let output;
1712
+ for (const open of subset.configs) {
1713
+ for (const p of subset.props.get(open) ?? []) {
1714
+ // what Next takes before the keys: `then` — it awaits the config — and `default`, which a next.config.ts transpiled to CommonJS is read from
1715
+ if (p.key === "then" || p.key === "default")
1716
+ return null;
1717
+ const value = tokens[p.from];
1718
+ const single = p.kind === "value" && p.to === p.from + 1 && literal(value);
1719
+ if (p.key === "basePath") {
1720
+ // a path from the root, without a trailing slash — anything else Next refuses
1721
+ if (settings.basePath !== undefined || !single || (value.value !== "" && !/^\/[^?#\s]*[^/?#\s]$/.test(value.value)))
1722
+ return null;
1723
+ settings.basePath = value.value;
1724
+ accepted.add(p.at);
1725
+ }
1726
+ else if (p.key === "pageExtensions") {
1727
+ if (settings.pageExtensions !== undefined || p.kind !== "value" || !isPunct(value, "[") || b.after[p.from] !== p.to)
1728
+ return null;
1729
+ const list = [];
1730
+ for (const [x, y] of itemsOf(tokens, b.after, p.from)) {
1731
+ if (x === y)
1732
+ continue;
1733
+ if (y !== x + 1 || !literal(tokens[x]))
1734
+ return null;
1735
+ list.push(tokens[x].value);
1736
+ }
1737
+ settings.pageExtensions = list;
1738
+ accepted.add(p.at);
1739
+ }
1740
+ else if (p.key === "output")
1741
+ output = output === undefined && single ? value.value : null;
1742
+ }
1743
+ }
1744
+ // a setting's name anywhere else — a nested object, a hook's code, a string — or inside a longer string (JSON the code parses)
1745
+ if (all.some((t, k) => (t.kind === "ident" || t.kind === "string") && SETTINGS.includes(t.value) && !accepted.has(k)))
1746
+ return null;
1747
+ if (all.some((t) => t.kind === "string" && !SETTINGS.includes(t.value) && SETTINGS.some((s) => t.value.includes(s))))
1748
+ return null;
1749
+ // a hook's code that copies properties, sets a prototype or writes a member by a name it computes — `config[name] = …`, `for (config[name] of …)`:
1750
+ // Next runs it after loading, and what it writes the reading can't place
1751
+ const ball = bracketsOf(all);
1752
+ for (let k = 0; k < all.length; k++) {
1753
+ const t = all[k];
1754
+ if ((t.kind === "ident" && COPIES.has(t.value)) || (t.kind === "string" && t.value === "__proto__"))
1755
+ return null;
1756
+ if (!isPunct(t, "["))
1757
+ continue;
1758
+ // unless a lone string or digits name it: `rules[0] = …`
1759
+ const close = ball.after[k] - 1;
1760
+ if (close === k + 2 && literal(all[k + 1]))
1761
+ continue;
1762
+ let digits = k + 1;
1763
+ while (digits < close && isDigit(all[digits]))
1764
+ digits++;
1765
+ if (digits === close && close > k + 1)
1766
+ continue;
1767
+ const prev = all[k - 1];
1768
+ const key = (isPunct(prev, "{") || isPunct(prev, ",") || isPunct(prev, "*") || (prev?.kind === "ident" && KEY_PREFIX.has(prev.value))) && (isPunct(all[close + 1], ":") || isPunct(all[close + 1], "("));
1769
+ const head = ball.parent[k];
1770
+ const loop = head >= 0 && isPunct(all[head], "(") && (isWord(all[head - 1], "for") || (isWord(all[head - 1], "await") && isWord(all[head - 2], "for"))) && (isWord(all[close + 1], "of") || isWord(all[close + 1], "in"));
1771
+ const member = (prev?.kind === "ident" || isPunct(prev, ")") || isPunct(prev, "]")) && (assigned(all, close) || loop);
1772
+ if (key || member)
1773
+ return null;
1774
+ }
1775
+ // and `output` a hook's code writes — `config.output = …`, `config["output"] = …`, in a pattern or a loop's head — or deletes
1776
+ const patterns = new Int32Array(all.length + 1);
1777
+ for (let k = 0; k < all.length; k++) {
1778
+ const after = ball.after[k];
1779
+ if ((isPunct(all[k], "{") || isPunct(all[k], "[")) && !DECLARE.has(all[k - 1]?.value ?? "") && isPunct(all[after], "=") && !isPunct(all[after + 1], "=")) {
1780
+ patterns[k]++;
1781
+ patterns[after]--;
1782
+ }
1783
+ }
1784
+ for (let k = 0, inPattern = 0; k < all.length; k++) {
1785
+ inPattern += patterns[k];
1786
+ const t = all[k];
1787
+ const dotted = t.kind === "ident" && t.value === "output" && isPunct(all[k - 1], ".");
1788
+ const bracketed = t.kind === "string" && t.value === "output" && isPunct(all[k - 1], "[") && isPunct(all[k + 1], "]");
1789
+ if (!dotted && !bracketed)
1790
+ continue;
1791
+ // parentheses around the target, TypeScript's `!` after it: `(config.output) = …`, `config.output! = …`
1792
+ let end = bracketed ? k + 1 : k;
1793
+ while (isPunct(all[end + 1], ")") || isPunct(all[end + 1], "!"))
1794
+ end++;
1795
+ const head = ball.parent[k - 1];
1796
+ const loop = head >= 0 && isPunct(all[head], "(") && (isWord(all[head - 1], "for") || (isWord(all[head - 1], "await") && isWord(all[head - 2], "for"))) && (isWord(all[end + 1], "of") || isWord(all[end + 1], "in"));
1797
+ if (changes(all, end) || inPattern > 0 || loop)
1798
+ output = null;
1799
+ }
1800
+ for (let k = 0; k < all.length; k++) {
1801
+ if (!isWord(all[k], "delete") || isPunct(all[k - 1], "."))
1802
+ continue;
1803
+ const to = initializerEnd(all, ball.after, k + 1);
1804
+ for (let j = k + 1; j < to; j++)
1805
+ if ((isWord(all[j], "output") && isPunct(all[j - 1], ".")) || (all[j].kind === "string" && all[j].value === "output"))
1806
+ output = null;
1807
+ }
1808
+ const read = { pageExtensions: settings.pageExtensions ?? PAGE_EXTENSIONS, basePath: settings.basePath ?? "" };
1809
+ if (output !== undefined)
1810
+ read.output = output;
1811
+ if (all.some((t) => (t.kind === "ident" || t.kind === "string") && t.value === "i18n"))
1812
+ read.i18n = true;
1813
+ return read;
1814
+ }
1815
+ /** Whether the tokens from `from` up to `to` are a value Next's build
1816
+ reads out of a module's `config` (extractValue): a string, a number,
1817
+ true, false, null or undefined, or an array or object of them — no
1818
+ spread, hole, computed key, shorthand, method or expression. */
1819
+ function extractable(tokens, after, from, to) {
1820
+ if (from >= to)
1821
+ return false;
1822
+ const t = tokens[from];
1823
+ if (to === from + 1 && (literal(t) || isWord(t, "true") || isWord(t, "false") || isWord(t, "null") || isWord(t, "undefined")))
1824
+ return true;
1825
+ // digits, and one decimal point: `12`, `1.5`
1826
+ if (/^\d$/.test(t.kind === "punct" ? t.value : "")) {
1827
+ let points = 0;
1828
+ for (let k = from; k < to; k++) {
1829
+ const u = tokens[k];
1830
+ if (u.kind !== "punct" || !/^[\d.]$/.test(u.value) || (u.value === "." && ++points > 1))
1831
+ return false;
1832
+ }
1833
+ return true;
1834
+ }
1835
+ if ((!isPunct(t, "[") && !isPunct(t, "{")) || after[from] !== to)
1836
+ return false;
1837
+ return itemsOf(tokens, after, from).every(([a, z], i, all) => {
1838
+ // a trailing comma leaves nothing after it
1839
+ if (a === z)
1840
+ return i === all.length - 1 && all.length > 1;
1841
+ if (isSpread(tokens, a))
1842
+ return false;
1843
+ if (t.value === "[")
1844
+ return extractable(tokens, after, a, z);
1845
+ const key = tokens[a];
1846
+ return (key.kind === "ident" || literal(key)) && isPunct(tokens[a + 1], ":") && extractable(tokens, after, a + 2, z);
1847
+ });
1848
+ }
1849
+ /** The items of the array or object that opens at `open`, as token ranges
1850
+ between its top-level commas. */
1851
+ function itemsOf(tokens, after, open) {
1852
+ const close = after[open] - 1;
1853
+ const items = [];
1854
+ let start = open + 1;
1855
+ for (let k = start; k <= close; k++) {
1856
+ const u = tokens[k];
1857
+ if (k < close && u.kind === "punct" && "([{".includes(u.value))
1858
+ k = after[k] - 1;
1859
+ else if (k === close || isPunct(u, ",")) {
1860
+ items.push([start, k]);
1861
+ start = k + 1;
1862
+ }
1863
+ }
1864
+ return items;
1865
+ }
1866
+ /** The matcher of a middleware file, read as Next's build reads it: only
1867
+ `export const config = { … }` (a type or `satisfies` allowed), every
1868
+ value in it a literal (extractable), `matcher` a string or an array of
1869
+ strings and `{ source }` objects. Anything else Next can't read either
1870
+ and runs the middleware on every path — or reads what doctor can't. */
1871
+ export function middlewareMatcher(text) {
1872
+ const tokens = tokenize(text);
1873
+ const b = bracketsOf(tokens);
1874
+ let config = null;
1875
+ let declared = -1;
1876
+ for (let i = 0; i < tokens.length; i++) {
1877
+ if (!isWord(tokens[i], "export") || isPunct(tokens[i - 1], ".") || !isWord(tokens[i + 1], "const") || !isWord(tokens[i + 2], "config"))
1878
+ continue;
1879
+ if (config !== null)
1880
+ return "unread";
1881
+ declared = i + 2;
1882
+ let eq = i + 3;
1883
+ if (isPunct(tokens[eq], ":"))
1884
+ eq = typeEnd(tokens, b.after, eq + 1, tokens.length);
1885
+ if (eq < 0 || !isPunct(tokens[eq], "=") || isPunct(tokens[eq + 1], "=") || !isPunct(tokens[eq + 1], "{"))
1886
+ return "unread";
1887
+ const open = eq + 1;
1888
+ const end = b.after[open];
1889
+ // the statement ends there — `;`, `,`, a new line, the file — or goes on with `satisfies T`, which Next reads through
1890
+ const tail = tokens[end];
1891
+ if (tail && !isPunct(tail, ";") && !isPunct(tail, ",") && !isWord(tail, "satisfies") && !(tail.nl === true && tail.kind === "ident"))
1892
+ return "unread";
1893
+ if (!extractable(tokens, b.after, open, end))
1894
+ return "unread";
1895
+ config = "none";
1896
+ for (const [a, z] of itemsOf(tokens, b.after, open)) {
1897
+ if (a === z || tokens[a].value !== "matcher")
1898
+ continue;
1899
+ if (config !== "none")
1900
+ return "unread";
1901
+ const v = a + 2;
1902
+ if (z === v + 1 && literal(tokens[v]))
1903
+ config = [tokens[v].value];
1904
+ else if (isPunct(tokens[v], "[")) {
1905
+ const sources = [];
1906
+ for (const [c, d] of itemsOf(tokens, b.after, v)) {
1907
+ if (c === d)
1908
+ continue;
1909
+ if (d === c + 1 && literal(tokens[c]))
1910
+ sources.push(tokens[c].value);
1911
+ else if (isPunct(tokens[c], "{")) {
1912
+ const source = itemsOf(tokens, b.after, c).filter(([e]) => tokens[e]?.value === "source" && isPunct(tokens[e + 1], ":"));
1913
+ if (source.length !== 1 || source[0][1] !== source[0][0] + 3 || !literal(tokens[source[0][0] + 2]))
1914
+ return "unread";
1915
+ sources.push(tokens[source[0][0] + 2].value);
1916
+ }
1917
+ else
1918
+ return "unread";
1919
+ }
1920
+ config = sources;
1921
+ }
1922
+ else
1923
+ return "unread";
1924
+ }
1925
+ i = end - 1;
1926
+ }
1927
+ // any other export that may be `config` — `export { config }`, `export let config`, a later declarator, `export *`, CommonJS — Next's reading doesn't see
1928
+ const rest = declared === -1 ? tokens : tokens.map((t, k) => (k === declared ? { ...t, value: "" } : t));
1929
+ if (setsExport(rest, b.after, "config", () => false))
1930
+ return "unread";
1931
+ return config ?? "none";
1932
+ }
1933
+ /** Whether `c` is one of the characters — never for the end of the string. */
1934
+ const oneOf = (chars, c) => c !== undefined && c.length === 1 && chars.includes(c);
1935
+ // what a path may hold for doctor to place it among a matcher's sources: nothing a URL would encode
1936
+ const PLAIN_PATH = /^\/[A-Za-z0-9/_.~-]*$/;
1937
+ /** Whether a matcher source may take the path `at` (without the basePath,
1938
+ which Next puts in front of both). False only when it surely can't: the
1939
+ literal the source starts with isn't how the path starts, or right
1940
+ after it comes a group — path-to-regexp's `(…)`, kept verbatim in the
1941
+ regex Next builds — that matches no start of the rest, in either case.
1942
+ Everything else may. */
1943
+ export function sourceMayTake(source, at) {
1944
+ if (!source.startsWith("/") || !PLAIN_PATH.test(at))
1945
+ return true;
1946
+ let i = 0;
1947
+ while (i < source.length && !":(){}*+?\\".includes(source[i]))
1948
+ i++;
1949
+ // a `/` or `.` right before a param or a group is its prefix, which a modifier makes optional: `/blog/:path*` takes `/blog`
1950
+ const required = oneOf(":(", source[i]) && oneOf("/.", source[i - 1]) ? i - 1 : i;
1951
+ const start = source.slice(0, required).toLowerCase();
1952
+ // the tab may ask with a trailing slash that Next takes off
1953
+ const paths = [at, at === "/" ? at : `${at}/`].map((p) => p.toLowerCase());
1954
+ if (!paths.some((p) => p.startsWith(start)))
1955
+ return false;
1956
+ if (source[i] !== "(" || source[i + 1] === "?")
1957
+ return true;
1958
+ // the group as path-to-regexp reads it: to its balanced `)`, a backslash taking the next character along
1959
+ let depth = 1;
1960
+ let j = i + 1;
1961
+ for (; j < source.length && depth > 0; j++) {
1962
+ const c = source[j];
1963
+ if (c === "\\")
1964
+ j++;
1965
+ else if (c === ")")
1966
+ depth--;
1967
+ // a capturing group inside is refused
1968
+ else if (c === "(" && source[j + 1] !== "?")
1969
+ return true;
1970
+ else if (c === "(")
1971
+ depth++;
1972
+ }
1973
+ // a modifier may skip the group; a backreference numbers groups differently inside Next's regex
1974
+ if (depth !== 0 || oneOf("?*+", source[j]) || /\\\d/.test(source.slice(i, j)))
1975
+ return true;
1976
+ // without a modifier the group's prefix is required: the group starts right after the whole literal
1977
+ const literalStart = source.slice(0, i).toLowerCase();
1978
+ const group = source.slice(i + 1, j - 1);
1979
+ try {
1980
+ for (const flags of ["y", "iy"]) {
1981
+ const re = new RegExp(group, flags);
1982
+ for (const [n, p] of paths.entries()) {
1983
+ if (!p.startsWith(literalStart))
1984
+ continue;
1985
+ // matched as the tab asks it, case and all
1986
+ re.lastIndex = literalStart.length;
1987
+ if (re.test(n === 0 ? at : `${at}/`))
1988
+ return true;
1989
+ }
1990
+ }
1991
+ }
1992
+ catch {
1993
+ return true;
1994
+ }
1995
+ return false;
1996
+ }
1997
+ /** Why a middleware file may run on `at` (a path without the basePath), or null when it surely doesn't. */
1998
+ function middlewareOn(m, at, i18n) {
1999
+ if (m.matcher === "none" || m.matcher === "unread")
2000
+ return { reason: m.matcher };
2001
+ const source = m.matcher.find((s) => sourceMayTake(s, at));
2002
+ if (source !== undefined)
2003
+ return { reason: "matcher", source };
2004
+ // with locales Next puts one in front of each source, and of the path
2005
+ return i18n && m.matcher.length > 0 ? { reason: "i18n" } : null;
2006
+ }
2007
+ /** Whether a version is at least `min`, by its major, minor and patch —
2008
+ false for one that doesn't start with three numbers. */
2009
+ export function atLeast(version, min) {
2010
+ const parts = (v) => /^v?(\d+)\.(\d+)\.(\d+)/.exec(v)?.slice(1).map(Number) ?? null;
2011
+ const a = parts(version);
2012
+ const b = parts(min);
2013
+ if (!a)
2014
+ return false;
2015
+ for (let i = 0; i < 3; i++)
2016
+ if (a[i] !== b[i])
2017
+ return a[i] > b[i];
2018
+ return true;
2019
+ }
2020
+ /** The version of a package the site's code resolves: the package's
2021
+ folder under the nearest node_modules, from the site's folder up — as
2022
+ Node and Next's bundler find it. Null when none is there. */
2023
+ async function installedVersion(dir, pkg) {
2024
+ for (let at = path.resolve(dir);;) {
2025
+ try {
2026
+ const raw = await readFile(path.join(at, "node_modules", ...pkg.split("/"), "package.json"), "utf8");
2027
+ const version = JSON.parse(raw).version;
2028
+ return typeof version === "string" ? version : null;
2029
+ }
2030
+ catch {
2031
+ // not here: the folder above
2032
+ }
2033
+ const up = path.dirname(at);
2034
+ if (up === at)
2035
+ return null;
2036
+ at = up;
2037
+ }
2038
+ }
2039
+ /** The middleware files Next runs for the site — `middleware`, and
2040
+ `proxy` unless the installed Next is older than 16, which names it so —
2041
+ with one of its page extensions, in the folder that holds the pages
2042
+ folder it reads, or else the app folder: the root, or `src/`. Read from
2043
+ the folder itself, not the file list, which may stop before it. */
2044
+ async function middlewareFiles(dir, extensions, next) {
2045
+ const folder = async (name) => ((await atRoot(dir, name)) ? name : (await atRoot(dir, `src/${name}`)) ? `src/${name}` : null);
2046
+ const read = (await folder("pages")) ?? (await folder("app"));
2047
+ const at = read?.startsWith("src/") ? "src" : "";
2048
+ let names;
2049
+ try {
2050
+ names = (await readdir(path.join(dir, at), { withFileTypes: true })).filter((e) => e.isFile()).map((e) => e.name).sort();
2051
+ }
2052
+ catch {
2053
+ return [];
2054
+ }
2055
+ const kinds = next === null || atLeast(next, "16.0.0") ? ["middleware", "proxy"] : ["middleware"];
2056
+ // Next's own test: the extensions joined into a pattern as they are
2057
+ let test;
2058
+ try {
2059
+ const re = new RegExp(`^(?:${kinds.join("|")})\\.(?:${extensions.join("|")})$`);
2060
+ test = (name) => re.test(name);
2061
+ }
2062
+ catch {
2063
+ test = (name) => extensions.some((ext) => kinds.some((kind) => name === `${kind}.${ext}`));
2064
+ }
2065
+ const found = [];
2066
+ for (const name of names.filter(test)) {
2067
+ const file = at ? `${at}/${name}` : name;
2068
+ let text;
2069
+ try {
2070
+ text = await readFile(path.join(dir, file), "utf8");
2071
+ }
2072
+ catch {
2073
+ found.push({ file, matcher: "unread" });
2074
+ continue;
2075
+ }
2076
+ found.push({ file, matcher: text.length > 2_000_000 ? "unread" : middlewareMatcher(text) });
2077
+ }
2078
+ return found;
2079
+ }
2080
+ export async function findLive(dir, maxFiles = 4000) {
2081
+ const { files } = await listFiles(dir, maxFiles);
2082
+ const config = NEXT_CONFIGS.find((name) => files.includes(name));
2083
+ let configText = null;
2084
+ if (config) {
2085
+ try {
2086
+ configText = await readFile(path.join(dir, config), "utf8");
2087
+ }
2088
+ catch {
2089
+ // unreadable: the defaults
2090
+ }
2091
+ }
2092
+ const settings = nextConfigOf(configText);
2093
+ const extensions = settings?.pageExtensions ?? null;
2094
+ const routeExtension = extensions === null || extensions.includes("ts") ? "ts" : (extensions[0] ?? "ts");
2095
+ // a folder under src/ that Next doesn't read: nothing in it runs
2096
+ const rootApp = await atRoot(dir, "app");
2097
+ const ignored = [...(rootApp ? ["src/app/"] : []), ...((await atRoot(dir, "pages")) ? ["src/pages/"] : [])];
2098
+ const appDir = rootApp ? "app" : (await atRoot(dir, "src/app")) ? "src/app" : undefined;
2099
+ const listeners = [];
2100
+ const routes = [];
2101
+ const pages = [];
2102
+ for (const file of files) {
2103
+ if (ignored.some((prefix) => file.startsWith(prefix)))
2104
+ continue;
2105
+ const route = extensions === null ? null : routeFileOf(file, extensions);
2106
+ const page = extensions === null ? null : pageFileOf(file, extensions);
2107
+ if (page !== null)
2108
+ pages.push({ file, path: page });
2109
+ let text;
2110
+ try {
2111
+ text = await readFile(path.join(dir, file), "utf8");
2112
+ }
2113
+ catch {
2114
+ continue;
2115
+ }
2116
+ if (text.length > 2_000_000)
2117
+ continue;
2118
+ const words = WORDS.test(text);
2119
+ const found = words ? liveInFile(file, text) : { listeners: [], handler: null, params: false };
2120
+ listeners.push(...found.listeners);
2121
+ if (route !== null)
2122
+ routes.push({ file, path: route, handler: found.handler, ...(found.params ? { params: true } : {}), ...(found.segment ? { segment: found.segment } : {}) });
2123
+ }
2124
+ const facts = { listeners, routes, pages, routeExtension };
2125
+ if (appDir)
2126
+ facts.appDir = appDir;
2127
+ if (settings?.basePath)
2128
+ facts.basePath = settings.basePath;
2129
+ if (settings === null && config)
2130
+ facts.config = config;
2131
+ if (config && settings && (settings.output === "export" || settings.output === null))
2132
+ facts.output = { file: config, value: settings.output };
2133
+ const sdk = await installedVersion(dir, "@mapled/next");
2134
+ if (sdk !== null)
2135
+ facts.sdk = sdk;
2136
+ if (settings?.i18n)
2137
+ facts.i18n = true;
2138
+ if (extensions !== null) {
2139
+ const middleware = await middlewareFiles(dir, extensions, await installedVersion(dir, "next"));
2140
+ if (middleware.length > 0)
2141
+ facts.middleware = middleware;
2142
+ }
2143
+ return facts;
2144
+ }
2145
+ /** Whether a literal endpoint names its own origin — `https://…`, `//host/…`
2146
+ — which the code doesn't show to be the site's: `watchRelease` fetches it
2147
+ as written, and no route file of this site is known to answer it. */
2148
+ export const fullAddress = (endpoint) => /^[a-z][a-z\d+.-]*:/i.test(endpoint) || /^[/\\][/\\]/.test(endpoint);
2149
+ const SITE = "http://site.invalid";
2150
+ /** The path on the site a literal endpoint asks — the only thing a route
2151
+ file can answer: a path from the site's root. Null for anything else:
2152
+ a relative endpoint resolves against each page's address, and a full
2153
+ address may be another origin's — no one route file is their answer. */
2154
+ const sitePath = (endpoint) => {
2155
+ if (!endpoint.startsWith("/") || fullAddress(endpoint))
2156
+ return null;
2157
+ let pathname;
2158
+ try {
2159
+ const url = new URL(endpoint, SITE);
2160
+ // the parser drops tabs and line breaks and reads `\` as `/`: whatever reaches another host isn't a path
2161
+ if (url.origin !== SITE)
2162
+ return null;
2163
+ pathname = url.pathname;
2164
+ }
2165
+ catch {
2166
+ return null;
2167
+ }
2168
+ // the parser percent-encodes; route folders are named as written
2169
+ try {
2170
+ pathname = decodeURI(pathname);
2171
+ }
2172
+ catch {
2173
+ // a stray % stays as it is
2174
+ }
2175
+ const bare = pathname.replace(/\/+$/, "");
2176
+ return bare === "" ? "/" : bare;
2177
+ };
2178
+ /** How a route folder's segment matches a path's, most specific first —
2179
+ as the App Router picks among routes that match: a static segment, a
2180
+ dynamic `[x]` (one segment), a catch-all `[...x]` (one or more), an
2181
+ optional catch-all `[[...x]]` (none or more). */
2182
+ const segmentRank = (segment) => (/^\[\[\.\.\.[^\]]+\]\]$/.test(segment) ? 3 : /^\[\.\.\.[^\]]+\]$/.test(segment) ? 2 : /^\[[^\]]+\]$/.test(segment) ? 1 : 0);
2183
+ const fileOf = (a) => (a.route ?? a.page).file;
2184
+ // the same ranks: a route before a page, then by file
2185
+ const before = (a, b) => !b || (a.route ? 0 : 1) - (b.route ? 0 : 1) < 0 || ((a.route ? 0 : 1) === (b.route ? 0 : 1) && fileOf(a) < fileOf(b));
2186
+ /** Looks paths up among the route files and pages: each file's segments
2187
+ are read once, and a lookup walks only the folders its path can match —
2188
+ each asked path once. */
2189
+ function answerer(routes, pages) {
2190
+ const split = (p) => (p === "/" ? [] : p.slice(1).split("/"));
2191
+ const root = { fixed: new Map() };
2192
+ const add = (answer, answers) => {
2193
+ const segments = split(answers);
2194
+ const ranks = segments.map(segmentRank);
2195
+ const a = { ...answer, ranks };
2196
+ let n = root;
2197
+ for (let i = 0; i < segments.length; i++) {
2198
+ const rank = ranks[i];
2199
+ if (rank >= 2) {
2200
+ // a catch-all is the last segment, or the folder matches nothing
2201
+ if (i !== segments.length - 1)
2202
+ return;
2203
+ if (rank === 2 && before(a, n.rest))
2204
+ n.rest = a;
2205
+ if (rank === 3 && before(a, n.optional))
2206
+ n.optional = a;
2207
+ return;
2208
+ }
2209
+ if (rank === 1)
2210
+ n = n.param ??= { fixed: new Map() };
2211
+ else {
2212
+ const next = n.fixed.get(segments[i]) ?? { fixed: new Map() };
2213
+ n.fixed.set(segments[i], next);
2214
+ n = next;
2215
+ }
2216
+ }
2217
+ if (before(a, n.end))
2218
+ n.end = a;
2219
+ };
2220
+ for (const route of routes)
2221
+ add({ route }, route.path);
2222
+ for (const page of pages)
2223
+ add({ page }, page.path);
2224
+ const order = (a, b) => {
2225
+ for (let i = 0; i < Math.max(a.length, b.length); i++)
2226
+ if ((a[i] ?? -1) !== (b[i] ?? -1))
2227
+ return (a[i] ?? -1) - (b[i] ?? -1);
2228
+ return 0;
2229
+ };
2230
+ const cache = new Map();
2231
+ return (at) => {
2232
+ if (cache.has(at))
2233
+ return cache.get(at);
2234
+ const asked = split(at);
2235
+ const found = [];
2236
+ // a tree: each node is reached one way, so a lookup visits it once at most
2237
+ const walk = (n, i) => {
2238
+ // a catch-all takes the rest: one segment or more, or — optional — none too
2239
+ if (n.rest && asked.length > i)
2240
+ found.push(n.rest);
2241
+ if (n.optional)
2242
+ found.push(n.optional);
2243
+ if (i === asked.length) {
2244
+ if (n.end)
2245
+ found.push(n.end);
2246
+ return;
2247
+ }
2248
+ const fixed = n.fixed.get(asked[i]);
2249
+ if (fixed)
2250
+ walk(fixed, i + 1);
2251
+ if (n.param)
2252
+ walk(n.param, i + 1);
2253
+ };
2254
+ walk(root, 0);
2255
+ let best;
2256
+ for (const c of found) {
2257
+ const o = best ? order(c.ranks, best.ranks) : -1;
2258
+ if (o < 0 || (o === 0 && before(c, best)))
2259
+ best = c;
2260
+ }
2261
+ cache.set(at, best);
2262
+ return best;
2263
+ };
2264
+ }
2265
+ /** Whether a route's folders take a param: `[x]`, `[...x]`, `[[...x]]`. */
2266
+ const dynamicRoute = (route) => route.path.split("/").some((s) => segmentRank(s) > 0);
2267
+ /** Whether every listener has a route that answers it. The first one
2268
+ that doesn't is the one to name. */
2269
+ export function liveState(facts) {
2270
+ // the conventional path first, then by file: the same answer every run
2271
+ const rank = (r) => (r.path === RELEASE_ROUTE ? 0 : 1);
2272
+ const handlerRoutes = facts.routes.filter((r) => r.handler === "GET").sort((a, b) => rank(a) - rank(b) || (a.file < b.file ? -1 : a.file > b.file ? 1 : 0));
2273
+ if (facts.listeners.length === 0)
2274
+ return { kind: "unused", handlerRoute: handlerRoutes[0] ?? null };
2275
+ const answerOf = answerer(facts.routes, facts.pages ?? []);
2276
+ const base = facts.basePath ?? "";
2277
+ let served = null;
2278
+ for (const listener of facts.listeners) {
2279
+ if (listener.endpoint === null) {
2280
+ // the code decides where it asks: with no release route at all it's
2281
+ // surely missing; with one — or routes the reading can't know — nothing says it's the route asked
2282
+ if (handlerRoutes.length === 0 && !facts.config)
2283
+ return { kind: "missing", listener, endpoint: null };
2284
+ return { kind: "unresolved", listener };
2285
+ }
2286
+ const written = sitePath(listener.endpoint);
2287
+ if (written === null)
2288
+ return { kind: "unresolved", listener };
2289
+ if (facts.config)
2290
+ return { kind: "config", listener, file: facts.config };
2291
+ // a static export has no server: no route runs, whatever the files say
2292
+ if (facts.output)
2293
+ return { kind: "export", listener, file: facts.output.file, output: facts.output.value };
2294
+ let at = written;
2295
+ // no endpoint given: from BASE_PATH_SINCE the SDK asks under the basePath Next writes into the bundle, before it at the root
2296
+ if (listener.byDefault && base !== "") {
2297
+ if (facts.sdk === undefined || !atLeast(facts.sdk, BASE_PATH_SINCE))
2298
+ return { kind: "default_base", listener, basePath: base, sdk: facts.sdk ?? null };
2299
+ at = `${base}${RELEASE_ROUTE}`;
2300
+ }
2301
+ // with a basePath, Next answers only under it: a route file's path follows it
2302
+ if (base !== "" && at !== base && !at.startsWith(`${base}/`))
2303
+ return { kind: "outside", listener, endpoint: at, basePath: base };
2304
+ const answer = answerOf(base === "" ? at : at.slice(base.length) || "/");
2305
+ if (!answer)
2306
+ return { kind: "missing", listener, endpoint: at };
2307
+ if (answer.page) {
2308
+ // a page takes the path: through a param, a route of its own at the path wins over it; a route can't share a page's path
2309
+ if (answer.ranks.some((r) => r > 0))
2310
+ return { kind: "missing", listener, endpoint: at };
2311
+ return { kind: "taken", listener, page: answer.page, endpoint: at };
2312
+ }
2313
+ const route = answer.route;
2314
+ // a dynamic route without the handler: a route of its own at the path wins over the param
2315
+ if (route.handler === null)
2316
+ return dynamicRoute(route) ? { kind: "missing", listener, endpoint: at } : { kind: "no_handler", listener, route };
2317
+ if (route.handler === "other")
2318
+ return { kind: "not_get", listener, route };
2319
+ if (route.params && dynamicRoute(route))
2320
+ return { kind: "params", listener, route, endpoint: at };
2321
+ if (route.segment)
2322
+ return { kind: "segment", listener, route, setting: route.segment };
2323
+ // middleware runs before the route: whatever it does there, the reading can't see
2324
+ for (const m of facts.middleware ?? []) {
2325
+ const on = middlewareOn(m, base === "" ? at : at.slice(base.length) || "/", facts.i18n === true);
2326
+ if (on)
2327
+ return { kind: "middleware", listener, route, path: at, file: m.file, ...on };
2328
+ }
2329
+ served ??= { listener, route, path: at };
2330
+ }
2331
+ return { kind: "served", ...served };
2332
+ }
2333
+ /** What `mapled scan` writes into the manifest: a listener nothing answers
2334
+ isn't listening; the route is the path the tab asks, not a dynamic route's pattern. */
2335
+ export function capabilitiesOf(state) {
2336
+ return state.kind === "served" ? { realtime: true, releaseRoute: state.path } : { realtime: false };
2337
+ }
2338
+ /** Where a route answering `endpoint` goes: in the App Router folder Next
2339
+ reads — without one, under `src/app` when the site keeps its code there
2340
+ — and without the basePath, which Next adds itself. */
2341
+ export function routeFileFor(endpoint, facts) {
2342
+ const underSrc = [...facts.routes.map((r) => r.file), ...(facts.pages ?? []).map((p) => p.file), ...facts.listeners.map((l) => l.file)].some((f) => f.startsWith("src/"));
2343
+ const base = facts.basePath ?? "";
2344
+ const local = base !== "" && (endpoint === base || endpoint.startsWith(`${base}/`)) ? endpoint.slice(base.length) || "/" : endpoint;
2345
+ return `${facts.appDir ?? (underSrc ? "src/app" : "app")}${local === "/" ? "" : local}/route.${facts.routeExtension ?? "ts"}`;
2346
+ }