@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/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nifrajs/cli",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "4.0.2",
|
|
4
4
|
"description": "The nifra CLI - zero-config dev/build/start for a nifra app (`nifra dev`, `nifra build`, `nifra start`).",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"bun",
|
|
@@ -17,6 +17,9 @@
|
|
|
17
17
|
"dev-server"
|
|
18
18
|
],
|
|
19
19
|
"type": "module",
|
|
20
|
+
"nifra": {
|
|
21
|
+
"environment": "backend"
|
|
22
|
+
},
|
|
20
23
|
"bin": {
|
|
21
24
|
"nifra": "./dist/cli.js"
|
|
22
25
|
},
|
|
@@ -66,15 +69,17 @@
|
|
|
66
69
|
"access": "public"
|
|
67
70
|
},
|
|
68
71
|
"dependencies": {
|
|
69
|
-
"@nifrajs/client": "
|
|
70
|
-
"@nifrajs/core": "
|
|
71
|
-
"@nifrajs/agent-review": "
|
|
72
|
-
"@nifrajs/
|
|
73
|
-
"@nifrajs/
|
|
74
|
-
"@nifrajs/
|
|
75
|
-
"@nifrajs/
|
|
76
|
-
"@nifrajs/
|
|
77
|
-
"
|
|
72
|
+
"@nifrajs/client": "4.0.2",
|
|
73
|
+
"@nifrajs/core": "4.0.2",
|
|
74
|
+
"@nifrajs/agent-review": "4.0.2",
|
|
75
|
+
"@nifrajs/i18n": "4.0.2",
|
|
76
|
+
"@nifrajs/mcp": "4.0.2",
|
|
77
|
+
"@nifrajs/mcp-db": "4.0.2",
|
|
78
|
+
"@nifrajs/runner": "4.0.2",
|
|
79
|
+
"@nifrajs/schema": "4.0.2",
|
|
80
|
+
"@nifrajs/testing": "4.0.2",
|
|
81
|
+
"@nifrajs/web": "4.0.2",
|
|
82
|
+
"create-nifra": "4.0.2"
|
|
78
83
|
},
|
|
79
84
|
"repository": {
|
|
80
85
|
"type": "git",
|
|
@@ -86,7 +91,7 @@
|
|
|
86
91
|
},
|
|
87
92
|
"homepage": "https://github.com/nifrajs/nifra#readme",
|
|
88
93
|
"peerDependencies": {
|
|
89
|
-
"typescript": "^5.0.0 || ^6.0.0"
|
|
94
|
+
"typescript": "^5.0.0 || ^6.0.0 || ^7.0.0"
|
|
90
95
|
},
|
|
91
96
|
"peerDependenciesMeta": {
|
|
92
97
|
"typescript": {
|
|
@@ -94,6 +99,8 @@
|
|
|
94
99
|
}
|
|
95
100
|
},
|
|
96
101
|
"devDependencies": {
|
|
97
|
-
"typescript": "^6.0.3"
|
|
102
|
+
"typescript": "^6.0.3",
|
|
103
|
+
"typescript5": "npm:typescript@5.9.3",
|
|
104
|
+
"typescript7": "npm:typescript@7.0.2"
|
|
98
105
|
}
|
|
99
106
|
}
|
package/src/app-files.ts
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Where an app's own entry files live, in one place for every command.
|
|
3
|
+
*
|
|
4
|
+
* - `nifra.config.ts` - the CLI's config (dev/build tooling); only the CLI imports it.
|
|
5
|
+
* - `backend/framework.ts` - the render adapter and the options the generated server entry imports,
|
|
6
|
+
* kept free of build tooling because it is bundled into every server build.
|
|
7
|
+
* - `backend/app.ts` - exports `backend`, the app's API (a `@nifrajs/core` server). Optional.
|
|
8
|
+
*/
|
|
9
|
+
import { existsSync } from "node:fs"
|
|
10
|
+
import { resolve } from "node:path"
|
|
11
|
+
|
|
12
|
+
export const CONFIG_FILE = "nifra.config.ts"
|
|
13
|
+
export const FRAMEWORK_FILE = "backend/framework.ts"
|
|
14
|
+
export const BACKEND_APP_FILE = "backend/app.ts"
|
|
15
|
+
|
|
16
|
+
/** The root files this layout retired, and where each lives now. */
|
|
17
|
+
export const RETIRED_ROOT_FILES: Readonly<Record<string, string>> = {
|
|
18
|
+
"backend.ts": BACKEND_APP_FILE,
|
|
19
|
+
"framework.ts": FRAMEWORK_FILE,
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** Refuse an app still on the retired root files: run against it, a command would silently miss them. */
|
|
23
|
+
export function assertCurrentLayout(cwd: string): void {
|
|
24
|
+
const retired = Object.entries(RETIRED_ROOT_FILES).filter(([file]) =>
|
|
25
|
+
existsSync(resolve(cwd, file)),
|
|
26
|
+
)
|
|
27
|
+
if (retired.length === 0) return
|
|
28
|
+
const moves = retired.map(([from, to]) => `${from} -> ${to}`).join(", ")
|
|
29
|
+
throw new Error(
|
|
30
|
+
`[nifra] this app uses the retired root layout (${moves}). Run \`nifra migrate layout\` to see the ` +
|
|
31
|
+
"move (it also splits each route into x.tsx and x.backend.ts), then `nifra migrate layout --write`.",
|
|
32
|
+
)
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** The file the generated server entry imports the adapter from: `backend/framework.ts` when it exists,
|
|
36
|
+
* else the config (a single-target app may keep everything in `nifra.config.ts`). */
|
|
37
|
+
export function adapterFile(cwd: string): string {
|
|
38
|
+
const framework = resolve(cwd, FRAMEWORK_FILE)
|
|
39
|
+
return existsSync(framework) ? framework : resolve(cwd, CONFIG_FILE)
|
|
40
|
+
}
|
package/src/assure-hydration.ts
CHANGED
|
@@ -1,9 +1,8 @@
|
|
|
1
1
|
import { existsSync, statSync } from "node:fs"
|
|
2
|
-
import { mkdir, mkdtemp, readFile, rm, writeFile } from "node:fs/promises"
|
|
2
|
+
import { copyFile, mkdir, mkdtemp, readFile, rm, writeFile } from "node:fs/promises"
|
|
3
3
|
import { tmpdir } from "node:os"
|
|
4
4
|
import { basename, join, resolve } from "node:path"
|
|
5
5
|
import { fileURLToPath, pathToFileURL } from "node:url"
|
|
6
|
-
import { inProcessClient } from "@nifrajs/client"
|
|
7
6
|
import { defineReplayFile } from "@nifrajs/core/replay"
|
|
8
7
|
import { type AppLike, runApp } from "@nifrajs/runner"
|
|
9
8
|
import { createWebApp, type RenderAdapter } from "@nifrajs/web"
|
|
@@ -12,6 +11,8 @@ import { discoverRoutes } from "@nifrajs/web/fs"
|
|
|
12
11
|
import type { BunPlugin } from "bun"
|
|
13
12
|
import { collectDoctorResult } from "./doctor.ts"
|
|
14
13
|
import { loadApp, resolvePlugins } from "./load.ts"
|
|
14
|
+
import { CHILD_OUTPUT_MAX_BYTES, joinHeadTail, readBoundedStream, readHeadTail } from "./mcp-io.ts"
|
|
15
|
+
import { frameworkWebAppOptions } from "./web-app-options.ts"
|
|
15
16
|
|
|
16
17
|
const HYDRATION_ASSURANCE = Symbol.for("nifra.hydration.assurance")
|
|
17
18
|
const SOURCE_GLOB = "**/*.{ts,tsx,js,jsx,vue,svelte,mdx}"
|
|
@@ -176,6 +177,8 @@ async function buildHydrationApp(cwd: string): Promise<BuiltHydrationApp | { ski
|
|
|
176
177
|
const client = await buildClient({
|
|
177
178
|
routesDir: loaded.routesDir,
|
|
178
179
|
outDir: clientOutput,
|
|
180
|
+
// The runner imports the entry from disk, where a `/assets/` chunk URL is a filesystem path.
|
|
181
|
+
publicPath: `${pathToFileURL(clientOutput).href}/`,
|
|
179
182
|
clientModule: loaded.framework.clientModule,
|
|
180
183
|
...(plugins.length === 0 ? {} : { plugins: plugins as BunPlugin[] }),
|
|
181
184
|
...(loaded.framework.conditions === undefined
|
|
@@ -186,6 +189,9 @@ async function buildHydrationApp(cwd: string): Promise<BuiltHydrationApp | { ski
|
|
|
186
189
|
...(loaded.framework.publicEnvPrefix === undefined
|
|
187
190
|
? {}
|
|
188
191
|
: { publicEnvPrefix: loaded.framework.publicEnvPrefix }),
|
|
192
|
+
...(loaded.framework.secretExemptions === undefined
|
|
193
|
+
? {}
|
|
194
|
+
: { secretExemptions: loaded.framework.secretExemptions }),
|
|
189
195
|
})
|
|
190
196
|
if (client.entry.trim() === "" || client.assets.length === 0) {
|
|
191
197
|
await rm(workDir, { recursive: true, force: true })
|
|
@@ -200,7 +206,7 @@ async function buildHydrationApp(cwd: string): Promise<BuiltHydrationApp | { ski
|
|
|
200
206
|
adapter: loaded.framework.adapter as RenderAdapter,
|
|
201
207
|
manifest: discoverRoutes(loaded.routesDir),
|
|
202
208
|
clientEntry: client.entry,
|
|
203
|
-
...(loaded.
|
|
209
|
+
...frameworkWebAppOptions(loaded.framework, loaded.backend),
|
|
204
210
|
}) as unknown as AppLike
|
|
205
211
|
return {
|
|
206
212
|
app: webApp,
|
|
@@ -232,21 +238,14 @@ async function sourceFiles(cwd: string): Promise<Array<{ file: string; content:
|
|
|
232
238
|
return files
|
|
233
239
|
}
|
|
234
240
|
|
|
241
|
+
/** How long a route may take to mark itself hydrated before the gate fails it. */
|
|
242
|
+
const HYDRATION_TIMEOUT_MS = 5_000
|
|
243
|
+
let entryCopies = 0
|
|
244
|
+
|
|
235
245
|
function outputPath(outputDir: string, entry: string): string {
|
|
236
246
|
return join(outputDir, basename(entry))
|
|
237
247
|
}
|
|
238
248
|
|
|
239
|
-
function globalValue(html: string, name: string): unknown {
|
|
240
|
-
const expression = new RegExp(`window\\.${name}=([\\s\\S]*?)(?:</script>|;window\\.)`)
|
|
241
|
-
const value = expression.exec(html)?.[1]?.replace(/;$/, "")
|
|
242
|
-
if (value === undefined) return undefined
|
|
243
|
-
try {
|
|
244
|
-
return JSON.parse(value)
|
|
245
|
-
} catch {
|
|
246
|
-
return undefined
|
|
247
|
-
}
|
|
248
|
-
}
|
|
249
|
-
|
|
250
249
|
async function runDomHydration(
|
|
251
250
|
cwd: string,
|
|
252
251
|
built: BuiltHydrationApp,
|
|
@@ -288,11 +287,21 @@ async function runDomHydration(
|
|
|
288
287
|
else if (name === "cancelAnimationFrame") globals[name] = (id: number) => clearTimeout(id)
|
|
289
288
|
else globals[name] = windowValue[name]
|
|
290
289
|
}
|
|
290
|
+
// Framework runtimes reach for more DOM globals (Vue's `SVGElement`, `Text`, `Comment`). Lend every
|
|
291
|
+
// one the runtime does not define itself, so its own `fetch`, `URL` and timers stay in place.
|
|
292
|
+
for (const name of Object.getOwnPropertyNames(windowValue)) {
|
|
293
|
+
if (name in globals) continue
|
|
294
|
+
let value: unknown
|
|
295
|
+
try {
|
|
296
|
+
value = windowValue[name]
|
|
297
|
+
} catch {
|
|
298
|
+
continue
|
|
299
|
+
}
|
|
300
|
+
previous.set(name, undefined)
|
|
301
|
+
globals[name] =
|
|
302
|
+
typeof value === "function" && !/^[A-Z]/.test(name) ? value.bind(windowValue) : value
|
|
303
|
+
}
|
|
291
304
|
const windowRecord = windowValue as Record<string, unknown>
|
|
292
|
-
windowRecord.__NIFRA_ROUTE__ = globalValue(html, "__NIFRA_ROUTE__")
|
|
293
|
-
windowRecord.__NIFRA_DATA__ = globalValue(html, "__NIFRA_DATA__")
|
|
294
|
-
windowRecord.__NIFRA_LAYOUT_DATA__ = globalValue(html, "__NIFRA_LAYOUT_DATA__")
|
|
295
|
-
windowRecord.__NIFRA_ACTION__ = globalValue(html, "__NIFRA_ACTION__")
|
|
296
305
|
windowRecord.console = console
|
|
297
306
|
const errors: string[] = []
|
|
298
307
|
const recover = (error: unknown, info?: unknown): void => {
|
|
@@ -310,7 +319,12 @@ async function runDomHydration(
|
|
|
310
319
|
documentValue.querySelector as (selector: string) => { innerHTML?: unknown } | null
|
|
311
320
|
)("#root")
|
|
312
321
|
const before = String(beforeRoot?.innerHTML ?? "")
|
|
313
|
-
|
|
322
|
+
// Bun evaluates a file module once per path, query or not, so each route imports its own copy.
|
|
323
|
+
// A fresh directory each time: Bun's resolver caches a directory's listing once it has read it.
|
|
324
|
+
const copyDir = join(built.outputDir, `route-${++entryCopies}`)
|
|
325
|
+
await mkdir(copyDir)
|
|
326
|
+
const entry = join(copyDir, basename(built.client.entry))
|
|
327
|
+
await copyFile(outputPath(built.outputDir, built.client.entry), entry)
|
|
314
328
|
const originalError = console.error
|
|
315
329
|
const originalWarn = console.warn
|
|
316
330
|
console.error = (...args) => {
|
|
@@ -323,10 +337,32 @@ async function runDomHydration(
|
|
|
323
337
|
if (/hydr|mismatch|server|client|expected|recover/i.test(message)) errors.push(message)
|
|
324
338
|
originalWarn(...args)
|
|
325
339
|
}
|
|
340
|
+
// A render that throws on the client surfaces from a scheduler task, not from the import.
|
|
341
|
+
const uncaught = (error: unknown): void => {
|
|
342
|
+
errors.push(`uncaught: ${error instanceof Error ? error.message : String(error)}`)
|
|
343
|
+
}
|
|
344
|
+
process.on("uncaughtException", uncaught)
|
|
345
|
+
process.on("unhandledRejection", uncaught)
|
|
346
|
+
let hydrated = false
|
|
326
347
|
try {
|
|
327
|
-
await import(
|
|
328
|
-
|
|
348
|
+
await import(pathToFileURL(entry).href)
|
|
349
|
+
// The entry marks <html> once the adapter has mounted the route. `Date.now` is pinned by the
|
|
350
|
+
// deterministic runtime, so the bound is measured with `performance.now`.
|
|
351
|
+
const marked = documentValue.documentElement as { hasAttribute(name: string): boolean }
|
|
352
|
+
const started = performance.now()
|
|
353
|
+
for (;;) {
|
|
354
|
+
hydrated = marked.hasAttribute("data-nifra-hydrated")
|
|
355
|
+
if (hydrated || errors.length > 0 || performance.now() - started >= HYDRATION_TIMEOUT_MS)
|
|
356
|
+
break
|
|
357
|
+
await new Promise((resolvePromise) => setTimeout(resolvePromise, 1))
|
|
358
|
+
}
|
|
359
|
+
// The framework commits the hydration in its own scheduler tasks after the marker's frame.
|
|
360
|
+
for (let tick = 0; tick < 10; tick++) {
|
|
361
|
+
await new Promise((resolvePromise) => setTimeout(resolvePromise, 0))
|
|
362
|
+
}
|
|
329
363
|
} finally {
|
|
364
|
+
process.off("uncaughtException", uncaught)
|
|
365
|
+
process.off("unhandledRejection", uncaught)
|
|
330
366
|
console.error = originalError
|
|
331
367
|
console.warn = originalWarn
|
|
332
368
|
}
|
|
@@ -335,6 +371,16 @@ async function runDomHydration(
|
|
|
335
371
|
)("#root")
|
|
336
372
|
const after = String(afterRoot?.innerHTML ?? "")
|
|
337
373
|
const diagnostics: import("./diagnostics.ts").Diagnostic[] = []
|
|
374
|
+
if (!hydrated && errors.length === 0) {
|
|
375
|
+
diagnostics.push(
|
|
376
|
+
diagnostic(
|
|
377
|
+
"NF-H001",
|
|
378
|
+
`client hydration did not complete within ${HYDRATION_TIMEOUT_MS / 1000}s`,
|
|
379
|
+
undefined,
|
|
380
|
+
[built.framework],
|
|
381
|
+
),
|
|
382
|
+
)
|
|
383
|
+
}
|
|
338
384
|
if (errors.length > 0 || before !== after) {
|
|
339
385
|
diagnostics.push(
|
|
340
386
|
diagnostic("NF-H001", "client hydration reported a recoverable mismatch", undefined, [
|
|
@@ -555,57 +601,104 @@ async function runHydrationProof(cwd: string, options: HydrationOptions): Promis
|
|
|
555
601
|
}
|
|
556
602
|
}
|
|
557
603
|
|
|
604
|
+
/** How long one hydration run may take: it builds the client and renders every route. */
|
|
605
|
+
const HYDRATION_RUN_TIMEOUT_MS = 300_000
|
|
606
|
+
|
|
607
|
+
/** Bounds on a hydration run beyond its {@link HydrationOptions}. */
|
|
608
|
+
export interface HydrationRunControl {
|
|
609
|
+
readonly signal?: AbortSignal
|
|
610
|
+
/** Default 300000 (five minutes). */
|
|
611
|
+
readonly timeoutMs?: number
|
|
612
|
+
}
|
|
613
|
+
|
|
558
614
|
/** Run hydration in a fresh process so the project's current client and framework modules are isolated. */
|
|
559
615
|
export async function runHydrationAssurance(
|
|
560
616
|
cwd: string,
|
|
561
617
|
options: HydrationOptions = {},
|
|
618
|
+
control: HydrationRunControl = {},
|
|
562
619
|
): Promise<HydrationResult> {
|
|
620
|
+
const failed = (reason: string): HydrationResult => ({
|
|
621
|
+
diagnostics: [diagnostic("NF-H001", `hydration runner failed: ${reason}`)],
|
|
622
|
+
})
|
|
623
|
+
if (control.signal?.aborted) return failed("cancelled")
|
|
563
624
|
const root = resolve(cwd)
|
|
564
625
|
const entry = fileURLToPath(import.meta.url)
|
|
626
|
+
// The answer line carries this token, so whatever project code prints cannot be read as the result.
|
|
627
|
+
const token = crypto.randomUUID()
|
|
565
628
|
const proc = Bun.spawn(
|
|
566
629
|
[process.execPath, entry, root, "--nifra-hydration-child", JSON.stringify(options)],
|
|
567
630
|
{
|
|
568
|
-
|
|
631
|
+
cwd: root,
|
|
632
|
+
// Explicit: without `env`, Bun passes the environment it started with, missing `--env-file` values.
|
|
633
|
+
env: process.env,
|
|
634
|
+
stdin: "pipe",
|
|
569
635
|
stdout: "pipe",
|
|
570
636
|
stderr: "pipe",
|
|
571
637
|
},
|
|
572
638
|
)
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
639
|
+
let stopped: string | undefined
|
|
640
|
+
const stop = (reason: string): void => {
|
|
641
|
+
stopped ??= reason
|
|
642
|
+
proc.kill()
|
|
643
|
+
}
|
|
644
|
+
const onAbort = (): void => stop("cancelled")
|
|
645
|
+
control.signal?.addEventListener("abort", onAbort, { once: true })
|
|
646
|
+
const timeoutMs = control.timeoutMs ?? HYDRATION_RUN_TIMEOUT_MS
|
|
647
|
+
const timer = setTimeout(() => stop(`timed out after ${timeoutMs} ms`), timeoutMs)
|
|
578
648
|
try {
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
649
|
+
try {
|
|
650
|
+
proc.stdin.write(`${token}\n`)
|
|
651
|
+
await proc.stdin.end()
|
|
652
|
+
} catch {
|
|
653
|
+
// The child exited before reading its token; its stderr says why.
|
|
654
|
+
}
|
|
655
|
+
const [stdout, stderr] = await Promise.all([
|
|
656
|
+
readBoundedStream(proc.stdout, CHILD_OUTPUT_MAX_BYTES, () =>
|
|
657
|
+
stop(`output exceeded ${CHILD_OUTPUT_MAX_BYTES} bytes`),
|
|
658
|
+
),
|
|
659
|
+
readHeadTail(proc.stderr, 2048, 2048),
|
|
660
|
+
proc.exited,
|
|
661
|
+
])
|
|
662
|
+
if (stopped !== undefined) return failed(stopped)
|
|
663
|
+
const prefix = `${token} `
|
|
664
|
+
const answer = stdout.text.split("\n").find((line) => line.startsWith(prefix))
|
|
665
|
+
if (answer !== undefined) {
|
|
666
|
+
try {
|
|
667
|
+
return JSON.parse(answer.slice(prefix.length)) as HydrationResult
|
|
668
|
+
} catch {
|
|
669
|
+
// A truncated answer line falls through to the stderr report.
|
|
670
|
+
}
|
|
588
671
|
}
|
|
672
|
+
return failed(joinHeadTail(stderr).trim() || "no result")
|
|
673
|
+
} finally {
|
|
674
|
+
clearTimeout(timer)
|
|
675
|
+
control.signal?.removeEventListener("abort", onAbort)
|
|
589
676
|
}
|
|
590
677
|
}
|
|
591
678
|
|
|
592
679
|
if (import.meta.main && process.argv.includes("--nifra-hydration-child")) {
|
|
680
|
+
// Bound before project code loads, so a `console.log` in a config or loader goes to stderr and
|
|
681
|
+
// never reaches the answer channel.
|
|
682
|
+
const stdout = process.stdout.write.bind(process.stdout)
|
|
683
|
+
const toStderr = (...args: unknown[]): void => {
|
|
684
|
+
process.stderr.write(`${args.map((arg) => String(arg)).join(" ")}\n`)
|
|
685
|
+
}
|
|
686
|
+
console.log = toStderr
|
|
687
|
+
console.info = toStderr
|
|
688
|
+
console.debug = toStderr
|
|
689
|
+
const token = (await readBoundedStream(Bun.stdin.stream(), 256)).text.trim()
|
|
593
690
|
const cwd = process.argv[2] ?? process.cwd()
|
|
594
691
|
const rawOptions = process.argv[4]
|
|
692
|
+
let result: HydrationResult
|
|
595
693
|
try {
|
|
596
694
|
process.chdir(cwd)
|
|
597
695
|
const options = rawOptions === undefined ? {} : (JSON.parse(rawOptions) as HydrationOptions)
|
|
598
|
-
|
|
599
|
-
await Bun.write(Bun.stdout, JSON.stringify(result))
|
|
696
|
+
result = await runHydrationProof(cwd, options)
|
|
600
697
|
} catch (error) {
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
diagnostics: [
|
|
605
|
-
diagnostic("NF-H001", error instanceof Error ? error.message : String(error)),
|
|
606
|
-
],
|
|
607
|
-
}),
|
|
608
|
-
)
|
|
698
|
+
result = {
|
|
699
|
+
diagnostics: [diagnostic("NF-H001", error instanceof Error ? error.message : String(error))],
|
|
700
|
+
}
|
|
609
701
|
}
|
|
702
|
+
await new Promise<void>((done) => stdout(`${token} ${JSON.stringify(result)}\n`, () => done()))
|
|
610
703
|
process.exit(0)
|
|
611
704
|
}
|
package/src/assure.ts
CHANGED
|
@@ -4,6 +4,7 @@ import { existsSync } from "node:fs"
|
|
|
4
4
|
import { join, resolve } from "node:path"
|
|
5
5
|
import { pathToFileURL } from "node:url"
|
|
6
6
|
import type { AssuranceConfig, AssuranceReport } from "@nifrajs/core/assurance"
|
|
7
|
+
import { CONFIG_FILE, FRAMEWORK_FILE } from "./app-files.ts"
|
|
7
8
|
import type { Diagnostic } from "./diagnostics.ts"
|
|
8
9
|
|
|
9
10
|
export interface AssureSink {
|
|
@@ -92,10 +93,24 @@ export async function loadAssuranceConfig(
|
|
|
92
93
|
|
|
93
94
|
export function formatAssuranceReport(report: AssuranceReport): string {
|
|
94
95
|
if (report.ok) {
|
|
96
|
+
const contracted = report.routes.filter((route) => route.responseContract !== undefined)
|
|
97
|
+
const enforced = contracted.filter((route) => route.responseContract === "enforce").length
|
|
98
|
+
const warned = contracted.filter((route) => route.responseContract === "warn").length
|
|
99
|
+
const unchecked = contracted.filter((route) => route.responseContract === "unchecked").length
|
|
100
|
+
const runtimeContract =
|
|
101
|
+
contracted.length === 0
|
|
102
|
+
? " No declared response/error schemas were found."
|
|
103
|
+
: " Runtime response contracts: " +
|
|
104
|
+
enforced +
|
|
105
|
+
" enforced, " +
|
|
106
|
+
warned +
|
|
107
|
+
" warn-only, " +
|
|
108
|
+
unchecked +
|
|
109
|
+
" typed-only."
|
|
95
110
|
const capability = report.capabilities
|
|
96
111
|
? ` Capability assurance covered ${report.capabilities.routes.length} route${report.capabilities.routes.length === 1 ? "" : "s"}.`
|
|
97
112
|
: ""
|
|
98
|
-
return `✓ route assurance: ${report.routes.length} route${report.routes.length === 1 ? "" : "s"} classified; all required evidence is present.${capability}`
|
|
113
|
+
return `✓ route assurance: ${report.routes.length} route${report.routes.length === 1 ? "" : "s"} classified; all required evidence is present.${capability}${runtimeContract}`
|
|
99
114
|
}
|
|
100
115
|
const messages = [
|
|
101
116
|
...report.findings.map((finding) => finding.message),
|
|
@@ -280,7 +295,7 @@ export async function collectAssureBundle(
|
|
|
280
295
|
})
|
|
281
296
|
|
|
282
297
|
await addGate("render", async () => {
|
|
283
|
-
if (!existsSync(join(cwd,
|
|
298
|
+
if (!existsSync(join(cwd, CONFIG_FILE)) && !existsSync(join(cwd, FRAMEWORK_FILE)))
|
|
284
299
|
return { diagnostics: [], skipReason: "no client render target configured" }
|
|
285
300
|
const { renderPages } = await import("./mcp-render.ts")
|
|
286
301
|
const result = await renderPages(cwd, [{ path: "/" }])
|
package/src/capabilities-tool.ts
CHANGED
|
@@ -16,11 +16,14 @@ import {
|
|
|
16
16
|
snapshotCapabilities,
|
|
17
17
|
validCapabilityId,
|
|
18
18
|
} from "@nifrajs/core/capabilities"
|
|
19
|
-
import {
|
|
19
|
+
import { expandOptionalParams } from "@nifrajs/core/pattern"
|
|
20
|
+
import { type ReflectedMount, type ReflectedRoute, reflectRoutes } from "@nifrajs/core/reflection"
|
|
20
21
|
import { scanStaticRouteText, stripComments, walkSource } from "./check.ts"
|
|
21
22
|
|
|
23
|
+
// Import and re-export clauses are spelled out (a default binding, then `* as ns` or a `{ … }` list)
|
|
24
|
+
// rather than scanned lazily, which is quadratic on a long semicolon-free module.
|
|
22
25
|
const EFFECT_IMPORT =
|
|
23
|
-
/\bimport\s
|
|
26
|
+
/\bimport\s*(?!type\b)(?:(?:[\w$]+\s*,?\s*)?(?:\*\s*(?:as\b\s*[\w$]+\s*)?|\{[^{}'"]*\}\s*)?from\s*)?["']([^"']+)["']|\bimport\s*\(\s*["']([^"']+)["']\s*\)|\brequire\s*\(\s*["']([^"']+)["']\s*\)|\bexport\s*(?!type\b)(?:\*\s*(?:as\b\s*[\w$]+\s*)?|\{[^{}'"]*\}\s*)from\s*["']([^"']+)["']/g
|
|
24
27
|
// `\\` is excluded from both inner classes: letting the class also match a lone backslash makes the
|
|
25
28
|
// `(A*(\\.A*)*)` shape ambiguous, which is exponential on a run of backslashes.
|
|
26
29
|
const TEMPLATE_EFFECT_IMPORT = /\b(?:import|require)\s*\(\s*`([^`$\\]*(?:\\.[^`$\\]*)*)`\s*\)/g
|
|
@@ -312,27 +315,44 @@ async function walkRouteModules(
|
|
|
312
315
|
return { covered, evidence, violations, truncations }
|
|
313
316
|
}
|
|
314
317
|
|
|
318
|
+
/** The reflected paths a statically collected pattern stands for; a malformed pattern is itself. */
|
|
319
|
+
function staticRouteForms(path: string): readonly string[] {
|
|
320
|
+
try {
|
|
321
|
+
return expandOptionalParams(path)
|
|
322
|
+
} catch {
|
|
323
|
+
return [path]
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
|
|
315
327
|
/** Build coverage-qualified static evidence for every reflected route. */
|
|
316
328
|
export async function collectCapabilityProjectReport(
|
|
317
329
|
cwd: string,
|
|
318
330
|
source: unknown,
|
|
319
331
|
policyInput: CapabilityPolicy,
|
|
332
|
+
options: {
|
|
333
|
+
readonly routes?: readonly ReflectedRoute[]
|
|
334
|
+
readonly mounts?: readonly ReflectedMount[]
|
|
335
|
+
} = {},
|
|
320
336
|
): Promise<CapabilityProjectReport> {
|
|
321
337
|
const policy = defineCapabilityPolicy(policyInput)
|
|
322
338
|
const sources = await readSources(cwd)
|
|
323
339
|
const automatic = new Map<string, Set<string>>()
|
|
324
340
|
for (const [file, content] of sources) {
|
|
325
341
|
for (const route of scanStaticRouteText(file, content)) {
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
342
|
+
// A path ending in optional params is reflected as one route per form it serves.
|
|
343
|
+
for (const path of staticRouteForms(route.path)) {
|
|
344
|
+
const modules = automatic.get(routeKey(route.method, path)) ?? new Set<string>()
|
|
345
|
+
modules.add(file)
|
|
346
|
+
automatic.set(routeKey(route.method, path), modules)
|
|
347
|
+
}
|
|
329
348
|
}
|
|
330
349
|
}
|
|
331
350
|
|
|
332
351
|
const evidenceRoutes = []
|
|
333
352
|
const violations: CapabilityImportViolation[] = []
|
|
334
353
|
const truncations: CapabilityProvenanceTruncation[] = []
|
|
335
|
-
|
|
354
|
+
const reflectedRoutes = options.routes ?? reflectRoutes(source)
|
|
355
|
+
for (const route of reflectedRoutes) {
|
|
336
356
|
const modules = new Set(automatic.get(routeKey(route.method, route.path)) ?? [])
|
|
337
357
|
for (const association of policy.provenance.routeModules ?? []) {
|
|
338
358
|
if (
|
|
@@ -394,7 +414,11 @@ export async function collectCapabilityProjectReport(
|
|
|
394
414
|
}
|
|
395
415
|
}
|
|
396
416
|
|
|
397
|
-
const evaluated = evaluateCapabilityAssurance(source, policy, {
|
|
417
|
+
const evaluated = evaluateCapabilityAssurance(source, policy, {
|
|
418
|
+
routes: evidenceRoutes,
|
|
419
|
+
reflectedRoutes,
|
|
420
|
+
...(options.mounts === undefined ? {} : { mounts: options.mounts }),
|
|
421
|
+
})
|
|
398
422
|
const report: CapabilityAssuranceReport =
|
|
399
423
|
violations.length === 0 && truncations.length === 0 && unmatchedSeams.length === 0
|
|
400
424
|
? evaluated
|
|
@@ -674,9 +698,11 @@ export async function runCapabilityCheck(
|
|
|
674
698
|
2,
|
|
675
699
|
),
|
|
676
700
|
)
|
|
677
|
-
} else if (ok) {
|
|
678
|
-
console.log("✓ capability assurance and lockfile are current")
|
|
679
701
|
} else {
|
|
702
|
+
if (ok) console.log("✓ capability assurance and lockfile are current")
|
|
703
|
+
// Stated on every run, passing or not: a gap is the report's own boundary, not a failure.
|
|
704
|
+
for (const gap of result.report.gaps ?? [])
|
|
705
|
+
console.log(`• known gap, not analyzed: ${gap.path} - ${gap.reason}`)
|
|
680
706
|
for (const finding of result.report.findings) console.log(`✖ ${finding.message}`)
|
|
681
707
|
for (const violation of result.violations)
|
|
682
708
|
console.log(
|