@nifrajs/cli 3.3.0 → 4.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +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 +7 -15
- package/dist/check-diagnostics.d.ts.map +1 -1
- package/dist/check-diagnostics.js +162 -713
- package/dist/check-diagnostics.js.map +1 -1
- package/dist/check-scan.d.ts +58 -7
- package/dist/check-scan.d.ts.map +1 -1
- package/dist/check-scan.js +323 -62
- package/dist/check-scan.js.map +1 -1
- package/dist/check.d.ts +1 -0
- package/dist/check.d.ts.map +1 -1
- package/dist/check.js +145 -54
- 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 +306 -87
- package/dist/cli.js.map +1 -1
- package/dist/command-catalog.d.ts +60 -5
- package/dist/command-catalog.d.ts.map +1 -1
- package/dist/command-catalog.js +298 -33
- 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 +3 -2
- package/dist/contracts.d.ts.map +1 -1
- package/dist/contracts.js +56 -16
- 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/diagnostics.d.ts +86 -0
- package/dist/diagnostics.d.ts.map +1 -1
- package/dist/diagnostics.js +85 -0
- package/dist/diagnostics.js.map +1 -1
- 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 +41 -21
- 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 +6 -4
- package/dist/introspect.d.ts.map +1 -1
- package/dist/introspect.js +93 -74
- 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 +71 -16
- 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.d.ts.map +1 -1
- package/dist/mcp-context.js +5 -6
- 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 +3 -0
- package/dist/mcp-exec.d.ts.map +1 -1
- package/dist/mcp-exec.js +263 -131
- package/dist/mcp-exec.js.map +1 -1
- package/dist/mcp-http.d.ts.map +1 -1
- package/dist/mcp-http.js +20 -5
- package/dist/mcp-http.js.map +1 -1
- package/dist/mcp-io.d.ts +26 -0
- package/dist/mcp-io.d.ts.map +1 -1
- package/dist/mcp-io.js +113 -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-path.d.ts +10 -0
- package/dist/mcp-path.d.ts.map +1 -0
- package/dist/mcp-path.js +51 -0
- package/dist/mcp-path.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-reflect.d.ts.map +1 -1
- package/dist/mcp-reflect.js +2 -2
- package/dist/mcp-reflect.js.map +1 -1
- package/dist/mcp-render.d.ts.map +1 -1
- package/dist/mcp-render.js +64 -45
- 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 +124 -25
- 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 +159 -55
- 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 +18 -5
- package/dist/mcp.d.ts.map +1 -1
- package/dist/mcp.js +165 -82
- 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 +982 -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 +28 -4
- package/dist/project-facts.d.ts.map +1 -1
- package/dist/project-facts.js +12 -2
- 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 +34 -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 +21 -0
- package/dist/review.d.ts.map +1 -0
- package/dist/review.js +823 -0
- package/dist/review.js.map +1 -0
- 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 +3 -1
- package/dist/rules/legacy.d.ts.map +1 -1
- package/dist/rules/legacy.js +829 -34
- 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 +154 -90
- 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.map +1 -1
- package/dist/sync-manifest.js +2 -1
- 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 +8 -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 +3 -0
- package/dist/work-graph.d.ts.map +1 -1
- package/dist/work-graph.js +33 -16
- package/dist/work-graph.js.map +1 -1
- package/docs/examples.json +185 -29
- package/docs/llms-full.txt +4123 -548
- package/docs/types.json +14391 -10060
- package/package.json +19 -11
- 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 +199 -797
- package/src/check-scan.ts +394 -63
- package/src/check.ts +146 -58
- package/src/cli.ts +347 -92
- package/src/command-catalog.ts +407 -45
- package/src/config-target.ts +47 -0
- package/src/contracts.ts +66 -15
- 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/diagnostics.ts +179 -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 +44 -31
- 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 +93 -82
- package/src/learn.ts +3 -3
- package/src/levels-tool.ts +47 -2
- package/src/load.ts +144 -32
- package/src/manifest-tool.ts +3 -2
- package/src/mcp-app-tool.ts +2 -0
- package/src/mcp-context.ts +5 -7
- package/src/mcp-delegate.ts +162 -0
- package/src/mcp-docs-tools.ts +23 -5
- package/src/mcp-exec.ts +301 -161
- package/src/mcp-http.ts +22 -5
- package/src/mcp-io.ts +130 -0
- package/src/mcp-isolate.ts +287 -0
- package/src/mcp-path.ts +50 -0
- package/src/mcp-project-child.ts +182 -0
- package/src/mcp-reflect.ts +3 -2
- package/src/mcp-render.ts +72 -42
- package/src/mcp-root.ts +130 -24
- package/src/mcp-run.ts +190 -56
- package/src/mcp-ws.ts +4 -1
- package/src/mcp.ts +193 -91
- package/src/migrate-layout.ts +1121 -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 +42 -8
- package/src/recipes/4.0.0.ts +35 -0
- package/src/recipes/index.ts +2 -0
- package/src/release-verification.ts +74 -5
- package/src/review.ts +988 -0
- 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 +947 -35
- 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 +175 -98
- package/src/sdk.ts +1215 -158
- package/src/search-terms.ts +24 -7
- package/src/smoke.ts +607 -0
- package/src/sync-manifest.ts +2 -1
- 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 +27 -0
- package/src/verification.ts +50 -12
- package/src/web-app-options.ts +57 -0
- package/src/work-graph.ts +40 -16
package/dist/check-scan.js
CHANGED
|
@@ -7,7 +7,9 @@
|
|
|
7
7
|
import { readFileSync, realpathSync } from "node:fs";
|
|
8
8
|
import { dirname, isAbsolute, join, resolve, sep } from "node:path";
|
|
9
9
|
import { Glob } from "bun";
|
|
10
|
-
import {
|
|
10
|
+
import { codeUnitOrder } from "./internal/code-unit-order.js";
|
|
11
|
+
import { scriptKindOf } from "./internal/script-kind.js";
|
|
12
|
+
import { loadProjectTypeScript, } from "./internal/typescript-import.js";
|
|
11
13
|
import { commentBlockMarkerReason } from "./rules/comment-markers.js";
|
|
12
14
|
// `fetch( <ws> ('|"|`) / (not /)` - a string/template arg starting with a single `/` is a relative URL,
|
|
13
15
|
// i.e. same-origin = this app's own API. `(?<![.\w])` skips `.fetch(` (a method) and `prefetch(`; the
|
|
@@ -28,17 +30,60 @@ const IGNORED = new RegExp(`${IGNORED_DIR.source}|${TEST_FILE.source}`);
|
|
|
28
30
|
/** Paths crossing the scanner boundary are project-relative identifiers, not filesystem paths. Keep
|
|
29
31
|
* them stable across runtimes so rules, diagnostics, ignore globs, and import chains have one shape. */
|
|
30
32
|
const normalizeProjectPath = (path) => path.replaceAll("\\", "/");
|
|
31
|
-
//
|
|
32
|
-
|
|
33
|
+
// Browser code by path, mirroring `@nifrajs/web/zones`: a suffix decides first, then the folder. A
|
|
34
|
+
// route's backend half (`x.backend.ts`), `backend/` and `*.fn.ts` run on the server only.
|
|
35
|
+
const BACKEND_SUFFIX = /\.backend\.[cm]?[jt]sx?$/;
|
|
36
|
+
const BROWSER_SUFFIX = /\.(?:frontend|shared)\.[^/]+$/;
|
|
37
|
+
const BROWSER_FOLDER = /(^|\/)(?:routes|frontend|shared)\//;
|
|
38
|
+
const BACKEND_FOLDER = /(^|\/)backend\//;
|
|
39
|
+
const SERVER_FN_FILE = /\.fn\.[cm]?[jt]sx?$/;
|
|
40
|
+
/** Whether a project file is browser code: a route's frontend half, `frontend/` or `shared/`. Pure. */
|
|
41
|
+
export function isBrowserSource(file) {
|
|
42
|
+
const path = normalizeProjectPath(file);
|
|
43
|
+
if (BACKEND_SUFFIX.test(path) || SERVER_FN_FILE.test(path))
|
|
44
|
+
return false;
|
|
45
|
+
if (BROWSER_SUFFIX.test(path))
|
|
46
|
+
return true;
|
|
47
|
+
return BROWSER_FOLDER.test(path) && !BACKEND_FOLDER.test(path);
|
|
48
|
+
}
|
|
49
|
+
/** Whether a resolved module is backend code a browser import may not reach (a `*.fn.ts` module ships
|
|
50
|
+
* as its generated stub, so it is not). A legacy `*.server` module counts: the build refuses it. */
|
|
51
|
+
function isBackendModule(file) {
|
|
52
|
+
const path = normalizeProjectPath(file);
|
|
53
|
+
if (SERVER_FN_FILE.test(path))
|
|
54
|
+
return false;
|
|
55
|
+
return (BACKEND_SUFFIX.test(path) ||
|
|
56
|
+
LEGACY_SERVER_FILE.test(path) ||
|
|
57
|
+
(BACKEND_FOLDER.test(path) && !BROWSER_SUFFIX.test(path)));
|
|
58
|
+
}
|
|
33
59
|
// Module specifiers that must never be VALUE-imported into a route module: node:/bun: builtins, common
|
|
34
60
|
// DB drivers/ORМ server entrypoints, and the conventional `./db` module the scaffold generates.
|
|
35
61
|
const SERVER_ONLY = /^(?:node:|bun:)|^(?:postgres|pg|mysql2|ioredis|redis|better-sqlite3|mongodb|@libsql\/client)$|^drizzle-orm\/(?:node-postgres|postgres-js|bun-sqlite|libsql|mysql2|pglite)\b|^(?:\.\.?\/)+db(?:\.[cm]?[jt]sx?)?$/;
|
|
36
|
-
// A static, non-type import with a string specifier
|
|
37
|
-
//
|
|
38
|
-
// after `import`, so `import\s+`
|
|
39
|
-
|
|
62
|
+
// A static, non-type import with a string specifier, or a re-export (`export … from "x"`), which
|
|
63
|
+
// pulls its module into the bundle the same way. `import type …` and `export type …` are erased at
|
|
64
|
+
// build, so they're safe and skipped. Dynamic `import(…)` has `(` right after `import`, so `import\s+`
|
|
65
|
+
// never matches it; it is read by DYNAMIC_IMPORT below.
|
|
66
|
+
// The clause between the keyword and `from` is spelled out (a default binding, then `* as ns` or a
|
|
67
|
+
// `{ … }` list) rather than scanned lazily: a lazy scan restarts at every `export const` line and is
|
|
68
|
+
// quadratic on a long semicolon-free module.
|
|
69
|
+
const STATIC_IMPORT = /\b(?:import\s*(?!type\b)(?:(?:[\w$]+\s*,?\s*)?(?:\*\s*(?:as\b\s*[\w$]+\s*)?|\{[^{}'"]*\}\s*)?from\s*)?|export\s*(?!type\b)(?:\*\s*(?:as\b\s*[\w$]+\s*)?|\{[^{}'"]*\}\s*)from\s*)['"]([^'"]+)['"]/g;
|
|
70
|
+
// A dynamic `import("x")` whose specifier is one string literal. The client build bundles its target
|
|
71
|
+
// as a lazy chunk, so it reaches the browser bundle like a static import does - also from inside a
|
|
72
|
+
// loader, which ships with the route module. A computed specifier cannot be followed and is skipped.
|
|
73
|
+
const DYNAMIC_IMPORT = /(?<![\w$.])import\s*\(\s*(["'`])([^"'`$\\\r\n]+)\1\s*[,)]/g;
|
|
74
|
+
// Without a parser: `typeof import("x")` and `import("x").Name` are type positions, erased at build.
|
|
75
|
+
// A runtime `import("x")` is a promise, so the only members read straight off it are its methods.
|
|
76
|
+
const TYPE_QUERY_BEFORE = /\btypeof\s*$/;
|
|
77
|
+
const TYPE_QUALIFIER_AFTER = /^\s*\.\s*(?!(?:then|catch|finally)\b)[A-Za-z_$]/;
|
|
40
78
|
const ROUTE_REGISTRATION_DQ = /\.([A-Za-z]+)\s*\(\s*"((?:\\.|[^"\\])*)"/g;
|
|
41
79
|
const ROUTE_REGISTRATION_SQ = /\.([A-Za-z]+)\s*\(\s*'((?:\\.|[^'\\])*)'/g;
|
|
80
|
+
// `all("/path", ...)` and `method("PURGE", "/path", ...)` / `method(["GET", "PURGE"], "/path", ...)`
|
|
81
|
+
// from `@nifrajs/core/methods`. They are free function calls, so the member-call patterns above do
|
|
82
|
+
// not see them.
|
|
83
|
+
const METHODS_SUBPATH = "@nifrajs/core/methods";
|
|
84
|
+
const ALL_ROUTES = /(?<![\w$.])all\s*\(\s*(?:"((?:\\.|[^"\\])*)"|'((?:\\.|[^'\\])*)')/g;
|
|
85
|
+
const METHOD_ROUTES = /(?<![\w$.])method\s*\(\s*(\[[^\]]*\]|"[^"\\]*"|'[^'\\]*')\s*,\s*(?:"((?:\\.|[^"\\])*)"|'((?:\\.|[^'\\])*)')/g;
|
|
86
|
+
const METHOD_NAME_LITERAL = /(["'])([A-Za-z][A-Za-z0-9-]{0,31})\1/g;
|
|
42
87
|
export const IDENT = /^[A-Za-z_$][A-Za-z0-9_$]*$/;
|
|
43
88
|
/** Line number (1-based) of a match index within `content`. */
|
|
44
89
|
const lineAt = (content, index) => content.slice(0, index).split("\n").length;
|
|
@@ -459,6 +504,38 @@ export function scanFetchText(file, content, externalMounts = []) {
|
|
|
459
504
|
}
|
|
460
505
|
return out;
|
|
461
506
|
}
|
|
507
|
+
// `new EventSource(` / `new WebSocket(` - a hand-rolled subscription to this app's own streaming
|
|
508
|
+
// surface. `new` is required so a local binding named `WebSocket` (a wrapper, a mock) stays quiet;
|
|
509
|
+
// The word boundary excludes member calls and identifiers such as `renew WebSocket(`; `new` keeps
|
|
510
|
+
// local wrapper bindings quiet while still matching the platform constructors.
|
|
511
|
+
const STREAM_CONSTRUCT = /(?<![\w$])new\s+(?:EventSource|WebSocket)\s*\(/g;
|
|
512
|
+
/** Scan one file's text for hand-rolled own-API `EventSource`/`WebSocket` constructions. Pure +
|
|
513
|
+
* line-accurate. Same contract as {@link scanFetchText}: only a relative-path first argument
|
|
514
|
+
* (`"/…"` not `"//…"`, no scheme) is this app's API, so absolute `ws(s)://` / `http(s)://` URLs
|
|
515
|
+
* stay quiet exactly like absolute fetch URLs do. `externalMounts` and the relative-URL rule behave
|
|
516
|
+
* identically. A relative `new WebSocket("/…")` throws in browsers (the WS constructor needs an
|
|
517
|
+
* absolute URL), so a hit is either broken or bypassing `api.*.ws()` - either way the typed call
|
|
518
|
+
* (`api.*.subscribe()` for `app.sse()` routes, `api.*.ws()` for `app.ws()` routes) is the fix, and
|
|
519
|
+
* it keeps the compiler catching drift. */
|
|
520
|
+
export function scanStreamText(file, content, externalMounts = []) {
|
|
521
|
+
const out = [];
|
|
522
|
+
const lines = content.split("\n");
|
|
523
|
+
const code = codePositionMask(content);
|
|
524
|
+
STREAM_CONSTRUCT.lastIndex = 0;
|
|
525
|
+
for (let m = STREAM_CONSTRUCT.exec(code); m !== null; m = STREAM_CONSTRUCT.exec(code)) {
|
|
526
|
+
const argument = firstArgumentIndex(content, m.index + m[0].length);
|
|
527
|
+
const quote = content[argument];
|
|
528
|
+
if (quote !== "'" && quote !== '"' && quote !== "`")
|
|
529
|
+
continue;
|
|
530
|
+
if (content[argument + 1] !== "/" || content[argument + 2] === "/")
|
|
531
|
+
continue;
|
|
532
|
+
if (externalMounts.length > 0 && matchesExternalMount(content, argument + 1, externalMounts))
|
|
533
|
+
continue;
|
|
534
|
+
const line = lineAt(content, m.index);
|
|
535
|
+
out.push({ file, line, snippet: (lines[line - 1] ?? "").trim() });
|
|
536
|
+
}
|
|
537
|
+
return out;
|
|
538
|
+
}
|
|
462
539
|
/** Statically collect simple Nifra route registrations from source, without importing app code. */
|
|
463
540
|
export function scanStaticRouteText(file, content, facts) {
|
|
464
541
|
const code = stripComments(content);
|
|
@@ -467,8 +544,47 @@ export function scanStaticRouteText(file, content, facts) {
|
|
|
467
544
|
return [
|
|
468
545
|
...scanRoutePattern(file, content, code, ROUTE_REGISTRATION_DQ, facts),
|
|
469
546
|
...scanRoutePattern(file, content, code, ROUTE_REGISTRATION_SQ, facts),
|
|
547
|
+
...(code.includes(METHODS_SUBPATH) ? scanMethodRoutes(file, content, code, facts) : []),
|
|
470
548
|
].sort(bySite);
|
|
471
549
|
}
|
|
550
|
+
/** Routes from `all()` and `method()`, one per literal method name; a computed name is not guessed. */
|
|
551
|
+
function scanMethodRoutes(file, content, code, facts) {
|
|
552
|
+
const out = [];
|
|
553
|
+
const lines = content.split("\n");
|
|
554
|
+
const collect = (index, callee, pathIndex, rawPath, quote, methods) => {
|
|
555
|
+
const path = parseQuotedLiteral(`${quote}${rawPath ?? ""}${quote}`);
|
|
556
|
+
if (path === undefined || !path.startsWith("/") || path.startsWith("//"))
|
|
557
|
+
return;
|
|
558
|
+
if (facts !== undefined) {
|
|
559
|
+
const source = facts.parse(file, content);
|
|
560
|
+
// Same contract as a member-call registration: a parsed source is authoritative, an
|
|
561
|
+
// unparseable one keeps the lexical finding.
|
|
562
|
+
if (source !== undefined &&
|
|
563
|
+
facts.isFunctionRouteCallAt(source, index, callee, pathIndex, path) !== true)
|
|
564
|
+
return;
|
|
565
|
+
}
|
|
566
|
+
const line = lineAt(content, index);
|
|
567
|
+
const snippet = (lines[line - 1] ?? "").trim();
|
|
568
|
+
for (const method of methods)
|
|
569
|
+
out.push({ file, line, snippet, method, path });
|
|
570
|
+
};
|
|
571
|
+
ALL_ROUTES.lastIndex = 0;
|
|
572
|
+
for (let m = ALL_ROUTES.exec(code); m !== null; m = ALL_ROUTES.exec(code)) {
|
|
573
|
+
const double = m[1] !== undefined;
|
|
574
|
+
collect(m.index, "all", 0, double ? m[1] : m[2], double ? '"' : "'", [...HTTP_VERBS]);
|
|
575
|
+
}
|
|
576
|
+
METHOD_ROUTES.lastIndex = 0;
|
|
577
|
+
for (let m = METHOD_ROUTES.exec(code); m !== null; m = METHOD_ROUTES.exec(code)) {
|
|
578
|
+
const names = new Set();
|
|
579
|
+
METHOD_NAME_LITERAL.lastIndex = 0;
|
|
580
|
+
for (let name = METHOD_NAME_LITERAL.exec(m[1] ?? ""); name !== null; name = METHOD_NAME_LITERAL.exec(m[1] ?? "")) {
|
|
581
|
+
names.add((name[2] ?? "").toUpperCase());
|
|
582
|
+
}
|
|
583
|
+
const double = m[2] !== undefined;
|
|
584
|
+
collect(m.index, "method", 1, double ? m[2] : m[3], double ? '"' : "'", [...names]);
|
|
585
|
+
}
|
|
586
|
+
return out;
|
|
587
|
+
}
|
|
472
588
|
/**
|
|
473
589
|
* SQL built by interpolating a value into the statement text. The value is not a parameter, it is
|
|
474
590
|
* STATEMENT, so anything the caller controls can end the literal and continue as SQL.
|
|
@@ -578,8 +694,16 @@ export const createSqlImports = (load) => ({
|
|
|
578
694
|
* a typecheck failure; parser recovery nodes are not evidence of anything, so a file that fails to
|
|
579
695
|
* parse contributes no findings and proves no constant. */
|
|
580
696
|
function parseSqlSource(ts, file, content) {
|
|
581
|
-
const kind =
|
|
582
|
-
|
|
697
|
+
const kind = scriptKindOf(ts, file);
|
|
698
|
+
let source;
|
|
699
|
+
try {
|
|
700
|
+
source = ts.createSourceFile(file, content, ts.ScriptTarget.Latest, true, kind);
|
|
701
|
+
}
|
|
702
|
+
catch {
|
|
703
|
+
// TypeScript 7 parses only the files its session preloaded; an imported module outside that set
|
|
704
|
+
// proves no constant, like one that does not parse.
|
|
705
|
+
return undefined;
|
|
706
|
+
}
|
|
583
707
|
const parseDiagnostics = source.parseDiagnostics;
|
|
584
708
|
return parseDiagnostics !== undefined && parseDiagnostics.length > 0 ? undefined : source;
|
|
585
709
|
}
|
|
@@ -1086,13 +1210,27 @@ export function scanInterpolatedSql(file, content, ts, imports) {
|
|
|
1086
1210
|
* answer a security rule must never give when it did not run.
|
|
1087
1211
|
*/
|
|
1088
1212
|
export async function scanProjectSql(cwd) {
|
|
1089
|
-
const
|
|
1090
|
-
|
|
1213
|
+
const sourceFiles = [];
|
|
1214
|
+
await walkSource(cwd, (file, content) => sourceFiles.push({ file, content }));
|
|
1215
|
+
const contents = new Map(sourceFiles.map(({ file, content }) => [file, content]));
|
|
1216
|
+
const sourceIndex = {
|
|
1217
|
+
files: sourceFiles.map(({ file }) => file),
|
|
1218
|
+
read: (file) => contents.get(file),
|
|
1219
|
+
};
|
|
1220
|
+
const loaded = await loadProjectTypeScript(cwd, undefined, sourceIndex);
|
|
1221
|
+
if (loaded.compiler === undefined)
|
|
1091
1222
|
return undefined;
|
|
1092
1223
|
const out = [];
|
|
1093
1224
|
const imports = createProjectSqlImports(cwd);
|
|
1094
|
-
|
|
1095
|
-
|
|
1225
|
+
try {
|
|
1226
|
+
for (const { file, content } of sourceFiles) {
|
|
1227
|
+
out.push(...scanInterpolatedSql(file, content, loaded.compiler, imports));
|
|
1228
|
+
}
|
|
1229
|
+
return out.sort(bySite);
|
|
1230
|
+
}
|
|
1231
|
+
finally {
|
|
1232
|
+
await loaded.session?.close();
|
|
1233
|
+
}
|
|
1096
1234
|
}
|
|
1097
1235
|
/** A declaration file. Excluded from cross-module resolution: `declare const X: string` has no
|
|
1098
1236
|
* initializer to read, so following one could only ever produce a "proved" the source never wrote. */
|
|
@@ -1184,6 +1322,16 @@ export const REMOVED_IMPORTS = [
|
|
|
1184
1322
|
since: "2.0",
|
|
1185
1323
|
replacement: 'import from "@nifrajs/core/budget" - the package folded into core and npm `latest` is still 1.13.0, so a `^2` range resolves to nothing',
|
|
1186
1324
|
},
|
|
1325
|
+
{
|
|
1326
|
+
specifier: "@nifrajs/web/server-only",
|
|
1327
|
+
since: "4.0",
|
|
1328
|
+
replacement: 'import "@nifrajs/web/backend-only" - and keep backend code under backend/ or in a route\'s x.backend.ts, where no browser build reaches it',
|
|
1329
|
+
},
|
|
1330
|
+
{
|
|
1331
|
+
specifier: "@nifrajs/web/plugins/vite-server-only",
|
|
1332
|
+
since: "4.0",
|
|
1333
|
+
replacement: "nothing - buildClientVite and the Vite dev server enforce the frontend/backend zones themselves; for a hand-built Vite config use viteLeakGuard from @nifrajs/web/plugins/vite-leak-guard",
|
|
1334
|
+
},
|
|
1187
1335
|
{
|
|
1188
1336
|
specifier: "@nifrajs/core/ws",
|
|
1189
1337
|
since: "2.0",
|
|
@@ -1199,12 +1347,13 @@ function isSideEffectImport(content, index) {
|
|
|
1199
1347
|
// no `from` in between.
|
|
1200
1348
|
return /^import\s*["'`]/.test(content.slice(index, index + 32));
|
|
1201
1349
|
}
|
|
1202
|
-
/** Flag an import of a specifier that no longer exists
|
|
1203
|
-
* of it, so `@nifrajs/budget/x`
|
|
1350
|
+
/** Flag an import of a specifier that no longer exists, or of a name that moved to a subpath
|
|
1351
|
+
* ({@link MOVED_EXPORTS}). Pure; matches the exact specifier or a subpath of it, so `@nifrajs/budget/x`
|
|
1352
|
+
* is caught alongside `@nifrajs/budget`. */
|
|
1204
1353
|
export function scanRemovedImports(file, content) {
|
|
1205
1354
|
const out = [];
|
|
1206
1355
|
const lines = content.split("\n");
|
|
1207
|
-
for (const edge of
|
|
1356
|
+
for (const edge of importEdges(content)) {
|
|
1208
1357
|
const removed = REMOVED_IMPORTS.find((entry) => edge.specifier === entry.specifier || edge.specifier.startsWith(`${entry.specifier}/`));
|
|
1209
1358
|
if (removed === undefined)
|
|
1210
1359
|
continue;
|
|
@@ -1213,33 +1362,89 @@ export function scanRemovedImports(file, content) {
|
|
|
1213
1362
|
const line = lineAt(content, edge.index);
|
|
1214
1363
|
out.push({ file, line, snippet: (lines[line - 1] ?? "").trim() });
|
|
1215
1364
|
}
|
|
1365
|
+
// A moved name's snippet is the whole statement on one line: the rule re-reads the site from it.
|
|
1366
|
+
for (const site of movedExportSites(content)) {
|
|
1367
|
+
const snippet = content.slice(site.index, site.end).replace(/\s+/g, " ");
|
|
1368
|
+
out.push({ file, line: lineAt(content, site.index), snippet });
|
|
1369
|
+
}
|
|
1370
|
+
return out;
|
|
1371
|
+
}
|
|
1372
|
+
/**
|
|
1373
|
+
* Named exports that left a module for one of its subpaths, and where they went.
|
|
1374
|
+
*
|
|
1375
|
+
* Unlike a removed specifier, a moved name does fail the typecheck - but that error names neither the
|
|
1376
|
+
* new home nor a fix. Listing it here gives the diagnostic both, and `nifra fix --code NF-C005`
|
|
1377
|
+
* rewrites the import ({@link rewriteMovedExports}).
|
|
1378
|
+
*/
|
|
1379
|
+
export const MOVED_EXPORTS = [
|
|
1380
|
+
// Compiler plugins sit on their own subpath so the adapter root, which every server and edge bundle
|
|
1381
|
+
// links, never pulls in build-time code.
|
|
1382
|
+
{ from: "@nifrajs/web-solid", names: ["solidBunPlugin"], to: "@nifrajs/web-solid/plugin" },
|
|
1383
|
+
{ from: "@nifrajs/web-svelte", names: ["svelteBunPlugin"], to: "@nifrajs/web-svelte/plugin" },
|
|
1384
|
+
];
|
|
1385
|
+
/** `import { a, b as c } from "x"` and `export { a } from "x"` - the forms a moved name is reached by.
|
|
1386
|
+
* `import type` / `export type` never match, so a type-only import is left to tsc as elsewhere here. */
|
|
1387
|
+
const NAMED_FROM = /\b(import|export)\s*\{([^{}]*)\}\s*from\s*(["'])([^"'\n]+)\3;?/g;
|
|
1388
|
+
/** Every import/re-export statement naming a {@link MOVED_EXPORTS} binding, in source order. Pure. */
|
|
1389
|
+
export function movedExportSites(content) {
|
|
1390
|
+
const positions = codePositionMask(content);
|
|
1391
|
+
const sites = [];
|
|
1392
|
+
const re = new RegExp(NAMED_FROM.source, NAMED_FROM.flags);
|
|
1393
|
+
for (let m = re.exec(content); m !== null; m = re.exec(content)) {
|
|
1394
|
+
if (positions[m.index] === " ")
|
|
1395
|
+
continue;
|
|
1396
|
+
const entry = MOVED_EXPORTS.find((candidate) => candidate.from === m?.[4]);
|
|
1397
|
+
if (entry === undefined)
|
|
1398
|
+
continue;
|
|
1399
|
+
const bindings = (m[2] ?? "")
|
|
1400
|
+
.split(",")
|
|
1401
|
+
.map((binding) => binding.trim())
|
|
1402
|
+
.filter((binding) => binding !== "");
|
|
1403
|
+
const moves = (binding) => !binding.startsWith("type ") && entry.names.includes(binding.split(/\s+/)[0] ?? "");
|
|
1404
|
+
const moved = bindings.filter(moves);
|
|
1405
|
+
if (moved.length === 0)
|
|
1406
|
+
continue;
|
|
1407
|
+
sites.push({
|
|
1408
|
+
index: m.index,
|
|
1409
|
+
end: m.index + m[0].length,
|
|
1410
|
+
keyword: m[1] ?? "import",
|
|
1411
|
+
quote: m[3] ?? '"',
|
|
1412
|
+
from: entry.from,
|
|
1413
|
+
to: entry.to,
|
|
1414
|
+
kept: bindings.filter((binding) => !moves(binding)),
|
|
1415
|
+
moved,
|
|
1416
|
+
});
|
|
1417
|
+
}
|
|
1418
|
+
return sites;
|
|
1419
|
+
}
|
|
1420
|
+
/** Point every moved binding at the module it moved to, leaving the rest of its statement in place.
|
|
1421
|
+
* Returns `content` unchanged when nothing moved, so applying it twice is a no-op. Pure. */
|
|
1422
|
+
export function rewriteMovedExports(content) {
|
|
1423
|
+
let out = content;
|
|
1424
|
+
for (const site of movedExportSites(content).reverse()) {
|
|
1425
|
+
const semicolon = content[site.end - 1] === ";" ? ";" : "";
|
|
1426
|
+
const statement = (bindings, from) => `${site.keyword} { ${bindings.join(", ")} } from ${site.quote}${from}${site.quote}${semicolon}`;
|
|
1427
|
+
const replacement = site.kept.length === 0
|
|
1428
|
+
? statement(site.moved, site.to)
|
|
1429
|
+
: `${statement(site.kept, site.from)}\n${statement(site.moved, site.to)}`;
|
|
1430
|
+
out = out.slice(0, site.index) + replacement + out.slice(site.end);
|
|
1431
|
+
}
|
|
1216
1432
|
return out;
|
|
1217
1433
|
}
|
|
1218
|
-
/** Scan a
|
|
1219
|
-
*
|
|
1220
|
-
* the offending `specifier` so the diagnostic can render the `
|
|
1434
|
+
/** Scan a browser module for server-only imports, static or a literal dynamic `import()`. Returns `[]`
|
|
1435
|
+
* for server code ({@link isBrowserSource}), where a server-only import is fine. Each finding carries
|
|
1436
|
+
* the offending `specifier` so the diagnostic can render the `file → specifier` chain. Pure. */
|
|
1221
1437
|
export function scanServerOnlyImports(file, content, facts) {
|
|
1222
|
-
if (!
|
|
1438
|
+
if (!isBrowserSource(file))
|
|
1223
1439
|
return [];
|
|
1224
1440
|
const out = [];
|
|
1225
1441
|
const lines = content.split("\n");
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
for (let m = STATIC_IMPORT.exec(code); m !== null; m = STATIC_IMPORT.exec(code)) {
|
|
1230
|
-
if (positions[m.index] === " ")
|
|
1231
|
-
continue;
|
|
1232
|
-
const specifier = m[1] ?? "";
|
|
1442
|
+
// Inline `import { type X } from "…"` is erased just like `import type` and is not an edge; a parse
|
|
1443
|
+
// failure keeps the lexical edge so the security rule fails closed.
|
|
1444
|
+
for (const { specifier, index } of importEdges(content, facts, file)) {
|
|
1233
1445
|
if (!SERVER_ONLY.test(specifier))
|
|
1234
1446
|
continue;
|
|
1235
|
-
|
|
1236
|
-
const source = facts.parse(file, content);
|
|
1237
|
-
// Inline `import { type X } from "…"` is erased just like `import type`; don't call it a
|
|
1238
|
-
// runtime leak. A parse failure keeps the old lexical finding so the security rule fails closed.
|
|
1239
|
-
if (source !== undefined && facts.isValueImportAt(source, m.index, specifier) === false)
|
|
1240
|
-
continue;
|
|
1241
|
-
}
|
|
1242
|
-
const line = lineAt(content, m.index);
|
|
1447
|
+
const line = lineAt(content, index);
|
|
1243
1448
|
out.push({ file, line, snippet: (lines[line - 1] ?? "").trim(), specifier });
|
|
1244
1449
|
}
|
|
1245
1450
|
return out;
|
|
@@ -1257,11 +1462,11 @@ export function scanServerOnlyImports(file, content, facts) {
|
|
|
1257
1462
|
// npm package. These are never local source we can walk into, so the chain ends here. (Same vocabulary
|
|
1258
1463
|
// as SERVER_ONLY, minus the relative `../db` arm - a relative `db` module IS local source we resolve.)
|
|
1259
1464
|
const SERVER_ONLY_SINK = /^(?:node:|bun:)|^(?:postgres|pg|mysql2|ioredis|redis|better-sqlite3|mongodb|@libsql\/client)$|^drizzle-orm\/(?:node-postgres|postgres-js|bun-sqlite|libsql|mysql2|pglite)\b/;
|
|
1260
|
-
// The
|
|
1261
|
-
const
|
|
1262
|
-
// The explicit poison-import marker (`@nifrajs/web/server-only`) - a
|
|
1263
|
-
// guard. A resolved file
|
|
1264
|
-
const
|
|
1465
|
+
// The retired `*.server` convention. Builds refuse such a module in browser code rather than empty it.
|
|
1466
|
+
const LEGACY_SERVER_FILE = /\.server(\.[cm]?[jt]sx?)?$/;
|
|
1467
|
+
// The explicit poison-import marker (`@nifrajs/web/backend-only`, or its retired `server-only` name) - a
|
|
1468
|
+
// module opting into the client-leak guard. A resolved file carrying this side-effect import is a sink.
|
|
1469
|
+
const BACKEND_ONLY_MARKER_IMPORT = /import\s+["']@nifrajs\/web\/(?:backend|server)-only["']/;
|
|
1265
1470
|
// Depth/visited caps keep the walk linear + cycle-safe. A route's server-only dependency sits within a
|
|
1266
1471
|
// few hops in practice; the bound stops a pathological graph from blowing up the per-file scan.
|
|
1267
1472
|
const TRANSITIVE_MAX_DEPTH = 8;
|
|
@@ -1278,11 +1483,10 @@ const staticImportRegex = () => new RegExp(STATIC_IMPORT.source, STATIC_IMPORT.f
|
|
|
1278
1483
|
* each with the match index (for line attribution). Mirrors {@link STATIC_IMPORT}, so `import type` +
|
|
1279
1484
|
* dynamic `import()` are already excluded. Uses a fresh regex instance, so it's safe under the reentrant
|
|
1280
1485
|
* transitive walk. Pure. */
|
|
1281
|
-
function staticImportEdges(content, facts, file) {
|
|
1486
|
+
function staticImportEdges(content, facts, file, positions = codePositionMask(content)) {
|
|
1282
1487
|
const edges = [];
|
|
1283
1488
|
const re = staticImportRegex();
|
|
1284
1489
|
const code = stripComments(content);
|
|
1285
|
-
const positions = codePositionMask(content);
|
|
1286
1490
|
for (let m = re.exec(code); m !== null; m = re.exec(code)) {
|
|
1287
1491
|
if (positions[m.index] === " ")
|
|
1288
1492
|
continue;
|
|
@@ -1302,6 +1506,54 @@ function staticImportEdges(content, facts, file) {
|
|
|
1302
1506
|
export function parseStaticImports(content, facts, file) {
|
|
1303
1507
|
return staticImportEdges(content, facts, file).map((e) => e.specifier);
|
|
1304
1508
|
}
|
|
1509
|
+
/** The literal dynamic `import("x")` calls in executable code, skipping type positions. With `facts`
|
|
1510
|
+
* the parser decides; without it (or when it cannot tell) the lexical rule does. Pure. */
|
|
1511
|
+
function dynamicImportEdges(content, facts, file, positions = codePositionMask(content)) {
|
|
1512
|
+
const edges = [];
|
|
1513
|
+
const re = new RegExp(DYNAMIC_IMPORT.source, DYNAMIC_IMPORT.flags);
|
|
1514
|
+
for (let m = re.exec(content); m !== null; m = re.exec(content)) {
|
|
1515
|
+
if (positions[m.index] === " ")
|
|
1516
|
+
continue; // inside a comment or a string
|
|
1517
|
+
const specifier = m[2];
|
|
1518
|
+
if (specifier === undefined)
|
|
1519
|
+
continue;
|
|
1520
|
+
const source = facts !== undefined && file !== undefined ? facts.parse(file, content) : undefined;
|
|
1521
|
+
const verdict = source === undefined || facts === undefined
|
|
1522
|
+
? undefined
|
|
1523
|
+
: facts.isDynamicImportAt(source, m.index, specifier);
|
|
1524
|
+
if (verdict === false)
|
|
1525
|
+
continue;
|
|
1526
|
+
if (verdict === undefined) {
|
|
1527
|
+
if (TYPE_QUERY_BEFORE.test(content.slice(Math.max(0, m.index - 16), m.index)))
|
|
1528
|
+
continue;
|
|
1529
|
+
const end = m.index + m[0].length;
|
|
1530
|
+
if (m[0].endsWith(")") && TYPE_QUALIFIER_AFTER.test(content.slice(end, end + 64)))
|
|
1531
|
+
continue;
|
|
1532
|
+
}
|
|
1533
|
+
edges.push({ specifier, index: m.index });
|
|
1534
|
+
}
|
|
1535
|
+
return edges;
|
|
1536
|
+
}
|
|
1537
|
+
/** Every import a module's runtime code makes - static and literal dynamic - in source order. Pure. */
|
|
1538
|
+
function importEdges(content, facts, file) {
|
|
1539
|
+
// Without an `import(` there is nothing for the dynamic pass to find; skip its regex.
|
|
1540
|
+
const positions = codePositionMask(content);
|
|
1541
|
+
const edges = staticImportEdges(content, facts, file, positions);
|
|
1542
|
+
if (!content.includes("import(") && !/import\s+\(/.test(content))
|
|
1543
|
+
return edges;
|
|
1544
|
+
return [...edges, ...dynamicImportEdges(content, facts, file, positions)].sort((a, b) => a.index - b.index);
|
|
1545
|
+
}
|
|
1546
|
+
/** The runtime imports of a module with the line each sits on, in source order. Pure. */
|
|
1547
|
+
export function importSites(content, file) {
|
|
1548
|
+
return importEdges(content, undefined, file).map(({ specifier, index }) => ({
|
|
1549
|
+
specifier,
|
|
1550
|
+
line: lineAt(content, index),
|
|
1551
|
+
}));
|
|
1552
|
+
}
|
|
1553
|
+
/** The specifiers {@link importEdges} finds, static and literal dynamic, in source order. Pure. */
|
|
1554
|
+
export function parseImports(content, facts, file) {
|
|
1555
|
+
return importEdges(content, facts, file).map((e) => e.specifier);
|
|
1556
|
+
}
|
|
1305
1557
|
/** The server-only SINK an import specifier names directly (a `node:`/`bun:` builtin or a known
|
|
1306
1558
|
* server-only package), or `undefined` if it isn't a by-name sink. Pure - the label is the specifier
|
|
1307
1559
|
* itself (it's already the actionable name). */
|
|
@@ -1311,10 +1563,11 @@ export function directSinkSpecifier(spec) {
|
|
|
1311
1563
|
/**
|
|
1312
1564
|
* BFS the LOCAL module graph from a route file for the SHORTEST import chain that reaches a server-only
|
|
1313
1565
|
* sink, returning `[routeFile, …as-written specifiers…, sink]` or `undefined` if none is reachable. A
|
|
1314
|
-
* node's outgoing edges are its static imports; an edge is followed only when it's a RELATIVE specifier
|
|
1566
|
+
* node's outgoing edges are its static and literal dynamic imports; an edge is followed only when it's a RELATIVE specifier
|
|
1315
1567
|
* that `resolve` maps to a readable local file (so the walk never descends into node_modules or chases an
|
|
1316
1568
|
* unresolvable alias). At each node, a by-name sink import (`node:fs`, `postgres`) OR a resolved
|
|
1317
|
-
*
|
|
1569
|
+
* `server-only`-marked dependency terminates the chain; a `*.server` dependency is never entered, since
|
|
1570
|
+
* the client build empties it. Bounded by depth + a visited set, so
|
|
1318
1571
|
* it's linear and cycle-free. `routeFile`/`routeContent` seed the walk; `resolve`/`read` supply the graph
|
|
1319
1572
|
* - pure given those, so it's unit-testable with a fake graph.
|
|
1320
1573
|
*/
|
|
@@ -1327,7 +1580,7 @@ export function walkServerOnlyChain(routeFile, routeContent, resolve, read, fact
|
|
|
1327
1580
|
for (const node of frontier) {
|
|
1328
1581
|
if (node.depth >= TRANSITIVE_MAX_DEPTH)
|
|
1329
1582
|
continue;
|
|
1330
|
-
for (const spec of
|
|
1583
|
+
for (const spec of parseImports(node.content, facts, node.abs)) {
|
|
1331
1584
|
// (a) A by-name sink (builtin / known server-only pkg) → the chain ends here (shortest first,
|
|
1332
1585
|
// since BFS reaches the nearest sink before any deeper one).
|
|
1333
1586
|
const sink = directSinkSpecifier(spec);
|
|
@@ -1340,15 +1593,17 @@ export function walkServerOnlyChain(routeFile, routeContent, resolve, read, fact
|
|
|
1340
1593
|
const abs = resolve(node.abs, spec);
|
|
1341
1594
|
if (abs === undefined || seen.has(abs))
|
|
1342
1595
|
continue;
|
|
1343
|
-
// (c)
|
|
1344
|
-
//
|
|
1345
|
-
if (
|
|
1596
|
+
// (c) Backend code is a sink: the browser build refuses it. A `*.fn.ts` module ships as its
|
|
1597
|
+
// generated stub, so its imports never reach the browser and the walk does not enter it.
|
|
1598
|
+
if (isBackendModule(abs))
|
|
1346
1599
|
return [...node.chain, spec];
|
|
1600
|
+
if (SERVER_FN_FILE.test(abs))
|
|
1601
|
+
continue;
|
|
1347
1602
|
const content = read(abs);
|
|
1348
1603
|
if (content === undefined)
|
|
1349
1604
|
continue; // unreadable → can't walk; treat as a leaf
|
|
1350
|
-
// (d) A resolved module that opts into the `
|
|
1351
|
-
if (
|
|
1605
|
+
// (d) A resolved module that opts into the `backend-only` marker is a sink too.
|
|
1606
|
+
if (BACKEND_ONLY_MARKER_IMPORT.test(content))
|
|
1352
1607
|
return [...node.chain, spec];
|
|
1353
1608
|
if (visited >= TRANSITIVE_MAX_VISITED)
|
|
1354
1609
|
continue;
|
|
@@ -1370,7 +1625,7 @@ export function walkServerOnlyChain(routeFile, routeContent, resolve, read, fact
|
|
|
1370
1625
|
* de-duplicated to the shortest chain per first-hop specifier. Non-route files yield `[]`.
|
|
1371
1626
|
*/
|
|
1372
1627
|
export function resolveServerOnlyChains(file, content, resolve, read, facts) {
|
|
1373
|
-
if (!
|
|
1628
|
+
if (!isBrowserSource(file))
|
|
1374
1629
|
return [];
|
|
1375
1630
|
const lines = content.split("\n");
|
|
1376
1631
|
const out = [];
|
|
@@ -1378,7 +1633,7 @@ export function resolveServerOnlyChains(file, content, resolve, read, facts) {
|
|
|
1378
1633
|
// Collect the route's import edges up front (fresh-regex scan) so the per-edge logic below can call
|
|
1379
1634
|
// the REENTRANT transitive walk without corrupting a shared regex's `lastIndex` (the walk also scans
|
|
1380
1635
|
// imports). Driving `STATIC_IMPORT.exec` here directly would restart this loop forever.
|
|
1381
|
-
for (const { specifier, index } of
|
|
1636
|
+
for (const { specifier, index } of importEdges(content, facts, file)) {
|
|
1382
1637
|
if (flaggedSpecifiers.has(specifier))
|
|
1383
1638
|
continue;
|
|
1384
1639
|
const line = lineAt(content, index);
|
|
@@ -1404,16 +1659,18 @@ export function resolveServerOnlyChains(file, content, resolve, read, facts) {
|
|
|
1404
1659
|
}
|
|
1405
1660
|
continue;
|
|
1406
1661
|
}
|
|
1407
|
-
//
|
|
1408
|
-
if (
|
|
1662
|
+
// A backend first hop and a marked module are sinks; a `*.fn.ts` hop ships as its stub.
|
|
1663
|
+
if (isBackendModule(abs)) {
|
|
1409
1664
|
flaggedSpecifiers.add(specifier);
|
|
1410
1665
|
out.push({ file, line, snippet, specifier, chain: [file, specifier], fallback: false });
|
|
1411
1666
|
continue;
|
|
1412
1667
|
}
|
|
1668
|
+
if (SERVER_FN_FILE.test(abs))
|
|
1669
|
+
continue;
|
|
1413
1670
|
const depContent = read(abs);
|
|
1414
1671
|
if (depContent === undefined)
|
|
1415
1672
|
continue;
|
|
1416
|
-
if (
|
|
1673
|
+
if (BACKEND_ONLY_MARKER_IMPORT.test(depContent)) {
|
|
1417
1674
|
flaggedSpecifiers.add(specifier);
|
|
1418
1675
|
out.push({ file, line, snippet, specifier, chain: [file, specifier], fallback: false });
|
|
1419
1676
|
continue;
|
|
@@ -1525,17 +1782,18 @@ export async function walkSource(cwd, visit, opts = {}) {
|
|
|
1525
1782
|
const rels = [];
|
|
1526
1783
|
for await (const rawRel of new Glob("**/*.{ts,tsx,mts,cts}").scan({ cwd, dot: false })) {
|
|
1527
1784
|
const rel = normalizeProjectPath(rawRel);
|
|
1528
|
-
if (!skip.test(rel))
|
|
1785
|
+
if (!skip.test(rel) && (opts.ignore === undefined || !opts.ignore(rel)))
|
|
1529
1786
|
rels.push(rel);
|
|
1530
1787
|
}
|
|
1531
1788
|
const ignored = await gitIgnored(cwd, rels);
|
|
1532
|
-
|
|
1789
|
+
// Glob order is the filesystem's, so findings would come out in a different order on each platform.
|
|
1790
|
+
for (const rel of rels.sort(codeUnitOrder)) {
|
|
1533
1791
|
if (ignored.has(rel))
|
|
1534
1792
|
continue;
|
|
1535
1793
|
visit(rel, await Bun.file(join(cwd, rel)).text());
|
|
1536
1794
|
}
|
|
1537
1795
|
}
|
|
1538
|
-
const bySite = (a, b) => a.file
|
|
1796
|
+
const bySite = (a, b) => codeUnitOrder(a.file, b.file) || a.line - b.line;
|
|
1539
1797
|
/** Collect own-API `fetch()` findings across the project. */
|
|
1540
1798
|
export async function scanProject(cwd) {
|
|
1541
1799
|
const out = [];
|
|
@@ -1552,8 +1810,11 @@ const GENERATED_MARKER = "GENERATED by @nifrajs/web generateServerManifest";
|
|
|
1552
1810
|
// The first route-import specifier's prefix up to `routes/` (e.g. `./`, `../`, `./src/`) - used to
|
|
1553
1811
|
// locate the routes dir relative to the manifest, and to strip to route-relative keys.
|
|
1554
1812
|
const ROUTES_PREFIX = /["'](\.{1,2}(?:\/[^"'/]+)*?\/routes\/)[^"']+["']/;
|
|
1555
|
-
//
|
|
1813
|
+
// The files route discovery collects (mirrors `@nifrajs/web/fs`'s filter): route files by extension,
|
|
1814
|
+
// each route's backend half, and `_middleware` modules.
|
|
1556
1815
|
const ROUTE_FILE_EXT = /\.(tsx|jsx|svelte|vue|mdx)$/;
|
|
1816
|
+
const ROUTE_BACKEND_HALF = /\.backend\.(?:[cm]?[jt]s)$/;
|
|
1817
|
+
const MIDDLEWARE_FILE = /(?:^|\/)_middleware\.(?:ts|js)$/;
|
|
1557
1818
|
/**
|
|
1558
1819
|
* Scan the project for committed, generated `server-manifest.ts` files and report any that have drifted
|
|
1559
1820
|
* from the live `routes/` tree. For each, the routes dir is derived from the manifest's own import
|
|
@@ -1582,9 +1843,9 @@ export async function scanServerManifestDrift(cwd) {
|
|
|
1582
1843
|
const routesDir = join(cwd, manifestDir, routesPrefix);
|
|
1583
1844
|
let discovered;
|
|
1584
1845
|
try {
|
|
1585
|
-
discovered = (await Array.fromAsync(new Glob("**/*.{tsx,jsx,svelte,vue,mdx}").scan({ cwd: routesDir })))
|
|
1846
|
+
discovered = (await Array.fromAsync(new Glob("**/*.{tsx,jsx,svelte,vue,mdx,ts,js}").scan({ cwd: routesDir })))
|
|
1586
1847
|
.map((f) => f.replaceAll("\\", "/"))
|
|
1587
|
-
.filter((f) => ROUTE_FILE_EXT.test(f));
|
|
1848
|
+
.filter((f) => ROUTE_FILE_EXT.test(f) || ROUTE_BACKEND_HALF.test(f) || MIDDLEWARE_FILE.test(f));
|
|
1588
1849
|
}
|
|
1589
1850
|
catch {
|
|
1590
1851
|
continue; // routes dir gone/unreadable - not a drift we can assess
|
|
@@ -1594,6 +1855,6 @@ export async function scanServerManifestDrift(cwd) {
|
|
|
1594
1855
|
findings.push({ file: rel, missing: drift.missing, extra: drift.extra });
|
|
1595
1856
|
}
|
|
1596
1857
|
}
|
|
1597
|
-
return findings.sort((a, b) => a.file
|
|
1858
|
+
return findings.sort((a, b) => codeUnitOrder(a.file, b.file));
|
|
1598
1859
|
}
|
|
1599
1860
|
//# sourceMappingURL=check-scan.js.map
|