@nifrajs/cli 3.5.0 → 4.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +42 -3
- package/dist/app-files.d.ts +11 -0
- package/dist/app-files.d.ts.map +1 -0
- package/dist/app-files.js +34 -0
- package/dist/app-files.js.map +1 -0
- package/dist/assure-hydration.d.ts +7 -1
- package/dist/assure-hydration.d.ts.map +1 -1
- package/dist/assure-hydration.js +128 -43
- package/dist/assure-hydration.js.map +1 -1
- package/dist/assure.d.ts.map +1 -1
- package/dist/assure.js +16 -2
- package/dist/assure.js.map +1 -1
- package/dist/capabilities-tool.d.ts +5 -1
- package/dist/capabilities-tool.d.ts.map +1 -1
- package/dist/capabilities-tool.js +32 -10
- package/dist/capabilities-tool.js.map +1 -1
- package/dist/cdn-check.d.ts +41 -0
- package/dist/cdn-check.d.ts.map +1 -0
- package/dist/cdn-check.js +207 -0
- package/dist/cdn-check.js.map +1 -0
- package/dist/check-diagnostics.d.ts +4 -0
- package/dist/check-diagnostics.d.ts.map +1 -1
- package/dist/check-diagnostics.js +13 -0
- package/dist/check-diagnostics.js.map +1 -1
- package/dist/check-scan.d.ts +49 -7
- package/dist/check-scan.d.ts.map +1 -1
- package/dist/check-scan.js +291 -62
- package/dist/check-scan.js.map +1 -1
- package/dist/check.d.ts.map +1 -1
- package/dist/check.js +99 -63
- package/dist/check.js.map +1 -1
- package/dist/cli.d.ts +42 -16
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +262 -82
- package/dist/cli.js.map +1 -1
- package/dist/command-catalog.d.ts +62 -5
- package/dist/command-catalog.d.ts.map +1 -1
- package/dist/command-catalog.js +275 -35
- package/dist/command-catalog.js.map +1 -1
- package/dist/config-target.d.ts +8 -0
- package/dist/config-target.d.ts.map +1 -0
- package/dist/config-target.js +45 -0
- package/dist/config-target.js.map +1 -0
- package/dist/contracts.d.ts.map +1 -1
- package/dist/contracts.js +52 -13
- package/dist/contracts.js.map +1 -1
- package/dist/db-audit.d.ts +37 -0
- package/dist/db-audit.d.ts.map +1 -0
- package/dist/db-audit.js +99 -0
- package/dist/db-audit.js.map +1 -0
- package/dist/db-child.d.ts +64 -0
- package/dist/db-child.d.ts.map +1 -0
- package/dist/db-child.js +213 -0
- package/dist/db-child.js.map +1 -0
- package/dist/db-config.d.ts +60 -0
- package/dist/db-config.d.ts.map +1 -0
- package/dist/db-config.js +176 -0
- package/dist/db-config.js.map +1 -0
- package/dist/db-tool.d.ts +84 -0
- package/dist/db-tool.d.ts.map +1 -0
- package/dist/db-tool.js +537 -0
- package/dist/db-tool.js.map +1 -0
- package/dist/dev-bun-config.d.ts +20 -13
- package/dist/dev-bun-config.d.ts.map +1 -1
- package/dist/dev-bun-config.js +98 -41
- package/dist/dev-bun-config.js.map +1 -1
- package/dist/dev-feed-tool.d.ts +106 -0
- package/dist/dev-feed-tool.d.ts.map +1 -0
- package/dist/dev-feed-tool.js +713 -0
- package/dist/dev-feed-tool.js.map +1 -0
- package/dist/dev-server-client.d.ts +67 -0
- package/dist/dev-server-client.d.ts.map +1 -0
- package/dist/dev-server-client.js +195 -0
- package/dist/dev-server-client.js.map +1 -0
- package/dist/diff-tool.d.ts +3 -3
- package/dist/diff-tool.d.ts.map +1 -1
- package/dist/diff-tool.js +6 -5
- package/dist/diff-tool.js.map +1 -1
- package/dist/docs-search.d.ts.map +1 -1
- package/dist/docs-search.js +12 -4
- package/dist/docs-search.js.map +1 -1
- package/dist/doctor.d.ts +17 -0
- package/dist/doctor.d.ts.map +1 -1
- package/dist/doctor.js +76 -15
- package/dist/doctor.js.map +1 -1
- package/dist/env-file.d.ts +15 -0
- package/dist/env-file.d.ts.map +1 -1
- package/dist/env-file.js +73 -1
- package/dist/env-file.js.map +1 -1
- package/dist/fix-recipes.d.ts.map +1 -1
- package/dist/fix-recipes.js +48 -22
- package/dist/fix-recipes.js.map +1 -1
- package/dist/frontend-guidance.js +5 -5
- package/dist/frontend-guidance.js.map +1 -1
- package/dist/i18n-check.d.ts +16 -0
- package/dist/i18n-check.d.ts.map +1 -0
- package/dist/i18n-check.js +154 -0
- package/dist/i18n-check.js.map +1 -0
- package/dist/init-agents.d.ts +49 -13
- package/dist/init-agents.d.ts.map +1 -1
- package/dist/init-agents.js +239 -34
- package/dist/init-agents.js.map +1 -1
- package/dist/internal/code-unit-order.d.ts +4 -0
- package/dist/internal/code-unit-order.d.ts.map +1 -0
- package/dist/internal/code-unit-order.js +6 -0
- package/dist/internal/code-unit-order.js.map +1 -0
- package/dist/internal/script-kind.d.ts +7 -0
- package/dist/internal/script-kind.d.ts.map +1 -0
- package/dist/internal/script-kind.js +12 -0
- package/dist/internal/script-kind.js.map +1 -0
- package/dist/internal/source-facts.d.ts +5 -0
- package/dist/internal/source-facts.d.ts.map +1 -1
- package/dist/internal/source-facts.js +61 -5
- package/dist/internal/source-facts.js.map +1 -1
- package/dist/internal/typescript-import.d.ts +49 -24
- package/dist/internal/typescript-import.d.ts.map +1 -1
- package/dist/internal/typescript-import.js +507 -67
- package/dist/internal/typescript-import.js.map +1 -1
- package/dist/introspect.d.ts +5 -1
- package/dist/introspect.d.ts.map +1 -1
- package/dist/introspect.js +92 -27
- package/dist/introspect.js.map +1 -1
- package/dist/learn.js +3 -3
- package/dist/learn.js.map +1 -1
- package/dist/levels-tool.d.ts +10 -0
- package/dist/levels-tool.d.ts.map +1 -1
- package/dist/levels-tool.js +33 -3
- package/dist/levels-tool.js.map +1 -1
- package/dist/load.d.ts +67 -17
- package/dist/load.d.ts.map +1 -1
- package/dist/load.js +78 -18
- package/dist/load.js.map +1 -1
- package/dist/manifest-tool.d.ts.map +1 -1
- package/dist/manifest-tool.js +3 -2
- package/dist/manifest-tool.js.map +1 -1
- package/dist/mcp-app-tool.d.ts.map +1 -1
- package/dist/mcp-app-tool.js +2 -0
- package/dist/mcp-app-tool.js.map +1 -1
- package/dist/mcp-context.js +1 -1
- package/dist/mcp-context.js.map +1 -1
- package/dist/mcp-delegate.d.ts +57 -0
- package/dist/mcp-delegate.d.ts.map +1 -0
- package/dist/mcp-delegate.js +130 -0
- package/dist/mcp-delegate.js.map +1 -0
- package/dist/mcp-docs-tools.d.ts.map +1 -1
- package/dist/mcp-docs-tools.js +23 -5
- package/dist/mcp-docs-tools.js.map +1 -1
- package/dist/mcp-exec.d.ts +2 -0
- package/dist/mcp-exec.d.ts.map +1 -1
- package/dist/mcp-exec.js +132 -108
- package/dist/mcp-exec.js.map +1 -1
- package/dist/mcp-http.d.ts.map +1 -1
- package/dist/mcp-http.js +17 -5
- package/dist/mcp-http.js.map +1 -1
- package/dist/mcp-io.d.ts +14 -0
- package/dist/mcp-io.d.ts.map +1 -1
- package/dist/mcp-io.js +45 -0
- package/dist/mcp-io.js.map +1 -1
- package/dist/mcp-isolate.d.ts +48 -0
- package/dist/mcp-isolate.d.ts.map +1 -0
- package/dist/mcp-isolate.js +222 -0
- package/dist/mcp-isolate.js.map +1 -0
- package/dist/mcp-project-child.d.ts +74 -0
- package/dist/mcp-project-child.d.ts.map +1 -0
- package/dist/mcp-project-child.js +130 -0
- package/dist/mcp-project-child.js.map +1 -0
- package/dist/mcp-render.js +2 -2
- package/dist/mcp-render.js.map +1 -1
- package/dist/mcp-root.d.ts +37 -9
- package/dist/mcp-root.d.ts.map +1 -1
- package/dist/mcp-root.js +110 -23
- package/dist/mcp-root.js.map +1 -1
- package/dist/mcp-run.d.ts +18 -1
- package/dist/mcp-run.d.ts.map +1 -1
- package/dist/mcp-run.js +82 -9
- package/dist/mcp-run.js.map +1 -1
- package/dist/mcp-ws.js +4 -1
- package/dist/mcp-ws.js.map +1 -1
- package/dist/mcp.d.ts +16 -3
- package/dist/mcp.d.ts.map +1 -1
- package/dist/mcp.js +81 -53
- package/dist/mcp.js.map +1 -1
- package/dist/migrate-layout.d.ts +44 -0
- package/dist/migrate-layout.d.ts.map +1 -0
- package/dist/migrate-layout.js +1155 -0
- package/dist/migrate-layout.js.map +1 -0
- package/dist/openapi-tool.d.ts +2 -2
- package/dist/openapi-tool.d.ts.map +1 -1
- package/dist/openapi-tool.js.map +1 -1
- package/dist/openapi-types.d.ts +2 -1
- package/dist/openapi-types.d.ts.map +1 -1
- package/dist/openapi-types.js +240 -12
- package/dist/openapi-types.js.map +1 -1
- package/dist/pipeline-report.d.ts +3 -2
- package/dist/pipeline-report.d.ts.map +1 -1
- package/dist/pipeline-report.js +19 -16
- package/dist/pipeline-report.js.map +1 -1
- package/dist/port.d.ts +5 -4
- package/dist/port.d.ts.map +1 -1
- package/dist/port.js +46 -29
- package/dist/port.js.map +1 -1
- package/dist/project-facts.d.ts +5 -0
- package/dist/project-facts.d.ts.map +1 -1
- package/dist/project-facts.js.map +1 -1
- package/dist/recipes/4.0.0.d.ts +9 -0
- package/dist/recipes/4.0.0.d.ts.map +1 -0
- package/dist/recipes/4.0.0.js +36 -0
- package/dist/recipes/4.0.0.js.map +1 -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 +11 -1
- package/dist/release-verification.d.ts.map +1 -1
- package/dist/release-verification.js +47 -4
- package/dist/release-verification.js.map +1 -1
- package/dist/review.d.ts.map +1 -1
- package/dist/review.js +12 -8
- package/dist/review.js.map +1 -1
- package/dist/route-types.d.ts +28 -0
- package/dist/route-types.d.ts.map +1 -0
- package/dist/route-types.js +106 -0
- package/dist/route-types.js.map +1 -0
- package/dist/rules/codes.d.ts +9 -0
- package/dist/rules/codes.d.ts.map +1 -1
- package/dist/rules/codes.js +9 -0
- package/dist/rules/codes.js.map +1 -1
- package/dist/rules/data-guard.d.ts +12 -0
- package/dist/rules/data-guard.d.ts.map +1 -0
- package/dist/rules/data-guard.js +213 -0
- package/dist/rules/data-guard.js.map +1 -0
- package/dist/rules/index.d.ts +3 -0
- package/dist/rules/index.d.ts.map +1 -1
- package/dist/rules/index.js +17 -5
- package/dist/rules/index.js.map +1 -1
- package/dist/rules/islands.d.ts.map +1 -1
- package/dist/rules/islands.js +7 -4
- package/dist/rules/islands.js.map +1 -1
- package/dist/rules/legacy.d.ts.map +1 -1
- package/dist/rules/legacy.js +71 -38
- package/dist/rules/legacy.js.map +1 -1
- package/dist/rules/nano.d.ts.map +1 -1
- package/dist/rules/nano.js +15 -6
- package/dist/rules/nano.js.map +1 -1
- package/dist/rules/pages.d.ts +15 -0
- package/dist/rules/pages.d.ts.map +1 -0
- package/dist/rules/pages.js +88 -0
- package/dist/rules/pages.js.map +1 -0
- package/dist/rules/routes.d.ts +11 -0
- package/dist/rules/routes.d.ts.map +1 -1
- package/dist/rules/routes.js +180 -2
- package/dist/rules/routes.js.map +1 -1
- package/dist/rules/secrets.d.ts +10 -0
- package/dist/rules/secrets.d.ts.map +1 -0
- package/dist/rules/secrets.js +169 -0
- package/dist/rules/secrets.js.map +1 -0
- package/dist/rules/security.d.ts +7 -0
- package/dist/rules/security.d.ts.map +1 -1
- package/dist/rules/security.js +171 -65
- package/dist/rules/security.js.map +1 -1
- package/dist/rules/typescript.d.ts +16 -0
- package/dist/rules/typescript.d.ts.map +1 -0
- package/dist/rules/typescript.js +59 -0
- package/dist/rules/typescript.js.map +1 -0
- package/dist/rules/zones.d.ts +17 -0
- package/dist/rules/zones.d.ts.map +1 -0
- package/dist/rules/zones.js +174 -0
- package/dist/rules/zones.js.map +1 -0
- package/dist/scaffold.d.ts +25 -12
- package/dist/scaffold.d.ts.map +1 -1
- package/dist/scaffold.js +132 -87
- package/dist/scaffold.js.map +1 -1
- package/dist/sdk.d.ts +17 -3
- package/dist/sdk.d.ts.map +1 -1
- package/dist/sdk.js +941 -153
- package/dist/sdk.js.map +1 -1
- package/dist/search-terms.d.ts +2 -0
- package/dist/search-terms.d.ts.map +1 -1
- package/dist/search-terms.js +23 -8
- package/dist/search-terms.js.map +1 -1
- package/dist/smoke.d.ts +79 -0
- package/dist/smoke.d.ts.map +1 -0
- package/dist/smoke.js +494 -0
- package/dist/smoke.js.map +1 -0
- package/dist/sync-manifest.d.ts +10 -2
- package/dist/sync-manifest.d.ts.map +1 -1
- package/dist/sync-manifest.js +44 -19
- package/dist/sync-manifest.js.map +1 -1
- package/dist/sync-routes.d.ts +4 -3
- package/dist/sync-routes.d.ts.map +1 -1
- package/dist/sync-routes.js +4 -3
- package/dist/sync-routes.js.map +1 -1
- package/dist/test-tool.d.ts.map +1 -1
- package/dist/test-tool.js +30 -11
- package/dist/test-tool.js.map +1 -1
- package/dist/types-search.d.ts.map +1 -1
- package/dist/types-search.js +11 -5
- package/dist/types-search.js.map +1 -1
- package/dist/upgrade.d.ts +36 -6
- package/dist/upgrade.d.ts.map +1 -1
- package/dist/upgrade.js +167 -29
- package/dist/upgrade.js.map +1 -1
- package/dist/verification-plan.d.ts.map +1 -1
- package/dist/verification-plan.js +4 -0
- package/dist/verification-plan.js.map +1 -1
- package/dist/verification.d.ts.map +1 -1
- package/dist/verification.js +45 -12
- package/dist/verification.js.map +1 -1
- package/dist/web-app-options.d.ts +19 -0
- package/dist/web-app-options.d.ts.map +1 -0
- package/dist/web-app-options.js +49 -0
- package/dist/web-app-options.js.map +1 -0
- package/dist/work-graph.d.ts.map +1 -1
- package/dist/work-graph.js +29 -13
- package/dist/work-graph.js.map +1 -1
- package/docs/examples.json +185 -29
- package/docs/llms-full.txt +3807 -580
- package/docs/types.json +14229 -10939
- package/package.json +19 -12
- package/src/app-files.ts +40 -0
- package/src/assure-hydration.ts +139 -46
- package/src/assure.ts +17 -2
- package/src/capabilities-tool.ts +35 -9
- package/src/cdn-check.ts +268 -0
- package/src/check-diagnostics.ts +17 -0
- package/src/check-scan.ts +358 -63
- package/src/check.ts +97 -62
- package/src/cli.ts +296 -87
- package/src/command-catalog.ts +389 -48
- package/src/config-target.ts +47 -0
- package/src/contracts.ts +54 -12
- package/src/db-audit.ts +128 -0
- package/src/db-child.ts +285 -0
- package/src/db-config.ts +224 -0
- package/src/db-tool.ts +690 -0
- package/src/dev-bun-config.ts +116 -40
- package/src/dev-feed-tool.ts +912 -0
- package/src/dev-server-client.ts +267 -0
- package/src/diff-tool.ts +6 -5
- package/src/docs-search.ts +10 -3
- package/src/doctor.ts +98 -15
- package/src/env-file.ts +78 -1
- package/src/fix-recipes.ts +53 -32
- package/src/frontend-guidance.ts +5 -5
- package/src/i18n-check.ts +179 -0
- package/src/init-agents.ts +301 -38
- package/src/internal/code-unit-order.ts +5 -0
- package/src/internal/script-kind.ts +11 -0
- package/src/internal/source-facts.ts +96 -22
- package/src/internal/typescript-import.ts +663 -68
- package/src/introspect.ts +90 -27
- package/src/learn.ts +3 -3
- package/src/levels-tool.ts +47 -2
- package/src/load.ts +140 -33
- package/src/manifest-tool.ts +3 -2
- package/src/mcp-app-tool.ts +2 -0
- package/src/mcp-context.ts +1 -1
- package/src/mcp-delegate.ts +162 -0
- package/src/mcp-docs-tools.ts +23 -5
- package/src/mcp-exec.ts +160 -132
- package/src/mcp-http.ts +19 -5
- package/src/mcp-io.ts +57 -0
- package/src/mcp-isolate.ts +287 -0
- package/src/mcp-project-child.ts +182 -0
- package/src/mcp-render.ts +2 -2
- package/src/mcp-root.ts +117 -22
- package/src/mcp-run.ts +104 -10
- package/src/mcp-ws.ts +4 -1
- package/src/mcp.ts +106 -64
- package/src/migrate-layout.ts +1338 -0
- package/src/openapi-tool.ts +7 -3
- package/src/openapi-types.ts +315 -13
- package/src/pipeline-report.ts +22 -18
- package/src/port.ts +53 -30
- package/src/project-facts.ts +5 -0
- package/src/recipes/4.0.0.ts +37 -0
- package/src/recipes/index.ts +2 -0
- package/src/release-verification.ts +74 -5
- package/src/review.ts +13 -8
- package/src/route-types.ts +133 -0
- package/src/rules/codes.ts +9 -0
- package/src/rules/data-guard.ts +230 -0
- package/src/rules/index.ts +18 -5
- package/src/rules/islands.ts +12 -4
- package/src/rules/legacy.ts +71 -39
- package/src/rules/nano.ts +18 -6
- package/src/rules/pages.ts +99 -0
- package/src/rules/routes.ts +181 -2
- package/src/rules/secrets.ts +174 -0
- package/src/rules/security.ts +198 -70
- package/src/rules/typescript.ts +77 -0
- package/src/rules/zones.ts +195 -0
- package/src/scaffold.ts +147 -95
- package/src/sdk.ts +1215 -158
- package/src/search-terms.ts +24 -7
- package/src/smoke.ts +607 -0
- package/src/sync-manifest.ts +50 -19
- package/src/sync-routes.ts +4 -3
- package/src/test-tool.ts +32 -11
- package/src/types-search.ts +11 -4
- package/src/upgrade.ts +205 -25
- package/src/verification-plan.ts +13 -0
- package/src/verification.ts +50 -12
- package/src/web-app-options.ts +57 -0
- package/src/work-graph.ts +27 -13
package/src/fix-recipes.ts
CHANGED
|
@@ -31,35 +31,27 @@ registerFixRecipe({
|
|
|
31
31
|
description: "Replace a direct secret comparison with a length check and timing-safe comparison.",
|
|
32
32
|
verify: "nifra check --lints-only",
|
|
33
33
|
async apply(root, diagnostic) {
|
|
34
|
-
if (diagnostic.file === undefined
|
|
34
|
+
if (diagnostic.file === undefined) return []
|
|
35
35
|
const path = await resolveInsideProject(root, diagnostic.file)
|
|
36
36
|
if (path === undefined) return []
|
|
37
|
-
const
|
|
38
|
-
const
|
|
39
|
-
|
|
40
|
-
if (
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
const left = comparison[1] as string
|
|
44
|
-
const operator = comparison[2] as string
|
|
45
|
-
const right = comparison[3] as string
|
|
46
|
-
const expression = `nifraTimingSafeEqual(${left}, ${right})`
|
|
47
|
-
const replacement = operator === "!==" || operator === "!=" ? `!${expression}` : expression
|
|
48
|
-
lines[index] = original.replace(comparison[0], replacement)
|
|
49
|
-
if (!lines.some((line) => line.includes("function nifraTimingSafeEqual"))) {
|
|
50
|
-
lines.unshift(
|
|
51
|
-
'import { timingSafeEqual } from "node:crypto"',
|
|
52
|
-
"",
|
|
53
|
-
"function nifraTimingSafeEqual(left: string, right: string): boolean {",
|
|
54
|
-
" const leftBytes = Buffer.from(left)",
|
|
55
|
-
" const rightBytes = Buffer.from(right)",
|
|
56
|
-
" return leftBytes.length === rightBytes.length && timingSafeEqual(leftBytes, rightBytes)",
|
|
57
|
-
"}",
|
|
58
|
-
"",
|
|
37
|
+
const { loadProjectTypeScript } = await import("./internal/typescript-import.ts")
|
|
38
|
+
const loaded = await loadProjectTypeScript(root)
|
|
39
|
+
if (loaded.unsupported !== undefined) throw loaded.unsupported
|
|
40
|
+
if (loaded.compiler === undefined)
|
|
41
|
+
throw new Error(
|
|
42
|
+
"[nifra] the timing-safe fix finds each comparison in the syntax tree, so it needs TypeScript - run `bun add -d typescript`, then rerun the fix",
|
|
59
43
|
)
|
|
44
|
+
try {
|
|
45
|
+
const { rewriteSecretComparisons } = await import("./rules/security.ts")
|
|
46
|
+
const source = await readFile(path, "utf8")
|
|
47
|
+
// Every site in the file at once, so the rest of that file's diagnostics find nothing left.
|
|
48
|
+
const rewritten = rewriteSecretComparisons(loaded.compiler, diagnostic.file, source)
|
|
49
|
+
if (rewritten === source) return []
|
|
50
|
+
await writeFile(path, rewritten, "utf8")
|
|
51
|
+
return [diagnostic.file]
|
|
52
|
+
} finally {
|
|
53
|
+
await loaded.session?.close()
|
|
60
54
|
}
|
|
61
|
-
await writeFile(path, lines.join("\n"), "utf8")
|
|
62
|
-
return [diagnostic.file]
|
|
63
55
|
},
|
|
64
56
|
})
|
|
65
57
|
|
|
@@ -70,7 +62,15 @@ registerFixRecipe({
|
|
|
70
62
|
async apply(root) {
|
|
71
63
|
const { syncServerManifests } = await import("./sync-manifest.ts")
|
|
72
64
|
const results = await syncServerManifests(root)
|
|
73
|
-
|
|
65
|
+
const changed = results.filter((result) => result.changed).map((result) => result.file)
|
|
66
|
+
const failed = results.filter((result) => result.error !== undefined)
|
|
67
|
+
if (failed.length > 0) {
|
|
68
|
+
const synced = changed.length === 0 ? "" : `synced ${changed.join(", ")}; `
|
|
69
|
+
throw new Error(
|
|
70
|
+
`[nifra] ${synced}cannot sync ${failed.map((result) => `${result.file}: ${result.error}`).join("; ")}`,
|
|
71
|
+
)
|
|
72
|
+
}
|
|
73
|
+
return changed
|
|
74
74
|
},
|
|
75
75
|
})
|
|
76
76
|
|
|
@@ -101,7 +101,7 @@ registerFixRecipe({
|
|
|
101
101
|
// Refusing is a result the caller has to see. Returning "changed nothing" made `nifra fix
|
|
102
102
|
// --code NF-C010` a silent no-op on the only findings that produce it.
|
|
103
103
|
if (!linked.ok) throw new Error(`[nifra] cannot rebuild ${packageName}: ${linked.reason}`)
|
|
104
|
-
const proc = Bun.spawn([
|
|
104
|
+
const proc = Bun.spawn([process.execPath, "run", linked.buildScript], {
|
|
105
105
|
cwd: linked.dir,
|
|
106
106
|
stdout: "ignore",
|
|
107
107
|
stderr: "pipe",
|
|
@@ -117,6 +117,24 @@ registerFixRecipe({
|
|
|
117
117
|
},
|
|
118
118
|
})
|
|
119
119
|
|
|
120
|
+
registerFixRecipe({
|
|
121
|
+
id: "imports.moved-export",
|
|
122
|
+
description: "Import a name that moved to a subpath from the module that now exports it.",
|
|
123
|
+
verify: "nifra check --lints-only",
|
|
124
|
+
async apply(root, diagnostic) {
|
|
125
|
+
if (diagnostic.file === undefined) return []
|
|
126
|
+
const path = await resolveInsideProject(root, diagnostic.file)
|
|
127
|
+
if (path === undefined) return []
|
|
128
|
+
const { rewriteMovedExports } = await import("./check-scan.ts")
|
|
129
|
+
const source = await readFile(path, "utf8")
|
|
130
|
+
// Rewrites every site in the file at once, so the rest of that file's diagnostics find nothing left.
|
|
131
|
+
const rewritten = rewriteMovedExports(source)
|
|
132
|
+
if (rewritten === source) return []
|
|
133
|
+
await writeFile(path, rewritten, "utf8")
|
|
134
|
+
return [diagnostic.file]
|
|
135
|
+
},
|
|
136
|
+
})
|
|
137
|
+
|
|
120
138
|
/**
|
|
121
139
|
* How many typecheck passes the reserved-segment codemod will run. A nested collision
|
|
122
140
|
* (`/api/delete/then`) only reveals its second site once the first is rewritten, so one pass is not
|
|
@@ -147,11 +165,14 @@ registerFixRecipe({
|
|
|
147
165
|
const changed = new Set<string>()
|
|
148
166
|
const skipped: string[] = []
|
|
149
167
|
for (let pass = 0; pass < RESERVED_SEGMENT_PASSES; pass += 1) {
|
|
150
|
-
const proc = Bun.spawn(
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
168
|
+
const proc = Bun.spawn(
|
|
169
|
+
[process.execPath, tscBin, "--noEmit", "--pretty", "false", "-p", tsconfig],
|
|
170
|
+
{
|
|
171
|
+
cwd: root,
|
|
172
|
+
stdout: "pipe",
|
|
173
|
+
stderr: "pipe",
|
|
174
|
+
},
|
|
175
|
+
)
|
|
155
176
|
const [stdout, stderr] = await Promise.all([
|
|
156
177
|
new Response(proc.stdout).text(),
|
|
157
178
|
new Response(proc.stderr).text(),
|
package/src/frontend-guidance.ts
CHANGED
|
@@ -52,11 +52,11 @@ export const FRONTEND_GUIDANCE: readonly GuidanceEntry[] = [
|
|
|
52
52
|
symptom:
|
|
53
53
|
"A client component crashes at build or runtime with a missing Node built-in, a leaked secret, or 'module not found in the browser'.",
|
|
54
54
|
cause:
|
|
55
|
-
"
|
|
56
|
-
fix: "Move the access into
|
|
55
|
+
"Browser code (a page, `frontend/` or `shared/`) imports backend code - `backend/`, a route's `.backend.ts` half, the DB, a secret, `node:*` - so the browser build refuses it.",
|
|
56
|
+
fix: "Move the access into the route's `.backend.ts` half (`loader`/`action` with `loaderOutput`/`actionOutput`) and read the result as `data` in the page. Reach the backend through the typed `api` argument; put code both sides need in `shared/`.",
|
|
57
57
|
verify:
|
|
58
58
|
"nifra_check - its transitive server-import scan flags the exact import chain into a client module.",
|
|
59
|
-
seeAlso: "
|
|
59
|
+
seeAlso: "project structure route backend half import rules",
|
|
60
60
|
},
|
|
61
61
|
{
|
|
62
62
|
id: "hydration-mismatch",
|
|
@@ -88,10 +88,10 @@ export const FRONTEND_GUIDANCE: readonly GuidanceEntry[] = [
|
|
|
88
88
|
"Loader data is typed `any` in the page, or a hand-written interface silently drifts from what the loader actually returns.",
|
|
89
89
|
cause:
|
|
90
90
|
"The component types its props by hand instead of inferring them from the loader, so a change to the loader is not a type error at the use site.",
|
|
91
|
-
fix:
|
|
91
|
+
fix: 'Type the page from its generated route types: `import type { Route } from "./+types/<name>"` and `props: Route.ComponentProps`. Its `data` is the `loaderOutput` schema\'s type - what actually reaches the browser - while `LoaderData<typeof loader>` also types fields the schema strips. Call the backend through the typed `api` (`Route.LoaderArgs`) so the request/response shape is inferred, not restated.',
|
|
92
92
|
verify:
|
|
93
93
|
"nifra_context prints the exact loader shape and typed call form; nifra_check fails on drift.",
|
|
94
|
-
seeAlso: "
|
|
94
|
+
seeAlso: "route types Route.ComponentProps loaderOutput typed client",
|
|
95
95
|
},
|
|
96
96
|
{
|
|
97
97
|
id: "list-key",
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `nifra i18n check [entry]` - runs `checkCatalogs()` from `@nifrajs/i18n/check` over an app's locale
|
|
3
|
+
* registry and catalogs. Unlike `nifra check`, this IMPORTS app code: the entry module, which exports
|
|
4
|
+
* `locales` (from `defineLocales`) and `catalogs` (by locale key: a catalog, or a function returning
|
|
5
|
+
* one or a promise of one, such as `() => import("./messages/fr.json")`), plus an optional `ignore`.
|
|
6
|
+
* The default entry is the first of {@link DEFAULT_ENTRIES} that exists.
|
|
7
|
+
*/
|
|
8
|
+
import { existsSync } from "node:fs"
|
|
9
|
+
import { relative, resolve } from "node:path"
|
|
10
|
+
import { pathToFileURL } from "node:url"
|
|
11
|
+
import type { MessageTree } from "@nifrajs/i18n"
|
|
12
|
+
import type { CatalogCheckOptions, CatalogCheckResult, CatalogFinding } from "@nifrajs/i18n/check"
|
|
13
|
+
|
|
14
|
+
export const DEFAULT_ENTRIES = [
|
|
15
|
+
"shared/i18n.ts",
|
|
16
|
+
"i18n.ts",
|
|
17
|
+
"lib/i18n.ts",
|
|
18
|
+
"src/i18n.ts",
|
|
19
|
+
"src/lib/i18n.ts",
|
|
20
|
+
"app/i18n.ts",
|
|
21
|
+
] as const
|
|
22
|
+
|
|
23
|
+
export interface I18nCheckOutput {
|
|
24
|
+
/** The entry module, relative to the project directory. */
|
|
25
|
+
readonly entry: string
|
|
26
|
+
readonly result: CatalogCheckResult
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const isObject = (value: unknown): value is Record<string, unknown> =>
|
|
30
|
+
value !== null && typeof value === "object"
|
|
31
|
+
|
|
32
|
+
/** A loaded module's default export when the value is a module namespace (`import("./fr.json")`). */
|
|
33
|
+
const unwrapModule = (value: unknown): unknown =>
|
|
34
|
+
isObject(value) && Object.prototype.toString.call(value) === "[object Module]"
|
|
35
|
+
? value.default
|
|
36
|
+
: value
|
|
37
|
+
|
|
38
|
+
const isRegistry = (value: unknown): value is CatalogCheckOptions["locales"] =>
|
|
39
|
+
isObject(value) &&
|
|
40
|
+
typeof value.default === "string" &&
|
|
41
|
+
Array.isArray(value.all) &&
|
|
42
|
+
typeof value.get === "function" &&
|
|
43
|
+
typeof value.chain === "function"
|
|
44
|
+
|
|
45
|
+
function resolveEntry(cwd: string, entry: string | undefined): string {
|
|
46
|
+
if (entry !== undefined) {
|
|
47
|
+
const path = resolve(cwd, entry)
|
|
48
|
+
if (!existsSync(path)) throw new Error(`i18n check: ${entry} does not exist`)
|
|
49
|
+
return path
|
|
50
|
+
}
|
|
51
|
+
for (const candidate of DEFAULT_ENTRIES) {
|
|
52
|
+
const path = resolve(cwd, candidate)
|
|
53
|
+
if (existsSync(path)) return path
|
|
54
|
+
}
|
|
55
|
+
throw new Error(
|
|
56
|
+
`i18n check: no entry module found (looked for ${DEFAULT_ENTRIES.join(", ")}). Pass one: nifra i18n check <entry>, a module exporting \`locales\` and \`catalogs\`.`,
|
|
57
|
+
)
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** Import the entry, load every catalog it names, and check them. */
|
|
61
|
+
export async function runI18nCheck(
|
|
62
|
+
cwd: string,
|
|
63
|
+
options: { readonly entry?: string | undefined } = {},
|
|
64
|
+
): Promise<I18nCheckOutput> {
|
|
65
|
+
const path = resolveEntry(cwd, options.entry)
|
|
66
|
+
const mod = (await import(pathToFileURL(path).href)) as Record<string, unknown>
|
|
67
|
+
const source = isObject(mod.default) && mod.locales === undefined ? mod.default : mod
|
|
68
|
+
const { locales, catalogs, ignore } = source
|
|
69
|
+
const entry = relative(cwd, path).replaceAll("\\", "/") || path
|
|
70
|
+
if (!isRegistry(locales)) {
|
|
71
|
+
throw new Error(
|
|
72
|
+
`i18n check: ${entry} must export \`locales\`, the registry defineLocales() returns`,
|
|
73
|
+
)
|
|
74
|
+
}
|
|
75
|
+
if (!isObject(catalogs)) {
|
|
76
|
+
throw new Error(
|
|
77
|
+
`i18n check: ${entry} must export \`catalogs\`, an object of catalogs by locale key`,
|
|
78
|
+
)
|
|
79
|
+
}
|
|
80
|
+
if (ignore !== undefined && !isObject(ignore)) {
|
|
81
|
+
throw new Error(`i18n check: \`ignore\` in ${entry} must be an object of key patterns by check`)
|
|
82
|
+
}
|
|
83
|
+
const loaded: Record<string, MessageTree | undefined> = {}
|
|
84
|
+
for (const key of Object.keys(catalogs)) {
|
|
85
|
+
let value = catalogs[key]
|
|
86
|
+
if (typeof value === "function") value = (value as () => unknown)()
|
|
87
|
+
loaded[key] = unwrapModule(await value) as MessageTree | undefined
|
|
88
|
+
}
|
|
89
|
+
// Loaded on use: the command catalog imports this module on every CLI start.
|
|
90
|
+
const { checkCatalogs } = await import("@nifrajs/i18n/check")
|
|
91
|
+
return {
|
|
92
|
+
entry,
|
|
93
|
+
result: checkCatalogs({
|
|
94
|
+
locales,
|
|
95
|
+
catalogs: loaded,
|
|
96
|
+
ignore: ignore as CatalogCheckOptions["ignore"],
|
|
97
|
+
}),
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const MARK = { error: "✖", warning: "⚠", info: "•" } as const
|
|
102
|
+
// Per-key findings that are lists rather than problems one by one: grouped per locale in the text.
|
|
103
|
+
const GROUPED = new Set(["missing-key", "unused-key", "untranslated"])
|
|
104
|
+
const SHOWN_KEYS = 6
|
|
105
|
+
|
|
106
|
+
// Catalog keys and text reach the terminal: control and bidi-override characters print as escapes, so
|
|
107
|
+
// a key can neither drive the terminal nor reorder what the line appears to say.
|
|
108
|
+
const unprintable = (code: number): boolean =>
|
|
109
|
+
code <= 0x1f ||
|
|
110
|
+
(code >= 0x7f && code <= 0x9f) ||
|
|
111
|
+
code === 0x200e ||
|
|
112
|
+
code === 0x200f ||
|
|
113
|
+
(code >= 0x202a && code <= 0x202e) ||
|
|
114
|
+
(code >= 0x2066 && code <= 0x2069)
|
|
115
|
+
const printable = (line: string): string => {
|
|
116
|
+
let out = ""
|
|
117
|
+
for (const char of line) {
|
|
118
|
+
const code = char.codePointAt(0) ?? 0
|
|
119
|
+
out += unprintable(code) ? `\\u${code.toString(16).padStart(4, "0")}` : char
|
|
120
|
+
}
|
|
121
|
+
return out
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
const percent = (share: number): string => `${(share * 100).toFixed(share === 1 ? 0 : 1)}%`
|
|
125
|
+
|
|
126
|
+
/** The human report: a coverage table, then findings, grouped where they are key lists. */
|
|
127
|
+
export function renderI18nCheck(out: I18nCheckOutput): readonly string[] {
|
|
128
|
+
const { result } = out
|
|
129
|
+
const lines = [`nifra i18n check - ${out.entry}`, ""]
|
|
130
|
+
const width = Math.max(6, ...result.coverage.map((row) => row.locale.length))
|
|
131
|
+
for (const row of result.coverage) {
|
|
132
|
+
const label = row.default ? "default" : row.draft ? "draft" : ""
|
|
133
|
+
const counts = row.default
|
|
134
|
+
? `${row.total} messages`
|
|
135
|
+
: `${row.translated}/${row.total} (${percent(row.coverage)})${row.own === row.translated ? "" : `, ${row.own} own`}`
|
|
136
|
+
lines.push(` ${row.locale.padEnd(width)} ${label.padEnd(7)} ${counts}`)
|
|
137
|
+
}
|
|
138
|
+
if (result.coverage.length > 0) lines.push("")
|
|
139
|
+
|
|
140
|
+
const groups = new Map<string, CatalogFinding[]>()
|
|
141
|
+
for (const finding of result.findings) {
|
|
142
|
+
if (!GROUPED.has(finding.code) || finding.key === undefined) {
|
|
143
|
+
lines.push(`${MARK[finding.severity]} ${finding.locale} ${finding.code}: ${finding.message}`)
|
|
144
|
+
continue
|
|
145
|
+
}
|
|
146
|
+
const id = `${finding.severity}\u0000${finding.locale}\u0000${finding.code}`
|
|
147
|
+
const group = groups.get(id)
|
|
148
|
+
if (group === undefined) groups.set(id, [finding])
|
|
149
|
+
else group.push(finding)
|
|
150
|
+
}
|
|
151
|
+
for (const group of groups.values()) {
|
|
152
|
+
const first = group[0] as CatalogFinding
|
|
153
|
+
const keys = group.map((finding) => finding.key as string)
|
|
154
|
+
const shown = keys.slice(0, SHOWN_KEYS).join(", ")
|
|
155
|
+
const more = keys.length > SHOWN_KEYS ? `, +${keys.length - SHOWN_KEYS} more` : ""
|
|
156
|
+
lines.push(
|
|
157
|
+
`${MARK[first.severity]} ${first.locale} ${first.code} (${keys.length}): ${shown}${more}`,
|
|
158
|
+
)
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
const count = (severity: CatalogFinding["severity"]): number =>
|
|
162
|
+
result.findings.filter((finding) => finding.severity === severity).length
|
|
163
|
+
const errors = count("error")
|
|
164
|
+
const warnings = count("warning")
|
|
165
|
+
if (result.findings.length > 0) lines.push("")
|
|
166
|
+
lines.push(
|
|
167
|
+
errors === 0 && warnings === 0
|
|
168
|
+
? "✓ catalogs are complete and consistent"
|
|
169
|
+
: `${errors} error${errors === 1 ? "" : "s"}, ${warnings} warning${warnings === 1 ? "" : "s"}${errors === 0 ? " - pass --strict to fail on warnings" : ""}`,
|
|
170
|
+
)
|
|
171
|
+
return lines.map(printable)
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/** Errors fail the run; with `strict`, warnings do too. Info never does. */
|
|
175
|
+
export function i18nCheckPassed(out: I18nCheckOutput, strict: boolean): boolean {
|
|
176
|
+
return out.result.findings.every(
|
|
177
|
+
(finding) => finding.severity === "info" || (finding.severity === "warning" && !strict),
|
|
178
|
+
)
|
|
179
|
+
}
|