@nifrajs/cli 4.0.0 → 4.0.2

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.
@@ -19,10 +19,19 @@ const GENERATED_MARKER = "GENERATED by @nifrajs/web generateServerManifest";
19
19
  // routes dir relative to the manifest and strips imports to route-relative keys.
20
20
  const ROUTES_PREFIX = /["'](\.{1,2}(?:\/[^"'/]+)*?\/routes\/)[^"']+["']/;
21
21
  const IGNORED = /(^|\/)(node_modules|dist(-[a-z0-9]+)?|build|\.nifra|\.git|\.wrangler|coverage)\//;
22
+ // The fs codes that mean the routes dir is absent or unreadable. Every other throw is the route table
23
+ // refusing to build, which must fail the command rather than read as "no manifest found".
24
+ const ROUTES_DIR_UNAVAILABLE = new Set(["ENOENT", "ENOTDIR", "EACCES", "EPERM"]);
25
+ function routesDirUnavailable(error) {
26
+ if (typeof error !== "object" || error === null || !("code" in error))
27
+ return false;
28
+ return typeof error.code === "string" && ROUTES_DIR_UNAVAILABLE.has(error.code);
29
+ }
22
30
  /**
23
31
  * Discover every generated `server-manifest.ts` under `cwd` and re-sync its route table from the live
24
32
  * `routes/` tree, preserving baked client assets. Returns one result per generated manifest. Never throws
25
- * on a per-manifest miss (an unreadable/absent routes dir is skipped).
33
+ * on a per-manifest failure: an absent or unreadable routes dir is skipped, and a route table that fails
34
+ * to build is reported in that manifest's `error`.
26
35
  */
27
36
  export async function syncServerManifests(cwd) {
28
37
  const { discoverRoutes } = await import("@nifrajs/web/fs");
@@ -45,8 +54,12 @@ export async function syncServerManifests(cwd) {
45
54
  try {
46
55
  next = resyncServerManifestSource(source, discoverRoutes(routesDir), routesPrefix);
47
56
  }
48
- catch {
49
- continue; // routes dir gone/unreadable - not a manifest we can sync
57
+ catch (error) {
58
+ if (routesDirUnavailable(error))
59
+ continue; // not a manifest we can sync
60
+ const message = error instanceof Error ? error.message : String(error);
61
+ results.push({ file: rel, changed: false, added: [], removed: [], error: message });
62
+ continue;
50
63
  }
51
64
  const before = parseManifestRouteFiles(source, routesPrefix);
52
65
  const after = parseManifestRouteFiles(next, routesPrefix);
@@ -66,34 +79,45 @@ export async function syncServerManifests(cwd) {
66
79
  }
67
80
  return results.sort((a, b) => codeUnitOrder(a.file, b.file));
68
81
  }
69
- /** CLI entry: sync every generated manifest, print a per-file summary + the client-rebuild caveat. */
70
- export async function runSyncManifest(cwd) {
71
- const results = await syncServerManifests(cwd);
72
- if (results.length === 0) {
73
- console.log("nifra sync-manifest: no generated server-manifest.ts found under this directory.");
74
- return true;
75
- }
82
+ /** True when no manifest's route table failed to build. */
83
+ export function syncManifestsOk(results) {
84
+ return results.every((r) => r.error === undefined);
85
+ }
86
+ /** Per-file summary lines + the client-rebuild caveat. */
87
+ export function renderSyncManifestResults(results) {
88
+ if (results.length === 0)
89
+ return ["nifra sync-manifest: no generated server-manifest.ts found under this directory."];
90
+ const lines = [];
76
91
  let anyChanged = false;
77
92
  for (const r of results) {
78
- if (r.refusedEmpty) {
79
- console.log(`⚠ ${r.file}: refused - routes/ has zero routes but the manifest had some; left untouched (delete the manifest yourself if this is intentional).`);
93
+ if (r.error !== undefined) {
94
+ lines.push(`✖ ${r.file}: routes/ does not build a route table; left untouched.`, ` ${r.error}`);
95
+ }
96
+ else if (r.refusedEmpty) {
97
+ lines.push(`⚠ ${r.file}: refused - routes/ has zero routes but the manifest had some; left untouched (delete the manifest yourself if this is intentional).`);
80
98
  }
81
99
  else if (r.changed) {
82
100
  anyChanged = true;
83
- console.log(`✓ synced ${r.file}`);
101
+ lines.push(`✓ synced ${r.file}`);
84
102
  if (r.added.length > 0)
85
- console.log(` added: ${r.added.join(", ")}`);
103
+ lines.push(` added: ${r.added.join(", ")}`);
86
104
  if (r.removed.length > 0)
87
- console.log(` removed: ${r.removed.join(", ")}`);
105
+ lines.push(` removed: ${r.removed.join(", ")}`);
88
106
  }
89
107
  else {
90
- console.log(`• ${r.file} already in sync`);
108
+ lines.push(`• ${r.file} already in sync`);
91
109
  }
92
110
  }
93
111
  if (anyChanged) {
94
- console.log("\nRe-synced the server manifest's route table. If you added or changed a HYDRATING route\n" +
112
+ lines.push("\nRe-synced the server manifest's route table. If you added or changed a HYDRATING route\n" +
95
113
  "component (not just a loader / SSR-only route), also run a full build so the client bundle matches.");
96
114
  }
97
- return true;
115
+ return lines;
116
+ }
117
+ /** CLI entry: sync every generated manifest and print the summary. False when a route table failed to build. */
118
+ export async function runSyncManifest(cwd) {
119
+ const results = await syncServerManifests(cwd);
120
+ console.log(renderSyncManifestResults(results).join("\n"));
121
+ return syncManifestsOk(results);
98
122
  }
99
123
  //# sourceMappingURL=sync-manifest.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"sync-manifest.js","sourceRoot":"","sources":["../src/sync-manifest.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"sync-manifest.js","sourceRoot":"","sources":["../src/sync-manifest.ts"],"names":[],"mappings":"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"}
package/docs/types.json CHANGED
@@ -15027,14 +15027,14 @@
15027
15027
  "name": "VERSION",
15028
15028
  "kind": "const",
15029
15029
  "package": "@nifrajs/core",
15030
- "signature": "export declare const VERSION: \"4.0.0\";",
15030
+ "signature": "export declare const VERSION: \"4.0.2\";",
15031
15031
  "doc": "/**\n * Current package version. A hardcoded literal on purpose - core runs on the edge (no fs), so it can't\n * read its own package.json at runtime. `scripts/version.ts` rewrites it on every release bump and\n * `check:publish` asserts it equals `@nifrajs/core`'s package version.\n */"
15032
15032
  },
15033
15033
  {
15034
15034
  "name": "VERSION",
15035
15035
  "kind": "const",
15036
15036
  "package": "nifra",
15037
- "signature": "export declare const VERSION: \"4.0.0\";",
15037
+ "signature": "export declare const VERSION: \"4.0.2\";",
15038
15038
  "doc": "/**\n * Current package version. A hardcoded literal on purpose - core runs on the edge (no fs), so it can't\n * read its own package.json at runtime. `scripts/version.ts` rewrites it on every release bump and\n * `check:publish` asserts it equals `@nifrajs/core`'s package version.\n */"
15039
15039
  },
15040
15040
  {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nifrajs/cli",
3
- "version": "4.0.0",
3
+ "version": "4.0.2",
4
4
  "description": "The nifra CLI - zero-config dev/build/start for a nifra app (`nifra dev`, `nifra build`, `nifra start`).",
5
5
  "keywords": [
6
6
  "bun",
@@ -69,17 +69,17 @@
69
69
  "access": "public"
70
70
  },
71
71
  "dependencies": {
72
- "@nifrajs/client": "4.0.0",
73
- "@nifrajs/core": "4.0.0",
74
- "@nifrajs/agent-review": "4.0.0",
75
- "@nifrajs/i18n": "4.0.0",
76
- "@nifrajs/mcp": "4.0.0",
77
- "@nifrajs/mcp-db": "4.0.0",
78
- "@nifrajs/runner": "4.0.0",
79
- "@nifrajs/schema": "4.0.0",
80
- "@nifrajs/testing": "4.0.0",
81
- "@nifrajs/web": "4.0.0",
82
- "create-nifra": "4.0.0"
72
+ "@nifrajs/client": "4.0.2",
73
+ "@nifrajs/core": "4.0.2",
74
+ "@nifrajs/agent-review": "4.0.2",
75
+ "@nifrajs/i18n": "4.0.2",
76
+ "@nifrajs/mcp": "4.0.2",
77
+ "@nifrajs/mcp-db": "4.0.2",
78
+ "@nifrajs/runner": "4.0.2",
79
+ "@nifrajs/schema": "4.0.2",
80
+ "@nifrajs/testing": "4.0.2",
81
+ "@nifrajs/web": "4.0.2",
82
+ "create-nifra": "4.0.2"
83
83
  },
84
84
  "repository": {
85
85
  "type": "git",
package/src/cli.ts CHANGED
@@ -304,7 +304,7 @@ set in the process environment is never overwritten by a file.
304
304
  ${renderCommandCatalogHelp()}`
305
305
 
306
306
  // Kept in lockstep with packages/cli/package.json by check:publish's version-consistency gate.
307
- const CLI_VERSION = "4.0.0"
307
+ const CLI_VERSION = "4.0.2"
308
308
 
309
309
  // A render adapter + nifra server are opaque to the CLI (it just forwards them); cast at the seam.
310
310
  const asAdapter = (v: unknown): RenderAdapter => v as RenderAdapter
@@ -31,6 +31,12 @@ import { reviewSpec } from "./review.ts"
31
31
  import type { RouteTypesReport } from "./route-types.ts"
32
32
  import type { SmokeReport } from "./smoke.ts"
33
33
  import type { StylexMigrationResult } from "./stylex-migrate.ts"
34
+ import {
35
+ renderSyncManifestResults,
36
+ type SyncManifestResult,
37
+ syncManifestsOk,
38
+ syncServerManifests,
39
+ } from "./sync-manifest.ts"
34
40
  import {
35
41
  collectProjectWorkGraph,
36
42
  type ProjectWorkGraphResult,
@@ -498,6 +504,10 @@ interface SyncCommandOutput {
498
504
  readonly ok: true
499
505
  readonly results: readonly unknown[]
500
506
  }
507
+ interface SyncManifestCommandOutput {
508
+ readonly ok: boolean
509
+ readonly results: readonly SyncManifestResult[]
510
+ }
501
511
 
502
512
  interface SmokeCommandOutput extends SmokeReport {}
503
513
 
@@ -1674,7 +1684,7 @@ const contractsSpec: CommandSpec<ContractsInput, ContractsCommandOutput> = {
1674
1684
  success: (out) => out.ok,
1675
1685
  }
1676
1686
 
1677
- const syncManifestSpec: CommandSpec<SyncInput, SyncCommandOutput> = {
1687
+ const syncManifestSpec: CommandSpec<SyncInput, SyncManifestCommandOutput> = {
1678
1688
  name: "sync-manifest",
1679
1689
  summary: "Regenerate generated server-manifest route tables without a full build.",
1680
1690
  input: SYNC_SCHEMA,
@@ -1687,16 +1697,11 @@ const syncManifestSpec: CommandSpec<SyncInput, SyncCommandOutput> = {
1687
1697
  stability: "stable",
1688
1698
  argv: { flags: [{ name: "json", field: "json", type: "boolean" }] },
1689
1699
  async run(_value, ctx) {
1690
- return {
1691
- ok: true,
1692
- results: await (await import("./sync-manifest.ts")).syncServerManifests(ctx.cwd),
1693
- }
1700
+ const results = await syncServerManifests(ctx.cwd)
1701
+ return { ok: syncManifestsOk(results), results }
1694
1702
  },
1695
- render: (out) => [
1696
- out.results.length === 0
1697
- ? "nifra sync-manifest: no generated server-manifest.ts found under this directory."
1698
- : `✓ checked ${out.results.length} generated server manifest${out.results.length === 1 ? "" : "s"}`,
1699
- ],
1703
+ render: (out) => renderSyncManifestResults(out.results),
1704
+ success: (out) => out.ok,
1700
1705
  }
1701
1706
 
1702
1707
  const syncRoutesSpec: CommandSpec<SyncInput, SyncCommandOutput> = {
@@ -62,7 +62,15 @@ registerFixRecipe({
62
62
  async apply(root) {
63
63
  const { syncServerManifests } = await import("./sync-manifest.ts")
64
64
  const results = await syncServerManifests(root)
65
- return results.filter((result) => result.changed).map((result) => result.file)
65
+ const changed = results.filter((result) => result.changed).map((result) => result.file)
66
+ const failed = results.filter((result) => result.error !== undefined)
67
+ if (failed.length > 0) {
68
+ const synced = changed.length === 0 ? "" : `synced ${changed.join(", ")}; `
69
+ throw new Error(
70
+ `[nifra] ${synced}cannot sync ${failed.map((result) => `${result.file}: ${result.error}`).join("; ")}`,
71
+ )
72
+ }
73
+ return changed
66
74
  },
67
75
  })
68
76
 
package/src/mcp-http.ts CHANGED
@@ -29,7 +29,7 @@ export { docsTools } from "./mcp-docs-tools.ts"
29
29
  export type { TypeEntry } from "./types-search.ts"
30
30
 
31
31
  // Kept in lockstep with packages/cli/package.json by check:publish's version-consistency gate.
32
- const VERSION = "4.0.0"
32
+ const VERSION = "4.0.2"
33
33
  const SERVER_INFO = { name: "nifra-docs", version: VERSION }
34
34
  // Derive the GET/health tool list from the tools actually served, so the line can never drift from them.
35
35
  const docsHealth = (tools: McpTool[]): string =>
@@ -24,6 +24,7 @@ import {
24
24
  readFileSync,
25
25
  renameSync,
26
26
  rmSync,
27
+ statSync,
27
28
  writeFileSync,
28
29
  } from "node:fs"
29
30
  import { dirname, extname, join, posix, relative, resolve } from "node:path"
@@ -55,6 +56,10 @@ const LEGACY_SERVER = /\.server(\.(?:[cm]?[jt]sx?|svelte|vue|mdx))$/
55
56
  const RETIRED_SPECIFIERS: Readonly<Record<string, string>> = {
56
57
  "@nifrajs/web/server-only": "@nifrajs/web/backend-only",
57
58
  }
59
+ /** Scripts and config that name app files by path: scanned for paths that moved, never rewritten. */
60
+ const TOOLING_FILE =
61
+ /(?:\.(?:json|jsonc|ya?ml|toml|sh|bash|zsh|ps1)|(?:^|\/)(?:Dockerfile|Makefile|Procfile))$/
62
+ const SELF_LOCATION = /\bimport\.meta\.(?:dirname|dir|filename|url)\b|\b__(?:dirname|filename)\b/
58
63
  const SKIP_DIRS = new Set(["node_modules", "dist", "build", "coverage", ".git", ".vite", "public"])
59
64
  const ZONE_DIRS = new Set(["frontend", "backend", "shared", "routes", "public"])
60
65
 
@@ -666,6 +671,144 @@ function resolveIn(
666
671
  return candidates.find((candidate) => files.has(candidate))
667
672
  }
668
673
 
674
+ /** Where `text` spells, inside quotes, an import specifier that resolves among `files`: [start, end). */
675
+ function liveSpecifierSpans(
676
+ ts: typeof TS,
677
+ file: string,
678
+ text: string,
679
+ files: ReadonlySet<string>,
680
+ ): Array<readonly [number, number]> {
681
+ const spans: Array<readonly [number, number]> = []
682
+ for (const { specifier } of importsOf(ts, file, text)) {
683
+ if (resolveIn(files, file, specifier) === undefined) continue
684
+ for (const quote of ['"', "'", "`"]) {
685
+ const quoted = `${quote}${specifier}${quote}`
686
+ for (let at = text.indexOf(quoted); at !== -1; at = text.indexOf(quoted, at + 1))
687
+ spans.push([at + 1, at + 1 + specifier.length])
688
+ }
689
+ }
690
+ return spans
691
+ }
692
+
693
+ const OWN_DIR = new Set(["import.meta.dir", "import.meta.dirname", "__dirname"])
694
+
695
+ /**
696
+ * Re-point the paths a module moving from `from` to `to` builds from its own location, so each still
697
+ * reaches what it did: `join(import.meta.dir, "..", "x")` (or `resolve`), `import.meta.dir + "/../x"`,
698
+ * `` `${__dirname}/../x` `` and `new URL("../x", import.meta.url)`, each with literal segments. `locate`
699
+ * maps an app-relative target to where it ends up. Returns the new text and the first use of the
700
+ * module's location it could not follow.
701
+ */
702
+ function rebaseOwnPaths(
703
+ ts: typeof TS,
704
+ from: string,
705
+ to: string,
706
+ text: string,
707
+ locate: (target: string) => string,
708
+ ): { readonly text: string; readonly unfollowed: string | undefined } {
709
+ const sf = ts.createSourceFile(
710
+ to,
711
+ text,
712
+ ts.ScriptTarget.Latest,
713
+ true,
714
+ to.endsWith("x") ? ts.ScriptKind.TSX : ts.ScriptKind.TS,
715
+ )
716
+ const edits: Array<{ start: number; end: number; text: string }> = []
717
+ const followed: Array<readonly [number, number]> = []
718
+ const isOwnDir = (node: TS.Node | undefined): node is TS.Expression =>
719
+ node !== undefined && OWN_DIR.has(node.getText(sf))
720
+ const isLiteral = (
721
+ node: TS.Node | undefined,
722
+ ): node is TS.StringLiteral | TS.NoSubstitutionTemplateLiteral =>
723
+ node !== undefined && (ts.isStringLiteral(node) || ts.isNoSubstitutionTemplateLiteral(node))
724
+ const quoted = (node: TS.Node, value: string): string => {
725
+ const quote = node.getText(sf)[0] ?? '"'
726
+ return `${quote}${value}${quote}`
727
+ }
728
+ // The path `relative` names from the module's old folder, named from its new folder.
729
+ const rebased = (relative: string): string => {
730
+ const target = locate(posix.normalize(posix.join(posix.dirname(from), relative)))
731
+ const next = posix.relative(posix.dirname(to), target) || "."
732
+ return relative.endsWith("/") && next !== "." ? `${next}/` : next
733
+ }
734
+ const follow = (anchor: TS.Node, start: number, end: number, replacement: string): void => {
735
+ followed.push([anchor.getStart(sf), anchor.getEnd()])
736
+ if (text.slice(start, end) !== replacement) edits.push({ start, end, text: replacement })
737
+ }
738
+
739
+ const visit = (node: TS.Node): void => {
740
+ if (ts.isCallExpression(node)) {
741
+ const [dir, ...parts] = node.arguments
742
+ const callee = node.expression.getText(sf)
743
+ const first = parts[0]
744
+ const last = parts[parts.length - 1]
745
+ if (
746
+ isOwnDir(dir) &&
747
+ /(?:^|\.)(?:join|resolve)$/.test(callee) &&
748
+ first !== undefined &&
749
+ last !== undefined &&
750
+ parts.every(isLiteral) &&
751
+ !(callee.endsWith("resolve") && parts.some((part) => part.text.startsWith("/")))
752
+ ) {
753
+ const next = rebased(posix.join(...parts.map((part) => part.text)))
754
+ const segments = parts.length === 1 ? [next] : next.split("/").filter((s) => s !== "")
755
+ const replacement = segments.map((segment) => quoted(first, segment)).join(", ")
756
+ follow(dir, first.getStart(sf), last.getEnd(), replacement)
757
+ return
758
+ }
759
+ }
760
+ if (
761
+ ts.isBinaryExpression(node) &&
762
+ node.operatorToken.kind === ts.SyntaxKind.PlusToken &&
763
+ isOwnDir(node.left) &&
764
+ isLiteral(node.right) &&
765
+ node.right.text.startsWith("/")
766
+ ) {
767
+ const next = rebased(node.right.text.slice(1) || ".")
768
+ follow(
769
+ node.left,
770
+ node.right.getStart(sf),
771
+ node.right.getEnd(),
772
+ quoted(node.right, `/${next}`),
773
+ )
774
+ return
775
+ }
776
+ if (ts.isTemplateExpression(node) && node.head.text === "" && node.templateSpans.length === 1) {
777
+ const span = node.templateSpans[0]
778
+ if (span !== undefined && isOwnDir(span.expression) && span.literal.text.startsWith("/")) {
779
+ const next = rebased(span.literal.text.slice(1) || ".")
780
+ const replacement = `\`\${${span.expression.getText(sf)}}/${next}\``
781
+ follow(span.expression, node.getStart(sf), node.getEnd(), replacement)
782
+ return
783
+ }
784
+ }
785
+ if (
786
+ ts.isNewExpression(node) &&
787
+ node.expression.getText(sf) === "URL" &&
788
+ node.arguments?.length === 2 &&
789
+ node.arguments[1]?.getText(sf) === "import.meta.url"
790
+ ) {
791
+ const path = node.arguments[0]
792
+ if (isLiteral(path) && !/^(?:\/|[a-z][a-z\d+.-]*:)/i.test(path.text)) {
793
+ const next = rebased(path.text)
794
+ const replacement = quoted(path, next.startsWith("..") ? next : `./${next}`)
795
+ follow(node.arguments[1], path.getStart(sf), path.getEnd(), replacement)
796
+ return
797
+ }
798
+ }
799
+ ts.forEachChild(node, visit)
800
+ }
801
+ visit(sf)
802
+
803
+ const unfollowed = [...text.matchAll(new RegExp(SELF_LOCATION.source, "g"))].find(
804
+ (match) => !followed.some(([start, end]) => start <= match.index && match.index < end),
805
+ )?.[0]
806
+ let next = text
807
+ for (const edit of edits.sort((a, b) => b.start - a.start))
808
+ next = next.slice(0, edit.start) + edit.text + next.slice(edit.end)
809
+ return { text: next, unfollowed }
810
+ }
811
+
669
812
  /** Rewrite one relative specifier from `from`'s new location to `target`'s new location, keeping the
670
813
  * author's style: extension kept when written, dropped when not, `/index` collapsed when it was. */
671
814
  function respecify(
@@ -947,21 +1090,25 @@ export async function migrateLayout(
947
1090
  }
948
1091
  edges.set(file, refs)
949
1092
  }
950
- const reach = (roots: readonly string[]): Set<string> => {
1093
+ const files = [...new Set([...next.keys(), ...everyFile.filter((f) => !original.has(f))])]
1094
+ const frontendRoots = files.filter(
1095
+ (f) => f.startsWith("routes/") && ROUTE_FILE.test(f) && !f.includes(".backend."),
1096
+ )
1097
+ // A route page the server imports - a generated `server-manifest.ts` imports them all - is rendered
1098
+ // there, not run as server code, so the server's walk stops at it: what only pages use stays frontend.
1099
+ const pages = new Set(frontendRoots)
1100
+ const reach = (roots: readonly string[], stopAt?: ReadonlySet<string>): Set<string> => {
951
1101
  const seen = new Set<string>()
952
1102
  const queue = [...roots]
953
1103
  while (queue.length > 0) {
954
1104
  const file = queue.pop() as string
955
1105
  if (seen.has(file)) continue
956
1106
  seen.add(file)
1107
+ if (stopAt?.has(file)) continue
957
1108
  for (const edge of edges.get(file) ?? []) if (!edge.typeOnly) queue.push(edge.target)
958
1109
  }
959
1110
  return seen
960
1111
  }
961
- const files = [...new Set([...next.keys(), ...everyFile.filter((f) => !original.has(f))])]
962
- const frontendRoots = files.filter(
963
- (f) => f.startsWith("routes/") && ROUTE_FILE.test(f) && !f.includes(".backend."),
964
- )
965
1112
  const backendRoots = files.filter(
966
1113
  (f) =>
967
1114
  (f.startsWith("routes/") && f.includes(".backend.")) ||
@@ -970,11 +1117,12 @@ export async function migrateLayout(
970
1117
  (isRootScript(f) && f !== "nifra.config.ts" && !isTestFile(f)),
971
1118
  )
972
1119
  const browser = reach(frontendRoots)
973
- const server = reach(backendRoots)
1120
+ const server = reach(backendRoots, pages)
974
1121
  // What the app's own server modules reach, without the root scripts: a root file in here is a module
975
1122
  // the app imports, not an entry, so it moves into a zone.
976
1123
  const appServer = reach(
977
1124
  backendRoots.filter((f) => f.includes("/") || ROOT_MOVES[f] !== undefined),
1125
+ pages,
978
1126
  )
979
1127
  // A `.server` module ran empty in the browser, so only server code used it: it moves under backend/.
980
1128
  for (const file of files) {
@@ -1048,19 +1196,91 @@ export async function migrateLayout(
1048
1196
  if (updated !== text && !moveMap.has(file) && original.get(file) === text) rewritten.push(file)
1049
1197
  }
1050
1198
 
1199
+ const moves = [...moveMap]
1200
+ .map(([from, to]) => ({ from, to }))
1201
+ .sort((a, b) => codeUnitOrder(a.from, b.from))
1202
+
1203
+ // A moved module's paths from its own location now start from its new folder: re-point the ones it
1204
+ // spells out, and report the first one it cannot follow.
1205
+ const unfollowed = new Map<string, string>()
1206
+ for (const { from, to } of moves) {
1207
+ const text = final.get(to)
1208
+ if (text === undefined || !SELF_LOCATION.test(text)) continue
1209
+ if (!/\.[cm]?[jt]sx?$/.test(to)) {
1210
+ unfollowed.set(to, SELF_LOCATION.exec(text)?.[0] ?? "")
1211
+ continue
1212
+ }
1213
+ const result = rebaseOwnPaths(ts, from, to, text, (target) =>
1214
+ known.has(target) ? newPath(target) : target,
1215
+ )
1216
+ final.set(to, result.text)
1217
+ if (result.unfollowed !== undefined) unfollowed.set(to, result.unfollowed)
1218
+ }
1219
+
1051
1220
  // A path in a string or comment is not an import, so nothing above rewrote it: a build script that
1052
- // writes `data/x.json` would keep writing to the old place.
1053
- for (const [file, text] of final) {
1221
+ // writes `data/x.json` would keep writing to the old place, and so would a deploy script or
1222
+ // `package.json` beside the code.
1223
+ const settled = new Set([...known].map(newPath))
1224
+ const scanned: Array<readonly [string, string]> = [...final]
1225
+ for (const file of everyFile) {
1226
+ if (
1227
+ !original.has(file) &&
1228
+ TOOLING_FILE.test(file) &&
1229
+ statSync(join(root, file)).size <= 256_000
1230
+ )
1231
+ scanned.push([newPath(file), readFileSync(join(root, file), "utf8")])
1232
+ }
1233
+ for (const [file, text] of scanned) {
1234
+ let live: ReadonlyArray<readonly [number, number]> | undefined
1054
1235
  for (const [from, to] of moveMap) {
1055
1236
  // `shared/lib/x.ts` contains `lib/x.ts`; that is the new path, not a stale one.
1056
1237
  const newPrefix = to.endsWith(from) ? to.slice(0, to.length - from.length) : undefined
1057
1238
  const stale = [
1058
1239
  ...text.matchAll(new RegExp(`(?<![\\w.-])${escapeRegExp(from)}(?![\\w])`, "g")),
1059
- ].some((match) => newPrefix === undefined || !text.slice(0, match.index).endsWith(newPrefix))
1240
+ ].some((match) => {
1241
+ if (newPrefix !== undefined && text.slice(0, match.index).endsWith(newPrefix)) return false
1242
+ // An import that resolves after the moves is current, even when it still reads like the old
1243
+ // path: `./x.json` from a module that moved together with `x.json`.
1244
+ live ??= liveSpecifierSpans(ts, file, text, settled)
1245
+ const end = match.index + from.length
1246
+ return !live.some(([start, stop]) => start <= match.index && end <= stop)
1247
+ })
1060
1248
  if (stale) issues.push({ file, reason: `mentions "${from}" by path; it moved to "${to}"` })
1061
1249
  }
1062
1250
  }
1063
1251
 
1252
+ // Paths a module finds at run time are not imports either: one built from the module's own location
1253
+ // now starts from its new folder, and a folder listed at run time loses whatever moved out of it.
1254
+ const leaving = new Map<string, Array<{ from: string; to: string }>>()
1255
+ for (const move of moves) {
1256
+ const own = unfollowed.get(move.to)
1257
+ if (own !== undefined) {
1258
+ issues.push({
1259
+ file: move.to,
1260
+ reason: `builds a path from its own location (${own}) that the migration could not re-point, and it moved from "${move.from}"; check that path by hand`,
1261
+ })
1262
+ }
1263
+ const dir = posix.dirname(move.from)
1264
+ if (dir === "." || ZONE_DIRS.has(dir.split("/")[0] ?? "")) continue
1265
+ leaving.set(dir, [...(leaving.get(dir) ?? []), move])
1266
+ }
1267
+ for (const [dir, group] of leaving) {
1268
+ const staying = files.filter(
1269
+ (f) => posix.dirname(f) === dir && !moveMap.has(f) && !isTestFile(f) && !f.endsWith(".d.ts"),
1270
+ )
1271
+ const first = group[0]
1272
+ if (staying.length === 0 || first === undefined) continue
1273
+ const others = group.length === 1 ? "" : ` with ${group.length - 1} other file(s)`
1274
+ const left =
1275
+ staying.length === 1
1276
+ ? `"${staying[0]}" stays`
1277
+ : `${staying.length} files such as "${staying[0]}" stay`
1278
+ issues.push({
1279
+ file: first.from,
1280
+ reason: `moves to "${first.to}"${others} but ${left} in ${dir}/; code or tooling that lists ${dir}/ no longer finds what moved - move the folder together by hand`,
1281
+ })
1282
+ }
1283
+
1064
1284
  // The server refuses data a loader or action returns without an output schema; only the author
1065
1285
  // knows which fields the browser may see.
1066
1286
  for (const [file, text] of final) {
@@ -1073,9 +1293,6 @@ export async function migrateLayout(
1073
1293
  }
1074
1294
  }
1075
1295
 
1076
- const moves = [...moveMap]
1077
- .map(([from, to]) => ({ from, to }))
1078
- .sort((a, b) => codeUnitOrder(a.from, b.from))
1079
1296
  if (options.write === true) {
1080
1297
  for (const [file, text] of final) {
1081
1298
  if (original.get(file) === text) continue
@@ -22,6 +22,8 @@ export const recipe: UpgradeRecipe = {
22
22
  "The Cloudflare Pages target is `cloudflare` (`cf-pages` is refused), and `export const target` in `nifra.config.ts` sets what `nifra build` emits.",
23
23
  '`withISR` no longer caches a page per query string by default: a request with any query parameter renders fresh and is not stored. Pass `query: ["page", ...]` to cache those parameters, or `query: "all"` to both `withISR` and `revalidateEndpoint` to keep the old keys.',
24
24
  "A page file under the backend's API prefix or any other mount (`routes/api/*.tsx` with a backend) now fails at startup, at build and in `nifra check` (NF-C027), instead of answering 404. Move the page, change `apiPrefix` in `backend/framework.ts`, or serve that path from the backend.",
25
+ 'Two page routes of one shape that share a path, such as `users/[id].tsx` beside `users/[slug].tsx`, fail dev, build and `nifra check` with "overlapping routes", because one could never be served. A static page beside a dynamic one (`users/me.tsx` beside `users/[id].tsx`) is fine: the more specific route serves the path they share. In a backend file, `nifra check` reports two same-method registrations that accept a shared path (NF-C024); make them disjoint (`/users/:id{[0-9]+}`) or mark the intended pair with a `// nifra-expect route-overlap` comment.',
26
+ "`nifra contracts check` (NF-K001) reports a changed contract for a route whose schema has a property named `description`, `title`, `default`, `example` or `examples`: earlier lock digests skipped such properties, so a change to one went unnoticed. Run `nifra contracts snapshot` once after upgrading and commit the lock.",
25
27
  "Request methods match exactly: a request sent as `post` or `Patch` answers 405 or 404 instead of reaching the `POST`/`PATCH` route.",
26
28
  "A request path's `.` and `..` segments (also `%2e` and backslashes) are resolved before routing on every runtime, so `/users/../posts` routes to `/posts` and `c.req.url` shows the resolved path.",
27
29
  "A JSON body is parsed only when its media type is `application/json` or `application/*+json`; `text/plain; x=application/json` no longer parses, and server functions answer it with 415.",