@nifrajs/cli 3.5.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 +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 +57 -5
- package/dist/command-catalog.d.ts.map +1 -1
- package/dist/command-catalog.js +270 -26
- 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 +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 +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 +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 +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 +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.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.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 +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 +374 -38
- 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 +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 +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 +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 +5 -0
- 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 +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 +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 +13 -0
- package/src/verification.ts +50 -12
- package/src/web-app-options.ts +57 -0
- package/src/work-graph.ts +27 -13
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;
|
|
@@ -499,8 +544,47 @@ export function scanStaticRouteText(file, content, facts) {
|
|
|
499
544
|
return [
|
|
500
545
|
...scanRoutePattern(file, content, code, ROUTE_REGISTRATION_DQ, facts),
|
|
501
546
|
...scanRoutePattern(file, content, code, ROUTE_REGISTRATION_SQ, facts),
|
|
547
|
+
...(code.includes(METHODS_SUBPATH) ? scanMethodRoutes(file, content, code, facts) : []),
|
|
502
548
|
].sort(bySite);
|
|
503
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
|
+
}
|
|
504
588
|
/**
|
|
505
589
|
* SQL built by interpolating a value into the statement text. The value is not a parameter, it is
|
|
506
590
|
* STATEMENT, so anything the caller controls can end the literal and continue as SQL.
|
|
@@ -610,8 +694,16 @@ export const createSqlImports = (load) => ({
|
|
|
610
694
|
* a typecheck failure; parser recovery nodes are not evidence of anything, so a file that fails to
|
|
611
695
|
* parse contributes no findings and proves no constant. */
|
|
612
696
|
function parseSqlSource(ts, file, content) {
|
|
613
|
-
const kind =
|
|
614
|
-
|
|
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
|
+
}
|
|
615
707
|
const parseDiagnostics = source.parseDiagnostics;
|
|
616
708
|
return parseDiagnostics !== undefined && parseDiagnostics.length > 0 ? undefined : source;
|
|
617
709
|
}
|
|
@@ -1118,13 +1210,27 @@ export function scanInterpolatedSql(file, content, ts, imports) {
|
|
|
1118
1210
|
* answer a security rule must never give when it did not run.
|
|
1119
1211
|
*/
|
|
1120
1212
|
export async function scanProjectSql(cwd) {
|
|
1121
|
-
const
|
|
1122
|
-
|
|
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)
|
|
1123
1222
|
return undefined;
|
|
1124
1223
|
const out = [];
|
|
1125
1224
|
const imports = createProjectSqlImports(cwd);
|
|
1126
|
-
|
|
1127
|
-
|
|
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
|
+
}
|
|
1128
1234
|
}
|
|
1129
1235
|
/** A declaration file. Excluded from cross-module resolution: `declare const X: string` has no
|
|
1130
1236
|
* initializer to read, so following one could only ever produce a "proved" the source never wrote. */
|
|
@@ -1216,6 +1322,16 @@ export const REMOVED_IMPORTS = [
|
|
|
1216
1322
|
since: "2.0",
|
|
1217
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',
|
|
1218
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
|
+
},
|
|
1219
1335
|
{
|
|
1220
1336
|
specifier: "@nifrajs/core/ws",
|
|
1221
1337
|
since: "2.0",
|
|
@@ -1231,12 +1347,13 @@ function isSideEffectImport(content, index) {
|
|
|
1231
1347
|
// no `from` in between.
|
|
1232
1348
|
return /^import\s*["'`]/.test(content.slice(index, index + 32));
|
|
1233
1349
|
}
|
|
1234
|
-
/** Flag an import of a specifier that no longer exists
|
|
1235
|
-
* 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`. */
|
|
1236
1353
|
export function scanRemovedImports(file, content) {
|
|
1237
1354
|
const out = [];
|
|
1238
1355
|
const lines = content.split("\n");
|
|
1239
|
-
for (const edge of
|
|
1356
|
+
for (const edge of importEdges(content)) {
|
|
1240
1357
|
const removed = REMOVED_IMPORTS.find((entry) => edge.specifier === entry.specifier || edge.specifier.startsWith(`${entry.specifier}/`));
|
|
1241
1358
|
if (removed === undefined)
|
|
1242
1359
|
continue;
|
|
@@ -1245,33 +1362,89 @@ export function scanRemovedImports(file, content) {
|
|
|
1245
1362
|
const line = lineAt(content, edge.index);
|
|
1246
1363
|
out.push({ file, line, snippet: (lines[line - 1] ?? "").trim() });
|
|
1247
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
|
+
}
|
|
1248
1370
|
return out;
|
|
1249
1371
|
}
|
|
1250
|
-
/**
|
|
1251
|
-
*
|
|
1252
|
-
*
|
|
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
|
+
}
|
|
1432
|
+
return out;
|
|
1433
|
+
}
|
|
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. */
|
|
1253
1437
|
export function scanServerOnlyImports(file, content, facts) {
|
|
1254
|
-
if (!
|
|
1438
|
+
if (!isBrowserSource(file))
|
|
1255
1439
|
return [];
|
|
1256
1440
|
const out = [];
|
|
1257
1441
|
const lines = content.split("\n");
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
for (let m = STATIC_IMPORT.exec(code); m !== null; m = STATIC_IMPORT.exec(code)) {
|
|
1262
|
-
if (positions[m.index] === " ")
|
|
1263
|
-
continue;
|
|
1264
|
-
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)) {
|
|
1265
1445
|
if (!SERVER_ONLY.test(specifier))
|
|
1266
1446
|
continue;
|
|
1267
|
-
|
|
1268
|
-
const source = facts.parse(file, content);
|
|
1269
|
-
// Inline `import { type X } from "…"` is erased just like `import type`; don't call it a
|
|
1270
|
-
// runtime leak. A parse failure keeps the old lexical finding so the security rule fails closed.
|
|
1271
|
-
if (source !== undefined && facts.isValueImportAt(source, m.index, specifier) === false)
|
|
1272
|
-
continue;
|
|
1273
|
-
}
|
|
1274
|
-
const line = lineAt(content, m.index);
|
|
1447
|
+
const line = lineAt(content, index);
|
|
1275
1448
|
out.push({ file, line, snippet: (lines[line - 1] ?? "").trim(), specifier });
|
|
1276
1449
|
}
|
|
1277
1450
|
return out;
|
|
@@ -1289,11 +1462,11 @@ export function scanServerOnlyImports(file, content, facts) {
|
|
|
1289
1462
|
// npm package. These are never local source we can walk into, so the chain ends here. (Same vocabulary
|
|
1290
1463
|
// as SERVER_ONLY, minus the relative `../db` arm - a relative `db` module IS local source we resolve.)
|
|
1291
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/;
|
|
1292
|
-
// The
|
|
1293
|
-
const
|
|
1294
|
-
// The explicit poison-import marker (`@nifrajs/web/server-only`) - a
|
|
1295
|
-
// guard. A resolved file
|
|
1296
|
-
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["']/;
|
|
1297
1470
|
// Depth/visited caps keep the walk linear + cycle-safe. A route's server-only dependency sits within a
|
|
1298
1471
|
// few hops in practice; the bound stops a pathological graph from blowing up the per-file scan.
|
|
1299
1472
|
const TRANSITIVE_MAX_DEPTH = 8;
|
|
@@ -1310,11 +1483,10 @@ const staticImportRegex = () => new RegExp(STATIC_IMPORT.source, STATIC_IMPORT.f
|
|
|
1310
1483
|
* each with the match index (for line attribution). Mirrors {@link STATIC_IMPORT}, so `import type` +
|
|
1311
1484
|
* dynamic `import()` are already excluded. Uses a fresh regex instance, so it's safe under the reentrant
|
|
1312
1485
|
* transitive walk. Pure. */
|
|
1313
|
-
function staticImportEdges(content, facts, file) {
|
|
1486
|
+
function staticImportEdges(content, facts, file, positions = codePositionMask(content)) {
|
|
1314
1487
|
const edges = [];
|
|
1315
1488
|
const re = staticImportRegex();
|
|
1316
1489
|
const code = stripComments(content);
|
|
1317
|
-
const positions = codePositionMask(content);
|
|
1318
1490
|
for (let m = re.exec(code); m !== null; m = re.exec(code)) {
|
|
1319
1491
|
if (positions[m.index] === " ")
|
|
1320
1492
|
continue;
|
|
@@ -1334,6 +1506,54 @@ function staticImportEdges(content, facts, file) {
|
|
|
1334
1506
|
export function parseStaticImports(content, facts, file) {
|
|
1335
1507
|
return staticImportEdges(content, facts, file).map((e) => e.specifier);
|
|
1336
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
|
+
}
|
|
1337
1557
|
/** The server-only SINK an import specifier names directly (a `node:`/`bun:` builtin or a known
|
|
1338
1558
|
* server-only package), or `undefined` if it isn't a by-name sink. Pure - the label is the specifier
|
|
1339
1559
|
* itself (it's already the actionable name). */
|
|
@@ -1343,10 +1563,11 @@ export function directSinkSpecifier(spec) {
|
|
|
1343
1563
|
/**
|
|
1344
1564
|
* BFS the LOCAL module graph from a route file for the SHORTEST import chain that reaches a server-only
|
|
1345
1565
|
* sink, returning `[routeFile, …as-written specifiers…, sink]` or `undefined` if none is reachable. A
|
|
1346
|
-
* 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
|
|
1347
1567
|
* that `resolve` maps to a readable local file (so the walk never descends into node_modules or chases an
|
|
1348
1568
|
* unresolvable alias). At each node, a by-name sink import (`node:fs`, `postgres`) OR a resolved
|
|
1349
|
-
*
|
|
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
|
|
1350
1571
|
* it's linear and cycle-free. `routeFile`/`routeContent` seed the walk; `resolve`/`read` supply the graph
|
|
1351
1572
|
* - pure given those, so it's unit-testable with a fake graph.
|
|
1352
1573
|
*/
|
|
@@ -1359,7 +1580,7 @@ export function walkServerOnlyChain(routeFile, routeContent, resolve, read, fact
|
|
|
1359
1580
|
for (const node of frontier) {
|
|
1360
1581
|
if (node.depth >= TRANSITIVE_MAX_DEPTH)
|
|
1361
1582
|
continue;
|
|
1362
|
-
for (const spec of
|
|
1583
|
+
for (const spec of parseImports(node.content, facts, node.abs)) {
|
|
1363
1584
|
// (a) A by-name sink (builtin / known server-only pkg) → the chain ends here (shortest first,
|
|
1364
1585
|
// since BFS reaches the nearest sink before any deeper one).
|
|
1365
1586
|
const sink = directSinkSpecifier(spec);
|
|
@@ -1372,15 +1593,17 @@ export function walkServerOnlyChain(routeFile, routeContent, resolve, read, fact
|
|
|
1372
1593
|
const abs = resolve(node.abs, spec);
|
|
1373
1594
|
if (abs === undefined || seen.has(abs))
|
|
1374
1595
|
continue;
|
|
1375
|
-
// (c)
|
|
1376
|
-
//
|
|
1377
|
-
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))
|
|
1378
1599
|
return [...node.chain, spec];
|
|
1600
|
+
if (SERVER_FN_FILE.test(abs))
|
|
1601
|
+
continue;
|
|
1379
1602
|
const content = read(abs);
|
|
1380
1603
|
if (content === undefined)
|
|
1381
1604
|
continue; // unreadable → can't walk; treat as a leaf
|
|
1382
|
-
// (d) A resolved module that opts into the `
|
|
1383
|
-
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))
|
|
1384
1607
|
return [...node.chain, spec];
|
|
1385
1608
|
if (visited >= TRANSITIVE_MAX_VISITED)
|
|
1386
1609
|
continue;
|
|
@@ -1402,7 +1625,7 @@ export function walkServerOnlyChain(routeFile, routeContent, resolve, read, fact
|
|
|
1402
1625
|
* de-duplicated to the shortest chain per first-hop specifier. Non-route files yield `[]`.
|
|
1403
1626
|
*/
|
|
1404
1627
|
export function resolveServerOnlyChains(file, content, resolve, read, facts) {
|
|
1405
|
-
if (!
|
|
1628
|
+
if (!isBrowserSource(file))
|
|
1406
1629
|
return [];
|
|
1407
1630
|
const lines = content.split("\n");
|
|
1408
1631
|
const out = [];
|
|
@@ -1410,7 +1633,7 @@ export function resolveServerOnlyChains(file, content, resolve, read, facts) {
|
|
|
1410
1633
|
// Collect the route's import edges up front (fresh-regex scan) so the per-edge logic below can call
|
|
1411
1634
|
// the REENTRANT transitive walk without corrupting a shared regex's `lastIndex` (the walk also scans
|
|
1412
1635
|
// imports). Driving `STATIC_IMPORT.exec` here directly would restart this loop forever.
|
|
1413
|
-
for (const { specifier, index } of
|
|
1636
|
+
for (const { specifier, index } of importEdges(content, facts, file)) {
|
|
1414
1637
|
if (flaggedSpecifiers.has(specifier))
|
|
1415
1638
|
continue;
|
|
1416
1639
|
const line = lineAt(content, index);
|
|
@@ -1436,16 +1659,18 @@ export function resolveServerOnlyChains(file, content, resolve, read, facts) {
|
|
|
1436
1659
|
}
|
|
1437
1660
|
continue;
|
|
1438
1661
|
}
|
|
1439
|
-
//
|
|
1440
|
-
if (
|
|
1662
|
+
// A backend first hop and a marked module are sinks; a `*.fn.ts` hop ships as its stub.
|
|
1663
|
+
if (isBackendModule(abs)) {
|
|
1441
1664
|
flaggedSpecifiers.add(specifier);
|
|
1442
1665
|
out.push({ file, line, snippet, specifier, chain: [file, specifier], fallback: false });
|
|
1443
1666
|
continue;
|
|
1444
1667
|
}
|
|
1668
|
+
if (SERVER_FN_FILE.test(abs))
|
|
1669
|
+
continue;
|
|
1445
1670
|
const depContent = read(abs);
|
|
1446
1671
|
if (depContent === undefined)
|
|
1447
1672
|
continue;
|
|
1448
|
-
if (
|
|
1673
|
+
if (BACKEND_ONLY_MARKER_IMPORT.test(depContent)) {
|
|
1449
1674
|
flaggedSpecifiers.add(specifier);
|
|
1450
1675
|
out.push({ file, line, snippet, specifier, chain: [file, specifier], fallback: false });
|
|
1451
1676
|
continue;
|
|
@@ -1557,17 +1782,18 @@ export async function walkSource(cwd, visit, opts = {}) {
|
|
|
1557
1782
|
const rels = [];
|
|
1558
1783
|
for await (const rawRel of new Glob("**/*.{ts,tsx,mts,cts}").scan({ cwd, dot: false })) {
|
|
1559
1784
|
const rel = normalizeProjectPath(rawRel);
|
|
1560
|
-
if (!skip.test(rel))
|
|
1785
|
+
if (!skip.test(rel) && (opts.ignore === undefined || !opts.ignore(rel)))
|
|
1561
1786
|
rels.push(rel);
|
|
1562
1787
|
}
|
|
1563
1788
|
const ignored = await gitIgnored(cwd, rels);
|
|
1564
|
-
|
|
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)) {
|
|
1565
1791
|
if (ignored.has(rel))
|
|
1566
1792
|
continue;
|
|
1567
1793
|
visit(rel, await Bun.file(join(cwd, rel)).text());
|
|
1568
1794
|
}
|
|
1569
1795
|
}
|
|
1570
|
-
const bySite = (a, b) => a.file
|
|
1796
|
+
const bySite = (a, b) => codeUnitOrder(a.file, b.file) || a.line - b.line;
|
|
1571
1797
|
/** Collect own-API `fetch()` findings across the project. */
|
|
1572
1798
|
export async function scanProject(cwd) {
|
|
1573
1799
|
const out = [];
|
|
@@ -1584,8 +1810,11 @@ const GENERATED_MARKER = "GENERATED by @nifrajs/web generateServerManifest";
|
|
|
1584
1810
|
// The first route-import specifier's prefix up to `routes/` (e.g. `./`, `../`, `./src/`) - used to
|
|
1585
1811
|
// locate the routes dir relative to the manifest, and to strip to route-relative keys.
|
|
1586
1812
|
const ROUTES_PREFIX = /["'](\.{1,2}(?:\/[^"'/]+)*?\/routes\/)[^"']+["']/;
|
|
1587
|
-
//
|
|
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.
|
|
1588
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)$/;
|
|
1589
1818
|
/**
|
|
1590
1819
|
* Scan the project for committed, generated `server-manifest.ts` files and report any that have drifted
|
|
1591
1820
|
* from the live `routes/` tree. For each, the routes dir is derived from the manifest's own import
|
|
@@ -1614,9 +1843,9 @@ export async function scanServerManifestDrift(cwd) {
|
|
|
1614
1843
|
const routesDir = join(cwd, manifestDir, routesPrefix);
|
|
1615
1844
|
let discovered;
|
|
1616
1845
|
try {
|
|
1617
|
-
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 })))
|
|
1618
1847
|
.map((f) => f.replaceAll("\\", "/"))
|
|
1619
|
-
.filter((f) => ROUTE_FILE_EXT.test(f));
|
|
1848
|
+
.filter((f) => ROUTE_FILE_EXT.test(f) || ROUTE_BACKEND_HALF.test(f) || MIDDLEWARE_FILE.test(f));
|
|
1620
1849
|
}
|
|
1621
1850
|
catch {
|
|
1622
1851
|
continue; // routes dir gone/unreadable - not a drift we can assess
|
|
@@ -1626,6 +1855,6 @@ export async function scanServerManifestDrift(cwd) {
|
|
|
1626
1855
|
findings.push({ file: rel, missing: drift.missing, extra: drift.extra });
|
|
1627
1856
|
}
|
|
1628
1857
|
}
|
|
1629
|
-
return findings.sort((a, b) => a.file
|
|
1858
|
+
return findings.sort((a, b) => codeUnitOrder(a.file, b.file));
|
|
1630
1859
|
}
|
|
1631
1860
|
//# sourceMappingURL=check-scan.js.map
|