@mapled/cli 0.12.0 → 0.13.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/dist/doctor.js +48 -2
- package/dist/live.d.ts +120 -12
- package/dist/live.js +1098 -261
- package/dist/scan.d.ts +8 -3
- package/dist/scan.js +68 -12
- package/package.json +1 -1
package/dist/live.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
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
|
|
@@ -14,6 +14,9 @@ import { appRoute, listFiles, tokenize } from "./scan.js";
|
|
|
14
14
|
does an imported name the file binds again. */
|
|
15
15
|
/** Where `<MapledLive />` and `watchRelease` ask when no endpoint is given. */
|
|
16
16
|
export const RELEASE_ROUTE = "/api/mapled/release";
|
|
17
|
+
/** The `@mapled/next` from which that default follows the site's basePath —
|
|
18
|
+
before it, a listener with no endpoint asks RELEASE_ROUTE as written. */
|
|
19
|
+
export const BASE_PATH_SINCE = "0.10.0";
|
|
17
20
|
/** The extensions a route file may have when next.config doesn't set `pageExtensions`. */
|
|
18
21
|
export const PAGE_EXTENSIONS = ["tsx", "ts", "jsx", "js"];
|
|
19
22
|
// the config files Next reads, in its order
|
|
@@ -95,9 +98,10 @@ const literal = (t) => t?.kind === "string" && !t.unclosed && t.value !== "${}";
|
|
|
95
98
|
const OPTIONS_TOKENS = 2000;
|
|
96
99
|
/** The endpoint of a JSX element's attributes, from just after its name.
|
|
97
100
|
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
|
|
101
|
+
code decides it — an expression, or spread props that may carry one —
|
|
102
|
+
and undefined when the element gives none: the SDK's default. */
|
|
99
103
|
function jsxEndpoint(tokens, from) {
|
|
100
|
-
let endpoint =
|
|
104
|
+
let endpoint = undefined;
|
|
101
105
|
let depth = 0;
|
|
102
106
|
for (let k = from; k < tokens.length; k++) {
|
|
103
107
|
if (k >= from + OPTIONS_TOKENS)
|
|
@@ -169,11 +173,12 @@ function propertyKey(tokens, k) {
|
|
|
169
173
|
read property by property: the last `endpoint: "…"` or spread wins;
|
|
170
174
|
null when the code decides it — options from elsewhere, a spread, a
|
|
171
175
|
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
|
|
176
|
+
plain string: a shorthand, a method, a getter, an expression — and
|
|
177
|
+
undefined when the options give none: the SDK's default. */
|
|
173
178
|
function callEndpoint(tokens, paren) {
|
|
174
179
|
if (!isPunct(tokens[paren + 1], "{"))
|
|
175
180
|
return null;
|
|
176
|
-
let endpoint =
|
|
181
|
+
let endpoint = undefined;
|
|
177
182
|
let depth = 0;
|
|
178
183
|
let property = false;
|
|
179
184
|
for (let k = paren + 1; k < tokens.length && k < paren + OPTIONS_TOKENS; k++) {
|
|
@@ -834,6 +839,7 @@ export function liveInFile(file, text) {
|
|
|
834
839
|
return isPunct(tokens[k - 1], ".") && ns?.kind === "ident" && namespaces.has(ns.value) && !isPunct(tokens[k - 3], ".") && !read().bound.has(ns.value);
|
|
835
840
|
};
|
|
836
841
|
const listeners = [];
|
|
842
|
+
const listener = (via, endpoint) => endpoint === undefined ? { file, via, endpoint: RELEASE_ROUTE, byDefault: true } : { file, via, endpoint };
|
|
837
843
|
for (let k = 0; k < tokens.length; k++) {
|
|
838
844
|
const t = tokens[k];
|
|
839
845
|
if (t.kind !== "ident")
|
|
@@ -841,10 +847,10 @@ export function liveInFile(file, text) {
|
|
|
841
847
|
if (names.live.has(t.value) && isPunct(tokens[k - 1], "<")) {
|
|
842
848
|
if (read().bound.has(t.value))
|
|
843
849
|
continue;
|
|
844
|
-
listeners.push(
|
|
850
|
+
listeners.push(listener("<MapledLive />", jsxEndpoint(tokens, k + 1)));
|
|
845
851
|
}
|
|
846
852
|
else if (t.value === "MapledLive" && isPunct(tokens[k - 3], "<") && ofNamespace(k, names.ns.live)) {
|
|
847
|
-
listeners.push(
|
|
853
|
+
listeners.push(listener("<MapledLive />", jsxEndpoint(tokens, k + 1)));
|
|
848
854
|
}
|
|
849
855
|
else if (names.watch.has(t.value) && isPunct(tokens[k + 1], "(") && !isPunct(tokens[k - 1], ".")) {
|
|
850
856
|
const prev = tokens[k - 1];
|
|
@@ -852,23 +858,107 @@ export function liveInFile(file, text) {
|
|
|
852
858
|
continue;
|
|
853
859
|
if (read().bound.has(t.value))
|
|
854
860
|
continue;
|
|
855
|
-
listeners.push(
|
|
861
|
+
listeners.push(listener("watchRelease", callEndpoint(tokens, k + 1)));
|
|
856
862
|
}
|
|
857
863
|
else if (t.value === "watchRelease" && isPunct(tokens[k + 1], "(") && ofNamespace(k, names.ns.watch)) {
|
|
858
|
-
listeners.push(
|
|
864
|
+
listeners.push(listener("watchRelease", callEndpoint(tokens, k + 1)));
|
|
859
865
|
}
|
|
860
866
|
}
|
|
861
|
-
// `dynamicParams` as the route exports
|
|
862
|
-
const
|
|
863
|
-
|
|
867
|
+
// `dynamicParams` and the segment settings as the route exports them, or an export the reading can't list
|
|
868
|
+
const listing = tokens.some((u, k) => isWord(u, "exports") || (isWord(u, "export") && isPunct(tokens[k + 1], "*")));
|
|
869
|
+
const sets = (name, keeps) => (listing || tokens.some((u) => isName(u, name))) && setsExport(tokens, read().b.after, name, keeps);
|
|
870
|
+
const params = sets("dynamicParams", (t) => isWord(t ?? undefined, "true"));
|
|
871
|
+
// "force-dynamic" keeps the route only while the handler's read names its own window (keepsWindow)
|
|
872
|
+
const forced = () => sets("dynamic", (t) => literal(t ?? undefined) && t.value === "auto") && !windowsKept(tokens, names, read().b.after);
|
|
873
|
+
const segment = sets("dynamic", SEGMENT_KEEPS.dynamic) ? "dynamic" : forced() ? "force-dynamic" : sets("fetchCache", SEGMENT_KEEPS.fetchCache) ? "fetchCache" : undefined;
|
|
874
|
+
return { listeners, handler: releaseHandlerOf(tokens, names, read), params, ...(segment ? { segment } : {}) };
|
|
864
875
|
}
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
`
|
|
871
|
-
|
|
876
|
+
/** Whether every release handler the file makes keeps a window of its own
|
|
877
|
+
for the pointer's read (keepsWindow), with the factory only ever called
|
|
878
|
+
where it's named: `const make = createReleaseHandler`, a namespace
|
|
879
|
+
passed on or taken apart, make handlers the reading doesn't see. */
|
|
880
|
+
function windowsKept(tokens, names, after) {
|
|
881
|
+
// the import clauses name them without calling: `import … from`, never past a string — `import "server-only"` has no clause
|
|
882
|
+
const clause = new Uint8Array(tokens.length);
|
|
883
|
+
for (let i = 0; i < tokens.length; i++) {
|
|
884
|
+
if (!isWord(tokens[i], "import") || isPunct(tokens[i - 1], ".") || isPunct(tokens[i + 1], "("))
|
|
885
|
+
continue;
|
|
886
|
+
let j = i + 1;
|
|
887
|
+
while (j < tokens.length && j < i + 200 && !isWord(tokens[j], "from") && tokens[j].kind !== "string" && !isPunct(tokens[j], ";"))
|
|
888
|
+
j = isPunct(tokens[j], "{") ? after[j] : j + 1;
|
|
889
|
+
if (isWord(tokens[j], "from"))
|
|
890
|
+
clause.fill(1, i, j + 1);
|
|
891
|
+
}
|
|
892
|
+
for (let k = 0; k < tokens.length; k++) {
|
|
893
|
+
const t = tokens[k];
|
|
894
|
+
if (t.kind !== "ident" || clause[k] === 1)
|
|
895
|
+
continue;
|
|
896
|
+
const ns = names.ns.handler.has(t.value) && !isPunct(tokens[k - 1], ".");
|
|
897
|
+
if (ns && !isPunct(tokens[k + 1], "."))
|
|
898
|
+
return false;
|
|
899
|
+
const direct = names.handler.has(t.value) && !isPunct(tokens[k - 1], ".");
|
|
900
|
+
const member = t.value === "createReleaseHandler" && isPunct(tokens[k - 1], ".") && tokens[k - 2]?.kind === "ident" && names.ns.handler.has(tokens[k - 2].value) && !isPunct(tokens[k - 3], ".");
|
|
901
|
+
if ((direct || member) && !(isPunct(tokens[k + 1], "(") && keepsWindow(tokens, after, k + 1)))
|
|
902
|
+
return false;
|
|
903
|
+
}
|
|
904
|
+
return true;
|
|
905
|
+
}
|
|
906
|
+
/** Whether a `createReleaseHandler` call, from its opening parenthesis,
|
|
907
|
+
leaves the pointer's read a window of its own: options written out as
|
|
908
|
+
an object whose `revalidate` is absent (3600) or digits above 0. On a
|
|
909
|
+
`force-dynamic` route Next 15 and later fetch a read with no window —
|
|
910
|
+
`revalidate: false` counts as none — uncached, past the `mapled` tag,
|
|
911
|
+
and the release moves before the webhook refreshes the pages. False
|
|
912
|
+
where the reading can't tell: options from elsewhere, a spread, a key
|
|
913
|
+
the code decides, a `revalidate` that isn't plain digits. */
|
|
914
|
+
function keepsWindow(tokens, after, paren) {
|
|
915
|
+
const open = paren + 1;
|
|
916
|
+
if (!isPunct(tokens[open], "{") || !isPunct(tokens[after[open]], ")"))
|
|
917
|
+
return false;
|
|
918
|
+
let kept = true;
|
|
919
|
+
for (const [a, z] of itemsOf(tokens, after, open)) {
|
|
920
|
+
if (a === z)
|
|
921
|
+
continue;
|
|
922
|
+
if (isSpread(tokens, a))
|
|
923
|
+
return false;
|
|
924
|
+
const { name, next } = propertyKey(tokens, a);
|
|
925
|
+
if (name === null)
|
|
926
|
+
return false;
|
|
927
|
+
if (name !== "revalidate")
|
|
928
|
+
continue;
|
|
929
|
+
// the last one wins, as in the object: `revalidate: 60`, `revalidate: 0.5`
|
|
930
|
+
const digits = tokens.slice(next + 1, z).map((u) => (u.kind === "punct" ? u.value : " ")).join("");
|
|
931
|
+
kept = isPunct(tokens[next], ":") && /^(\d+\.?\d*|\.\d+)$/.test(digits) && Number(digits) > 0;
|
|
932
|
+
}
|
|
933
|
+
return kept;
|
|
934
|
+
}
|
|
935
|
+
/** Whether the token is the name — as an identifier or a string: `export { x as "name" }`. */
|
|
936
|
+
const isName = (u, name) => (u?.kind === "ident" || u?.kind === "string") && u.value === name;
|
|
937
|
+
/** The route segment settings that change how Next serves the release
|
|
938
|
+
route or reads what the handler reads, in the order doctor names them:
|
|
939
|
+
`dynamic` can make it a static copy built without the tab's request,
|
|
940
|
+
and `fetchCache` can make the handler's read skip the `mapled` cache
|
|
941
|
+
tag, so the release moves before the webhook refreshes the pages.
|
|
942
|
+
`revalidate` isn't one: it only fills in fetches that don't name their
|
|
943
|
+
own window, and the handler's names one (Next 14's fetch patch; the
|
|
944
|
+
live smoke with `revalidate = 1` on Next 15). "force-dynamic" is the
|
|
945
|
+
third: harmless while the handler keeps its read's window, and past
|
|
946
|
+
the tag when it doesn't (keepsWindow). */
|
|
947
|
+
export const SEGMENT_SETTINGS = ["dynamic", "fetchCache"];
|
|
948
|
+
/** For each, the values written out that leave the route as the handler
|
|
949
|
+
sets it up: the default, and for `dynamic` also "force-dynamic" — the
|
|
950
|
+
route reads its request, so it is dynamic anyway; what it does to the
|
|
951
|
+
handler's read is checked apart (windowsKept). */
|
|
952
|
+
const SEGMENT_KEEPS = {
|
|
953
|
+
dynamic: (t) => literal(t ?? undefined) && (t.value === "auto" || t.value === "force-dynamic"),
|
|
954
|
+
fetchCache: (t) => literal(t ?? undefined) && t.value === "auto",
|
|
955
|
+
};
|
|
956
|
+
/** Whether the route may export `name` as anything but a value `keeps`
|
|
957
|
+
accepts, written out on a `const`. Next reads only the module's
|
|
958
|
+
export: a local variable, a property or a string that says the name is
|
|
959
|
+
nothing to it. An export the reading can't list — `export *`,
|
|
960
|
+
CommonJS — may be it. */
|
|
961
|
+
function setsExport(tokens, after, name, keeps) {
|
|
872
962
|
for (let i = 0; i < tokens.length; i++) {
|
|
873
963
|
const t = tokens[i];
|
|
874
964
|
// CommonJS: `module.exports = { dynamicParams: false }`, `exports.dynamicParams = false`
|
|
@@ -888,7 +978,7 @@ function setsParams(tokens, after) {
|
|
|
888
978
|
// a clause, with or without `from`: `export { x as dynamicParams }`, `export { dynamicParams }`, `export { x as "dynamicParams" }`
|
|
889
979
|
if (isPunct(tokens[j], "{")) {
|
|
890
980
|
for (let k = j + 1; k < after[j] - 1; k++)
|
|
891
|
-
if (
|
|
981
|
+
if (isName(tokens[k], name) && !isWord(tokens[k + 1], "as"))
|
|
892
982
|
return true;
|
|
893
983
|
continue;
|
|
894
984
|
}
|
|
@@ -897,8 +987,8 @@ function setsParams(tokens, after) {
|
|
|
897
987
|
const declare = tokens[j];
|
|
898
988
|
if (declare?.kind !== "ident" || !DECLARE.has(declare.value)) {
|
|
899
989
|
// `export function dynamicParams`, `export class …`, `export import dynamicParams = …`
|
|
900
|
-
const
|
|
901
|
-
if (
|
|
990
|
+
const named = isPunct(tokens[j + 1], "*") ? j + 2 : j + 1;
|
|
991
|
+
if (isName(tokens[named], name))
|
|
902
992
|
return true;
|
|
903
993
|
continue;
|
|
904
994
|
}
|
|
@@ -907,13 +997,13 @@ function setsParams(tokens, after) {
|
|
|
907
997
|
let next;
|
|
908
998
|
if (isPunct(tokens[k], "{") || isPunct(tokens[k], "[")) {
|
|
909
999
|
for (let p = k + 1; p < after[k] - 1; p++)
|
|
910
|
-
if (
|
|
1000
|
+
if (isName(tokens[p], name))
|
|
911
1001
|
return true;
|
|
912
1002
|
next = after[k];
|
|
913
1003
|
}
|
|
914
1004
|
else if (tokens[k].kind === "ident") {
|
|
915
|
-
// only a `const` keeps
|
|
916
|
-
if (
|
|
1005
|
+
// only a `const` keeps its value: a `let` or `var` may change it later
|
|
1006
|
+
if (isName(tokens[k], name) && (declare.value !== "const" || !keeps(writtenValue(tokens, after, k))))
|
|
917
1007
|
return true;
|
|
918
1008
|
next = k + 1;
|
|
919
1009
|
}
|
|
@@ -932,15 +1022,16 @@ function setsParams(tokens, after) {
|
|
|
932
1022
|
}
|
|
933
1023
|
return false;
|
|
934
1024
|
}
|
|
935
|
-
/**
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
1025
|
+
/** The one token a declaration at `k` writes as its value: `name = true`,
|
|
1026
|
+
with or without a type (`: boolean`), the value in parentheses or
|
|
1027
|
+
followed by `as` / `satisfies` / `!`. Null when the value is anything
|
|
1028
|
+
else — an expression, more than one token — or there is none. */
|
|
1029
|
+
function writtenValue(tokens, after, k) {
|
|
939
1030
|
if (tokens[k].kind !== "ident")
|
|
940
|
-
return
|
|
1031
|
+
return null;
|
|
941
1032
|
const eq = isPunct(tokens[k + 1], ":") ? typeEnd(tokens, after, k + 2, tokens.length) : k + 1;
|
|
942
1033
|
if (eq < 0 || !isPunct(tokens[eq], "=") || isPunct(tokens[eq + 1], "="))
|
|
943
|
-
return
|
|
1034
|
+
return null;
|
|
944
1035
|
let from = eq + 1;
|
|
945
1036
|
let to = initializerEnd(tokens, after, from);
|
|
946
1037
|
for (;;) {
|
|
@@ -958,7 +1049,7 @@ function paramsDefault(tokens, after, k) {
|
|
|
958
1049
|
while (end > from + 1 && isPunct(tokens[end - 1], "!"))
|
|
959
1050
|
end--;
|
|
960
1051
|
if (!isPunct(tokens[from], "(") || after[from] !== end)
|
|
961
|
-
return end === from + 1
|
|
1052
|
+
return end === from + 1 ? tokens[from] : null;
|
|
962
1053
|
from++;
|
|
963
1054
|
to = end - 1;
|
|
964
1055
|
}
|
|
@@ -1008,267 +1099,983 @@ async function atRoot(dir, name) {
|
|
|
1008
1099
|
const SETTINGS = ["pageExtensions", "basePath"];
|
|
1009
1100
|
// calls that set a property of an object they're given, by a name or from another object, or give it a prototype that may carry one
|
|
1010
1101
|
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
|
-
|
|
1102
|
+
// Next's hooks: the functions a config may hold, which Next calls only once it has loaded the config — webpack's as it builds, the routes' as it collects them
|
|
1103
|
+
const HOOKS = new Set(["webpack", "headers", "redirects", "rewrites", "generateBuildId", "exportPathMap"]);
|
|
1104
|
+
// the names the subset reads without a binding, which the file may not bind
|
|
1105
|
+
const AMBIENT = new Set(["process", "require", "module", "exports", "URL", "undefined", "NaN", "Infinity", "globalThis", "eval", "arguments"]);
|
|
1106
|
+
// the words that start a statement of the subset; none goes on with the expression a line break comes after
|
|
1107
|
+
const STARTS = new Set(["import", "export", "const", "let", "var", "module"]);
|
|
1108
|
+
// words after which JavaScript reads a `/` as a regex, not a division — the contextual ones (`of`, `await`, `let`) either way
|
|
1109
|
+
const BEFORE_REGEX = new Set(["return", "typeof", "case", "delete", "do", "else", "in", "instanceof", "new", "of", "throw", "void", "yield", "await", "extends", "let"]);
|
|
1110
|
+
// words no value ends with: past a line break after one, the expression goes on
|
|
1111
|
+
const GOES_ON = new Set([...BEFORE_REGEX, "as", "satisfies", "keyof", "default", "export", "import", "const", "var"]);
|
|
1112
|
+
// brackets and what closes each
|
|
1113
|
+
const OPENS = new Map([
|
|
1114
|
+
["(", ")"],
|
|
1115
|
+
["[", "]"],
|
|
1116
|
+
["{", "}"],
|
|
1117
|
+
]);
|
|
1118
|
+
// how deep values may nest, so a deep file can't overflow the stack
|
|
1119
|
+
const DEPTH = 200;
|
|
1120
|
+
const isDigit = (t) => t?.kind === "punct" && /^\d$/.test(t.value);
|
|
1121
|
+
/** Whether every bracket closes with its own kind, and none closes nothing. */
|
|
1122
|
+
function balanced(tokens, b) {
|
|
1027
1123
|
for (let k = 0; k < tokens.length; k++) {
|
|
1028
1124
|
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)
|
|
1125
|
+
if (t.kind !== "punct")
|
|
1045
1126
|
continue;
|
|
1046
|
-
const
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
if (
|
|
1053
|
-
return
|
|
1127
|
+
const close = OPENS.get(t.value);
|
|
1128
|
+
if (close !== undefined) {
|
|
1129
|
+
const at = b.after[k] - 1;
|
|
1130
|
+
if (!isPunct(tokens[at], close) || b.open[at] !== k)
|
|
1131
|
+
return false;
|
|
1132
|
+
}
|
|
1133
|
+
else if ((t.value === ")" || t.value === "]" || t.value === "}") && b.open[k] < 0)
|
|
1134
|
+
return false;
|
|
1054
1135
|
}
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1136
|
+
return true;
|
|
1137
|
+
}
|
|
1138
|
+
/** Whether each `/` of the tokens is what JavaScript reads there: a
|
|
1139
|
+
division after a value — a name, a literal, `]`, a `)` that closes no
|
|
1140
|
+
`if (…)` — and a regex anywhere else. The tokens read a regex only after
|
|
1141
|
+
punctuation that ends no value (and `return`, `typeof`), so they differ
|
|
1142
|
+
after `+`, after `else`, after `if (x)`: a brace in such a regex would
|
|
1143
|
+
move every bracket after it. After `}` JavaScript can read either. */
|
|
1144
|
+
function slashesAgree(tokens, b) {
|
|
1058
1145
|
for (let k = 0; k < tokens.length; k++) {
|
|
1059
|
-
|
|
1060
|
-
|
|
1146
|
+
const t = tokens[k];
|
|
1147
|
+
const regex = t.kind === "regex";
|
|
1148
|
+
if (!regex && !isPunct(t, "/"))
|
|
1149
|
+
continue;
|
|
1150
|
+
const p = tokens[k - 1];
|
|
1151
|
+
let division;
|
|
1152
|
+
if (p === undefined)
|
|
1153
|
+
division = false;
|
|
1154
|
+
else if (p.kind === "ident")
|
|
1155
|
+
division = !BEFORE_REGEX.has(p.value) || isPunct(tokens[k - 2], ".");
|
|
1156
|
+
else if (p.kind === "string" || p.kind === "regex" || isDigit(p) || p.value === "]")
|
|
1157
|
+
division = true;
|
|
1158
|
+
else if (p.value === ")") {
|
|
1159
|
+
const open = b.open[k - 1];
|
|
1160
|
+
const head = open >= 0 ? tokens[open - 1] : undefined;
|
|
1161
|
+
const condition = head?.kind === "ident" && !isPunct(tokens[open - 2], ".") && (["if", "while", "for", "with"].includes(head.value) || (head.value === "await" && isWord(tokens[open - 2], "for")));
|
|
1162
|
+
division = !condition;
|
|
1061
1163
|
}
|
|
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);
|
|
1164
|
+
else if (p.value === "}")
|
|
1165
|
+
return false;
|
|
1075
1166
|
else
|
|
1076
|
-
|
|
1167
|
+
division = false;
|
|
1168
|
+
if (regex === division)
|
|
1169
|
+
return false;
|
|
1077
1170
|
}
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1171
|
+
return true;
|
|
1172
|
+
}
|
|
1173
|
+
/** The file as the declarative subset nextConfigOf reads (see there), or
|
|
1174
|
+
null when anything in it is outside the subset. */
|
|
1175
|
+
function declarative(tokens, b) {
|
|
1176
|
+
const bindings = new Map();
|
|
1177
|
+
const props = new Map();
|
|
1178
|
+
const configs = [];
|
|
1179
|
+
let exported = null;
|
|
1180
|
+
let exports = 0;
|
|
1181
|
+
let depth = 0;
|
|
1182
|
+
const guard = (fail, f) => {
|
|
1183
|
+
if (depth >= DEPTH)
|
|
1184
|
+
return fail;
|
|
1185
|
+
depth++;
|
|
1186
|
+
try {
|
|
1187
|
+
return f();
|
|
1188
|
+
}
|
|
1189
|
+
finally {
|
|
1190
|
+
depth--;
|
|
1086
1191
|
}
|
|
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
1192
|
};
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
if (
|
|
1132
|
-
|
|
1133
|
-
|
|
1193
|
+
// the items between the brackets that open at `open`: a trailing comma adds none, a hole makes it null
|
|
1194
|
+
const listOf = (open) => {
|
|
1195
|
+
const items = itemsOf(tokens, b.after, open);
|
|
1196
|
+
if (items.length === 1 && items[0][0] === items[0][1])
|
|
1197
|
+
return [];
|
|
1198
|
+
if (items.length > 1 && items[items.length - 1][0] === items[items.length - 1][1])
|
|
1199
|
+
items.pop();
|
|
1200
|
+
return items.some(([x, y]) => x === y) ? null : items;
|
|
1201
|
+
};
|
|
1202
|
+
// [from, to) split at its commas outside brackets
|
|
1203
|
+
const splitAt = (from, to) => {
|
|
1204
|
+
const out = [];
|
|
1205
|
+
let start = from;
|
|
1206
|
+
for (let k = from; k <= to; k++) {
|
|
1207
|
+
const t = tokens[k];
|
|
1208
|
+
if (k < to && t?.kind === "punct" && OPENS.has(t.value))
|
|
1209
|
+
k = b.after[k] - 1;
|
|
1210
|
+
else if (k === to || isPunct(t, ",")) {
|
|
1211
|
+
out.push([start, k]);
|
|
1212
|
+
start = k + 1;
|
|
1213
|
+
}
|
|
1134
1214
|
}
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1215
|
+
return out;
|
|
1216
|
+
};
|
|
1217
|
+
// a value's last token: a line break after it may end the statement
|
|
1218
|
+
const endsValue = (t) => t !== undefined && (t.kind === "string" || t.kind === "regex" || (t.kind === "ident" && !GOES_ON.has(t.value)) || isDigit(t) || isPunct(t, ")") || isPunct(t, "]") || isPunct(t, "}"));
|
|
1219
|
+
// where the statement at `k` ends: its `;`, or a line that starts another after a value
|
|
1220
|
+
const statementEnd = (k) => {
|
|
1221
|
+
for (let j = k; j < tokens.length; j++) {
|
|
1222
|
+
const t = tokens[j];
|
|
1223
|
+
if (isPunct(t, ";"))
|
|
1224
|
+
return j;
|
|
1225
|
+
if (j > k && t.nl === true && t.kind === "ident" && STARTS.has(t.value) && endsValue(tokens[j - 1]))
|
|
1226
|
+
return j;
|
|
1227
|
+
if (t.kind === "punct" && OPENS.has(t.value))
|
|
1228
|
+
j = b.after[j] - 1;
|
|
1229
|
+
}
|
|
1230
|
+
return tokens.length;
|
|
1231
|
+
};
|
|
1232
|
+
const bind = (k, binding) => {
|
|
1233
|
+
const t = tokens[k];
|
|
1234
|
+
if (t?.kind !== "ident" || AMBIENT.has(t.value) || bindings.has(t.value))
|
|
1235
|
+
return false;
|
|
1236
|
+
bindings.set(t.value, { ...binding, at: k, uses: 0 });
|
|
1237
|
+
return true;
|
|
1238
|
+
};
|
|
1239
|
+
// a type as next.config writes one: `NextConfig`, `import("next").NextConfig`, with type arguments of the same and `[]` after; `const`
|
|
1240
|
+
const simpleType = (j, to, level = 0) => {
|
|
1241
|
+
if (level > 8 || j >= to)
|
|
1242
|
+
return -1;
|
|
1243
|
+
if (isWord(tokens[j], "const"))
|
|
1244
|
+
return j + 1;
|
|
1245
|
+
if (isWord(tokens[j], "import")) {
|
|
1246
|
+
if (!(isPunct(tokens[j + 1], "(") && literal(tokens[j + 2]) && isPunct(tokens[j + 3], ")") && isPunct(tokens[j + 4], ".")))
|
|
1247
|
+
return -1;
|
|
1248
|
+
j += 5;
|
|
1249
|
+
}
|
|
1250
|
+
if (tokens[j]?.kind !== "ident")
|
|
1251
|
+
return -1;
|
|
1252
|
+
j++;
|
|
1253
|
+
while (isPunct(tokens[j], ".") && tokens[j + 1]?.kind === "ident")
|
|
1254
|
+
j += 2;
|
|
1255
|
+
if (isPunct(tokens[j], "<")) {
|
|
1256
|
+
do {
|
|
1257
|
+
j = simpleType(j + 1, to, level + 1);
|
|
1258
|
+
if (j < 0)
|
|
1259
|
+
return -1;
|
|
1260
|
+
} while (isPunct(tokens[j], ","));
|
|
1261
|
+
if (!isPunct(tokens[j], ">"))
|
|
1262
|
+
return -1;
|
|
1263
|
+
j++;
|
|
1264
|
+
}
|
|
1265
|
+
while (isPunct(tokens[j], "[") && isPunct(tokens[j + 1], "]"))
|
|
1266
|
+
j += 2;
|
|
1267
|
+
return j <= to ? j : -1;
|
|
1268
|
+
};
|
|
1269
|
+
// what may follow a value to the end of its range: `as T`, `satisfies T`, on its line
|
|
1270
|
+
const suffix = (j, to) => {
|
|
1271
|
+
while (j < to) {
|
|
1272
|
+
if (!(isWord(tokens[j], "as") || isWord(tokens[j], "satisfies")) || tokens[j].nl === true)
|
|
1273
|
+
return false;
|
|
1274
|
+
j = simpleType(j + 1, to);
|
|
1275
|
+
if (j < 0)
|
|
1276
|
+
return false;
|
|
1277
|
+
}
|
|
1278
|
+
return j === to;
|
|
1279
|
+
};
|
|
1280
|
+
// where the value in [from, to) ends before its types, or -1
|
|
1281
|
+
const valueEnd = (from, to) => {
|
|
1138
1282
|
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))
|
|
1283
|
+
const t = tokens[k];
|
|
1284
|
+
if (t.kind === "punct" && OPENS.has(t.value))
|
|
1146
1285
|
k = b.after[k] - 1;
|
|
1147
|
-
if (
|
|
1148
|
-
|
|
1286
|
+
else if ((isWord(t, "as") || isWord(t, "satisfies")) && k > from && !isPunct(tokens[k - 1], "."))
|
|
1287
|
+
return suffix(k, to) ? k : -1;
|
|
1149
1288
|
}
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1289
|
+
return to;
|
|
1290
|
+
};
|
|
1291
|
+
// a use of the name at `k` in a role, after its binding: the first settles what the binding's value is for that role
|
|
1292
|
+
const use = (k, role) => {
|
|
1293
|
+
const binding = bindings.get(tokens[k].value);
|
|
1294
|
+
if (binding === undefined || binding.at >= k || binding.busy === true)
|
|
1295
|
+
return false;
|
|
1296
|
+
if (binding.role !== undefined && binding.role !== role)
|
|
1297
|
+
return false;
|
|
1298
|
+
binding.role = role;
|
|
1299
|
+
binding.uses++;
|
|
1300
|
+
// the config is taken once: a second use would hand it to more code
|
|
1301
|
+
if (role === "config" && binding.uses > 1)
|
|
1302
|
+
return false;
|
|
1303
|
+
if (binding.uses > 1)
|
|
1304
|
+
return true;
|
|
1305
|
+
binding.busy = true;
|
|
1306
|
+
const fine = settle(binding, role);
|
|
1307
|
+
delete binding.busy;
|
|
1308
|
+
return fine;
|
|
1309
|
+
};
|
|
1310
|
+
const settle = (binding, role) => {
|
|
1311
|
+
switch (role) {
|
|
1312
|
+
case "config":
|
|
1313
|
+
return binding.kind === "value" && configValue(binding.from, binding.to);
|
|
1314
|
+
case "data":
|
|
1315
|
+
return binding.kind === "none" || (binding.kind === "value" && data(binding.from, binding.to));
|
|
1156
1316
|
}
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1317
|
+
};
|
|
1318
|
+
// the config in [from, to): the object, or a name of it — a plugin's call is code the reading doesn't run (block 348)
|
|
1319
|
+
const configValue = (from, to) => guard(false, () => {
|
|
1320
|
+
const end = valueEnd(from, to);
|
|
1321
|
+
if (end <= from)
|
|
1322
|
+
return false;
|
|
1323
|
+
const t = tokens[from];
|
|
1324
|
+
if (isPunct(t, "(") && b.after[from] === end)
|
|
1325
|
+
return configValue(from + 1, end - 1);
|
|
1326
|
+
if (isPunct(t, "{") && b.after[from] === end) {
|
|
1327
|
+
configs.push(from);
|
|
1328
|
+
return object(from, true);
|
|
1329
|
+
}
|
|
1330
|
+
return t.kind === "ident" && from + 1 === end && use(from, "config");
|
|
1331
|
+
});
|
|
1332
|
+
// an object literal's properties: values that are data, a shorthand's name; functions only as Next's hooks, at the config's top
|
|
1333
|
+
const object = (open, config) => guard(false, () => {
|
|
1334
|
+
const items = listOf(open);
|
|
1335
|
+
if (items === null)
|
|
1336
|
+
return false;
|
|
1337
|
+
const list = [];
|
|
1338
|
+
for (const [x, y] of items) {
|
|
1339
|
+
// `async` or `get` is a key itself when what follows ends one: `{ get: 1 }`, `{ async }`
|
|
1340
|
+
const endsKey = (j) => j === y || isPunct(tokens[j], ":") || isPunct(tokens[j], "(");
|
|
1341
|
+
let k = x;
|
|
1342
|
+
const async = isWord(tokens[k], "async") && !endsKey(k + 1);
|
|
1343
|
+
if (async)
|
|
1344
|
+
k++;
|
|
1345
|
+
// a generator, a spread, a getter or a setter: code the reading doesn't run, or properties from elsewhere
|
|
1346
|
+
if (isPunct(tokens[k], "*") || isPunct(tokens[k], ".") || ((isWord(tokens[k], "get") || isWord(tokens[k], "set")) && !endsKey(k + 1)))
|
|
1347
|
+
return false;
|
|
1348
|
+
const t = tokens[k];
|
|
1349
|
+
let key;
|
|
1350
|
+
let at = k;
|
|
1351
|
+
let next = k + 1;
|
|
1352
|
+
if (t?.kind === "ident" || literal(t))
|
|
1353
|
+
key = t.value;
|
|
1354
|
+
else if (isPunct(t, "[") && literal(tokens[k + 1]) && isPunct(tokens[k + 2], "]")) {
|
|
1355
|
+
key = tokens[k + 1].value;
|
|
1356
|
+
at = k + 1;
|
|
1357
|
+
next = k + 3;
|
|
1358
|
+
}
|
|
1161
1359
|
else
|
|
1162
|
-
|
|
1163
|
-
|
|
1360
|
+
return false;
|
|
1361
|
+
// `__proto__: …` sets the object's prototype
|
|
1362
|
+
if (key === "__proto__")
|
|
1363
|
+
return false;
|
|
1364
|
+
const hook = config && HOOKS.has(key);
|
|
1365
|
+
if (next === y) {
|
|
1366
|
+
if (async || t?.kind !== "ident" || !use(k, "data"))
|
|
1367
|
+
return false;
|
|
1368
|
+
list.push({ key, at, kind: "shorthand", from: k, to: y });
|
|
1369
|
+
}
|
|
1370
|
+
else if (isPunct(tokens[next], ":") && !async) {
|
|
1371
|
+
if (functionOf(tokens, b.after, next + 1, y) !== null) {
|
|
1372
|
+
if (!hook)
|
|
1373
|
+
return false;
|
|
1374
|
+
list.push({ key, at, kind: "hook", from: next + 1, to: y });
|
|
1375
|
+
}
|
|
1376
|
+
else {
|
|
1377
|
+
if (!data(next + 1, y))
|
|
1378
|
+
return false;
|
|
1379
|
+
list.push({ key, at, kind: "value", from: next + 1, to: y });
|
|
1380
|
+
}
|
|
1381
|
+
}
|
|
1382
|
+
else if (isPunct(tokens[next], "(")) {
|
|
1383
|
+
// a method: `async headers() {…}`
|
|
1384
|
+
const body = bodyAfter(tokens, b.after[next]);
|
|
1385
|
+
if (!hook || body === null || b.after[body] !== y)
|
|
1386
|
+
return false;
|
|
1387
|
+
list.push({ key, at, kind: "hook", from: next, to: y });
|
|
1388
|
+
}
|
|
1389
|
+
else
|
|
1390
|
+
return false;
|
|
1164
1391
|
}
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1392
|
+
props.set(open, list);
|
|
1393
|
+
return true;
|
|
1394
|
+
});
|
|
1395
|
+
// data in [from, to), to its end but for types
|
|
1396
|
+
const data = (from, to) => guard(false, () => {
|
|
1397
|
+
const e = expr(from, to);
|
|
1398
|
+
return e > from && (e === to || suffix(e, to));
|
|
1399
|
+
});
|
|
1400
|
+
const expr = (k, to) => guard(-1, () => {
|
|
1401
|
+
let j = binary(k, to);
|
|
1402
|
+
if (j < 0 || j >= to || !isPunct(tokens[j], "?") || isPunct(tokens[j + 1], "?") || isPunct(tokens[j + 1], "."))
|
|
1403
|
+
return j;
|
|
1404
|
+
j = expr(j + 1, to);
|
|
1405
|
+
if (j < 0 || j >= to || !isPunct(tokens[j], ":"))
|
|
1406
|
+
return -1;
|
|
1407
|
+
return expr(j + 1, to);
|
|
1408
|
+
});
|
|
1409
|
+
const binary = (k, to) => {
|
|
1410
|
+
let j = unary(k, to);
|
|
1411
|
+
while (j >= 0 && j < to) {
|
|
1412
|
+
const n = operator(j);
|
|
1413
|
+
if (n <= 0)
|
|
1414
|
+
return n < 0 ? -1 : j;
|
|
1415
|
+
j = unary(j + n, to);
|
|
1174
1416
|
}
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1417
|
+
return j;
|
|
1418
|
+
};
|
|
1419
|
+
// the binary operator at `j`, by its tokens: 0 where the operand ends, -1 for an assignment or an operator the subset doesn't read
|
|
1420
|
+
const operator = (j) => {
|
|
1421
|
+
const t = tokens[j];
|
|
1422
|
+
const u = tokens[j + 1];
|
|
1423
|
+
const v = tokens[j + 2];
|
|
1424
|
+
if (t?.kind !== "punct")
|
|
1425
|
+
return 0;
|
|
1426
|
+
switch (t.value) {
|
|
1427
|
+
case "=":
|
|
1428
|
+
case "!":
|
|
1429
|
+
return isPunct(u, "=") ? (isPunct(v, "=") ? 3 : 2) : -1;
|
|
1430
|
+
case "&":
|
|
1431
|
+
case "|":
|
|
1432
|
+
case "?":
|
|
1433
|
+
if (!isPunct(u, t.value))
|
|
1434
|
+
return t.value === "?" ? 0 : -1;
|
|
1435
|
+
return isPunct(v, "=") ? -1 : 2;
|
|
1436
|
+
case "+":
|
|
1437
|
+
return isPunct(u, "+") || isPunct(u, "=") ? -1 : 1;
|
|
1438
|
+
}
|
|
1439
|
+
return 0;
|
|
1440
|
+
};
|
|
1441
|
+
const unary = (k, to) => {
|
|
1442
|
+
let j = k;
|
|
1443
|
+
while (j < to && j < k + 16 && (isPunct(tokens[j], "!") || (isPunct(tokens[j], "-") && !isPunct(tokens[j + 1], "-"))))
|
|
1444
|
+
j++;
|
|
1445
|
+
return primary(j, to);
|
|
1446
|
+
};
|
|
1447
|
+
const numberEnd = (k, to) => {
|
|
1448
|
+
let j = k;
|
|
1449
|
+
let point = false;
|
|
1450
|
+
while (j < to && (j === k || tokens[j].nl !== true)) {
|
|
1451
|
+
if (isDigit(tokens[j]))
|
|
1452
|
+
j++;
|
|
1453
|
+
else if (isPunct(tokens[j], ".") && !point && isDigit(tokens[j + 1])) {
|
|
1454
|
+
point = true;
|
|
1455
|
+
j++;
|
|
1456
|
+
}
|
|
1182
1457
|
else
|
|
1183
|
-
|
|
1184
|
-
return;
|
|
1458
|
+
break;
|
|
1185
1459
|
}
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1460
|
+
return j;
|
|
1461
|
+
};
|
|
1462
|
+
// the arguments of the call that opens at `open`, each data
|
|
1463
|
+
const args = (open, to) => {
|
|
1464
|
+
const close = b.after[open];
|
|
1465
|
+
const items = close <= to ? listOf(open) : null;
|
|
1466
|
+
return items !== null && items.every(([x, y]) => data(x, y)) ? close : -1;
|
|
1467
|
+
};
|
|
1468
|
+
const primary = (k, to) => {
|
|
1469
|
+
const t = tokens[k];
|
|
1470
|
+
if (t === undefined || k >= to)
|
|
1471
|
+
return -1;
|
|
1472
|
+
if (t.kind === "string")
|
|
1473
|
+
return literal(t) ? k + 1 : -1;
|
|
1474
|
+
if (t.kind === "regex")
|
|
1475
|
+
return k + 1;
|
|
1476
|
+
if (t.kind === "punct") {
|
|
1477
|
+
if (isDigit(t) || (t.value === "." && isDigit(tokens[k + 1])))
|
|
1478
|
+
return numberEnd(k, to);
|
|
1479
|
+
const close = b.after[k];
|
|
1480
|
+
if (!OPENS.has(t.value) || close > to)
|
|
1481
|
+
return -1;
|
|
1482
|
+
if (t.value === "(")
|
|
1483
|
+
return data(k + 1, close - 1) ? close : -1;
|
|
1484
|
+
if (t.value === "[")
|
|
1485
|
+
return listOf(k)?.every(([x, y]) => data(x, y)) ? close : -1;
|
|
1486
|
+
return object(k, false) ? close : -1;
|
|
1193
1487
|
}
|
|
1194
|
-
const
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1488
|
+
const next = tokens[k + 1];
|
|
1489
|
+
switch (t.value) {
|
|
1490
|
+
case "true":
|
|
1491
|
+
case "false":
|
|
1492
|
+
case "null":
|
|
1493
|
+
case "undefined":
|
|
1494
|
+
case "NaN":
|
|
1495
|
+
case "Infinity":
|
|
1496
|
+
return k + 1;
|
|
1497
|
+
case "process":
|
|
1498
|
+
// `process.env.NAME`, `process.env["NAME"]`, `process.cwd()`
|
|
1499
|
+
if (!isPunct(next, "."))
|
|
1500
|
+
return -1;
|
|
1501
|
+
if (isWord(tokens[k + 2], "env") && isPunct(tokens[k + 3], ".") && tokens[k + 4]?.kind === "ident")
|
|
1502
|
+
return k + 5;
|
|
1503
|
+
if (isWord(tokens[k + 2], "env") && isPunct(tokens[k + 3], "[") && literal(tokens[k + 4]) && isPunct(tokens[k + 5], "]"))
|
|
1504
|
+
return k + 6;
|
|
1505
|
+
return isWord(tokens[k + 2], "cwd") && isPunct(tokens[k + 3], "(") && isPunct(tokens[k + 4], ")") ? k + 5 : -1;
|
|
1506
|
+
case "import":
|
|
1507
|
+
// `import.meta.dirname`, `.filename`, `.url`
|
|
1508
|
+
return isPunct(next, ".") && isWord(tokens[k + 2], "meta") && isPunct(tokens[k + 3], ".") && ["dirname", "filename", "url"].some((w) => isWord(tokens[k + 4], w)) ? k + 5 : -1;
|
|
1509
|
+
case "require":
|
|
1510
|
+
// `require.resolve("m")`: a path; nothing is loaded
|
|
1511
|
+
return isPunct(next, ".") && isWord(tokens[k + 2], "resolve") && isPunct(tokens[k + 3], "(") && literal(tokens[k + 4]) && isPunct(tokens[k + 5], ")") ? k + 6 : -1;
|
|
1512
|
+
case "new":
|
|
1513
|
+
// `new URL("…")`
|
|
1514
|
+
return isWord(next, "URL") && isPunct(tokens[k + 2], "(") ? args(k + 2, to) : -1;
|
|
1515
|
+
}
|
|
1516
|
+
if ((t.value === "__dirname" || t.value === "__filename") && !bindings.has(t.value))
|
|
1517
|
+
return k + 1;
|
|
1518
|
+
// a name declared with data — not a member of it or a call
|
|
1519
|
+
return !isPunct(next, ".") && !isPunct(next, "(") && use(k, "data") ? k + 1 : -1;
|
|
1520
|
+
};
|
|
1521
|
+
// statements
|
|
1522
|
+
const exportValue = (from, e) => {
|
|
1523
|
+
if (from >= e)
|
|
1524
|
+
return false;
|
|
1525
|
+
exports++;
|
|
1526
|
+
exported = { from, to: e };
|
|
1527
|
+
return true;
|
|
1528
|
+
};
|
|
1529
|
+
// `import type { NextConfig } from "next"`, `import type X from "m"`, `import type * as M from "m"`: types, erased — the only import, since a module
|
|
1530
|
+
// loaded runs its code, and which module a name loads the spelling doesn't show (Node's self-reference, a next.config.ts's tsconfig paths)
|
|
1531
|
+
const typeImport = (k, e) => {
|
|
1532
|
+
if (!isWord(tokens[k + 1], "type") || !(e >= k + 5 && isWord(tokens[e - 2], "from") && literal(tokens[e - 1])))
|
|
1533
|
+
return false;
|
|
1534
|
+
const stop = e - 2;
|
|
1535
|
+
const j = k + 2;
|
|
1536
|
+
if (isPunct(tokens[j], "{"))
|
|
1537
|
+
return b.after[j] === stop;
|
|
1538
|
+
if (isPunct(tokens[j], "*"))
|
|
1539
|
+
return isWord(tokens[j + 1], "as") && tokens[j + 2]?.kind === "ident" && j + 3 === stop;
|
|
1540
|
+
return tokens[j]?.kind === "ident" && !isWord(tokens[j], "from") && j + 1 === stop;
|
|
1541
|
+
};
|
|
1542
|
+
const exportStatement = (k, e) => {
|
|
1543
|
+
const t = tokens[k + 1];
|
|
1544
|
+
if (isWord(t, "default"))
|
|
1545
|
+
return exportValue(k + 2, e);
|
|
1546
|
+
// `export type { T }`, `export type { T } from "m"`: types, erased
|
|
1547
|
+
if (isWord(t, "type") && isPunct(tokens[k + 2], "{")) {
|
|
1548
|
+
const close = b.after[k + 2];
|
|
1549
|
+
return close === e || (close + 2 === e && isWord(tokens[close], "from") && literal(tokens[close + 1]));
|
|
1550
|
+
}
|
|
1551
|
+
// `export { config as default }`: the one export
|
|
1552
|
+
if (!isPunct(t, "{") || b.after[k + 1] !== e)
|
|
1553
|
+
return false;
|
|
1554
|
+
const items = listOf(k + 1);
|
|
1555
|
+
if (items === null || items.length !== 1)
|
|
1556
|
+
return false;
|
|
1557
|
+
const [x, y] = items[0];
|
|
1558
|
+
const named = isWord(tokens[x + 2], "default") || (literal(tokens[x + 2]) && tokens[x + 2].value === "default");
|
|
1559
|
+
if (!(y === x + 3 && tokens[x].kind === "ident" && isWord(tokens[x + 1], "as") && named))
|
|
1560
|
+
return false;
|
|
1561
|
+
exports++;
|
|
1562
|
+
exported = { name: x };
|
|
1563
|
+
return true;
|
|
1564
|
+
};
|
|
1565
|
+
const declaration = (j, e) => {
|
|
1566
|
+
if (j >= e)
|
|
1567
|
+
return false;
|
|
1568
|
+
for (const [x, y] of splitAt(j, e)) {
|
|
1569
|
+
if (x === y)
|
|
1570
|
+
return false;
|
|
1571
|
+
const t = tokens[x];
|
|
1572
|
+
if (t.kind === "ident") {
|
|
1573
|
+
let v = x + 1;
|
|
1574
|
+
// a type: `const nextConfig: NextConfig = …`
|
|
1575
|
+
if (isPunct(tokens[v], ":")) {
|
|
1576
|
+
v = simpleType(v + 1, y);
|
|
1577
|
+
if (v < 0)
|
|
1578
|
+
return false;
|
|
1210
1579
|
}
|
|
1580
|
+
if (v === y) {
|
|
1581
|
+
if (!bind(x, { kind: "none" }))
|
|
1582
|
+
return false;
|
|
1583
|
+
continue;
|
|
1584
|
+
}
|
|
1585
|
+
if (!isPunct(tokens[v], "=") || isPunct(tokens[v + 1], "=") || v + 1 >= y || !bind(x, { kind: "value", from: v + 1, to: y }))
|
|
1586
|
+
return false;
|
|
1211
1587
|
}
|
|
1588
|
+
else
|
|
1589
|
+
return false;
|
|
1212
1590
|
}
|
|
1213
|
-
|
|
1214
|
-
opaque = true;
|
|
1591
|
+
return true;
|
|
1215
1592
|
};
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1593
|
+
const statement = (k, e) => {
|
|
1594
|
+
const t = tokens[k];
|
|
1595
|
+
// a directive: "use strict"
|
|
1596
|
+
if (e === k + 1 && literal(t))
|
|
1597
|
+
return true;
|
|
1598
|
+
if (t.kind !== "ident")
|
|
1599
|
+
return false;
|
|
1600
|
+
switch (t.value) {
|
|
1601
|
+
case "import":
|
|
1602
|
+
return typeImport(k, e);
|
|
1603
|
+
case "export":
|
|
1604
|
+
return exportStatement(k, e);
|
|
1605
|
+
case "const":
|
|
1606
|
+
case "let":
|
|
1607
|
+
case "var":
|
|
1608
|
+
return declaration(k + 1, e);
|
|
1609
|
+
case "module":
|
|
1610
|
+
return isPunct(tokens[k + 1], ".") && isWord(tokens[k + 2], "exports") && isPunct(tokens[k + 3], "=") && !isPunct(tokens[k + 4], "=") && exportValue(k + 4, e);
|
|
1611
|
+
}
|
|
1612
|
+
return false;
|
|
1613
|
+
};
|
|
1614
|
+
for (let k = 0; k < tokens.length;) {
|
|
1615
|
+
if (isPunct(tokens[k], ";")) {
|
|
1616
|
+
k++;
|
|
1617
|
+
continue;
|
|
1618
|
+
}
|
|
1619
|
+
const e = statementEnd(k);
|
|
1620
|
+
if (e === k || !statement(k, e))
|
|
1621
|
+
return null;
|
|
1622
|
+
k = e;
|
|
1623
|
+
}
|
|
1624
|
+
if (exports > 1)
|
|
1625
|
+
return null;
|
|
1626
|
+
const out = exported;
|
|
1627
|
+
if (out !== null && !("name" in out ? use(out.name, "config") : configValue(out.from, out.to)))
|
|
1628
|
+
return null;
|
|
1629
|
+
// a name the load doesn't use still has its value computed: data, or a module loaded
|
|
1630
|
+
for (const binding of bindings.values()) {
|
|
1631
|
+
if (binding.uses > 0 || binding.kind !== "value")
|
|
1632
|
+
continue;
|
|
1633
|
+
binding.role = "data";
|
|
1634
|
+
binding.uses++;
|
|
1635
|
+
if (!data(binding.from, binding.to))
|
|
1636
|
+
return null;
|
|
1637
|
+
}
|
|
1638
|
+
return { configs, props };
|
|
1639
|
+
}
|
|
1640
|
+
/** next.config's `pageExtensions`, `basePath` and `output`, read only when
|
|
1641
|
+
the file is written in a declarative subset of JavaScript, where no code
|
|
1642
|
+
runs while Next loads it — neither the file's nor a module's:
|
|
1643
|
+
|
|
1644
|
+
- statements: type imports (`import type { NextConfig } from "next"`),
|
|
1645
|
+
declarations, and one export of the config object itself —
|
|
1646
|
+
`module.exports = {…}`, `export default nextConfig` or `export {
|
|
1647
|
+
config as default }`;
|
|
1648
|
+
- values: literals, and arrays and objects of them, `process.env.NAME`,
|
|
1649
|
+
`process.cwd()`, `__dirname`, `import.meta.dirname`, `new URL(…)`,
|
|
1650
|
+
`require.resolve("…")`, names declared so; `!`, `+`, comparisons,
|
|
1651
|
+
`&&`, `||`, `??` and `? :` over them; a type after `as` or
|
|
1652
|
+
`satisfies`;
|
|
1653
|
+
- functions only as Next's hooks (`webpack`, `headers`, `redirects`,
|
|
1654
|
+
`rewrites`, `generateBuildId`, `exportPathMap`) at the top of the
|
|
1655
|
+
exported object: Next calls them once it has loaded the config.
|
|
1656
|
+
|
|
1657
|
+
Anything else makes it null: a module loaded in any way (`import "…"`,
|
|
1658
|
+
`import x from "…"`, `require(…)`) — its code runs, and which module a
|
|
1659
|
+
name loads its spelling doesn't show (Node's self-reference, a
|
|
1660
|
+
next.config.ts's tsconfig `paths`); so a plugin's call around the
|
|
1661
|
+
config (`withMDX(nextConfig)`) and a `path` helper too (block 348). And
|
|
1662
|
+
any other statement, a function or class of the file, a getter, a
|
|
1663
|
+
spread, a member of a value, a call, a name bound twice or over one the
|
|
1664
|
+
subset reads (`require`, `process`), the config's name used twice. Each
|
|
1665
|
+
setting: a literal, once, in that object — `pageExtensions` or
|
|
1666
|
+
`basePath` named anywhere else in the file makes it null, and an
|
|
1667
|
+
`output` the reading can't take (an expression, a second one, one a
|
|
1668
|
+
hook's code writes) makes only `output` null. `live.test.ts` holds the
|
|
1669
|
+
reading to Next's own `loadConfig` over generated configs.
|
|
1670
|
+
|
|
1671
|
+
What the reading trusts: Next calls the hooks only after loading, and a
|
|
1672
|
+
hook leaves the settings it loaded alone. */
|
|
1673
|
+
export function nextConfigOf(config) {
|
|
1674
|
+
if (config === null)
|
|
1675
|
+
return { pageExtensions: PAGE_EXTENSIONS, basePath: "" };
|
|
1676
|
+
// what JavaScript reads apart from the tokens: a hashbang line, CommonJS's `<!--` and `-->` comments — `-->` may follow a `/* … */` on its line — anywhere
|
|
1677
|
+
if (config.startsWith("#!") || /<!--|-->/.test(config))
|
|
1678
|
+
return null;
|
|
1679
|
+
// every line terminator JavaScript knows ends a `//` comment: `\r`, U+2028 and U+2029 too
|
|
1680
|
+
config = config.replace(/\r\n?|[\u2028\u2029]/g, "\n");
|
|
1681
|
+
const hidden = [];
|
|
1682
|
+
const ambiguous = [];
|
|
1683
|
+
const tokens = tokenize(config, hidden, ambiguous, true);
|
|
1684
|
+
const b = bracketsOf(tokens);
|
|
1685
|
+
if (!balanced(tokens, b) || !slashesAgree(tokens, b))
|
|
1686
|
+
return null;
|
|
1687
|
+
// the code of each template's `${…}` — a hook's only, since data has no such template — read for the names it mentions
|
|
1688
|
+
const all = [...tokens];
|
|
1689
|
+
for (let i = 0; i < hidden.length; i++) {
|
|
1690
|
+
if (i >= 1000)
|
|
1691
|
+
return null;
|
|
1692
|
+
const part = tokenize(`;\n${hidden[i]}\n;`, hidden, ambiguous, true);
|
|
1693
|
+
if (!slashesAgree(part, bracketsOf(part)))
|
|
1694
|
+
return null;
|
|
1695
|
+
for (const t of part)
|
|
1696
|
+
all.push(t);
|
|
1697
|
+
}
|
|
1698
|
+
// a `/` right after `}`, in the file or a template's code, may start a regex that holds a brace
|
|
1699
|
+
if (ambiguous.length > 0)
|
|
1700
|
+
return null;
|
|
1701
|
+
// an identifier with an escape in its name, a backslash outside a string, a string a line break cut off
|
|
1702
|
+
if (all.some((t) => isPunct(t, "\\") || (t.kind === "ident" && t.escaped === true) || (t.kind === "string" && t.unclosed === true)))
|
|
1703
|
+
return null;
|
|
1704
|
+
const subset = declarative(tokens, b);
|
|
1705
|
+
if (subset === null)
|
|
1221
1706
|
return null;
|
|
1222
|
-
// each setting: a literal,
|
|
1707
|
+
// each setting: a literal, once, in the config object
|
|
1223
1708
|
const settings = {};
|
|
1224
1709
|
const accepted = new Set();
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
for (
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1710
|
+
// `output` never makes the config unreadable: only its value is in doubt — undefined while unset, null when it can't be read
|
|
1711
|
+
let output;
|
|
1712
|
+
for (const open of subset.configs) {
|
|
1713
|
+
for (const p of subset.props.get(open) ?? []) {
|
|
1714
|
+
// what Next takes before the keys: `then` — it awaits the config — and `default`, which a next.config.ts transpiled to CommonJS is read from
|
|
1715
|
+
if (p.key === "then" || p.key === "default")
|
|
1716
|
+
return null;
|
|
1717
|
+
const value = tokens[p.from];
|
|
1718
|
+
const single = p.kind === "value" && p.to === p.from + 1 && literal(value);
|
|
1719
|
+
if (p.key === "basePath") {
|
|
1720
|
+
// a path from the root, without a trailing slash — anything else Next refuses
|
|
1721
|
+
if (settings.basePath !== undefined || !single || (value.value !== "" && !/^\/[^?#\s]*[^/?#\s]$/.test(value.value)))
|
|
1232
1722
|
return null;
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1723
|
+
settings.basePath = value.value;
|
|
1724
|
+
accepted.add(p.at);
|
|
1725
|
+
}
|
|
1726
|
+
else if (p.key === "pageExtensions") {
|
|
1727
|
+
if (settings.pageExtensions !== undefined || p.kind !== "value" || !isPunct(value, "[") || b.after[p.from] !== p.to)
|
|
1728
|
+
return null;
|
|
1729
|
+
const list = [];
|
|
1730
|
+
for (const [x, y] of itemsOf(tokens, b.after, p.from)) {
|
|
1731
|
+
if (x === y)
|
|
1732
|
+
continue;
|
|
1733
|
+
if (y !== x + 1 || !literal(tokens[x]))
|
|
1240
1734
|
return null;
|
|
1241
|
-
|
|
1242
|
-
if (name === "basePath") {
|
|
1243
|
-
const value = tokens[v];
|
|
1244
|
-
// a path from the root, without a trailing slash — anything else Next refuses
|
|
1245
|
-
if (!plain(value) || (value.value !== "" && !/^\/[^?#\s]*[^/?#\s]$/.test(value.value)))
|
|
1246
|
-
return null;
|
|
1247
|
-
settings[name] = value.value;
|
|
1248
|
-
}
|
|
1249
|
-
else {
|
|
1250
|
-
const list = [];
|
|
1251
|
-
for (let j = v + 1; j < valueEnd - 1; j++) {
|
|
1252
|
-
if (plain(tokens[j]) && (isPunct(tokens[j + 1], ",") || j + 1 === valueEnd - 1))
|
|
1253
|
-
list.push(tokens[j].value);
|
|
1254
|
-
else if (!isPunct(tokens[j], ","))
|
|
1255
|
-
return null;
|
|
1256
|
-
}
|
|
1257
|
-
settings[name] = list;
|
|
1258
|
-
}
|
|
1259
|
-
accepted.add(isPunct(tokens[next - 1], "]") ? next - 2 : next - 1);
|
|
1735
|
+
list.push(tokens[x].value);
|
|
1260
1736
|
}
|
|
1737
|
+
settings.pageExtensions = list;
|
|
1738
|
+
accepted.add(p.at);
|
|
1261
1739
|
}
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
k = b.after[k] - 1;
|
|
1265
|
-
else if (isPunct(t, ","))
|
|
1266
|
-
property = true;
|
|
1740
|
+
else if (p.key === "output")
|
|
1741
|
+
output = output === undefined && single ? value.value : null;
|
|
1267
1742
|
}
|
|
1268
1743
|
}
|
|
1269
|
-
|
|
1744
|
+
// a setting's name anywhere else — a nested object, a hook's code, a string — or inside a longer string (JSON the code parses)
|
|
1745
|
+
if (all.some((t, k) => (t.kind === "ident" || t.kind === "string") && SETTINGS.includes(t.value) && !accepted.has(k)))
|
|
1270
1746
|
return null;
|
|
1271
|
-
|
|
1747
|
+
if (all.some((t) => t.kind === "string" && !SETTINGS.includes(t.value) && SETTINGS.some((s) => t.value.includes(s))))
|
|
1748
|
+
return null;
|
|
1749
|
+
// a hook's code that copies properties, sets a prototype or writes a member by a name it computes — `config[name] = …`, `for (config[name] of …)`:
|
|
1750
|
+
// Next runs it after loading, and what it writes the reading can't place
|
|
1751
|
+
const ball = bracketsOf(all);
|
|
1752
|
+
for (let k = 0; k < all.length; k++) {
|
|
1753
|
+
const t = all[k];
|
|
1754
|
+
if ((t.kind === "ident" && COPIES.has(t.value)) || (t.kind === "string" && t.value === "__proto__"))
|
|
1755
|
+
return null;
|
|
1756
|
+
if (!isPunct(t, "["))
|
|
1757
|
+
continue;
|
|
1758
|
+
// unless a lone string or digits name it: `rules[0] = …`
|
|
1759
|
+
const close = ball.after[k] - 1;
|
|
1760
|
+
if (close === k + 2 && literal(all[k + 1]))
|
|
1761
|
+
continue;
|
|
1762
|
+
let digits = k + 1;
|
|
1763
|
+
while (digits < close && isDigit(all[digits]))
|
|
1764
|
+
digits++;
|
|
1765
|
+
if (digits === close && close > k + 1)
|
|
1766
|
+
continue;
|
|
1767
|
+
const prev = all[k - 1];
|
|
1768
|
+
const key = (isPunct(prev, "{") || isPunct(prev, ",") || isPunct(prev, "*") || (prev?.kind === "ident" && KEY_PREFIX.has(prev.value))) && (isPunct(all[close + 1], ":") || isPunct(all[close + 1], "("));
|
|
1769
|
+
const head = ball.parent[k];
|
|
1770
|
+
const loop = head >= 0 && isPunct(all[head], "(") && (isWord(all[head - 1], "for") || (isWord(all[head - 1], "await") && isWord(all[head - 2], "for"))) && (isWord(all[close + 1], "of") || isWord(all[close + 1], "in"));
|
|
1771
|
+
const member = (prev?.kind === "ident" || isPunct(prev, ")") || isPunct(prev, "]")) && (assigned(all, close) || loop);
|
|
1772
|
+
if (key || member)
|
|
1773
|
+
return null;
|
|
1774
|
+
}
|
|
1775
|
+
// and `output` a hook's code writes — `config.output = …`, `config["output"] = …`, in a pattern or a loop's head — or deletes
|
|
1776
|
+
const patterns = new Int32Array(all.length + 1);
|
|
1777
|
+
for (let k = 0; k < all.length; k++) {
|
|
1778
|
+
const after = ball.after[k];
|
|
1779
|
+
if ((isPunct(all[k], "{") || isPunct(all[k], "[")) && !DECLARE.has(all[k - 1]?.value ?? "") && isPunct(all[after], "=") && !isPunct(all[after + 1], "=")) {
|
|
1780
|
+
patterns[k]++;
|
|
1781
|
+
patterns[after]--;
|
|
1782
|
+
}
|
|
1783
|
+
}
|
|
1784
|
+
for (let k = 0, inPattern = 0; k < all.length; k++) {
|
|
1785
|
+
inPattern += patterns[k];
|
|
1786
|
+
const t = all[k];
|
|
1787
|
+
const dotted = t.kind === "ident" && t.value === "output" && isPunct(all[k - 1], ".");
|
|
1788
|
+
const bracketed = t.kind === "string" && t.value === "output" && isPunct(all[k - 1], "[") && isPunct(all[k + 1], "]");
|
|
1789
|
+
if (!dotted && !bracketed)
|
|
1790
|
+
continue;
|
|
1791
|
+
// parentheses around the target, TypeScript's `!` after it: `(config.output) = …`, `config.output! = …`
|
|
1792
|
+
let end = bracketed ? k + 1 : k;
|
|
1793
|
+
while (isPunct(all[end + 1], ")") || isPunct(all[end + 1], "!"))
|
|
1794
|
+
end++;
|
|
1795
|
+
const head = ball.parent[k - 1];
|
|
1796
|
+
const loop = head >= 0 && isPunct(all[head], "(") && (isWord(all[head - 1], "for") || (isWord(all[head - 1], "await") && isWord(all[head - 2], "for"))) && (isWord(all[end + 1], "of") || isWord(all[end + 1], "in"));
|
|
1797
|
+
if (changes(all, end) || inPattern > 0 || loop)
|
|
1798
|
+
output = null;
|
|
1799
|
+
}
|
|
1800
|
+
for (let k = 0; k < all.length; k++) {
|
|
1801
|
+
if (!isWord(all[k], "delete") || isPunct(all[k - 1], "."))
|
|
1802
|
+
continue;
|
|
1803
|
+
const to = initializerEnd(all, ball.after, k + 1);
|
|
1804
|
+
for (let j = k + 1; j < to; j++)
|
|
1805
|
+
if ((isWord(all[j], "output") && isPunct(all[j - 1], ".")) || (all[j].kind === "string" && all[j].value === "output"))
|
|
1806
|
+
output = null;
|
|
1807
|
+
}
|
|
1808
|
+
const read = { pageExtensions: settings.pageExtensions ?? PAGE_EXTENSIONS, basePath: settings.basePath ?? "" };
|
|
1809
|
+
if (output !== undefined)
|
|
1810
|
+
read.output = output;
|
|
1811
|
+
if (all.some((t) => (t.kind === "ident" || t.kind === "string") && t.value === "i18n"))
|
|
1812
|
+
read.i18n = true;
|
|
1813
|
+
return read;
|
|
1814
|
+
}
|
|
1815
|
+
/** Whether the tokens from `from` up to `to` are a value Next's build
|
|
1816
|
+
reads out of a module's `config` (extractValue): a string, a number,
|
|
1817
|
+
true, false, null or undefined, or an array or object of them — no
|
|
1818
|
+
spread, hole, computed key, shorthand, method or expression. */
|
|
1819
|
+
function extractable(tokens, after, from, to) {
|
|
1820
|
+
if (from >= to)
|
|
1821
|
+
return false;
|
|
1822
|
+
const t = tokens[from];
|
|
1823
|
+
if (to === from + 1 && (literal(t) || isWord(t, "true") || isWord(t, "false") || isWord(t, "null") || isWord(t, "undefined")))
|
|
1824
|
+
return true;
|
|
1825
|
+
// digits, and one decimal point: `12`, `1.5`
|
|
1826
|
+
if (/^\d$/.test(t.kind === "punct" ? t.value : "")) {
|
|
1827
|
+
let points = 0;
|
|
1828
|
+
for (let k = from; k < to; k++) {
|
|
1829
|
+
const u = tokens[k];
|
|
1830
|
+
if (u.kind !== "punct" || !/^[\d.]$/.test(u.value) || (u.value === "." && ++points > 1))
|
|
1831
|
+
return false;
|
|
1832
|
+
}
|
|
1833
|
+
return true;
|
|
1834
|
+
}
|
|
1835
|
+
if ((!isPunct(t, "[") && !isPunct(t, "{")) || after[from] !== to)
|
|
1836
|
+
return false;
|
|
1837
|
+
return itemsOf(tokens, after, from).every(([a, z], i, all) => {
|
|
1838
|
+
// a trailing comma leaves nothing after it
|
|
1839
|
+
if (a === z)
|
|
1840
|
+
return i === all.length - 1 && all.length > 1;
|
|
1841
|
+
if (isSpread(tokens, a))
|
|
1842
|
+
return false;
|
|
1843
|
+
if (t.value === "[")
|
|
1844
|
+
return extractable(tokens, after, a, z);
|
|
1845
|
+
const key = tokens[a];
|
|
1846
|
+
return (key.kind === "ident" || literal(key)) && isPunct(tokens[a + 1], ":") && extractable(tokens, after, a + 2, z);
|
|
1847
|
+
});
|
|
1848
|
+
}
|
|
1849
|
+
/** The items of the array or object that opens at `open`, as token ranges
|
|
1850
|
+
between its top-level commas. */
|
|
1851
|
+
function itemsOf(tokens, after, open) {
|
|
1852
|
+
const close = after[open] - 1;
|
|
1853
|
+
const items = [];
|
|
1854
|
+
let start = open + 1;
|
|
1855
|
+
for (let k = start; k <= close; k++) {
|
|
1856
|
+
const u = tokens[k];
|
|
1857
|
+
if (k < close && u.kind === "punct" && "([{".includes(u.value))
|
|
1858
|
+
k = after[k] - 1;
|
|
1859
|
+
else if (k === close || isPunct(u, ",")) {
|
|
1860
|
+
items.push([start, k]);
|
|
1861
|
+
start = k + 1;
|
|
1862
|
+
}
|
|
1863
|
+
}
|
|
1864
|
+
return items;
|
|
1865
|
+
}
|
|
1866
|
+
/** The matcher of a middleware file, read as Next's build reads it: only
|
|
1867
|
+
`export const config = { … }` (a type or `satisfies` allowed), every
|
|
1868
|
+
value in it a literal (extractable), `matcher` a string or an array of
|
|
1869
|
+
strings and `{ source }` objects. Anything else Next can't read either
|
|
1870
|
+
and runs the middleware on every path — or reads what doctor can't. */
|
|
1871
|
+
export function middlewareMatcher(text) {
|
|
1872
|
+
const tokens = tokenize(text);
|
|
1873
|
+
const b = bracketsOf(tokens);
|
|
1874
|
+
let config = null;
|
|
1875
|
+
let declared = -1;
|
|
1876
|
+
for (let i = 0; i < tokens.length; i++) {
|
|
1877
|
+
if (!isWord(tokens[i], "export") || isPunct(tokens[i - 1], ".") || !isWord(tokens[i + 1], "const") || !isWord(tokens[i + 2], "config"))
|
|
1878
|
+
continue;
|
|
1879
|
+
if (config !== null)
|
|
1880
|
+
return "unread";
|
|
1881
|
+
declared = i + 2;
|
|
1882
|
+
let eq = i + 3;
|
|
1883
|
+
if (isPunct(tokens[eq], ":"))
|
|
1884
|
+
eq = typeEnd(tokens, b.after, eq + 1, tokens.length);
|
|
1885
|
+
if (eq < 0 || !isPunct(tokens[eq], "=") || isPunct(tokens[eq + 1], "=") || !isPunct(tokens[eq + 1], "{"))
|
|
1886
|
+
return "unread";
|
|
1887
|
+
const open = eq + 1;
|
|
1888
|
+
const end = b.after[open];
|
|
1889
|
+
// the statement ends there — `;`, `,`, a new line, the file — or goes on with `satisfies T`, which Next reads through
|
|
1890
|
+
const tail = tokens[end];
|
|
1891
|
+
if (tail && !isPunct(tail, ";") && !isPunct(tail, ",") && !isWord(tail, "satisfies") && !(tail.nl === true && tail.kind === "ident"))
|
|
1892
|
+
return "unread";
|
|
1893
|
+
if (!extractable(tokens, b.after, open, end))
|
|
1894
|
+
return "unread";
|
|
1895
|
+
config = "none";
|
|
1896
|
+
for (const [a, z] of itemsOf(tokens, b.after, open)) {
|
|
1897
|
+
if (a === z || tokens[a].value !== "matcher")
|
|
1898
|
+
continue;
|
|
1899
|
+
if (config !== "none")
|
|
1900
|
+
return "unread";
|
|
1901
|
+
const v = a + 2;
|
|
1902
|
+
if (z === v + 1 && literal(tokens[v]))
|
|
1903
|
+
config = [tokens[v].value];
|
|
1904
|
+
else if (isPunct(tokens[v], "[")) {
|
|
1905
|
+
const sources = [];
|
|
1906
|
+
for (const [c, d] of itemsOf(tokens, b.after, v)) {
|
|
1907
|
+
if (c === d)
|
|
1908
|
+
continue;
|
|
1909
|
+
if (d === c + 1 && literal(tokens[c]))
|
|
1910
|
+
sources.push(tokens[c].value);
|
|
1911
|
+
else if (isPunct(tokens[c], "{")) {
|
|
1912
|
+
const source = itemsOf(tokens, b.after, c).filter(([e]) => tokens[e]?.value === "source" && isPunct(tokens[e + 1], ":"));
|
|
1913
|
+
if (source.length !== 1 || source[0][1] !== source[0][0] + 3 || !literal(tokens[source[0][0] + 2]))
|
|
1914
|
+
return "unread";
|
|
1915
|
+
sources.push(tokens[source[0][0] + 2].value);
|
|
1916
|
+
}
|
|
1917
|
+
else
|
|
1918
|
+
return "unread";
|
|
1919
|
+
}
|
|
1920
|
+
config = sources;
|
|
1921
|
+
}
|
|
1922
|
+
else
|
|
1923
|
+
return "unread";
|
|
1924
|
+
}
|
|
1925
|
+
i = end - 1;
|
|
1926
|
+
}
|
|
1927
|
+
// any other export that may be `config` — `export { config }`, `export let config`, a later declarator, `export *`, CommonJS — Next's reading doesn't see
|
|
1928
|
+
const rest = declared === -1 ? tokens : tokens.map((t, k) => (k === declared ? { ...t, value: "" } : t));
|
|
1929
|
+
if (setsExport(rest, b.after, "config", () => false))
|
|
1930
|
+
return "unread";
|
|
1931
|
+
return config ?? "none";
|
|
1932
|
+
}
|
|
1933
|
+
/** Whether `c` is one of the characters — never for the end of the string. */
|
|
1934
|
+
const oneOf = (chars, c) => c !== undefined && c.length === 1 && chars.includes(c);
|
|
1935
|
+
// what a path may hold for doctor to place it among a matcher's sources: nothing a URL would encode
|
|
1936
|
+
const PLAIN_PATH = /^\/[A-Za-z0-9/_.~-]*$/;
|
|
1937
|
+
/** Whether a matcher source may take the path `at` (without the basePath,
|
|
1938
|
+
which Next puts in front of both). False only when it surely can't: the
|
|
1939
|
+
literal the source starts with isn't how the path starts, or right
|
|
1940
|
+
after it comes a group — path-to-regexp's `(…)`, kept verbatim in the
|
|
1941
|
+
regex Next builds — that matches no start of the rest, in either case.
|
|
1942
|
+
Everything else may. */
|
|
1943
|
+
export function sourceMayTake(source, at) {
|
|
1944
|
+
if (!source.startsWith("/") || !PLAIN_PATH.test(at))
|
|
1945
|
+
return true;
|
|
1946
|
+
let i = 0;
|
|
1947
|
+
while (i < source.length && !":(){}*+?\\".includes(source[i]))
|
|
1948
|
+
i++;
|
|
1949
|
+
// a `/` or `.` right before a param or a group is its prefix, which a modifier makes optional: `/blog/:path*` takes `/blog`
|
|
1950
|
+
const required = oneOf(":(", source[i]) && oneOf("/.", source[i - 1]) ? i - 1 : i;
|
|
1951
|
+
const start = source.slice(0, required).toLowerCase();
|
|
1952
|
+
// the tab may ask with a trailing slash that Next takes off
|
|
1953
|
+
const paths = [at, at === "/" ? at : `${at}/`].map((p) => p.toLowerCase());
|
|
1954
|
+
if (!paths.some((p) => p.startsWith(start)))
|
|
1955
|
+
return false;
|
|
1956
|
+
if (source[i] !== "(" || source[i + 1] === "?")
|
|
1957
|
+
return true;
|
|
1958
|
+
// the group as path-to-regexp reads it: to its balanced `)`, a backslash taking the next character along
|
|
1959
|
+
let depth = 1;
|
|
1960
|
+
let j = i + 1;
|
|
1961
|
+
for (; j < source.length && depth > 0; j++) {
|
|
1962
|
+
const c = source[j];
|
|
1963
|
+
if (c === "\\")
|
|
1964
|
+
j++;
|
|
1965
|
+
else if (c === ")")
|
|
1966
|
+
depth--;
|
|
1967
|
+
// a capturing group inside is refused
|
|
1968
|
+
else if (c === "(" && source[j + 1] !== "?")
|
|
1969
|
+
return true;
|
|
1970
|
+
else if (c === "(")
|
|
1971
|
+
depth++;
|
|
1972
|
+
}
|
|
1973
|
+
// a modifier may skip the group; a backreference numbers groups differently inside Next's regex
|
|
1974
|
+
if (depth !== 0 || oneOf("?*+", source[j]) || /\\\d/.test(source.slice(i, j)))
|
|
1975
|
+
return true;
|
|
1976
|
+
// without a modifier the group's prefix is required: the group starts right after the whole literal
|
|
1977
|
+
const literalStart = source.slice(0, i).toLowerCase();
|
|
1978
|
+
const group = source.slice(i + 1, j - 1);
|
|
1979
|
+
try {
|
|
1980
|
+
for (const flags of ["y", "iy"]) {
|
|
1981
|
+
const re = new RegExp(group, flags);
|
|
1982
|
+
for (const [n, p] of paths.entries()) {
|
|
1983
|
+
if (!p.startsWith(literalStart))
|
|
1984
|
+
continue;
|
|
1985
|
+
// matched as the tab asks it, case and all
|
|
1986
|
+
re.lastIndex = literalStart.length;
|
|
1987
|
+
if (re.test(n === 0 ? at : `${at}/`))
|
|
1988
|
+
return true;
|
|
1989
|
+
}
|
|
1990
|
+
}
|
|
1991
|
+
}
|
|
1992
|
+
catch {
|
|
1993
|
+
return true;
|
|
1994
|
+
}
|
|
1995
|
+
return false;
|
|
1996
|
+
}
|
|
1997
|
+
/** Why a middleware file may run on `at` (a path without the basePath), or null when it surely doesn't. */
|
|
1998
|
+
function middlewareOn(m, at, i18n) {
|
|
1999
|
+
if (m.matcher === "none" || m.matcher === "unread")
|
|
2000
|
+
return { reason: m.matcher };
|
|
2001
|
+
const source = m.matcher.find((s) => sourceMayTake(s, at));
|
|
2002
|
+
if (source !== undefined)
|
|
2003
|
+
return { reason: "matcher", source };
|
|
2004
|
+
// with locales Next puts one in front of each source, and of the path
|
|
2005
|
+
return i18n && m.matcher.length > 0 ? { reason: "i18n" } : null;
|
|
2006
|
+
}
|
|
2007
|
+
/** Whether a version is at least `min`, by its major, minor and patch —
|
|
2008
|
+
false for one that doesn't start with three numbers. */
|
|
2009
|
+
export function atLeast(version, min) {
|
|
2010
|
+
const parts = (v) => /^v?(\d+)\.(\d+)\.(\d+)/.exec(v)?.slice(1).map(Number) ?? null;
|
|
2011
|
+
const a = parts(version);
|
|
2012
|
+
const b = parts(min);
|
|
2013
|
+
if (!a)
|
|
2014
|
+
return false;
|
|
2015
|
+
for (let i = 0; i < 3; i++)
|
|
2016
|
+
if (a[i] !== b[i])
|
|
2017
|
+
return a[i] > b[i];
|
|
2018
|
+
return true;
|
|
2019
|
+
}
|
|
2020
|
+
/** The version of a package the site's code resolves: the package's
|
|
2021
|
+
folder under the nearest node_modules, from the site's folder up — as
|
|
2022
|
+
Node and Next's bundler find it. Null when none is there. */
|
|
2023
|
+
async function installedVersion(dir, pkg) {
|
|
2024
|
+
for (let at = path.resolve(dir);;) {
|
|
2025
|
+
try {
|
|
2026
|
+
const raw = await readFile(path.join(at, "node_modules", ...pkg.split("/"), "package.json"), "utf8");
|
|
2027
|
+
const version = JSON.parse(raw).version;
|
|
2028
|
+
return typeof version === "string" ? version : null;
|
|
2029
|
+
}
|
|
2030
|
+
catch {
|
|
2031
|
+
// not here: the folder above
|
|
2032
|
+
}
|
|
2033
|
+
const up = path.dirname(at);
|
|
2034
|
+
if (up === at)
|
|
2035
|
+
return null;
|
|
2036
|
+
at = up;
|
|
2037
|
+
}
|
|
2038
|
+
}
|
|
2039
|
+
/** The middleware files Next runs for the site — `middleware`, and
|
|
2040
|
+
`proxy` unless the installed Next is older than 16, which names it so —
|
|
2041
|
+
with one of its page extensions, in the folder that holds the pages
|
|
2042
|
+
folder it reads, or else the app folder: the root, or `src/`. Read from
|
|
2043
|
+
the folder itself, not the file list, which may stop before it. */
|
|
2044
|
+
async function middlewareFiles(dir, extensions, next) {
|
|
2045
|
+
const folder = async (name) => ((await atRoot(dir, name)) ? name : (await atRoot(dir, `src/${name}`)) ? `src/${name}` : null);
|
|
2046
|
+
const read = (await folder("pages")) ?? (await folder("app"));
|
|
2047
|
+
const at = read?.startsWith("src/") ? "src" : "";
|
|
2048
|
+
let names;
|
|
2049
|
+
try {
|
|
2050
|
+
names = (await readdir(path.join(dir, at), { withFileTypes: true })).filter((e) => e.isFile()).map((e) => e.name).sort();
|
|
2051
|
+
}
|
|
2052
|
+
catch {
|
|
2053
|
+
return [];
|
|
2054
|
+
}
|
|
2055
|
+
const kinds = next === null || atLeast(next, "16.0.0") ? ["middleware", "proxy"] : ["middleware"];
|
|
2056
|
+
// Next's own test: the extensions joined into a pattern as they are
|
|
2057
|
+
let test;
|
|
2058
|
+
try {
|
|
2059
|
+
const re = new RegExp(`^(?:${kinds.join("|")})\\.(?:${extensions.join("|")})$`);
|
|
2060
|
+
test = (name) => re.test(name);
|
|
2061
|
+
}
|
|
2062
|
+
catch {
|
|
2063
|
+
test = (name) => extensions.some((ext) => kinds.some((kind) => name === `${kind}.${ext}`));
|
|
2064
|
+
}
|
|
2065
|
+
const found = [];
|
|
2066
|
+
for (const name of names.filter(test)) {
|
|
2067
|
+
const file = at ? `${at}/${name}` : name;
|
|
2068
|
+
let text;
|
|
2069
|
+
try {
|
|
2070
|
+
text = await readFile(path.join(dir, file), "utf8");
|
|
2071
|
+
}
|
|
2072
|
+
catch {
|
|
2073
|
+
found.push({ file, matcher: "unread" });
|
|
2074
|
+
continue;
|
|
2075
|
+
}
|
|
2076
|
+
found.push({ file, matcher: text.length > 2_000_000 ? "unread" : middlewareMatcher(text) });
|
|
2077
|
+
}
|
|
2078
|
+
return found;
|
|
1272
2079
|
}
|
|
1273
2080
|
export async function findLive(dir, maxFiles = 4000) {
|
|
1274
2081
|
const { files } = await listFiles(dir, maxFiles);
|
|
@@ -1312,7 +2119,7 @@ export async function findLive(dir, maxFiles = 4000) {
|
|
|
1312
2119
|
const found = words ? liveInFile(file, text) : { listeners: [], handler: null, params: false };
|
|
1313
2120
|
listeners.push(...found.listeners);
|
|
1314
2121
|
if (route !== null)
|
|
1315
|
-
routes.push({ file, path: route, handler: found.handler, ...(found.params ? { params: true } : {}) });
|
|
2122
|
+
routes.push({ file, path: route, handler: found.handler, ...(found.params ? { params: true } : {}), ...(found.segment ? { segment: found.segment } : {}) });
|
|
1316
2123
|
}
|
|
1317
2124
|
const facts = { listeners, routes, pages, routeExtension };
|
|
1318
2125
|
if (appDir)
|
|
@@ -1321,6 +2128,18 @@ export async function findLive(dir, maxFiles = 4000) {
|
|
|
1321
2128
|
facts.basePath = settings.basePath;
|
|
1322
2129
|
if (settings === null && config)
|
|
1323
2130
|
facts.config = config;
|
|
2131
|
+
if (config && settings && (settings.output === "export" || settings.output === null))
|
|
2132
|
+
facts.output = { file: config, value: settings.output };
|
|
2133
|
+
const sdk = await installedVersion(dir, "@mapled/next");
|
|
2134
|
+
if (sdk !== null)
|
|
2135
|
+
facts.sdk = sdk;
|
|
2136
|
+
if (settings?.i18n)
|
|
2137
|
+
facts.i18n = true;
|
|
2138
|
+
if (extensions !== null) {
|
|
2139
|
+
const middleware = await middlewareFiles(dir, extensions, await installedVersion(dir, "next"));
|
|
2140
|
+
if (middleware.length > 0)
|
|
2141
|
+
facts.middleware = middleware;
|
|
2142
|
+
}
|
|
1324
2143
|
return facts;
|
|
1325
2144
|
}
|
|
1326
2145
|
/** Whether a literal endpoint names its own origin — `https://…`, `//host/…`
|
|
@@ -1464,11 +2283,21 @@ export function liveState(facts) {
|
|
|
1464
2283
|
return { kind: "missing", listener, endpoint: null };
|
|
1465
2284
|
return { kind: "unresolved", listener };
|
|
1466
2285
|
}
|
|
1467
|
-
const
|
|
1468
|
-
if (
|
|
2286
|
+
const written = sitePath(listener.endpoint);
|
|
2287
|
+
if (written === null)
|
|
1469
2288
|
return { kind: "unresolved", listener };
|
|
1470
2289
|
if (facts.config)
|
|
1471
2290
|
return { kind: "config", listener, file: facts.config };
|
|
2291
|
+
// a static export has no server: no route runs, whatever the files say
|
|
2292
|
+
if (facts.output)
|
|
2293
|
+
return { kind: "export", listener, file: facts.output.file, output: facts.output.value };
|
|
2294
|
+
let at = written;
|
|
2295
|
+
// no endpoint given: from BASE_PATH_SINCE the SDK asks under the basePath Next writes into the bundle, before it at the root
|
|
2296
|
+
if (listener.byDefault && base !== "") {
|
|
2297
|
+
if (facts.sdk === undefined || !atLeast(facts.sdk, BASE_PATH_SINCE))
|
|
2298
|
+
return { kind: "default_base", listener, basePath: base, sdk: facts.sdk ?? null };
|
|
2299
|
+
at = `${base}${RELEASE_ROUTE}`;
|
|
2300
|
+
}
|
|
1472
2301
|
// with a basePath, Next answers only under it: a route file's path follows it
|
|
1473
2302
|
if (base !== "" && at !== base && !at.startsWith(`${base}/`))
|
|
1474
2303
|
return { kind: "outside", listener, endpoint: at, basePath: base };
|
|
@@ -1489,6 +2318,14 @@ export function liveState(facts) {
|
|
|
1489
2318
|
return { kind: "not_get", listener, route };
|
|
1490
2319
|
if (route.params && dynamicRoute(route))
|
|
1491
2320
|
return { kind: "params", listener, route, endpoint: at };
|
|
2321
|
+
if (route.segment)
|
|
2322
|
+
return { kind: "segment", listener, route, setting: route.segment };
|
|
2323
|
+
// middleware runs before the route: whatever it does there, the reading can't see
|
|
2324
|
+
for (const m of facts.middleware ?? []) {
|
|
2325
|
+
const on = middlewareOn(m, base === "" ? at : at.slice(base.length) || "/", facts.i18n === true);
|
|
2326
|
+
if (on)
|
|
2327
|
+
return { kind: "middleware", listener, route, path: at, file: m.file, ...on };
|
|
2328
|
+
}
|
|
1492
2329
|
served ??= { listener, route, path: at };
|
|
1493
2330
|
}
|
|
1494
2331
|
return { kind: "served", ...served };
|