@mapled/cli 0.12.0 → 0.14.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 +2 -2
- package/dist/doctor.js +99 -6
- package/dist/live.d.ts +206 -40
- package/dist/live.js +1370 -314
- package/dist/scan.d.ts +8 -3
- package/dist/scan.js +68 -12
- package/package.json +1 -1
package/dist/live.js
CHANGED
|
@@ -1,33 +1,39 @@
|
|
|
1
|
-
import { readFile, stat } from "node:fs/promises";
|
|
1
|
+
import { readdir, readFile, stat } from "node:fs/promises";
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import { appRoute, listFiles, tokenize } from "./scan.js";
|
|
4
4
|
/** Live updates (§18.8) as the site's code shows them. A Next.js site
|
|
5
5
|
follows Publish in open tabs with two pieces: `<MapledLive />` from
|
|
6
6
|
"@mapled/next/live" (or `watchRelease` from "@mapled/next") asks a
|
|
7
7
|
route of the site every few seconds, and that route answers with
|
|
8
|
-
`createReleaseHandler` from "@mapled/next/server".
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
8
|
+
`createReleaseHandler` from "@mapled/next/server". The Pages Router
|
|
9
|
+
has its own two: `<MapledLive />` from "@mapled/next/live/pages", and
|
|
10
|
+
an API route whose default export is `createPagesReleaseHandler`. A
|
|
11
|
+
listener without the route polls a 404 forever and nothing says so —
|
|
12
|
+
`mapled doctor` reads both and says which is missing, and `mapled
|
|
13
|
+
scan` records the result in the manifest (`capabilities.realtime`,
|
|
14
|
+
`releaseRoute`) so MAPLED.md can tell the next agent. The code is read
|
|
15
|
+
with the scanner's tokenizer: comments and strings never count as a
|
|
16
|
+
use, and neither does an imported name the file binds again. */
|
|
15
17
|
/** Where `<MapledLive />` and `watchRelease` ask when no endpoint is given. */
|
|
16
18
|
export const RELEASE_ROUTE = "/api/mapled/release";
|
|
19
|
+
/** The `@mapled/next` from which that default follows the site's basePath —
|
|
20
|
+
before it, a listener with no endpoint asks RELEASE_ROUTE as written. */
|
|
21
|
+
export const BASE_PATH_SINCE = "0.10.0";
|
|
17
22
|
/** The extensions a route file may have when next.config doesn't set `pageExtensions`. */
|
|
18
23
|
export const PAGE_EXTENSIONS = ["tsx", "ts", "jsx", "js"];
|
|
19
24
|
// the config files Next reads, in its order
|
|
20
25
|
const NEXT_CONFIGS = ["next.config.js", "next.config.mjs", "next.config.ts", "next.config.mts", "next.config.cjs"];
|
|
21
|
-
// the
|
|
22
|
-
const WORDS = /MapledLive|watchRelease|createReleaseHandler|\\u/;
|
|
26
|
+
// the names as written, or an escape that may spell one: `import { w\u0061tchRelease as w }`
|
|
27
|
+
const WORDS = /MapledLive|watchRelease|createReleaseHandler|createPagesReleaseHandler|\\u/;
|
|
23
28
|
/** A member `object.name` as one name among the file's: no identifier
|
|
24
29
|
reads the same. A namespace's `ns.createReleaseHandler` is one — a
|
|
25
30
|
namespace can't be written to, so its member is the import wherever
|
|
26
31
|
`ns` itself is. */
|
|
27
32
|
const memberName = (object, name) => `${object}.${name}`;
|
|
28
|
-
/** The imports of the
|
|
33
|
+
/** The imports of the exports, and of their modules as namespaces; type-only imports don't count. */
|
|
29
34
|
function importsOf(tokens) {
|
|
30
|
-
const
|
|
35
|
+
const names = () => ({ live: new Set(), livePages: new Set(), watch: new Set(), handler: new Set(), pagesHandler: new Set() });
|
|
36
|
+
const found = { ...names(), ns: names() };
|
|
31
37
|
for (let i = 0; i < tokens.length; i++) {
|
|
32
38
|
const t = tokens[i];
|
|
33
39
|
if (t.kind !== "ident" || t.value !== "import")
|
|
@@ -70,18 +76,26 @@ function importsOf(tokens) {
|
|
|
70
76
|
for (const n of names) {
|
|
71
77
|
if (source === "@mapled/next/live" && n.imported === "MapledLive")
|
|
72
78
|
found.live.add(n.local);
|
|
79
|
+
if (source === "@mapled/next/live/pages" && n.imported === "MapledLive")
|
|
80
|
+
found.livePages.add(n.local);
|
|
73
81
|
if (source === "@mapled/next" && n.imported === "watchRelease")
|
|
74
82
|
found.watch.add(n.local);
|
|
75
83
|
if (source === "@mapled/next/server" && n.imported === "createReleaseHandler")
|
|
76
84
|
found.handler.add(n.local);
|
|
85
|
+
if (source === "@mapled/next/server" && n.imported === "createPagesReleaseHandler")
|
|
86
|
+
found.pagesHandler.add(n.local);
|
|
77
87
|
}
|
|
78
88
|
if (namespace !== null) {
|
|
79
89
|
if (source === "@mapled/next/live")
|
|
80
90
|
found.ns.live.add(namespace);
|
|
91
|
+
if (source === "@mapled/next/live/pages")
|
|
92
|
+
found.ns.livePages.add(namespace);
|
|
81
93
|
if (source === "@mapled/next")
|
|
82
94
|
found.ns.watch.add(namespace);
|
|
83
|
-
if (source === "@mapled/next/server")
|
|
95
|
+
if (source === "@mapled/next/server") {
|
|
84
96
|
found.ns.handler.add(namespace);
|
|
97
|
+
found.ns.pagesHandler.add(namespace);
|
|
98
|
+
}
|
|
85
99
|
}
|
|
86
100
|
}
|
|
87
101
|
return found;
|
|
@@ -95,9 +109,10 @@ const literal = (t) => t?.kind === "string" && !t.unclosed && t.value !== "${}";
|
|
|
95
109
|
const OPTIONS_TOKENS = 2000;
|
|
96
110
|
/** The endpoint of a JSX element's attributes, from just after its name.
|
|
97
111
|
The last of `endpoint=` and spread props wins, as in JSX; null when the
|
|
98
|
-
code decides it — an expression, or spread props that may carry one
|
|
112
|
+
code decides it — an expression, or spread props that may carry one —
|
|
113
|
+
and undefined when the element gives none: the SDK's default. */
|
|
99
114
|
function jsxEndpoint(tokens, from) {
|
|
100
|
-
let endpoint =
|
|
115
|
+
let endpoint = undefined;
|
|
101
116
|
let depth = 0;
|
|
102
117
|
for (let k = from; k < tokens.length; k++) {
|
|
103
118
|
if (k >= from + OPTIONS_TOKENS)
|
|
@@ -169,11 +184,12 @@ function propertyKey(tokens, k) {
|
|
|
169
184
|
read property by property: the last `endpoint: "…"` or spread wins;
|
|
170
185
|
null when the code decides it — options from elsewhere, a spread, a
|
|
171
186
|
key the reading can't read (`[key]`), or an `endpoint` that isn't a
|
|
172
|
-
plain string: a shorthand, a method, a getter, an expression
|
|
187
|
+
plain string: a shorthand, a method, a getter, an expression — and
|
|
188
|
+
undefined when the options give none: the SDK's default. */
|
|
173
189
|
function callEndpoint(tokens, paren) {
|
|
174
190
|
if (!isPunct(tokens[paren + 1], "{"))
|
|
175
191
|
return null;
|
|
176
|
-
let endpoint =
|
|
192
|
+
let endpoint = undefined;
|
|
177
193
|
let depth = 0;
|
|
178
194
|
let property = false;
|
|
179
195
|
for (let k = paren + 1; k < tokens.length && k < paren + OPTIONS_TOKENS; k++) {
|
|
@@ -363,9 +379,44 @@ function declarationsOf(tokens, after) {
|
|
|
363
379
|
continue;
|
|
364
380
|
out.push({ name, at: i + 1, exported: exportedAt(i), from: body, to: after[body], fn: true, async: isWord(tokens[i - 1], "async") });
|
|
365
381
|
}
|
|
382
|
+
else if (t.value === "export" && isWord(tokens[i + 1], "default") && !isPunct(tokens[i - 1], ".")) {
|
|
383
|
+
const d = defaultExport(tokens, after, i);
|
|
384
|
+
if (d)
|
|
385
|
+
out.push(d);
|
|
386
|
+
}
|
|
366
387
|
}
|
|
367
388
|
return out;
|
|
368
389
|
}
|
|
390
|
+
/** What `export default` at `i` exports, as a declaration named `default`
|
|
391
|
+
— a reserved word, so no binding of the file reads the same: an
|
|
392
|
+
expression's value; an anonymous function's body; the name of a named
|
|
393
|
+
function, which is the binding the module exports (a function
|
|
394
|
+
declaration the loop above reads too, so writing to that name later
|
|
395
|
+
changes the default as well). A class, a generator or a type is none. */
|
|
396
|
+
function defaultExport(tokens, after, i) {
|
|
397
|
+
const at = i + 1;
|
|
398
|
+
let j = i + 2;
|
|
399
|
+
const async = isWord(tokens[j], "async") && isWord(tokens[j + 1], "function") && !tokens[j + 1].nl;
|
|
400
|
+
if (async)
|
|
401
|
+
j++;
|
|
402
|
+
if (isWord(tokens[j], "function")) {
|
|
403
|
+
if (isPunct(tokens[j + 1], "*"))
|
|
404
|
+
return null;
|
|
405
|
+
if (tokens[j + 1]?.kind === "ident")
|
|
406
|
+
return { name: "default", at, exported: true, from: j + 1, to: j + 2, fn: false, async: false };
|
|
407
|
+
let open = j + 1;
|
|
408
|
+
while (open < tokens.length && open < j + 50 && !isPunct(tokens[open], "("))
|
|
409
|
+
open++;
|
|
410
|
+
if (!isPunct(tokens[open], "("))
|
|
411
|
+
return null;
|
|
412
|
+
const body = bodyAfter(tokens, after[open]);
|
|
413
|
+
return body === null ? null : { name: "default", at, exported: true, from: body, to: after[body], fn: true, async };
|
|
414
|
+
}
|
|
415
|
+
if (["class", "abstract", "interface"].some((w) => isWord(tokens[j], w)))
|
|
416
|
+
return null;
|
|
417
|
+
const to = initializerEnd(tokens, after, i + 2);
|
|
418
|
+
return { name: "default", at, exported: true, from: i + 2, to, fn: false, async: false };
|
|
419
|
+
}
|
|
369
420
|
/** The local names an `export { a as GET, b }` clause exports, by exported
|
|
370
421
|
name; re-exports `from` another module don't count — their code is elsewhere. */
|
|
371
422
|
function exportClauses(tokens) {
|
|
@@ -720,21 +771,29 @@ function readingOf(tokens) {
|
|
|
720
771
|
const bound = new Set([...rebound, ...declarations.map((d) => d.name)]);
|
|
721
772
|
return { b, declarations, rebound, bound };
|
|
722
773
|
}
|
|
723
|
-
/**
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
774
|
+
/** The App Router's factory, whose handler a route exports as GET, and the
|
|
775
|
+
Pages Router's, whose handler an API route exports as its default. */
|
|
776
|
+
const APP_HANDLER = (names) => ({ name: "createReleaseHandler", direct: names.handler, ns: names.ns.handler });
|
|
777
|
+
const PAGES_HANDLER = (names) => ({ name: "createPagesReleaseHandler", direct: names.pagesHandler, ns: names.ns.pagesHandler });
|
|
778
|
+
/** Whether the route's export — `GET` of an App Router route, `default` of
|
|
779
|
+
a Pages API route — answers with the factory's handler: the export is a
|
|
780
|
+
handler — `export const GET = createReleaseHandler(…)`, `export { release
|
|
781
|
+
as GET }`, `const GET = make()`, `export default
|
|
782
|
+
createPagesReleaseHandler(…)`, `export default release` — or a function
|
|
783
|
+
that always returns a handler's response: `export async function
|
|
784
|
+
GET(req) { return release(req); }`, `export default function handler(req,
|
|
785
|
+
res) { return release(req, res); }`. An export that only mentions the
|
|
786
|
+
handler, calls it and drops the result, returns it uncalled (`() =>
|
|
787
|
+
release`), or answers something else on another branch or by a throw
|
|
788
|
+
doesn't. A name declared more than once, or returned more than once, is
|
|
789
|
+
what all its values agree on, a name bound or changed any other way is
|
|
790
|
+
nothing the reading knows, and neither is the factory where the file
|
|
791
|
+
binds its name again — or a namespace's `ns.createReleaseHandler` where
|
|
792
|
+
it binds `ns` again. */
|
|
793
|
+
function releaseHandlerOf(tokens, factory, exported, read) {
|
|
794
|
+
const handlers = factory.direct;
|
|
736
795
|
// `createReleaseHandler(` under a name it's imported as, or `ns.createReleaseHandler(` under a namespace's — not `a.ns.createReleaseHandler(`, a member of something else
|
|
737
|
-
const member = (k) => tokens[k].value ===
|
|
796
|
+
const member = (k) => tokens[k].value === factory.name && isPunct(tokens[k - 1], ".") && tokens[k - 2]?.kind === "ident" && factory.ns.has(tokens[k - 2].value) && !isPunct(tokens[k - 3], ".");
|
|
738
797
|
const calls = (k) => (handlers.has(tokens[k].value) || member(k)) && isPunct(tokens[k + 1], "(");
|
|
739
798
|
if (!tokens.some((u, k) => u.kind === "ident" && calls(k)))
|
|
740
799
|
return null;
|
|
@@ -777,9 +836,9 @@ function releaseHandlerOf(tokens, names, read) {
|
|
|
777
836
|
if (!bound.has(h))
|
|
778
837
|
grow(h, 1 << FACTORY);
|
|
779
838
|
// a namespace's member is the factory wherever the namespace is the import; bound again, neither is anything the reading knows
|
|
780
|
-
for (const ns of
|
|
839
|
+
for (const ns of factory.ns)
|
|
781
840
|
if (!bound.has(ns))
|
|
782
|
-
grow(memberName(ns,
|
|
841
|
+
grow(memberName(ns, factory.name), 1 << FACTORY);
|
|
783
842
|
while (queue.length > 0) {
|
|
784
843
|
const [name, bits] = queue.pop();
|
|
785
844
|
for (const [i, term] of readers.get(name) ?? []) {
|
|
@@ -795,7 +854,7 @@ function releaseHandlerOf(tokens, names, read) {
|
|
|
795
854
|
ofValue[i] = ofValue[i] | fresh;
|
|
796
855
|
const d = values[i];
|
|
797
856
|
// a declaration under an import's name — the factory's or its namespace's — is nothing the reading knows either
|
|
798
|
-
if (handlers.has(d) ||
|
|
857
|
+
if (handlers.has(d) || factory.ns.has(d))
|
|
799
858
|
continue;
|
|
800
859
|
const counts = agree.get(d) ?? [0, 0, 0];
|
|
801
860
|
agree.set(d, counts);
|
|
@@ -812,17 +871,18 @@ function releaseHandlerOf(tokens, names, read) {
|
|
|
812
871
|
}
|
|
813
872
|
}
|
|
814
873
|
const serves = (name) => ((levels.get(name) ?? 0) & (1 << HANDLER)) !== 0;
|
|
815
|
-
const direct = serves(
|
|
816
|
-
const clause = (exportClauses(tokens).get(
|
|
874
|
+
const direct = serves(exported) && declarations.some((d) => d.name === exported && d.exported);
|
|
875
|
+
const clause = (exportClauses(tokens).get(exported) ?? []).some(serves);
|
|
817
876
|
return direct || clause ? "GET" : "other";
|
|
818
877
|
}
|
|
819
|
-
/** The listeners one file mounts and, for a route file
|
|
820
|
-
serves the release
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
878
|
+
/** The listeners one file mounts and, for a route file of `router`,
|
|
879
|
+
whether it serves the release: an App Router route through its GET, a
|
|
880
|
+
Pages API route through its default export. An imported name the file
|
|
881
|
+
binds again — a parameter, a declaration, destructuring — may not be
|
|
882
|
+
the import where it's used, and the reading can't tell where: none of
|
|
883
|
+
its uses count. A namespace's name the same: its members count only
|
|
884
|
+
where `ns` itself is the import. */
|
|
885
|
+
export function liveInFile(file, text, router = "app") {
|
|
826
886
|
const tokens = tokenize(text);
|
|
827
887
|
const names = importsOf(tokens);
|
|
828
888
|
let reading = null;
|
|
@@ -834,6 +894,10 @@ export function liveInFile(file, text) {
|
|
|
834
894
|
return isPunct(tokens[k - 1], ".") && ns?.kind === "ident" && namespaces.has(ns.value) && !isPunct(tokens[k - 3], ".") && !read().bound.has(ns.value);
|
|
835
895
|
};
|
|
836
896
|
const listeners = [];
|
|
897
|
+
const listener = (via, endpoint, pages = false) => ({
|
|
898
|
+
...(endpoint === undefined ? { file, via, endpoint: RELEASE_ROUTE, byDefault: true } : { file, via, endpoint }),
|
|
899
|
+
...(pages ? { router: "pages" } : {}),
|
|
900
|
+
});
|
|
837
901
|
for (let k = 0; k < tokens.length; k++) {
|
|
838
902
|
const t = tokens[k];
|
|
839
903
|
if (t.kind !== "ident")
|
|
@@ -841,10 +905,18 @@ export function liveInFile(file, text) {
|
|
|
841
905
|
if (names.live.has(t.value) && isPunct(tokens[k - 1], "<")) {
|
|
842
906
|
if (read().bound.has(t.value))
|
|
843
907
|
continue;
|
|
844
|
-
listeners.push(
|
|
908
|
+
listeners.push(listener("<MapledLive />", jsxEndpoint(tokens, k + 1)));
|
|
909
|
+
}
|
|
910
|
+
else if (names.livePages.has(t.value) && isPunct(tokens[k - 1], "<")) {
|
|
911
|
+
if (read().bound.has(t.value))
|
|
912
|
+
continue;
|
|
913
|
+
listeners.push(listener("<MapledLive />", jsxEndpoint(tokens, k + 1), true));
|
|
845
914
|
}
|
|
846
915
|
else if (t.value === "MapledLive" && isPunct(tokens[k - 3], "<") && ofNamespace(k, names.ns.live)) {
|
|
847
|
-
listeners.push(
|
|
916
|
+
listeners.push(listener("<MapledLive />", jsxEndpoint(tokens, k + 1)));
|
|
917
|
+
}
|
|
918
|
+
else if (t.value === "MapledLive" && isPunct(tokens[k - 3], "<") && ofNamespace(k, names.ns.livePages)) {
|
|
919
|
+
listeners.push(listener("<MapledLive />", jsxEndpoint(tokens, k + 1), true));
|
|
848
920
|
}
|
|
849
921
|
else if (names.watch.has(t.value) && isPunct(tokens[k + 1], "(") && !isPunct(tokens[k - 1], ".")) {
|
|
850
922
|
const prev = tokens[k - 1];
|
|
@@ -852,23 +924,114 @@ export function liveInFile(file, text) {
|
|
|
852
924
|
continue;
|
|
853
925
|
if (read().bound.has(t.value))
|
|
854
926
|
continue;
|
|
855
|
-
listeners.push(
|
|
927
|
+
listeners.push(listener("watchRelease", callEndpoint(tokens, k + 1)));
|
|
856
928
|
}
|
|
857
929
|
else if (t.value === "watchRelease" && isPunct(tokens[k + 1], "(") && ofNamespace(k, names.ns.watch)) {
|
|
858
|
-
listeners.push(
|
|
930
|
+
listeners.push(listener("watchRelease", callEndpoint(tokens, k + 1)));
|
|
859
931
|
}
|
|
860
932
|
}
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
933
|
+
if (router === "pages") {
|
|
934
|
+
// an API route: segment config is the App Router's; what may break the handler is the runtime
|
|
935
|
+
const handler = releaseHandlerOf(tokens, PAGES_HANDLER(names), "default", read);
|
|
936
|
+
const runtime = handler === null ? undefined : pagesRuntime(tokens, read().b);
|
|
937
|
+
return { listeners, handler, params: false, ...(runtime !== undefined ? { runtime } : {}) };
|
|
938
|
+
}
|
|
939
|
+
// `dynamicParams` and the segment settings as the route exports them, or an export the reading can't list
|
|
940
|
+
const listing = tokens.some((u, k) => isWord(u, "exports") || (isWord(u, "export") && isPunct(tokens[k + 1], "*")));
|
|
941
|
+
const sets = (name, keeps) => (listing || tokens.some((u) => isName(u, name))) && setsExport(tokens, read().b.after, name, keeps);
|
|
942
|
+
const params = sets("dynamicParams", (t) => isWord(t ?? undefined, "true"));
|
|
943
|
+
// "force-dynamic" keeps the route only while the handler's read names its own window (keepsWindow)
|
|
944
|
+
const forced = () => sets("dynamic", (t) => literal(t ?? undefined) && t.value === "auto") && !windowsKept(tokens, names, read().b.after);
|
|
945
|
+
const segment = sets("dynamic", SEGMENT_KEEPS.dynamic) ? "dynamic" : forced() ? "force-dynamic" : sets("fetchCache", SEGMENT_KEEPS.fetchCache) ? "fetchCache" : undefined;
|
|
946
|
+
return { listeners, handler: releaseHandlerOf(tokens, APP_HANDLER(names), "GET", read), params, ...(segment ? { segment } : {}) };
|
|
864
947
|
}
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
`
|
|
871
|
-
|
|
948
|
+
/** Whether every release handler the file makes keeps a window of its own
|
|
949
|
+
for the pointer's read (keepsWindow), with the factory only ever called
|
|
950
|
+
where it's named: `const make = createReleaseHandler`, a namespace
|
|
951
|
+
passed on or taken apart, make handlers the reading doesn't see. */
|
|
952
|
+
function windowsKept(tokens, names, after) {
|
|
953
|
+
// the import clauses name them without calling: `import … from`, never past a string — `import "server-only"` has no clause
|
|
954
|
+
const clause = new Uint8Array(tokens.length);
|
|
955
|
+
for (let i = 0; i < tokens.length; i++) {
|
|
956
|
+
if (!isWord(tokens[i], "import") || isPunct(tokens[i - 1], ".") || isPunct(tokens[i + 1], "("))
|
|
957
|
+
continue;
|
|
958
|
+
let j = i + 1;
|
|
959
|
+
while (j < tokens.length && j < i + 200 && !isWord(tokens[j], "from") && tokens[j].kind !== "string" && !isPunct(tokens[j], ";"))
|
|
960
|
+
j = isPunct(tokens[j], "{") ? after[j] : j + 1;
|
|
961
|
+
if (isWord(tokens[j], "from"))
|
|
962
|
+
clause.fill(1, i, j + 1);
|
|
963
|
+
}
|
|
964
|
+
for (let k = 0; k < tokens.length; k++) {
|
|
965
|
+
const t = tokens[k];
|
|
966
|
+
if (t.kind !== "ident" || clause[k] === 1)
|
|
967
|
+
continue;
|
|
968
|
+
const ns = names.ns.handler.has(t.value) && !isPunct(tokens[k - 1], ".");
|
|
969
|
+
if (ns && !isPunct(tokens[k + 1], "."))
|
|
970
|
+
return false;
|
|
971
|
+
const direct = names.handler.has(t.value) && !isPunct(tokens[k - 1], ".");
|
|
972
|
+
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], ".");
|
|
973
|
+
if ((direct || member) && !(isPunct(tokens[k + 1], "(") && keepsWindow(tokens, after, k + 1)))
|
|
974
|
+
return false;
|
|
975
|
+
}
|
|
976
|
+
return true;
|
|
977
|
+
}
|
|
978
|
+
/** Whether a `createReleaseHandler` call, from its opening parenthesis,
|
|
979
|
+
leaves the pointer's read a window of its own: options written out as
|
|
980
|
+
an object whose `revalidate` is absent (3600) or digits above 0. On a
|
|
981
|
+
`force-dynamic` route Next 15 and later fetch a read with no window —
|
|
982
|
+
`revalidate: false` counts as none — uncached, past the `mapled` tag,
|
|
983
|
+
and the release moves before the webhook refreshes the pages. False
|
|
984
|
+
where the reading can't tell: options from elsewhere, a spread, a key
|
|
985
|
+
the code decides, a `revalidate` that isn't plain digits. */
|
|
986
|
+
function keepsWindow(tokens, after, paren) {
|
|
987
|
+
const open = paren + 1;
|
|
988
|
+
if (!isPunct(tokens[open], "{") || !isPunct(tokens[after[open]], ")"))
|
|
989
|
+
return false;
|
|
990
|
+
let kept = true;
|
|
991
|
+
for (const [a, z] of itemsOf(tokens, after, open)) {
|
|
992
|
+
if (a === z)
|
|
993
|
+
continue;
|
|
994
|
+
if (isSpread(tokens, a))
|
|
995
|
+
return false;
|
|
996
|
+
const { name, next } = propertyKey(tokens, a);
|
|
997
|
+
if (name === null)
|
|
998
|
+
return false;
|
|
999
|
+
if (name !== "revalidate")
|
|
1000
|
+
continue;
|
|
1001
|
+
// the last one wins, as in the object: `revalidate: 60`, `revalidate: 0.5`
|
|
1002
|
+
const digits = tokens.slice(next + 1, z).map((u) => (u.kind === "punct" ? u.value : " ")).join("");
|
|
1003
|
+
kept = isPunct(tokens[next], ":") && /^(\d+\.?\d*|\.\d+)$/.test(digits) && Number(digits) > 0;
|
|
1004
|
+
}
|
|
1005
|
+
return kept;
|
|
1006
|
+
}
|
|
1007
|
+
/** Whether the token is the name — as an identifier or a string: `export { x as "name" }`. */
|
|
1008
|
+
const isName = (u, name) => (u?.kind === "ident" || u?.kind === "string") && u.value === name;
|
|
1009
|
+
/** The route segment settings that change how Next serves the release
|
|
1010
|
+
route or reads what the handler reads, in the order doctor names them:
|
|
1011
|
+
`dynamic` can make it a static copy built without the tab's request,
|
|
1012
|
+
and `fetchCache` can make the handler's read skip the `mapled` cache
|
|
1013
|
+
tag, so the release moves before the webhook refreshes the pages.
|
|
1014
|
+
`revalidate` isn't one: it only fills in fetches that don't name their
|
|
1015
|
+
own window, and the handler's names one (Next 14's fetch patch; the
|
|
1016
|
+
live smoke with `revalidate = 1` on Next 15). "force-dynamic" is the
|
|
1017
|
+
third: harmless while the handler keeps its read's window, and past
|
|
1018
|
+
the tag when it doesn't (keepsWindow). */
|
|
1019
|
+
export const SEGMENT_SETTINGS = ["dynamic", "fetchCache"];
|
|
1020
|
+
/** For each, the values written out that leave the route as the handler
|
|
1021
|
+
sets it up: the default, and for `dynamic` also "force-dynamic" — the
|
|
1022
|
+
route reads its request, so it is dynamic anyway; what it does to the
|
|
1023
|
+
handler's read is checked apart (windowsKept). */
|
|
1024
|
+
const SEGMENT_KEEPS = {
|
|
1025
|
+
dynamic: (t) => literal(t ?? undefined) && (t.value === "auto" || t.value === "force-dynamic"),
|
|
1026
|
+
fetchCache: (t) => literal(t ?? undefined) && t.value === "auto",
|
|
1027
|
+
};
|
|
1028
|
+
/** Whether the route may export `name` as anything but a value `keeps`
|
|
1029
|
+
accepts, written out on a `const` (`keeps` gets the one token written,
|
|
1030
|
+
and where the declarator's name is). Next reads only the module's
|
|
1031
|
+
export: a local variable, a property or a string that says the name is
|
|
1032
|
+
nothing to it. An export the reading can't list — `export *`,
|
|
1033
|
+
CommonJS — may be it. */
|
|
1034
|
+
function setsExport(tokens, after, name, keeps) {
|
|
872
1035
|
for (let i = 0; i < tokens.length; i++) {
|
|
873
1036
|
const t = tokens[i];
|
|
874
1037
|
// CommonJS: `module.exports = { dynamicParams: false }`, `exports.dynamicParams = false`
|
|
@@ -888,7 +1051,7 @@ function setsParams(tokens, after) {
|
|
|
888
1051
|
// a clause, with or without `from`: `export { x as dynamicParams }`, `export { dynamicParams }`, `export { x as "dynamicParams" }`
|
|
889
1052
|
if (isPunct(tokens[j], "{")) {
|
|
890
1053
|
for (let k = j + 1; k < after[j] - 1; k++)
|
|
891
|
-
if (
|
|
1054
|
+
if (isName(tokens[k], name) && !isWord(tokens[k + 1], "as"))
|
|
892
1055
|
return true;
|
|
893
1056
|
continue;
|
|
894
1057
|
}
|
|
@@ -897,8 +1060,8 @@ function setsParams(tokens, after) {
|
|
|
897
1060
|
const declare = tokens[j];
|
|
898
1061
|
if (declare?.kind !== "ident" || !DECLARE.has(declare.value)) {
|
|
899
1062
|
// `export function dynamicParams`, `export class …`, `export import dynamicParams = …`
|
|
900
|
-
const
|
|
901
|
-
if (
|
|
1063
|
+
const named = isPunct(tokens[j + 1], "*") ? j + 2 : j + 1;
|
|
1064
|
+
if (isName(tokens[named], name))
|
|
902
1065
|
return true;
|
|
903
1066
|
continue;
|
|
904
1067
|
}
|
|
@@ -907,13 +1070,13 @@ function setsParams(tokens, after) {
|
|
|
907
1070
|
let next;
|
|
908
1071
|
if (isPunct(tokens[k], "{") || isPunct(tokens[k], "[")) {
|
|
909
1072
|
for (let p = k + 1; p < after[k] - 1; p++)
|
|
910
|
-
if (
|
|
1073
|
+
if (isName(tokens[p], name))
|
|
911
1074
|
return true;
|
|
912
1075
|
next = after[k];
|
|
913
1076
|
}
|
|
914
1077
|
else if (tokens[k].kind === "ident") {
|
|
915
|
-
// only a `const` keeps
|
|
916
|
-
if (
|
|
1078
|
+
// only a `const` keeps its value: a `let` or `var` may change it later
|
|
1079
|
+
if (isName(tokens[k], name) && (declare.value !== "const" || !keeps(writtenValue(tokens, after, k), k)))
|
|
917
1080
|
return true;
|
|
918
1081
|
next = k + 1;
|
|
919
1082
|
}
|
|
@@ -932,15 +1095,16 @@ function setsParams(tokens, after) {
|
|
|
932
1095
|
}
|
|
933
1096
|
return false;
|
|
934
1097
|
}
|
|
935
|
-
/**
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
1098
|
+
/** The one token a declaration at `k` writes as its value: `name = true`,
|
|
1099
|
+
with or without a type (`: boolean`), the value in parentheses or
|
|
1100
|
+
followed by `as` / `satisfies` / `!`. Null when the value is anything
|
|
1101
|
+
else — an expression, more than one token — or there is none. */
|
|
1102
|
+
function writtenValue(tokens, after, k) {
|
|
939
1103
|
if (tokens[k].kind !== "ident")
|
|
940
|
-
return
|
|
1104
|
+
return null;
|
|
941
1105
|
const eq = isPunct(tokens[k + 1], ":") ? typeEnd(tokens, after, k + 2, tokens.length) : k + 1;
|
|
942
1106
|
if (eq < 0 || !isPunct(tokens[eq], "=") || isPunct(tokens[eq + 1], "="))
|
|
943
|
-
return
|
|
1107
|
+
return null;
|
|
944
1108
|
let from = eq + 1;
|
|
945
1109
|
let to = initializerEnd(tokens, after, from);
|
|
946
1110
|
for (;;) {
|
|
@@ -958,7 +1122,7 @@ function paramsDefault(tokens, after, k) {
|
|
|
958
1122
|
while (end > from + 1 && isPunct(tokens[end - 1], "!"))
|
|
959
1123
|
end--;
|
|
960
1124
|
if (!isPunct(tokens[from], "(") || after[from] !== end)
|
|
961
|
-
return end === from + 1
|
|
1125
|
+
return end === from + 1 ? tokens[from] : null;
|
|
962
1126
|
from++;
|
|
963
1127
|
to = end - 1;
|
|
964
1128
|
}
|
|
@@ -1008,267 +1172,1066 @@ async function atRoot(dir, name) {
|
|
|
1008
1172
|
const SETTINGS = ["pageExtensions", "basePath"];
|
|
1009
1173
|
// calls that set a property of an object they're given, by a name or from another object, or give it a prototype that may carry one
|
|
1010
1174
|
const COPIES = new Set(["assign", "defineProperty", "defineProperties", "Reflect", "setPrototypeOf", "__defineGetter__", "__defineSetter__", "__proto__"]);
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
export
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1175
|
+
// 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
|
|
1176
|
+
const HOOKS = new Set(["webpack", "headers", "redirects", "rewrites", "generateBuildId", "exportPathMap"]);
|
|
1177
|
+
// the names the subset reads without a binding, which the file may not bind
|
|
1178
|
+
const AMBIENT = new Set(["process", "require", "module", "exports", "URL", "undefined", "NaN", "Infinity", "globalThis", "eval", "arguments"]);
|
|
1179
|
+
// the words that start a statement of the subset; none goes on with the expression a line break comes after
|
|
1180
|
+
const STARTS = new Set(["import", "export", "const", "let", "var", "module"]);
|
|
1181
|
+
// words after which JavaScript reads a `/` as a regex, not a division — the contextual ones (`of`, `await`, `let`) either way
|
|
1182
|
+
const BEFORE_REGEX = new Set(["return", "typeof", "case", "delete", "do", "else", "in", "instanceof", "new", "of", "throw", "void", "yield", "await", "extends", "let"]);
|
|
1183
|
+
// words no value ends with: past a line break after one, the expression goes on
|
|
1184
|
+
const GOES_ON = new Set([...BEFORE_REGEX, "as", "satisfies", "keyof", "default", "export", "import", "const", "var"]);
|
|
1185
|
+
// brackets and what closes each
|
|
1186
|
+
const OPENS = new Map([
|
|
1187
|
+
["(", ")"],
|
|
1188
|
+
["[", "]"],
|
|
1189
|
+
["{", "}"],
|
|
1190
|
+
]);
|
|
1191
|
+
// how deep values may nest, so a deep file can't overflow the stack
|
|
1192
|
+
const DEPTH = 200;
|
|
1193
|
+
const isDigit = (t) => t?.kind === "punct" && /^\d$/.test(t.value);
|
|
1194
|
+
/** Whether every bracket closes with its own kind, and none closes nothing. */
|
|
1195
|
+
function balanced(tokens, b) {
|
|
1027
1196
|
for (let k = 0; k < tokens.length; k++) {
|
|
1028
1197
|
const t = tokens[k];
|
|
1029
|
-
|
|
1030
|
-
if (isPunct(t, "\\") || (t.kind === "ident" && (t.escaped === true || COPIES.has(t.value))) || (t.kind === "string" && t.value === "__proto__"))
|
|
1031
|
-
return null;
|
|
1032
|
-
// a setting's name inside a longer string: JSON the code parses
|
|
1033
|
-
if (t.kind === "string" && !SETTINGS.includes(t.value) && SETTINGS.some((s) => t.value.includes(s)))
|
|
1034
|
-
return null;
|
|
1035
|
-
if (!isPunct(t, "["))
|
|
1036
|
-
continue;
|
|
1037
|
-
// a computed key or member written to, unless a lone string or digits name it: `{ [name]: … }`, `config[name] = …`, not `rules[0] = …`
|
|
1038
|
-
const close = b.after[k] - 1;
|
|
1039
|
-
if (close === k + 2 && literal(tokens[k + 1]))
|
|
1040
|
-
continue;
|
|
1041
|
-
let digits = k + 1;
|
|
1042
|
-
while (digits < close && tokens[digits].kind === "punct" && /^\d$/.test(tokens[digits].value))
|
|
1043
|
-
digits++;
|
|
1044
|
-
if (digits === close && close > k + 1)
|
|
1198
|
+
if (t.kind !== "punct")
|
|
1045
1199
|
continue;
|
|
1046
|
-
const
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
if (
|
|
1053
|
-
return
|
|
1200
|
+
const close = OPENS.get(t.value);
|
|
1201
|
+
if (close !== undefined) {
|
|
1202
|
+
const at = b.after[k] - 1;
|
|
1203
|
+
if (!isPunct(tokens[at], close) || b.open[at] !== k)
|
|
1204
|
+
return false;
|
|
1205
|
+
}
|
|
1206
|
+
else if ((t.value === ")" || t.value === "]" || t.value === "}") && b.open[k] < 0)
|
|
1207
|
+
return false;
|
|
1054
1208
|
}
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1209
|
+
return true;
|
|
1210
|
+
}
|
|
1211
|
+
/** Whether each `/` of the tokens is what JavaScript reads there: a
|
|
1212
|
+
division after a value — a name, a literal, `]`, a `)` that closes no
|
|
1213
|
+
`if (…)` — and a regex anywhere else. The tokens read a regex only after
|
|
1214
|
+
punctuation that ends no value (and `return`, `typeof`), so they differ
|
|
1215
|
+
after `+`, after `else`, after `if (x)`: a brace in such a regex would
|
|
1216
|
+
move every bracket after it. After `}` JavaScript can read either. */
|
|
1217
|
+
function slashesAgree(tokens, b) {
|
|
1058
1218
|
for (let k = 0; k < tokens.length; k++) {
|
|
1059
|
-
|
|
1060
|
-
|
|
1219
|
+
const t = tokens[k];
|
|
1220
|
+
const regex = t.kind === "regex";
|
|
1221
|
+
if (!regex && !isPunct(t, "/"))
|
|
1222
|
+
continue;
|
|
1223
|
+
const p = tokens[k - 1];
|
|
1224
|
+
let division;
|
|
1225
|
+
if (p === undefined)
|
|
1226
|
+
division = false;
|
|
1227
|
+
else if (p.kind === "ident")
|
|
1228
|
+
division = !BEFORE_REGEX.has(p.value) || isPunct(tokens[k - 2], ".");
|
|
1229
|
+
else if (p.kind === "string" || p.kind === "regex" || isDigit(p) || p.value === "]")
|
|
1230
|
+
division = true;
|
|
1231
|
+
else if (p.value === ")") {
|
|
1232
|
+
const open = b.open[k - 1];
|
|
1233
|
+
const head = open >= 0 ? tokens[open - 1] : undefined;
|
|
1234
|
+
const condition = head?.kind === "ident" && !isPunct(tokens[open - 2], ".") && (["if", "while", "for", "with"].includes(head.value) || (head.value === "await" && isWord(tokens[open - 2], "for")));
|
|
1235
|
+
division = !condition;
|
|
1061
1236
|
}
|
|
1062
|
-
else if (
|
|
1063
|
-
|
|
1064
|
-
}
|
|
1065
|
-
// `export { default } from "./base.mjs"`: the config is another module's
|
|
1066
|
-
const reexported = tokens.some((t, k) => isWord(t, "export") && isPunct(tokens[k + 1], "{") && isWord(tokens[b.after[k + 1]], "from") && tokens.slice(k + 2, b.after[k + 1] - 1).some((u) => isWord(u, "default")));
|
|
1067
|
-
// the objects the config is made of, and whether any part comes from elsewhere
|
|
1068
|
-
const objects = new Set();
|
|
1069
|
-
let opaque = reexported || exported.length + names.length > 1;
|
|
1070
|
-
const byName = new Map();
|
|
1071
|
-
for (const d of declarations) {
|
|
1072
|
-
const list = byName.get(d.name);
|
|
1073
|
-
if (list)
|
|
1074
|
-
list.push(d);
|
|
1237
|
+
else if (p.value === "}")
|
|
1238
|
+
return false;
|
|
1075
1239
|
else
|
|
1076
|
-
|
|
1240
|
+
division = false;
|
|
1241
|
+
if (regex === division)
|
|
1242
|
+
return false;
|
|
1077
1243
|
}
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1244
|
+
return true;
|
|
1245
|
+
}
|
|
1246
|
+
/** The file as the declarative subset nextConfigOf reads (see there), or
|
|
1247
|
+
null when anything in it is outside the subset. */
|
|
1248
|
+
function declarative(tokens, b) {
|
|
1249
|
+
const bindings = new Map();
|
|
1250
|
+
const props = new Map();
|
|
1251
|
+
const configs = [];
|
|
1252
|
+
let exported = null;
|
|
1253
|
+
let exports = 0;
|
|
1254
|
+
let depth = 0;
|
|
1255
|
+
const guard = (fail, f) => {
|
|
1256
|
+
if (depth >= DEPTH)
|
|
1257
|
+
return fail;
|
|
1258
|
+
depth++;
|
|
1259
|
+
try {
|
|
1260
|
+
return f();
|
|
1261
|
+
}
|
|
1262
|
+
finally {
|
|
1263
|
+
depth--;
|
|
1086
1264
|
}
|
|
1087
|
-
}
|
|
1088
|
-
for (let k = 0, depth = 0; k < tokens.length; k++) {
|
|
1089
|
-
depth += patterns[k];
|
|
1090
|
-
if (depth > 0 && tokens[k].kind === "ident")
|
|
1091
|
-
reassigned.add(tokens[k].value);
|
|
1092
|
-
// a loop's head without a declaration: `for (config of variants)`
|
|
1093
|
-
const loop = isPunct(tokens[k - 1], "(") && (isWord(tokens[k - 2], "for") || (isWord(tokens[k - 2], "await") && isWord(tokens[k - 3], "for")));
|
|
1094
|
-
if (loop && tokens[k].kind === "ident" && (isWord(tokens[k + 1], "of") || isWord(tokens[k + 1], "in")))
|
|
1095
|
-
reassigned.add(tokens[k].value);
|
|
1096
|
-
}
|
|
1097
|
-
// names taken from a file of the site — `import x from "./x"`, `const x = require("./x")`: a plugin there may set anything
|
|
1098
|
-
const relative = (k) => (isWord(tokens[k], "require") || isWord(tokens[k], "import")) && isPunct(tokens[k + 1], "(") && tokens[k + 2]?.kind === "string" && tokens[k + 2].value.startsWith(".");
|
|
1099
|
-
const loadsBefore = new Int32Array(tokens.length + 1);
|
|
1100
|
-
for (let k = 0; k < tokens.length; k++)
|
|
1101
|
-
loadsBefore[k + 1] = loadsBefore[k] + (relative(k) ? 1 : 0);
|
|
1102
|
-
const local = new Set(declarations.filter((d) => loadsBefore[d.to] > loadsBefore[d.from]).map((d) => d.name));
|
|
1103
|
-
for (let k = 0; k < tokens.length; k++) {
|
|
1104
|
-
if (!isWord(tokens[k], "import") || isPunct(tokens[k + 1], "("))
|
|
1105
|
-
continue;
|
|
1106
|
-
const bound = [];
|
|
1107
|
-
let j = k + 1;
|
|
1108
|
-
for (; j < tokens.length && j < k + 200 && !isPunct(tokens[j], ";") && !isWord(tokens[j], "from"); j++)
|
|
1109
|
-
if (tokens[j].kind === "ident")
|
|
1110
|
-
bound.push(tokens[j].value);
|
|
1111
|
-
if (isWord(tokens[j], "from") && tokens[j + 1]?.kind === "string" && tokens[j + 1].value.startsWith("."))
|
|
1112
|
-
for (const name of bound)
|
|
1113
|
-
local.add(name);
|
|
1114
|
-
}
|
|
1115
|
-
// each name is read once: a name passed twice, or one that names itself, adds nothing
|
|
1116
|
-
const visited = new Set();
|
|
1117
|
-
const fromName = (name, depth) => {
|
|
1118
|
-
if (visited.has(name))
|
|
1119
|
-
return;
|
|
1120
|
-
visited.add(name);
|
|
1121
|
-
const ds = byName.get(name) ?? [];
|
|
1122
|
-
// declared once, with a value, never assigned again: that value; anything else — an import, a parameter — is from elsewhere
|
|
1123
|
-
if (ds.length !== 1 || reassigned.has(name) || ds[0].fn || ds[0].from === ds[0].to)
|
|
1124
|
-
opaque = true;
|
|
1125
|
-
else
|
|
1126
|
-
visit(ds[0].from, ds[0].to, depth + 1);
|
|
1127
1265
|
};
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
if (
|
|
1132
|
-
|
|
1133
|
-
|
|
1266
|
+
// the items between the brackets that open at `open`: a trailing comma adds none, a hole makes it null
|
|
1267
|
+
const listOf = (open) => {
|
|
1268
|
+
const items = itemsOf(tokens, b.after, open);
|
|
1269
|
+
if (items.length === 1 && items[0][0] === items[0][1])
|
|
1270
|
+
return [];
|
|
1271
|
+
if (items.length > 1 && items[items.length - 1][0] === items[items.length - 1][1])
|
|
1272
|
+
items.pop();
|
|
1273
|
+
return items.some(([x, y]) => x === y) ? null : items;
|
|
1274
|
+
};
|
|
1275
|
+
// [from, to) split at its commas outside brackets
|
|
1276
|
+
const splitAt = (from, to) => {
|
|
1277
|
+
const out = [];
|
|
1278
|
+
let start = from;
|
|
1279
|
+
for (let k = from; k <= to; k++) {
|
|
1280
|
+
const t = tokens[k];
|
|
1281
|
+
if (k < to && t?.kind === "punct" && OPENS.has(t.value))
|
|
1282
|
+
k = b.after[k] - 1;
|
|
1283
|
+
else if (k === to || isPunct(t, ",")) {
|
|
1284
|
+
out.push([start, k]);
|
|
1285
|
+
start = k + 1;
|
|
1286
|
+
}
|
|
1134
1287
|
}
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1288
|
+
return out;
|
|
1289
|
+
};
|
|
1290
|
+
// a value's last token: a line break after it may end the statement
|
|
1291
|
+
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, "}"));
|
|
1292
|
+
// where the statement at `k` ends: its `;`, or a line that starts another after a value
|
|
1293
|
+
const statementEnd = (k) => {
|
|
1294
|
+
for (let j = k; j < tokens.length; j++) {
|
|
1295
|
+
const t = tokens[j];
|
|
1296
|
+
if (isPunct(t, ";"))
|
|
1297
|
+
return j;
|
|
1298
|
+
if (j > k && t.nl === true && t.kind === "ident" && STARTS.has(t.value) && endsValue(tokens[j - 1]))
|
|
1299
|
+
return j;
|
|
1300
|
+
if (t.kind === "punct" && OPENS.has(t.value))
|
|
1301
|
+
j = b.after[j] - 1;
|
|
1302
|
+
}
|
|
1303
|
+
return tokens.length;
|
|
1304
|
+
};
|
|
1305
|
+
const bind = (k, binding) => {
|
|
1306
|
+
const t = tokens[k];
|
|
1307
|
+
if (t?.kind !== "ident" || AMBIENT.has(t.value) || bindings.has(t.value))
|
|
1308
|
+
return false;
|
|
1309
|
+
bindings.set(t.value, { ...binding, at: k, uses: 0 });
|
|
1310
|
+
return true;
|
|
1311
|
+
};
|
|
1312
|
+
// a type as next.config writes one: `NextConfig`, `import("next").NextConfig`, with type arguments of the same and `[]` after; `const`
|
|
1313
|
+
const simpleType = (j, to, level = 0) => {
|
|
1314
|
+
if (level > 8 || j >= to)
|
|
1315
|
+
return -1;
|
|
1316
|
+
if (isWord(tokens[j], "const"))
|
|
1317
|
+
return j + 1;
|
|
1318
|
+
if (isWord(tokens[j], "import")) {
|
|
1319
|
+
if (!(isPunct(tokens[j + 1], "(") && literal(tokens[j + 2]) && isPunct(tokens[j + 3], ")") && isPunct(tokens[j + 4], ".")))
|
|
1320
|
+
return -1;
|
|
1321
|
+
j += 5;
|
|
1322
|
+
}
|
|
1323
|
+
if (tokens[j]?.kind !== "ident")
|
|
1324
|
+
return -1;
|
|
1325
|
+
j++;
|
|
1326
|
+
while (isPunct(tokens[j], ".") && tokens[j + 1]?.kind === "ident")
|
|
1327
|
+
j += 2;
|
|
1328
|
+
if (isPunct(tokens[j], "<")) {
|
|
1329
|
+
do {
|
|
1330
|
+
j = simpleType(j + 1, to, level + 1);
|
|
1331
|
+
if (j < 0)
|
|
1332
|
+
return -1;
|
|
1333
|
+
} while (isPunct(tokens[j], ","));
|
|
1334
|
+
if (!isPunct(tokens[j], ">"))
|
|
1335
|
+
return -1;
|
|
1336
|
+
j++;
|
|
1337
|
+
}
|
|
1338
|
+
while (isPunct(tokens[j], "[") && isPunct(tokens[j + 1], "]"))
|
|
1339
|
+
j += 2;
|
|
1340
|
+
return j <= to ? j : -1;
|
|
1341
|
+
};
|
|
1342
|
+
// what may follow a value to the end of its range: `as T`, `satisfies T`, on its line
|
|
1343
|
+
const suffix = (j, to) => {
|
|
1344
|
+
while (j < to) {
|
|
1345
|
+
if (!(isWord(tokens[j], "as") || isWord(tokens[j], "satisfies")) || tokens[j].nl === true)
|
|
1346
|
+
return false;
|
|
1347
|
+
j = simpleType(j + 1, to);
|
|
1348
|
+
if (j < 0)
|
|
1349
|
+
return false;
|
|
1350
|
+
}
|
|
1351
|
+
return j === to;
|
|
1352
|
+
};
|
|
1353
|
+
// where the value in [from, to) ends before its types, or -1
|
|
1354
|
+
const valueEnd = (from, to) => {
|
|
1138
1355
|
for (let k = from; k < to; k++) {
|
|
1139
|
-
const
|
|
1140
|
-
|
|
1141
|
-
if (k > from && (isWord(u, "as") || isWord(u, "satisfies")))
|
|
1142
|
-
end = k;
|
|
1143
|
-
else if (k > from && u.nl === true && u.kind === "ident" && u.value !== "in" && u.value !== "instanceof" && (p.kind !== "punct" || ")]}".includes(p.value) || /^\d$/.test(p.value)))
|
|
1144
|
-
end = k;
|
|
1145
|
-
else if (u.kind === "punct" && "([{".includes(u.value))
|
|
1356
|
+
const t = tokens[k];
|
|
1357
|
+
if (t.kind === "punct" && OPENS.has(t.value))
|
|
1146
1358
|
k = b.after[k] - 1;
|
|
1147
|
-
if (
|
|
1148
|
-
|
|
1359
|
+
else if ((isWord(t, "as") || isWord(t, "satisfies")) && k > from && !isPunct(tokens[k - 1], "."))
|
|
1360
|
+
return suffix(k, to) ? k : -1;
|
|
1149
1361
|
}
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1362
|
+
return to;
|
|
1363
|
+
};
|
|
1364
|
+
// 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
|
|
1365
|
+
const use = (k, role) => {
|
|
1366
|
+
const binding = bindings.get(tokens[k].value);
|
|
1367
|
+
if (binding === undefined || binding.at >= k || binding.busy === true)
|
|
1368
|
+
return false;
|
|
1369
|
+
if (binding.role !== undefined && binding.role !== role)
|
|
1370
|
+
return false;
|
|
1371
|
+
binding.role = role;
|
|
1372
|
+
binding.uses++;
|
|
1373
|
+
// the config is taken once: a second use would hand it to more code
|
|
1374
|
+
if (role === "config" && binding.uses > 1)
|
|
1375
|
+
return false;
|
|
1376
|
+
if (binding.uses > 1)
|
|
1377
|
+
return true;
|
|
1378
|
+
binding.busy = true;
|
|
1379
|
+
const fine = settle(binding, role);
|
|
1380
|
+
delete binding.busy;
|
|
1381
|
+
return fine;
|
|
1382
|
+
};
|
|
1383
|
+
const settle = (binding, role) => {
|
|
1384
|
+
switch (role) {
|
|
1385
|
+
case "config":
|
|
1386
|
+
return binding.kind === "value" && configValue(binding.from, binding.to);
|
|
1387
|
+
case "data":
|
|
1388
|
+
return binding.kind === "none" || (binding.kind === "value" && data(binding.from, binding.to));
|
|
1156
1389
|
}
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1390
|
+
};
|
|
1391
|
+
// 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)
|
|
1392
|
+
const configValue = (from, to) => guard(false, () => {
|
|
1393
|
+
const end = valueEnd(from, to);
|
|
1394
|
+
if (end <= from)
|
|
1395
|
+
return false;
|
|
1396
|
+
const t = tokens[from];
|
|
1397
|
+
if (isPunct(t, "(") && b.after[from] === end)
|
|
1398
|
+
return configValue(from + 1, end - 1);
|
|
1399
|
+
if (isPunct(t, "{") && b.after[from] === end) {
|
|
1400
|
+
configs.push(from);
|
|
1401
|
+
return object(from, true);
|
|
1402
|
+
}
|
|
1403
|
+
return t.kind === "ident" && from + 1 === end && use(from, "config");
|
|
1404
|
+
});
|
|
1405
|
+
// an object literal's properties: values that are data, a shorthand's name; functions only as Next's hooks, at the config's top
|
|
1406
|
+
const object = (open, config) => guard(false, () => {
|
|
1407
|
+
const items = listOf(open);
|
|
1408
|
+
if (items === null)
|
|
1409
|
+
return false;
|
|
1410
|
+
const list = [];
|
|
1411
|
+
for (const [x, y] of items) {
|
|
1412
|
+
// `async` or `get` is a key itself when what follows ends one: `{ get: 1 }`, `{ async }`
|
|
1413
|
+
const endsKey = (j) => j === y || isPunct(tokens[j], ":") || isPunct(tokens[j], "(");
|
|
1414
|
+
let k = x;
|
|
1415
|
+
const async = isWord(tokens[k], "async") && !endsKey(k + 1);
|
|
1416
|
+
if (async)
|
|
1417
|
+
k++;
|
|
1418
|
+
// a generator, a spread, a getter or a setter: code the reading doesn't run, or properties from elsewhere
|
|
1419
|
+
if (isPunct(tokens[k], "*") || isPunct(tokens[k], ".") || ((isWord(tokens[k], "get") || isWord(tokens[k], "set")) && !endsKey(k + 1)))
|
|
1420
|
+
return false;
|
|
1421
|
+
const t = tokens[k];
|
|
1422
|
+
let key;
|
|
1423
|
+
let at = k;
|
|
1424
|
+
let next = k + 1;
|
|
1425
|
+
if (t?.kind === "ident" || literal(t))
|
|
1426
|
+
key = t.value;
|
|
1427
|
+
else if (isPunct(t, "[") && literal(tokens[k + 1]) && isPunct(tokens[k + 2], "]")) {
|
|
1428
|
+
key = tokens[k + 1].value;
|
|
1429
|
+
at = k + 1;
|
|
1430
|
+
next = k + 3;
|
|
1431
|
+
}
|
|
1161
1432
|
else
|
|
1162
|
-
|
|
1163
|
-
|
|
1433
|
+
return false;
|
|
1434
|
+
// `__proto__: …` sets the object's prototype
|
|
1435
|
+
if (key === "__proto__")
|
|
1436
|
+
return false;
|
|
1437
|
+
const hook = config && HOOKS.has(key);
|
|
1438
|
+
if (next === y) {
|
|
1439
|
+
if (async || t?.kind !== "ident" || !use(k, "data"))
|
|
1440
|
+
return false;
|
|
1441
|
+
list.push({ key, at, kind: "shorthand", from: k, to: y });
|
|
1442
|
+
}
|
|
1443
|
+
else if (isPunct(tokens[next], ":") && !async) {
|
|
1444
|
+
if (functionOf(tokens, b.after, next + 1, y) !== null) {
|
|
1445
|
+
if (!hook)
|
|
1446
|
+
return false;
|
|
1447
|
+
list.push({ key, at, kind: "hook", from: next + 1, to: y });
|
|
1448
|
+
}
|
|
1449
|
+
else {
|
|
1450
|
+
if (!data(next + 1, y))
|
|
1451
|
+
return false;
|
|
1452
|
+
list.push({ key, at, kind: "value", from: next + 1, to: y });
|
|
1453
|
+
}
|
|
1454
|
+
}
|
|
1455
|
+
else if (isPunct(tokens[next], "(")) {
|
|
1456
|
+
// a method: `async headers() {…}`
|
|
1457
|
+
const body = bodyAfter(tokens, b.after[next]);
|
|
1458
|
+
if (!hook || body === null || b.after[body] !== y)
|
|
1459
|
+
return false;
|
|
1460
|
+
list.push({ key, at, kind: "hook", from: next, to: y });
|
|
1461
|
+
}
|
|
1462
|
+
else
|
|
1463
|
+
return false;
|
|
1164
1464
|
}
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1465
|
+
props.set(open, list);
|
|
1466
|
+
return true;
|
|
1467
|
+
});
|
|
1468
|
+
// data in [from, to), to its end but for types
|
|
1469
|
+
const data = (from, to) => guard(false, () => {
|
|
1470
|
+
const e = expr(from, to);
|
|
1471
|
+
return e > from && (e === to || suffix(e, to));
|
|
1472
|
+
});
|
|
1473
|
+
const expr = (k, to) => guard(-1, () => {
|
|
1474
|
+
let j = binary(k, to);
|
|
1475
|
+
if (j < 0 || j >= to || !isPunct(tokens[j], "?") || isPunct(tokens[j + 1], "?") || isPunct(tokens[j + 1], "."))
|
|
1476
|
+
return j;
|
|
1477
|
+
j = expr(j + 1, to);
|
|
1478
|
+
if (j < 0 || j >= to || !isPunct(tokens[j], ":"))
|
|
1479
|
+
return -1;
|
|
1480
|
+
return expr(j + 1, to);
|
|
1481
|
+
});
|
|
1482
|
+
const binary = (k, to) => {
|
|
1483
|
+
let j = unary(k, to);
|
|
1484
|
+
while (j >= 0 && j < to) {
|
|
1485
|
+
const n = operator(j);
|
|
1486
|
+
if (n <= 0)
|
|
1487
|
+
return n < 0 ? -1 : j;
|
|
1488
|
+
j = unary(j + n, to);
|
|
1174
1489
|
}
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1490
|
+
return j;
|
|
1491
|
+
};
|
|
1492
|
+
// 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
|
|
1493
|
+
const operator = (j) => {
|
|
1494
|
+
const t = tokens[j];
|
|
1495
|
+
const u = tokens[j + 1];
|
|
1496
|
+
const v = tokens[j + 2];
|
|
1497
|
+
if (t?.kind !== "punct")
|
|
1498
|
+
return 0;
|
|
1499
|
+
switch (t.value) {
|
|
1500
|
+
case "=":
|
|
1501
|
+
case "!":
|
|
1502
|
+
return isPunct(u, "=") ? (isPunct(v, "=") ? 3 : 2) : -1;
|
|
1503
|
+
case "&":
|
|
1504
|
+
case "|":
|
|
1505
|
+
case "?":
|
|
1506
|
+
if (!isPunct(u, t.value))
|
|
1507
|
+
return t.value === "?" ? 0 : -1;
|
|
1508
|
+
return isPunct(v, "=") ? -1 : 2;
|
|
1509
|
+
case "+":
|
|
1510
|
+
return isPunct(u, "+") || isPunct(u, "=") ? -1 : 1;
|
|
1511
|
+
}
|
|
1512
|
+
return 0;
|
|
1513
|
+
};
|
|
1514
|
+
const unary = (k, to) => {
|
|
1515
|
+
let j = k;
|
|
1516
|
+
while (j < to && j < k + 16 && (isPunct(tokens[j], "!") || (isPunct(tokens[j], "-") && !isPunct(tokens[j + 1], "-"))))
|
|
1517
|
+
j++;
|
|
1518
|
+
return primary(j, to);
|
|
1519
|
+
};
|
|
1520
|
+
const numberEnd = (k, to) => {
|
|
1521
|
+
let j = k;
|
|
1522
|
+
let point = false;
|
|
1523
|
+
while (j < to && (j === k || tokens[j].nl !== true)) {
|
|
1524
|
+
if (isDigit(tokens[j]))
|
|
1525
|
+
j++;
|
|
1526
|
+
else if (isPunct(tokens[j], ".") && !point && isDigit(tokens[j + 1])) {
|
|
1527
|
+
point = true;
|
|
1528
|
+
j++;
|
|
1529
|
+
}
|
|
1182
1530
|
else
|
|
1183
|
-
|
|
1184
|
-
return;
|
|
1531
|
+
break;
|
|
1185
1532
|
}
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1533
|
+
return j;
|
|
1534
|
+
};
|
|
1535
|
+
// the arguments of the call that opens at `open`, each data
|
|
1536
|
+
const args = (open, to) => {
|
|
1537
|
+
const close = b.after[open];
|
|
1538
|
+
const items = close <= to ? listOf(open) : null;
|
|
1539
|
+
return items !== null && items.every(([x, y]) => data(x, y)) ? close : -1;
|
|
1540
|
+
};
|
|
1541
|
+
const primary = (k, to) => {
|
|
1542
|
+
const t = tokens[k];
|
|
1543
|
+
if (t === undefined || k >= to)
|
|
1544
|
+
return -1;
|
|
1545
|
+
if (t.kind === "string")
|
|
1546
|
+
return literal(t) ? k + 1 : -1;
|
|
1547
|
+
if (t.kind === "regex")
|
|
1548
|
+
return k + 1;
|
|
1549
|
+
if (t.kind === "punct") {
|
|
1550
|
+
if (isDigit(t) || (t.value === "." && isDigit(tokens[k + 1])))
|
|
1551
|
+
return numberEnd(k, to);
|
|
1552
|
+
const close = b.after[k];
|
|
1553
|
+
if (!OPENS.has(t.value) || close > to)
|
|
1554
|
+
return -1;
|
|
1555
|
+
if (t.value === "(")
|
|
1556
|
+
return data(k + 1, close - 1) ? close : -1;
|
|
1557
|
+
if (t.value === "[")
|
|
1558
|
+
return listOf(k)?.every(([x, y]) => data(x, y)) ? close : -1;
|
|
1559
|
+
return object(k, false) ? close : -1;
|
|
1193
1560
|
}
|
|
1194
|
-
const
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1561
|
+
const next = tokens[k + 1];
|
|
1562
|
+
switch (t.value) {
|
|
1563
|
+
case "true":
|
|
1564
|
+
case "false":
|
|
1565
|
+
case "null":
|
|
1566
|
+
case "undefined":
|
|
1567
|
+
case "NaN":
|
|
1568
|
+
case "Infinity":
|
|
1569
|
+
return k + 1;
|
|
1570
|
+
case "process":
|
|
1571
|
+
// `process.env.NAME`, `process.env["NAME"]`, `process.cwd()`
|
|
1572
|
+
if (!isPunct(next, "."))
|
|
1573
|
+
return -1;
|
|
1574
|
+
if (isWord(tokens[k + 2], "env") && isPunct(tokens[k + 3], ".") && tokens[k + 4]?.kind === "ident")
|
|
1575
|
+
return k + 5;
|
|
1576
|
+
if (isWord(tokens[k + 2], "env") && isPunct(tokens[k + 3], "[") && literal(tokens[k + 4]) && isPunct(tokens[k + 5], "]"))
|
|
1577
|
+
return k + 6;
|
|
1578
|
+
return isWord(tokens[k + 2], "cwd") && isPunct(tokens[k + 3], "(") && isPunct(tokens[k + 4], ")") ? k + 5 : -1;
|
|
1579
|
+
case "import":
|
|
1580
|
+
// `import.meta.dirname`, `.filename`, `.url`
|
|
1581
|
+
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;
|
|
1582
|
+
case "require":
|
|
1583
|
+
// `require.resolve("m")`: a path; nothing is loaded
|
|
1584
|
+
return isPunct(next, ".") && isWord(tokens[k + 2], "resolve") && isPunct(tokens[k + 3], "(") && literal(tokens[k + 4]) && isPunct(tokens[k + 5], ")") ? k + 6 : -1;
|
|
1585
|
+
case "new":
|
|
1586
|
+
// `new URL("…")`
|
|
1587
|
+
return isWord(next, "URL") && isPunct(tokens[k + 2], "(") ? args(k + 2, to) : -1;
|
|
1588
|
+
}
|
|
1589
|
+
if ((t.value === "__dirname" || t.value === "__filename") && !bindings.has(t.value))
|
|
1590
|
+
return k + 1;
|
|
1591
|
+
// a name declared with data — not a member of it or a call
|
|
1592
|
+
return !isPunct(next, ".") && !isPunct(next, "(") && use(k, "data") ? k + 1 : -1;
|
|
1593
|
+
};
|
|
1594
|
+
// statements
|
|
1595
|
+
const exportValue = (from, e) => {
|
|
1596
|
+
if (from >= e)
|
|
1597
|
+
return false;
|
|
1598
|
+
exports++;
|
|
1599
|
+
exported = { from, to: e };
|
|
1600
|
+
return true;
|
|
1601
|
+
};
|
|
1602
|
+
// `import type { NextConfig } from "next"`, `import type X from "m"`, `import type * as M from "m"`: types, erased — the only import, since a module
|
|
1603
|
+
// 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)
|
|
1604
|
+
const typeImport = (k, e) => {
|
|
1605
|
+
if (!isWord(tokens[k + 1], "type") || !(e >= k + 5 && isWord(tokens[e - 2], "from") && literal(tokens[e - 1])))
|
|
1606
|
+
return false;
|
|
1607
|
+
const stop = e - 2;
|
|
1608
|
+
const j = k + 2;
|
|
1609
|
+
if (isPunct(tokens[j], "{"))
|
|
1610
|
+
return b.after[j] === stop;
|
|
1611
|
+
if (isPunct(tokens[j], "*"))
|
|
1612
|
+
return isWord(tokens[j + 1], "as") && tokens[j + 2]?.kind === "ident" && j + 3 === stop;
|
|
1613
|
+
return tokens[j]?.kind === "ident" && !isWord(tokens[j], "from") && j + 1 === stop;
|
|
1614
|
+
};
|
|
1615
|
+
const exportStatement = (k, e) => {
|
|
1616
|
+
const t = tokens[k + 1];
|
|
1617
|
+
if (isWord(t, "default"))
|
|
1618
|
+
return exportValue(k + 2, e);
|
|
1619
|
+
// `export type { T }`, `export type { T } from "m"`: types, erased
|
|
1620
|
+
if (isWord(t, "type") && isPunct(tokens[k + 2], "{")) {
|
|
1621
|
+
const close = b.after[k + 2];
|
|
1622
|
+
return close === e || (close + 2 === e && isWord(tokens[close], "from") && literal(tokens[close + 1]));
|
|
1623
|
+
}
|
|
1624
|
+
// `export { config as default }`: the one export
|
|
1625
|
+
if (!isPunct(t, "{") || b.after[k + 1] !== e)
|
|
1626
|
+
return false;
|
|
1627
|
+
const items = listOf(k + 1);
|
|
1628
|
+
if (items === null || items.length !== 1)
|
|
1629
|
+
return false;
|
|
1630
|
+
const [x, y] = items[0];
|
|
1631
|
+
const named = isWord(tokens[x + 2], "default") || (literal(tokens[x + 2]) && tokens[x + 2].value === "default");
|
|
1632
|
+
if (!(y === x + 3 && tokens[x].kind === "ident" && isWord(tokens[x + 1], "as") && named))
|
|
1633
|
+
return false;
|
|
1634
|
+
exports++;
|
|
1635
|
+
exported = { name: x };
|
|
1636
|
+
return true;
|
|
1637
|
+
};
|
|
1638
|
+
const declaration = (j, e) => {
|
|
1639
|
+
if (j >= e)
|
|
1640
|
+
return false;
|
|
1641
|
+
for (const [x, y] of splitAt(j, e)) {
|
|
1642
|
+
if (x === y)
|
|
1643
|
+
return false;
|
|
1644
|
+
const t = tokens[x];
|
|
1645
|
+
if (t.kind === "ident") {
|
|
1646
|
+
let v = x + 1;
|
|
1647
|
+
// a type: `const nextConfig: NextConfig = …`
|
|
1648
|
+
if (isPunct(tokens[v], ":")) {
|
|
1649
|
+
v = simpleType(v + 1, y);
|
|
1650
|
+
if (v < 0)
|
|
1651
|
+
return false;
|
|
1210
1652
|
}
|
|
1653
|
+
if (v === y) {
|
|
1654
|
+
if (!bind(x, { kind: "none" }))
|
|
1655
|
+
return false;
|
|
1656
|
+
continue;
|
|
1657
|
+
}
|
|
1658
|
+
if (!isPunct(tokens[v], "=") || isPunct(tokens[v + 1], "=") || v + 1 >= y || !bind(x, { kind: "value", from: v + 1, to: y }))
|
|
1659
|
+
return false;
|
|
1211
1660
|
}
|
|
1661
|
+
else
|
|
1662
|
+
return false;
|
|
1212
1663
|
}
|
|
1213
|
-
|
|
1214
|
-
|
|
1664
|
+
return true;
|
|
1665
|
+
};
|
|
1666
|
+
const statement = (k, e) => {
|
|
1667
|
+
const t = tokens[k];
|
|
1668
|
+
// a directive: "use strict"
|
|
1669
|
+
if (e === k + 1 && literal(t))
|
|
1670
|
+
return true;
|
|
1671
|
+
if (t.kind !== "ident")
|
|
1672
|
+
return false;
|
|
1673
|
+
switch (t.value) {
|
|
1674
|
+
case "import":
|
|
1675
|
+
return typeImport(k, e);
|
|
1676
|
+
case "export":
|
|
1677
|
+
return exportStatement(k, e);
|
|
1678
|
+
case "const":
|
|
1679
|
+
case "let":
|
|
1680
|
+
case "var":
|
|
1681
|
+
return declaration(k + 1, e);
|
|
1682
|
+
case "module":
|
|
1683
|
+
return isPunct(tokens[k + 1], ".") && isWord(tokens[k + 2], "exports") && isPunct(tokens[k + 3], "=") && !isPunct(tokens[k + 4], "=") && exportValue(k + 4, e);
|
|
1684
|
+
}
|
|
1685
|
+
return false;
|
|
1215
1686
|
};
|
|
1216
|
-
for (
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1687
|
+
for (let k = 0; k < tokens.length;) {
|
|
1688
|
+
if (isPunct(tokens[k], ";")) {
|
|
1689
|
+
k++;
|
|
1690
|
+
continue;
|
|
1691
|
+
}
|
|
1692
|
+
const e = statementEnd(k);
|
|
1693
|
+
if (e === k || !statement(k, e))
|
|
1694
|
+
return null;
|
|
1695
|
+
k = e;
|
|
1696
|
+
}
|
|
1697
|
+
if (exports > 1)
|
|
1698
|
+
return null;
|
|
1699
|
+
const out = exported;
|
|
1700
|
+
if (out !== null && !("name" in out ? use(out.name, "config") : configValue(out.from, out.to)))
|
|
1221
1701
|
return null;
|
|
1222
|
-
//
|
|
1702
|
+
// a name the load doesn't use still has its value computed: data, or a module loaded
|
|
1703
|
+
for (const binding of bindings.values()) {
|
|
1704
|
+
if (binding.uses > 0 || binding.kind !== "value")
|
|
1705
|
+
continue;
|
|
1706
|
+
binding.role = "data";
|
|
1707
|
+
binding.uses++;
|
|
1708
|
+
if (!data(binding.from, binding.to))
|
|
1709
|
+
return null;
|
|
1710
|
+
}
|
|
1711
|
+
return { configs, props };
|
|
1712
|
+
}
|
|
1713
|
+
/** next.config's `pageExtensions`, `basePath` and `output`, read only when
|
|
1714
|
+
the file is written in a declarative subset of JavaScript, where no code
|
|
1715
|
+
runs while Next loads it — neither the file's nor a module's:
|
|
1716
|
+
|
|
1717
|
+
- statements: type imports (`import type { NextConfig } from "next"`),
|
|
1718
|
+
declarations, and one export of the config object itself —
|
|
1719
|
+
`module.exports = {…}`, `export default nextConfig` or `export {
|
|
1720
|
+
config as default }`;
|
|
1721
|
+
- values: literals, and arrays and objects of them, `process.env.NAME`,
|
|
1722
|
+
`process.cwd()`, `__dirname`, `import.meta.dirname`, `new URL(…)`,
|
|
1723
|
+
`require.resolve("…")`, names declared so; `!`, `+`, comparisons,
|
|
1724
|
+
`&&`, `||`, `??` and `? :` over them; a type after `as` or
|
|
1725
|
+
`satisfies`;
|
|
1726
|
+
- functions only as Next's hooks (`webpack`, `headers`, `redirects`,
|
|
1727
|
+
`rewrites`, `generateBuildId`, `exportPathMap`) at the top of the
|
|
1728
|
+
exported object: Next calls them once it has loaded the config.
|
|
1729
|
+
|
|
1730
|
+
Anything else makes it null: a module loaded in any way (`import "…"`,
|
|
1731
|
+
`import x from "…"`, `require(…)`) — its code runs, and which module a
|
|
1732
|
+
name loads its spelling doesn't show (Node's self-reference, a
|
|
1733
|
+
next.config.ts's tsconfig `paths`); so a plugin's call around the
|
|
1734
|
+
config (`withMDX(nextConfig)`) and a `path` helper too (block 348). And
|
|
1735
|
+
any other statement, a function or class of the file, a getter, a
|
|
1736
|
+
spread, a member of a value, a call, a name bound twice or over one the
|
|
1737
|
+
subset reads (`require`, `process`), the config's name used twice. Each
|
|
1738
|
+
setting: a literal, once, in that object — `pageExtensions` or
|
|
1739
|
+
`basePath` named anywhere else in the file makes it null, and an
|
|
1740
|
+
`output` the reading can't take (an expression, a second one, one a
|
|
1741
|
+
hook's code writes) makes only `output` null. `live.test.ts` holds the
|
|
1742
|
+
reading to Next's own `loadConfig` over generated configs.
|
|
1743
|
+
|
|
1744
|
+
What the reading trusts: Next calls the hooks only after loading, and a
|
|
1745
|
+
hook leaves the settings it loaded alone. */
|
|
1746
|
+
export function nextConfigOf(config) {
|
|
1747
|
+
if (config === null)
|
|
1748
|
+
return { pageExtensions: PAGE_EXTENSIONS, basePath: "" };
|
|
1749
|
+
// what JavaScript reads apart from the tokens: a hashbang line, CommonJS's `<!--` and `-->` comments — `-->` may follow a `/* … */` on its line — anywhere
|
|
1750
|
+
if (config.startsWith("#!") || /<!--|-->/.test(config))
|
|
1751
|
+
return null;
|
|
1752
|
+
// every line terminator JavaScript knows ends a `//` comment: `\r`, U+2028 and U+2029 too
|
|
1753
|
+
config = config.replace(/\r\n?|[\u2028\u2029]/g, "\n");
|
|
1754
|
+
const hidden = [];
|
|
1755
|
+
const ambiguous = [];
|
|
1756
|
+
const tokens = tokenize(config, hidden, ambiguous, true);
|
|
1757
|
+
const b = bracketsOf(tokens);
|
|
1758
|
+
if (!balanced(tokens, b) || !slashesAgree(tokens, b))
|
|
1759
|
+
return null;
|
|
1760
|
+
// the code of each template's `${…}` — a hook's only, since data has no such template — read for the names it mentions
|
|
1761
|
+
const all = [...tokens];
|
|
1762
|
+
for (let i = 0; i < hidden.length; i++) {
|
|
1763
|
+
if (i >= 1000)
|
|
1764
|
+
return null;
|
|
1765
|
+
const part = tokenize(`;\n${hidden[i]}\n;`, hidden, ambiguous, true);
|
|
1766
|
+
if (!slashesAgree(part, bracketsOf(part)))
|
|
1767
|
+
return null;
|
|
1768
|
+
for (const t of part)
|
|
1769
|
+
all.push(t);
|
|
1770
|
+
}
|
|
1771
|
+
// a `/` right after `}`, in the file or a template's code, may start a regex that holds a brace
|
|
1772
|
+
if (ambiguous.length > 0)
|
|
1773
|
+
return null;
|
|
1774
|
+
// an identifier with an escape in its name, a backslash outside a string, a string a line break cut off
|
|
1775
|
+
if (all.some((t) => isPunct(t, "\\") || (t.kind === "ident" && t.escaped === true) || (t.kind === "string" && t.unclosed === true)))
|
|
1776
|
+
return null;
|
|
1777
|
+
const subset = declarative(tokens, b);
|
|
1778
|
+
if (subset === null)
|
|
1779
|
+
return null;
|
|
1780
|
+
// each setting: a literal, once, in the config object
|
|
1223
1781
|
const settings = {};
|
|
1224
1782
|
const accepted = new Set();
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
for (
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1783
|
+
// `output` never makes the config unreadable: only its value is in doubt — undefined while unset, null when it can't be read
|
|
1784
|
+
let output;
|
|
1785
|
+
for (const open of subset.configs) {
|
|
1786
|
+
for (const p of subset.props.get(open) ?? []) {
|
|
1787
|
+
// what Next takes before the keys: `then` — it awaits the config — and `default`, which a next.config.ts transpiled to CommonJS is read from
|
|
1788
|
+
if (p.key === "then" || p.key === "default")
|
|
1789
|
+
return null;
|
|
1790
|
+
const value = tokens[p.from];
|
|
1791
|
+
const single = p.kind === "value" && p.to === p.from + 1 && literal(value);
|
|
1792
|
+
if (p.key === "basePath") {
|
|
1793
|
+
// a path from the root, without a trailing slash — anything else Next refuses
|
|
1794
|
+
if (settings.basePath !== undefined || !single || (value.value !== "" && !/^\/[^?#\s]*[^/?#\s]$/.test(value.value)))
|
|
1232
1795
|
return null;
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1796
|
+
settings.basePath = value.value;
|
|
1797
|
+
accepted.add(p.at);
|
|
1798
|
+
}
|
|
1799
|
+
else if (p.key === "pageExtensions") {
|
|
1800
|
+
if (settings.pageExtensions !== undefined || p.kind !== "value" || !isPunct(value, "[") || b.after[p.from] !== p.to)
|
|
1801
|
+
return null;
|
|
1802
|
+
const list = [];
|
|
1803
|
+
for (const [x, y] of itemsOf(tokens, b.after, p.from)) {
|
|
1804
|
+
if (x === y)
|
|
1805
|
+
continue;
|
|
1806
|
+
if (y !== x + 1 || !literal(tokens[x]))
|
|
1240
1807
|
return null;
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1808
|
+
list.push(tokens[x].value);
|
|
1809
|
+
}
|
|
1810
|
+
settings.pageExtensions = list;
|
|
1811
|
+
accepted.add(p.at);
|
|
1812
|
+
}
|
|
1813
|
+
else if (p.key === "output")
|
|
1814
|
+
output = output === undefined && single ? value.value : null;
|
|
1815
|
+
}
|
|
1816
|
+
}
|
|
1817
|
+
// a setting's name anywhere else — a nested object, a hook's code, a string — or inside a longer string (JSON the code parses)
|
|
1818
|
+
if (all.some((t, k) => (t.kind === "ident" || t.kind === "string") && SETTINGS.includes(t.value) && !accepted.has(k)))
|
|
1819
|
+
return null;
|
|
1820
|
+
if (all.some((t) => t.kind === "string" && !SETTINGS.includes(t.value) && SETTINGS.some((s) => t.value.includes(s))))
|
|
1821
|
+
return null;
|
|
1822
|
+
// 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 …)`:
|
|
1823
|
+
// Next runs it after loading, and what it writes the reading can't place
|
|
1824
|
+
const ball = bracketsOf(all);
|
|
1825
|
+
for (let k = 0; k < all.length; k++) {
|
|
1826
|
+
const t = all[k];
|
|
1827
|
+
if ((t.kind === "ident" && COPIES.has(t.value)) || (t.kind === "string" && t.value === "__proto__"))
|
|
1828
|
+
return null;
|
|
1829
|
+
if (!isPunct(t, "["))
|
|
1830
|
+
continue;
|
|
1831
|
+
// unless a lone string or digits name it: `rules[0] = …`
|
|
1832
|
+
const close = ball.after[k] - 1;
|
|
1833
|
+
if (close === k + 2 && literal(all[k + 1]))
|
|
1834
|
+
continue;
|
|
1835
|
+
let digits = k + 1;
|
|
1836
|
+
while (digits < close && isDigit(all[digits]))
|
|
1837
|
+
digits++;
|
|
1838
|
+
if (digits === close && close > k + 1)
|
|
1839
|
+
continue;
|
|
1840
|
+
const prev = all[k - 1];
|
|
1841
|
+
const key = (isPunct(prev, "{") || isPunct(prev, ",") || isPunct(prev, "*") || (prev?.kind === "ident" && KEY_PREFIX.has(prev.value))) && (isPunct(all[close + 1], ":") || isPunct(all[close + 1], "("));
|
|
1842
|
+
const head = ball.parent[k];
|
|
1843
|
+
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"));
|
|
1844
|
+
const member = (prev?.kind === "ident" || isPunct(prev, ")") || isPunct(prev, "]")) && (assigned(all, close) || loop);
|
|
1845
|
+
if (key || member)
|
|
1846
|
+
return null;
|
|
1847
|
+
}
|
|
1848
|
+
// and `output` a hook's code writes — `config.output = …`, `config["output"] = …`, in a pattern or a loop's head — or deletes
|
|
1849
|
+
const patterns = new Int32Array(all.length + 1);
|
|
1850
|
+
for (let k = 0; k < all.length; k++) {
|
|
1851
|
+
const after = ball.after[k];
|
|
1852
|
+
if ((isPunct(all[k], "{") || isPunct(all[k], "[")) && !DECLARE.has(all[k - 1]?.value ?? "") && isPunct(all[after], "=") && !isPunct(all[after + 1], "=")) {
|
|
1853
|
+
patterns[k]++;
|
|
1854
|
+
patterns[after]--;
|
|
1855
|
+
}
|
|
1856
|
+
}
|
|
1857
|
+
for (let k = 0, inPattern = 0; k < all.length; k++) {
|
|
1858
|
+
inPattern += patterns[k];
|
|
1859
|
+
const t = all[k];
|
|
1860
|
+
const dotted = t.kind === "ident" && t.value === "output" && isPunct(all[k - 1], ".");
|
|
1861
|
+
const bracketed = t.kind === "string" && t.value === "output" && isPunct(all[k - 1], "[") && isPunct(all[k + 1], "]");
|
|
1862
|
+
if (!dotted && !bracketed)
|
|
1863
|
+
continue;
|
|
1864
|
+
// parentheses around the target, TypeScript's `!` after it: `(config.output) = …`, `config.output! = …`
|
|
1865
|
+
let end = bracketed ? k + 1 : k;
|
|
1866
|
+
while (isPunct(all[end + 1], ")") || isPunct(all[end + 1], "!"))
|
|
1867
|
+
end++;
|
|
1868
|
+
const head = ball.parent[k - 1];
|
|
1869
|
+
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"));
|
|
1870
|
+
if (changes(all, end) || inPattern > 0 || loop)
|
|
1871
|
+
output = null;
|
|
1872
|
+
}
|
|
1873
|
+
for (let k = 0; k < all.length; k++) {
|
|
1874
|
+
if (!isWord(all[k], "delete") || isPunct(all[k - 1], "."))
|
|
1875
|
+
continue;
|
|
1876
|
+
const to = initializerEnd(all, ball.after, k + 1);
|
|
1877
|
+
for (let j = k + 1; j < to; j++)
|
|
1878
|
+
if ((isWord(all[j], "output") && isPunct(all[j - 1], ".")) || (all[j].kind === "string" && all[j].value === "output"))
|
|
1879
|
+
output = null;
|
|
1880
|
+
}
|
|
1881
|
+
const read = { pageExtensions: settings.pageExtensions ?? PAGE_EXTENSIONS, basePath: settings.basePath ?? "" };
|
|
1882
|
+
if (output !== undefined)
|
|
1883
|
+
read.output = output;
|
|
1884
|
+
if (all.some((t) => (t.kind === "ident" || t.kind === "string") && t.value === "i18n"))
|
|
1885
|
+
read.i18n = true;
|
|
1886
|
+
return read;
|
|
1887
|
+
}
|
|
1888
|
+
/** Whether the tokens from `from` up to `to` are a value Next's build
|
|
1889
|
+
reads out of a module's `config` (extractValue): a string, a number,
|
|
1890
|
+
true, false, null or undefined, or an array or object of them — no
|
|
1891
|
+
spread, hole, computed key, shorthand, method or expression. */
|
|
1892
|
+
function extractable(tokens, after, from, to) {
|
|
1893
|
+
if (from >= to)
|
|
1894
|
+
return false;
|
|
1895
|
+
const t = tokens[from];
|
|
1896
|
+
if (to === from + 1 && (literal(t) || isWord(t, "true") || isWord(t, "false") || isWord(t, "null") || isWord(t, "undefined")))
|
|
1897
|
+
return true;
|
|
1898
|
+
// digits, and one decimal point: `12`, `1.5`
|
|
1899
|
+
if (/^\d$/.test(t.kind === "punct" ? t.value : "")) {
|
|
1900
|
+
let points = 0;
|
|
1901
|
+
for (let k = from; k < to; k++) {
|
|
1902
|
+
const u = tokens[k];
|
|
1903
|
+
if (u.kind !== "punct" || !/^[\d.]$/.test(u.value) || (u.value === "." && ++points > 1))
|
|
1904
|
+
return false;
|
|
1905
|
+
}
|
|
1906
|
+
return true;
|
|
1907
|
+
}
|
|
1908
|
+
if ((!isPunct(t, "[") && !isPunct(t, "{")) || after[from] !== to)
|
|
1909
|
+
return false;
|
|
1910
|
+
return itemsOf(tokens, after, from).every(([a, z], i, all) => {
|
|
1911
|
+
// a trailing comma leaves nothing after it
|
|
1912
|
+
if (a === z)
|
|
1913
|
+
return i === all.length - 1 && all.length > 1;
|
|
1914
|
+
if (isSpread(tokens, a))
|
|
1915
|
+
return false;
|
|
1916
|
+
if (t.value === "[")
|
|
1917
|
+
return extractable(tokens, after, a, z);
|
|
1918
|
+
const key = tokens[a];
|
|
1919
|
+
return (key.kind === "ident" || literal(key)) && isPunct(tokens[a + 1], ":") && extractable(tokens, after, a + 2, z);
|
|
1920
|
+
});
|
|
1921
|
+
}
|
|
1922
|
+
/** The items of the array or object that opens at `open`, as token ranges
|
|
1923
|
+
between its top-level commas. */
|
|
1924
|
+
function itemsOf(tokens, after, open) {
|
|
1925
|
+
const close = after[open] - 1;
|
|
1926
|
+
const items = [];
|
|
1927
|
+
let start = open + 1;
|
|
1928
|
+
for (let k = start; k <= close; k++) {
|
|
1929
|
+
const u = tokens[k];
|
|
1930
|
+
if (k < close && u.kind === "punct" && "([{".includes(u.value))
|
|
1931
|
+
k = after[k] - 1;
|
|
1932
|
+
else if (k === close || isPunct(u, ",")) {
|
|
1933
|
+
items.push([start, k]);
|
|
1934
|
+
start = k + 1;
|
|
1935
|
+
}
|
|
1936
|
+
}
|
|
1937
|
+
return items;
|
|
1938
|
+
}
|
|
1939
|
+
/** The matcher of a middleware file, read as Next's build reads it: only
|
|
1940
|
+
`export const config = { … }` (a type or `satisfies` allowed), every
|
|
1941
|
+
value in it a literal (extractable), `matcher` a string or an array of
|
|
1942
|
+
strings and `{ source }` objects. Anything else Next can't read either
|
|
1943
|
+
and runs the middleware on every path — or reads what doctor can't. */
|
|
1944
|
+
export function middlewareMatcher(text) {
|
|
1945
|
+
const tokens = tokenize(text);
|
|
1946
|
+
const b = bracketsOf(tokens);
|
|
1947
|
+
let config = null;
|
|
1948
|
+
let declared = -1;
|
|
1949
|
+
for (let i = 0; i < tokens.length; i++) {
|
|
1950
|
+
if (!isWord(tokens[i], "export") || isPunct(tokens[i - 1], ".") || !isWord(tokens[i + 1], "const") || !isWord(tokens[i + 2], "config"))
|
|
1951
|
+
continue;
|
|
1952
|
+
if (config !== null)
|
|
1953
|
+
return "unread";
|
|
1954
|
+
declared = i + 2;
|
|
1955
|
+
let eq = i + 3;
|
|
1956
|
+
if (isPunct(tokens[eq], ":"))
|
|
1957
|
+
eq = typeEnd(tokens, b.after, eq + 1, tokens.length);
|
|
1958
|
+
if (eq < 0 || !isPunct(tokens[eq], "=") || isPunct(tokens[eq + 1], "=") || !isPunct(tokens[eq + 1], "{"))
|
|
1959
|
+
return "unread";
|
|
1960
|
+
const open = eq + 1;
|
|
1961
|
+
const end = b.after[open];
|
|
1962
|
+
// the statement ends there — `;`, `,`, a new line, the file — or goes on with `satisfies T`, which Next reads through
|
|
1963
|
+
const tail = tokens[end];
|
|
1964
|
+
if (tail && !isPunct(tail, ";") && !isPunct(tail, ",") && !isWord(tail, "satisfies") && !(tail.nl === true && tail.kind === "ident"))
|
|
1965
|
+
return "unread";
|
|
1966
|
+
if (!extractable(tokens, b.after, open, end))
|
|
1967
|
+
return "unread";
|
|
1968
|
+
config = "none";
|
|
1969
|
+
for (const [a, z] of itemsOf(tokens, b.after, open)) {
|
|
1970
|
+
if (a === z || tokens[a].value !== "matcher")
|
|
1971
|
+
continue;
|
|
1972
|
+
if (config !== "none")
|
|
1973
|
+
return "unread";
|
|
1974
|
+
const v = a + 2;
|
|
1975
|
+
if (z === v + 1 && literal(tokens[v]))
|
|
1976
|
+
config = [tokens[v].value];
|
|
1977
|
+
else if (isPunct(tokens[v], "[")) {
|
|
1978
|
+
const sources = [];
|
|
1979
|
+
for (const [c, d] of itemsOf(tokens, b.after, v)) {
|
|
1980
|
+
if (c === d)
|
|
1981
|
+
continue;
|
|
1982
|
+
if (d === c + 1 && literal(tokens[c]))
|
|
1983
|
+
sources.push(tokens[c].value);
|
|
1984
|
+
else if (isPunct(tokens[c], "{")) {
|
|
1985
|
+
const source = itemsOf(tokens, b.after, c).filter(([e]) => tokens[e]?.value === "source" && isPunct(tokens[e + 1], ":"));
|
|
1986
|
+
if (source.length !== 1 || source[0][1] !== source[0][0] + 3 || !literal(tokens[source[0][0] + 2]))
|
|
1987
|
+
return "unread";
|
|
1988
|
+
sources.push(tokens[source[0][0] + 2].value);
|
|
1258
1989
|
}
|
|
1259
|
-
|
|
1990
|
+
else
|
|
1991
|
+
return "unread";
|
|
1260
1992
|
}
|
|
1993
|
+
config = sources;
|
|
1261
1994
|
}
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
k = b.after[k] - 1;
|
|
1265
|
-
else if (isPunct(t, ","))
|
|
1266
|
-
property = true;
|
|
1995
|
+
else
|
|
1996
|
+
return "unread";
|
|
1267
1997
|
}
|
|
1998
|
+
i = end - 1;
|
|
1268
1999
|
}
|
|
1269
|
-
|
|
2000
|
+
// any other export that may be `config` — `export { config }`, `export let config`, a later declarator, `export *`, CommonJS — Next's reading doesn't see
|
|
2001
|
+
const rest = declared === -1 ? tokens : tokens.map((t, k) => (k === declared ? { ...t, value: "" } : t));
|
|
2002
|
+
if (setsExport(rest, b.after, "config", () => false))
|
|
2003
|
+
return "unread";
|
|
2004
|
+
return config ?? "none";
|
|
2005
|
+
}
|
|
2006
|
+
/** The literal a declarator at `at` gives its name as Next's build reads
|
|
2007
|
+
it (extractExportedConstValue): `name = "…"`, a type allowed before the
|
|
2008
|
+
`=` and `satisfies T` after the literal — not `as T`, parentheses or
|
|
2009
|
+
anything else, which Next leaves unread. Null for any other value. */
|
|
2010
|
+
function nextLiteral(tokens, after, at) {
|
|
2011
|
+
const eq = isPunct(tokens[at + 1], ":") ? typeEnd(tokens, after, at + 2, tokens.length) : at + 1;
|
|
2012
|
+
if (eq < 0 || !isPunct(tokens[eq], "=") || isPunct(tokens[eq + 1], "="))
|
|
2013
|
+
return null;
|
|
2014
|
+
const v = tokens[eq + 1];
|
|
2015
|
+
if (!literal(v))
|
|
1270
2016
|
return null;
|
|
1271
|
-
|
|
2017
|
+
const end = initializerEnd(tokens, after, eq + 1);
|
|
2018
|
+
let j = eq + 2;
|
|
2019
|
+
if (isWord(tokens[j], "satisfies"))
|
|
2020
|
+
j = typeEnd(tokens, after, j + 1, end);
|
|
2021
|
+
return j === end ? v : null;
|
|
2022
|
+
}
|
|
2023
|
+
/** The runtime a Pages Router API route runs on, as Next's build reads
|
|
2024
|
+
it (getPagesPageStaticInfo): `export const runtime`, and without it
|
|
2025
|
+
`runtime` in `export const config` — each only as a literal. Undefined
|
|
2026
|
+
for Node's: neither is set, or "nodejs" is written out. The literal
|
|
2027
|
+
when the file writes out another — "edge" or "experimental-edge", where
|
|
2028
|
+
the route gets no Node request and response to write the answer to
|
|
2029
|
+
(any other value fails the build). Null when the file may set one where
|
|
2030
|
+
doctor can't read it: an export it can't list, a value that isn't one
|
|
2031
|
+
plain literal, a spread or a key the code computes in the config. Next
|
|
2032
|
+
leaves what it can't read on Node's, but reads some forms doctor
|
|
2033
|
+
doesn't (a regex elsewhere in the config): doctor calls Node's only
|
|
2034
|
+
what it sees to be. */
|
|
2035
|
+
function pagesRuntime(tokens, b) {
|
|
2036
|
+
// `export const runtime = "…"`: it wins over the config's — when Next reads it; one it can't leaves the config's
|
|
2037
|
+
const own = [];
|
|
2038
|
+
const plain = (_, at) => {
|
|
2039
|
+
const t = nextLiteral(tokens, b.after, at);
|
|
2040
|
+
if (t === null)
|
|
2041
|
+
return false;
|
|
2042
|
+
own.push(t.value);
|
|
2043
|
+
return true;
|
|
2044
|
+
};
|
|
2045
|
+
// declared twice, it's the first to Next and the file fails to build: nothing doctor can call Node's
|
|
2046
|
+
if (setsExport(tokens, b.after, "runtime", plain) || own.length > 1)
|
|
2047
|
+
return null;
|
|
2048
|
+
if (own.length === 1)
|
|
2049
|
+
return own[0] === "nodejs" ? undefined : own[0];
|
|
2050
|
+
let runtime = undefined;
|
|
2051
|
+
let declared = -1;
|
|
2052
|
+
for (let i = 0; i < tokens.length; i++) {
|
|
2053
|
+
if (!isWord(tokens[i], "export") || isPunct(tokens[i - 1], ".") || !isWord(tokens[i + 1], "const") || !isWord(tokens[i + 2], "config"))
|
|
2054
|
+
continue;
|
|
2055
|
+
if (declared !== -1)
|
|
2056
|
+
return null;
|
|
2057
|
+
declared = i + 2;
|
|
2058
|
+
let eq = i + 3;
|
|
2059
|
+
if (isPunct(tokens[eq], ":"))
|
|
2060
|
+
eq = typeEnd(tokens, b.after, eq + 1, tokens.length);
|
|
2061
|
+
if (eq < 0 || !isPunct(tokens[eq], "=") || isPunct(tokens[eq + 1], "=") || !isPunct(tokens[eq + 1], "{"))
|
|
2062
|
+
return null;
|
|
2063
|
+
const open = eq + 1;
|
|
2064
|
+
const end = b.after[open];
|
|
2065
|
+
// the statement ends there, or goes on with `satisfies T`
|
|
2066
|
+
const tail = tokens[end];
|
|
2067
|
+
if (tail && !isPunct(tail, ";") && !isPunct(tail, ",") && !isWord(tail, "satisfies") && !(tail.nl === true && tail.kind === "ident"))
|
|
2068
|
+
return null;
|
|
2069
|
+
for (const [a, z] of itemsOf(tokens, b.after, open)) {
|
|
2070
|
+
if (a === z)
|
|
2071
|
+
continue;
|
|
2072
|
+
if (isSpread(tokens, a))
|
|
2073
|
+
return null;
|
|
2074
|
+
const { name, next } = propertyKey(tokens, a);
|
|
2075
|
+
if (name === null)
|
|
2076
|
+
return null;
|
|
2077
|
+
// the last one wins, as in the object; a shorthand, a method or a getter is code
|
|
2078
|
+
if (name === "runtime")
|
|
2079
|
+
runtime = isPunct(tokens[next], ":") && z === next + 2 && literal(tokens[next + 1]) ? tokens[next + 1].value : null;
|
|
2080
|
+
}
|
|
2081
|
+
i = end - 1;
|
|
2082
|
+
}
|
|
2083
|
+
// any other export that may be `config`, as in middlewareMatcher
|
|
2084
|
+
const rest = declared === -1 ? tokens : tokens.map((t, k) => (k === declared ? { ...t, value: "" } : t));
|
|
2085
|
+
if (setsExport(rest, b.after, "config", () => false))
|
|
2086
|
+
return null;
|
|
2087
|
+
return runtime === "nodejs" ? undefined : runtime;
|
|
2088
|
+
}
|
|
2089
|
+
/** Whether `c` is one of the characters — never for the end of the string. */
|
|
2090
|
+
const oneOf = (chars, c) => c !== undefined && c.length === 1 && chars.includes(c);
|
|
2091
|
+
// what a path may hold for doctor to place it among a matcher's sources: nothing a URL would encode
|
|
2092
|
+
const PLAIN_PATH = /^\/[A-Za-z0-9/_.~-]*$/;
|
|
2093
|
+
/** Whether a matcher source may take the path `at` (without the basePath,
|
|
2094
|
+
which Next puts in front of both). False only when it surely can't: the
|
|
2095
|
+
literal the source starts with isn't how the path starts, or right
|
|
2096
|
+
after it comes a group — path-to-regexp's `(…)`, kept verbatim in the
|
|
2097
|
+
regex Next builds — that matches no start of the rest, in either case.
|
|
2098
|
+
Everything else may. */
|
|
2099
|
+
export function sourceMayTake(source, at) {
|
|
2100
|
+
if (!source.startsWith("/") || !PLAIN_PATH.test(at))
|
|
2101
|
+
return true;
|
|
2102
|
+
let i = 0;
|
|
2103
|
+
while (i < source.length && !":(){}*+?\\".includes(source[i]))
|
|
2104
|
+
i++;
|
|
2105
|
+
// a `/` or `.` right before a param or a group is its prefix, which a modifier makes optional: `/blog/:path*` takes `/blog`
|
|
2106
|
+
const required = oneOf(":(", source[i]) && oneOf("/.", source[i - 1]) ? i - 1 : i;
|
|
2107
|
+
const start = source.slice(0, required).toLowerCase();
|
|
2108
|
+
// the tab may ask with a trailing slash that Next takes off
|
|
2109
|
+
const paths = [at, at === "/" ? at : `${at}/`].map((p) => p.toLowerCase());
|
|
2110
|
+
if (!paths.some((p) => p.startsWith(start)))
|
|
2111
|
+
return false;
|
|
2112
|
+
if (source[i] !== "(" || source[i + 1] === "?")
|
|
2113
|
+
return true;
|
|
2114
|
+
// the group as path-to-regexp reads it: to its balanced `)`, a backslash taking the next character along
|
|
2115
|
+
let depth = 1;
|
|
2116
|
+
let j = i + 1;
|
|
2117
|
+
for (; j < source.length && depth > 0; j++) {
|
|
2118
|
+
const c = source[j];
|
|
2119
|
+
if (c === "\\")
|
|
2120
|
+
j++;
|
|
2121
|
+
else if (c === ")")
|
|
2122
|
+
depth--;
|
|
2123
|
+
// a capturing group inside is refused
|
|
2124
|
+
else if (c === "(" && source[j + 1] !== "?")
|
|
2125
|
+
return true;
|
|
2126
|
+
else if (c === "(")
|
|
2127
|
+
depth++;
|
|
2128
|
+
}
|
|
2129
|
+
// a modifier may skip the group; a backreference numbers groups differently inside Next's regex
|
|
2130
|
+
if (depth !== 0 || oneOf("?*+", source[j]) || /\\\d/.test(source.slice(i, j)))
|
|
2131
|
+
return true;
|
|
2132
|
+
// without a modifier the group's prefix is required: the group starts right after the whole literal
|
|
2133
|
+
const literalStart = source.slice(0, i).toLowerCase();
|
|
2134
|
+
const group = source.slice(i + 1, j - 1);
|
|
2135
|
+
try {
|
|
2136
|
+
for (const flags of ["y", "iy"]) {
|
|
2137
|
+
const re = new RegExp(group, flags);
|
|
2138
|
+
for (const [n, p] of paths.entries()) {
|
|
2139
|
+
if (!p.startsWith(literalStart))
|
|
2140
|
+
continue;
|
|
2141
|
+
// matched as the tab asks it, case and all
|
|
2142
|
+
re.lastIndex = literalStart.length;
|
|
2143
|
+
if (re.test(n === 0 ? at : `${at}/`))
|
|
2144
|
+
return true;
|
|
2145
|
+
}
|
|
2146
|
+
}
|
|
2147
|
+
}
|
|
2148
|
+
catch {
|
|
2149
|
+
return true;
|
|
2150
|
+
}
|
|
2151
|
+
return false;
|
|
2152
|
+
}
|
|
2153
|
+
/** Why a middleware file may run on `at` (a path without the basePath), or null when it surely doesn't. */
|
|
2154
|
+
function middlewareOn(m, at, i18n) {
|
|
2155
|
+
if (m.matcher === "none" || m.matcher === "unread")
|
|
2156
|
+
return { reason: m.matcher };
|
|
2157
|
+
const source = m.matcher.find((s) => sourceMayTake(s, at));
|
|
2158
|
+
if (source !== undefined)
|
|
2159
|
+
return { reason: "matcher", source };
|
|
2160
|
+
// with locales Next puts one in front of each source, and of the path
|
|
2161
|
+
return i18n && m.matcher.length > 0 ? { reason: "i18n" } : null;
|
|
2162
|
+
}
|
|
2163
|
+
/** Whether a version is at least `min`, by its major, minor and patch —
|
|
2164
|
+
false for one that doesn't start with three numbers. */
|
|
2165
|
+
export function atLeast(version, min) {
|
|
2166
|
+
const parts = (v) => /^v?(\d+)\.(\d+)\.(\d+)/.exec(v)?.slice(1).map(Number) ?? null;
|
|
2167
|
+
const a = parts(version);
|
|
2168
|
+
const b = parts(min);
|
|
2169
|
+
if (!a)
|
|
2170
|
+
return false;
|
|
2171
|
+
for (let i = 0; i < 3; i++)
|
|
2172
|
+
if (a[i] !== b[i])
|
|
2173
|
+
return a[i] > b[i];
|
|
2174
|
+
return true;
|
|
2175
|
+
}
|
|
2176
|
+
/** The version of a package the site's code resolves: the package's
|
|
2177
|
+
folder under the nearest node_modules, from the site's folder up — as
|
|
2178
|
+
Node and Next's bundler find it. Null when none is there. */
|
|
2179
|
+
async function installedVersion(dir, pkg) {
|
|
2180
|
+
for (let at = path.resolve(dir);;) {
|
|
2181
|
+
try {
|
|
2182
|
+
const raw = await readFile(path.join(at, "node_modules", ...pkg.split("/"), "package.json"), "utf8");
|
|
2183
|
+
const version = JSON.parse(raw).version;
|
|
2184
|
+
return typeof version === "string" ? version : null;
|
|
2185
|
+
}
|
|
2186
|
+
catch {
|
|
2187
|
+
// not here: the folder above
|
|
2188
|
+
}
|
|
2189
|
+
const up = path.dirname(at);
|
|
2190
|
+
if (up === at)
|
|
2191
|
+
return null;
|
|
2192
|
+
at = up;
|
|
2193
|
+
}
|
|
2194
|
+
}
|
|
2195
|
+
/** The middleware files Next runs for the site — `middleware`, and
|
|
2196
|
+
`proxy` unless the installed Next is older than 16, which names it so —
|
|
2197
|
+
with one of its page extensions, in the folder that holds the pages
|
|
2198
|
+
folder it reads, or else the app folder: the root, or `src/`. Read from
|
|
2199
|
+
the folder itself, not the file list, which may stop before it. */
|
|
2200
|
+
async function middlewareFiles(dir, extensions, next) {
|
|
2201
|
+
const folder = async (name) => ((await atRoot(dir, name)) ? name : (await atRoot(dir, `src/${name}`)) ? `src/${name}` : null);
|
|
2202
|
+
const read = (await folder("pages")) ?? (await folder("app"));
|
|
2203
|
+
const at = read?.startsWith("src/") ? "src" : "";
|
|
2204
|
+
let names;
|
|
2205
|
+
try {
|
|
2206
|
+
names = (await readdir(path.join(dir, at), { withFileTypes: true })).filter((e) => e.isFile()).map((e) => e.name).sort();
|
|
2207
|
+
}
|
|
2208
|
+
catch {
|
|
2209
|
+
return [];
|
|
2210
|
+
}
|
|
2211
|
+
const kinds = next === null || atLeast(next, "16.0.0") ? ["middleware", "proxy"] : ["middleware"];
|
|
2212
|
+
// Next's own test: the extensions joined into a pattern as they are
|
|
2213
|
+
let test;
|
|
2214
|
+
try {
|
|
2215
|
+
const re = new RegExp(`^(?:${kinds.join("|")})\\.(?:${extensions.join("|")})$`);
|
|
2216
|
+
test = (name) => re.test(name);
|
|
2217
|
+
}
|
|
2218
|
+
catch {
|
|
2219
|
+
test = (name) => extensions.some((ext) => kinds.some((kind) => name === `${kind}.${ext}`));
|
|
2220
|
+
}
|
|
2221
|
+
const found = [];
|
|
2222
|
+
for (const name of names.filter(test)) {
|
|
2223
|
+
const file = at ? `${at}/${name}` : name;
|
|
2224
|
+
let text;
|
|
2225
|
+
try {
|
|
2226
|
+
text = await readFile(path.join(dir, file), "utf8");
|
|
2227
|
+
}
|
|
2228
|
+
catch {
|
|
2229
|
+
found.push({ file, matcher: "unread" });
|
|
2230
|
+
continue;
|
|
2231
|
+
}
|
|
2232
|
+
found.push({ file, matcher: text.length > 2_000_000 ? "unread" : middlewareMatcher(text) });
|
|
2233
|
+
}
|
|
2234
|
+
return found;
|
|
1272
2235
|
}
|
|
1273
2236
|
export async function findLive(dir, maxFiles = 4000) {
|
|
1274
2237
|
const { files } = await listFiles(dir, maxFiles);
|
|
@@ -1287,8 +2250,10 @@ export async function findLive(dir, maxFiles = 4000) {
|
|
|
1287
2250
|
const routeExtension = extensions === null || extensions.includes("ts") ? "ts" : (extensions[0] ?? "ts");
|
|
1288
2251
|
// a folder under src/ that Next doesn't read: nothing in it runs
|
|
1289
2252
|
const rootApp = await atRoot(dir, "app");
|
|
1290
|
-
const
|
|
2253
|
+
const rootPages = await atRoot(dir, "pages");
|
|
2254
|
+
const ignored = [...(rootApp ? ["src/app/"] : []), ...(rootPages ? ["src/pages/"] : [])];
|
|
1291
2255
|
const appDir = rootApp ? "app" : (await atRoot(dir, "src/app")) ? "src/app" : undefined;
|
|
2256
|
+
const pagesDir = rootPages ? "pages" : (await atRoot(dir, "src/pages")) ? "src/pages" : undefined;
|
|
1292
2257
|
const listeners = [];
|
|
1293
2258
|
const routes = [];
|
|
1294
2259
|
const pages = [];
|
|
@@ -1297,7 +2262,9 @@ export async function findLive(dir, maxFiles = 4000) {
|
|
|
1297
2262
|
continue;
|
|
1298
2263
|
const route = extensions === null ? null : routeFileOf(file, extensions);
|
|
1299
2264
|
const page = extensions === null ? null : pageFileOf(file, extensions);
|
|
1300
|
-
|
|
2265
|
+
// a Pages Router file whose path is /api or under it is an API route, as Next tells them apart
|
|
2266
|
+
const api = page !== null && /^(?:src\/)?pages\//.test(file) && (page === "/api" || page.startsWith("/api/")) ? page : null;
|
|
2267
|
+
if (page !== null && api === null)
|
|
1301
2268
|
pages.push({ file, path: page });
|
|
1302
2269
|
let text;
|
|
1303
2270
|
try {
|
|
@@ -1309,18 +2276,34 @@ export async function findLive(dir, maxFiles = 4000) {
|
|
|
1309
2276
|
if (text.length > 2_000_000)
|
|
1310
2277
|
continue;
|
|
1311
2278
|
const words = WORDS.test(text);
|
|
1312
|
-
const found = words ? liveInFile(file, text) : { listeners: [], handler: null, params: false };
|
|
2279
|
+
const found = words ? liveInFile(file, text, api !== null ? "pages" : "app") : { listeners: [], handler: null, params: false };
|
|
1313
2280
|
listeners.push(...found.listeners);
|
|
1314
2281
|
if (route !== null)
|
|
1315
|
-
routes.push({ file, path: route, handler: found.handler, ...(found.params ? { params: true } : {}) });
|
|
2282
|
+
routes.push({ file, path: route, handler: found.handler, ...(found.params ? { params: true } : {}), ...(found.segment ? { segment: found.segment } : {}) });
|
|
2283
|
+
if (api !== null)
|
|
2284
|
+
routes.push({ file, path: api, handler: found.handler, router: "pages", ...(found.runtime !== undefined ? { runtime: found.runtime } : {}) });
|
|
1316
2285
|
}
|
|
1317
2286
|
const facts = { listeners, routes, pages, routeExtension };
|
|
1318
2287
|
if (appDir)
|
|
1319
2288
|
facts.appDir = appDir;
|
|
2289
|
+
if (pagesDir)
|
|
2290
|
+
facts.pagesDir = pagesDir;
|
|
1320
2291
|
if (settings?.basePath)
|
|
1321
2292
|
facts.basePath = settings.basePath;
|
|
1322
2293
|
if (settings === null && config)
|
|
1323
2294
|
facts.config = config;
|
|
2295
|
+
if (config && settings && (settings.output === "export" || settings.output === null))
|
|
2296
|
+
facts.output = { file: config, value: settings.output };
|
|
2297
|
+
const sdk = await installedVersion(dir, "@mapled/next");
|
|
2298
|
+
if (sdk !== null)
|
|
2299
|
+
facts.sdk = sdk;
|
|
2300
|
+
if (settings?.i18n)
|
|
2301
|
+
facts.i18n = true;
|
|
2302
|
+
if (extensions !== null) {
|
|
2303
|
+
const middleware = await middlewareFiles(dir, extensions, await installedVersion(dir, "next"));
|
|
2304
|
+
if (middleware.length > 0)
|
|
2305
|
+
facts.middleware = middleware;
|
|
2306
|
+
}
|
|
1324
2307
|
return facts;
|
|
1325
2308
|
}
|
|
1326
2309
|
/** Whether a literal endpoint names its own origin — `https://…`, `//host/…`
|
|
@@ -1445,8 +2428,26 @@ function answerer(routes, pages) {
|
|
|
1445
2428
|
}
|
|
1446
2429
|
/** Whether a route's folders take a param: `[x]`, `[...x]`, `[[...x]]`. */
|
|
1447
2430
|
const dynamicRoute = (route) => route.path.split("/").some((s) => segmentRank(s) > 0);
|
|
2431
|
+
/** Which router runs a listener's code, as far as its file tells: a file
|
|
2432
|
+
of the App Router folder is the App Router's; the Pages Router's
|
|
2433
|
+
`<MapledLive />`, a file of the pages folder, or any file of a site
|
|
2434
|
+
whose only router folder is the pages one — the Pages Router's. Null
|
|
2435
|
+
for anything else: a file either router may render. */
|
|
2436
|
+
export function runsIn(listener, facts) {
|
|
2437
|
+
const under = (dir) => dir !== undefined && listener.file.startsWith(`${dir}/`);
|
|
2438
|
+
if (under(facts.appDir))
|
|
2439
|
+
return "app";
|
|
2440
|
+
if (listener.router === "pages" || under(facts.pagesDir) || (facts.appDir === undefined && facts.pagesDir !== undefined))
|
|
2441
|
+
return "pages";
|
|
2442
|
+
return null;
|
|
2443
|
+
}
|
|
1448
2444
|
/** Whether every listener has a route that answers it. The first one
|
|
1449
|
-
that doesn't is the one to name.
|
|
2445
|
+
that doesn't is the one to name. An API route answers with the live
|
|
2446
|
+
release (`createPagesReleaseHandler`), which moves the moment someone
|
|
2447
|
+
publishes: it serves only listeners the Pages Router runs, whose pages
|
|
2448
|
+
read that same release in getServerSideProps (README). A listener the
|
|
2449
|
+
App Router may run refreshes on it before the webhook refreshes what
|
|
2450
|
+
the App Router's pages read, into a release they don't show yet. */
|
|
1450
2451
|
export function liveState(facts) {
|
|
1451
2452
|
// the conventional path first, then by file: the same answer every run
|
|
1452
2453
|
const rank = (r) => (r.path === RELEASE_ROUTE ? 0 : 1);
|
|
@@ -1457,38 +2458,77 @@ export function liveState(facts) {
|
|
|
1457
2458
|
const base = facts.basePath ?? "";
|
|
1458
2459
|
let served = null;
|
|
1459
2460
|
for (const listener of facts.listeners) {
|
|
2461
|
+
// the Pages Router's component in the App Router can't mount, whatever it asks
|
|
2462
|
+
if (listener.router === "pages" && facts.appDir !== undefined && listener.file.startsWith(`${facts.appDir}/`))
|
|
2463
|
+
return { kind: "router", listener, dir: facts.appDir };
|
|
2464
|
+
const pagesRun = runsIn(listener, facts) === "pages";
|
|
2465
|
+
// the route it lacks: an API route for a listener the Pages Router runs
|
|
2466
|
+
const missing = (endpoint) => ({ kind: "missing", listener, endpoint, ...(pagesRun ? { pages: true } : {}) });
|
|
1460
2467
|
if (listener.endpoint === null) {
|
|
1461
2468
|
// the code decides where it asks: with no release route at all it's
|
|
1462
2469
|
// surely missing; with one — or routes the reading can't know — nothing says it's the route asked
|
|
1463
2470
|
if (handlerRoutes.length === 0 && !facts.config)
|
|
1464
|
-
return
|
|
2471
|
+
return missing(null);
|
|
1465
2472
|
return { kind: "unresolved", listener };
|
|
1466
2473
|
}
|
|
1467
|
-
const
|
|
1468
|
-
if (
|
|
2474
|
+
const written = sitePath(listener.endpoint);
|
|
2475
|
+
if (written === null)
|
|
1469
2476
|
return { kind: "unresolved", listener };
|
|
1470
2477
|
if (facts.config)
|
|
1471
2478
|
return { kind: "config", listener, file: facts.config };
|
|
2479
|
+
// a static export has no server: no route runs, whatever the files say
|
|
2480
|
+
if (facts.output)
|
|
2481
|
+
return { kind: "export", listener, file: facts.output.file, output: facts.output.value };
|
|
2482
|
+
let at = written;
|
|
2483
|
+
// no endpoint given: from BASE_PATH_SINCE the SDK asks under the basePath Next writes into the bundle, before it at the root
|
|
2484
|
+
if (listener.byDefault && base !== "") {
|
|
2485
|
+
if (facts.sdk === undefined || !atLeast(facts.sdk, BASE_PATH_SINCE))
|
|
2486
|
+
return { kind: "default_base", listener, basePath: base, sdk: facts.sdk ?? null };
|
|
2487
|
+
at = `${base}${RELEASE_ROUTE}`;
|
|
2488
|
+
}
|
|
1472
2489
|
// with a basePath, Next answers only under it: a route file's path follows it
|
|
1473
2490
|
if (base !== "" && at !== base && !at.startsWith(`${base}/`))
|
|
1474
2491
|
return { kind: "outside", listener, endpoint: at, basePath: base };
|
|
1475
2492
|
const answer = answerOf(base === "" ? at : at.slice(base.length) || "/");
|
|
1476
2493
|
if (!answer)
|
|
1477
|
-
return
|
|
2494
|
+
return missing(at);
|
|
1478
2495
|
if (answer.page) {
|
|
1479
2496
|
// a page takes the path: through a param, a route of its own at the path wins over it; a route can't share a page's path
|
|
1480
2497
|
if (answer.ranks.some((r) => r > 0))
|
|
1481
|
-
return
|
|
2498
|
+
return missing(at);
|
|
1482
2499
|
return { kind: "taken", listener, page: answer.page, endpoint: at };
|
|
1483
2500
|
}
|
|
1484
2501
|
const route = answer.route;
|
|
1485
2502
|
// a dynamic route without the handler: a route of its own at the path wins over the param
|
|
1486
|
-
if (route.handler === null)
|
|
1487
|
-
return
|
|
1488
|
-
if (route.
|
|
1489
|
-
|
|
1490
|
-
|
|
1491
|
-
|
|
2503
|
+
if (route.handler === null && dynamicRoute(route))
|
|
2504
|
+
return missing(at);
|
|
2505
|
+
if (route.router === "pages") {
|
|
2506
|
+
// an API route that isn't the release route is, to a listener the App Router may run, a path taken
|
|
2507
|
+
if (route.handler === null)
|
|
2508
|
+
return pagesRun ? { kind: "no_handler", listener, route } : { kind: "taken", listener, page: { file: route.file, path: route.path }, endpoint: at };
|
|
2509
|
+
if (!pagesRun)
|
|
2510
|
+
return { kind: "pointer", listener, route, path: at };
|
|
2511
|
+
if (route.handler === "other")
|
|
2512
|
+
return { kind: "not_get", listener, route };
|
|
2513
|
+
if (route.runtime !== undefined)
|
|
2514
|
+
return { kind: "runtime", listener, route, runtime: route.runtime };
|
|
2515
|
+
}
|
|
2516
|
+
else {
|
|
2517
|
+
if (route.handler === null)
|
|
2518
|
+
return { kind: "no_handler", listener, route };
|
|
2519
|
+
if (route.handler === "other")
|
|
2520
|
+
return { kind: "not_get", listener, route };
|
|
2521
|
+
if (route.params && dynamicRoute(route))
|
|
2522
|
+
return { kind: "params", listener, route, endpoint: at };
|
|
2523
|
+
if (route.segment)
|
|
2524
|
+
return { kind: "segment", listener, route, setting: route.segment };
|
|
2525
|
+
}
|
|
2526
|
+
// middleware runs before the route: whatever it does there, the reading can't see
|
|
2527
|
+
for (const m of facts.middleware ?? []) {
|
|
2528
|
+
const on = middlewareOn(m, base === "" ? at : at.slice(base.length) || "/", facts.i18n === true);
|
|
2529
|
+
if (on)
|
|
2530
|
+
return { kind: "middleware", listener, route, path: at, file: m.file, ...on };
|
|
2531
|
+
}
|
|
1492
2532
|
served ??= { listener, route, path: at };
|
|
1493
2533
|
}
|
|
1494
2534
|
return { kind: "served", ...served };
|
|
@@ -1502,8 +2542,24 @@ export function capabilitiesOf(state) {
|
|
|
1502
2542
|
reads — without one, under `src/app` when the site keeps its code there
|
|
1503
2543
|
— and without the basePath, which Next adds itself. */
|
|
1504
2544
|
export function routeFileFor(endpoint, facts) {
|
|
1505
|
-
const
|
|
2545
|
+
const local = localPath(endpoint, facts);
|
|
2546
|
+
return `${facts.appDir ?? (underSrc(facts) ? "src/app" : "app")}${local === "/" ? "" : local}/route.${facts.routeExtension ?? "ts"}`;
|
|
2547
|
+
}
|
|
2548
|
+
/** Where an API route answering `endpoint` goes, for a listener the Pages
|
|
2549
|
+
Router runs: in the pages folder Next reads — without one, under
|
|
2550
|
+
`src/pages` when the site keeps its code there — and without the
|
|
2551
|
+
basePath. Null for a path no API route answers: Next serves them at
|
|
2552
|
+
/api and under it only. */
|
|
2553
|
+
export function pagesRouteFileFor(endpoint, facts) {
|
|
2554
|
+
const local = localPath(endpoint, facts);
|
|
2555
|
+
if (local !== "/api" && !local.startsWith("/api/"))
|
|
2556
|
+
return null;
|
|
2557
|
+
return `${facts.pagesDir ?? (underSrc(facts) ? "src/pages" : "pages")}${local === "/api" ? "/api/index" : local}.${facts.routeExtension ?? "ts"}`;
|
|
2558
|
+
}
|
|
2559
|
+
/** Whether the site keeps its code under src/: some route, page or listener is there. */
|
|
2560
|
+
const underSrc = (facts) => [...facts.routes.map((r) => r.file), ...(facts.pages ?? []).map((p) => p.file), ...facts.listeners.map((l) => l.file)].some((f) => f.startsWith("src/"));
|
|
2561
|
+
/** An endpoint's path without the basePath, which Next adds itself. */
|
|
2562
|
+
function localPath(endpoint, facts) {
|
|
1506
2563
|
const base = facts.basePath ?? "";
|
|
1507
|
-
|
|
1508
|
-
return `${facts.appDir ?? (underSrc ? "src/app" : "app")}${local === "/" ? "" : local}/route.${facts.routeExtension ?? "ts"}`;
|
|
2564
|
+
return base !== "" && (endpoint === base || endpoint.startsWith(`${base}/`)) ? endpoint.slice(base.length) || "/" : endpoint;
|
|
1509
2565
|
}
|