@nifrajs/cli 4.0.2 → 5.0.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 +3 -0
- package/dist/agent-files.d.ts +107 -0
- package/dist/agent-files.d.ts.map +1 -0
- package/dist/agent-files.js +162 -0
- package/dist/agent-files.js.map +1 -0
- package/dist/app-files.d.ts +6 -0
- package/dist/app-files.d.ts.map +1 -1
- package/dist/app-files.js +27 -2
- package/dist/app-files.js.map +1 -1
- package/dist/assure-hydration.d.ts.map +1 -1
- package/dist/assure-hydration.js +7 -2
- package/dist/assure-hydration.js.map +1 -1
- package/dist/capabilities-tool.d.ts.map +1 -1
- package/dist/capabilities-tool.js +13 -3
- package/dist/capabilities-tool.js.map +1 -1
- package/dist/check-diagnostics.d.ts +20 -2
- package/dist/check-diagnostics.d.ts.map +1 -1
- package/dist/check-diagnostics.js +54 -7
- package/dist/check-diagnostics.js.map +1 -1
- package/dist/check-scan.d.ts +75 -22
- package/dist/check-scan.d.ts.map +1 -1
- package/dist/check-scan.js +1475 -310
- package/dist/check-scan.js.map +1 -1
- package/dist/check.d.ts +2 -2
- package/dist/check.d.ts.map +1 -1
- package/dist/check.js +192 -65
- package/dist/check.js.map +1 -1
- package/dist/cli.d.ts +7 -0
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +112 -5
- package/dist/cli.js.map +1 -1
- package/dist/client-module.d.ts +18 -0
- package/dist/client-module.d.ts.map +1 -0
- package/dist/client-module.js +49 -0
- package/dist/client-module.js.map +1 -0
- package/dist/command-catalog.d.ts +16 -5
- package/dist/command-catalog.d.ts.map +1 -1
- package/dist/command-catalog.js +133 -29
- package/dist/command-catalog.js.map +1 -1
- package/dist/component-check.d.ts +13 -0
- package/dist/component-check.d.ts.map +1 -0
- package/dist/component-check.js +59 -0
- package/dist/component-check.js.map +1 -0
- package/dist/contract-proof.d.ts +1 -1
- package/dist/contract-proof.d.ts.map +1 -1
- package/dist/contract-proof.js +2 -24
- package/dist/contract-proof.js.map +1 -1
- package/dist/contracts.d.ts +19 -0
- package/dist/contracts.d.ts.map +1 -1
- package/dist/contracts.js +50 -3
- package/dist/contracts.js.map +1 -1
- package/dist/db-tool.d.ts.map +1 -1
- package/dist/db-tool.js +4 -3
- package/dist/db-tool.js.map +1 -1
- package/dist/dev-bun-config.d.ts +5 -1
- package/dist/dev-bun-config.d.ts.map +1 -1
- package/dist/dev-bun-config.js +44 -11
- package/dist/dev-bun-config.js.map +1 -1
- package/dist/diagnostics.d.ts +3 -0
- package/dist/diagnostics.d.ts.map +1 -1
- package/dist/diagnostics.js.map +1 -1
- package/dist/diff-tool.d.ts +9 -8
- package/dist/diff-tool.d.ts.map +1 -1
- package/dist/diff-tool.js +28 -24
- package/dist/diff-tool.js.map +1 -1
- package/dist/doctor.d.ts +13 -0
- package/dist/doctor.d.ts.map +1 -1
- package/dist/doctor.js +23 -15
- package/dist/doctor.js.map +1 -1
- package/dist/env-file.d.ts +6 -0
- package/dist/env-file.d.ts.map +1 -1
- package/dist/env-file.js +6 -0
- package/dist/env-file.js.map +1 -1
- package/dist/fix-recipes.d.ts.map +1 -1
- package/dist/fix-recipes.js +21 -2
- package/dist/fix-recipes.js.map +1 -1
- package/dist/hand-built-manifest.d.ts +27 -0
- package/dist/hand-built-manifest.d.ts.map +1 -0
- package/dist/hand-built-manifest.js +124 -0
- package/dist/hand-built-manifest.js.map +1 -0
- package/dist/hooks-report.d.ts +23 -0
- package/dist/hooks-report.d.ts.map +1 -0
- package/dist/hooks-report.js +104 -0
- package/dist/hooks-report.js.map +1 -0
- package/dist/init-agents.d.ts +2 -2
- package/dist/init-agents.d.ts.map +1 -1
- package/dist/init-agents.js +17 -11
- package/dist/init-agents.js.map +1 -1
- package/dist/internal/nifra-project.d.ts +41 -0
- package/dist/internal/nifra-project.d.ts.map +1 -0
- package/dist/internal/nifra-project.js +183 -0
- package/dist/internal/nifra-project.js.map +1 -0
- package/dist/internal/project-specifiers.d.ts +16 -0
- package/dist/internal/project-specifiers.d.ts.map +1 -0
- package/dist/internal/project-specifiers.js +396 -0
- package/dist/internal/project-specifiers.js.map +1 -0
- package/dist/internal/script-kind.d.ts.map +1 -1
- package/dist/internal/script-kind.js +2 -1
- package/dist/internal/script-kind.js.map +1 -1
- package/dist/internal/source-facts.d.ts +2 -0
- package/dist/internal/source-facts.d.ts.map +1 -1
- package/dist/internal/source-facts.js +11 -0
- package/dist/internal/source-facts.js.map +1 -1
- package/dist/internal/typescript-import.d.ts +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 +6 -6
- package/dist/introspect.d.ts.map +1 -1
- package/dist/introspect.js +55 -24
- package/dist/introspect.js.map +1 -1
- package/dist/levels-tool.d.ts.map +1 -1
- package/dist/levels-tool.js +93 -15
- package/dist/levels-tool.js.map +1 -1
- package/dist/load.d.ts +31 -8
- package/dist/load.d.ts.map +1 -1
- package/dist/load.js +37 -7
- package/dist/load.js.map +1 -1
- package/dist/mcp-context.d.ts +5 -5
- package/dist/mcp-context.d.ts.map +1 -1
- package/dist/mcp-context.js +2 -2
- package/dist/mcp-context.js.map +1 -1
- package/dist/mcp-exec.d.ts +12 -4
- package/dist/mcp-exec.d.ts.map +1 -1
- package/dist/mcp-exec.js +50 -13
- package/dist/mcp-exec.js.map +1 -1
- package/dist/mcp-http.js +1 -1
- package/dist/mcp-io.d.ts +17 -0
- package/dist/mcp-io.d.ts.map +1 -1
- package/dist/mcp-io.js +40 -0
- package/dist/mcp-io.js.map +1 -1
- package/dist/mcp-isolate.d.ts.map +1 -1
- package/dist/mcp-isolate.js +3 -2
- package/dist/mcp-isolate.js.map +1 -1
- package/dist/mcp-project-child.js +1 -1
- package/dist/mcp-project-child.js.map +1 -1
- package/dist/mcp-root.d.ts.map +1 -1
- package/dist/mcp-root.js +50 -10
- package/dist/mcp-root.js.map +1 -1
- package/dist/mcp-ws.d.ts.map +1 -1
- package/dist/mcp-ws.js +3 -1
- package/dist/mcp-ws.js.map +1 -1
- package/dist/mcp.d.ts.map +1 -1
- package/dist/mcp.js +8 -2
- package/dist/mcp.js.map +1 -1
- package/dist/migrate-layout.d.ts +7 -0
- package/dist/migrate-layout.d.ts.map +1 -1
- package/dist/migrate-layout.js +778 -64
- package/dist/migrate-layout.js.map +1 -1
- package/dist/output-schemas.d.ts +94 -0
- package/dist/output-schemas.d.ts.map +1 -0
- package/dist/output-schemas.js +864 -0
- package/dist/output-schemas.js.map +1 -0
- package/dist/port.d.ts.map +1 -1
- package/dist/port.js +3 -2
- package/dist/port.js.map +1 -1
- package/dist/project-facts.d.ts +15 -4
- package/dist/project-facts.d.ts.map +1 -1
- package/dist/project-facts.js +1 -0
- package/dist/project-facts.js.map +1 -1
- package/dist/recipes/5.0.0.d.ts +11 -0
- package/dist/recipes/5.0.0.d.ts.map +1 -0
- package/dist/recipes/5.0.0.js +34 -0
- package/dist/recipes/5.0.0.js.map +1 -0
- package/dist/recipes/index.d.ts +7 -0
- package/dist/recipes/index.d.ts.map +1 -1
- package/dist/recipes/index.js +2 -0
- package/dist/recipes/index.js.map +1 -1
- package/dist/replay.d.ts.map +1 -1
- package/dist/replay.js +12 -3
- package/dist/replay.js.map +1 -1
- package/dist/review.d.ts +3 -1
- package/dist/review.d.ts.map +1 -1
- package/dist/review.js +38 -7
- package/dist/review.js.map +1 -1
- package/dist/rules/codes.d.ts +10 -0
- package/dist/rules/codes.d.ts.map +1 -1
- package/dist/rules/codes.js +10 -0
- package/dist/rules/codes.js.map +1 -1
- package/dist/rules/comment-markers.d.ts +16 -7
- package/dist/rules/comment-markers.d.ts.map +1 -1
- package/dist/rules/comment-markers.js +58 -19
- package/dist/rules/comment-markers.js.map +1 -1
- package/dist/rules/contract-visibility.d.ts +3 -0
- package/dist/rules/contract-visibility.d.ts.map +1 -0
- package/dist/rules/contract-visibility.js +337 -0
- package/dist/rules/contract-visibility.js.map +1 -0
- package/dist/rules/data-guard.d.ts +7 -0
- package/dist/rules/data-guard.d.ts.map +1 -1
- package/dist/rules/data-guard.js +319 -37
- package/dist/rules/data-guard.js.map +1 -1
- package/dist/rules/layout.d.ts +4 -0
- package/dist/rules/layout.d.ts.map +1 -0
- package/dist/rules/layout.js +19 -0
- package/dist/rules/layout.js.map +1 -0
- package/dist/rules/legacy.d.ts.map +1 -1
- package/dist/rules/legacy.js +158 -43
- package/dist/rules/legacy.js.map +1 -1
- package/dist/rules/pages.d.ts +24 -0
- package/dist/rules/pages.d.ts.map +1 -1
- package/dist/rules/pages.js +221 -3
- package/dist/rules/pages.js.map +1 -1
- package/dist/rules/routes.d.ts +6 -0
- package/dist/rules/routes.d.ts.map +1 -1
- package/dist/rules/routes.js +111 -16
- package/dist/rules/routes.js.map +1 -1
- package/dist/rules/secrets.js +2 -2
- package/dist/rules/secrets.js.map +1 -1
- package/dist/rules/security.d.ts +12 -1
- package/dist/rules/security.d.ts.map +1 -1
- package/dist/rules/security.js +281 -68
- package/dist/rules/security.js.map +1 -1
- package/dist/rules/zones.d.ts +13 -0
- package/dist/rules/zones.d.ts.map +1 -1
- package/dist/rules/zones.js +192 -40
- package/dist/rules/zones.js.map +1 -1
- package/dist/scaffold.d.ts.map +1 -1
- package/dist/scaffold.js +6 -4
- package/dist/scaffold.js.map +1 -1
- package/dist/sdk.d.ts.map +1 -1
- package/dist/sdk.js +14 -5
- package/dist/sdk.js.map +1 -1
- package/dist/stylex-migrate.d.ts.map +1 -1
- package/dist/stylex-migrate.js +163 -7
- package/dist/stylex-migrate.js.map +1 -1
- package/dist/test-tool.d.ts.map +1 -1
- package/dist/test-tool.js +12 -3
- package/dist/test-tool.js.map +1 -1
- package/dist/typecheck-coverage.d.ts +20 -0
- package/dist/typecheck-coverage.d.ts.map +1 -0
- package/dist/typecheck-coverage.js +129 -0
- package/dist/typecheck-coverage.js.map +1 -0
- package/dist/upgrade.d.ts +69 -0
- package/dist/upgrade.d.ts.map +1 -1
- package/dist/upgrade.js +420 -77
- package/dist/upgrade.js.map +1 -1
- package/dist/verification-plan.d.ts.map +1 -1
- package/dist/verification-plan.js +11 -0
- package/dist/verification-plan.js.map +1 -1
- package/dist/version-drift.d.ts +22 -0
- package/dist/version-drift.d.ts.map +1 -0
- package/dist/version-drift.js +42 -0
- package/dist/version-drift.js.map +1 -0
- package/dist/web-app-options.d.ts +1 -1
- package/dist/web-app-options.d.ts.map +1 -1
- package/dist/web-app-options.js +1 -1
- package/dist/web-app-options.js.map +1 -1
- package/dist/work-graph.d.ts.map +1 -1
- package/dist/work-graph.js +20 -8
- package/dist/work-graph.js.map +1 -1
- package/docs/examples.json +54 -6
- package/docs/llms-full.txt +1539 -138
- package/docs/types.json +1456 -206
- package/package.json +11 -12
- package/src/agent-files.ts +203 -0
- package/src/app-files.ts +34 -4
- package/src/assure-hydration.ts +15 -2
- package/src/capabilities-tool.ts +15 -3
- package/src/check-diagnostics.ts +73 -9
- package/src/check-scan.ts +1773 -381
- package/src/check.ts +237 -76
- package/src/cli.ts +141 -5
- package/src/client-module.ts +61 -0
- package/src/command-catalog.ts +166 -37
- package/src/component-check.ts +79 -0
- package/src/contract-proof.ts +3 -25
- package/src/contracts.ts +60 -3
- package/src/db-tool.ts +5 -2
- package/src/dev-bun-config.ts +51 -10
- package/src/diagnostics.ts +3 -0
- package/src/diff-tool.ts +33 -26
- package/src/doctor.ts +43 -21
- package/src/env-file.ts +7 -0
- package/src/fix-recipes.ts +25 -2
- package/src/hand-built-manifest.ts +144 -0
- package/src/hooks-report.ts +135 -0
- package/src/init-agents.ts +17 -11
- package/src/internal/nifra-project.ts +191 -0
- package/src/internal/project-specifiers.ts +424 -0
- package/src/internal/script-kind.ts +2 -1
- package/src/internal/source-facts.ts +25 -0
- package/src/internal/typescript-import.ts +10 -3
- package/src/introspect.ts +65 -27
- package/src/levels-tool.ts +117 -20
- package/src/load.ts +78 -14
- package/src/mcp-context.ts +7 -7
- package/src/mcp-exec.ts +60 -17
- package/src/mcp-http.ts +1 -1
- package/src/mcp-io.ts +41 -0
- package/src/mcp-isolate.ts +9 -2
- package/src/mcp-project-child.ts +1 -1
- package/src/mcp-root.ts +58 -8
- package/src/mcp-ws.ts +4 -2
- package/src/mcp.ts +8 -3
- package/src/migrate-layout.ts +885 -63
- package/src/output-schemas.ts +1084 -0
- package/src/port.ts +3 -2
- package/src/project-facts.ts +17 -3
- package/src/recipes/5.0.0.ts +35 -0
- package/src/recipes/index.ts +10 -0
- package/src/replay.ts +14 -3
- package/src/review.ts +39 -10
- package/src/rules/codes.ts +10 -0
- package/src/rules/comment-markers.ts +57 -21
- package/src/rules/contract-visibility.ts +383 -0
- package/src/rules/data-guard.ts +351 -42
- package/src/rules/layout.ts +22 -0
- package/src/rules/legacy.ts +184 -51
- package/src/rules/pages.ts +238 -3
- package/src/rules/routes.ts +124 -16
- package/src/rules/secrets.ts +2 -2
- package/src/rules/security.ts +352 -71
- package/src/rules/zones.ts +205 -38
- package/src/scaffold.ts +6 -4
- package/src/sdk.ts +14 -5
- package/src/stylex-migrate.ts +134 -7
- package/src/test-tool.ts +18 -3
- package/src/typecheck-coverage.ts +134 -0
- package/src/upgrade.ts +487 -71
- package/src/verification-plan.ts +24 -0
- package/src/version-drift.ts +67 -0
- package/src/web-app-options.ts +2 -2
- package/src/work-graph.ts +17 -12
package/src/cli.ts
CHANGED
|
@@ -24,7 +24,7 @@ import { formatShadowedPages, shadowedPages } from "@nifrajs/web/route-manifest"
|
|
|
24
24
|
import type { BunPlugin } from "bun"
|
|
25
25
|
import { adapterFile, BACKEND_APP_FILE } from "./app-files.ts"
|
|
26
26
|
import { bindCommandArgv, findCommandSpec, renderCommandCatalogHelp } from "./command-catalog.ts"
|
|
27
|
-
import { applyEnvFiles, forgetAutoLoadedEnv, takeEnvFileFlags } from "./env-file.ts"
|
|
27
|
+
import { applyEnvFiles, forgetAutoLoadedEnv, REFLECT_ENV, takeEnvFileFlags } from "./env-file.ts"
|
|
28
28
|
import { FRAMEWORK_WEB_OPTIONS, type LoadedApp, loadApp, type NifraFramework } from "./load.ts"
|
|
29
29
|
import { chooseBuildPipeline, describePipeline } from "./pipeline-guard.ts"
|
|
30
30
|
import {
|
|
@@ -165,6 +165,10 @@ Usage:
|
|
|
165
165
|
against it: a route the target can't honour (an ssr route on a
|
|
166
166
|
static build, ISR where there's no revalidation) exits nonzero
|
|
167
167
|
with the consequence - run in CI so it fails the build, not prod.
|
|
168
|
+
nifra routes --hooks [--json] List the hooks each backend route and each prefix's unmatched
|
|
169
|
+
[--since <file>] paths actually run: kind, plugin name, and where it was
|
|
170
|
+
registered (app, group, merge, mount). --since diffs against a
|
|
171
|
+
saved --json report.
|
|
168
172
|
nifra init-agents [--force] [--json] Retrofit an EXISTING app with the agent files a new app ships:
|
|
169
173
|
.mcp.json + .cursor/mcp.json (register this project's nifra MCP),
|
|
170
174
|
each agent's pointer to AGENTS.md (CLAUDE.md, GEMINI.md,
|
|
@@ -201,6 +205,8 @@ Usage:
|
|
|
201
205
|
backend code in browser code). Run as "done"; --json for agents;
|
|
202
206
|
--sarif emits the same stable diagnostics as SARIF 2.1.0 for external
|
|
203
207
|
review tools; --lints-only skips tsc for a near-instant inner-loop pass.
|
|
208
|
+
With a contracts.lock.json, both modes import backend/app.ts
|
|
209
|
+
(running its top-level code) to compare the routes it serves.
|
|
204
210
|
nifra verify [--release] [--json] Run the shared repository verification gate. --release runs the
|
|
205
211
|
full build, test, coverage, corpus, consumer, and cross-runtime gate.
|
|
206
212
|
nifra fix [--code <NF-code>] Apply a registered diagnostic recipe, then print the remaining
|
|
@@ -262,7 +268,8 @@ Usage:
|
|
|
262
268
|
[--list] [--json] apply exact imports, pin every matching dependency (keeping
|
|
263
269
|
[--exact] ^/~, or exact with --exact), then verify with nifra check.
|
|
264
270
|
[--allow-downgrade] Dry-run by default; --write applies then verifies (--no-verify
|
|
265
|
-
|
|
271
|
+
[--no-hooks] to skip), then prints how hook scope changed (--no-hooks to
|
|
272
|
+
skip). --list shows available targets. A version newer than
|
|
266
273
|
this CLI prints the command for that release's CLI. Fail-closed
|
|
267
274
|
on an unknown version or a rollback (--allow-downgrade
|
|
268
275
|
overrides). Deterministic + idempotent.
|
|
@@ -276,6 +283,10 @@ Usage:
|
|
|
276
283
|
params, the data its output schemas let through, and api typed
|
|
277
284
|
by backend/app.ts. dev, build and check refresh them too;
|
|
278
285
|
--check fails when one is stale.
|
|
286
|
+
nifra schemas [--write] [--json] Draft loaderOutput/actionOutput from each loader's and
|
|
287
|
+
action's return type for review; --write adds the missing
|
|
288
|
+
ones (marked nifra:generated) and refreshes generated ones.
|
|
289
|
+
Lists the returned fields a hand-written schema drops.
|
|
279
290
|
nifra port [--target <t>] [--json] Portability linter: print a feature × deploy-target capability
|
|
280
291
|
[--ci] [--strict] matrix (in-memory stores, in-process cron/WebSocket, Bun/Deno
|
|
281
292
|
globals, node: builtins) with file:line evidence. --target auto-
|
|
@@ -304,12 +315,23 @@ set in the process environment is never overwritten by a file.
|
|
|
304
315
|
${renderCommandCatalogHelp()}`
|
|
305
316
|
|
|
306
317
|
// Kept in lockstep with packages/cli/package.json by check:publish's version-consistency gate.
|
|
307
|
-
const CLI_VERSION = "
|
|
318
|
+
const CLI_VERSION = "5.0.0"
|
|
308
319
|
|
|
309
320
|
// A render adapter + nifra server are opaque to the CLI (it just forwards them); cast at the seam.
|
|
310
321
|
const asAdapter = (v: unknown): RenderAdapter => v as RenderAdapter
|
|
311
322
|
const asBunPlugins = (v: readonly unknown[]): BunPlugin[] => v as BunPlugin[]
|
|
312
323
|
|
|
324
|
+
// biome-ignore lint/plugin/requireSafetyCommentForTypeAssertion: Trusted application configuration supplies native Vite plugins; the pinned pipeline validates their hooks.
|
|
325
|
+
const asComponentPlugins = (
|
|
326
|
+
value: readonly unknown[],
|
|
327
|
+
): NonNullable<
|
|
328
|
+
import("@nifrajs/web/server-components/build").ComponentPipelineOptions["plugins"]
|
|
329
|
+
> =>
|
|
330
|
+
// biome-ignore lint/plugin/requireSafetyCommentForTypeAssertion: Trusted config forwards the pinned Vite plugin ABI; this does not accept request data.
|
|
331
|
+
value as NonNullable<
|
|
332
|
+
import("@nifrajs/web/server-components/build").ComponentPipelineOptions["plugins"]
|
|
333
|
+
>
|
|
334
|
+
|
|
313
335
|
/**
|
|
314
336
|
* Render an error for the CLI, unwrapping the detail a bare `.message` drops.
|
|
315
337
|
*
|
|
@@ -345,6 +367,27 @@ export function formatCliError(err: unknown): string {
|
|
|
345
367
|
}
|
|
346
368
|
|
|
347
369
|
async function dev(app: LoadedApp, flags: Flags): Promise<void> {
|
|
370
|
+
if (app.framework.componentPipeline !== undefined) {
|
|
371
|
+
if (flags.bun)
|
|
372
|
+
throw new Error(
|
|
373
|
+
"[nifra/components] Components require the Vite 8 pipeline; Bun remains a server runtime",
|
|
374
|
+
)
|
|
375
|
+
const { devServerComponents } = await import("@nifrajs/web/server-components/build")
|
|
376
|
+
if (
|
|
377
|
+
app.resolvedPlugins.clientPlugins.length > 0 ||
|
|
378
|
+
app.resolvedPlugins.serverPlugins.length > 0
|
|
379
|
+
)
|
|
380
|
+
throw new Error(
|
|
381
|
+
"[nifra/components] Use Vite equivalents for client/server Bun plugins in the component pipeline",
|
|
382
|
+
)
|
|
383
|
+
await devServerComponents({
|
|
384
|
+
...app.framework.componentPipeline,
|
|
385
|
+
root: app.cwd,
|
|
386
|
+
port: flags.port,
|
|
387
|
+
plugins: asComponentPlugins(app.resolvedPlugins.vitePlugins),
|
|
388
|
+
})
|
|
389
|
+
return
|
|
390
|
+
}
|
|
348
391
|
if (flags.vite && flags.bun) {
|
|
349
392
|
throw new Error("[nifra] `nifra dev` takes `--vite` or `--bun`, not both.")
|
|
350
393
|
}
|
|
@@ -511,6 +554,7 @@ async function dev(app: LoadedApp, flags: Flags): Promise<void> {
|
|
|
511
554
|
...(fw.conditions ? { conditions: fw.conditions } : {}),
|
|
512
555
|
...(fw.define ? { define: fw.define } : {}),
|
|
513
556
|
indicator: showsIndicator(flags, fw),
|
|
557
|
+
allowedHosts: fw.dev?.allowedHosts ?? [],
|
|
514
558
|
createApp: (clientEntry, importQuery, dev) =>
|
|
515
559
|
createWebApp({
|
|
516
560
|
adapter: asAdapter(fw.adapter),
|
|
@@ -693,7 +737,7 @@ async function assertFreshWorkspaceDists(cwd: string): Promise<void> {
|
|
|
693
737
|
let rootPackage: Record<string, unknown>
|
|
694
738
|
try {
|
|
695
739
|
rootPackage = JSON.parse(readFileSync(pkgPath, "utf8")) as Record<string, unknown>
|
|
696
|
-
//
|
|
740
|
+
// nifra-expect NF-S001: not a security gate. An unreadable/non-JSON package.json is a separate
|
|
697
741
|
// build problem; returning here skips the freshness check rather than masking a denial.
|
|
698
742
|
} catch {
|
|
699
743
|
return // an unreadable/!JSON package.json is a separate problem; don't mask it as a stale-dist failure
|
|
@@ -727,6 +771,40 @@ async function buildForTarget(
|
|
|
727
771
|
throw new Error("[nifra] `nifra build` takes `--vite` or `--bun`, not both.")
|
|
728
772
|
}
|
|
729
773
|
const { framework: fw } = app
|
|
774
|
+
if (fw.componentPipeline !== undefined) {
|
|
775
|
+
if (flags.bun)
|
|
776
|
+
throw new Error(
|
|
777
|
+
"[nifra/components] Components require the Vite 8 pipeline; Bun remains a server runtime",
|
|
778
|
+
)
|
|
779
|
+
if (target === "static")
|
|
780
|
+
throw new Error(
|
|
781
|
+
"[nifra/components] Component owners require private per-request execution; choose bun, node, deno, cloudflare, or vercel",
|
|
782
|
+
)
|
|
783
|
+
if (
|
|
784
|
+
app.resolvedPlugins.clientPlugins.length > 0 ||
|
|
785
|
+
app.resolvedPlugins.serverPlugins.length > 0
|
|
786
|
+
)
|
|
787
|
+
throw new Error(
|
|
788
|
+
"[nifra/components] Use Vite equivalents for client/server Bun plugins in the component pipeline",
|
|
789
|
+
)
|
|
790
|
+
const { buildServerComponents } = await import("@nifrajs/web/server-components/build")
|
|
791
|
+
const outDir =
|
|
792
|
+
target === "vercel" && flags.out === undefined
|
|
793
|
+
? resolve(app.cwd, ".vercel/output")
|
|
794
|
+
: app.outDir
|
|
795
|
+
const result = await buildServerComponents({
|
|
796
|
+
...fw.componentPipeline,
|
|
797
|
+
target,
|
|
798
|
+
root: app.cwd,
|
|
799
|
+
outDir,
|
|
800
|
+
plugins: asComponentPlugins(app.resolvedPlugins.vitePlugins),
|
|
801
|
+
})
|
|
802
|
+
await assertFreshWorkspaceDists(app.cwd)
|
|
803
|
+
console.log(
|
|
804
|
+
`[nifra/components] Built ${fw.componentPipeline.adapter} for ${target} (${result.build}) in ${outDir}`,
|
|
805
|
+
)
|
|
806
|
+
return
|
|
807
|
+
}
|
|
730
808
|
const cssCodeSplitRequested = fw.cssCodeSplit === false
|
|
731
809
|
const hasBunTransforms =
|
|
732
810
|
app.resolvedPlugins.clientPlugins.length > 0 || app.resolvedPlugins.serverPlugins.length > 0
|
|
@@ -945,6 +1023,42 @@ function installReflectionExitHint(): (command: string | undefined) => void {
|
|
|
945
1023
|
}
|
|
946
1024
|
}
|
|
947
1025
|
|
|
1026
|
+
/** Commands that import the app only to read what it declares, never sending it a request, so the
|
|
1027
|
+
* app may skip connecting to its services. `assure` qualifies without its bundle lane, which renders. */
|
|
1028
|
+
const READ_ONLY_COMMANDS: ReadonlySet<string> = new Set([
|
|
1029
|
+
"capabilities",
|
|
1030
|
+
"check",
|
|
1031
|
+
"context",
|
|
1032
|
+
"contracts",
|
|
1033
|
+
"diff",
|
|
1034
|
+
"doctor",
|
|
1035
|
+
"manifest",
|
|
1036
|
+
"openapi",
|
|
1037
|
+
"routes",
|
|
1038
|
+
"sync-manifest",
|
|
1039
|
+
"sync-routes",
|
|
1040
|
+
])
|
|
1041
|
+
|
|
1042
|
+
const HELD_OPEN_GRACE_MS = 2_000
|
|
1043
|
+
|
|
1044
|
+
/**
|
|
1045
|
+
* A command that imports the app for its answer has already answered when the app's own work - a
|
|
1046
|
+
* database or Redis client reconnecting, an interval - would keep the process alive indefinitely.
|
|
1047
|
+
* A process that exits on its own never reaches this unref'd timer; a held one is ended with a note.
|
|
1048
|
+
*/
|
|
1049
|
+
function exitIfHeldOpen(command: string): void {
|
|
1050
|
+
setTimeout(() => {
|
|
1051
|
+
process.stderr.write(
|
|
1052
|
+
`[nifra] \`nifra ${command}\` has finished, but something the app started when it was imported ` +
|
|
1053
|
+
`(an open connection or a timer) is keeping the process alive, so nifra is exiting.` +
|
|
1054
|
+
(process.env[REFLECT_ENV] === command
|
|
1055
|
+
? ` To skip that work while nifra only reads the app, check process.env.${REFLECT_ENV}, which is set to "${command}".\n`
|
|
1056
|
+
: "\n"),
|
|
1057
|
+
)
|
|
1058
|
+
process.exit(typeof process.exitCode === "number" ? process.exitCode : 0)
|
|
1059
|
+
}, HELD_OPEN_GRACE_MS).unref()
|
|
1060
|
+
}
|
|
1061
|
+
|
|
948
1062
|
/** Set on the copy of `nifra mcp` that serves without `.env` files, so it does not restart again. */
|
|
949
1063
|
const MCP_WITHOUT_ENV_FILES = "NIFRA_MCP_WITHOUT_ENV_FILES"
|
|
950
1064
|
|
|
@@ -1051,6 +1165,9 @@ async function main(): Promise<void> {
|
|
|
1051
1165
|
const catalogSpec = findCommandSpec(dbSub === undefined ? command : `db-${dbSub}`)
|
|
1052
1166
|
if (catalogSpec?.transports.includes("cli")) {
|
|
1053
1167
|
const markReflecting = installReflectionExitHint()
|
|
1168
|
+
if (READ_ONLY_COMMANDS.has(command) || (command === "assure" && !assureBundleRequested(argv))) {
|
|
1169
|
+
process.env[REFLECT_ENV] = command
|
|
1170
|
+
}
|
|
1054
1171
|
try {
|
|
1055
1172
|
const input = bindCommandArgv(catalogSpec, argv.slice(dbSub === undefined ? 1 : 2))
|
|
1056
1173
|
markReflecting(command)
|
|
@@ -1086,6 +1203,7 @@ async function main(): Promise<void> {
|
|
|
1086
1203
|
? candidate
|
|
1087
1204
|
: 1
|
|
1088
1205
|
}
|
|
1206
|
+
exitIfHeldOpen(command)
|
|
1089
1207
|
return
|
|
1090
1208
|
}
|
|
1091
1209
|
if (command === "verify") {
|
|
@@ -1150,6 +1268,7 @@ async function main(): Promise<void> {
|
|
|
1150
1268
|
json: argv.includes("--json"),
|
|
1151
1269
|
list: argv.includes("--list"),
|
|
1152
1270
|
verify: !argv.includes("--no-verify"),
|
|
1271
|
+
hooks: !argv.includes("--no-hooks"),
|
|
1153
1272
|
allowDowngrade: argv.includes("--allow-downgrade"),
|
|
1154
1273
|
exact: argv.includes("--exact"),
|
|
1155
1274
|
cliVersion: CLI_VERSION,
|
|
@@ -1278,12 +1397,29 @@ function findCliCommand(values: readonly string[]): number {
|
|
|
1278
1397
|
return values.findIndex((value) => isCliCommandToken(value))
|
|
1279
1398
|
}
|
|
1280
1399
|
|
|
1400
|
+
/**
|
|
1401
|
+
* The command vector from one runtime argv: the command, then its arguments. Arguments typed before
|
|
1402
|
+
* the command (`nifra --json check`) follow the command's own, ahead of any `--`, rather than being
|
|
1403
|
+
* dropped. Only when index 1 is the script: a runtime flag there means the leading entries are the
|
|
1404
|
+
* runtime's own, not the caller's.
|
|
1405
|
+
*/
|
|
1406
|
+
export function commandArgv(values: readonly string[], commandIndex: number): readonly string[] {
|
|
1407
|
+
const command = values.slice(commandIndex)
|
|
1408
|
+
const script = values[1]
|
|
1409
|
+
if (commandIndex <= 2 || script === undefined || script.startsWith("-")) return command
|
|
1410
|
+
const leading = values.slice(2, commandIndex)
|
|
1411
|
+
const separator = command.indexOf("--")
|
|
1412
|
+
return separator === -1
|
|
1413
|
+
? [...command, ...leading]
|
|
1414
|
+
: [...command.slice(0, separator), ...leading, ...command.slice(separator)]
|
|
1415
|
+
}
|
|
1416
|
+
|
|
1281
1417
|
function cliArgv(): readonly string[] {
|
|
1282
1418
|
const reexec = reexecCliArgv()
|
|
1283
1419
|
if (reexec !== undefined) return reexec
|
|
1284
1420
|
for (const values of [Bun.argv, process.argv]) {
|
|
1285
1421
|
const commandIndex = findCliCommand(values)
|
|
1286
|
-
if (commandIndex >= 0) return values
|
|
1422
|
+
if (commandIndex >= 0) return commandArgv(values, commandIndex)
|
|
1287
1423
|
}
|
|
1288
1424
|
return Bun.argv.slice(2)
|
|
1289
1425
|
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* An app's `clientModule` when its config names a first-party file. The browser bundle's generated
|
|
3
|
+
* entry imports it, so the build classifies that file, and everything it imports, as browser code.
|
|
4
|
+
*/
|
|
5
|
+
import { existsSync, readFileSync } from "node:fs"
|
|
6
|
+
import { join } from "node:path"
|
|
7
|
+
import { CONFIG_FILE, FRAMEWORK_FILE } from "./app-files.ts"
|
|
8
|
+
import { stripComments } from "./check-scan.ts"
|
|
9
|
+
import type { SpecifierResolver } from "./internal/project-specifiers.ts"
|
|
10
|
+
|
|
11
|
+
const CLIENT_MODULE_EXPORT =
|
|
12
|
+
/\bexport\s+const\s+clientModule\s*(?::\s*string\s*)?=\s*(?:"([^"\\\r\n]*)"|'([^'\\\r\n]*)')/
|
|
13
|
+
|
|
14
|
+
export interface ClientModuleRoot {
|
|
15
|
+
/** The project-relative config file that names it. */
|
|
16
|
+
readonly config: string
|
|
17
|
+
readonly line: number
|
|
18
|
+
readonly specifier: string
|
|
19
|
+
/** The real path of the file it names. */
|
|
20
|
+
readonly file: string
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** The project-relative directory of every app among `files`: one holding a nifra config. */
|
|
24
|
+
export function appDirs(files: readonly string[]): string[] {
|
|
25
|
+
const apps = new Set<string>()
|
|
26
|
+
for (const file of files) {
|
|
27
|
+
for (const config of [CONFIG_FILE, FRAMEWORK_FILE]) {
|
|
28
|
+
if (file === config) apps.add("")
|
|
29
|
+
else if (file.endsWith(`/${config}`)) apps.add(file.slice(0, -config.length - 1))
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
return [...apps]
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* 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`, and a relative path against the app root. `undefined`
|
|
38
|
+
* for a package specifier, a value that is not a string literal, or a path to no file.
|
|
39
|
+
*/
|
|
40
|
+
export function clientModuleRoot(
|
|
41
|
+
root: string,
|
|
42
|
+
app: string,
|
|
43
|
+
resolve: SpecifierResolver,
|
|
44
|
+
): ClientModuleRoot | undefined {
|
|
45
|
+
const config = existsSync(join(root, app, CONFIG_FILE)) ? CONFIG_FILE : FRAMEWORK_FILE
|
|
46
|
+
const path = join(root, app, config)
|
|
47
|
+
if (!existsSync(path)) return undefined
|
|
48
|
+
const text = readFileSync(path, "utf8")
|
|
49
|
+
// `stripComments` keeps offsets, so the match index is a position in `text`.
|
|
50
|
+
const match = CLIENT_MODULE_EXPORT.exec(stripComments(text))
|
|
51
|
+
if (match === null) return undefined
|
|
52
|
+
const specifier = match[1] ?? match[2] ?? ""
|
|
53
|
+
const file = resolve(join(root, app, CONFIG_FILE), specifier)
|
|
54
|
+
if (file === undefined) return undefined
|
|
55
|
+
return {
|
|
56
|
+
config: app === "" ? config : `${app}/${config}`,
|
|
57
|
+
line: text.slice(0, match.index).split("\n").length,
|
|
58
|
+
specifier,
|
|
59
|
+
file,
|
|
60
|
+
}
|
|
61
|
+
}
|
package/src/command-catalog.ts
CHANGED
|
@@ -22,9 +22,10 @@ import { type Diagnostic, diagnostic, normalizeSeverity, toSarifLog } from "./di
|
|
|
22
22
|
import type { DoctorResult } from "./doctor.ts"
|
|
23
23
|
import { type I18nCheckOutput, i18nCheckPassed, renderI18nCheck } from "./i18n-check.ts"
|
|
24
24
|
import type { VerificationLevelsResult } from "./levels-tool.ts"
|
|
25
|
-
import type {
|
|
25
|
+
import type { AppLoader, IntrospectedApp } from "./load.ts"
|
|
26
26
|
import type { ManifestEmitCommandResult } from "./manifest-tool.ts"
|
|
27
27
|
import type { LayoutMigrationResult } from "./migrate-layout.ts"
|
|
28
|
+
import type { OutputSchemaStatus, OutputSchemasReport } from "./output-schemas.ts"
|
|
28
29
|
import { collectPortResult, type PortResult, renderReport } from "./port.ts"
|
|
29
30
|
import type { ReplayResult } from "./replay.ts"
|
|
30
31
|
import { reviewSpec } from "./review.ts"
|
|
@@ -80,8 +81,8 @@ export interface CommandCtx {
|
|
|
80
81
|
readonly signal?: AbortSignal
|
|
81
82
|
readonly progress?: (message: string) => void
|
|
82
83
|
readonly cliVersion?: string
|
|
83
|
-
/** Adapter-provided cache for commands that need the loaded
|
|
84
|
-
readonly loadApp?:
|
|
84
|
+
/** Adapter-provided cache for commands that need the loaded app. */
|
|
85
|
+
readonly loadApp?: AppLoader
|
|
85
86
|
}
|
|
86
87
|
|
|
87
88
|
export interface CommandSpec<Input, Output> {
|
|
@@ -328,6 +329,8 @@ interface RoutesInput {
|
|
|
328
329
|
readonly json?: boolean | undefined
|
|
329
330
|
readonly graph?: boolean | undefined
|
|
330
331
|
readonly modes?: boolean | undefined
|
|
332
|
+
readonly hooks?: boolean | undefined
|
|
333
|
+
readonly since?: string | undefined
|
|
331
334
|
readonly target?: string | undefined
|
|
332
335
|
readonly dir?: string | undefined
|
|
333
336
|
}
|
|
@@ -390,6 +393,12 @@ interface TypesInput {
|
|
|
390
393
|
readonly dir?: string | undefined
|
|
391
394
|
}
|
|
392
395
|
|
|
396
|
+
interface SchemasInput {
|
|
397
|
+
readonly write?: boolean | undefined
|
|
398
|
+
readonly json?: boolean | undefined
|
|
399
|
+
readonly dir?: string | undefined
|
|
400
|
+
}
|
|
401
|
+
|
|
393
402
|
interface SnapshotInput {
|
|
394
403
|
readonly out?: string | undefined
|
|
395
404
|
readonly json?: boolean | undefined
|
|
@@ -458,7 +467,7 @@ interface AssureCommandOutput {
|
|
|
458
467
|
interface LevelsCommandOutput extends VerificationLevelsResult {}
|
|
459
468
|
interface RoutesCommandOutput {
|
|
460
469
|
readonly ok: boolean
|
|
461
|
-
readonly mode: "default" | "graph" | "modes"
|
|
470
|
+
readonly mode: "default" | "graph" | "modes" | "hooks"
|
|
462
471
|
readonly data: unknown
|
|
463
472
|
readonly text: string
|
|
464
473
|
}
|
|
@@ -493,6 +502,9 @@ interface DiffCommandOutput {
|
|
|
493
502
|
readonly hasBreaking: boolean
|
|
494
503
|
readonly changes: readonly unknown[]
|
|
495
504
|
}
|
|
505
|
+
interface RouteDiffCommandOutput extends DiffCommandOutput {
|
|
506
|
+
readonly text: string
|
|
507
|
+
}
|
|
496
508
|
interface ContractsCommandOutput {
|
|
497
509
|
readonly ok: boolean
|
|
498
510
|
readonly lock?: ContractsLock
|
|
@@ -647,15 +659,20 @@ const ROUTES_SCHEMA = input<RoutesInput>(
|
|
|
647
659
|
json: { type: "boolean" },
|
|
648
660
|
graph: { type: "boolean" },
|
|
649
661
|
modes: { type: "boolean" },
|
|
662
|
+
hooks: { type: "boolean" },
|
|
663
|
+
since: { type: "string" },
|
|
650
664
|
target: { type: "string" },
|
|
651
665
|
dir: { type: "string" },
|
|
652
666
|
}),
|
|
653
667
|
(value) => {
|
|
654
668
|
const raw = withDir(record(value))
|
|
655
|
-
if (raw.graph
|
|
656
|
-
throw new TypeError("graph and
|
|
669
|
+
if ([raw.graph, raw.modes, raw.hooks].filter((flag) => flag === true).length > 1)
|
|
670
|
+
throw new TypeError("graph, modes and hooks cannot be combined")
|
|
671
|
+
if (raw.since !== undefined && raw.hooks !== true)
|
|
672
|
+
throw new TypeError("since compares hook reports and needs hooks")
|
|
657
673
|
return {
|
|
658
|
-
...parseBooleanFlags(raw, ["json", "graph", "modes"]),
|
|
674
|
+
...parseBooleanFlags(raw, ["json", "graph", "modes", "hooks"]),
|
|
675
|
+
since: optionalString(raw.since, "since"),
|
|
659
676
|
target: optionalString(raw.target, "target"),
|
|
660
677
|
...(raw.dir === undefined ? {} : { dir: raw.dir }),
|
|
661
678
|
}
|
|
@@ -781,6 +798,17 @@ const TYPES_SCHEMA = input<TypesInput>(
|
|
|
781
798
|
},
|
|
782
799
|
)
|
|
783
800
|
|
|
801
|
+
const SCHEMAS_SCHEMA = input<SchemasInput>(
|
|
802
|
+
objectSchema({ write: { type: "boolean" }, json: { type: "boolean" }, dir: { type: "string" } }),
|
|
803
|
+
(value) => {
|
|
804
|
+
const raw = withDir(record(value))
|
|
805
|
+
return {
|
|
806
|
+
...parseBooleanFlags(raw, ["write", "json"]),
|
|
807
|
+
...(raw.dir === undefined ? {} : { dir: raw.dir }),
|
|
808
|
+
}
|
|
809
|
+
},
|
|
810
|
+
)
|
|
811
|
+
|
|
784
812
|
const SNAPSHOT_SCHEMA = input<SnapshotInput>(
|
|
785
813
|
objectSchema({ out: { type: "string" }, json: { type: "boolean" }, dir: { type: "string" } }),
|
|
786
814
|
(value) => {
|
|
@@ -944,9 +972,10 @@ const I18N_SCHEMA = input<I18nInput>(
|
|
|
944
972
|
},
|
|
945
973
|
)
|
|
946
974
|
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
975
|
+
/** The app as the backend readers see it: an API-only app is its `backend/app.ts` alone. */
|
|
976
|
+
function backendAppFor(ctx: CommandCtx): Promise<IntrospectedApp> {
|
|
977
|
+
if (ctx.loadApp !== undefined) return ctx.loadApp.backend()
|
|
978
|
+
return import("./load.ts").then(({ loadBackendApp }) => loadBackendApp(ctx.cwd))
|
|
950
979
|
}
|
|
951
980
|
|
|
952
981
|
function structuredDiagnostics(out: CheckCommandOutput): readonly Diagnostic[] {
|
|
@@ -1195,7 +1224,8 @@ const manifestSpec: CommandSpec<ManifestInput, ManifestEmitCommandResult | DiffC
|
|
|
1195
1224
|
|
|
1196
1225
|
const routesSpec: CommandSpec<RoutesInput, RoutesCommandOutput> = {
|
|
1197
1226
|
name: "routes",
|
|
1198
|
-
summary:
|
|
1227
|
+
summary:
|
|
1228
|
+
"List, graph, or target-check every page and API route the app serves, or the hooks each runs.",
|
|
1199
1229
|
input: ROUTES_SCHEMA,
|
|
1200
1230
|
output: output({
|
|
1201
1231
|
type: "object",
|
|
@@ -1209,12 +1239,47 @@ const routesSpec: CommandSpec<RoutesInput, RoutesCommandOutput> = {
|
|
|
1209
1239
|
{ name: "json", field: "json", type: "boolean" },
|
|
1210
1240
|
{ name: "graph", field: "graph", type: "boolean" },
|
|
1211
1241
|
{ name: "modes", field: "modes", type: "boolean" },
|
|
1242
|
+
{ name: "hooks", field: "hooks", type: "boolean" },
|
|
1243
|
+
{ name: "since", field: "since", type: "string" },
|
|
1212
1244
|
{ name: "target", field: "target", type: "string" },
|
|
1213
1245
|
],
|
|
1214
1246
|
},
|
|
1215
1247
|
async run(value, ctx) {
|
|
1216
|
-
|
|
1248
|
+
if (value.hooks === true) {
|
|
1249
|
+
const app = await backendAppFor(ctx)
|
|
1250
|
+
const hooks = await import("./hooks-report.ts")
|
|
1251
|
+
const report = await hooks.collectHookReport(app.backend)
|
|
1252
|
+
if (value.since === undefined) {
|
|
1253
|
+
return { ok: true, mode: "hooks", data: report, text: hooks.renderHookReport(report) }
|
|
1254
|
+
}
|
|
1255
|
+
const saved = hooks.parseHookReport(
|
|
1256
|
+
await Bun.file(resolve(ctx.cwd, value.since))
|
|
1257
|
+
.text()
|
|
1258
|
+
.catch(() => ""),
|
|
1259
|
+
)
|
|
1260
|
+
if (saved === undefined) {
|
|
1261
|
+
return {
|
|
1262
|
+
ok: false,
|
|
1263
|
+
mode: "hooks",
|
|
1264
|
+
data: { report, error: `${value.since} is not a saved hook report` },
|
|
1265
|
+
text: `[nifra] ${value.since} is not a saved hook report (write one with \`nifra routes --hooks --json\`)`,
|
|
1266
|
+
}
|
|
1267
|
+
}
|
|
1268
|
+
const changes = hooks.diffHookReports(saved, report)
|
|
1269
|
+
return {
|
|
1270
|
+
ok: true,
|
|
1271
|
+
mode: "hooks",
|
|
1272
|
+
data: { report, changes },
|
|
1273
|
+
text: hooks.renderHookDiff(changes),
|
|
1274
|
+
}
|
|
1275
|
+
}
|
|
1276
|
+
const app = await backendAppFor(ctx)
|
|
1217
1277
|
if (value.modes === true) {
|
|
1278
|
+
if (app.framework === undefined) {
|
|
1279
|
+
throw new Error(
|
|
1280
|
+
`[nifra] routes --modes reads page routes, and ${ctx.cwd} is an API-only app (backend/app.ts, no web config)`,
|
|
1281
|
+
)
|
|
1282
|
+
}
|
|
1218
1283
|
const { buildRouteManifest, renderRouteManifest } = await import(
|
|
1219
1284
|
"@nifrajs/web/route-manifest"
|
|
1220
1285
|
)
|
|
@@ -1264,7 +1329,7 @@ const contextSpec: CommandSpec<ContextInput, ContextCommandOutput> = {
|
|
|
1264
1329
|
async run(value, ctx) {
|
|
1265
1330
|
return {
|
|
1266
1331
|
ok: true,
|
|
1267
|
-
text: (await import("./introspect.ts")).describeProject(await
|
|
1332
|
+
text: (await import("./introspect.ts")).describeProject(await backendAppFor(ctx), {
|
|
1268
1333
|
...(value.path === undefined ? {} : { path: value.path }),
|
|
1269
1334
|
...(value.kind === undefined ? {} : { kind: value.kind }),
|
|
1270
1335
|
}),
|
|
@@ -1292,10 +1357,7 @@ const openApiSpec: CommandSpec<OpenApiInput, OpenApiCommandOutput> = {
|
|
|
1292
1357
|
],
|
|
1293
1358
|
},
|
|
1294
1359
|
async run(value, ctx) {
|
|
1295
|
-
const app =
|
|
1296
|
-
ctx.loadApp !== undefined
|
|
1297
|
-
? await ctx.loadApp()
|
|
1298
|
-
: await import("./load.ts").then(({ loadBackendApp }) => loadBackendApp(ctx.cwd))
|
|
1360
|
+
const app = await backendAppFor(ctx)
|
|
1299
1361
|
const { renderOpenApiWithTypes } = await import("./openapi-tool.ts")
|
|
1300
1362
|
const format = value.format ?? "json"
|
|
1301
1363
|
return {
|
|
@@ -1430,6 +1492,8 @@ const migrateSpec: CommandSpec<MigrateInput, MigrateCommandOutput> = {
|
|
|
1430
1492
|
splits: { type: "array" },
|
|
1431
1493
|
moves: { type: "array" },
|
|
1432
1494
|
rewritten: { type: "array" },
|
|
1495
|
+
tooling: { type: "array" },
|
|
1496
|
+
mode: { type: "string" },
|
|
1433
1497
|
},
|
|
1434
1498
|
required: ["ok", "write", "issues"],
|
|
1435
1499
|
}),
|
|
@@ -1459,13 +1523,14 @@ const migrateSpec: CommandSpec<MigrateInput, MigrateCommandOutput> = {
|
|
|
1459
1523
|
if (out.kind === "layout") {
|
|
1460
1524
|
return [
|
|
1461
1525
|
out.ok
|
|
1462
|
-
? `✓ ${out.splits.length} route file${out.splits.length === 1 ? "" : "s"} split, ${out.moves.length} file${out.moves.length === 1 ? "" : "s"} moved`
|
|
1526
|
+
? `✓ ${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`
|
|
1463
1527
|
: `⚠ ${out.issues.length} issue${out.issues.length === 1 ? "" : "s"} need a decision; everything else is planned`,
|
|
1464
1528
|
...out.splits.map(
|
|
1465
1529
|
(split) => ` split ${split.file} → ${split.backend} (${split.moved.join(", ")})`,
|
|
1466
1530
|
),
|
|
1467
1531
|
...out.moves.map((move) => ` move ${move.from} → ${move.to}`),
|
|
1468
1532
|
...out.rewritten.map((file) => ` edit ${file} (imports)`),
|
|
1533
|
+
...out.tooling.map((edit) => ` edit ${edit.file} (${edit.change})`),
|
|
1469
1534
|
...out.issues.map((issue) => ` ⚠ ${issue.file}: ${issue.reason}`),
|
|
1470
1535
|
...(out.write ? [] : [" dry run: pass --write to apply"]),
|
|
1471
1536
|
]
|
|
@@ -1586,6 +1651,75 @@ const typesSpec: CommandSpec<TypesInput, RouteTypesReport> = {
|
|
|
1586
1651
|
success: (out) => out.ok,
|
|
1587
1652
|
}
|
|
1588
1653
|
|
|
1654
|
+
const SCHEMA_STATUS_LINE: Readonly<Record<OutputSchemaStatus, string>> = {
|
|
1655
|
+
missing: "+ would write",
|
|
1656
|
+
generated: "+ wrote",
|
|
1657
|
+
stale: "↻ stale",
|
|
1658
|
+
regenerated: "↻ rewrote",
|
|
1659
|
+
current: "✓ current",
|
|
1660
|
+
declared: "· hand-written",
|
|
1661
|
+
"no-data": "· no data",
|
|
1662
|
+
unreadable: "⚠ unreadable",
|
|
1663
|
+
}
|
|
1664
|
+
|
|
1665
|
+
const schemasSpec: CommandSpec<SchemasInput, OutputSchemasReport> = {
|
|
1666
|
+
name: "schemas",
|
|
1667
|
+
summary:
|
|
1668
|
+
"Write a reviewable `loaderOutput`/`actionOutput` from the loader's or action's return type where a route has none (`--write`), keep generated ones current, and list returned fields a hand-written schema drops.",
|
|
1669
|
+
input: SCHEMAS_SCHEMA,
|
|
1670
|
+
output: output({
|
|
1671
|
+
type: "object",
|
|
1672
|
+
properties: {
|
|
1673
|
+
ok: { type: "boolean" },
|
|
1674
|
+
write: { type: "boolean" },
|
|
1675
|
+
entries: { type: "array" },
|
|
1676
|
+
written: { type: "array" },
|
|
1677
|
+
warnings: { type: "array" },
|
|
1678
|
+
},
|
|
1679
|
+
required: ["ok", "write", "entries", "written", "warnings"],
|
|
1680
|
+
}),
|
|
1681
|
+
transports: ["cli"],
|
|
1682
|
+
stability: "stable",
|
|
1683
|
+
argv: {
|
|
1684
|
+
flags: [
|
|
1685
|
+
{ name: "write", field: "write", type: "boolean" },
|
|
1686
|
+
{ name: "json", field: "json", type: "boolean" },
|
|
1687
|
+
{ name: "dir", field: "dir", type: "string" },
|
|
1688
|
+
],
|
|
1689
|
+
},
|
|
1690
|
+
async run(value, ctx) {
|
|
1691
|
+
const { outputSchemas } = await import("./output-schemas.ts")
|
|
1692
|
+
return outputSchemas(resolve(ctx.cwd, value.dir ?? "."), { write: value.write === true })
|
|
1693
|
+
},
|
|
1694
|
+
render: (out) => {
|
|
1695
|
+
const shown = out.entries.filter(
|
|
1696
|
+
(entry) =>
|
|
1697
|
+
entry.status !== "current" &&
|
|
1698
|
+
entry.status !== "no-data" &&
|
|
1699
|
+
(entry.status !== "declared" || entry.dropped.length > 0),
|
|
1700
|
+
)
|
|
1701
|
+
const current = out.entries.filter((entry) => entry.status === "current").length
|
|
1702
|
+
return [
|
|
1703
|
+
...out.warnings.map((warning) => `⚠ ${warning}`),
|
|
1704
|
+
`${out.ok ? "✓" : "✖"} ${out.entries.length} output schema${out.entries.length === 1 ? "" : "s"} read, ${current} generated and current`,
|
|
1705
|
+
...shown.map((entry) => {
|
|
1706
|
+
const detail =
|
|
1707
|
+
entry.reason ??
|
|
1708
|
+
(entry.edited === true
|
|
1709
|
+
? "edited by hand under its nifra:generated line"
|
|
1710
|
+
: entry.dropped.length > 0
|
|
1711
|
+
? `drops ${entry.dropped.join(", ")}`
|
|
1712
|
+
: undefined)
|
|
1713
|
+
return ` ${SCHEMA_STATUS_LINE[entry.status]} ${entry.file}:${entry.line} ${entry.schema}${detail === undefined ? "" : ` - ${detail}`}`
|
|
1714
|
+
}),
|
|
1715
|
+
...(out.write || !out.entries.some((e) => e.status === "missing" || e.status === "stale")
|
|
1716
|
+
? []
|
|
1717
|
+
: [" dry run: pass --write to apply"]),
|
|
1718
|
+
]
|
|
1719
|
+
},
|
|
1720
|
+
success: (out) => out.ok,
|
|
1721
|
+
}
|
|
1722
|
+
|
|
1589
1723
|
const snapshotSpec: CommandSpec<SnapshotInput, SnapshotCommandOutput> = {
|
|
1590
1724
|
name: "snapshot",
|
|
1591
1725
|
summary: "Write the backend API contract as a versioned JSON baseline.",
|
|
@@ -1610,18 +1744,16 @@ const snapshotSpec: CommandSpec<SnapshotInput, SnapshotCommandOutput> = {
|
|
|
1610
1744
|
},
|
|
1611
1745
|
async run(value, ctx) {
|
|
1612
1746
|
const mod = await import("./diff-tool.ts")
|
|
1613
|
-
const routes = await mod.snapshotBackend(ctx.cwd)
|
|
1614
1747
|
const file = value.out ?? mod.DEFAULT_SNAPSHOT_FILE
|
|
1615
|
-
const snapshot =
|
|
1616
|
-
|
|
1617
|
-
return { ok: true, file, routes: routes.length, snapshot }
|
|
1748
|
+
const snapshot = await mod.writeSnapshot(ctx.cwd, file)
|
|
1749
|
+
return { ok: true, file, routes: snapshot.routes.length, snapshot }
|
|
1618
1750
|
},
|
|
1619
1751
|
render: (out) => [
|
|
1620
1752
|
`[nifra] wrote ${out.routes} route${out.routes === 1 ? "" : "s"} to ${out.file}`,
|
|
1621
1753
|
],
|
|
1622
1754
|
}
|
|
1623
1755
|
|
|
1624
|
-
const diffSpec: CommandSpec<DiffInput,
|
|
1756
|
+
const diffSpec: CommandSpec<DiffInput, RouteDiffCommandOutput> = {
|
|
1625
1757
|
name: "diff",
|
|
1626
1758
|
summary: "Compare the current backend contract with a baseline and fail on breaking changes.",
|
|
1627
1759
|
input: DIFF_SCHEMA,
|
|
@@ -1638,21 +1770,17 @@ const diffSpec: CommandSpec<DiffInput, DiffCommandOutput> = {
|
|
|
1638
1770
|
stability: "stable",
|
|
1639
1771
|
argv: { positionals: ["baseline"], flags: [{ name: "json", field: "json", type: "boolean" }] },
|
|
1640
1772
|
async run(value, ctx) {
|
|
1641
|
-
const
|
|
1642
|
-
const
|
|
1643
|
-
|
|
1644
|
-
|
|
1645
|
-
|
|
1646
|
-
|
|
1647
|
-
|
|
1648
|
-
|
|
1773
|
+
const { diffAgainstBaseline, formatDiff } = await import("./diff-tool.ts")
|
|
1774
|
+
const diff = await diffAgainstBaseline(ctx.cwd, value.baseline)
|
|
1775
|
+
return {
|
|
1776
|
+
ok: !diff.hasBreaking,
|
|
1777
|
+
hasBreaking: diff.hasBreaking,
|
|
1778
|
+
changes: diff.changes,
|
|
1779
|
+
text: formatDiff(diff),
|
|
1780
|
+
}
|
|
1649
1781
|
},
|
|
1650
|
-
render: (out) => [
|
|
1651
|
-
|
|
1652
|
-
...(out.changes as readonly { method: string; path: string; message: string }[]).map(
|
|
1653
|
-
(c) => `${c.method} ${c.path}: ${c.message}`,
|
|
1654
|
-
),
|
|
1655
|
-
],
|
|
1782
|
+
render: (out) => [out.text],
|
|
1783
|
+
json: ({ ok, hasBreaking, changes }) => ({ ok, hasBreaking, changes }),
|
|
1656
1784
|
success: (out) => out.ok,
|
|
1657
1785
|
}
|
|
1658
1786
|
|
|
@@ -1880,6 +2008,7 @@ export const commandSpecs = Object.freeze([
|
|
|
1880
2008
|
migrateSpec,
|
|
1881
2009
|
targetSpec,
|
|
1882
2010
|
typesSpec,
|
|
2011
|
+
schemasSpec,
|
|
1883
2012
|
snapshotSpec,
|
|
1884
2013
|
diffSpec,
|
|
1885
2014
|
contractsSpec,
|