@mapled/cli 0.10.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/README.md +54 -3
- package/dist/commands.d.ts +9 -0
- package/dist/commands.js +103 -2
- package/dist/doctor.d.ts +5 -0
- package/dist/doctor.js +99 -1
- package/dist/index.js +11 -2
- package/dist/live.d.ts +156 -0
- package/dist/live.js +1509 -0
- package/dist/manifest.d.ts +14 -0
- package/dist/manifest.js +54 -2
- package/dist/pin.js +7 -0
- package/dist/report.d.ts +127 -0
- package/dist/report.js +425 -0
- package/dist/scan.d.ts +22 -2
- package/dist/scan.js +126 -23
- package/dist/schema.d.ts +3 -0
- package/dist/schema.js +2 -0
- package/package.json +2 -2
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
|
+
}
|