@nifrajs/cli 4.0.2 → 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 +10 -2
- 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 +9 -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 +36 -9
- package/dist/capabilities-tool.js.map +1 -1
- package/dist/check-diagnostics.d.ts +22 -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 +83 -24
- package/dist/check-scan.d.ts.map +1 -1
- package/dist/check-scan.js +1516 -318
- 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 +203 -70
- package/dist/check.js.map +1 -1
- package/dist/cli.d.ts +9 -0
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +215 -50
- package/dist/cli.js.map +1 -1
- package/dist/client-module.d.ts +20 -0
- package/dist/client-module.d.ts.map +1 -0
- package/dist/client-module.js +73 -0
- package/dist/client-module.js.map +1 -0
- package/dist/command-catalog.d.ts +24 -5
- package/dist/command-catalog.d.ts.map +1 -1
- package/dist/command-catalog.js +194 -45
- 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/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/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 +2 -0
- package/dist/fix-recipes.d.ts.map +1 -1
- package/dist/fix-recipes.js +102 -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 +18 -2
- package/dist/init-agents.d.ts.map +1 -1
- package/dist/init-agents.js +20 -13
- 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/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 +29 -0
- package/dist/internal/project-specifiers.d.ts.map +1 -0
- package/dist/internal/project-specifiers.js +435 -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 +8 -1
- package/dist/internal/source-facts.d.ts.map +1 -1
- package/dist/internal/source-facts.js +22 -5
- 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 +10 -6
- package/dist/internal/typescript-import.js.map +1 -1
- package/dist/introspect.d.ts +15 -6
- package/dist/introspect.d.ts.map +1 -1
- package/dist/introspect.js +87 -25
- package/dist/introspect.js.map +1 -1
- package/dist/levels-tool.d.ts.map +1 -1
- package/dist/levels-tool.js +98 -23
- package/dist/levels-tool.js.map +1 -1
- package/dist/load.d.ts +33 -10
- package/dist/load.d.ts.map +1 -1
- package/dist/load.js +60 -10
- 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 +8 -23
- 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 +13 -4
- package/dist/mcp-exec.d.ts.map +1 -1
- package/dist/mcp-exec.js +56 -14
- package/dist/mcp-exec.js.map +1 -1
- package/dist/mcp-http.js +1 -1
- package/dist/mcp-io.d.ts +30 -0
- package/dist/mcp-io.d.ts.map +1 -1
- package/dist/mcp-io.js +63 -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-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-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 +48 -19
- package/dist/mcp.js.map +1 -1
- package/dist/migrate-layout.d.ts +47 -1
- package/dist/migrate-layout.d.ts.map +1 -1
- package/dist/migrate-layout.js +1972 -170
- package/dist/migrate-layout.js.map +1 -1
- package/dist/output-schemas.d.ts +128 -0
- package/dist/output-schemas.d.ts.map +1 -0
- package/dist/output-schemas.js +975 -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/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/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/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/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 +43 -7
- package/dist/review.js.map +1 -1
- package/dist/rules/codes.d.ts +12 -0
- package/dist/rules/codes.d.ts.map +1 -1
- package/dist/rules/codes.js +12 -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 +13 -0
- package/dist/rules/data-guard.d.ts.map +1 -1
- package/dist/rules/data-guard.js +356 -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 +178 -44
- package/dist/rules/legacy.js.map +1 -1
- package/dist/rules/pages.d.ts +50 -0
- package/dist/rules/pages.d.ts.map +1 -1
- package/dist/rules/pages.js +436 -4
- 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 +13 -1
- package/dist/rules/security.d.ts.map +1 -1
- package/dist/rules/security.js +313 -69
- 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 +218 -44
- 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 +290 -24
- package/dist/stylex-migrate.js.map +1 -1
- package/dist/test-tool.d.ts.map +1 -1
- package/dist/test-tool.js +6 -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 +84 -0
- package/dist/upgrade.d.ts.map +1 -1
- package/dist/upgrade.js +498 -78
- package/dist/upgrade.js.map +1 -1
- package/dist/verification-plan.d.ts.map +1 -1
- package/dist/verification-plan.js +16 -2
- 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 +94 -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 +55 -7
- package/docs/llms-full.txt +2043 -222
- package/docs/types.json +1573 -246
- package/package.json +11 -12
- package/src/agent-files.ts +203 -0
- package/src/app-files.ts +34 -4
- package/src/assure-hydration.ts +18 -2
- package/src/assure.ts +88 -10
- package/src/capabilities-tool.ts +47 -9
- package/src/check-diagnostics.ts +75 -9
- package/src/check-scan.ts +1816 -377
- package/src/check.ts +258 -81
- package/src/cli.ts +242 -37
- package/src/client-module.ts +97 -0
- package/src/command-catalog.ts +237 -52
- 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/dev-feed-tool.ts +112 -1
- 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 +116 -2
- package/src/hand-built-manifest.ts +144 -0
- package/src/hooks-report.ts +135 -0
- package/src/init-agents.ts +21 -14
- package/src/internal/config-paths.ts +154 -0
- package/src/internal/installed-package.ts +15 -0
- package/src/internal/nifra-project.ts +191 -0
- package/src/internal/project-specifiers.ts +477 -0
- package/src/internal/script-kind.ts +2 -1
- package/src/internal/source-facts.ts +48 -5
- package/src/internal/typescript-import.ts +15 -6
- package/src/introspect.ts +104 -28
- package/src/levels-tool.ts +124 -29
- package/src/load.ts +107 -17
- package/src/mcp-context.ts +12 -25
- package/src/mcp-delegate.ts +22 -10
- package/src/mcp-exec.ts +68 -18
- package/src/mcp-http.ts +1 -1
- package/src/mcp-io.ts +74 -0
- package/src/mcp-isolate.ts +9 -2
- package/src/mcp-path.ts +8 -22
- 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 +56 -22
- package/src/migrate-layout.ts +2257 -176
- package/src/output-schemas.ts +1240 -0
- package/src/port.ts +3 -2
- package/src/project-facts.ts +17 -3
- package/src/project-path.ts +44 -13
- package/src/recipes/5.0.0.ts +35 -0
- package/src/recipes/index.ts +10 -0
- package/src/release-verification.ts +20 -0
- package/src/replay.ts +14 -3
- package/src/review.ts +44 -10
- package/src/rules/codes.ts +12 -0
- package/src/rules/comment-markers.ts +57 -21
- package/src/rules/contract-visibility.ts +383 -0
- package/src/rules/data-guard.ts +391 -42
- package/src/rules/layout.ts +22 -0
- package/src/rules/legacy.ts +206 -52
- package/src/rules/pages.ts +481 -4
- package/src/rules/routes.ts +124 -16
- package/src/rules/secrets.ts +2 -2
- package/src/rules/security.ts +391 -72
- package/src/rules/zones.ts +232 -42
- package/src/scaffold.ts +6 -4
- package/src/sdk.ts +14 -5
- package/src/stylex-migrate.ts +251 -24
- package/src/test-tool.ts +12 -3
- package/src/typecheck-coverage.ts +134 -0
- package/src/upgrade.ts +576 -72
- package/src/verification-plan.ts +33 -2
- package/src/version-drift.ts +118 -0
- package/src/web-app-options.ts +2 -2
- package/src/work-graph.ts +17 -12
package/src/command-catalog.ts
CHANGED
|
@@ -17,14 +17,15 @@ 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"
|
|
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
|
}
|
|
@@ -364,6 +367,8 @@ interface MigrateInput {
|
|
|
364
367
|
readonly from?: "tailwind" | undefined
|
|
365
368
|
readonly to?: "stylex" | undefined
|
|
366
369
|
readonly write?: boolean | undefined
|
|
370
|
+
/** `migrate layout`: list the folders modules would leave in package.json zones instead. */
|
|
371
|
+
readonly zones?: boolean | undefined
|
|
367
372
|
readonly json?: boolean | undefined
|
|
368
373
|
readonly dir?: string | undefined
|
|
369
374
|
}
|
|
@@ -390,6 +395,13 @@ interface TypesInput {
|
|
|
390
395
|
readonly dir?: string | undefined
|
|
391
396
|
}
|
|
392
397
|
|
|
398
|
+
interface SchemasInput {
|
|
399
|
+
readonly write?: boolean | undefined
|
|
400
|
+
readonly check?: boolean | undefined
|
|
401
|
+
readonly json?: boolean | undefined
|
|
402
|
+
readonly dir?: string | undefined
|
|
403
|
+
}
|
|
404
|
+
|
|
393
405
|
interface SnapshotInput {
|
|
394
406
|
readonly out?: string | undefined
|
|
395
407
|
readonly json?: boolean | undefined
|
|
@@ -458,7 +470,7 @@ interface AssureCommandOutput {
|
|
|
458
470
|
interface LevelsCommandOutput extends VerificationLevelsResult {}
|
|
459
471
|
interface RoutesCommandOutput {
|
|
460
472
|
readonly ok: boolean
|
|
461
|
-
readonly mode: "default" | "graph" | "modes"
|
|
473
|
+
readonly mode: "default" | "graph" | "modes" | "hooks"
|
|
462
474
|
readonly data: unknown
|
|
463
475
|
readonly text: string
|
|
464
476
|
}
|
|
@@ -477,6 +489,8 @@ interface FixCommandOutput {
|
|
|
477
489
|
/** Recipes that refused, with the reason. A recipe that cannot act says so here instead of
|
|
478
490
|
* aborting the run or - worse - reporting nothing at all. */
|
|
479
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 }[]
|
|
480
494
|
readonly diagnostics: readonly unknown[]
|
|
481
495
|
}
|
|
482
496
|
type MigrateCommandOutput =
|
|
@@ -493,6 +507,9 @@ interface DiffCommandOutput {
|
|
|
493
507
|
readonly hasBreaking: boolean
|
|
494
508
|
readonly changes: readonly unknown[]
|
|
495
509
|
}
|
|
510
|
+
interface RouteDiffCommandOutput extends DiffCommandOutput {
|
|
511
|
+
readonly text: string
|
|
512
|
+
}
|
|
496
513
|
interface ContractsCommandOutput {
|
|
497
514
|
readonly ok: boolean
|
|
498
515
|
readonly lock?: ContractsLock
|
|
@@ -647,15 +664,20 @@ const ROUTES_SCHEMA = input<RoutesInput>(
|
|
|
647
664
|
json: { type: "boolean" },
|
|
648
665
|
graph: { type: "boolean" },
|
|
649
666
|
modes: { type: "boolean" },
|
|
667
|
+
hooks: { type: "boolean" },
|
|
668
|
+
since: { type: "string" },
|
|
650
669
|
target: { type: "string" },
|
|
651
670
|
dir: { type: "string" },
|
|
652
671
|
}),
|
|
653
672
|
(value) => {
|
|
654
673
|
const raw = withDir(record(value))
|
|
655
|
-
if (raw.graph
|
|
656
|
-
throw new TypeError("graph and
|
|
674
|
+
if ([raw.graph, raw.modes, raw.hooks].filter((flag) => flag === true).length > 1)
|
|
675
|
+
throw new TypeError("graph, modes and hooks cannot be combined")
|
|
676
|
+
if (raw.since !== undefined && raw.hooks !== true)
|
|
677
|
+
throw new TypeError("since compares hook reports and needs hooks")
|
|
657
678
|
return {
|
|
658
|
-
...parseBooleanFlags(raw, ["json", "graph", "modes"]),
|
|
679
|
+
...parseBooleanFlags(raw, ["json", "graph", "modes", "hooks"]),
|
|
680
|
+
since: optionalString(raw.since, "since"),
|
|
659
681
|
target: optionalString(raw.target, "target"),
|
|
660
682
|
...(raw.dir === undefined ? {} : { dir: raw.dir }),
|
|
661
683
|
}
|
|
@@ -734,13 +756,14 @@ const MIGRATE_SCHEMA = input<MigrateInput>(
|
|
|
734
756
|
from: { type: "string", enum: ["tailwind"] },
|
|
735
757
|
to: { type: "string", enum: ["stylex"] },
|
|
736
758
|
write: { type: "boolean" },
|
|
759
|
+
zones: { type: "boolean" },
|
|
737
760
|
json: { type: "boolean" },
|
|
738
761
|
dir: { type: "string" },
|
|
739
762
|
}),
|
|
740
763
|
(value) => {
|
|
741
764
|
const raw = withDir(record(value))
|
|
742
765
|
const flags = {
|
|
743
|
-
...parseBooleanFlags(raw, ["write", "json"]),
|
|
766
|
+
...parseBooleanFlags(raw, ["write", "zones", "json"]),
|
|
744
767
|
...(raw.dir === undefined ? {} : { dir: raw.dir }),
|
|
745
768
|
}
|
|
746
769
|
const kind = optionalString(raw.kind, "kind")
|
|
@@ -749,6 +772,7 @@ const MIGRATE_SCHEMA = input<MigrateInput>(
|
|
|
749
772
|
throw new TypeError("the migration must be `layout` (or --from tailwind --to stylex)")
|
|
750
773
|
return { kind, ...flags }
|
|
751
774
|
}
|
|
775
|
+
if (raw.zones === true) throw new TypeError("--zones applies to `migrate layout` only")
|
|
752
776
|
const from = optionalString(raw.from, "from")
|
|
753
777
|
const to = optionalString(raw.to, "to")
|
|
754
778
|
if (from !== "tailwind") throw new TypeError("from must be tailwind")
|
|
@@ -781,6 +805,22 @@ const TYPES_SCHEMA = input<TypesInput>(
|
|
|
781
805
|
},
|
|
782
806
|
)
|
|
783
807
|
|
|
808
|
+
const SCHEMAS_SCHEMA = input<SchemasInput>(
|
|
809
|
+
objectSchema({
|
|
810
|
+
write: { type: "boolean" },
|
|
811
|
+
check: { type: "boolean" },
|
|
812
|
+
json: { type: "boolean" },
|
|
813
|
+
dir: { type: "string" },
|
|
814
|
+
}),
|
|
815
|
+
(value) => {
|
|
816
|
+
const raw = withDir(record(value))
|
|
817
|
+
return {
|
|
818
|
+
...parseBooleanFlags(raw, ["write", "check", "json"]),
|
|
819
|
+
...(raw.dir === undefined ? {} : { dir: raw.dir }),
|
|
820
|
+
}
|
|
821
|
+
},
|
|
822
|
+
)
|
|
823
|
+
|
|
784
824
|
const SNAPSHOT_SCHEMA = input<SnapshotInput>(
|
|
785
825
|
objectSchema({ out: { type: "string" }, json: { type: "boolean" }, dir: { type: "string" } }),
|
|
786
826
|
(value) => {
|
|
@@ -944,9 +984,10 @@ const I18N_SCHEMA = input<I18nInput>(
|
|
|
944
984
|
},
|
|
945
985
|
)
|
|
946
986
|
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
987
|
+
/** The app as the backend readers see it: an API-only app is its `backend/app.ts` alone. */
|
|
988
|
+
function backendAppFor(ctx: CommandCtx): Promise<IntrospectedApp> {
|
|
989
|
+
if (ctx.loadApp !== undefined) return ctx.loadApp.backend()
|
|
990
|
+
return import("./load.ts").then(({ loadBackendApp }) => loadBackendApp(ctx.cwd))
|
|
950
991
|
}
|
|
951
992
|
|
|
952
993
|
function structuredDiagnostics(out: CheckCommandOutput): readonly Diagnostic[] {
|
|
@@ -1195,7 +1236,8 @@ const manifestSpec: CommandSpec<ManifestInput, ManifestEmitCommandResult | DiffC
|
|
|
1195
1236
|
|
|
1196
1237
|
const routesSpec: CommandSpec<RoutesInput, RoutesCommandOutput> = {
|
|
1197
1238
|
name: "routes",
|
|
1198
|
-
summary:
|
|
1239
|
+
summary:
|
|
1240
|
+
"List, graph, or target-check every page and API route the app serves, or the hooks each runs.",
|
|
1199
1241
|
input: ROUTES_SCHEMA,
|
|
1200
1242
|
output: output({
|
|
1201
1243
|
type: "object",
|
|
@@ -1209,12 +1251,47 @@ const routesSpec: CommandSpec<RoutesInput, RoutesCommandOutput> = {
|
|
|
1209
1251
|
{ name: "json", field: "json", type: "boolean" },
|
|
1210
1252
|
{ name: "graph", field: "graph", type: "boolean" },
|
|
1211
1253
|
{ name: "modes", field: "modes", type: "boolean" },
|
|
1254
|
+
{ name: "hooks", field: "hooks", type: "boolean" },
|
|
1255
|
+
{ name: "since", field: "since", type: "string" },
|
|
1212
1256
|
{ name: "target", field: "target", type: "string" },
|
|
1213
1257
|
],
|
|
1214
1258
|
},
|
|
1215
1259
|
async run(value, ctx) {
|
|
1216
|
-
|
|
1260
|
+
if (value.hooks === true) {
|
|
1261
|
+
const app = await backendAppFor(ctx)
|
|
1262
|
+
const hooks = await import("./hooks-report.ts")
|
|
1263
|
+
const report = await hooks.collectHookReport(app.backend)
|
|
1264
|
+
if (value.since === undefined) {
|
|
1265
|
+
return { ok: true, mode: "hooks", data: report, text: hooks.renderHookReport(report) }
|
|
1266
|
+
}
|
|
1267
|
+
const saved = hooks.parseHookReport(
|
|
1268
|
+
await Bun.file(resolve(ctx.cwd, value.since))
|
|
1269
|
+
.text()
|
|
1270
|
+
.catch(() => ""),
|
|
1271
|
+
)
|
|
1272
|
+
if (saved === undefined) {
|
|
1273
|
+
return {
|
|
1274
|
+
ok: false,
|
|
1275
|
+
mode: "hooks",
|
|
1276
|
+
data: { report, error: `${value.since} is not a saved hook report` },
|
|
1277
|
+
text: `[nifra] ${value.since} is not a saved hook report (write one with \`nifra routes --hooks --json\`)`,
|
|
1278
|
+
}
|
|
1279
|
+
}
|
|
1280
|
+
const changes = hooks.diffHookReports(saved, report)
|
|
1281
|
+
return {
|
|
1282
|
+
ok: true,
|
|
1283
|
+
mode: "hooks",
|
|
1284
|
+
data: { report, changes },
|
|
1285
|
+
text: hooks.renderHookDiff(changes),
|
|
1286
|
+
}
|
|
1287
|
+
}
|
|
1288
|
+
const app = await backendAppFor(ctx)
|
|
1217
1289
|
if (value.modes === true) {
|
|
1290
|
+
if (app.framework === undefined) {
|
|
1291
|
+
throw new Error(
|
|
1292
|
+
`[nifra] routes --modes reads page routes, and ${ctx.cwd} is an API-only app (backend/app.ts, no web config)`,
|
|
1293
|
+
)
|
|
1294
|
+
}
|
|
1218
1295
|
const { buildRouteManifest, renderRouteManifest } = await import(
|
|
1219
1296
|
"@nifrajs/web/route-manifest"
|
|
1220
1297
|
)
|
|
@@ -1264,7 +1341,7 @@ const contextSpec: CommandSpec<ContextInput, ContextCommandOutput> = {
|
|
|
1264
1341
|
async run(value, ctx) {
|
|
1265
1342
|
return {
|
|
1266
1343
|
ok: true,
|
|
1267
|
-
text: (await import("./introspect.ts")).describeProject(await
|
|
1344
|
+
text: (await import("./introspect.ts")).describeProject(await backendAppFor(ctx), {
|
|
1268
1345
|
...(value.path === undefined ? {} : { path: value.path }),
|
|
1269
1346
|
...(value.kind === undefined ? {} : { kind: value.kind }),
|
|
1270
1347
|
}),
|
|
@@ -1292,10 +1369,7 @@ const openApiSpec: CommandSpec<OpenApiInput, OpenApiCommandOutput> = {
|
|
|
1292
1369
|
],
|
|
1293
1370
|
},
|
|
1294
1371
|
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))
|
|
1372
|
+
const app = await backendAppFor(ctx)
|
|
1299
1373
|
const { renderOpenApiWithTypes } = await import("./openapi-tool.ts")
|
|
1300
1374
|
const format = value.format ?? "json"
|
|
1301
1375
|
return {
|
|
@@ -1356,6 +1430,7 @@ const fixSpec: CommandSpec<FixInput, FixCommandOutput> = {
|
|
|
1356
1430
|
ok: { type: "boolean" },
|
|
1357
1431
|
changed: { type: "array" },
|
|
1358
1432
|
failed: { type: "array" },
|
|
1433
|
+
skipped: { type: "array" },
|
|
1359
1434
|
diagnostics: { type: "array" },
|
|
1360
1435
|
},
|
|
1361
1436
|
required: ["ok", "diagnostics"],
|
|
@@ -1370,25 +1445,37 @@ const fixSpec: CommandSpec<FixInput, FixCommandOutput> = {
|
|
|
1370
1445
|
},
|
|
1371
1446
|
async run(value, ctx) {
|
|
1372
1447
|
const { collectCheckResult } = await import("./check.ts")
|
|
1373
|
-
const { applyDiagnosticRecipe } = await import("./fix-recipes.ts")
|
|
1448
|
+
const { applyDiagnosticRecipe, getFixRecipe } = await import("./fix-recipes.ts")
|
|
1374
1449
|
const result = await collectCheckResult(ctx.cwd, {
|
|
1375
1450
|
lintsOnly: true,
|
|
1376
1451
|
...(ctx.signal === undefined ? {} : { signal: ctx.signal }),
|
|
1377
1452
|
})
|
|
1378
1453
|
const changed: string[] = []
|
|
1379
1454
|
const failed: { code: string; reason: string }[] = []
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
|
|
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({
|
|
1388
1463
|
code: diagnostic.code,
|
|
1389
|
-
reason:
|
|
1464
|
+
reason: `${recipe.description} Review the finding, then run \`nifra fix --code ${diagnostic.code}\`.`,
|
|
1390
1465
|
})
|
|
1391
|
-
|
|
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
|
+
}
|
|
1392
1479
|
const final = await collectCheckResult(ctx.cwd, {
|
|
1393
1480
|
lintsOnly: true,
|
|
1394
1481
|
...(ctx.signal === undefined ? {} : { signal: ctx.signal }),
|
|
@@ -1397,6 +1484,7 @@ const fixSpec: CommandSpec<FixInput, FixCommandOutput> = {
|
|
|
1397
1484
|
ok: final.ok,
|
|
1398
1485
|
changed,
|
|
1399
1486
|
failed,
|
|
1487
|
+
skipped,
|
|
1400
1488
|
diagnostics: final.structuredDiagnostics ?? final.diagnostics,
|
|
1401
1489
|
}
|
|
1402
1490
|
},
|
|
@@ -1404,6 +1492,7 @@ const fixSpec: CommandSpec<FixInput, FixCommandOutput> = {
|
|
|
1404
1492
|
out.ok ? "✓ fixes applied" : "✖ unresolved diagnostics remain",
|
|
1405
1493
|
...out.changed.map((file) => ` changed ${file}`),
|
|
1406
1494
|
...out.failed.map((f) => ` could not fix ${f.code}: ${f.reason}`),
|
|
1495
|
+
...out.skipped.map((s) => ` needs review ${s.code}: ${s.reason}`),
|
|
1407
1496
|
...out.diagnostics.map((d) => ` ${String((d as { code?: unknown }).code ?? "diagnostic")}`),
|
|
1408
1497
|
],
|
|
1409
1498
|
success: (out) => out.ok,
|
|
@@ -1430,6 +1519,9 @@ const migrateSpec: CommandSpec<MigrateInput, MigrateCommandOutput> = {
|
|
|
1430
1519
|
splits: { type: "array" },
|
|
1431
1520
|
moves: { type: "array" },
|
|
1432
1521
|
rewritten: { type: "array" },
|
|
1522
|
+
tooling: { type: "array" },
|
|
1523
|
+
followUps: { type: "array" },
|
|
1524
|
+
mode: { type: "string" },
|
|
1433
1525
|
},
|
|
1434
1526
|
required: ["ok", "write", "issues"],
|
|
1435
1527
|
}),
|
|
@@ -1441,6 +1533,7 @@ const migrateSpec: CommandSpec<MigrateInput, MigrateCommandOutput> = {
|
|
|
1441
1533
|
{ name: "from", field: "from", type: "string" },
|
|
1442
1534
|
{ name: "to", field: "to", type: "string" },
|
|
1443
1535
|
{ name: "write", field: "write", type: "boolean" },
|
|
1536
|
+
{ name: "zones", field: "zones", type: "boolean" },
|
|
1444
1537
|
{ name: "json", field: "json", type: "boolean" },
|
|
1445
1538
|
{ name: "dir", field: "dir", type: "string" },
|
|
1446
1539
|
],
|
|
@@ -1450,7 +1543,8 @@ const migrateSpec: CommandSpec<MigrateInput, MigrateCommandOutput> = {
|
|
|
1450
1543
|
const write = value.write === undefined ? {} : { write: value.write }
|
|
1451
1544
|
if (value.kind === "layout") {
|
|
1452
1545
|
const { migrateLayout } = await import("./migrate-layout.ts")
|
|
1453
|
-
|
|
1546
|
+
const options = { ...write, zones: value.zones === true }
|
|
1547
|
+
return { kind: "layout" as const, ...(await migrateLayout(dir, options)) }
|
|
1454
1548
|
}
|
|
1455
1549
|
const { migrateTailwindToStylex } = await import("./stylex-migrate.ts")
|
|
1456
1550
|
return migrateTailwindToStylex(dir, write)
|
|
@@ -1459,14 +1553,25 @@ const migrateSpec: CommandSpec<MigrateInput, MigrateCommandOutput> = {
|
|
|
1459
1553
|
if (out.kind === "layout") {
|
|
1460
1554
|
return [
|
|
1461
1555
|
out.ok
|
|
1462
|
-
? `✓ ${out.splits.length} route file${out.splits.length === 1 ? "" : "s"} split, ${out.moves.length} file${out.moves.length === 1 ? "" : "s"} moved`
|
|
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`
|
|
1463
1557
|
: `⚠ ${out.issues.length} issue${out.issues.length === 1 ? "" : "s"} need a decision; everything else is planned`,
|
|
1464
1558
|
...out.splits.map(
|
|
1465
|
-
(split) =>
|
|
1559
|
+
(split) =>
|
|
1560
|
+
` split ${split.file} → ${split.backend} (${split.moved.join(", ")})${split.shared === undefined ? "" : `, shared data → ${split.shared}`}`,
|
|
1466
1561
|
),
|
|
1467
1562
|
...out.moves.map((move) => ` move ${move.from} → ${move.to}`),
|
|
1468
1563
|
...out.rewritten.map((file) => ` edit ${file} (imports)`),
|
|
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
|
+
),
|
|
1469
1571
|
...out.issues.map((issue) => ` ⚠ ${issue.file}: ${issue.reason}`),
|
|
1572
|
+
...out.followUps.map(
|
|
1573
|
+
(step) => ` next ${step.file}: ${step.reason} - \`${step.command}\``,
|
|
1574
|
+
),
|
|
1470
1575
|
...(out.write ? [] : [" dry run: pass --write to apply"]),
|
|
1471
1576
|
]
|
|
1472
1577
|
}
|
|
@@ -1586,6 +1691,90 @@ const typesSpec: CommandSpec<TypesInput, RouteTypesReport> = {
|
|
|
1586
1691
|
success: (out) => out.ok,
|
|
1587
1692
|
}
|
|
1588
1693
|
|
|
1694
|
+
const SCHEMA_STATUS_LINE: Readonly<Record<OutputSchemaStatus, string>> = {
|
|
1695
|
+
missing: "+ would write",
|
|
1696
|
+
generated: "+ wrote",
|
|
1697
|
+
stale: "↻ stale",
|
|
1698
|
+
regenerated: "↻ rewrote",
|
|
1699
|
+
current: "✓ current",
|
|
1700
|
+
declared: "· hand-written",
|
|
1701
|
+
"no-data": "· no data",
|
|
1702
|
+
unreadable: "⚠ unreadable",
|
|
1703
|
+
}
|
|
1704
|
+
|
|
1705
|
+
const schemasSpec: CommandSpec<SchemasInput, OutputSchemasReport> = {
|
|
1706
|
+
name: "schemas",
|
|
1707
|
+
summary:
|
|
1708
|
+
"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.",
|
|
1709
|
+
input: SCHEMAS_SCHEMA,
|
|
1710
|
+
output: output({
|
|
1711
|
+
type: "object",
|
|
1712
|
+
properties: {
|
|
1713
|
+
ok: { type: "boolean" },
|
|
1714
|
+
write: { type: "boolean" },
|
|
1715
|
+
entries: { type: "array" },
|
|
1716
|
+
written: { type: "array" },
|
|
1717
|
+
warnings: { type: "array" },
|
|
1718
|
+
},
|
|
1719
|
+
required: ["ok", "write", "entries", "written", "warnings"],
|
|
1720
|
+
}),
|
|
1721
|
+
transports: ["cli"],
|
|
1722
|
+
stability: "stable",
|
|
1723
|
+
argv: {
|
|
1724
|
+
flags: [
|
|
1725
|
+
{ name: "write", field: "write", type: "boolean" },
|
|
1726
|
+
{ name: "check", field: "check", type: "boolean" },
|
|
1727
|
+
{ name: "json", field: "json", type: "boolean" },
|
|
1728
|
+
{ name: "dir", field: "dir", type: "string" },
|
|
1729
|
+
],
|
|
1730
|
+
},
|
|
1731
|
+
async run(value, ctx) {
|
|
1732
|
+
const { outputSchemas } = await import("./output-schemas.ts")
|
|
1733
|
+
return outputSchemas(resolve(ctx.cwd, value.dir ?? "."), {
|
|
1734
|
+
write: value.write === true,
|
|
1735
|
+
check: value.check === true,
|
|
1736
|
+
})
|
|
1737
|
+
},
|
|
1738
|
+
render: (out) => {
|
|
1739
|
+
const shown = out.entries.filter(
|
|
1740
|
+
(entry) =>
|
|
1741
|
+
(entry.status !== "current" || entry.open !== undefined) &&
|
|
1742
|
+
entry.status !== "no-data" &&
|
|
1743
|
+
(entry.status !== "declared" || entry.dropped.length > 0),
|
|
1744
|
+
)
|
|
1745
|
+
const current = out.entries.filter((entry) => entry.status === "current").length
|
|
1746
|
+
return [
|
|
1747
|
+
...out.warnings.map((warning) => `⚠ ${warning}`),
|
|
1748
|
+
`${out.ok ? "✓" : "✖"} ${out.entries.length} output schema${out.entries.length === 1 ? "" : "s"} read, ${current} generated and current`,
|
|
1749
|
+
...shown.map((entry) => {
|
|
1750
|
+
const detail =
|
|
1751
|
+
entry.reason ??
|
|
1752
|
+
(entry.edited === true
|
|
1753
|
+
? "edited by hand under its nifra:generated line"
|
|
1754
|
+
: entry.dropped.length > 0
|
|
1755
|
+
? `drops ${entry.dropped.join(", ")}`
|
|
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")
|
|
1769
|
+
}),
|
|
1770
|
+
...(out.write || !out.entries.some((e) => e.status === "missing" || e.status === "stale")
|
|
1771
|
+
? []
|
|
1772
|
+
: [" dry run: pass --write to apply"]),
|
|
1773
|
+
]
|
|
1774
|
+
},
|
|
1775
|
+
success: (out) => out.ok,
|
|
1776
|
+
}
|
|
1777
|
+
|
|
1589
1778
|
const snapshotSpec: CommandSpec<SnapshotInput, SnapshotCommandOutput> = {
|
|
1590
1779
|
name: "snapshot",
|
|
1591
1780
|
summary: "Write the backend API contract as a versioned JSON baseline.",
|
|
@@ -1610,18 +1799,16 @@ const snapshotSpec: CommandSpec<SnapshotInput, SnapshotCommandOutput> = {
|
|
|
1610
1799
|
},
|
|
1611
1800
|
async run(value, ctx) {
|
|
1612
1801
|
const mod = await import("./diff-tool.ts")
|
|
1613
|
-
const routes = await mod.snapshotBackend(ctx.cwd)
|
|
1614
1802
|
const file = value.out ?? mod.DEFAULT_SNAPSHOT_FILE
|
|
1615
|
-
const snapshot =
|
|
1616
|
-
|
|
1617
|
-
return { ok: true, file, routes: routes.length, snapshot }
|
|
1803
|
+
const snapshot = await mod.writeSnapshot(ctx.cwd, file)
|
|
1804
|
+
return { ok: true, file, routes: snapshot.routes.length, snapshot }
|
|
1618
1805
|
},
|
|
1619
1806
|
render: (out) => [
|
|
1620
1807
|
`[nifra] wrote ${out.routes} route${out.routes === 1 ? "" : "s"} to ${out.file}`,
|
|
1621
1808
|
],
|
|
1622
1809
|
}
|
|
1623
1810
|
|
|
1624
|
-
const diffSpec: CommandSpec<DiffInput,
|
|
1811
|
+
const diffSpec: CommandSpec<DiffInput, RouteDiffCommandOutput> = {
|
|
1625
1812
|
name: "diff",
|
|
1626
1813
|
summary: "Compare the current backend contract with a baseline and fail on breaking changes.",
|
|
1627
1814
|
input: DIFF_SCHEMA,
|
|
@@ -1638,21 +1825,17 @@ const diffSpec: CommandSpec<DiffInput, DiffCommandOutput> = {
|
|
|
1638
1825
|
stability: "stable",
|
|
1639
1826
|
argv: { positionals: ["baseline"], flags: [{ name: "json", field: "json", type: "boolean" }] },
|
|
1640
1827
|
async run(value, ctx) {
|
|
1641
|
-
const
|
|
1642
|
-
const
|
|
1643
|
-
|
|
1644
|
-
|
|
1645
|
-
|
|
1646
|
-
|
|
1647
|
-
|
|
1648
|
-
|
|
1828
|
+
const { diffAgainstBaseline, formatDiff } = await import("./diff-tool.ts")
|
|
1829
|
+
const diff = await diffAgainstBaseline(ctx.cwd, value.baseline)
|
|
1830
|
+
return {
|
|
1831
|
+
ok: !diff.hasBreaking,
|
|
1832
|
+
hasBreaking: diff.hasBreaking,
|
|
1833
|
+
changes: diff.changes,
|
|
1834
|
+
text: formatDiff(diff),
|
|
1835
|
+
}
|
|
1649
1836
|
},
|
|
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
|
-
],
|
|
1837
|
+
render: (out) => [out.text],
|
|
1838
|
+
json: ({ ok, hasBreaking, changes }) => ({ ok, hasBreaking, changes }),
|
|
1656
1839
|
success: (out) => out.ok,
|
|
1657
1840
|
}
|
|
1658
1841
|
|
|
@@ -1880,6 +2063,7 @@ export const commandSpecs = Object.freeze([
|
|
|
1880
2063
|
migrateSpec,
|
|
1881
2064
|
targetSpec,
|
|
1882
2065
|
typesSpec,
|
|
2066
|
+
schemasSpec,
|
|
1883
2067
|
snapshotSpec,
|
|
1884
2068
|
diffSpec,
|
|
1885
2069
|
contractsSpec,
|
|
@@ -1891,6 +2075,7 @@ export const commandSpecs = Object.freeze([
|
|
|
1891
2075
|
portSpec,
|
|
1892
2076
|
i18nSpec,
|
|
1893
2077
|
errorsSpec,
|
|
2078
|
+
explainSpec,
|
|
1894
2079
|
logsSpec,
|
|
1895
2080
|
cdnCheckSpec,
|
|
1896
2081
|
dbSchemaSpec,
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from "node:fs"
|
|
2
|
+
import { resolve } from "node:path"
|
|
3
|
+
import {
|
|
4
|
+
isUnsupportedTypeScriptError,
|
|
5
|
+
type UnsupportedTypeScriptError,
|
|
6
|
+
} from "./internal/typescript-import.ts"
|
|
7
|
+
import type { SourceIndex } from "./rules/index.ts"
|
|
8
|
+
|
|
9
|
+
type ComponentSource = typeof import("@nifrajs/web/server-components/source")
|
|
10
|
+
|
|
11
|
+
export interface ComponentCheck {
|
|
12
|
+
readonly receipts: ReadonlySet<string>
|
|
13
|
+
/** Set when the project's TypeScript has no compiler API to read component modules with
|
|
14
|
+
* (TypeScript 7): no module gets a receipt, so the ordinary import rule judges each one. */
|
|
15
|
+
readonly unsupported: UnsupportedTypeScriptError | undefined
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const COMPONENT_SERVER = /\.component\.server\.[cm]?[jt]sx?$/
|
|
19
|
+
|
|
20
|
+
/** A source-only proof. nifra check never executes nifra.config.ts. */
|
|
21
|
+
export async function componentCheckReceipts(
|
|
22
|
+
root: string,
|
|
23
|
+
source: SourceIndex,
|
|
24
|
+
load: () => Promise<ComponentSource> = () => import("@nifrajs/web/server-components/source"),
|
|
25
|
+
): Promise<ComponentCheck> {
|
|
26
|
+
const receipts = new Set<string>()
|
|
27
|
+
const result: ComponentCheck = { receipts, unsupported: undefined }
|
|
28
|
+
if (!source.files.some((file) => COMPONENT_SERVER.test(file))) return result
|
|
29
|
+
const configFile = ["nifra.config.ts", "backend/framework.ts"].find((file) =>
|
|
30
|
+
existsSync(resolve(root, file)),
|
|
31
|
+
)
|
|
32
|
+
if (configFile === undefined) return result
|
|
33
|
+
let components: ComponentSource
|
|
34
|
+
try {
|
|
35
|
+
components = await load()
|
|
36
|
+
} catch (error) {
|
|
37
|
+
if (isUnsupportedTypeScriptError(error)) return { receipts, unsupported: error }
|
|
38
|
+
throw error
|
|
39
|
+
}
|
|
40
|
+
const pipeline = components.staticComponentPipeline(
|
|
41
|
+
readFileSync(resolve(root, configFile), "utf8"),
|
|
42
|
+
configFile,
|
|
43
|
+
)
|
|
44
|
+
if (typeof pipeline !== "object" || pipeline === null || Array.isArray(pipeline)) return result
|
|
45
|
+
// biome-ignore lint/plugin/requireSafetyCommentForTypeAssertion: Static receipt inspection checks object shape before reading fields; the source scanner never executes configuration.
|
|
46
|
+
const options = pipeline as Record<string, unknown>
|
|
47
|
+
if (
|
|
48
|
+
// biome-ignore lint/plugin/requireSafetyCommentForTypeAssertion: Static receipt inspection checks object shape before reading fields; the source scanner never executes configuration.
|
|
49
|
+
!["react", "vue", "svelte", "solid", "preact"].includes(options.adapter as string) ||
|
|
50
|
+
typeof options.entries !== "object" ||
|
|
51
|
+
options.entries === null ||
|
|
52
|
+
Array.isArray(options.entries)
|
|
53
|
+
)
|
|
54
|
+
return result
|
|
55
|
+
// biome-ignore lint/plugin/requireSafetyCommentForTypeAssertion: Static receipt inspection checks object shape before reading fields; the source scanner never executes configuration.
|
|
56
|
+
const entries = options.entries as Record<string, unknown>
|
|
57
|
+
if (
|
|
58
|
+
typeof entries.client !== "string" ||
|
|
59
|
+
typeof entries.ssr !== "string" ||
|
|
60
|
+
(options.adapter === "react" && typeof entries.rsc !== "string") ||
|
|
61
|
+
Object.values(entries).some(
|
|
62
|
+
(file) => typeof file !== "string" || !existsSync(resolve(root, file)),
|
|
63
|
+
)
|
|
64
|
+
)
|
|
65
|
+
return result
|
|
66
|
+
for (const file of source.files) {
|
|
67
|
+
if (!COMPONENT_SERVER.test(file)) continue
|
|
68
|
+
const content = source.read(file)
|
|
69
|
+
if (content === undefined) continue
|
|
70
|
+
try {
|
|
71
|
+
components.compileComponentModule(content, resolve(root, file), root, "client")
|
|
72
|
+
receipts.add(file)
|
|
73
|
+
receipts.add(resolve(root, file))
|
|
74
|
+
} catch {
|
|
75
|
+
/* A module without a compilation receipt remains forbidden by the ordinary import rule. */
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
return result
|
|
79
|
+
}
|
package/src/contract-proof.ts
CHANGED
|
@@ -1,14 +1,12 @@
|
|
|
1
|
-
import { existsSync, realpathSync } from "node:fs"
|
|
2
|
-
import { resolve, sep } from "node:path"
|
|
3
1
|
import type { AssuranceFinding, AssuranceReport, AssuredRoute } from "@nifrajs/core/assurance"
|
|
4
2
|
import type {
|
|
5
3
|
AssuredCapabilityRoute,
|
|
6
4
|
CapabilityAssuranceReport,
|
|
7
5
|
CapabilityFinding,
|
|
8
6
|
} from "@nifrajs/core/capabilities"
|
|
9
|
-
import {
|
|
7
|
+
import type { RouteChange, RoutesDiff } from "@nifrajs/core/diff"
|
|
10
8
|
import type { CheckResult } from "./check.ts"
|
|
11
|
-
import { DEFAULT_SNAPSHOT_FILE,
|
|
9
|
+
import { DEFAULT_SNAPSHOT_FILE, diffAgainstBaseline } from "./diff-tool.ts"
|
|
12
10
|
import { collectProjectVerification } from "./verification.ts"
|
|
13
11
|
|
|
14
12
|
const routeKey = (method: string, path: string): string => `${method.toUpperCase()}\n${path}`
|
|
@@ -59,15 +57,6 @@ export interface ContractProofOptions {
|
|
|
59
57
|
readonly signal?: AbortSignal
|
|
60
58
|
}
|
|
61
59
|
|
|
62
|
-
function safeBaselinePath(cwd: string, baselinePath: string): string {
|
|
63
|
-
const root = resolve(cwd)
|
|
64
|
-
const target = resolve(root, baselinePath)
|
|
65
|
-
if (target !== root && !target.startsWith(`${root}${sep}`)) {
|
|
66
|
-
throw new Error("contract proof baseline must stay inside the project directory")
|
|
67
|
-
}
|
|
68
|
-
return target
|
|
69
|
-
}
|
|
70
|
-
|
|
71
60
|
function errorMessage(error: unknown): string {
|
|
72
61
|
return error instanceof Error ? error.message : String(error)
|
|
73
62
|
}
|
|
@@ -144,18 +133,7 @@ export async function collectContractProof(
|
|
|
144
133
|
options: ContractProofOptions = {},
|
|
145
134
|
): Promise<ContractProofReport> {
|
|
146
135
|
const baselinePath = options.baselinePath ?? DEFAULT_SNAPSHOT_FILE
|
|
147
|
-
const
|
|
148
|
-
if (!existsSync(resolvedBaseline)) {
|
|
149
|
-
throw new Error(`contract proof baseline not found: ${resolvedBaseline}`)
|
|
150
|
-
}
|
|
151
|
-
const realRoot = realpathSync(resolve(cwd))
|
|
152
|
-
const realBaseline = realpathSync(resolvedBaseline)
|
|
153
|
-
if (realBaseline !== realRoot && !realBaseline.startsWith(`${realRoot}${sep}`)) {
|
|
154
|
-
throw new Error("contract proof baseline must stay inside the project directory")
|
|
155
|
-
}
|
|
156
|
-
const baseline = parseSnapshotFile(await Bun.file(resolvedBaseline).text(), baselinePath)
|
|
157
|
-
const current = await snapshotBackend(cwd)
|
|
158
|
-
const diff = diffRouteSnapshots(baseline.routes, current)
|
|
136
|
+
const diff = await diffAgainstBaseline(cwd, baselinePath, { contain: true })
|
|
159
137
|
const verification = await collectProjectVerification(cwd, {
|
|
160
138
|
...(options.signal === undefined ? {} : { signal: options.signal }),
|
|
161
139
|
})
|