@mapled/cli 0.11.0 → 0.12.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,1509 @@
1
+ import { 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 extensions a route file may have when next.config doesn't set `pageExtensions`. */
18
+ export const PAGE_EXTENSIONS = ["tsx", "ts", "jsx", "js"];
19
+ // the config files Next reads, in its order
20
+ const NEXT_CONFIGS = ["next.config.js", "next.config.mjs", "next.config.ts", "next.config.mts", "next.config.cjs"];
21
+ // the three names as written, or an escape that may spell one: `import { w\u0061tchRelease as w }`
22
+ const WORDS = /MapledLive|watchRelease|createReleaseHandler|\\u/;
23
+ /** A member `object.name` as one name among the file's: no identifier
24
+ reads the same. A namespace's `ns.createReleaseHandler` is one — a
25
+ namespace can't be written to, so its member is the import wherever
26
+ `ns` itself is. */
27
+ const memberName = (object, name) => `${object}.${name}`;
28
+ /** The imports of the three exports, and of their modules as namespaces; type-only imports don't count. */
29
+ function importsOf(tokens) {
30
+ const found = { live: new Set(), watch: new Set(), handler: new Set(), ns: { live: new Set(), watch: new Set(), handler: new Set() } };
31
+ for (let i = 0; i < tokens.length; i++) {
32
+ const t = tokens[i];
33
+ if (t.kind !== "ident" || t.value !== "import")
34
+ continue;
35
+ if (tokens[i + 1]?.kind === "ident" && tokens[i + 1].value === "type")
36
+ continue;
37
+ const names = [];
38
+ let namespace = null;
39
+ let j = i + 1;
40
+ let source = null;
41
+ let inBraces = false;
42
+ for (; j < tokens.length && j < i + 200; j++) {
43
+ const u = tokens[j];
44
+ if (u.kind === "punct" && u.value === "{")
45
+ inBraces = true;
46
+ else if (u.kind === "punct" && u.value === "}")
47
+ inBraces = false;
48
+ else if (u.kind === "ident" && u.value === "from" && !inBraces) {
49
+ source = tokens[j + 1]?.kind === "string" ? tokens[j + 1].value : null;
50
+ break;
51
+ }
52
+ else if (u.kind === "punct" && u.value === ";")
53
+ break;
54
+ else if (!inBraces && u.kind === "punct" && u.value === "*" && tokens[j + 1]?.kind === "ident" && tokens[j + 1].value === "as" && tokens[j + 2]?.kind === "ident") {
55
+ // `import * as ns`, `import def, * as ns`
56
+ namespace = tokens[j + 2].value;
57
+ j += 2;
58
+ }
59
+ else if (inBraces && u.kind === "ident") {
60
+ if (u.value === "type")
61
+ continue;
62
+ const prev = tokens[j - 1];
63
+ if (prev?.kind === "ident" && prev.value === "as")
64
+ continue;
65
+ const local = tokens[j + 1]?.kind === "ident" && tokens[j + 1].value === "as" && tokens[j + 2]?.kind === "ident" ? tokens[j + 2].value : u.value;
66
+ names.push({ imported: u.value, local });
67
+ }
68
+ }
69
+ i = j;
70
+ for (const n of names) {
71
+ if (source === "@mapled/next/live" && n.imported === "MapledLive")
72
+ found.live.add(n.local);
73
+ if (source === "@mapled/next" && n.imported === "watchRelease")
74
+ found.watch.add(n.local);
75
+ if (source === "@mapled/next/server" && n.imported === "createReleaseHandler")
76
+ found.handler.add(n.local);
77
+ }
78
+ if (namespace !== null) {
79
+ if (source === "@mapled/next/live")
80
+ found.ns.live.add(namespace);
81
+ if (source === "@mapled/next")
82
+ found.ns.watch.add(namespace);
83
+ if (source === "@mapled/next/server")
84
+ found.ns.handler.add(namespace);
85
+ }
86
+ }
87
+ return found;
88
+ }
89
+ const isPunct = (t, value) => t?.kind === "punct" && t.value === value;
90
+ const isWord = (t, value) => t?.kind === "ident" && t.value === value;
91
+ const isSpread = (tokens, k) => isPunct(tokens[k], ".") && isPunct(tokens[k + 1], ".") && isPunct(tokens[k + 2], ".");
92
+ /** A string the code writes out: closed, and not a template with a substitution (its value reads `${}`). */
93
+ const literal = (t) => t?.kind === "string" && !t.unclosed && t.value !== "${}";
94
+ // how far an element's attributes or a call's options are read; past it the endpoint can't be told
95
+ const OPTIONS_TOKENS = 2000;
96
+ /** The endpoint of a JSX element's attributes, from just after its name.
97
+ The last of `endpoint=` and spread props wins, as in JSX; null when the
98
+ code decides it — an expression, or spread props that may carry one. */
99
+ function jsxEndpoint(tokens, from) {
100
+ let endpoint = RELEASE_ROUTE;
101
+ let depth = 0;
102
+ for (let k = from; k < tokens.length; k++) {
103
+ if (k >= from + OPTIONS_TOKENS)
104
+ return null;
105
+ const t = tokens[k];
106
+ if (t.kind === "punct" && t.value === "{") {
107
+ if (depth === 0 && isSpread(tokens, k + 1))
108
+ endpoint = null;
109
+ depth++;
110
+ }
111
+ else if (t.kind === "punct" && t.value === "}") {
112
+ // the code around the element closes before it does
113
+ if (depth === 0)
114
+ return endpoint;
115
+ depth--;
116
+ }
117
+ else if (depth === 0 && t.kind === "punct" && (t.value === ">" || t.value === "<" || t.value === ";"))
118
+ return endpoint;
119
+ // the whole attribute name: `data-endpoint`, `x:endpoint` and `endpoint-x` are other attributes
120
+ else if (depth === 0 && isWord(t, "endpoint") && !isPunct(tokens[k - 1], "-") && !isPunct(tokens[k - 1], ":") && !isPunct(tokens[k + 1], "-") && !isPunct(tokens[k + 1], ":")) {
121
+ // `<MapledLive endpoint />` passes true, not the default; a JSX name takes no escapes, so an escaped one can't be read
122
+ if (!isPunct(tokens[k + 1], "=") || t.escaped === true) {
123
+ endpoint = null;
124
+ continue;
125
+ }
126
+ const v = tokens[k + 2];
127
+ // a string a line break cut off runs on in JSX: past it the element can't be read
128
+ if (v?.kind === "string" && v.unclosed)
129
+ return null;
130
+ // a JSX attribute's string reads no backslash escapes and decodes HTML entities: with either, it isn't the text the tokenizer read
131
+ if (v?.kind === "string")
132
+ endpoint = v.escaped || v.value.includes("&") ? null : v.value;
133
+ else if (isPunct(v, "{") && literal(tokens[k + 3]) && isPunct(tokens[k + 4], "}"))
134
+ endpoint = tokens[k + 3].value;
135
+ else
136
+ endpoint = null;
137
+ }
138
+ }
139
+ // the file ends after the attributes
140
+ return endpoint;
141
+ }
142
+ // words that may stand before a property's key: `get endpoint() {…}`, `async *endpoint() {…}`
143
+ const KEY_PREFIX = new Set(["get", "set", "async", "static"]);
144
+ /** The key of the object property that starts at `k` and the token after
145
+ it; `name` is null when the code decides the key — `[expr]` — and
146
+ undefined when no property starts there (`{}`, a trailing comma). */
147
+ function propertyKey(tokens, k) {
148
+ // a prefix word is the key itself when what follows ends it: `{ get: 1 }`, `{ async }`, `{ set() {} }`
149
+ const ends = (u) => isPunct(u, ":") || isPunct(u, "(") || isPunct(u, ",") || isPunct(u, "}") || isPunct(u, "=");
150
+ while ((isPunct(tokens[k], "*") || (tokens[k]?.kind === "ident" && KEY_PREFIX.has(tokens[k].value) && !ends(tokens[k + 1]))) && k < tokens.length)
151
+ k++;
152
+ const t = tokens[k];
153
+ if (isPunct(t, "[")) {
154
+ // a literal computed key reads as written; any other — a template with a substitution too — is the code's
155
+ if (literal(tokens[k + 1]) && isPunct(tokens[k + 2], "]"))
156
+ return { name: tokens[k + 1].value, next: k + 3 };
157
+ return { name: null, next: k + 1 };
158
+ }
159
+ if (t?.kind === "string" && !literal(t))
160
+ return { name: null, next: k + 1 };
161
+ if (t?.kind === "ident" || t?.kind === "string")
162
+ return { name: t.value, next: k + 1 };
163
+ // a numeric key: `{ 1: … }`
164
+ if (t?.kind === "punct" && /^\d$/.test(t.value))
165
+ return { name: t.value, next: k + 1 };
166
+ return { name: undefined, next: k };
167
+ }
168
+ /** The endpoint of a call's options object, from its opening parenthesis,
169
+ read property by property: the last `endpoint: "…"` or spread wins;
170
+ null when the code decides it — options from elsewhere, a spread, a
171
+ key the reading can't read (`[key]`), or an `endpoint` that isn't a
172
+ plain string: a shorthand, a method, a getter, an expression. */
173
+ function callEndpoint(tokens, paren) {
174
+ if (!isPunct(tokens[paren + 1], "{"))
175
+ return null;
176
+ let endpoint = RELEASE_ROUTE;
177
+ let depth = 0;
178
+ let property = false;
179
+ for (let k = paren + 1; k < tokens.length && k < paren + OPTIONS_TOKENS; k++) {
180
+ const t = tokens[k];
181
+ if (depth === 1 && property) {
182
+ property = false;
183
+ if (isSpread(tokens, k))
184
+ endpoint = null;
185
+ else {
186
+ const { name, next } = propertyKey(tokens, k);
187
+ if (name === null)
188
+ endpoint = null;
189
+ else if (name === "endpoint") {
190
+ const v = tokens[next + 1];
191
+ const after = tokens[next + 2];
192
+ endpoint = isPunct(tokens[next], ":") && literal(v) && (isPunct(after, ",") || isPunct(after, "}")) ? v.value : null;
193
+ }
194
+ }
195
+ }
196
+ if (t.kind === "punct" && (t.value === "{" || t.value === "(" || t.value === "[")) {
197
+ depth++;
198
+ if (depth === 1)
199
+ property = true;
200
+ }
201
+ else if (t.kind === "punct" && (t.value === "}" || t.value === ")" || t.value === "]")) {
202
+ depth--;
203
+ if (depth === 0)
204
+ return endpoint;
205
+ }
206
+ else if (depth === 1 && isPunct(t, ","))
207
+ property = true;
208
+ }
209
+ return null;
210
+ }
211
+ const DECLARE = new Set(["const", "let", "var"]);
212
+ // words that start a statement and never continue an expression
213
+ const STATEMENT = new Set(["export", "import", "const", "let", "var", "return", "if", "for", "while", "do", "throw", "try", "switch"]);
214
+ const BEFORE_FUNCTION_VALUE = new Set(["async", "new", "await", "return", "typeof", "void", "yield", "default"]);
215
+ function bracketsOf(tokens) {
216
+ const after = new Int32Array(tokens.length + 1).fill(tokens.length);
217
+ const open = new Int32Array(tokens.length).fill(-1);
218
+ const parent = new Int32Array(tokens.length).fill(-1);
219
+ const stack = [];
220
+ for (let k = 0; k < tokens.length; k++) {
221
+ const t = tokens[k];
222
+ if (t.kind === "punct" && (t.value === "}" || t.value === ")" || t.value === "]")) {
223
+ const o = stack.pop();
224
+ if (o !== undefined) {
225
+ after[o] = k + 1;
226
+ open[k] = o;
227
+ }
228
+ }
229
+ parent[k] = stack.length > 0 ? stack[stack.length - 1] : -1;
230
+ if (t.kind === "punct" && (t.value === "{" || t.value === "(" || t.value === "["))
231
+ stack.push(k);
232
+ }
233
+ return { after, open, parent };
234
+ }
235
+ /** The token after the bracket that `open` opens, or the end. */
236
+ function closing(tokens, open) {
237
+ let depth = 0;
238
+ for (let k = open; k < tokens.length; k++) {
239
+ const t = tokens[k];
240
+ if (t.kind !== "punct")
241
+ continue;
242
+ if (t.value === "{" || t.value === "(" || t.value === "[")
243
+ depth++;
244
+ else if (t.value === "}" || t.value === ")" || t.value === "]") {
245
+ depth--;
246
+ if (depth === 0)
247
+ return k + 1;
248
+ }
249
+ }
250
+ return tokens.length;
251
+ }
252
+ /** Where an initializer that starts at `from` ends: a `;` outside
253
+ brackets — or a `,`, between declarators; a return's value runs on
254
+ past a comma — the next statement, or the end of the block around it.
255
+ Brackets are stepped over whole (`after`, from `bracketsOf`), so
256
+ nested initializers don't make a deep file quadratic. */
257
+ function initializerEnd(tokens, after, from, commas = true) {
258
+ for (let k = from; k < tokens.length; k++) {
259
+ const t = tokens[k];
260
+ if (t.kind === "punct" && (t.value === "{" || t.value === "(" || t.value === "["))
261
+ k = after[k] - 1;
262
+ else if (t.kind === "punct" && (t.value === "}" || t.value === ")" || t.value === "]"))
263
+ return k;
264
+ else if (t.kind === "punct" && (t.value === ";" || (commas && t.value === ",")))
265
+ return k;
266
+ else if (k > from && t.kind === "ident" && !isPunct(tokens[k - 1], ".")) {
267
+ if (STATEMENT.has(t.value))
268
+ return k;
269
+ // a type's commas (`as Record<string, X>`) aren't the declarator's
270
+ if (t.value === "as" || t.value === "satisfies") {
271
+ const end = typeEnd(tokens, after, k + 1, tokens.length);
272
+ if (end > k + 1)
273
+ k = end - 1;
274
+ continue;
275
+ }
276
+ // a declaration after a line without a semicolon, not `= async function`
277
+ const prev = tokens[k - 1];
278
+ const valueFollows = prev.kind === "punct" ? !")]}".includes(prev.value) : prev.kind === "ident" && BEFORE_FUNCTION_VALUE.has(prev.value);
279
+ if ((t.value === "function" || t.value === "class") && !valueFollows)
280
+ return k;
281
+ }
282
+ }
283
+ return tokens.length;
284
+ }
285
+ /** Past a function's parameters and return type to the brace of its body. */
286
+ function bodyAfter(tokens, from) {
287
+ let depth = 0;
288
+ for (let k = from; k < tokens.length && k < from + 400; k++) {
289
+ const u = tokens[k];
290
+ if (depth === 0 && isPunct(u, "{"))
291
+ return k;
292
+ // an overload signature has no body
293
+ if (depth === 0 && isPunct(u, ";"))
294
+ return null;
295
+ if (u.kind === "punct" && (u.value === "(" || u.value === "["))
296
+ depth++;
297
+ else if (u.kind === "punct" && (u.value === ")" || u.value === "]"))
298
+ depth--;
299
+ }
300
+ return null;
301
+ }
302
+ /** `const|let|var name = …` and `function name(…) { … }`, each with the
303
+ tokens of its value: the initializer, or the function's body; a name
304
+ declared without a value is here too, with none. */
305
+ function declarationsOf(tokens, after) {
306
+ const out = [];
307
+ const exportedAt = (k) => {
308
+ const prev = tokens[k - 1];
309
+ if (prev?.kind === "ident" && prev.value === "async")
310
+ return tokens[k - 2]?.kind === "ident" && tokens[k - 2].value === "export";
311
+ return prev?.kind === "ident" && prev.value === "export";
312
+ };
313
+ for (let i = 0; i < tokens.length; i++) {
314
+ const t = tokens[i];
315
+ if (t.kind !== "ident")
316
+ continue;
317
+ if (DECLARE.has(t.value) && !isPunct(tokens[i - 1], ".")) {
318
+ const exported = exportedAt(i);
319
+ let k = i + 1;
320
+ // one declarator after another: `const a = …, b = …`
321
+ while (tokens[k]?.kind === "ident") {
322
+ const name = tokens[k].value;
323
+ let eq = k + 1;
324
+ if (isPunct(tokens[eq], ":")) {
325
+ // a type annotation up to the `=`
326
+ let depth = 0;
327
+ for (; eq < tokens.length && eq < k + 400; eq++) {
328
+ const u = tokens[eq];
329
+ if (u.kind !== "punct")
330
+ continue;
331
+ if (u.value === "{" || u.value === "(" || u.value === "[")
332
+ depth++;
333
+ else if (u.value === "}" || u.value === ")" || u.value === "]")
334
+ depth--;
335
+ else if (depth === 0 && (u.value === "=" || u.value === ";"))
336
+ break;
337
+ }
338
+ }
339
+ if (!isPunct(tokens[eq], "=")) {
340
+ // `let h;`, `let a, b`, `for (const h of …)`: bound, to nothing the reading can see
341
+ out.push({ name, at: k, exported, from: eq, to: eq, fn: false, async: false });
342
+ if (!isPunct(tokens[eq], ","))
343
+ break;
344
+ k = eq + 1;
345
+ continue;
346
+ }
347
+ const to = initializerEnd(tokens, after, eq + 1);
348
+ out.push({ name, at: k, exported, from: eq + 1, to, fn: false, async: false });
349
+ if (!isPunct(tokens[to], ","))
350
+ break;
351
+ k = to + 1;
352
+ }
353
+ }
354
+ else if (t.value === "function" && tokens[i + 1]?.kind === "ident") {
355
+ const name = tokens[i + 1].value;
356
+ let open = i + 2;
357
+ while (open < tokens.length && open < i + 50 && !isPunct(tokens[open], "("))
358
+ open++;
359
+ if (!isPunct(tokens[open], "("))
360
+ continue;
361
+ const body = bodyAfter(tokens, after[open]);
362
+ if (body === null)
363
+ continue;
364
+ out.push({ name, at: i + 1, exported: exportedAt(i), from: body, to: after[body], fn: true, async: isWord(tokens[i - 1], "async") });
365
+ }
366
+ }
367
+ return out;
368
+ }
369
+ /** The local names an `export { a as GET, b }` clause exports, by exported
370
+ name; re-exports `from` another module don't count — their code is elsewhere. */
371
+ function exportClauses(tokens) {
372
+ const out = new Map();
373
+ for (let i = 0; i < tokens.length; i++) {
374
+ const t = tokens[i];
375
+ if (t.kind !== "ident" || t.value !== "export" || !isPunct(tokens[i + 1], "{"))
376
+ continue;
377
+ const end = closing(tokens, i + 1);
378
+ const from = tokens[end];
379
+ if (from?.kind === "ident" && from.value === "from")
380
+ continue;
381
+ for (let k = i + 2; k < end - 1; k++) {
382
+ const u = tokens[k];
383
+ if (u.kind !== "ident" || u.value === "type")
384
+ continue;
385
+ const renamed = tokens[k + 1]?.kind === "ident" && tokens[k + 1].value === "as" && tokens[k + 2]?.kind === "ident";
386
+ const exported = renamed ? tokens[k + 2].value : u.value;
387
+ const list = out.get(exported);
388
+ if (list)
389
+ list.push(u.value);
390
+ else
391
+ out.set(exported, [u.value]);
392
+ if (renamed)
393
+ k += 2;
394
+ }
395
+ i = end - 1;
396
+ }
397
+ return out;
398
+ }
399
+ /** What a name is to the release, as a level: calling a `FACTORY` gives
400
+ a `HANDLER` (`createReleaseHandler` itself, or `make` in `function
401
+ make() { return createReleaseHandler(…) }`), calling a handler gives the
402
+ `RESPONSE` — the release the tab reads. A call goes one level down; a
403
+ function goes one level up from what it returns. */
404
+ const RESPONSE = 0;
405
+ const HANDLER = 1;
406
+ const FACTORY = 2;
407
+ const NOT_A_VALUE = new Set(["new", "typeof", "void", "delete", "function", "class", "this", "super", "null", "true", "false", "undefined", "async", "await", "yield", "return"]);
408
+ const CONTROL = new Set(["if", "for", "while", "switch", "catch", "with"]);
409
+ /** The term of the expression in [from, to), or null. A member `o.name`
410
+ is a term under `memberName`: a namespace's `ns.createReleaseHandler`
411
+ is seeded as the factory (`releaseHandlerOf`); any other member is a
412
+ name nothing gives a level. */
413
+ function termOf(tokens, after, from, to, up, cap = FACTORY) {
414
+ let k = from;
415
+ while (k < to && isWord(tokens[k], "await"))
416
+ k++;
417
+ const t = tokens[k];
418
+ if (k >= to || t?.kind !== "ident" || NOT_A_VALUE.has(t.value))
419
+ return null;
420
+ let name = t.value;
421
+ let j = k + 1;
422
+ if (j + 1 < to && isPunct(tokens[j], ".") && tokens[j + 1]?.kind === "ident") {
423
+ name = memberName(name, tokens[j + 1].value);
424
+ j += 2;
425
+ }
426
+ let calls = 0;
427
+ while (j < to && isPunct(tokens[j], "(")) {
428
+ j = after[j];
429
+ calls++;
430
+ }
431
+ if (j > to || !typesOnly(tokens, after, j, to))
432
+ return null;
433
+ return { name, calls, up, cap };
434
+ }
435
+ /** Whether [j, to) is nothing but `as Type` / `satisfies Type`, once or
436
+ more: a type doesn't change the value, an operator past it (`h as T ||
437
+ other`, `h as T, other`) does. */
438
+ function typesOnly(tokens, after, j, to) {
439
+ while (j < to) {
440
+ if (!isWord(tokens[j], "as") && !isWord(tokens[j], "satisfies"))
441
+ return false;
442
+ const end = typeEnd(tokens, after, j + 1, to);
443
+ if (end <= j + 1)
444
+ return false;
445
+ j = end;
446
+ }
447
+ return true;
448
+ }
449
+ // words a type goes on after: `keyof typeof x`, `readonly string[]`
450
+ const TYPE_PREFIX = new Set(["keyof", "typeof", "readonly", "unique", "infer", "extends"]);
451
+ /** The token after the type that starts at `j` and ends by `to` — `<…>`
452
+ held together with its commas, unions, intersections, function types
453
+ — or -1 when its brackets don't close by then. Two names in a row
454
+ outside `<…>` are a new statement, or the next `as`. */
455
+ function typeEnd(tokens, after, j, to) {
456
+ let angles = 0;
457
+ let named = false;
458
+ for (let k = j; k < to; k++) {
459
+ const u = tokens[k];
460
+ if (u.kind !== "punct") {
461
+ if (angles === 0 && named && !TYPE_PREFIX.has(tokens[k - 1].value))
462
+ return k;
463
+ named = true;
464
+ continue;
465
+ }
466
+ if (u.value === "(" || u.value === "[" || u.value === "{") {
467
+ if (after[k] > to)
468
+ return -1;
469
+ k = after[k] - 1;
470
+ named = true;
471
+ }
472
+ else if (u.value === "<") {
473
+ angles++;
474
+ named = false;
475
+ }
476
+ else if (u.value === ">" && angles > 0) {
477
+ angles--;
478
+ named = true;
479
+ }
480
+ else if (/^\d$/.test(u.value))
481
+ named = true;
482
+ else if (u.value === "=>" || u.value === ".")
483
+ named = false;
484
+ // a union or an intersection; `||` and `&&` are the value's
485
+ else if ((u.value === "|" || u.value === "&") && !isPunct(tokens[k + 1], u.value))
486
+ named = false;
487
+ // type arguments, and conditional types inside them
488
+ else if (angles > 0 && (u.value === "," || u.value === "?" || u.value === ":"))
489
+ named = false;
490
+ else
491
+ return angles === 0 ? k : -1;
492
+ }
493
+ return angles === 0 ? to : -1;
494
+ }
495
+ /** The function a value in [from, to) is — `function (…) {…}`, `async (…)
496
+ => …`, `x => …` — as its block (the braces) or its expression; null
497
+ when the whole value isn't a function. */
498
+ function functionOf(tokens, after, from, to) {
499
+ if (from >= to)
500
+ return null;
501
+ let k = from;
502
+ const async = isWord(tokens[k], "async");
503
+ if (async)
504
+ k++;
505
+ let arrow;
506
+ if (isWord(tokens[k], "function")) {
507
+ // a generator gives back an iterator, never a response
508
+ if (isPunct(tokens[k + 1], "*"))
509
+ return null;
510
+ let open = k + 1;
511
+ while (open < to && !isPunct(tokens[open], "("))
512
+ open++;
513
+ const body = open < to ? bodyAfter(tokens, after[open]) : null;
514
+ return body !== null && after[body] === to ? { block: true, from: body, to, async } : null;
515
+ }
516
+ if (tokens[k]?.kind === "ident" && isPunct(tokens[k + 1], "=>"))
517
+ arrow = k + 1;
518
+ else if (isPunct(tokens[k], "(")) {
519
+ arrow = after[k];
520
+ if (isPunct(tokens[arrow], ":")) {
521
+ // past a return type to the arrow
522
+ let depth = 0;
523
+ for (; arrow < to; arrow++) {
524
+ const u = tokens[arrow];
525
+ if (depth === 0 && isPunct(u, "=>"))
526
+ break;
527
+ if (u.kind === "punct" && (u.value === "(" || u.value === "[" || u.value === "{"))
528
+ depth++;
529
+ else if (u.kind === "punct" && (u.value === ")" || u.value === "]" || u.value === "}"))
530
+ depth--;
531
+ }
532
+ }
533
+ if (!isPunct(tokens[arrow], "=>"))
534
+ return null;
535
+ }
536
+ else
537
+ return null;
538
+ const body = arrow + 1;
539
+ if (isPunct(tokens[body], "{"))
540
+ return after[body] === to ? { block: true, from: body, to, async } : null;
541
+ return body < to ? { block: false, from: body, to, async } : null;
542
+ }
543
+ // words a block of the statements around it follows: `else { … }`, `try { … }`
544
+ const BLOCK_AFTER = new Set(["else", "try", "finally", "do", "catch"]);
545
+ // words whose condition a block of the statements around it follows: `if (…) { … }`
546
+ const BLOCK_AFTER_CONDITION = new Set(["if", "for", "while", "switch", "catch", "with"]);
547
+ /** Whether the `{` at `k`, among statements, opens a block of them — `if
548
+ (…) {`, `else {`, `try {`, `case …: {`, a bare `{` — and not a
549
+ function, a method under any name, a class or anything else whose
550
+ returns are its own. The list is of what's known to be a block, not
551
+ of what's known not to be: whatever it doesn't name keeps its returns. */
552
+ function blockAt(tokens, b, k) {
553
+ const prev = tokens[k - 1];
554
+ if (prev === undefined || isPunct(prev, ";") || isPunct(prev, "{") || isPunct(prev, ":"))
555
+ return true;
556
+ const word = (at, words) => tokens[at]?.kind === "ident" && words.has(tokens[at].value) && !isPunct(tokens[at - 1], ".");
557
+ if (word(k - 1, BLOCK_AFTER))
558
+ return true;
559
+ if (!isPunct(prev, ")"))
560
+ return false;
561
+ const open = b.open[k - 1];
562
+ return open > 0 && (word(open - 1, BLOCK_AFTER_CONDITION) || (isWord(tokens[open - 1], "await") && isWord(tokens[open - 2], "for")));
563
+ }
564
+ /** The returns of each function body in `bodies`, by its `{`, and the
565
+ bodies that throw. A return or a throw is the body's own when only
566
+ blocks of its statements (`blockAt`) stand between them; one inside
567
+ anything else — a nested function, a method under any name, a class —
568
+ belongs to that. */
569
+ function returnsIn(tokens, b, bodies) {
570
+ // for each bracket: whose statements it holds (-1: none known), and whether it holds statements
571
+ const owner = new Int32Array(tokens.length).fill(-1);
572
+ const statements = new Uint8Array(tokens.length);
573
+ const out = new Map();
574
+ const throwing = new Set();
575
+ for (let k = 0; k < tokens.length; k++) {
576
+ const t = tokens[k];
577
+ const p = b.parent[k];
578
+ if (t.kind === "punct" && (t.value === "{" || t.value === "(" || t.value === "[")) {
579
+ if (bodies.has(k)) {
580
+ owner[k] = k;
581
+ statements[k] = 1;
582
+ }
583
+ else if (t.value === "{" && (p === -1 || statements[p] === 1) && blockAt(tokens, b, k)) {
584
+ owner[k] = p === -1 ? -1 : owner[p];
585
+ statements[k] = 1;
586
+ }
587
+ }
588
+ else if (p !== -1 && (isWord(t, "return") || isWord(t, "throw")) && !isPunct(tokens[k - 1], ".") && !isPunct(tokens[k + 1], ":")) {
589
+ const body = owner[p];
590
+ if (body === -1)
591
+ continue;
592
+ if (t.value === "throw") {
593
+ throwing.add(body);
594
+ continue;
595
+ }
596
+ const list = out.get(body);
597
+ if (list)
598
+ list.push(k);
599
+ else
600
+ out.set(body, [k]);
601
+ }
602
+ }
603
+ return { returns: out, throwing };
604
+ }
605
+ /** Where the value a `return` at `k` gives ends: past commas; a line
606
+ break right after `return` ends the statement, and it returns nothing. */
607
+ function returnEnd(tokens, after, k) {
608
+ return tokens[k + 1]?.nl ? k + 1 : initializerEnd(tokens, after, k + 1, false);
609
+ }
610
+ /** What a declaration gives, one value per way out: its value, the
611
+ expression its arrow returns, or each return of its function body —
612
+ a name is what all of them agree on, so a GET that answers the
613
+ handler's response on one branch and anything else on another isn't
614
+ a handler's answer. A body that throws, or whose last statement isn't
615
+ a return of its own (it may run off its end), gives one value the
616
+ reading can't see. Null: a value that isn't a term. */
617
+ function valuesOf(tokens, b, d, fn, own) {
618
+ const after = b.after;
619
+ if (!fn)
620
+ return [termOf(tokens, after, d.from, d.to, 0)];
621
+ // an async function gives a promise: of a response that's still the answer a route awaits; of a handler it's no handler
622
+ const cap = fn.async ? HANDLER : FACTORY;
623
+ if (!fn.block)
624
+ return [termOf(tokens, after, fn.from, fn.to, 1, cap)];
625
+ const returns = own.returns.get(fn.from) ?? [];
626
+ const last = returns[returns.length - 1];
627
+ if (last === undefined || own.throwing.has(fn.from) || b.parent[last] !== fn.from)
628
+ return [null];
629
+ // the last return is a statement of the body, not the branch of an `if … else return`
630
+ const prev = tokens[last - 1];
631
+ const opener = isPunct(prev, ")") ? b.open[last - 1] : -1;
632
+ const head = (at) => (tokens[at]?.kind === "ident" && !isPunct(tokens[at - 1], ".") ? tokens[at].value : "");
633
+ const condition = opener > 0 && (BLOCK_AFTER_CONDITION.has(head(opener - 1)) || (head(opener - 1) === "await" && head(opener - 2) === "for"));
634
+ const statement = last - 1 === fn.from || isPunct(prev, ";") || isPunct(prev, "}") || (tokens[last].nl === true && prev.kind !== "ident" && !condition);
635
+ if (!statement)
636
+ return [null];
637
+ // nothing but `;` between the last return's value and the body's closing brace
638
+ let end = returnEnd(tokens, after, last);
639
+ while (isPunct(tokens[end], ";"))
640
+ end++;
641
+ if (end !== fn.to - 1)
642
+ return [null];
643
+ return returns.map((k) => termOf(tokens, after, k + 1, returnEnd(tokens, after, k), 1, cap));
644
+ }
645
+ /** `name = …`, `name += …` and every other assignment, `name++`, `--name`. */
646
+ function changes(tokens, k) {
647
+ const punct = (at) => (tokens[at]?.kind === "punct" ? tokens[at].value : "");
648
+ const twice = (a, c) => punct(a) === c && punct(a + 1) === c;
649
+ return twice(k - 2, "+") || twice(k - 2, "-") || assigned(tokens, k);
650
+ }
651
+ /** Whether what ends at `k` — a name, a member `o[k]` — is assigned to or updated after it: `= …`, `??= …`, `++`. */
652
+ function assigned(tokens, k) {
653
+ const punct = (at) => (tokens[at]?.kind === "punct" ? tokens[at].value : "");
654
+ const twice = (a, c) => punct(a) === c && punct(a + 1) === c;
655
+ if (twice(k + 1, "+") || twice(k + 1, "-"))
656
+ return true;
657
+ let j = k + 1;
658
+ let operator = "";
659
+ while (operator.length < 3 && punct(j).length === 1 && "+-*/%&|^?<>".includes(punct(j)))
660
+ operator += punct(j++);
661
+ if (punct(j) !== "=" || punct(j + 1) === "=")
662
+ return false;
663
+ // `<=` and `>=` compare; `<<=`, `**=`, `??=`, `||=` … assign
664
+ return operator !== "<" && operator !== ">" && operator !== "?";
665
+ }
666
+ /** Names the code binds or changes other than by a declaration the
667
+ reading follows: parameters, destructuring — in a declaration or an
668
+ assignment (`({ h } = other)`, `[h] = list`) —, `catch (e)`, a loop's
669
+ head, a class, an enum or a namespace, an assignment or an update.
670
+ Where such a name is read it may not be what its declaration says, so
671
+ the reading gives it no level, whichever function it's in: a GET that
672
+ answers through it is one doctor can't confirm. */
673
+ function reboundNames(tokens, b, declared) {
674
+ // spans all of whose names are bound, as a running count: nested spans stay linear
675
+ const marks = new Int32Array(tokens.length + 1);
676
+ const span = (from, to) => {
677
+ if (from >= to)
678
+ return;
679
+ marks[from]++;
680
+ marks[to]--;
681
+ };
682
+ const out = new Set();
683
+ for (let k = 0; k < tokens.length; k++) {
684
+ const t = tokens[k];
685
+ const prev = tokens[k - 1];
686
+ if (isPunct(t, "(")) {
687
+ const next = tokens[b.after[k]];
688
+ const head = prev?.kind === "ident" && !isPunct(tokens[k - 2], ".") ? prev.value : null;
689
+ const condition = head !== null && CONTROL.has(head) && head !== "catch" && head !== "for";
690
+ // `(…) =>`, `name(…) {`, `(…): Type`, `function (…)`; `catch (e)` and a loop's head
691
+ const binds = isPunct(next, "=>") || (!condition && (isPunct(next, "{") || isPunct(next, ":"))) || head === "function" || head === "catch" || head === "for" || isWord(tokens[k - 2], "function");
692
+ if (binds)
693
+ span(k + 1, b.after[k] - 1);
694
+ }
695
+ else if ((isPunct(t, "{") || isPunct(t, "[")) && isPunct(tokens[b.after[k]], "=") && !isPunct(tokens[b.after[k] + 1], "=")) {
696
+ // a pattern assigned to: every name in it may be bound (a member `o[k] = …` is taken as one too)
697
+ span(k + 1, b.after[k] - 1);
698
+ }
699
+ else if (t.kind === "ident" && !isPunct(prev, ".")) {
700
+ if (isPunct(tokens[k + 1], "=>") || isWord(prev, "class") || isWord(prev, "enum") || isWord(prev, "namespace"))
701
+ out.add(t.value);
702
+ else if (DECLARE.has(t.value) && (isPunct(tokens[k + 1], "{") || isPunct(tokens[k + 1], "[")))
703
+ span(k + 2, b.after[k + 1] - 1);
704
+ else if (!declared.has(k) && !isWord(prev, "type") && changes(tokens, k))
705
+ out.add(t.value);
706
+ }
707
+ }
708
+ let depth = 0;
709
+ for (let k = 0; k < tokens.length; k++) {
710
+ depth += marks[k];
711
+ if (depth > 0 && tokens[k].kind === "ident")
712
+ out.add(tokens[k].value);
713
+ }
714
+ return out;
715
+ }
716
+ function readingOf(tokens) {
717
+ const b = bracketsOf(tokens);
718
+ const declarations = declarationsOf(tokens, b.after);
719
+ const rebound = reboundNames(tokens, b, new Set(declarations.map((d) => d.at)));
720
+ const bound = new Set([...rebound, ...declarations.map((d) => d.name)]);
721
+ return { b, declarations, rebound, bound };
722
+ }
723
+ /** Whether the route's exported GET answers with the release handler:
724
+ GET is a handler — `export const GET = createReleaseHandler(…)`,
725
+ `export { release as GET }`, `const GET = make()` — or a function that
726
+ always returns a handler's response: `export async function GET(req) {
727
+ return release(req); }`. A GET that only mentions the handler, calls it
728
+ and drops the result, returns it uncalled (`() => release`), or answers
729
+ something else on another branch or by a throw doesn't. A name declared
730
+ more than once, or returned more than once, is what all its values agree on,
731
+ a name bound or changed any other way is nothing the reading knows,
732
+ and neither is `createReleaseHandler` where the file binds its name
733
+ again — or a namespace's `ns.createReleaseHandler` where it binds `ns` again. */
734
+ function releaseHandlerOf(tokens, names, read) {
735
+ const handlers = names.handler;
736
+ // `createReleaseHandler(` under a name it's imported as, or `ns.createReleaseHandler(` under a namespace's — not `a.ns.createReleaseHandler(`, a member of something else
737
+ 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], ".");
738
+ const calls = (k) => (handlers.has(tokens[k].value) || member(k)) && isPunct(tokens[k + 1], "(");
739
+ if (!tokens.some((u, k) => u.kind === "ident" && calls(k)))
740
+ return null;
741
+ const { b, declarations, rebound, bound } = read();
742
+ const after = b.after;
743
+ 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)));
744
+ const own = returnsIn(tokens, b, new Set(fns.flatMap((fn) => (fn?.block ? [fn.from] : []))));
745
+ // every value a name is given — each declaration's, each return's — with the term it reads
746
+ const values = [];
747
+ const readers = new Map();
748
+ const declared = new Map();
749
+ declarations.forEach((d, i) => {
750
+ for (const term of valuesOf(tokens, b, d, fns[i], own)) {
751
+ const v = values.push(d.name) - 1;
752
+ declared.set(d.name, (declared.get(d.name) ?? 0) + 1);
753
+ if (term === null)
754
+ continue;
755
+ const list = readers.get(term.name);
756
+ if (list)
757
+ list.push([v, term]);
758
+ else
759
+ readers.set(term.name, [[v, term]]);
760
+ }
761
+ });
762
+ // levels as bits: of each name, of each value, and how many values of a name reach each level
763
+ const levels = new Map();
764
+ const ofValue = new Uint8Array(values.length);
765
+ const agree = new Map();
766
+ const queue = [];
767
+ const grow = (name, bits) => {
768
+ if (rebound.has(name))
769
+ return;
770
+ const had = levels.get(name) ?? 0;
771
+ if ((had | bits) === had)
772
+ return;
773
+ levels.set(name, had | bits);
774
+ queue.push([name, bits & ~had]);
775
+ };
776
+ for (const h of handlers)
777
+ if (!bound.has(h))
778
+ grow(h, 1 << FACTORY);
779
+ // a namespace's member is the factory wherever the namespace is the import; bound again, neither is anything the reading knows
780
+ for (const ns of names.ns.handler)
781
+ if (!bound.has(ns))
782
+ grow(memberName(ns, "createReleaseHandler"), 1 << FACTORY);
783
+ while (queue.length > 0) {
784
+ const [name, bits] = queue.pop();
785
+ for (const [i, term] of readers.get(name) ?? []) {
786
+ let reached = 0;
787
+ for (let level = RESPONSE; level <= FACTORY; level++) {
788
+ const to = level - term.calls + term.up;
789
+ if (bits & (1 << level) && to >= RESPONSE && to <= term.cap)
790
+ reached |= 1 << to;
791
+ }
792
+ const fresh = reached & ~ofValue[i];
793
+ if (fresh === 0)
794
+ continue;
795
+ ofValue[i] = ofValue[i] | fresh;
796
+ const d = values[i];
797
+ // a declaration under an import's name — the factory's or its namespace's — is nothing the reading knows either
798
+ if (handlers.has(d) || names.ns.handler.has(d))
799
+ continue;
800
+ const counts = agree.get(d) ?? [0, 0, 0];
801
+ agree.set(d, counts);
802
+ let all = 0;
803
+ for (let level = RESPONSE; level <= FACTORY; level++) {
804
+ if (!(fresh & (1 << level)))
805
+ continue;
806
+ counts[level] = counts[level] + 1;
807
+ if (counts[level] === declared.get(d))
808
+ all |= 1 << level;
809
+ }
810
+ if (all !== 0)
811
+ grow(d, all);
812
+ }
813
+ }
814
+ const serves = (name) => ((levels.get(name) ?? 0) & (1 << HANDLER)) !== 0;
815
+ const direct = serves("GET") && declarations.some((d) => d.name === "GET" && d.exported);
816
+ const clause = (exportClauses(tokens).get("GET") ?? []).some(serves);
817
+ return direct || clause ? "GET" : "other";
818
+ }
819
+ /** The listeners one file mounts and, for a route file, whether its GET
820
+ serves the release. An imported name the file binds again — a
821
+ parameter, a declaration, destructuring — may not be the import where
822
+ it's used, and the reading can't tell where: none of its uses count.
823
+ A namespace's name the same: its members count only where `ns` itself
824
+ is the import. */
825
+ export function liveInFile(file, text) {
826
+ const tokens = tokenize(text);
827
+ const names = importsOf(tokens);
828
+ let reading = null;
829
+ const read = () => (reading ??= readingOf(tokens));
830
+ /** Whether the name at `k` is a member of one of `namespaces` as imported:
831
+ `ns.name`, not `a.ns.name` — a member of something else — and `ns` not bound again. */
832
+ const ofNamespace = (k, namespaces) => {
833
+ const ns = tokens[k - 2];
834
+ return isPunct(tokens[k - 1], ".") && ns?.kind === "ident" && namespaces.has(ns.value) && !isPunct(tokens[k - 3], ".") && !read().bound.has(ns.value);
835
+ };
836
+ const listeners = [];
837
+ for (let k = 0; k < tokens.length; k++) {
838
+ const t = tokens[k];
839
+ if (t.kind !== "ident")
840
+ continue;
841
+ if (names.live.has(t.value) && isPunct(tokens[k - 1], "<")) {
842
+ if (read().bound.has(t.value))
843
+ continue;
844
+ listeners.push({ file, via: "<MapledLive />", endpoint: jsxEndpoint(tokens, k + 1) });
845
+ }
846
+ else if (t.value === "MapledLive" && isPunct(tokens[k - 3], "<") && ofNamespace(k, names.ns.live)) {
847
+ listeners.push({ file, via: "<MapledLive />", endpoint: jsxEndpoint(tokens, k + 1) });
848
+ }
849
+ else if (names.watch.has(t.value) && isPunct(tokens[k + 1], "(") && !isPunct(tokens[k - 1], ".")) {
850
+ const prev = tokens[k - 1];
851
+ if (prev?.kind === "ident" && prev.value === "function")
852
+ continue;
853
+ if (read().bound.has(t.value))
854
+ continue;
855
+ listeners.push({ file, via: "watchRelease", endpoint: callEndpoint(tokens, k + 1) });
856
+ }
857
+ else if (t.value === "watchRelease" && isPunct(tokens[k + 1], "(") && ofNamespace(k, names.ns.watch)) {
858
+ listeners.push({ file, via: "watchRelease", endpoint: callEndpoint(tokens, k + 1) });
859
+ }
860
+ }
861
+ // `dynamicParams` as the route exports it, or an export the reading can't list
862
+ const params = tokens.some((u, k) => namesParams(u) || isWord(u, "exports") || (isWord(u, "export") && isPunct(tokens[k + 1], "*"))) && setsParams(tokens, read().b.after);
863
+ return { listeners, handler: releaseHandlerOf(tokens, names, read), params };
864
+ }
865
+ const namesParams = (u) => (u?.kind === "ident" || u?.kind === "string") && u.value === "dynamicParams";
866
+ /** Whether the route may export `dynamicParams` as anything but the
867
+ default written out (`export const dynamicParams = true`). Next reads
868
+ only the module's export: a local variable, a property or a string that
869
+ says the name is nothing to it. An export the reading can't list —
870
+ `export *`, CommonJS — may be it. */
871
+ function setsParams(tokens, after) {
872
+ for (let i = 0; i < tokens.length; i++) {
873
+ const t = tokens[i];
874
+ // CommonJS: `module.exports = { dynamicParams: false }`, `exports.dynamicParams = false`
875
+ if (isWord(t, "exports"))
876
+ return true;
877
+ if (!isWord(t, "export") || isPunct(tokens[i - 1], "."))
878
+ continue;
879
+ let j = i + 1;
880
+ // `export * from "./segment"` may carry it, and so may TypeScript's `export =`; `export * as ns` names only ns
881
+ if (isPunct(tokens[j], "="))
882
+ return true;
883
+ if (isPunct(tokens[j], "*")) {
884
+ if (!isWord(tokens[j + 1], "as"))
885
+ return true;
886
+ continue;
887
+ }
888
+ // a clause, with or without `from`: `export { x as dynamicParams }`, `export { dynamicParams }`, `export { x as "dynamicParams" }`
889
+ if (isPunct(tokens[j], "{")) {
890
+ for (let k = j + 1; k < after[j] - 1; k++)
891
+ if (namesParams(tokens[k]) && !isWord(tokens[k + 1], "as"))
892
+ return true;
893
+ continue;
894
+ }
895
+ while (isWord(tokens[j], "declare") || isWord(tokens[j], "async") || isWord(tokens[j], "abstract"))
896
+ j++;
897
+ const declare = tokens[j];
898
+ if (declare?.kind !== "ident" || !DECLARE.has(declare.value)) {
899
+ // `export function dynamicParams`, `export class …`, `export import dynamicParams = …`
900
+ const name = isPunct(tokens[j + 1], "*") ? j + 2 : j + 1;
901
+ if (namesParams(tokens[name]))
902
+ return true;
903
+ continue;
904
+ }
905
+ // each declarator: `export const a = 1, dynamicParams = false`, `export const { dynamicParams } = config`
906
+ for (let k = j + 1; k < tokens.length;) {
907
+ let next;
908
+ if (isPunct(tokens[k], "{") || isPunct(tokens[k], "[")) {
909
+ for (let p = k + 1; p < after[k] - 1; p++)
910
+ if (namesParams(tokens[p]))
911
+ return true;
912
+ next = after[k];
913
+ }
914
+ else if (tokens[k].kind === "ident") {
915
+ // only a `const` keeps the default: a `let` or `var` may change it later
916
+ if (namesParams(tokens[k]) && (declare.value !== "const" || !paramsDefault(tokens, after, k)))
917
+ return true;
918
+ next = k + 1;
919
+ }
920
+ else
921
+ break;
922
+ if (isPunct(tokens[next], ":"))
923
+ next = typeEnd(tokens, after, next + 1, tokens.length);
924
+ if (next < 0)
925
+ break;
926
+ if (isPunct(tokens[next], "="))
927
+ next = initializerEnd(tokens, after, next + 1);
928
+ if (!isPunct(tokens[next], ","))
929
+ break;
930
+ k = next + 1;
931
+ }
932
+ }
933
+ return false;
934
+ }
935
+ /** Whether the mention of `dynamicParams` at `k` writes out the default:
936
+ `dynamicParams = true`, with or without a type (`: boolean`), the `true`
937
+ in parentheses or followed by `as` / `satisfies` / `!`. */
938
+ function paramsDefault(tokens, after, k) {
939
+ if (tokens[k].kind !== "ident")
940
+ return false;
941
+ const eq = isPunct(tokens[k + 1], ":") ? typeEnd(tokens, after, k + 2, tokens.length) : k + 1;
942
+ if (eq < 0 || !isPunct(tokens[eq], "=") || isPunct(tokens[eq + 1], "="))
943
+ return false;
944
+ let from = eq + 1;
945
+ let to = initializerEnd(tokens, after, from);
946
+ for (;;) {
947
+ // where the value ends: `x as T`, `x satisfies T` and `x!` are x, and a
948
+ // line without a semicolon ends before a name that starts the next one
949
+ let end = to;
950
+ for (let j = from; j < end; j++) {
951
+ const u = tokens[j];
952
+ const p = tokens[j - 1];
953
+ if (j > from && (isWord(u, "as") || isWord(u, "satisfies") || (u.nl === true && u.kind === "ident" && (p.kind !== "punct" || ")]}".includes(p.value)))))
954
+ end = j;
955
+ else if (u.kind === "punct" && "([{".includes(u.value))
956
+ j = after[j] - 1;
957
+ }
958
+ while (end > from + 1 && isPunct(tokens[end - 1], "!"))
959
+ end--;
960
+ if (!isPunct(tokens[from], "(") || after[from] !== end)
961
+ return end === from + 1 && isWord(tokens[from], "true");
962
+ from++;
963
+ to = end - 1;
964
+ }
965
+ }
966
+ /** The route files of the App Router with the path each answers. */
967
+ export function routeFileOf(file, extensions = PAGE_EXTENSIONS) {
968
+ const m = /^(?:src\/)?app\/(?:(.+)\/)?route\.(.+)$/.exec(file);
969
+ if (!m || !extensions.includes(m[2]))
970
+ return null;
971
+ return appRoute(m[1] ? m[1].split("/") : []);
972
+ }
973
+ // Pages Router files that are no page of their own
974
+ const PAGES_SPECIAL = new Set(["_app", "_document", "_error"]);
975
+ /** The page files of both routers with the path each answers: an App
976
+ Router `page.<ext>`, and any Pages Router file but `_app`, `_document`
977
+ and `_error`, with the longest page extension its name ends in taken
978
+ off, as Next does, and `index` naming its folder. */
979
+ export function pageFileOf(file, extensions = PAGE_EXTENSIONS) {
980
+ const app = /^(?:src\/)?app\/(?:(.+)\/)?page\.(.+)$/.exec(file);
981
+ if (app)
982
+ return extensions.includes(app[2]) ? appRoute(app[1] ? app[1].split("/") : []) : null;
983
+ const pages = /^(?:src\/)?pages\/(.+)$/.exec(file);
984
+ if (!pages)
985
+ return null;
986
+ const name = pages[1];
987
+ const ext = extensions.filter((e) => name.endsWith(`.${e}`)).sort((a, b) => b.length - a.length)[0];
988
+ if (ext === undefined || name.length === ext.length + 1)
989
+ return null;
990
+ const segments = name.slice(0, -(ext.length + 1)).split("/");
991
+ if (segments.length === 1 && PAGES_SPECIAL.has(segments[0]))
992
+ return null;
993
+ if (segments[segments.length - 1] === "index")
994
+ segments.pop();
995
+ return "/" + segments.join("/");
996
+ }
997
+ /** Whether the site has the folder at its root: Next reads `app` and
998
+ `pages` there, and under `src/` only when the root has none. */
999
+ async function atRoot(dir, name) {
1000
+ try {
1001
+ return (await stat(path.join(dir, name))).isDirectory();
1002
+ }
1003
+ catch {
1004
+ return false;
1005
+ }
1006
+ }
1007
+ // the settings of next.config that move where a file answers
1008
+ const SETTINGS = ["pageExtensions", "basePath"];
1009
+ // 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
1010
+ const COPIES = new Set(["assign", "defineProperty", "defineProperties", "Reflect", "setPrototypeOf", "__defineGetter__", "__defineSetter__", "__proto__"]);
1011
+ /** next.config's `pageExtensions` and `basePath`, read only where the file
1012
+ writes them out: a literal, once, directly in the object it exports —
1013
+ `module.exports = {…}`, `export default nextConfig`, or passed to a
1014
+ plugin: `withMDX(nextConfig)`. Null when it may set either anywhere
1015
+ else: a value or a name the code builds (`["page" + "Extensions"]`, an
1016
+ escaped identifier, `Object.assign`, `defineProperty`, a prototype
1017
+ set by `setPrototypeOf` or `__proto__`, a member in a loop's head), an object from
1018
+ another file or spread in, a member (`config.basePath = …`), a nested
1019
+ object, any other mention — then no route can be checked. */
1020
+ export function nextConfigOf(config) {
1021
+ if (config === null)
1022
+ return { pageExtensions: PAGE_EXTENSIONS, basePath: "" };
1023
+ const tokens = tokenize(config);
1024
+ const b = bracketsOf(tokens);
1025
+ const declarations = declarationsOf(tokens, b.after);
1026
+ const declaredAt = new Set(declarations.map((d) => d.at));
1027
+ for (let k = 0; k < tokens.length; k++) {
1028
+ const t = tokens[k];
1029
+ // an identifier with an escape in its name, or a call that sets or copies properties
1030
+ if (isPunct(t, "\\") || (t.kind === "ident" && (t.escaped === true || COPIES.has(t.value))) || (t.kind === "string" && t.value === "__proto__"))
1031
+ return null;
1032
+ // a setting's name inside a longer string: JSON the code parses
1033
+ if (t.kind === "string" && !SETTINGS.includes(t.value) && SETTINGS.some((s) => t.value.includes(s)))
1034
+ return null;
1035
+ if (!isPunct(t, "["))
1036
+ continue;
1037
+ // a computed key or member written to, unless a lone string or digits name it: `{ [name]: … }`, `config[name] = …`, not `rules[0] = …`
1038
+ const close = b.after[k] - 1;
1039
+ if (close === k + 2 && literal(tokens[k + 1]))
1040
+ continue;
1041
+ let digits = k + 1;
1042
+ while (digits < close && tokens[digits].kind === "punct" && /^\d$/.test(tokens[digits].value))
1043
+ digits++;
1044
+ if (digits === close && close > k + 1)
1045
+ continue;
1046
+ const prev = tokens[k - 1];
1047
+ const key = (isPunct(prev, "{") || isPunct(prev, ",") || isPunct(prev, "*") || (prev?.kind === "ident" && KEY_PREFIX.has(prev.value))) && (isPunct(tokens[close + 1], ":") || isPunct(tokens[close + 1], "("));
1048
+ // a loop's head writes each value to it: `for (config[name] of values)`
1049
+ const head = b.parent[k];
1050
+ const loop = head >= 0 && isPunct(tokens[head], "(") && (isWord(tokens[head - 1], "for") || (isWord(tokens[head - 1], "await") && isWord(tokens[head - 2], "for"))) && (isWord(tokens[close + 1], "of") || isWord(tokens[close + 1], "in"));
1051
+ const member = (prev?.kind === "ident" || isPunct(prev, ")") || isPunct(prev, "]")) && (assigned(tokens, close) || loop);
1052
+ if (key || member)
1053
+ return null;
1054
+ }
1055
+ // what the file exports: `module.exports = …`, `export default …`, `export { config as default }`
1056
+ const exported = [];
1057
+ const names = exportClauses(tokens).get("default") ?? [];
1058
+ for (let k = 0; k < tokens.length; k++) {
1059
+ if (isWord(tokens[k], "module") && !isPunct(tokens[k - 1], ".") && isPunct(tokens[k + 1], ".") && isWord(tokens[k + 2], "exports") && isPunct(tokens[k + 3], "=") && !isPunct(tokens[k + 4], "=")) {
1060
+ exported.push([k + 4, initializerEnd(tokens, b.after, k + 4, false)]);
1061
+ }
1062
+ else if (isWord(tokens[k], "export") && isWord(tokens[k + 1], "default"))
1063
+ exported.push([k + 2, initializerEnd(tokens, b.after, k + 2, false)]);
1064
+ }
1065
+ // `export { default } from "./base.mjs"`: the config is another module's
1066
+ const reexported = tokens.some((t, k) => isWord(t, "export") && isPunct(tokens[k + 1], "{") && isWord(tokens[b.after[k + 1]], "from") && tokens.slice(k + 2, b.after[k + 1] - 1).some((u) => isWord(u, "default")));
1067
+ // the objects the config is made of, and whether any part comes from elsewhere
1068
+ const objects = new Set();
1069
+ let opaque = reexported || exported.length + names.length > 1;
1070
+ const byName = new Map();
1071
+ for (const d of declarations) {
1072
+ const list = byName.get(d.name);
1073
+ if (list)
1074
+ list.push(d);
1075
+ else
1076
+ byName.set(d.name, [d]);
1077
+ }
1078
+ const reassigned = new Set(tokens.flatMap((u, k) => (u.kind === "ident" && !isPunct(tokens[k - 1], ".") && !declaredAt.has(k) && changes(tokens, k) ? [u.value] : [])));
1079
+ // and every name in a pattern assigned to — `({ config } = presets)`, `[a, config] = list` — as a running count: nested patterns stay linear
1080
+ const patterns = new Int32Array(tokens.length + 1);
1081
+ for (let k = 0; k < tokens.length; k++) {
1082
+ const after = b.after[k];
1083
+ if ((isPunct(tokens[k], "{") || isPunct(tokens[k], "[")) && !DECLARE.has(tokens[k - 1]?.value ?? "") && isPunct(tokens[after], "=") && !isPunct(tokens[after + 1], "=")) {
1084
+ patterns[k]++;
1085
+ patterns[after]--;
1086
+ }
1087
+ }
1088
+ for (let k = 0, depth = 0; k < tokens.length; k++) {
1089
+ depth += patterns[k];
1090
+ if (depth > 0 && tokens[k].kind === "ident")
1091
+ reassigned.add(tokens[k].value);
1092
+ // a loop's head without a declaration: `for (config of variants)`
1093
+ const loop = isPunct(tokens[k - 1], "(") && (isWord(tokens[k - 2], "for") || (isWord(tokens[k - 2], "await") && isWord(tokens[k - 3], "for")));
1094
+ if (loop && tokens[k].kind === "ident" && (isWord(tokens[k + 1], "of") || isWord(tokens[k + 1], "in")))
1095
+ reassigned.add(tokens[k].value);
1096
+ }
1097
+ // names taken from a file of the site — `import x from "./x"`, `const x = require("./x")`: a plugin there may set anything
1098
+ const relative = (k) => (isWord(tokens[k], "require") || isWord(tokens[k], "import")) && isPunct(tokens[k + 1], "(") && tokens[k + 2]?.kind === "string" && tokens[k + 2].value.startsWith(".");
1099
+ const loadsBefore = new Int32Array(tokens.length + 1);
1100
+ for (let k = 0; k < tokens.length; k++)
1101
+ loadsBefore[k + 1] = loadsBefore[k] + (relative(k) ? 1 : 0);
1102
+ const local = new Set(declarations.filter((d) => loadsBefore[d.to] > loadsBefore[d.from]).map((d) => d.name));
1103
+ for (let k = 0; k < tokens.length; k++) {
1104
+ if (!isWord(tokens[k], "import") || isPunct(tokens[k + 1], "("))
1105
+ continue;
1106
+ const bound = [];
1107
+ let j = k + 1;
1108
+ for (; j < tokens.length && j < k + 200 && !isPunct(tokens[j], ";") && !isWord(tokens[j], "from"); j++)
1109
+ if (tokens[j].kind === "ident")
1110
+ bound.push(tokens[j].value);
1111
+ if (isWord(tokens[j], "from") && tokens[j + 1]?.kind === "string" && tokens[j + 1].value.startsWith("."))
1112
+ for (const name of bound)
1113
+ local.add(name);
1114
+ }
1115
+ // each name is read once: a name passed twice, or one that names itself, adds nothing
1116
+ const visited = new Set();
1117
+ const fromName = (name, depth) => {
1118
+ if (visited.has(name))
1119
+ return;
1120
+ visited.add(name);
1121
+ const ds = byName.get(name) ?? [];
1122
+ // declared once, with a value, never assigned again: that value; anything else — an import, a parameter — is from elsewhere
1123
+ if (ds.length !== 1 || reassigned.has(name) || ds[0].fn || ds[0].from === ds[0].to)
1124
+ opaque = true;
1125
+ else
1126
+ visit(ds[0].from, ds[0].to, depth + 1);
1127
+ };
1128
+ const visit = (from, to, depth) => {
1129
+ if (opaque)
1130
+ return;
1131
+ if (depth > 16 || from >= to) {
1132
+ opaque = true;
1133
+ return;
1134
+ }
1135
+ // where the value ends: `x as T` and `x satisfies T` are x, and a line
1136
+ // without a semicolon ends before a name that starts the next one
1137
+ let end = to;
1138
+ for (let k = from; k < to; k++) {
1139
+ const u = tokens[k];
1140
+ const p = tokens[k - 1];
1141
+ if (k > from && (isWord(u, "as") || isWord(u, "satisfies")))
1142
+ end = k;
1143
+ else if (k > from && u.nl === true && u.kind === "ident" && u.value !== "in" && u.value !== "instanceof" && (p.kind !== "punct" || ")]}".includes(p.value) || /^\d$/.test(p.value)))
1144
+ end = k;
1145
+ else if (u.kind === "punct" && "([{".includes(u.value))
1146
+ k = b.after[k] - 1;
1147
+ if (end !== to)
1148
+ break;
1149
+ }
1150
+ let k = from;
1151
+ while (k < end && isWord(tokens[k], "await"))
1152
+ k++;
1153
+ if (k >= end) {
1154
+ opaque = true;
1155
+ return;
1156
+ }
1157
+ const t = tokens[k];
1158
+ if ((isPunct(t, "{") || isPunct(t, "(")) && b.after[k] === end) {
1159
+ if (t.value === "{")
1160
+ objects.add(k);
1161
+ else
1162
+ visit(k + 1, end - 1, depth + 1);
1163
+ return;
1164
+ }
1165
+ // a function: its code is here, and whatever it sets, the mentions below see
1166
+ if (isWord(t, "function") || (isWord(t, "async") && isWord(tokens[k + 1], "function")) || functionOf(tokens, b.after, k, end))
1167
+ return;
1168
+ // a plugin's name or option: `require("@next/mdx")`, `{ enabled: true }`
1169
+ if (t?.kind === "string" || (t?.kind === "punct" && /^\d$/.test(t.value)) || isWord(t, "true") || isWord(t, "false") || isWord(t, "null") || isWord(t, "undefined"))
1170
+ return;
1171
+ if (t?.kind !== "ident") {
1172
+ opaque = true;
1173
+ return;
1174
+ }
1175
+ let j = k + 1;
1176
+ while (j < end && isPunct(tokens[j], ".") && tokens[j + 1]?.kind === "ident")
1177
+ j += 2;
1178
+ if (j === end) {
1179
+ // a name, or a member (`base.config`) — a value from elsewhere
1180
+ if (j === k + 1)
1181
+ fromName(t.value, depth);
1182
+ else
1183
+ opaque = true;
1184
+ return;
1185
+ }
1186
+ // a call, or calls: `withMDX(nextConfig)`, `createMDX()(config)` — a
1187
+ // plugin from a package keeps the config it's given; `require("…")`, a
1188
+ // plugin from the site's own files, or a call given no config — its
1189
+ // value comes from elsewhere
1190
+ if (!isPunct(tokens[j], "(") || (isWord(t, "require") && j === k + 1 && b.after[j] === end) || relative(k) || local.has(t.value)) {
1191
+ opaque = true;
1192
+ return;
1193
+ }
1194
+ const literal = (u) => u?.kind === "string" || (u?.kind === "punct" && /^\d$/.test(u.value)) || isWord(u, "true") || isWord(u, "false") || isWord(u, "null") || isWord(u, "undefined");
1195
+ let given = false;
1196
+ for (; j < end && isPunct(tokens[j], "("); j = b.after[j]) {
1197
+ let start = j + 1;
1198
+ const close = b.after[j] - 1;
1199
+ given = false;
1200
+ for (let a = start; a <= close; a++) {
1201
+ const u = tokens[a];
1202
+ if (a < close && u.kind === "punct" && "([{".includes(u.value))
1203
+ a = b.after[a] - 1;
1204
+ else if (a === close || isPunct(u, ",")) {
1205
+ if (a > start) {
1206
+ given ||= !literal(tokens[start]);
1207
+ visit(start, a, depth + 1);
1208
+ }
1209
+ start = a + 1;
1210
+ }
1211
+ }
1212
+ }
1213
+ if (j !== end || !given)
1214
+ opaque = true;
1215
+ };
1216
+ for (const [from, to] of exported)
1217
+ visit(from, to, 0);
1218
+ for (const name of names)
1219
+ fromName(name, 0);
1220
+ if (opaque)
1221
+ return null;
1222
+ // each setting: a literal, directly in one of those objects, and not mentioned anywhere else
1223
+ const settings = {};
1224
+ const accepted = new Set();
1225
+ for (const open of objects) {
1226
+ const last = b.after[open] - 1;
1227
+ let property = true;
1228
+ for (let k = open + 1; k < last; k++) {
1229
+ if (property) {
1230
+ property = false;
1231
+ if (isSpread(tokens, k))
1232
+ return null;
1233
+ const { name, next } = propertyKey(tokens, k);
1234
+ if (name === "pageExtensions" || name === "basePath") {
1235
+ if (settings[name] !== undefined || !isPunct(tokens[next], ":"))
1236
+ return null;
1237
+ const v = next + 1;
1238
+ const valueEnd = name === "basePath" ? v + 1 : isPunct(tokens[v], "[") ? b.after[v] : v;
1239
+ if (valueEnd === v || (!isPunct(tokens[valueEnd], ",") && valueEnd !== last))
1240
+ return null;
1241
+ const plain = (u) => u?.kind === "string" && !u.unclosed && u.value !== "${}";
1242
+ if (name === "basePath") {
1243
+ const value = tokens[v];
1244
+ // a path from the root, without a trailing slash — anything else Next refuses
1245
+ if (!plain(value) || (value.value !== "" && !/^\/[^?#\s]*[^/?#\s]$/.test(value.value)))
1246
+ return null;
1247
+ settings[name] = value.value;
1248
+ }
1249
+ else {
1250
+ const list = [];
1251
+ for (let j = v + 1; j < valueEnd - 1; j++) {
1252
+ if (plain(tokens[j]) && (isPunct(tokens[j + 1], ",") || j + 1 === valueEnd - 1))
1253
+ list.push(tokens[j].value);
1254
+ else if (!isPunct(tokens[j], ","))
1255
+ return null;
1256
+ }
1257
+ settings[name] = list;
1258
+ }
1259
+ accepted.add(isPunct(tokens[next - 1], "]") ? next - 2 : next - 1);
1260
+ }
1261
+ }
1262
+ const t = tokens[k];
1263
+ if (t.kind === "punct" && "([{".includes(t.value))
1264
+ k = b.after[k] - 1;
1265
+ else if (isPunct(t, ","))
1266
+ property = true;
1267
+ }
1268
+ }
1269
+ if (tokens.some((t, k) => (t.kind === "ident" || t.kind === "string") && SETTINGS.includes(t.value) && !accepted.has(k)))
1270
+ return null;
1271
+ return { pageExtensions: settings.pageExtensions ?? PAGE_EXTENSIONS, basePath: settings.basePath ?? "" };
1272
+ }
1273
+ export async function findLive(dir, maxFiles = 4000) {
1274
+ const { files } = await listFiles(dir, maxFiles);
1275
+ const config = NEXT_CONFIGS.find((name) => files.includes(name));
1276
+ let configText = null;
1277
+ if (config) {
1278
+ try {
1279
+ configText = await readFile(path.join(dir, config), "utf8");
1280
+ }
1281
+ catch {
1282
+ // unreadable: the defaults
1283
+ }
1284
+ }
1285
+ const settings = nextConfigOf(configText);
1286
+ const extensions = settings?.pageExtensions ?? null;
1287
+ const routeExtension = extensions === null || extensions.includes("ts") ? "ts" : (extensions[0] ?? "ts");
1288
+ // a folder under src/ that Next doesn't read: nothing in it runs
1289
+ const rootApp = await atRoot(dir, "app");
1290
+ const ignored = [...(rootApp ? ["src/app/"] : []), ...((await atRoot(dir, "pages")) ? ["src/pages/"] : [])];
1291
+ const appDir = rootApp ? "app" : (await atRoot(dir, "src/app")) ? "src/app" : undefined;
1292
+ const listeners = [];
1293
+ const routes = [];
1294
+ const pages = [];
1295
+ for (const file of files) {
1296
+ if (ignored.some((prefix) => file.startsWith(prefix)))
1297
+ continue;
1298
+ const route = extensions === null ? null : routeFileOf(file, extensions);
1299
+ const page = extensions === null ? null : pageFileOf(file, extensions);
1300
+ if (page !== null)
1301
+ pages.push({ file, path: page });
1302
+ let text;
1303
+ try {
1304
+ text = await readFile(path.join(dir, file), "utf8");
1305
+ }
1306
+ catch {
1307
+ continue;
1308
+ }
1309
+ if (text.length > 2_000_000)
1310
+ continue;
1311
+ const words = WORDS.test(text);
1312
+ const found = words ? liveInFile(file, text) : { listeners: [], handler: null, params: false };
1313
+ listeners.push(...found.listeners);
1314
+ if (route !== null)
1315
+ routes.push({ file, path: route, handler: found.handler, ...(found.params ? { params: true } : {}) });
1316
+ }
1317
+ const facts = { listeners, routes, pages, routeExtension };
1318
+ if (appDir)
1319
+ facts.appDir = appDir;
1320
+ if (settings?.basePath)
1321
+ facts.basePath = settings.basePath;
1322
+ if (settings === null && config)
1323
+ facts.config = config;
1324
+ return facts;
1325
+ }
1326
+ /** Whether a literal endpoint names its own origin — `https://…`, `//host/…`
1327
+ — which the code doesn't show to be the site's: `watchRelease` fetches it
1328
+ as written, and no route file of this site is known to answer it. */
1329
+ export const fullAddress = (endpoint) => /^[a-z][a-z\d+.-]*:/i.test(endpoint) || /^[/\\][/\\]/.test(endpoint);
1330
+ const SITE = "http://site.invalid";
1331
+ /** The path on the site a literal endpoint asks — the only thing a route
1332
+ file can answer: a path from the site's root. Null for anything else:
1333
+ a relative endpoint resolves against each page's address, and a full
1334
+ address may be another origin's — no one route file is their answer. */
1335
+ const sitePath = (endpoint) => {
1336
+ if (!endpoint.startsWith("/") || fullAddress(endpoint))
1337
+ return null;
1338
+ let pathname;
1339
+ try {
1340
+ const url = new URL(endpoint, SITE);
1341
+ // the parser drops tabs and line breaks and reads `\` as `/`: whatever reaches another host isn't a path
1342
+ if (url.origin !== SITE)
1343
+ return null;
1344
+ pathname = url.pathname;
1345
+ }
1346
+ catch {
1347
+ return null;
1348
+ }
1349
+ // the parser percent-encodes; route folders are named as written
1350
+ try {
1351
+ pathname = decodeURI(pathname);
1352
+ }
1353
+ catch {
1354
+ // a stray % stays as it is
1355
+ }
1356
+ const bare = pathname.replace(/\/+$/, "");
1357
+ return bare === "" ? "/" : bare;
1358
+ };
1359
+ /** How a route folder's segment matches a path's, most specific first —
1360
+ as the App Router picks among routes that match: a static segment, a
1361
+ dynamic `[x]` (one segment), a catch-all `[...x]` (one or more), an
1362
+ optional catch-all `[[...x]]` (none or more). */
1363
+ const segmentRank = (segment) => (/^\[\[\.\.\.[^\]]+\]\]$/.test(segment) ? 3 : /^\[\.\.\.[^\]]+\]$/.test(segment) ? 2 : /^\[[^\]]+\]$/.test(segment) ? 1 : 0);
1364
+ const fileOf = (a) => (a.route ?? a.page).file;
1365
+ // the same ranks: a route before a page, then by file
1366
+ 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));
1367
+ /** Looks paths up among the route files and pages: each file's segments
1368
+ are read once, and a lookup walks only the folders its path can match —
1369
+ each asked path once. */
1370
+ function answerer(routes, pages) {
1371
+ const split = (p) => (p === "/" ? [] : p.slice(1).split("/"));
1372
+ const root = { fixed: new Map() };
1373
+ const add = (answer, answers) => {
1374
+ const segments = split(answers);
1375
+ const ranks = segments.map(segmentRank);
1376
+ const a = { ...answer, ranks };
1377
+ let n = root;
1378
+ for (let i = 0; i < segments.length; i++) {
1379
+ const rank = ranks[i];
1380
+ if (rank >= 2) {
1381
+ // a catch-all is the last segment, or the folder matches nothing
1382
+ if (i !== segments.length - 1)
1383
+ return;
1384
+ if (rank === 2 && before(a, n.rest))
1385
+ n.rest = a;
1386
+ if (rank === 3 && before(a, n.optional))
1387
+ n.optional = a;
1388
+ return;
1389
+ }
1390
+ if (rank === 1)
1391
+ n = n.param ??= { fixed: new Map() };
1392
+ else {
1393
+ const next = n.fixed.get(segments[i]) ?? { fixed: new Map() };
1394
+ n.fixed.set(segments[i], next);
1395
+ n = next;
1396
+ }
1397
+ }
1398
+ if (before(a, n.end))
1399
+ n.end = a;
1400
+ };
1401
+ for (const route of routes)
1402
+ add({ route }, route.path);
1403
+ for (const page of pages)
1404
+ add({ page }, page.path);
1405
+ const order = (a, b) => {
1406
+ for (let i = 0; i < Math.max(a.length, b.length); i++)
1407
+ if ((a[i] ?? -1) !== (b[i] ?? -1))
1408
+ return (a[i] ?? -1) - (b[i] ?? -1);
1409
+ return 0;
1410
+ };
1411
+ const cache = new Map();
1412
+ return (at) => {
1413
+ if (cache.has(at))
1414
+ return cache.get(at);
1415
+ const asked = split(at);
1416
+ const found = [];
1417
+ // a tree: each node is reached one way, so a lookup visits it once at most
1418
+ const walk = (n, i) => {
1419
+ // a catch-all takes the rest: one segment or more, or — optional — none too
1420
+ if (n.rest && asked.length > i)
1421
+ found.push(n.rest);
1422
+ if (n.optional)
1423
+ found.push(n.optional);
1424
+ if (i === asked.length) {
1425
+ if (n.end)
1426
+ found.push(n.end);
1427
+ return;
1428
+ }
1429
+ const fixed = n.fixed.get(asked[i]);
1430
+ if (fixed)
1431
+ walk(fixed, i + 1);
1432
+ if (n.param)
1433
+ walk(n.param, i + 1);
1434
+ };
1435
+ walk(root, 0);
1436
+ let best;
1437
+ for (const c of found) {
1438
+ const o = best ? order(c.ranks, best.ranks) : -1;
1439
+ if (o < 0 || (o === 0 && before(c, best)))
1440
+ best = c;
1441
+ }
1442
+ cache.set(at, best);
1443
+ return best;
1444
+ };
1445
+ }
1446
+ /** Whether a route's folders take a param: `[x]`, `[...x]`, `[[...x]]`. */
1447
+ const dynamicRoute = (route) => route.path.split("/").some((s) => segmentRank(s) > 0);
1448
+ /** Whether every listener has a route that answers it. The first one
1449
+ that doesn't is the one to name. */
1450
+ export function liveState(facts) {
1451
+ // the conventional path first, then by file: the same answer every run
1452
+ const rank = (r) => (r.path === RELEASE_ROUTE ? 0 : 1);
1453
+ 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));
1454
+ if (facts.listeners.length === 0)
1455
+ return { kind: "unused", handlerRoute: handlerRoutes[0] ?? null };
1456
+ const answerOf = answerer(facts.routes, facts.pages ?? []);
1457
+ const base = facts.basePath ?? "";
1458
+ let served = null;
1459
+ for (const listener of facts.listeners) {
1460
+ if (listener.endpoint === null) {
1461
+ // the code decides where it asks: with no release route at all it's
1462
+ // surely missing; with one — or routes the reading can't know — nothing says it's the route asked
1463
+ if (handlerRoutes.length === 0 && !facts.config)
1464
+ return { kind: "missing", listener, endpoint: null };
1465
+ return { kind: "unresolved", listener };
1466
+ }
1467
+ const at = sitePath(listener.endpoint);
1468
+ if (at === null)
1469
+ return { kind: "unresolved", listener };
1470
+ if (facts.config)
1471
+ return { kind: "config", listener, file: facts.config };
1472
+ // with a basePath, Next answers only under it: a route file's path follows it
1473
+ if (base !== "" && at !== base && !at.startsWith(`${base}/`))
1474
+ return { kind: "outside", listener, endpoint: at, basePath: base };
1475
+ const answer = answerOf(base === "" ? at : at.slice(base.length) || "/");
1476
+ if (!answer)
1477
+ return { kind: "missing", listener, endpoint: at };
1478
+ if (answer.page) {
1479
+ // 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
1480
+ if (answer.ranks.some((r) => r > 0))
1481
+ return { kind: "missing", listener, endpoint: at };
1482
+ return { kind: "taken", listener, page: answer.page, endpoint: at };
1483
+ }
1484
+ const route = answer.route;
1485
+ // a dynamic route without the handler: a route of its own at the path wins over the param
1486
+ if (route.handler === null)
1487
+ return dynamicRoute(route) ? { kind: "missing", listener, endpoint: at } : { kind: "no_handler", listener, route };
1488
+ if (route.handler === "other")
1489
+ return { kind: "not_get", listener, route };
1490
+ if (route.params && dynamicRoute(route))
1491
+ return { kind: "params", listener, route, endpoint: at };
1492
+ served ??= { listener, route, path: at };
1493
+ }
1494
+ return { kind: "served", ...served };
1495
+ }
1496
+ /** What `mapled scan` writes into the manifest: a listener nothing answers
1497
+ isn't listening; the route is the path the tab asks, not a dynamic route's pattern. */
1498
+ export function capabilitiesOf(state) {
1499
+ return state.kind === "served" ? { realtime: true, releaseRoute: state.path } : { realtime: false };
1500
+ }
1501
+ /** Where a route answering `endpoint` goes: in the App Router folder Next
1502
+ reads — without one, under `src/app` when the site keeps its code there
1503
+ — and without the basePath, which Next adds itself. */
1504
+ export function routeFileFor(endpoint, facts) {
1505
+ 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/"));
1506
+ const base = facts.basePath ?? "";
1507
+ const local = base !== "" && (endpoint === base || endpoint.startsWith(`${base}/`)) ? endpoint.slice(base.length) || "/" : endpoint;
1508
+ return `${facts.appDir ?? (underSrc ? "src/app" : "app")}${local === "/" ? "" : local}/route.${facts.routeExtension ?? "ts"}`;
1509
+ }