@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.
- package/dist/cli.js +1 -1
- package/dist/command-catalog.d.ts +6 -1
- package/dist/command-catalog.d.ts.map +1 -1
- package/dist/command-catalog.js +5 -9
- package/dist/command-catalog.js.map +1 -1
- package/dist/fix-recipes.d.ts.map +1 -1
- package/dist/fix-recipes.js +7 -1
- package/dist/fix-recipes.js.map +1 -1
- package/dist/mcp-http.js +1 -1
- package/dist/migrate-layout.d.ts.map +1 -1
- package/dist/migrate-layout.js +185 -12
- package/dist/migrate-layout.js.map +1 -1
- package/dist/recipes/4.0.0.d.ts.map +1 -1
- package/dist/recipes/4.0.0.js +2 -0
- package/dist/recipes/4.0.0.js.map +1 -1
- package/dist/sync-manifest.d.ts +10 -2
- package/dist/sync-manifest.d.ts.map +1 -1
- package/dist/sync-manifest.js +42 -18
- package/dist/sync-manifest.js.map +1 -1
- package/docs/types.json +2 -2
- package/package.json +12 -12
- package/src/cli.ts +1 -1
- package/src/command-catalog.ts +15 -10
- package/src/fix-recipes.ts +9 -1
- package/src/mcp-http.ts +1 -1
- package/src/migrate-layout.ts +229 -12
- package/src/recipes/4.0.0.ts +2 -0
- package/src/sync-manifest.ts +48 -18
package/dist/sync-manifest.js
CHANGED
|
@@ -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
|
|
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
|
-
|
|
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
|
-
/**
|
|
70
|
-
export
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
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.
|
|
79
|
-
|
|
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
|
-
|
|
101
|
+
lines.push(`✓ synced ${r.file}`);
|
|
84
102
|
if (r.added.length > 0)
|
|
85
|
-
|
|
103
|
+
lines.push(` added: ${r.added.join(", ")}`);
|
|
86
104
|
if (r.removed.length > 0)
|
|
87
|
-
|
|
105
|
+
lines.push(` removed: ${r.removed.join(", ")}`);
|
|
88
106
|
}
|
|
89
107
|
else {
|
|
90
|
-
|
|
108
|
+
lines.push(`• ${r.file} already in sync`);
|
|
91
109
|
}
|
|
92
110
|
}
|
|
93
111
|
if (anyChanged) {
|
|
94
|
-
|
|
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
|
|
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":"AAAA;;;;;;;;;;GAUG;AAEH,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAA;AAChC,OAAO,EAAE,IAAI,EAAE,MAAM,KAAK,CAAA;AAC1B,OAAO,EAAE,aAAa,EAAE,MAAM,+BAA+B,CAAA;AAE7D,kGAAkG;AAClG,kGAAkG;AAClG,MAAM,gBAAgB,GAAG,kDAAkD,CAAA;AAC3E,qGAAqG;AACrG,iFAAiF;AACjF,MAAM,aAAa,GAAG,kDAAkD,CAAA;AACxE,MAAM,OAAO,GAAG,kFAAkF,CAAA;
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
73
|
-
"@nifrajs/core": "4.0.
|
|
74
|
-
"@nifrajs/agent-review": "4.0.
|
|
75
|
-
"@nifrajs/i18n": "4.0.
|
|
76
|
-
"@nifrajs/mcp": "4.0.
|
|
77
|
-
"@nifrajs/mcp-db": "4.0.
|
|
78
|
-
"@nifrajs/runner": "4.0.
|
|
79
|
-
"@nifrajs/schema": "4.0.
|
|
80
|
-
"@nifrajs/testing": "4.0.
|
|
81
|
-
"@nifrajs/web": "4.0.
|
|
82
|
-
"create-nifra": "4.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.
|
|
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
|
package/src/command-catalog.ts
CHANGED
|
@@ -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,
|
|
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
|
-
|
|
1691
|
-
|
|
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
|
-
|
|
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> = {
|
package/src/fix-recipes.ts
CHANGED
|
@@ -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
|
-
|
|
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.
|
|
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 =>
|
package/src/migrate-layout.ts
CHANGED
|
@@ -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
|
|
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
|
-
|
|
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) =>
|
|
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
|
package/src/recipes/4.0.0.ts
CHANGED
|
@@ -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.",
|