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