@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/port.ts
CHANGED
|
@@ -17,9 +17,11 @@
|
|
|
17
17
|
import { join } from "node:path"
|
|
18
18
|
import { Glob } from "bun"
|
|
19
19
|
import { stripComments } from "./check.ts"
|
|
20
|
+
import { readConfigTarget } from "./config-target.ts"
|
|
21
|
+
import { codeUnitOrder } from "./internal/code-unit-order.ts"
|
|
20
22
|
|
|
21
23
|
/** The five deploy targets a nifra `site` app can build for (mirrors create-nifra's DEPLOY presets). */
|
|
22
|
-
export const TARGETS = ["bun", "node", "deno", "
|
|
24
|
+
export const TARGETS = ["bun", "node", "deno", "cloudflare", "vercel"] as const
|
|
23
25
|
export type Target = (typeof TARGETS)[number]
|
|
24
26
|
|
|
25
27
|
const isTarget = (value: string): value is Target => (TARGETS as readonly string[]).includes(value)
|
|
@@ -56,14 +58,14 @@ const inMemoryVerdicts: Readonly<Record<Target, Verdict>> = {
|
|
|
56
58
|
bun: "caveat",
|
|
57
59
|
node: "caveat",
|
|
58
60
|
deno: "caveat",
|
|
59
|
-
|
|
61
|
+
cloudflare: "unsupported",
|
|
60
62
|
vercel: "unsupported",
|
|
61
63
|
}
|
|
62
64
|
const inMemoryReasons = (shared: string): Readonly<Record<Target, string>> => ({
|
|
63
65
|
bun: "ok single-instance; state is lost on restart and not shared across instances",
|
|
64
66
|
node: "ok single-instance; state is lost on restart and not shared across instances",
|
|
65
67
|
deno: "ok single-instance; state is lost on restart and not shared across instances",
|
|
66
|
-
|
|
68
|
+
cloudflare: shared,
|
|
67
69
|
vercel: shared,
|
|
68
70
|
})
|
|
69
71
|
|
|
@@ -105,14 +107,14 @@ export const FEATURES: Readonly<Record<FeatureId, FeatureSpec>> = {
|
|
|
105
107
|
bun: "ok",
|
|
106
108
|
node: "ok",
|
|
107
109
|
deno: "ok",
|
|
108
|
-
|
|
110
|
+
cloudflare: "caveat",
|
|
109
111
|
vercel: "unsupported",
|
|
110
112
|
},
|
|
111
113
|
reasons: {
|
|
112
114
|
bun: "long-lived process runs the scheduler loop",
|
|
113
115
|
node: "long-lived process runs the scheduler loop",
|
|
114
116
|
deno: "long-lived process runs the scheduler loop",
|
|
115
|
-
|
|
117
|
+
cloudflare:
|
|
116
118
|
"no long-lived loop on Workers - use the platform `scheduled` trigger via toFetchHandler(app, { scheduled }) and a wrangler [triggers] cron",
|
|
117
119
|
vercel: "no long-lived process - use a Vercel Cron Job hitting an endpoint",
|
|
118
120
|
},
|
|
@@ -124,14 +126,14 @@ export const FEATURES: Readonly<Record<FeatureId, FeatureSpec>> = {
|
|
|
124
126
|
bun: "ok",
|
|
125
127
|
node: "ok",
|
|
126
128
|
deno: "ok",
|
|
127
|
-
|
|
129
|
+
cloudflare: "caveat",
|
|
128
130
|
vercel: "unsupported",
|
|
129
131
|
},
|
|
130
132
|
reasons: {
|
|
131
133
|
bun: "all sockets live in one process; app.publish broadcasts to every client",
|
|
132
134
|
node: "all sockets live in one process; app.publish broadcasts to every client",
|
|
133
135
|
deno: "all sockets live in one process; app.publish broadcasts to every client",
|
|
134
|
-
|
|
136
|
+
cloudflare:
|
|
135
137
|
"a stateless fetch can't broadcast across isolates - wrap the app in createWebSocketHub (a Durable Object) from @nifrajs/workers",
|
|
136
138
|
vercel: "Vercel's serverless functions don't hold long-lived WebSocket connections",
|
|
137
139
|
},
|
|
@@ -143,14 +145,14 @@ export const FEATURES: Readonly<Record<FeatureId, FeatureSpec>> = {
|
|
|
143
145
|
bun: "ok",
|
|
144
146
|
node: "unsupported",
|
|
145
147
|
deno: "unsupported",
|
|
146
|
-
|
|
148
|
+
cloudflare: "unsupported",
|
|
147
149
|
vercel: "unsupported",
|
|
148
150
|
},
|
|
149
151
|
reasons: {
|
|
150
152
|
bun: "the Bun runtime provides the Bun global",
|
|
151
153
|
node: "no Bun global on Node - use the node: / Web equivalents",
|
|
152
154
|
deno: "no Bun global on Deno - use the Deno / Web equivalents",
|
|
153
|
-
|
|
155
|
+
cloudflare: "no Bun global in the Workers runtime",
|
|
154
156
|
vercel: "no Bun global in the Vercel runtime",
|
|
155
157
|
},
|
|
156
158
|
},
|
|
@@ -161,14 +163,14 @@ export const FEATURES: Readonly<Record<FeatureId, FeatureSpec>> = {
|
|
|
161
163
|
bun: "unsupported",
|
|
162
164
|
node: "unsupported",
|
|
163
165
|
deno: "ok",
|
|
164
|
-
|
|
166
|
+
cloudflare: "unsupported",
|
|
165
167
|
vercel: "unsupported",
|
|
166
168
|
},
|
|
167
169
|
reasons: {
|
|
168
170
|
bun: "no Deno global on Bun - use the Bun / Web equivalents",
|
|
169
171
|
node: "no Deno global on Node - use the node: / Web equivalents",
|
|
170
172
|
deno: "the Deno runtime provides the Deno global",
|
|
171
|
-
|
|
173
|
+
cloudflare: "no Deno global in the Workers runtime",
|
|
172
174
|
vercel: "no Deno global in the Vercel runtime",
|
|
173
175
|
},
|
|
174
176
|
},
|
|
@@ -179,14 +181,14 @@ export const FEATURES: Readonly<Record<FeatureId, FeatureSpec>> = {
|
|
|
179
181
|
bun: "ok",
|
|
180
182
|
node: "ok",
|
|
181
183
|
deno: "ok",
|
|
182
|
-
|
|
184
|
+
cloudflare: "caveat",
|
|
183
185
|
vercel: "caveat",
|
|
184
186
|
},
|
|
185
187
|
reasons: {
|
|
186
188
|
bun: "Bun implements the node: builtins",
|
|
187
189
|
node: "native node: builtins",
|
|
188
190
|
deno: "Deno implements the node: builtins",
|
|
189
|
-
|
|
191
|
+
cloudflare:
|
|
190
192
|
'Workers needs `compatibility_flags = ["nodejs_compat"]` in wrangler.toml (and not every builtin is covered)',
|
|
191
193
|
vercel: "needs the Node.js runtime (not the Edge runtime) so node: builtins resolve",
|
|
192
194
|
},
|
|
@@ -242,8 +244,9 @@ const WS_ROUTE = /\.ws\s*\(/g
|
|
|
242
244
|
const WS_HUB = /(?<![.\w$])createWebSocketHub\b/
|
|
243
245
|
// `Bun.` / `Deno.` runtime globals. `(?<![.\w$])` rejects `myBun.x`, `globalThis.Bun` stays a real use
|
|
244
246
|
// (we want it), and `obj.Bun.` won't match because the char before `Bun` would be `.`.
|
|
245
|
-
const BUN_GLOBAL = /(?<![.\w$])Bun\s*\./g
|
|
246
|
-
const DENO_GLOBAL = /(?<![.\w$])Deno\s*\./g
|
|
247
|
+
const BUN_GLOBAL = /(?<![.\w$])(?:globalThis\.)?Bun\s*\./g
|
|
248
|
+
const DENO_GLOBAL = /(?<![.\w$])(?:globalThis\.)?Deno\s*\./g
|
|
249
|
+
const BUN_MODULE_IMPORT = /(?:from|import)\s*\(?\s*["'`]bun(?:\/[^"'`]*)?["'`]/g
|
|
247
250
|
// `node:` builtin specifier in any import/require/export-from form.
|
|
248
251
|
const NODE_BUILTIN = /(?:from|import|require)\s*\(?\s*["'`](node:[A-Za-z0-9/_-]+)["'`]/g
|
|
249
252
|
|
|
@@ -298,8 +301,12 @@ export function scanFileForFeatures(file: string, content: string, state: ScanSt
|
|
|
298
301
|
// WS routes are collected unconditionally; the hub-presence post-pass decides whether they're a hazard.
|
|
299
302
|
pushHits(add("in-process-websocket"), file, content, code, WS_ROUTE)
|
|
300
303
|
pushHits(add("bun-runtime-api"), file, content, code, BUN_GLOBAL)
|
|
304
|
+
pushHits(add("bun-runtime-api"), file, content, code, BUN_MODULE_IMPORT)
|
|
301
305
|
pushHits(add("deno-runtime-api"), file, content, code, DENO_GLOBAL)
|
|
302
306
|
pushHits(add("node-builtin"), file, content, code, NODE_BUILTIN)
|
|
307
|
+
for (const evidence of state.evidence.values()) {
|
|
308
|
+
evidence.sort((a, b) => codeUnitOrder(a.file, b.file) || a.line - b.line)
|
|
309
|
+
}
|
|
303
310
|
}
|
|
304
311
|
|
|
305
312
|
// Mirror nifra check/doctor's ignore set: deps, build output, generated/per-runtime dist dirs, VCS.
|
|
@@ -323,7 +330,7 @@ async function walkPortSource(
|
|
|
323
330
|
}
|
|
324
331
|
|
|
325
332
|
const byEvidence = (a: Evidence, b: Evidence): number =>
|
|
326
|
-
a.file
|
|
333
|
+
codeUnitOrder(a.file, b.file) || a.line - b.line
|
|
327
334
|
|
|
328
335
|
/** Run every detector over the project and return the detected features (sorted, stable). */
|
|
329
336
|
export async function detectFeatures(cwd: string): Promise<DetectedFeature[]> {
|
|
@@ -353,12 +360,18 @@ export async function detectFeatures(cwd: string): Promise<DetectedFeature[]> {
|
|
|
353
360
|
export interface ResolvedTarget {
|
|
354
361
|
readonly target: Target
|
|
355
362
|
/** How the target was determined - surfaced so the report explains itself. */
|
|
356
|
-
readonly source:
|
|
363
|
+
readonly source:
|
|
364
|
+
| "flag"
|
|
365
|
+
| "config"
|
|
366
|
+
| "package-json-build"
|
|
367
|
+
| "package-json-deploy"
|
|
368
|
+
| "wrangler"
|
|
369
|
+
| "vercel"
|
|
357
370
|
}
|
|
358
371
|
|
|
359
372
|
const BUILD_SCRIPT_TARGET: ReadonlyArray<readonly [RegExp, Target]> = [
|
|
360
373
|
// create-nifra's canonical per-target build scripts (build-bun.ts / build-node.ts / build-deno.ts /
|
|
361
|
-
// build-vercel.ts), plus the bare `build.ts` the
|
|
374
|
+
// build-vercel.ts), plus the bare `build.ts` the cloudflare preset uses.
|
|
362
375
|
[/build-vercel\.[cm]?[jt]s\b/, "vercel"],
|
|
363
376
|
[/build-deno\.[cm]?[jt]s\b/, "deno"],
|
|
364
377
|
[/build-node\.[cm]?[jt]s\b/, "node"],
|
|
@@ -367,7 +380,7 @@ const BUILD_SCRIPT_TARGET: ReadonlyArray<readonly [RegExp, Target]> = [
|
|
|
367
380
|
const DEPLOY_SCRIPT_TARGET: ReadonlyArray<readonly [RegExp, Target]> = [
|
|
368
381
|
[/\bvercel\s+deploy\b/, "vercel"],
|
|
369
382
|
[/\bdeployctl\b/, "deno"],
|
|
370
|
-
[/wrangler\s+pages\s+deploy\b/, "
|
|
383
|
+
[/wrangler\s+pages\s+deploy\b/, "cloudflare"],
|
|
371
384
|
[/\bdocker\s+build\b/, "node"],
|
|
372
385
|
]
|
|
373
386
|
|
|
@@ -376,8 +389,9 @@ interface PackageJsonScripts {
|
|
|
376
389
|
}
|
|
377
390
|
|
|
378
391
|
/**
|
|
379
|
-
* Resolve the app's deploy target. `--target` (validated) always wins
|
|
380
|
-
* order, from: the canonical `build` script
|
|
392
|
+
* Resolve the app's deploy target. `--target` (validated) always wins, then the `target` that
|
|
393
|
+
* `nifra.config.ts` exports. Otherwise infer it, in priority order, from: the canonical `build` script
|
|
394
|
+
* (an older create-nifra's `build-<target>.ts`), the `deploy` script
|
|
381
395
|
* (the vendor CLI it shells out to), a `wrangler.toml` (Cloudflare Pages), or a `vercel.json`. Returns
|
|
382
396
|
* `undefined` when nothing signals a target (the matrix still prints; CI gating needs an explicit one).
|
|
383
397
|
*/
|
|
@@ -385,11 +399,18 @@ export async function resolveTarget(
|
|
|
385
399
|
cwd: string,
|
|
386
400
|
override?: string,
|
|
387
401
|
): Promise<ResolvedTarget | undefined> {
|
|
388
|
-
|
|
389
|
-
if (
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
402
|
+
const checked = (value: string, label: string): Target => {
|
|
403
|
+
if (isTarget(value)) return value
|
|
404
|
+
throw new Error(
|
|
405
|
+
value === "cf-pages"
|
|
406
|
+
? `[nifra] the ${label} "cf-pages" is now "cloudflare"`
|
|
407
|
+
: `[nifra] invalid ${label} "${value}". options: ${TARGETS.join(", ")}`,
|
|
408
|
+
)
|
|
409
|
+
}
|
|
410
|
+
if (override !== undefined) return { target: checked(override, "--target"), source: "flag" }
|
|
411
|
+
const configured = readConfigTarget(cwd)
|
|
412
|
+
if (configured !== undefined && configured !== "static") {
|
|
413
|
+
return { target: checked(configured, "target in nifra.config.ts"), source: "config" }
|
|
393
414
|
}
|
|
394
415
|
|
|
395
416
|
const pkg = await readJson<PackageJsonScripts>(join(cwd, "package.json"))
|
|
@@ -406,7 +427,7 @@ export async function resolveTarget(
|
|
|
406
427
|
}
|
|
407
428
|
|
|
408
429
|
if (await Bun.file(join(cwd, "wrangler.toml")).exists()) {
|
|
409
|
-
return { target: "
|
|
430
|
+
return { target: "cloudflare", source: "wrangler" }
|
|
410
431
|
}
|
|
411
432
|
if (
|
|
412
433
|
(await Bun.file(join(cwd, "vercel.json")).exists()) ||
|
|
@@ -513,7 +534,7 @@ export async function collectPortResult(
|
|
|
513
534
|
// --- Rendering ----------------------------------------------------------------------------------------
|
|
514
535
|
|
|
515
536
|
const CELL: Readonly<Record<Verdict, string>> = { ok: "✓", caveat: "⚠", unsupported: "✗" }
|
|
516
|
-
const COL_WIDTH =
|
|
537
|
+
const COL_WIDTH = 12 // widest target header ("cloudflare") + padding
|
|
517
538
|
|
|
518
539
|
const padCell = (s: string, width: number): string => {
|
|
519
540
|
// Pad accounting for the symbol's display width (✓/⚠/✗ render as 1 cell in a monospace terminal).
|
|
@@ -530,7 +551,7 @@ export function renderReport(result: PortResult, opts: { readonly strict?: boole
|
|
|
530
551
|
out.push(`target: ${resolved.target} (detected from ${describeSource(resolved.source)})`)
|
|
531
552
|
} else {
|
|
532
553
|
out.push(
|
|
533
|
-
"target: not detected - pass --target <bun|node|deno|
|
|
554
|
+
"target: not detected - pass --target <bun|node|deno|cloudflare|vercel> to gate against one",
|
|
534
555
|
)
|
|
535
556
|
}
|
|
536
557
|
out.push("")
|
|
@@ -610,6 +631,8 @@ function describeSource(source: ResolvedTarget["source"]): string {
|
|
|
610
631
|
switch (source) {
|
|
611
632
|
case "flag":
|
|
612
633
|
return "--target"
|
|
634
|
+
case "config":
|
|
635
|
+
return "nifra.config.ts"
|
|
613
636
|
case "package-json-build":
|
|
614
637
|
return "the package.json build script"
|
|
615
638
|
case "package-json-deploy":
|
|
@@ -655,7 +678,7 @@ export async function runPort(cwd: string, opts: RunPortOptions = {}): Promise<b
|
|
|
655
678
|
if (result.resolved === undefined) {
|
|
656
679
|
if (!opts.json) {
|
|
657
680
|
console.error(
|
|
658
|
-
"\n[nifra] --ci needs a deploy target to gate against, and none was detected. Pass --target <bun|node|deno|
|
|
681
|
+
"\n[nifra] --ci needs a deploy target to gate against, and none was detected. Pass --target <bun|node|deno|cloudflare|vercel>.",
|
|
659
682
|
)
|
|
660
683
|
}
|
|
661
684
|
return false
|
package/src/project-facts.ts
CHANGED
|
@@ -11,6 +11,7 @@ import type {
|
|
|
11
11
|
TransitiveServerImportFinding,
|
|
12
12
|
} from "./check-scan.ts"
|
|
13
13
|
import type { DoctorResult } from "./doctor.ts"
|
|
14
|
+
import type { TypeScriptSession } from "./internal/typescript-import.ts"
|
|
14
15
|
import type { PipelineReport } from "./pipeline-report.ts"
|
|
15
16
|
import type { RulePack, SourceIndex } from "./rules/index.ts"
|
|
16
17
|
|
|
@@ -47,6 +48,10 @@ export interface ContractCheckFacts {
|
|
|
47
48
|
export interface ProjectCheckFacts {
|
|
48
49
|
readonly typecheck: CheckTypecheckResult
|
|
49
50
|
readonly sqlCompilerAvailable: boolean
|
|
51
|
+
/** Set when a project TypeScript install is present but outside the scanner adapter range. */
|
|
52
|
+
readonly unsupportedTypeScriptVersion?: string
|
|
53
|
+
/** Shared syntax/checker session for built-in rules; omitted from serialized check output. */
|
|
54
|
+
readonly typescriptSession?: TypeScriptSession
|
|
50
55
|
readonly checkConfigError?: string
|
|
51
56
|
readonly checkConfigWarnings: readonly string[]
|
|
52
57
|
readonly contracts: ContractCheckFacts
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import type { UpgradeRecipe } from "./index.ts"
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Upgrade recipe for Nifra 4.0.
|
|
5
|
+
*
|
|
6
|
+
* The runner pins the fixed group and moves the retired `server-only` marker import. The layout move
|
|
7
|
+
* is `nifra migrate layout`'s job, so the notes lead with it, then cover what an app may observe.
|
|
8
|
+
*/
|
|
9
|
+
export const recipe: UpgradeRecipe = {
|
|
10
|
+
version: "4.0.0",
|
|
11
|
+
pins: [
|
|
12
|
+
{ match: "@nifrajs/", to: "4.0.0" },
|
|
13
|
+
{ match: "create-nifra", to: "4.0.0" },
|
|
14
|
+
{ match: "nifra", to: "4.0.0" },
|
|
15
|
+
],
|
|
16
|
+
importMoves: [{ from: "@nifrajs/web/server-only", to: "@nifrajs/web/backend-only" }],
|
|
17
|
+
notes: [
|
|
18
|
+
"Run `nifra migrate layout` (a dry run; `--write` applies it). An app's code now lives in `routes/`, `frontend/`, `backend/` and `shared/`: each route splits into `x.tsx` (the page) and `x.backend.ts` (`loader`, `action`, `middleware` and the other server exports), `_middleware.ts` folds into `_layout.backend.ts`, the root `backend.ts` and `framework.ts` move to `backend/app.ts` and `backend/framework.ts`, `*.server` modules move under `backend/`, and `ServerOnly` becomes `BackendOnly`. It lists what it could not decide.",
|
|
19
|
+
"Browser builds and dev servers refuse backend code, server packages and any first-party file in no zone, naming the import chain; `nifra check` reports the same (NF-C004, NF-C028) and private environment reads in browser code (NF-C029).",
|
|
20
|
+
"A loader or action that returns data needs `loaderOutput` / `actionOutput` in its backend half, or the request fails; the data is projected through the schema, so undeclared keys never reach the page. A deferred value is declared with `t.deferred(schema)`. `nifra check` reports a loader without one (NF-C030).",
|
|
21
|
+
"Builds fail on what looks like a credential in a client bundle, a `public/` file or a prerendered page; exempt a reviewed false positive with `secretExemptions` in `nifra.config.ts`.",
|
|
22
|
+
"The Cloudflare Pages target is `cloudflare` (`cf-pages` is refused), and `export const target` in `nifra.config.ts` sets what `nifra build` emits.",
|
|
23
|
+
'`withISR` no longer caches a page per query string by default: a request with any query parameter renders fresh and is not stored. Pass `query: ["page", ...]` to cache those parameters, or `query: "all"` to both `withISR` and `revalidateEndpoint` to keep the old keys.',
|
|
24
|
+
"A page file under the backend's API prefix or any other mount (`routes/api/*.tsx` with a backend) now fails at startup, at build and in `nifra check` (NF-C027), instead of answering 404. Move the page, change `apiPrefix` in `backend/framework.ts`, or serve that path from the backend.",
|
|
25
|
+
'Two page routes of one shape that share a path, such as `users/[id].tsx` beside `users/[slug].tsx`, fail dev, build and `nifra check` with "overlapping routes", because one could never be served. A static page beside a dynamic one (`users/me.tsx` beside `users/[id].tsx`) is fine: the more specific route serves the path they share. In a backend file, `nifra check` reports two same-method registrations that accept a shared path (NF-C024); make them disjoint (`/users/:id{[0-9]+}`) or mark the intended pair with a `// nifra-expect route-overlap` comment.',
|
|
26
|
+
"`nifra contracts check` (NF-K001) reports a changed contract for a route whose schema has a property named `description`, `title`, `default`, `example` or `examples`: earlier lock digests skipped such properties, so a change to one went unnoticed. Run `nifra contracts snapshot` once after upgrading and commit the lock.",
|
|
27
|
+
"Request methods match exactly: a request sent as `post` or `Patch` answers 405 or 404 instead of reaching the `POST`/`PATCH` route.",
|
|
28
|
+
"A request path's `.` and `..` segments (also `%2e` and backslashes) are resolved before routing on every runtime, so `/users/../posts` routes to `/posts` and `c.req.url` shows the resolved path.",
|
|
29
|
+
"A JSON body is parsed only when its media type is `application/json` or `application/*+json`; `text/plain; x=application/json` no longer parses, and server functions answer it with 415.",
|
|
30
|
+
"`revalidateEndpoint()` and `previewEndpoint()` throw at construction without a `secret`.",
|
|
31
|
+
'Rendering a hydrating page with an empty `clientEntry` throws instead of emitting `<script src="">`.',
|
|
32
|
+
'The typed client refuses a `.` or `..` path param value (`{ ok: false, status: 0, error: { error: "invalid_path" } }`) instead of sending a request to another path.',
|
|
33
|
+
"`jwt()`, `verifyCsrfToken()` and `verifyDownloadUrl()` accept only canonical unpadded base64url signatures.",
|
|
34
|
+
"In plural messages, `#` is formatted in the locale's number format (`1,000 items`), not as plain digits.",
|
|
35
|
+
"`solidBunPlugin` is exported from `@nifrajs/web-solid/plugin` and `svelteBunPlugin` from `@nifrajs/web-svelte/plugin`; the adapter roots export only the render adapter. Run `nifra fix --code NF-C005` to rewrite the imports.",
|
|
36
|
+
],
|
|
37
|
+
}
|
package/src/recipes/index.ts
CHANGED
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
import { recipe as v1_8_0 } from "./1.8.0.ts"
|
|
8
8
|
import { recipe as v2_0_0 } from "./2.0.0.ts"
|
|
9
9
|
import { recipe as v3_0_0 } from "./3.0.0.ts"
|
|
10
|
+
import { recipe as v4_0_0 } from "./4.0.0.ts"
|
|
10
11
|
|
|
11
12
|
/** A pin rule: every dependency whose name starts with `match` is set to version `to`. */
|
|
12
13
|
export interface PinRule {
|
|
@@ -42,6 +43,7 @@ const RECIPES: Record<string, UpgradeRecipe> = {
|
|
|
42
43
|
[v1_8_0.version]: v1_8_0,
|
|
43
44
|
[v2_0_0.version]: v2_0_0,
|
|
44
45
|
[v3_0_0.version]: v3_0_0,
|
|
46
|
+
[v4_0_0.version]: v4_0_0,
|
|
45
47
|
}
|
|
46
48
|
|
|
47
49
|
export function getRecipe(version: string): UpgradeRecipe | undefined {
|
|
@@ -16,7 +16,8 @@ import {
|
|
|
16
16
|
} from "./verification-plan.ts"
|
|
17
17
|
|
|
18
18
|
export type ReleaseVerificationMode = VerificationPlanMode
|
|
19
|
-
|
|
19
|
+
/** `undeclared`: the project's package.json has no script for the gate, so it was not run. */
|
|
20
|
+
export type ReleaseGateStatus = "pass" | "fail" | "skipped" | "undeclared"
|
|
20
21
|
|
|
21
22
|
export interface ReleaseGateResult {
|
|
22
23
|
readonly id: string
|
|
@@ -24,6 +25,8 @@ export interface ReleaseGateResult {
|
|
|
24
25
|
readonly commands: readonly string[]
|
|
25
26
|
readonly exitCode?: number
|
|
26
27
|
readonly message?: string
|
|
28
|
+
/** The last lines a failed gate's command printed, so a CI log shows why it failed. */
|
|
29
|
+
readonly output?: string
|
|
27
30
|
readonly remediation: string
|
|
28
31
|
}
|
|
29
32
|
|
|
@@ -33,6 +36,8 @@ export interface ReleaseVerificationResult {
|
|
|
33
36
|
readonly gates: readonly ReleaseGateResult[]
|
|
34
37
|
/** Release gates intentionally absent from the selected shorter plan. */
|
|
35
38
|
readonly omittedReleaseGateIds: readonly string[]
|
|
39
|
+
/** Gates a shared runner left out; a local verification runs them. */
|
|
40
|
+
readonly localGateIds: readonly string[]
|
|
36
41
|
}
|
|
37
42
|
|
|
38
43
|
export interface ReleaseCommandResult {
|
|
@@ -49,6 +54,11 @@ export interface ReleaseCommandSpec {
|
|
|
49
54
|
|
|
50
55
|
export interface ReleaseVerificationOptions {
|
|
51
56
|
readonly mode?: ReleaseVerificationMode
|
|
57
|
+
/**
|
|
58
|
+
* Leave out the gates the plan keeps off CI (`workflowRequired: false`): timing on a shared
|
|
59
|
+
* runner swings too far run to run to gate a release on.
|
|
60
|
+
*/
|
|
61
|
+
readonly sharedRunner?: boolean
|
|
52
62
|
readonly runCommand?: (spec: ReleaseCommandSpec) => Promise<ReleaseCommandResult>
|
|
53
63
|
}
|
|
54
64
|
|
|
@@ -67,6 +77,18 @@ const runBunCommand = async (spec: ReleaseCommandSpec): Promise<ReleaseCommandRe
|
|
|
67
77
|
return { exitCode, stdout, stderr }
|
|
68
78
|
}
|
|
69
79
|
|
|
80
|
+
const OUTPUT_TAIL_LINES = 40
|
|
81
|
+
|
|
82
|
+
/** The last lines a command printed, stdout before stderr, or undefined when it printed nothing. */
|
|
83
|
+
const outputTail = (result: ReleaseCommandResult): string | undefined => {
|
|
84
|
+
const printed = [result.stdout, result.stderr]
|
|
85
|
+
.map((text) => text?.trimEnd() ?? "")
|
|
86
|
+
.filter((text) => text !== "")
|
|
87
|
+
return printed.length === 0
|
|
88
|
+
? undefined
|
|
89
|
+
: printed.join("\n").split("\n").slice(-OUTPUT_TAIL_LINES).join("\n")
|
|
90
|
+
}
|
|
91
|
+
|
|
70
92
|
const parsePackage = (path: string): Record<string, unknown> | undefined => {
|
|
71
93
|
try {
|
|
72
94
|
const value: unknown = JSON.parse(readFileSync(path, "utf8"))
|
|
@@ -153,7 +175,7 @@ const runGate = async (
|
|
|
153
175
|
})
|
|
154
176
|
lastExitCode = result.exitCode
|
|
155
177
|
if (result.exitCode !== 0) {
|
|
156
|
-
|
|
178
|
+
const failed: Omit<ReleaseGateResult, "output"> & { output?: string } = {
|
|
157
179
|
id: gate.id,
|
|
158
180
|
status: "fail",
|
|
159
181
|
commands: gate.commands.map(commandLabel),
|
|
@@ -161,6 +183,9 @@ const runGate = async (
|
|
|
161
183
|
message: `${commandLabel(args)} exited with code ${result.exitCode}`,
|
|
162
184
|
remediation: gate.remediation,
|
|
163
185
|
}
|
|
186
|
+
const output = outputTail(result)
|
|
187
|
+
if (output !== undefined) failed.output = output
|
|
188
|
+
return failed
|
|
164
189
|
}
|
|
165
190
|
}
|
|
166
191
|
return {
|
|
@@ -180,6 +205,23 @@ const skippedGate = (gate: VerificationGateSpec, failedId: string): ReleaseGateR
|
|
|
180
205
|
remediation: `Fix the ${failedId} gate first, then rerun verification.`,
|
|
181
206
|
})
|
|
182
207
|
|
|
208
|
+
const undeclaredGate = (gate: VerificationGateSpec, script: string): ReleaseGateResult => ({
|
|
209
|
+
id: gate.id,
|
|
210
|
+
status: "undeclared",
|
|
211
|
+
commands: gate.commands.map(commandLabel),
|
|
212
|
+
message: `\`${script}\` is not a script in package.json`,
|
|
213
|
+
remediation: `Declare a \`${script}\` script to run this gate.`,
|
|
214
|
+
})
|
|
215
|
+
|
|
216
|
+
/** The first `bun run <script>` of a gate that the root package.json does not declare. */
|
|
217
|
+
const undeclaredScript = (
|
|
218
|
+
gate: VerificationGateSpec,
|
|
219
|
+
scripts: ReadonlySet<string>,
|
|
220
|
+
): string | undefined =>
|
|
221
|
+
gate.commands.find(
|
|
222
|
+
([verb, script]) => verb === "run" && script !== undefined && !scripts.has(script),
|
|
223
|
+
)?.[1]
|
|
224
|
+
|
|
183
225
|
/** Collect the same gate result rendered by the CLI and used by repository scripts. */
|
|
184
226
|
export async function collectReleaseVerification(
|
|
185
227
|
start: string,
|
|
@@ -189,14 +231,27 @@ export async function collectReleaseVerification(
|
|
|
189
231
|
const root = await resolveVerificationRoot(start)
|
|
190
232
|
const runCommand = options.runCommand ?? runBunCommand
|
|
191
233
|
const fixtureRoot = mkdtempSync(join(realpathSync(tmpdir()), "nifra-verify-"))
|
|
234
|
+
const declared = parsePackage(join(root, "package.json"))?.scripts
|
|
235
|
+
const scripts = new Set(
|
|
236
|
+
declared !== null && typeof declared === "object" ? Object.keys(declared) : [],
|
|
237
|
+
)
|
|
238
|
+
const plan = gatePlan(mode)
|
|
239
|
+
const local = options.sharedRunner === true ? plan.filter((gate) => !gate.workflowRequired) : []
|
|
192
240
|
const gates: ReleaseGateResult[] = []
|
|
193
241
|
try {
|
|
194
242
|
let failedId: string | undefined
|
|
195
|
-
for (const gate of
|
|
243
|
+
for (const gate of plan) {
|
|
244
|
+
if (local.includes(gate)) continue
|
|
196
245
|
if (failedId !== undefined) {
|
|
197
246
|
gates.push(skippedGate(gate, failedId))
|
|
198
247
|
continue
|
|
199
248
|
}
|
|
249
|
+
// The plan names the nifra repository's own scripts; a project without one has nothing to run.
|
|
250
|
+
const missing = undeclaredScript(gate, scripts)
|
|
251
|
+
if (missing !== undefined) {
|
|
252
|
+
gates.push(undeclaredGate(gate, missing))
|
|
253
|
+
continue
|
|
254
|
+
}
|
|
200
255
|
const result = await runGate(root, gate, runCommand, fixtureRoot)
|
|
201
256
|
gates.push(result)
|
|
202
257
|
if (result.status === "fail") failedId = result.id
|
|
@@ -205,10 +260,13 @@ export async function collectReleaseVerification(
|
|
|
205
260
|
rmSync(fixtureRoot, { recursive: true, force: true })
|
|
206
261
|
}
|
|
207
262
|
return {
|
|
208
|
-
ok:
|
|
263
|
+
ok:
|
|
264
|
+
gates.some((gate) => gate.status === "pass") &&
|
|
265
|
+
gates.every((gate) => gate.status === "pass" || gate.status === "undeclared"),
|
|
209
266
|
mode,
|
|
210
267
|
gates,
|
|
211
268
|
omittedReleaseGateIds: omittedVerificationGateIds(mode),
|
|
269
|
+
localGateIds: local.map((gate) => gate.id),
|
|
212
270
|
}
|
|
213
271
|
}
|
|
214
272
|
|
|
@@ -218,7 +276,10 @@ export function renderReleaseVerification(result: ReleaseVerificationResult): st
|
|
|
218
276
|
const marker = gate.status === "pass" ? "✓" : gate.status === "fail" ? "✗" : "-"
|
|
219
277
|
lines.push(`${marker} ${gate.id}: ${gate.status}`)
|
|
220
278
|
if (gate.message !== undefined) lines.push(` ${gate.message}`)
|
|
221
|
-
if (gate.
|
|
279
|
+
if (gate.output !== undefined)
|
|
280
|
+
lines.push(...gate.output.split("\n").map((line) => ` | ${line}`))
|
|
281
|
+
if (gate.status === "fail" || gate.status === "skipped")
|
|
282
|
+
lines.push(` fix: ${gate.remediation}`)
|
|
222
283
|
}
|
|
223
284
|
if (result.omittedReleaseGateIds.length > 0) {
|
|
224
285
|
lines.push(
|
|
@@ -227,6 +288,13 @@ export function renderReleaseVerification(result: ReleaseVerificationResult): st
|
|
|
227
288
|
" run `bun run check:release` for the full release gate.",
|
|
228
289
|
)
|
|
229
290
|
}
|
|
291
|
+
if (result.localGateIds.length > 0) {
|
|
292
|
+
lines.push(
|
|
293
|
+
"",
|
|
294
|
+
`note: a shared runner leaves out the timing gates: ${result.localGateIds.join(", ")}`,
|
|
295
|
+
" run `bun run check:release` locally for them.",
|
|
296
|
+
)
|
|
297
|
+
}
|
|
230
298
|
lines.push("", result.ok ? "✓ verification passed" : "✗ verification failed")
|
|
231
299
|
return lines.join("\n")
|
|
232
300
|
}
|
|
@@ -246,6 +314,7 @@ if (import.meta.main) {
|
|
|
246
314
|
const mode: ReleaseVerificationMode = process.argv.includes("--release") ? "release" : "default"
|
|
247
315
|
const ok = await runReleaseVerification(process.cwd(), {
|
|
248
316
|
mode,
|
|
317
|
+
sharedRunner: process.argv.includes("--shared-runner"),
|
|
249
318
|
json: process.argv.includes("--json"),
|
|
250
319
|
})
|
|
251
320
|
if (!ok) process.exitCode = 1
|
package/src/review.ts
CHANGED
|
@@ -615,23 +615,20 @@ async function composeCollectedReport(
|
|
|
615
615
|
const changedPaths = new Set(scope.changedPaths)
|
|
616
616
|
const retainedByCheck = new Map<ReviewCheckId, CandidateFinding[]>()
|
|
617
617
|
let totalOutOfScope = 0
|
|
618
|
+
let typeErrorOutOfScope = false
|
|
618
619
|
for (const id of CHECK_IDS) {
|
|
619
|
-
const source = candidatesByCheck.get(id) ?? []
|
|
620
620
|
const retained: CandidateFinding[] = []
|
|
621
|
-
|
|
622
|
-
for (const candidate of source) {
|
|
621
|
+
for (const candidate of candidatesByCheck.get(id) ?? []) {
|
|
623
622
|
if (
|
|
624
623
|
scope.kind === "diff" &&
|
|
625
624
|
candidate.location !== undefined &&
|
|
626
625
|
!changedPaths.has(candidate.location.path)
|
|
627
626
|
) {
|
|
628
|
-
outOfScope += 1
|
|
629
627
|
totalOutOfScope += 1
|
|
628
|
+
if (id === "typecheck" && candidate.severity === "error") typeErrorOutOfScope = true
|
|
630
629
|
} else retained.push(candidate)
|
|
631
630
|
}
|
|
632
631
|
retainedByCheck.set(id, retained)
|
|
633
|
-
// Store the per-check count temporarily on the scope map below through a side channel-free map.
|
|
634
|
-
void outOfScope
|
|
635
632
|
}
|
|
636
633
|
|
|
637
634
|
const checks: ReviewCheckResult[] = []
|
|
@@ -653,6 +650,11 @@ async function composeCollectedReport(
|
|
|
653
650
|
: checkResult.typecheckNote?.toLowerCase().includes("typescript")
|
|
654
651
|
? "missing-typescript"
|
|
655
652
|
: "unsupported"
|
|
653
|
+
} else if (id === "typecheck" && typeErrorOutOfScope) {
|
|
654
|
+
// A change can break a caller it never touched, so a type error outside the diff cannot be
|
|
655
|
+
// ruled out as this change's: the required check stays open instead of passing.
|
|
656
|
+
status = "unavailable"
|
|
657
|
+
reasonCode = "filtered-out-of-scope"
|
|
656
658
|
} else if (!configured) {
|
|
657
659
|
status = "skipped"
|
|
658
660
|
reasonCode = "not-configured"
|
|
@@ -671,7 +673,8 @@ async function composeCollectedReport(
|
|
|
671
673
|
id,
|
|
672
674
|
required: REQUIRED_CHECKS.has(id),
|
|
673
675
|
status,
|
|
674
|
-
duration:
|
|
676
|
+
duration:
|
|
677
|
+
status === "unavailable" && reasonCode !== "filtered-out-of-scope" ? "timeout" : "standard",
|
|
675
678
|
counts: countsFor(retained, outOfScope),
|
|
676
679
|
findingIds: retained.map((candidate) => candidate.finding.id),
|
|
677
680
|
evidence: checkEvidence,
|
|
@@ -942,7 +945,9 @@ export function renderReviewReport(report: ReviewReport): readonly string[] {
|
|
|
942
945
|
for (const check of report.checks) {
|
|
943
946
|
if (check.status === "skipped") lines.push(` skipped ${check.id}`)
|
|
944
947
|
if (check.status === "unavailable" || check.status === "error")
|
|
945
|
-
lines.push(
|
|
948
|
+
lines.push(
|
|
949
|
+
` unavailable ${check.id}${check.reasonCode === undefined ? "" : ` (${check.reasonCode})`}`,
|
|
950
|
+
)
|
|
946
951
|
}
|
|
947
952
|
return lines
|
|
948
953
|
}
|