@mapled/cli 0.12.0 → 0.14.0

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