@nifrajs/cli 5.0.0 → 5.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +7 -2
- package/dist/assure-hydration.d.ts.map +1 -1
- package/dist/assure-hydration.js +4 -2
- package/dist/assure-hydration.js.map +1 -1
- package/dist/assure.d.ts +12 -2
- package/dist/assure.d.ts.map +1 -1
- package/dist/assure.js +59 -8
- package/dist/assure.js.map +1 -1
- package/dist/capabilities-tool.d.ts.map +1 -1
- package/dist/capabilities-tool.js +24 -7
- package/dist/capabilities-tool.js.map +1 -1
- package/dist/check-diagnostics.d.ts +2 -0
- package/dist/check-diagnostics.d.ts.map +1 -1
- package/dist/check-diagnostics.js.map +1 -1
- package/dist/check-scan.d.ts +8 -2
- package/dist/check-scan.d.ts.map +1 -1
- package/dist/check-scan.js +43 -10
- package/dist/check-scan.js.map +1 -1
- package/dist/check.d.ts.map +1 -1
- package/dist/check.js +39 -33
- package/dist/check.js.map +1 -1
- package/dist/cli.d.ts +2 -0
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +106 -48
- package/dist/cli.js.map +1 -1
- package/dist/client-module.d.ts +5 -3
- package/dist/client-module.d.ts.map +1 -1
- package/dist/client-module.js +34 -10
- package/dist/client-module.js.map +1 -1
- package/dist/command-catalog.d.ts +9 -1
- package/dist/command-catalog.d.ts.map +1 -1
- package/dist/command-catalog.js +67 -22
- package/dist/command-catalog.js.map +1 -1
- package/dist/dev-feed-tool.d.ts +18 -1
- package/dist/dev-feed-tool.d.ts.map +1 -1
- package/dist/dev-feed-tool.js +93 -0
- package/dist/dev-feed-tool.js.map +1 -1
- package/dist/fix-recipes.d.ts +2 -0
- package/dist/fix-recipes.d.ts.map +1 -1
- package/dist/fix-recipes.js +93 -12
- package/dist/fix-recipes.js.map +1 -1
- package/dist/init-agents.d.ts +16 -0
- package/dist/init-agents.d.ts.map +1 -1
- package/dist/init-agents.js +3 -2
- package/dist/init-agents.js.map +1 -1
- package/dist/internal/config-paths.d.ts +16 -0
- package/dist/internal/config-paths.d.ts.map +1 -0
- package/dist/internal/config-paths.js +124 -0
- package/dist/internal/config-paths.js.map +1 -0
- package/dist/internal/installed-package.d.ts +4 -0
- package/dist/internal/installed-package.d.ts.map +1 -0
- package/dist/internal/installed-package.js +17 -0
- package/dist/internal/installed-package.js.map +1 -0
- package/dist/internal/project-specifiers.d.ts +14 -1
- package/dist/internal/project-specifiers.d.ts.map +1 -1
- package/dist/internal/project-specifiers.js +54 -15
- package/dist/internal/project-specifiers.js.map +1 -1
- package/dist/internal/source-facts.d.ts +6 -1
- package/dist/internal/source-facts.d.ts.map +1 -1
- package/dist/internal/source-facts.js +11 -5
- package/dist/internal/source-facts.js.map +1 -1
- package/dist/internal/typescript-import.d.ts.map +1 -1
- package/dist/internal/typescript-import.js +5 -3
- package/dist/internal/typescript-import.js.map +1 -1
- package/dist/introspect.d.ts +9 -0
- package/dist/introspect.d.ts.map +1 -1
- package/dist/introspect.js +32 -1
- package/dist/introspect.js.map +1 -1
- package/dist/levels-tool.d.ts.map +1 -1
- package/dist/levels-tool.js +5 -8
- package/dist/levels-tool.js.map +1 -1
- package/dist/load.d.ts +2 -2
- package/dist/load.d.ts.map +1 -1
- package/dist/load.js +23 -3
- package/dist/load.js.map +1 -1
- package/dist/mcp-context.d.ts.map +1 -1
- package/dist/mcp-context.js +6 -21
- package/dist/mcp-context.js.map +1 -1
- package/dist/mcp-delegate.d.ts +2 -2
- package/dist/mcp-delegate.d.ts.map +1 -1
- package/dist/mcp-delegate.js +19 -7
- package/dist/mcp-delegate.js.map +1 -1
- package/dist/mcp-exec.d.ts +1 -0
- package/dist/mcp-exec.d.ts.map +1 -1
- package/dist/mcp-exec.js +8 -3
- package/dist/mcp-exec.js.map +1 -1
- package/dist/mcp-http.js +1 -1
- package/dist/mcp-io.d.ts +13 -0
- package/dist/mcp-io.d.ts.map +1 -1
- package/dist/mcp-io.js +23 -0
- package/dist/mcp-io.js.map +1 -1
- package/dist/mcp-path.d.ts +2 -1
- package/dist/mcp-path.d.ts.map +1 -1
- package/dist/mcp-path.js +9 -24
- package/dist/mcp-path.js.map +1 -1
- package/dist/mcp.d.ts.map +1 -1
- package/dist/mcp.js +40 -17
- package/dist/mcp.js.map +1 -1
- package/dist/migrate-layout.d.ts +40 -1
- package/dist/migrate-layout.d.ts.map +1 -1
- package/dist/migrate-layout.js +1316 -228
- package/dist/migrate-layout.js.map +1 -1
- package/dist/output-schemas.d.ts +34 -0
- package/dist/output-schemas.d.ts.map +1 -1
- package/dist/output-schemas.js +132 -21
- package/dist/output-schemas.js.map +1 -1
- package/dist/project-path.d.ts +6 -0
- package/dist/project-path.d.ts.map +1 -1
- package/dist/project-path.js +48 -14
- package/dist/project-path.js.map +1 -1
- package/dist/release-verification.d.ts +5 -0
- package/dist/release-verification.d.ts.map +1 -1
- package/dist/release-verification.js +14 -0
- package/dist/release-verification.js.map +1 -1
- package/dist/review.d.ts.map +1 -1
- package/dist/review.js +5 -0
- package/dist/review.js.map +1 -1
- package/dist/rules/codes.d.ts +3 -1
- package/dist/rules/codes.d.ts.map +1 -1
- package/dist/rules/codes.js +3 -1
- package/dist/rules/codes.js.map +1 -1
- package/dist/rules/data-guard.d.ts +6 -0
- package/dist/rules/data-guard.d.ts.map +1 -1
- package/dist/rules/data-guard.js +37 -0
- package/dist/rules/data-guard.js.map +1 -1
- package/dist/rules/legacy.d.ts.map +1 -1
- package/dist/rules/legacy.js +20 -1
- package/dist/rules/legacy.js.map +1 -1
- package/dist/rules/pages.d.ts +29 -3
- package/dist/rules/pages.d.ts.map +1 -1
- package/dist/rules/pages.js +236 -22
- package/dist/rules/pages.js.map +1 -1
- package/dist/rules/security.d.ts +3 -2
- package/dist/rules/security.d.ts.map +1 -1
- package/dist/rules/security.js +42 -11
- package/dist/rules/security.js.map +1 -1
- package/dist/rules/zones.d.ts.map +1 -1
- package/dist/rules/zones.js +31 -9
- package/dist/rules/zones.js.map +1 -1
- package/dist/stylex-migrate.d.ts.map +1 -1
- package/dist/stylex-migrate.js +127 -17
- package/dist/stylex-migrate.js.map +1 -1
- package/dist/test-tool.d.ts.map +1 -1
- package/dist/test-tool.js +3 -9
- package/dist/test-tool.js.map +1 -1
- package/dist/upgrade.d.ts +20 -5
- package/dist/upgrade.d.ts.map +1 -1
- package/dist/upgrade.js +86 -9
- package/dist/upgrade.js.map +1 -1
- package/dist/verification-plan.d.ts.map +1 -1
- package/dist/verification-plan.js +5 -2
- package/dist/verification-plan.js.map +1 -1
- package/dist/version-drift.d.ts.map +1 -1
- package/dist/version-drift.js +55 -3
- package/dist/version-drift.js.map +1 -1
- package/docs/examples.json +2 -2
- package/docs/llms-full.txt +481 -61
- package/docs/types.json +147 -70
- package/package.json +11 -11
- package/src/assure-hydration.ts +4 -1
- package/src/assure.ts +88 -10
- package/src/capabilities-tool.ts +33 -7
- package/src/check-diagnostics.ts +2 -0
- package/src/check-scan.ts +56 -9
- package/src/check.ts +55 -39
- package/src/cli.ts +104 -35
- package/src/client-module.ts +45 -9
- package/src/command-catalog.ts +77 -21
- package/src/dev-feed-tool.ts +112 -1
- package/src/fix-recipes.ts +101 -10
- package/src/init-agents.ts +4 -3
- package/src/internal/config-paths.ts +154 -0
- package/src/internal/installed-package.ts +15 -0
- package/src/internal/project-specifiers.ts +69 -16
- package/src/internal/source-facts.ts +23 -5
- package/src/internal/typescript-import.ts +5 -3
- package/src/introspect.ts +39 -1
- package/src/levels-tool.ts +7 -9
- package/src/load.ts +29 -3
- package/src/mcp-context.ts +5 -18
- package/src/mcp-delegate.ts +22 -10
- package/src/mcp-exec.ts +9 -2
- package/src/mcp-http.ts +1 -1
- package/src/mcp-io.ts +33 -0
- package/src/mcp-path.ts +8 -22
- package/src/mcp.ts +48 -19
- package/src/migrate-layout.ts +1496 -237
- package/src/output-schemas.ts +179 -23
- package/src/project-path.ts +44 -13
- package/src/release-verification.ts +20 -0
- package/src/review.ts +5 -0
- package/src/rules/codes.ts +3 -1
- package/src/rules/data-guard.ts +40 -0
- package/src/rules/legacy.ts +22 -1
- package/src/rules/pages.ts +266 -24
- package/src/rules/security.ts +49 -11
- package/src/rules/zones.ts +30 -7
- package/src/stylex-migrate.ts +117 -17
- package/src/test-tool.ts +3 -9
- package/src/upgrade.ts +96 -8
- package/src/verification-plan.ts +9 -2
- package/src/version-drift.ts +53 -2
package/src/cli.ts
CHANGED
|
@@ -126,6 +126,9 @@ Usage:
|
|
|
126
126
|
one left. Exits 1 while current-code errors are open.
|
|
127
127
|
nifra logs [--since <n>] [--level <l>] [--grep <s>] The dev server's console, server and
|
|
128
128
|
browser, each line tagged with its request.
|
|
129
|
+
nifra explain [<message>] [--stack <s>] [--name <n>] A structured diagnostic for an error: its
|
|
130
|
+
code, the frame in your source, and the cause and fix when
|
|
131
|
+
nifra knows them. No message: the dev server's latest error.
|
|
129
132
|
nifra cdn-check <url> Check a deployed page behind a CDN: served from cache, no
|
|
130
133
|
internal or CDN-only headers reaching the visitor, and soft
|
|
131
134
|
navigations get page data, not the cached document. Exits 1
|
|
@@ -273,20 +276,23 @@ Usage:
|
|
|
273
276
|
this CLI prints the command for that release's CLI. Fail-closed
|
|
274
277
|
on an unknown version or a rollback (--allow-downgrade
|
|
275
278
|
overrides). Deterministic + idempotent.
|
|
276
|
-
nifra migrate layout [--write] [--json]
|
|
279
|
+
nifra migrate layout [--write] [--zones] [--json]
|
|
277
280
|
Move an app onto the frontend/backend split: split each route
|
|
278
281
|
into x.tsx + x.backend.ts, fold _middleware.ts into
|
|
279
282
|
_layout.backend.ts, move backend.ts/framework.ts under backend/,
|
|
280
|
-
and zone the other modules into frontend/, backend/ or shared
|
|
281
|
-
|
|
283
|
+
and zone the other modules into frontend/, backend/ or shared/
|
|
284
|
+
(--zones keeps them in place and lists them in package.json
|
|
285
|
+
"nifra.zones"). Dry-run by default; --write applies.
|
|
282
286
|
nifra types [--check] [--json] Generate each route's ./+types module under .nifra/types: its
|
|
283
287
|
params, the data its output schemas let through, and api typed
|
|
284
288
|
by backend/app.ts. dev, build and check refresh them too;
|
|
285
289
|
--check fails when one is stale.
|
|
286
|
-
nifra schemas [--write] [--json]
|
|
290
|
+
nifra schemas [--write] [--check] [--json]
|
|
291
|
+
Draft loaderOutput/actionOutput from each loader's and
|
|
287
292
|
action's return type for review; --write adds the missing
|
|
288
293
|
ones (marked nifra:generated) and refreshes generated ones.
|
|
289
|
-
Lists the returned fields a hand-written schema drops
|
|
294
|
+
Lists the returned fields a hand-written schema drops;
|
|
295
|
+
--check fails on a missing, stale or any-typed one.
|
|
290
296
|
nifra port [--target <t>] [--json] Portability linter: print a feature × deploy-target capability
|
|
291
297
|
[--ci] [--strict] matrix (in-memory stores, in-process cron/WebSocket, Bun/Deno
|
|
292
298
|
globals, node: builtins) with file:line evidence. --target auto-
|
|
@@ -315,7 +321,7 @@ set in the process environment is never overwritten by a file.
|
|
|
315
321
|
${renderCommandCatalogHelp()}`
|
|
316
322
|
|
|
317
323
|
// Kept in lockstep with packages/cli/package.json by check:publish's version-consistency gate.
|
|
318
|
-
const CLI_VERSION = "5.
|
|
324
|
+
const CLI_VERSION = "5.1.0"
|
|
319
325
|
|
|
320
326
|
// A render adapter + nifra server are opaque to the CLI (it just forwards them); cast at the seam.
|
|
321
327
|
const asAdapter = (v: unknown): RenderAdapter => v as RenderAdapter
|
|
@@ -366,6 +372,14 @@ export function formatCliError(err: unknown): string {
|
|
|
366
372
|
return String(err)
|
|
367
373
|
}
|
|
368
374
|
|
|
375
|
+
/** A failed command's message on stderr and, for a `--json` caller parsing stdout, as JSON there. */
|
|
376
|
+
function reportFailure(err: unknown, argv: readonly string[]): void {
|
|
377
|
+
const message = formatCliError(err)
|
|
378
|
+
console.error(message)
|
|
379
|
+
if (argv.includes("--json") || argv.includes("--json=true"))
|
|
380
|
+
console.log(JSON.stringify({ ok: false, error: message }, null, 2))
|
|
381
|
+
}
|
|
382
|
+
|
|
369
383
|
async function dev(app: LoadedApp, flags: Flags): Promise<void> {
|
|
370
384
|
if (app.framework.componentPipeline !== undefined) {
|
|
371
385
|
if (flags.bun)
|
|
@@ -954,35 +968,73 @@ async function start(app: LoadedApp, flags: Flags): Promise<void> {
|
|
|
954
968
|
await import(serverFile)
|
|
955
969
|
}
|
|
956
970
|
|
|
971
|
+
const VALUE_FLAGS: Readonly<Record<string, "port" | "out" | "target">> = {
|
|
972
|
+
"--port": "port",
|
|
973
|
+
"-p": "port",
|
|
974
|
+
"--out": "out",
|
|
975
|
+
"--target": "target",
|
|
976
|
+
}
|
|
977
|
+
|
|
978
|
+
type SwitchField = "poll" | "report" | "vite" | "bun" | "allowDuplicateIdentity" | "noIndicator"
|
|
979
|
+
|
|
980
|
+
const SWITCH_FLAGS: Readonly<Record<string, SwitchField>> = {
|
|
981
|
+
"--poll": "poll",
|
|
982
|
+
"--report": "report",
|
|
983
|
+
"--vite": "vite",
|
|
984
|
+
"--bun": "bun",
|
|
985
|
+
"--allow-duplicate-identity": "allowDuplicateIdentity",
|
|
986
|
+
"--no-indicator": "noIndicator",
|
|
987
|
+
}
|
|
988
|
+
|
|
989
|
+
/** The flags of `nifra dev`, `build` and `start`, as `--flag value` or `--flag=value`. Anything else is
|
|
990
|
+
* refused: an ignored `--target=vercel` would build the default target and exit 0. */
|
|
957
991
|
export function parseFlags(args: readonly string[]): Flags {
|
|
958
992
|
// Port precedence: `--port`/`-p` (most specific) > `PORT` env > the framework default. The default is
|
|
959
993
|
// the SAME uncommon port for `nifra dev` and `nifra start` (DEFAULT_DEV_PORT) so a project's URL is
|
|
960
994
|
// stable across commands and doesn't collide with the usual 3000/5173/8080 crowd.
|
|
961
995
|
let port = Number(Bun.env.PORT ?? DEFAULT_DEV_PORT)
|
|
962
996
|
let out: string | undefined
|
|
963
|
-
let poll = Bun.env.CHOKIDAR_USEPOLLING === "1"
|
|
964
997
|
let target: string | undefined
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
998
|
+
const switches: Record<SwitchField, boolean> = {
|
|
999
|
+
poll: Bun.env.CHOKIDAR_USEPOLLING === "1",
|
|
1000
|
+
report: false,
|
|
1001
|
+
vite: false,
|
|
1002
|
+
bun: false,
|
|
1003
|
+
allowDuplicateIdentity: false,
|
|
1004
|
+
noIndicator: false,
|
|
1005
|
+
}
|
|
970
1006
|
for (let i = 0; i < args.length; i++) {
|
|
971
|
-
const
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
1007
|
+
const arg = args[i] as string
|
|
1008
|
+
const equals = arg.startsWith("--") ? arg.indexOf("=") : -1
|
|
1009
|
+
const name = equals === -1 ? arg : arg.slice(0, equals)
|
|
1010
|
+
const valueField = Object.hasOwn(VALUE_FLAGS, name) ? VALUE_FLAGS[name] : undefined
|
|
1011
|
+
if (valueField !== undefined) {
|
|
1012
|
+
const value = equals === -1 ? args[++i] : arg.slice(equals + 1)
|
|
1013
|
+
if (value === undefined || value === "" || (equals === -1 && value.startsWith("-")))
|
|
1014
|
+
throw new Error(`[nifra] ${name} needs a value`)
|
|
1015
|
+
if (valueField === "port") port = Number(value)
|
|
1016
|
+
else if (valueField === "out") out = value
|
|
1017
|
+
else target = value
|
|
1018
|
+
continue
|
|
1019
|
+
}
|
|
1020
|
+
const switchField = Object.hasOwn(SWITCH_FLAGS, name) ? SWITCH_FLAGS[name] : undefined
|
|
1021
|
+
if (switchField !== undefined) {
|
|
1022
|
+
const value = equals === -1 ? "true" : arg.slice(equals + 1)
|
|
1023
|
+
if (value !== "true" && value !== "false")
|
|
1024
|
+
throw new Error(`[nifra] ${name} takes no value (or =true / =false)`)
|
|
1025
|
+
switches[switchField] = value === "true"
|
|
1026
|
+
continue
|
|
1027
|
+
}
|
|
1028
|
+
throw new Error(
|
|
1029
|
+
arg.startsWith("-")
|
|
1030
|
+
? `[nifra] unknown option: ${arg} - see \`nifra help\``
|
|
1031
|
+
: `[nifra] unexpected argument: ${arg} - see \`nifra help\``,
|
|
1032
|
+
)
|
|
981
1033
|
}
|
|
982
1034
|
if (!Number.isFinite(port) || port < 0 || port > 65535) {
|
|
983
1035
|
throw new Error(`[nifra] invalid --port: ${port}`)
|
|
984
1036
|
}
|
|
985
|
-
return { port, out,
|
|
1037
|
+
return { port, out, target, ...switches }
|
|
986
1038
|
}
|
|
987
1039
|
|
|
988
1040
|
/**
|
|
@@ -1190,7 +1242,7 @@ async function main(): Promise<void> {
|
|
|
1190
1242
|
// A thrown error is nifra's own reporting path, not a silent abort: clear the sentinel so the
|
|
1191
1243
|
// hint stays reserved for the case where the process died inside the app import.
|
|
1192
1244
|
markReflecting(undefined)
|
|
1193
|
-
|
|
1245
|
+
reportFailure(err, argv)
|
|
1194
1246
|
const candidate =
|
|
1195
1247
|
typeof err === "object" && err !== null && "exitCode" in err
|
|
1196
1248
|
? (err as { readonly exitCode?: unknown }).exitCode
|
|
@@ -1253,6 +1305,7 @@ async function main(): Promise<void> {
|
|
|
1253
1305
|
console.error(formatCliError(err))
|
|
1254
1306
|
process.exitCode = 1
|
|
1255
1307
|
}
|
|
1308
|
+
exitIfHeldOpen(command)
|
|
1256
1309
|
return
|
|
1257
1310
|
}
|
|
1258
1311
|
// `upgrade` is a pure cwd file-transformer (package.json pins + import moves) driven by a per-release
|
|
@@ -1275,9 +1328,10 @@ async function main(): Promise<void> {
|
|
|
1275
1328
|
})
|
|
1276
1329
|
if (!ok) process.exitCode = 1
|
|
1277
1330
|
} catch (err) {
|
|
1278
|
-
|
|
1331
|
+
reportFailure(err, argv)
|
|
1279
1332
|
process.exitCode = 1
|
|
1280
1333
|
}
|
|
1334
|
+
exitIfHeldOpen(command)
|
|
1281
1335
|
return
|
|
1282
1336
|
}
|
|
1283
1337
|
// `port` is a pure cwd-based portability linter (scans source, doesn't run the app) - like `check`/
|
|
@@ -1288,17 +1342,32 @@ async function main(): Promise<void> {
|
|
|
1288
1342
|
process.exitCode = 1
|
|
1289
1343
|
return
|
|
1290
1344
|
}
|
|
1345
|
+
if (argv.includes("--help") || argv.includes("-h")) {
|
|
1346
|
+
console.log(HELP)
|
|
1347
|
+
return
|
|
1348
|
+
}
|
|
1291
1349
|
const flags = parseFlags(argv.slice(1))
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1350
|
+
try {
|
|
1351
|
+
const app = await loadApp(process.cwd(), flags.out)
|
|
1352
|
+
if (command !== "start" && process.env.NIFRA_BUN_DEV_TOKEN === undefined) {
|
|
1353
|
+
const { unservedRoutesWarning } = await import("./introspect.ts")
|
|
1354
|
+
const unserved = unservedRoutesWarning(app)
|
|
1355
|
+
if (unserved !== undefined) console.warn(unserved)
|
|
1356
|
+
}
|
|
1357
|
+
if (command !== "start") {
|
|
1358
|
+
const { refreshRouteTypes, watchRouteTypes } = await import("./route-types.ts")
|
|
1359
|
+
refreshRouteTypes(app.cwd)
|
|
1360
|
+
// A relaunched `nifra dev --bun` child runs beside its parent, which already watches.
|
|
1361
|
+
if (command === "dev" && process.env.NIFRA_BUN_DEV_TOKEN === undefined)
|
|
1362
|
+
watchRouteTypes(app.cwd)
|
|
1363
|
+
}
|
|
1364
|
+
if (command === "dev") await dev(app, flags)
|
|
1365
|
+
else if (command === "build") await buildForTarget(app, flags.target, flags)
|
|
1366
|
+
else await start(app, flags)
|
|
1367
|
+
} finally {
|
|
1368
|
+
// `dev` and `start` keep serving after they return; a build is over.
|
|
1369
|
+
if (command === "build") exitIfHeldOpen(command)
|
|
1298
1370
|
}
|
|
1299
|
-
if (command === "dev") await dev(app, flags)
|
|
1300
|
-
else if (command === "build") await buildForTarget(app, flags.target, flags)
|
|
1301
|
-
else await start(app, flags)
|
|
1302
1371
|
}
|
|
1303
1372
|
|
|
1304
1373
|
/**
|
|
@@ -1485,7 +1554,7 @@ if (isMainModule() || isBunDevReexecChild() || hasCliCommandToken()) {
|
|
|
1485
1554
|
try {
|
|
1486
1555
|
await main()
|
|
1487
1556
|
} catch (err) {
|
|
1488
|
-
|
|
1557
|
+
reportFailure(err, cliArgv())
|
|
1489
1558
|
process.exitCode = 1
|
|
1490
1559
|
}
|
|
1491
1560
|
}
|
package/src/client-module.ts
CHANGED
|
@@ -4,8 +4,10 @@
|
|
|
4
4
|
*/
|
|
5
5
|
import { existsSync, readFileSync } from "node:fs"
|
|
6
6
|
import { join } from "node:path"
|
|
7
|
+
import type * as TS from "typescript"
|
|
7
8
|
import { CONFIG_FILE, FRAMEWORK_FILE } from "./app-files.ts"
|
|
8
9
|
import { stripComments } from "./check-scan.ts"
|
|
10
|
+
import { configPathReader, exportedInitializer } from "./internal/config-paths.ts"
|
|
9
11
|
import type { SpecifierResolver } from "./internal/project-specifiers.ts"
|
|
10
12
|
|
|
11
13
|
const CLIENT_MODULE_EXPORT =
|
|
@@ -34,28 +36,62 @@ export function appDirs(files: readonly string[]): string[] {
|
|
|
34
36
|
|
|
35
37
|
/**
|
|
36
38
|
* The file `clientModule` names in `app`, read the way the loader reads it: from `nifra.config.ts`
|
|
37
|
-
* when it exists, else `backend/framework.ts
|
|
38
|
-
*
|
|
39
|
+
* when it exists, else `backend/framework.ts`; a string against the app root, and, with `ts`, a
|
|
40
|
+
* `resolve`/`join` of the config's own folder against that folder. `undefined` for a package
|
|
41
|
+
* specifier, a value built any other way, or a path to no file.
|
|
39
42
|
*/
|
|
40
43
|
export function clientModuleRoot(
|
|
41
44
|
root: string,
|
|
42
45
|
app: string,
|
|
43
46
|
resolve: SpecifierResolver,
|
|
47
|
+
ts?: typeof TS,
|
|
44
48
|
): ClientModuleRoot | undefined {
|
|
45
49
|
const config = existsSync(join(root, app, CONFIG_FILE)) ? CONFIG_FILE : FRAMEWORK_FILE
|
|
46
50
|
const path = join(root, app, config)
|
|
47
51
|
if (!existsSync(path)) return undefined
|
|
48
52
|
const text = readFileSync(path, "utf8")
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
53
|
+
const found = ts === undefined ? literalClientModule(text) : staticClientModule(ts, path, text)
|
|
54
|
+
if (found === undefined) return undefined
|
|
55
|
+
const file =
|
|
56
|
+
found.base === "app"
|
|
57
|
+
? resolve(join(root, app, CONFIG_FILE), found.specifier)
|
|
58
|
+
: resolve(path, found.specifier.startsWith("../") ? found.specifier : `./${found.specifier}`)
|
|
54
59
|
if (file === undefined) return undefined
|
|
55
60
|
return {
|
|
56
61
|
config: app === "" ? config : `${app}/${config}`,
|
|
57
|
-
line: text.slice(0,
|
|
58
|
-
specifier,
|
|
62
|
+
line: text.slice(0, found.index).split("\n").length,
|
|
63
|
+
specifier: found.specifier,
|
|
59
64
|
file,
|
|
60
65
|
}
|
|
61
66
|
}
|
|
67
|
+
|
|
68
|
+
interface FoundClientModule {
|
|
69
|
+
readonly base: "app" | "config"
|
|
70
|
+
readonly specifier: string
|
|
71
|
+
readonly index: number
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function literalClientModule(text: string): FoundClientModule | undefined {
|
|
75
|
+
// `stripComments` keeps offsets, so the match index is a position in `text`.
|
|
76
|
+
const match = CLIENT_MODULE_EXPORT.exec(stripComments(text))
|
|
77
|
+
if (match === null) return undefined
|
|
78
|
+
return { base: "app", specifier: match[1] ?? match[2] ?? "", index: match.index }
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function staticClientModule(
|
|
82
|
+
ts: typeof TS,
|
|
83
|
+
path: string,
|
|
84
|
+
text: string,
|
|
85
|
+
): FoundClientModule | undefined {
|
|
86
|
+
let sf: TS.SourceFile
|
|
87
|
+
try {
|
|
88
|
+
sf = ts.createSourceFile(path, text, ts.ScriptTarget.Latest, true, ts.ScriptKind.TS)
|
|
89
|
+
} catch {
|
|
90
|
+
// TypeScript 7 parses only what its session preloaded; the literal form still reads.
|
|
91
|
+
return literalClientModule(text)
|
|
92
|
+
}
|
|
93
|
+
const initializer = exportedInitializer(ts, sf, "clientModule")
|
|
94
|
+
const found = configPathReader(ts, sf)(initializer)
|
|
95
|
+
if (initializer === undefined || found === undefined) return undefined
|
|
96
|
+
return { base: found.base, specifier: found.path, index: initializer.getStart(sf) }
|
|
97
|
+
}
|
package/src/command-catalog.ts
CHANGED
|
@@ -17,7 +17,7 @@ import { cdnCheckSpec } from "./cdn-check.ts"
|
|
|
17
17
|
import { type CheckResult, renderCheckReport } from "./check.ts"
|
|
18
18
|
import type { ContractsLock } from "./contracts.ts"
|
|
19
19
|
import { dbAuditSpec, dbQuerySpec, dbRoleSpec, dbSchemaSpec } from "./db-tool.ts"
|
|
20
|
-
import { errorsSpec, logsSpec } from "./dev-feed-tool.ts"
|
|
20
|
+
import { errorsSpec, explainSpec, logsSpec } from "./dev-feed-tool.ts"
|
|
21
21
|
import { type Diagnostic, diagnostic, normalizeSeverity, toSarifLog } from "./diagnostics.ts"
|
|
22
22
|
import type { DoctorResult } from "./doctor.ts"
|
|
23
23
|
import { type I18nCheckOutput, i18nCheckPassed, renderI18nCheck } from "./i18n-check.ts"
|
|
@@ -367,6 +367,8 @@ interface MigrateInput {
|
|
|
367
367
|
readonly from?: "tailwind" | undefined
|
|
368
368
|
readonly to?: "stylex" | undefined
|
|
369
369
|
readonly write?: boolean | undefined
|
|
370
|
+
/** `migrate layout`: list the folders modules would leave in package.json zones instead. */
|
|
371
|
+
readonly zones?: boolean | undefined
|
|
370
372
|
readonly json?: boolean | undefined
|
|
371
373
|
readonly dir?: string | undefined
|
|
372
374
|
}
|
|
@@ -395,6 +397,7 @@ interface TypesInput {
|
|
|
395
397
|
|
|
396
398
|
interface SchemasInput {
|
|
397
399
|
readonly write?: boolean | undefined
|
|
400
|
+
readonly check?: boolean | undefined
|
|
398
401
|
readonly json?: boolean | undefined
|
|
399
402
|
readonly dir?: string | undefined
|
|
400
403
|
}
|
|
@@ -486,6 +489,8 @@ interface FixCommandOutput {
|
|
|
486
489
|
/** Recipes that refused, with the reason. A recipe that cannot act says so here instead of
|
|
487
490
|
* aborting the run or - worse - reporting nothing at all. */
|
|
488
491
|
readonly failed: readonly { readonly code: string; readonly reason: string }[]
|
|
492
|
+
/** Findings whose recipe runs only when its code is named, because its change needs review. */
|
|
493
|
+
readonly skipped: readonly { readonly code: string; readonly reason: string }[]
|
|
489
494
|
readonly diagnostics: readonly unknown[]
|
|
490
495
|
}
|
|
491
496
|
type MigrateCommandOutput =
|
|
@@ -751,13 +756,14 @@ const MIGRATE_SCHEMA = input<MigrateInput>(
|
|
|
751
756
|
from: { type: "string", enum: ["tailwind"] },
|
|
752
757
|
to: { type: "string", enum: ["stylex"] },
|
|
753
758
|
write: { type: "boolean" },
|
|
759
|
+
zones: { type: "boolean" },
|
|
754
760
|
json: { type: "boolean" },
|
|
755
761
|
dir: { type: "string" },
|
|
756
762
|
}),
|
|
757
763
|
(value) => {
|
|
758
764
|
const raw = withDir(record(value))
|
|
759
765
|
const flags = {
|
|
760
|
-
...parseBooleanFlags(raw, ["write", "json"]),
|
|
766
|
+
...parseBooleanFlags(raw, ["write", "zones", "json"]),
|
|
761
767
|
...(raw.dir === undefined ? {} : { dir: raw.dir }),
|
|
762
768
|
}
|
|
763
769
|
const kind = optionalString(raw.kind, "kind")
|
|
@@ -766,6 +772,7 @@ const MIGRATE_SCHEMA = input<MigrateInput>(
|
|
|
766
772
|
throw new TypeError("the migration must be `layout` (or --from tailwind --to stylex)")
|
|
767
773
|
return { kind, ...flags }
|
|
768
774
|
}
|
|
775
|
+
if (raw.zones === true) throw new TypeError("--zones applies to `migrate layout` only")
|
|
769
776
|
const from = optionalString(raw.from, "from")
|
|
770
777
|
const to = optionalString(raw.to, "to")
|
|
771
778
|
if (from !== "tailwind") throw new TypeError("from must be tailwind")
|
|
@@ -799,11 +806,16 @@ const TYPES_SCHEMA = input<TypesInput>(
|
|
|
799
806
|
)
|
|
800
807
|
|
|
801
808
|
const SCHEMAS_SCHEMA = input<SchemasInput>(
|
|
802
|
-
objectSchema({
|
|
809
|
+
objectSchema({
|
|
810
|
+
write: { type: "boolean" },
|
|
811
|
+
check: { type: "boolean" },
|
|
812
|
+
json: { type: "boolean" },
|
|
813
|
+
dir: { type: "string" },
|
|
814
|
+
}),
|
|
803
815
|
(value) => {
|
|
804
816
|
const raw = withDir(record(value))
|
|
805
817
|
return {
|
|
806
|
-
...parseBooleanFlags(raw, ["write", "json"]),
|
|
818
|
+
...parseBooleanFlags(raw, ["write", "check", "json"]),
|
|
807
819
|
...(raw.dir === undefined ? {} : { dir: raw.dir }),
|
|
808
820
|
}
|
|
809
821
|
},
|
|
@@ -1418,6 +1430,7 @@ const fixSpec: CommandSpec<FixInput, FixCommandOutput> = {
|
|
|
1418
1430
|
ok: { type: "boolean" },
|
|
1419
1431
|
changed: { type: "array" },
|
|
1420
1432
|
failed: { type: "array" },
|
|
1433
|
+
skipped: { type: "array" },
|
|
1421
1434
|
diagnostics: { type: "array" },
|
|
1422
1435
|
},
|
|
1423
1436
|
required: ["ok", "diagnostics"],
|
|
@@ -1432,25 +1445,37 @@ const fixSpec: CommandSpec<FixInput, FixCommandOutput> = {
|
|
|
1432
1445
|
},
|
|
1433
1446
|
async run(value, ctx) {
|
|
1434
1447
|
const { collectCheckResult } = await import("./check.ts")
|
|
1435
|
-
const { applyDiagnosticRecipe } = await import("./fix-recipes.ts")
|
|
1448
|
+
const { applyDiagnosticRecipe, getFixRecipe } = await import("./fix-recipes.ts")
|
|
1436
1449
|
const result = await collectCheckResult(ctx.cwd, {
|
|
1437
1450
|
lintsOnly: true,
|
|
1438
1451
|
...(ctx.signal === undefined ? {} : { signal: ctx.signal }),
|
|
1439
1452
|
})
|
|
1440
1453
|
const changed: string[] = []
|
|
1441
1454
|
const failed: { code: string; reason: string }[] = []
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
|
|
1455
|
+
const skipped: { code: string; reason: string }[] = []
|
|
1456
|
+
for (const diagnostic of result.structuredDiagnostics ?? []) {
|
|
1457
|
+
if (value.code !== undefined && diagnostic.code !== value.code) continue
|
|
1458
|
+
const recipe =
|
|
1459
|
+
diagnostic.fix?.recipe === undefined ? undefined : getFixRecipe(diagnostic.fix.recipe)
|
|
1460
|
+
if (value.code === undefined && recipe?.onlyWhenNamed === true) {
|
|
1461
|
+
if (!skipped.some((entry) => entry.code === diagnostic.code))
|
|
1462
|
+
skipped.push({
|
|
1450
1463
|
code: diagnostic.code,
|
|
1451
|
-
reason:
|
|
1464
|
+
reason: `${recipe.description} Review the finding, then run \`nifra fix --code ${diagnostic.code}\`.`,
|
|
1452
1465
|
})
|
|
1453
|
-
|
|
1466
|
+
continue
|
|
1467
|
+
}
|
|
1468
|
+
// One recipe that cannot act must not cancel the others, and must not pass silently either:
|
|
1469
|
+
// record why and keep going, so a run over many diagnostics still reports every outcome.
|
|
1470
|
+
try {
|
|
1471
|
+
changed.push(...(await applyDiagnosticRecipe(ctx.cwd, diagnostic)))
|
|
1472
|
+
} catch (error) {
|
|
1473
|
+
failed.push({
|
|
1474
|
+
code: diagnostic.code,
|
|
1475
|
+
reason: error instanceof Error ? error.message : String(error),
|
|
1476
|
+
})
|
|
1477
|
+
}
|
|
1478
|
+
}
|
|
1454
1479
|
const final = await collectCheckResult(ctx.cwd, {
|
|
1455
1480
|
lintsOnly: true,
|
|
1456
1481
|
...(ctx.signal === undefined ? {} : { signal: ctx.signal }),
|
|
@@ -1459,6 +1484,7 @@ const fixSpec: CommandSpec<FixInput, FixCommandOutput> = {
|
|
|
1459
1484
|
ok: final.ok,
|
|
1460
1485
|
changed,
|
|
1461
1486
|
failed,
|
|
1487
|
+
skipped,
|
|
1462
1488
|
diagnostics: final.structuredDiagnostics ?? final.diagnostics,
|
|
1463
1489
|
}
|
|
1464
1490
|
},
|
|
@@ -1466,6 +1492,7 @@ const fixSpec: CommandSpec<FixInput, FixCommandOutput> = {
|
|
|
1466
1492
|
out.ok ? "✓ fixes applied" : "✖ unresolved diagnostics remain",
|
|
1467
1493
|
...out.changed.map((file) => ` changed ${file}`),
|
|
1468
1494
|
...out.failed.map((f) => ` could not fix ${f.code}: ${f.reason}`),
|
|
1495
|
+
...out.skipped.map((s) => ` needs review ${s.code}: ${s.reason}`),
|
|
1469
1496
|
...out.diagnostics.map((d) => ` ${String((d as { code?: unknown }).code ?? "diagnostic")}`),
|
|
1470
1497
|
],
|
|
1471
1498
|
success: (out) => out.ok,
|
|
@@ -1493,6 +1520,7 @@ const migrateSpec: CommandSpec<MigrateInput, MigrateCommandOutput> = {
|
|
|
1493
1520
|
moves: { type: "array" },
|
|
1494
1521
|
rewritten: { type: "array" },
|
|
1495
1522
|
tooling: { type: "array" },
|
|
1523
|
+
followUps: { type: "array" },
|
|
1496
1524
|
mode: { type: "string" },
|
|
1497
1525
|
},
|
|
1498
1526
|
required: ["ok", "write", "issues"],
|
|
@@ -1505,6 +1533,7 @@ const migrateSpec: CommandSpec<MigrateInput, MigrateCommandOutput> = {
|
|
|
1505
1533
|
{ name: "from", field: "from", type: "string" },
|
|
1506
1534
|
{ name: "to", field: "to", type: "string" },
|
|
1507
1535
|
{ name: "write", field: "write", type: "boolean" },
|
|
1536
|
+
{ name: "zones", field: "zones", type: "boolean" },
|
|
1508
1537
|
{ name: "json", field: "json", type: "boolean" },
|
|
1509
1538
|
{ name: "dir", field: "dir", type: "string" },
|
|
1510
1539
|
],
|
|
@@ -1514,7 +1543,8 @@ const migrateSpec: CommandSpec<MigrateInput, MigrateCommandOutput> = {
|
|
|
1514
1543
|
const write = value.write === undefined ? {} : { write: value.write }
|
|
1515
1544
|
if (value.kind === "layout") {
|
|
1516
1545
|
const { migrateLayout } = await import("./migrate-layout.ts")
|
|
1517
|
-
|
|
1546
|
+
const options = { ...write, zones: value.zones === true }
|
|
1547
|
+
return { kind: "layout" as const, ...(await migrateLayout(dir, options)) }
|
|
1518
1548
|
}
|
|
1519
1549
|
const { migrateTailwindToStylex } = await import("./stylex-migrate.ts")
|
|
1520
1550
|
return migrateTailwindToStylex(dir, write)
|
|
@@ -1526,12 +1556,22 @@ const migrateSpec: CommandSpec<MigrateInput, MigrateCommandOutput> = {
|
|
|
1526
1556
|
? `✓ ${out.mode === "api" ? "API-only app" : `${out.splits.length} route file${out.splits.length === 1 ? "" : "s"} split`}, ${out.moves.length} file${out.moves.length === 1 ? "" : "s"} moved`
|
|
1527
1557
|
: `⚠ ${out.issues.length} issue${out.issues.length === 1 ? "" : "s"} need a decision; everything else is planned`,
|
|
1528
1558
|
...out.splits.map(
|
|
1529
|
-
(split) =>
|
|
1559
|
+
(split) =>
|
|
1560
|
+
` split ${split.file} → ${split.backend} (${split.moved.join(", ")})${split.shared === undefined ? "" : `, shared data → ${split.shared}`}`,
|
|
1530
1561
|
),
|
|
1531
1562
|
...out.moves.map((move) => ` move ${move.from} → ${move.to}`),
|
|
1532
1563
|
...out.rewritten.map((file) => ` edit ${file} (imports)`),
|
|
1533
1564
|
...out.tooling.map((edit) => ` edit ${edit.file} (${edit.change})`),
|
|
1565
|
+
...out.schemas
|
|
1566
|
+
.filter((entry) => entry.reason === undefined)
|
|
1567
|
+
.map(
|
|
1568
|
+
(entry) =>
|
|
1569
|
+
` draft ${entry.file} ${entry.schema} - review what it lets the browser see${entry.omitted === undefined ? "" : `; leaves out ${entry.omitted.join(", ")}`}${entry.open === undefined ? "" : `; admits any value at ${entry.open.join(", ")}, typed any or unknown`}`,
|
|
1570
|
+
),
|
|
1534
1571
|
...out.issues.map((issue) => ` ⚠ ${issue.file}: ${issue.reason}`),
|
|
1572
|
+
...out.followUps.map(
|
|
1573
|
+
(step) => ` next ${step.file}: ${step.reason} - \`${step.command}\``,
|
|
1574
|
+
),
|
|
1535
1575
|
...(out.write ? [] : [" dry run: pass --write to apply"]),
|
|
1536
1576
|
]
|
|
1537
1577
|
}
|
|
@@ -1683,18 +1723,22 @@ const schemasSpec: CommandSpec<SchemasInput, OutputSchemasReport> = {
|
|
|
1683
1723
|
argv: {
|
|
1684
1724
|
flags: [
|
|
1685
1725
|
{ name: "write", field: "write", type: "boolean" },
|
|
1726
|
+
{ name: "check", field: "check", type: "boolean" },
|
|
1686
1727
|
{ name: "json", field: "json", type: "boolean" },
|
|
1687
1728
|
{ name: "dir", field: "dir", type: "string" },
|
|
1688
1729
|
],
|
|
1689
1730
|
},
|
|
1690
1731
|
async run(value, ctx) {
|
|
1691
1732
|
const { outputSchemas } = await import("./output-schemas.ts")
|
|
1692
|
-
return outputSchemas(resolve(ctx.cwd, value.dir ?? "."), {
|
|
1733
|
+
return outputSchemas(resolve(ctx.cwd, value.dir ?? "."), {
|
|
1734
|
+
write: value.write === true,
|
|
1735
|
+
check: value.check === true,
|
|
1736
|
+
})
|
|
1693
1737
|
},
|
|
1694
1738
|
render: (out) => {
|
|
1695
1739
|
const shown = out.entries.filter(
|
|
1696
1740
|
(entry) =>
|
|
1697
|
-
entry.status !== "current" &&
|
|
1741
|
+
(entry.status !== "current" || entry.open !== undefined) &&
|
|
1698
1742
|
entry.status !== "no-data" &&
|
|
1699
1743
|
(entry.status !== "declared" || entry.dropped.length > 0),
|
|
1700
1744
|
)
|
|
@@ -1709,8 +1753,19 @@ const schemasSpec: CommandSpec<SchemasInput, OutputSchemasReport> = {
|
|
|
1709
1753
|
? "edited by hand under its nifra:generated line"
|
|
1710
1754
|
: entry.dropped.length > 0
|
|
1711
1755
|
? `drops ${entry.dropped.join(", ")}`
|
|
1712
|
-
: undefined
|
|
1713
|
-
|
|
1756
|
+
: entry.omitted !== undefined
|
|
1757
|
+
? `leaves out ${entry.omitted.join(", ")}`
|
|
1758
|
+
: undefined)
|
|
1759
|
+
const open =
|
|
1760
|
+
entry.open === undefined
|
|
1761
|
+
? []
|
|
1762
|
+
: [
|
|
1763
|
+
` admits any value at ${entry.open.join(", ")}: the type there is any or unknown, so the output guard drops nothing below it`,
|
|
1764
|
+
]
|
|
1765
|
+
return [
|
|
1766
|
+
` ${SCHEMA_STATUS_LINE[entry.status]} ${entry.file}:${entry.line} ${entry.schema}${detail === undefined ? "" : ` - ${detail}`}`,
|
|
1767
|
+
...open,
|
|
1768
|
+
].join("\n")
|
|
1714
1769
|
}),
|
|
1715
1770
|
...(out.write || !out.entries.some((e) => e.status === "missing" || e.status === "stale")
|
|
1716
1771
|
? []
|
|
@@ -2020,6 +2075,7 @@ export const commandSpecs = Object.freeze([
|
|
|
2020
2075
|
portSpec,
|
|
2021
2076
|
i18nSpec,
|
|
2022
2077
|
errorsSpec,
|
|
2078
|
+
explainSpec,
|
|
2023
2079
|
logsSpec,
|
|
2024
2080
|
cdnCheckSpec,
|
|
2025
2081
|
dbSchemaSpec,
|