@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/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 = RELEASE_ROUTE;
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 = RELEASE_ROUTE;
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({ file, via: "<MapledLive />", endpoint: jsxEndpoint(tokens, k + 1) });
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({ file, via: "<MapledLive />", endpoint: jsxEndpoint(tokens, k + 1) });
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({ file, via: "watchRelease", endpoint: callEndpoint(tokens, k + 1) });
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({ file, via: "watchRelease", endpoint: callEndpoint(tokens, k + 1) });
864
+ listeners.push(listener("watchRelease", callEndpoint(tokens, k + 1)));
859
865
  }
860
866
  }
861
- // `dynamicParams` as the route exports it, or an export the reading can't list
862
- const params = tokens.some((u, k) => namesParams(u) || isWord(u, "exports") || (isWord(u, "export") && isPunct(tokens[k + 1], "*"))) && setsParams(tokens, read().b.after);
863
- return { listeners, handler: releaseHandlerOf(tokens, names, read), params };
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
- const namesParams = (u) => (u?.kind === "ident" || u?.kind === "string") && u.value === "dynamicParams";
866
- /** Whether the route may export `dynamicParams` as anything but the
867
- default written out (`export const dynamicParams = true`). Next reads
868
- only the module's export: a local variable, a property or a string that
869
- says the name is nothing to it. An export the reading can't list —
870
- `export *`, CommonJS — may be it. */
871
- function setsParams(tokens, after) {
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 (namesParams(tokens[k]) && !isWord(tokens[k + 1], "as"))
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 name = isPunct(tokens[j + 1], "*") ? j + 2 : j + 1;
901
- if (namesParams(tokens[name]))
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 (namesParams(tokens[p]))
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 the default: a `let` or `var` may change it later
916
- if (namesParams(tokens[k]) && (declare.value !== "const" || !paramsDefault(tokens, after, k)))
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
- /** Whether the mention of `dynamicParams` at `k` writes out the default:
936
- `dynamicParams = true`, with or without a type (`: boolean`), the `true`
937
- in parentheses or followed by `as` / `satisfies` / `!`. */
938
- function paramsDefault(tokens, after, k) {
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 false;
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 false;
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 && isWord(tokens[from], "true");
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
- /** next.config's `pageExtensions` and `basePath`, read only where the file
1012
- writes them out: a literal, once, directly in the object it exports —
1013
- `module.exports = {…}`, `export default nextConfig`, or passed to a
1014
- plugin: `withMDX(nextConfig)`. Null when it may set either anywhere
1015
- else: a value or a name the code builds (`["page" + "Extensions"]`, an
1016
- escaped identifier, `Object.assign`, `defineProperty`, a prototype
1017
- set by `setPrototypeOf` or `__proto__`, a member in a loop's head), an object from
1018
- another file or spread in, a member (`config.basePath = …`), a nested
1019
- object, any other mention — then no route can be checked. */
1020
- export function nextConfigOf(config) {
1021
- if (config === null)
1022
- return { pageExtensions: PAGE_EXTENSIONS, basePath: "" };
1023
- const tokens = tokenize(config);
1024
- const b = bracketsOf(tokens);
1025
- const declarations = declarationsOf(tokens, b.after);
1026
- const declaredAt = new Set(declarations.map((d) => d.at));
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
- // an identifier with an escape in its name, or a call that sets or copies properties
1030
- if (isPunct(t, "\\") || (t.kind === "ident" && (t.escaped === true || COPIES.has(t.value))) || (t.kind === "string" && t.value === "__proto__"))
1031
- return null;
1032
- // a setting's name inside a longer string: JSON the code parses
1033
- if (t.kind === "string" && !SETTINGS.includes(t.value) && SETTINGS.some((s) => t.value.includes(s)))
1034
- return null;
1035
- if (!isPunct(t, "["))
1036
- continue;
1037
- // a computed key or member written to, unless a lone string or digits name it: `{ [name]: … }`, `config[name] = …`, not `rules[0] = …`
1038
- const close = b.after[k] - 1;
1039
- if (close === k + 2 && literal(tokens[k + 1]))
1040
- continue;
1041
- let digits = k + 1;
1042
- while (digits < close && tokens[digits].kind === "punct" && /^\d$/.test(tokens[digits].value))
1043
- digits++;
1044
- if (digits === close && close > k + 1)
1125
+ if (t.kind !== "punct")
1045
1126
  continue;
1046
- const prev = tokens[k - 1];
1047
- const key = (isPunct(prev, "{") || isPunct(prev, ",") || isPunct(prev, "*") || (prev?.kind === "ident" && KEY_PREFIX.has(prev.value))) && (isPunct(tokens[close + 1], ":") || isPunct(tokens[close + 1], "("));
1048
- // a loop's head writes each value to it: `for (config[name] of values)`
1049
- const head = b.parent[k];
1050
- const loop = head >= 0 && isPunct(tokens[head], "(") && (isWord(tokens[head - 1], "for") || (isWord(tokens[head - 1], "await") && isWord(tokens[head - 2], "for"))) && (isWord(tokens[close + 1], "of") || isWord(tokens[close + 1], "in"));
1051
- const member = (prev?.kind === "ident" || isPunct(prev, ")") || isPunct(prev, "]")) && (assigned(tokens, close) || loop);
1052
- if (key || member)
1053
- return null;
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
- // what the file exports: `module.exports = …`, `export default …`, `export { config as default }`
1056
- const exported = [];
1057
- const names = exportClauses(tokens).get("default") ?? [];
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
- if (isWord(tokens[k], "module") && !isPunct(tokens[k - 1], ".") && isPunct(tokens[k + 1], ".") && isWord(tokens[k + 2], "exports") && isPunct(tokens[k + 3], "=") && !isPunct(tokens[k + 4], "=")) {
1060
- exported.push([k + 4, initializerEnd(tokens, b.after, k + 4, false)]);
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 (isWord(tokens[k], "export") && isWord(tokens[k + 1], "default"))
1063
- exported.push([k + 2, initializerEnd(tokens, b.after, k + 2, false)]);
1064
- }
1065
- // `export { default } from "./base.mjs"`: the config is another module's
1066
- const reexported = tokens.some((t, k) => isWord(t, "export") && isPunct(tokens[k + 1], "{") && isWord(tokens[b.after[k + 1]], "from") && tokens.slice(k + 2, b.after[k + 1] - 1).some((u) => isWord(u, "default")));
1067
- // the objects the config is made of, and whether any part comes from elsewhere
1068
- const objects = new Set();
1069
- let opaque = reexported || exported.length + names.length > 1;
1070
- const byName = new Map();
1071
- for (const d of declarations) {
1072
- const list = byName.get(d.name);
1073
- if (list)
1074
- list.push(d);
1164
+ else if (p.value === "}")
1165
+ return false;
1075
1166
  else
1076
- byName.set(d.name, [d]);
1167
+ division = false;
1168
+ if (regex === division)
1169
+ return false;
1077
1170
  }
1078
- const reassigned = new Set(tokens.flatMap((u, k) => (u.kind === "ident" && !isPunct(tokens[k - 1], ".") && !declaredAt.has(k) && changes(tokens, k) ? [u.value] : [])));
1079
- // and every name in a pattern assigned to — `({ config } = presets)`, `[a, config] = list` — as a running count: nested patterns stay linear
1080
- const patterns = new Int32Array(tokens.length + 1);
1081
- for (let k = 0; k < tokens.length; k++) {
1082
- const after = b.after[k];
1083
- if ((isPunct(tokens[k], "{") || isPunct(tokens[k], "[")) && !DECLARE.has(tokens[k - 1]?.value ?? "") && isPunct(tokens[after], "=") && !isPunct(tokens[after + 1], "=")) {
1084
- patterns[k]++;
1085
- patterns[after]--;
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
- const visit = (from, to, depth) => {
1129
- if (opaque)
1130
- return;
1131
- if (depth > 16 || from >= to) {
1132
- opaque = true;
1133
- return;
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
- // where the value ends: `x as T` and `x satisfies T` are x, and a line
1136
- // without a semicolon ends before a name that starts the next one
1137
- let end = to;
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 u = tokens[k];
1140
- const p = tokens[k - 1];
1141
- if (k > from && (isWord(u, "as") || isWord(u, "satisfies")))
1142
- end = k;
1143
- else if (k > from && u.nl === true && u.kind === "ident" && u.value !== "in" && u.value !== "instanceof" && (p.kind !== "punct" || ")]}".includes(p.value) || /^\d$/.test(p.value)))
1144
- end = k;
1145
- else if (u.kind === "punct" && "([{".includes(u.value))
1283
+ const t = tokens[k];
1284
+ if (t.kind === "punct" && OPENS.has(t.value))
1146
1285
  k = b.after[k] - 1;
1147
- if (end !== to)
1148
- break;
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
- let k = from;
1151
- while (k < end && isWord(tokens[k], "await"))
1152
- k++;
1153
- if (k >= end) {
1154
- opaque = true;
1155
- return;
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
- const t = tokens[k];
1158
- if ((isPunct(t, "{") || isPunct(t, "(")) && b.after[k] === end) {
1159
- if (t.value === "{")
1160
- objects.add(k);
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
- visit(k + 1, end - 1, depth + 1);
1163
- return;
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
- // a function: its code is here, and whatever it sets, the mentions below see
1166
- if (isWord(t, "function") || (isWord(t, "async") && isWord(tokens[k + 1], "function")) || functionOf(tokens, b.after, k, end))
1167
- return;
1168
- // a plugin's name or option: `require("@next/mdx")`, `{ enabled: true }`
1169
- if (t?.kind === "string" || (t?.kind === "punct" && /^\d$/.test(t.value)) || isWord(t, "true") || isWord(t, "false") || isWord(t, "null") || isWord(t, "undefined"))
1170
- return;
1171
- if (t?.kind !== "ident") {
1172
- opaque = true;
1173
- return;
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
- let j = k + 1;
1176
- while (j < end && isPunct(tokens[j], ".") && tokens[j + 1]?.kind === "ident")
1177
- j += 2;
1178
- if (j === end) {
1179
- // a name, or a member (`base.config`) — a value from elsewhere
1180
- if (j === k + 1)
1181
- fromName(t.value, depth);
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
- opaque = true;
1184
- return;
1458
+ break;
1185
1459
  }
1186
- // a call, or calls: `withMDX(nextConfig)`, `createMDX()(config)` — a
1187
- // plugin from a package keeps the config it's given; `require("…")`, a
1188
- // plugin from the site's own files, or a call given no config — its
1189
- // value comes from elsewhere
1190
- if (!isPunct(tokens[j], "(") || (isWord(t, "require") && j === k + 1 && b.after[j] === end) || relative(k) || local.has(t.value)) {
1191
- opaque = true;
1192
- return;
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 literal = (u) => u?.kind === "string" || (u?.kind === "punct" && /^\d$/.test(u.value)) || isWord(u, "true") || isWord(u, "false") || isWord(u, "null") || isWord(u, "undefined");
1195
- let given = false;
1196
- for (; j < end && isPunct(tokens[j], "("); j = b.after[j]) {
1197
- let start = j + 1;
1198
- const close = b.after[j] - 1;
1199
- given = false;
1200
- for (let a = start; a <= close; a++) {
1201
- const u = tokens[a];
1202
- if (a < close && u.kind === "punct" && "([{".includes(u.value))
1203
- a = b.after[a] - 1;
1204
- else if (a === close || isPunct(u, ",")) {
1205
- if (a > start) {
1206
- given ||= !literal(tokens[start]);
1207
- visit(start, a, depth + 1);
1208
- }
1209
- start = a + 1;
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
- if (j !== end || !given)
1214
- opaque = true;
1591
+ return true;
1215
1592
  };
1216
- for (const [from, to] of exported)
1217
- visit(from, to, 0);
1218
- for (const name of names)
1219
- fromName(name, 0);
1220
- if (opaque)
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, directly in one of those objects, and not mentioned anywhere else
1707
+ // each setting: a literal, once, in the config object
1223
1708
  const settings = {};
1224
1709
  const accepted = new Set();
1225
- for (const open of objects) {
1226
- const last = b.after[open] - 1;
1227
- let property = true;
1228
- for (let k = open + 1; k < last; k++) {
1229
- if (property) {
1230
- property = false;
1231
- if (isSpread(tokens, k))
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
- const { name, next } = propertyKey(tokens, k);
1234
- if (name === "pageExtensions" || name === "basePath") {
1235
- if (settings[name] !== undefined || !isPunct(tokens[next], ":"))
1236
- return null;
1237
- const v = next + 1;
1238
- const valueEnd = name === "basePath" ? v + 1 : isPunct(tokens[v], "[") ? b.after[v] : v;
1239
- if (valueEnd === v || (!isPunct(tokens[valueEnd], ",") && valueEnd !== last))
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
- const plain = (u) => u?.kind === "string" && !u.unclosed && u.value !== "${}";
1242
- if (name === "basePath") {
1243
- const value = tokens[v];
1244
- // a path from the root, without a trailing slash — anything else Next refuses
1245
- if (!plain(value) || (value.value !== "" && !/^\/[^?#\s]*[^/?#\s]$/.test(value.value)))
1246
- return null;
1247
- settings[name] = value.value;
1248
- }
1249
- else {
1250
- const list = [];
1251
- for (let j = v + 1; j < valueEnd - 1; j++) {
1252
- if (plain(tokens[j]) && (isPunct(tokens[j + 1], ",") || j + 1 === valueEnd - 1))
1253
- list.push(tokens[j].value);
1254
- else if (!isPunct(tokens[j], ","))
1255
- return null;
1256
- }
1257
- settings[name] = list;
1258
- }
1259
- accepted.add(isPunct(tokens[next - 1], "]") ? next - 2 : next - 1);
1735
+ list.push(tokens[x].value);
1260
1736
  }
1737
+ settings.pageExtensions = list;
1738
+ accepted.add(p.at);
1261
1739
  }
1262
- const t = tokens[k];
1263
- if (t.kind === "punct" && "([{".includes(t.value))
1264
- k = b.after[k] - 1;
1265
- else if (isPunct(t, ","))
1266
- property = true;
1740
+ else if (p.key === "output")
1741
+ output = output === undefined && single ? value.value : null;
1267
1742
  }
1268
1743
  }
1269
- if (tokens.some((t, k) => (t.kind === "ident" || t.kind === "string") && SETTINGS.includes(t.value) && !accepted.has(k)))
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
- return { pageExtensions: settings.pageExtensions ?? PAGE_EXTENSIONS, basePath: settings.basePath ?? "" };
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 at = sitePath(listener.endpoint);
1468
- if (at === null)
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 };