@nifrajs/cli 4.0.2 → 5.1.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 +10 -2
- package/dist/agent-files.d.ts +107 -0
- package/dist/agent-files.d.ts.map +1 -0
- package/dist/agent-files.js +162 -0
- package/dist/agent-files.js.map +1 -0
- package/dist/app-files.d.ts +6 -0
- package/dist/app-files.d.ts.map +1 -1
- package/dist/app-files.js +27 -2
- package/dist/app-files.js.map +1 -1
- package/dist/assure-hydration.d.ts.map +1 -1
- package/dist/assure-hydration.js +9 -2
- package/dist/assure-hydration.js.map +1 -1
- package/dist/assure.d.ts +12 -2
- package/dist/assure.d.ts.map +1 -1
- package/dist/assure.js +59 -8
- package/dist/assure.js.map +1 -1
- package/dist/capabilities-tool.d.ts.map +1 -1
- package/dist/capabilities-tool.js +36 -9
- package/dist/capabilities-tool.js.map +1 -1
- package/dist/check-diagnostics.d.ts +22 -2
- package/dist/check-diagnostics.d.ts.map +1 -1
- package/dist/check-diagnostics.js +54 -7
- package/dist/check-diagnostics.js.map +1 -1
- package/dist/check-scan.d.ts +83 -24
- package/dist/check-scan.d.ts.map +1 -1
- package/dist/check-scan.js +1516 -318
- package/dist/check-scan.js.map +1 -1
- package/dist/check.d.ts +2 -2
- package/dist/check.d.ts.map +1 -1
- package/dist/check.js +203 -70
- package/dist/check.js.map +1 -1
- package/dist/cli.d.ts +9 -0
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +215 -50
- package/dist/cli.js.map +1 -1
- package/dist/client-module.d.ts +20 -0
- package/dist/client-module.d.ts.map +1 -0
- package/dist/client-module.js +73 -0
- package/dist/client-module.js.map +1 -0
- package/dist/command-catalog.d.ts +24 -5
- package/dist/command-catalog.d.ts.map +1 -1
- package/dist/command-catalog.js +194 -45
- package/dist/command-catalog.js.map +1 -1
- package/dist/component-check.d.ts +13 -0
- package/dist/component-check.d.ts.map +1 -0
- package/dist/component-check.js +59 -0
- package/dist/component-check.js.map +1 -0
- package/dist/contract-proof.d.ts +1 -1
- package/dist/contract-proof.d.ts.map +1 -1
- package/dist/contract-proof.js +2 -24
- package/dist/contract-proof.js.map +1 -1
- package/dist/contracts.d.ts +19 -0
- package/dist/contracts.d.ts.map +1 -1
- package/dist/contracts.js +50 -3
- package/dist/contracts.js.map +1 -1
- package/dist/db-tool.d.ts.map +1 -1
- package/dist/db-tool.js +4 -3
- package/dist/db-tool.js.map +1 -1
- package/dist/dev-bun-config.d.ts +5 -1
- package/dist/dev-bun-config.d.ts.map +1 -1
- package/dist/dev-bun-config.js +44 -11
- package/dist/dev-bun-config.js.map +1 -1
- package/dist/dev-feed-tool.d.ts +18 -1
- package/dist/dev-feed-tool.d.ts.map +1 -1
- package/dist/dev-feed-tool.js +93 -0
- package/dist/dev-feed-tool.js.map +1 -1
- package/dist/diagnostics.d.ts +3 -0
- package/dist/diagnostics.d.ts.map +1 -1
- package/dist/diagnostics.js.map +1 -1
- package/dist/diff-tool.d.ts +9 -8
- package/dist/diff-tool.d.ts.map +1 -1
- package/dist/diff-tool.js +28 -24
- package/dist/diff-tool.js.map +1 -1
- package/dist/doctor.d.ts +13 -0
- package/dist/doctor.d.ts.map +1 -1
- package/dist/doctor.js +23 -15
- package/dist/doctor.js.map +1 -1
- package/dist/env-file.d.ts +6 -0
- package/dist/env-file.d.ts.map +1 -1
- package/dist/env-file.js +6 -0
- package/dist/env-file.js.map +1 -1
- package/dist/fix-recipes.d.ts +2 -0
- package/dist/fix-recipes.d.ts.map +1 -1
- package/dist/fix-recipes.js +102 -2
- package/dist/fix-recipes.js.map +1 -1
- package/dist/hand-built-manifest.d.ts +27 -0
- package/dist/hand-built-manifest.d.ts.map +1 -0
- package/dist/hand-built-manifest.js +124 -0
- package/dist/hand-built-manifest.js.map +1 -0
- package/dist/hooks-report.d.ts +23 -0
- package/dist/hooks-report.d.ts.map +1 -0
- package/dist/hooks-report.js +104 -0
- package/dist/hooks-report.js.map +1 -0
- package/dist/init-agents.d.ts +18 -2
- package/dist/init-agents.d.ts.map +1 -1
- package/dist/init-agents.js +20 -13
- package/dist/init-agents.js.map +1 -1
- package/dist/internal/config-paths.d.ts +16 -0
- package/dist/internal/config-paths.d.ts.map +1 -0
- package/dist/internal/config-paths.js +124 -0
- package/dist/internal/config-paths.js.map +1 -0
- package/dist/internal/installed-package.d.ts +4 -0
- package/dist/internal/installed-package.d.ts.map +1 -0
- package/dist/internal/installed-package.js +17 -0
- package/dist/internal/installed-package.js.map +1 -0
- package/dist/internal/nifra-project.d.ts +41 -0
- package/dist/internal/nifra-project.d.ts.map +1 -0
- package/dist/internal/nifra-project.js +183 -0
- package/dist/internal/nifra-project.js.map +1 -0
- package/dist/internal/project-specifiers.d.ts +29 -0
- package/dist/internal/project-specifiers.d.ts.map +1 -0
- package/dist/internal/project-specifiers.js +435 -0
- package/dist/internal/project-specifiers.js.map +1 -0
- package/dist/internal/script-kind.d.ts.map +1 -1
- package/dist/internal/script-kind.js +2 -1
- package/dist/internal/script-kind.js.map +1 -1
- package/dist/internal/source-facts.d.ts +8 -1
- package/dist/internal/source-facts.d.ts.map +1 -1
- package/dist/internal/source-facts.js +22 -5
- package/dist/internal/source-facts.js.map +1 -1
- package/dist/internal/typescript-import.d.ts +1 -1
- package/dist/internal/typescript-import.d.ts.map +1 -1
- package/dist/internal/typescript-import.js +10 -6
- package/dist/internal/typescript-import.js.map +1 -1
- package/dist/introspect.d.ts +15 -6
- package/dist/introspect.d.ts.map +1 -1
- package/dist/introspect.js +87 -25
- package/dist/introspect.js.map +1 -1
- package/dist/levels-tool.d.ts.map +1 -1
- package/dist/levels-tool.js +98 -23
- package/dist/levels-tool.js.map +1 -1
- package/dist/load.d.ts +33 -10
- package/dist/load.d.ts.map +1 -1
- package/dist/load.js +60 -10
- package/dist/load.js.map +1 -1
- package/dist/mcp-context.d.ts +5 -5
- package/dist/mcp-context.d.ts.map +1 -1
- package/dist/mcp-context.js +8 -23
- package/dist/mcp-context.js.map +1 -1
- package/dist/mcp-delegate.d.ts +2 -2
- package/dist/mcp-delegate.d.ts.map +1 -1
- package/dist/mcp-delegate.js +19 -7
- package/dist/mcp-delegate.js.map +1 -1
- package/dist/mcp-exec.d.ts +13 -4
- package/dist/mcp-exec.d.ts.map +1 -1
- package/dist/mcp-exec.js +56 -14
- package/dist/mcp-exec.js.map +1 -1
- package/dist/mcp-http.js +1 -1
- package/dist/mcp-io.d.ts +30 -0
- package/dist/mcp-io.d.ts.map +1 -1
- package/dist/mcp-io.js +63 -0
- package/dist/mcp-io.js.map +1 -1
- package/dist/mcp-isolate.d.ts.map +1 -1
- package/dist/mcp-isolate.js +3 -2
- package/dist/mcp-isolate.js.map +1 -1
- package/dist/mcp-path.d.ts +2 -1
- package/dist/mcp-path.d.ts.map +1 -1
- package/dist/mcp-path.js +9 -24
- package/dist/mcp-path.js.map +1 -1
- package/dist/mcp-project-child.js +1 -1
- package/dist/mcp-project-child.js.map +1 -1
- package/dist/mcp-root.d.ts.map +1 -1
- package/dist/mcp-root.js +50 -10
- package/dist/mcp-root.js.map +1 -1
- package/dist/mcp-ws.d.ts.map +1 -1
- package/dist/mcp-ws.js +3 -1
- package/dist/mcp-ws.js.map +1 -1
- package/dist/mcp.d.ts.map +1 -1
- package/dist/mcp.js +48 -19
- package/dist/mcp.js.map +1 -1
- package/dist/migrate-layout.d.ts +47 -1
- package/dist/migrate-layout.d.ts.map +1 -1
- package/dist/migrate-layout.js +1972 -170
- package/dist/migrate-layout.js.map +1 -1
- package/dist/output-schemas.d.ts +128 -0
- package/dist/output-schemas.d.ts.map +1 -0
- package/dist/output-schemas.js +975 -0
- package/dist/output-schemas.js.map +1 -0
- package/dist/port.d.ts.map +1 -1
- package/dist/port.js +3 -2
- package/dist/port.js.map +1 -1
- package/dist/project-facts.d.ts +15 -4
- package/dist/project-facts.d.ts.map +1 -1
- package/dist/project-facts.js +1 -0
- package/dist/project-facts.js.map +1 -1
- package/dist/project-path.d.ts +6 -0
- package/dist/project-path.d.ts.map +1 -1
- package/dist/project-path.js +48 -14
- package/dist/project-path.js.map +1 -1
- package/dist/recipes/5.0.0.d.ts +11 -0
- package/dist/recipes/5.0.0.d.ts.map +1 -0
- package/dist/recipes/5.0.0.js +34 -0
- package/dist/recipes/5.0.0.js.map +1 -0
- package/dist/recipes/index.d.ts +7 -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 +5 -0
- package/dist/release-verification.d.ts.map +1 -1
- package/dist/release-verification.js +14 -0
- package/dist/release-verification.js.map +1 -1
- package/dist/replay.d.ts.map +1 -1
- package/dist/replay.js +12 -3
- package/dist/replay.js.map +1 -1
- package/dist/review.d.ts +3 -1
- package/dist/review.d.ts.map +1 -1
- package/dist/review.js +43 -7
- package/dist/review.js.map +1 -1
- package/dist/rules/codes.d.ts +12 -0
- package/dist/rules/codes.d.ts.map +1 -1
- package/dist/rules/codes.js +12 -0
- package/dist/rules/codes.js.map +1 -1
- package/dist/rules/comment-markers.d.ts +16 -7
- package/dist/rules/comment-markers.d.ts.map +1 -1
- package/dist/rules/comment-markers.js +58 -19
- package/dist/rules/comment-markers.js.map +1 -1
- package/dist/rules/contract-visibility.d.ts +3 -0
- package/dist/rules/contract-visibility.d.ts.map +1 -0
- package/dist/rules/contract-visibility.js +337 -0
- package/dist/rules/contract-visibility.js.map +1 -0
- package/dist/rules/data-guard.d.ts +13 -0
- package/dist/rules/data-guard.d.ts.map +1 -1
- package/dist/rules/data-guard.js +356 -37
- package/dist/rules/data-guard.js.map +1 -1
- package/dist/rules/layout.d.ts +4 -0
- package/dist/rules/layout.d.ts.map +1 -0
- package/dist/rules/layout.js +19 -0
- package/dist/rules/layout.js.map +1 -0
- package/dist/rules/legacy.d.ts.map +1 -1
- package/dist/rules/legacy.js +178 -44
- package/dist/rules/legacy.js.map +1 -1
- package/dist/rules/pages.d.ts +50 -0
- package/dist/rules/pages.d.ts.map +1 -1
- package/dist/rules/pages.js +436 -4
- package/dist/rules/pages.js.map +1 -1
- package/dist/rules/routes.d.ts +6 -0
- package/dist/rules/routes.d.ts.map +1 -1
- package/dist/rules/routes.js +111 -16
- package/dist/rules/routes.js.map +1 -1
- package/dist/rules/secrets.js +2 -2
- package/dist/rules/secrets.js.map +1 -1
- package/dist/rules/security.d.ts +13 -1
- package/dist/rules/security.d.ts.map +1 -1
- package/dist/rules/security.js +313 -69
- package/dist/rules/security.js.map +1 -1
- package/dist/rules/zones.d.ts +13 -0
- package/dist/rules/zones.d.ts.map +1 -1
- package/dist/rules/zones.js +218 -44
- package/dist/rules/zones.js.map +1 -1
- package/dist/scaffold.d.ts.map +1 -1
- package/dist/scaffold.js +6 -4
- package/dist/scaffold.js.map +1 -1
- package/dist/sdk.d.ts.map +1 -1
- package/dist/sdk.js +14 -5
- package/dist/sdk.js.map +1 -1
- package/dist/stylex-migrate.d.ts.map +1 -1
- package/dist/stylex-migrate.js +290 -24
- package/dist/stylex-migrate.js.map +1 -1
- package/dist/test-tool.d.ts.map +1 -1
- package/dist/test-tool.js +6 -3
- package/dist/test-tool.js.map +1 -1
- package/dist/typecheck-coverage.d.ts +20 -0
- package/dist/typecheck-coverage.d.ts.map +1 -0
- package/dist/typecheck-coverage.js +129 -0
- package/dist/typecheck-coverage.js.map +1 -0
- package/dist/upgrade.d.ts +84 -0
- package/dist/upgrade.d.ts.map +1 -1
- package/dist/upgrade.js +498 -78
- package/dist/upgrade.js.map +1 -1
- package/dist/verification-plan.d.ts.map +1 -1
- package/dist/verification-plan.js +16 -2
- package/dist/verification-plan.js.map +1 -1
- package/dist/version-drift.d.ts +22 -0
- package/dist/version-drift.d.ts.map +1 -0
- package/dist/version-drift.js +94 -0
- package/dist/version-drift.js.map +1 -0
- package/dist/web-app-options.d.ts +1 -1
- package/dist/web-app-options.d.ts.map +1 -1
- package/dist/web-app-options.js +1 -1
- package/dist/web-app-options.js.map +1 -1
- package/dist/work-graph.d.ts.map +1 -1
- package/dist/work-graph.js +20 -8
- package/dist/work-graph.js.map +1 -1
- package/docs/examples.json +55 -7
- package/docs/llms-full.txt +2043 -222
- package/docs/types.json +1573 -246
- package/package.json +11 -12
- package/src/agent-files.ts +203 -0
- package/src/app-files.ts +34 -4
- package/src/assure-hydration.ts +18 -2
- package/src/assure.ts +88 -10
- package/src/capabilities-tool.ts +47 -9
- package/src/check-diagnostics.ts +75 -9
- package/src/check-scan.ts +1816 -377
- package/src/check.ts +258 -81
- package/src/cli.ts +242 -37
- package/src/client-module.ts +97 -0
- package/src/command-catalog.ts +237 -52
- package/src/component-check.ts +79 -0
- package/src/contract-proof.ts +3 -25
- package/src/contracts.ts +60 -3
- package/src/db-tool.ts +5 -2
- package/src/dev-bun-config.ts +51 -10
- package/src/dev-feed-tool.ts +112 -1
- package/src/diagnostics.ts +3 -0
- package/src/diff-tool.ts +33 -26
- package/src/doctor.ts +43 -21
- package/src/env-file.ts +7 -0
- package/src/fix-recipes.ts +116 -2
- package/src/hand-built-manifest.ts +144 -0
- package/src/hooks-report.ts +135 -0
- package/src/init-agents.ts +21 -14
- package/src/internal/config-paths.ts +154 -0
- package/src/internal/installed-package.ts +15 -0
- package/src/internal/nifra-project.ts +191 -0
- package/src/internal/project-specifiers.ts +477 -0
- package/src/internal/script-kind.ts +2 -1
- package/src/internal/source-facts.ts +48 -5
- package/src/internal/typescript-import.ts +15 -6
- package/src/introspect.ts +104 -28
- package/src/levels-tool.ts +124 -29
- package/src/load.ts +107 -17
- package/src/mcp-context.ts +12 -25
- package/src/mcp-delegate.ts +22 -10
- package/src/mcp-exec.ts +68 -18
- package/src/mcp-http.ts +1 -1
- package/src/mcp-io.ts +74 -0
- package/src/mcp-isolate.ts +9 -2
- package/src/mcp-path.ts +8 -22
- package/src/mcp-project-child.ts +1 -1
- package/src/mcp-root.ts +58 -8
- package/src/mcp-ws.ts +4 -2
- package/src/mcp.ts +56 -22
- package/src/migrate-layout.ts +2257 -176
- package/src/output-schemas.ts +1240 -0
- package/src/port.ts +3 -2
- package/src/project-facts.ts +17 -3
- package/src/project-path.ts +44 -13
- package/src/recipes/5.0.0.ts +35 -0
- package/src/recipes/index.ts +10 -0
- package/src/release-verification.ts +20 -0
- package/src/replay.ts +14 -3
- package/src/review.ts +44 -10
- package/src/rules/codes.ts +12 -0
- package/src/rules/comment-markers.ts +57 -21
- package/src/rules/contract-visibility.ts +383 -0
- package/src/rules/data-guard.ts +391 -42
- package/src/rules/layout.ts +22 -0
- package/src/rules/legacy.ts +206 -52
- package/src/rules/pages.ts +481 -4
- package/src/rules/routes.ts +124 -16
- package/src/rules/secrets.ts +2 -2
- package/src/rules/security.ts +391 -72
- package/src/rules/zones.ts +232 -42
- package/src/scaffold.ts +6 -4
- package/src/sdk.ts +14 -5
- package/src/stylex-migrate.ts +251 -24
- package/src/test-tool.ts +12 -3
- package/src/typecheck-coverage.ts +134 -0
- package/src/upgrade.ts +576 -72
- package/src/verification-plan.ts +33 -2
- package/src/version-drift.ts +118 -0
- package/src/web-app-options.ts +2 -2
- package/src/work-graph.ts +17 -12
package/src/rules/zones.ts
CHANGED
|
@@ -8,21 +8,38 @@
|
|
|
8
8
|
* anything but shared code. NF-C029 owns private environment reads in browser code.
|
|
9
9
|
*/
|
|
10
10
|
import { existsSync, readFileSync, realpathSync } from "node:fs"
|
|
11
|
-
import {
|
|
11
|
+
import { isAbsolute, join, relative } from "node:path"
|
|
12
12
|
import {
|
|
13
|
+
browserDenial,
|
|
13
14
|
type Classification,
|
|
14
15
|
createZoneClassifier,
|
|
15
16
|
importAllowed,
|
|
16
17
|
importRuleMessage,
|
|
17
18
|
privateEnvReads,
|
|
18
19
|
privateEnvReason,
|
|
20
|
+
specifierPackage,
|
|
19
21
|
type ZoneClassifier,
|
|
22
|
+
ZoneConfigError,
|
|
20
23
|
} from "@nifrajs/web/zones"
|
|
21
24
|
import { CONFIG_FILE, FRAMEWORK_FILE } from "../app-files.ts"
|
|
22
|
-
import { importSites, stripComments } from "../check-scan.ts"
|
|
25
|
+
import { COMPONENT_FILE, importSites, stripComments } from "../check-scan.ts"
|
|
26
|
+
import { appDirs, clientModuleRoot } from "../client-module.ts"
|
|
27
|
+
import { componentCheckReceipts } from "../component-check.ts"
|
|
23
28
|
import { type Diagnostic, diagnostic } from "../diagnostics.ts"
|
|
24
29
|
import { codeUnitOrder } from "../internal/code-unit-order.ts"
|
|
30
|
+
import { installedManifest } from "../internal/installed-package.ts"
|
|
31
|
+
import {
|
|
32
|
+
createApiOnlyProbe,
|
|
33
|
+
createKindProbe,
|
|
34
|
+
createNifraProjectProbe,
|
|
35
|
+
createWebAppProbe,
|
|
36
|
+
hasWebConfig,
|
|
37
|
+
withApiKindHint,
|
|
38
|
+
} from "../internal/nifra-project.ts"
|
|
39
|
+
import { createSpecifierResolver } from "../internal/project-specifiers.ts"
|
|
40
|
+
import { createSourceFacts } from "../internal/source-facts.ts"
|
|
25
41
|
import type { CheckRule, RuleContext } from "./index.ts"
|
|
42
|
+
import { loadRuleTypeScript } from "./typescript.ts"
|
|
26
43
|
|
|
27
44
|
const ZONE_FOLDER = new Set(["routes", "frontend", "backend", "shared"])
|
|
28
45
|
const FRONTEND = new Set(["route-frontend", "frontend"])
|
|
@@ -31,11 +48,35 @@ const BACKEND = new Set(["backend", "route-backend"])
|
|
|
31
48
|
const BROWSER_CODE = new Set(["route-frontend", "frontend", "shared"])
|
|
32
49
|
const CODE_FILE = /\.(?:[cm]?[jt]sx?|svelte|vue|mdx)$/
|
|
33
50
|
|
|
34
|
-
/**
|
|
35
|
-
|
|
51
|
+
/**
|
|
52
|
+
* The app a project file belongs to: the path before its first zone folder, else the deepest app in
|
|
53
|
+
* `apps` that encloses it, since a folder `nifra.zones` declares or a `.frontend`-style suffix zones a
|
|
54
|
+
* file anywhere in its app. `undefined` for a file in no app.
|
|
55
|
+
*/
|
|
56
|
+
function appOf(file: string, apps: readonly string[]): string | undefined {
|
|
36
57
|
const parts = file.split("/")
|
|
37
58
|
const at = parts.findIndex((part, i) => i < parts.length - 1 && ZONE_FOLDER.has(part))
|
|
38
|
-
|
|
59
|
+
if (at !== -1) return parts.slice(0, at).join("/")
|
|
60
|
+
let nearest: string | undefined
|
|
61
|
+
for (const app of apps) {
|
|
62
|
+
if (app !== "" && !file.startsWith(`${app}/`)) continue
|
|
63
|
+
if (nearest === undefined || app.length > nearest.length) nearest = app
|
|
64
|
+
}
|
|
65
|
+
return nearest
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* A file's text as the build compiles it. The source index holds a `.svelte`/`.vue` file as its script
|
|
70
|
+
* blocks only, for the parsers; its markup carries expressions the build compiles too, so a rule that
|
|
71
|
+
* reads components itself gets the whole file.
|
|
72
|
+
*/
|
|
73
|
+
export function buildText(ctx: RuleContext, file: string): string | undefined {
|
|
74
|
+
if (!COMPONENT_FILE.test(file)) return ctx.sources.read(file)
|
|
75
|
+
try {
|
|
76
|
+
return readFileSync(join(ctx.root, file), "utf8")
|
|
77
|
+
} catch {
|
|
78
|
+
return undefined
|
|
79
|
+
}
|
|
39
80
|
}
|
|
40
81
|
|
|
41
82
|
interface ZonedFile {
|
|
@@ -48,28 +89,62 @@ interface ZonedFile {
|
|
|
48
89
|
export function zonedFiles(ctx: RuleContext): {
|
|
49
90
|
readonly files: readonly ZonedFile[]
|
|
50
91
|
readonly classifierFor: (app: string) => ZoneClassifier
|
|
92
|
+
/** Settings that cannot be applied (`nifra.zones`, `nifra.kind`); their apps' files are left out. */
|
|
93
|
+
readonly configErrors: ReadonlyArray<{ readonly file: string; readonly message: string }>
|
|
51
94
|
} {
|
|
52
|
-
const classifiers = new Map<string, ZoneClassifier>()
|
|
53
|
-
const
|
|
95
|
+
const classifiers = new Map<string, ZoneClassifier | ZoneConfigError>()
|
|
96
|
+
const load = (app: string): ZoneClassifier | ZoneConfigError => {
|
|
54
97
|
let classifier = classifiers.get(app)
|
|
55
98
|
if (classifier === undefined) {
|
|
56
99
|
const appRoot = join(ctx.root, app)
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
100
|
+
try {
|
|
101
|
+
classifier = createZoneClassifier({
|
|
102
|
+
appRoot,
|
|
103
|
+
generatedFiles: [join(appRoot, "server-manifest.ts")],
|
|
104
|
+
})
|
|
105
|
+
} catch (error) {
|
|
106
|
+
if (!(error instanceof ZoneConfigError)) throw error
|
|
107
|
+
classifier = error
|
|
108
|
+
}
|
|
61
109
|
classifiers.set(app, classifier)
|
|
62
110
|
}
|
|
63
111
|
return classifier
|
|
64
112
|
}
|
|
113
|
+
const classifierFor = (app: string): ZoneClassifier => {
|
|
114
|
+
const classifier = load(app)
|
|
115
|
+
if (classifier instanceof ZoneConfigError) throw classifier
|
|
116
|
+
return classifier
|
|
117
|
+
}
|
|
65
118
|
const files: ZonedFile[] = []
|
|
119
|
+
const configErrors: Array<{ file: string; message: string }> = []
|
|
120
|
+
const reportOnce = (file: string, message: string): void => {
|
|
121
|
+
if (!configErrors.some((entry) => entry.message === message))
|
|
122
|
+
configErrors.push({ file, message })
|
|
123
|
+
}
|
|
124
|
+
// Another framework's app keeps folders named `routes/` too; nifra builds none of it.
|
|
125
|
+
const inNifraProject = createNifraProjectProbe(ctx.root)
|
|
126
|
+
const kindOf = createKindProbe(ctx.root)
|
|
127
|
+
const apiOnly = createApiOnlyProbe(ctx.root)
|
|
128
|
+
const apps = appDirs(ctx.sources.files)
|
|
66
129
|
for (const file of ctx.sources.files) {
|
|
67
130
|
if (!CODE_FILE.test(file)) continue
|
|
68
|
-
const app = appOf(file)
|
|
69
|
-
if (app === undefined) continue
|
|
70
|
-
|
|
131
|
+
const app = appOf(file, apps)
|
|
132
|
+
if (app === undefined || !inNifraProject(app)) continue
|
|
133
|
+
const declared = kindOf(app)
|
|
134
|
+
if (declared.invalid !== undefined) {
|
|
135
|
+
reportOnce(declared.manifest ?? "package.json", declared.invalid)
|
|
136
|
+
continue
|
|
137
|
+
}
|
|
138
|
+
// An API-only project has no browser, so nothing in it is zoned.
|
|
139
|
+
if (apiOnly(app)) continue
|
|
140
|
+
const classifier = load(app)
|
|
141
|
+
if (classifier instanceof ZoneConfigError) {
|
|
142
|
+
reportOnce(app === "" ? "package.json" : `${app}/package.json`, classifier.message)
|
|
143
|
+
continue
|
|
144
|
+
}
|
|
145
|
+
files.push({ file, app, classification: classifier.classify(join(ctx.root, file)) })
|
|
71
146
|
}
|
|
72
|
-
return { files, classifierFor }
|
|
147
|
+
return { files, classifierFor, configErrors }
|
|
73
148
|
}
|
|
74
149
|
|
|
75
150
|
const real = (path: string): string => {
|
|
@@ -87,41 +162,131 @@ function display(root: string, file: string): string {
|
|
|
87
162
|
return rel.startsWith("../") || isAbsolute(rel) ? file : rel
|
|
88
163
|
}
|
|
89
164
|
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
}
|
|
165
|
+
// The unzoned files one check follows through, at most: each is a build refusal of its own.
|
|
166
|
+
const MAX_UNZONED = 5000
|
|
167
|
+
|
|
168
|
+
interface Importer {
|
|
169
|
+
readonly file: string
|
|
170
|
+
readonly app: string
|
|
171
|
+
/** `"error"` for a file in no zone, reached from code a build loads. */
|
|
172
|
+
readonly zone: Classification["zone"]
|
|
99
173
|
}
|
|
100
174
|
|
|
101
175
|
export const zoneImportRule: CheckRule = {
|
|
102
176
|
code: "NF-C028",
|
|
103
177
|
title: "Zone import check",
|
|
104
178
|
async scan(ctx) {
|
|
105
|
-
const { files, classifierFor } = zonedFiles(ctx)
|
|
106
|
-
const
|
|
107
|
-
|
|
108
|
-
|
|
179
|
+
const { files, classifierFor, configErrors } = zonedFiles(ctx)
|
|
180
|
+
const { receipts } = await componentCheckReceipts(ctx.root, ctx.sources)
|
|
181
|
+
const findings: Diagnostic[] = configErrors.map(({ file, message }) =>
|
|
182
|
+
diagnostic({
|
|
183
|
+
code: "NF-C028",
|
|
184
|
+
severity: "error",
|
|
185
|
+
file,
|
|
186
|
+
message,
|
|
187
|
+
verify: "nifra check",
|
|
188
|
+
}),
|
|
189
|
+
)
|
|
190
|
+
// Outside any web app, a finding may only mean the project serves no pages: say how to declare it.
|
|
191
|
+
const inWebApp = createWebAppProbe(ctx.root)
|
|
192
|
+
const resolveSpecifier = createSpecifierResolver()
|
|
193
|
+
// Without a compiler nothing proves an import erased, so every import stays an edge.
|
|
194
|
+
const { compiler } = await loadRuleTypeScript(ctx)
|
|
195
|
+
const facts =
|
|
196
|
+
compiler === undefined
|
|
197
|
+
? undefined
|
|
198
|
+
: createSourceFacts(compiler, {
|
|
199
|
+
keepsTypeOnlyImports: (file) =>
|
|
200
|
+
resolveSpecifier.keepsTypeOnlyImports(isAbsolute(file) ? file : join(ctx.root, file)),
|
|
201
|
+
})
|
|
202
|
+
const queue: Importer[] = []
|
|
203
|
+
const queued = new Set<string>()
|
|
204
|
+
const follow = (importer: Importer): void => {
|
|
205
|
+
if (queued.has(importer.file) || queued.size >= MAX_UNZONED + files.length) return
|
|
206
|
+
queued.add(importer.file)
|
|
207
|
+
queue.push(importer)
|
|
208
|
+
}
|
|
209
|
+
for (const { file, app, classification } of files) {
|
|
210
|
+
const zone = classification.zone
|
|
211
|
+
if (zone !== "error" && zone !== "library" && zone !== "generated")
|
|
212
|
+
follow({ file, app, zone })
|
|
213
|
+
}
|
|
214
|
+
// The browser entry imports the app's `clientModule`, so the build classifies it like any file it
|
|
215
|
+
// loads, and loads what it imports.
|
|
216
|
+
for (const app of appDirs(ctx.sources.files)) {
|
|
217
|
+
const entry = clientModuleRoot(ctx.root, app, resolveSpecifier, compiler)
|
|
218
|
+
if (entry === undefined) continue
|
|
219
|
+
let classification: Classification
|
|
220
|
+
try {
|
|
221
|
+
classification = classifierFor(app).classify(entry.file)
|
|
222
|
+
} catch (error) {
|
|
223
|
+
if (error instanceof ZoneConfigError) continue
|
|
224
|
+
throw error
|
|
225
|
+
}
|
|
226
|
+
const name = display(ctx.root, entry.file)
|
|
227
|
+
const denial = browserDenial(classification)
|
|
228
|
+
if (denial !== undefined) {
|
|
229
|
+
findings.push(
|
|
230
|
+
diagnostic({
|
|
231
|
+
code: "NF-C028",
|
|
232
|
+
severity: "error",
|
|
233
|
+
file: entry.config,
|
|
234
|
+
line: entry.line,
|
|
235
|
+
message: `clientModule ${JSON.stringify(entry.specifier)} puts ${name} in the browser bundle: ${denial}`,
|
|
236
|
+
evidence: [`clientModule: ${entry.specifier}`, `zone: ${classification.zone}`],
|
|
237
|
+
verify: "nifra check",
|
|
238
|
+
}),
|
|
239
|
+
)
|
|
240
|
+
}
|
|
241
|
+
if (classification.zone === "error" && classification.package === undefined)
|
|
242
|
+
follow({ file: name, app, zone: "error" })
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
// An undeclared package is one fact, however many files import it.
|
|
246
|
+
const reportedPackages = new Set<string>()
|
|
247
|
+
// An array iterator reads the length on every step, so files `follow` appends are visited too.
|
|
248
|
+
for (const { file, app, zone: from } of queue) {
|
|
109
249
|
const content = ctx.sources.read(file)
|
|
110
250
|
if (content === undefined) continue
|
|
111
|
-
for (const { specifier, line } of importSites(content, file)) {
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
251
|
+
for (const { specifier, line } of importSites(content, file, facts)) {
|
|
252
|
+
let target = resolveSpecifier(join(ctx.root, file), specifier)
|
|
253
|
+
let to: Classification
|
|
254
|
+
if (target === undefined) {
|
|
255
|
+
// An installed package is judged here only by the entry it names, when it gives each
|
|
256
|
+
// entry its own side; a package-wide side is NF-C004's in browser code.
|
|
257
|
+
const name = specifierPackage(specifier)
|
|
258
|
+
const manifest =
|
|
259
|
+
name === undefined ? undefined : installedManifest(join(ctx.root, file), name)
|
|
260
|
+
if (manifest === undefined) continue
|
|
261
|
+
to = classifierFor(app).classify(manifest, specifier)
|
|
262
|
+
if (to.entry === undefined) continue
|
|
263
|
+
target = manifest
|
|
264
|
+
} else to = classifierFor(app).classify(target, specifier)
|
|
265
|
+
const targetName = to.entry === undefined ? display(ctx.root, target) : specifier
|
|
266
|
+
if (
|
|
267
|
+
["frontend", "route-frontend", "shared"].includes(from) &&
|
|
268
|
+
(receipts.has(target) || receipts.has(targetName))
|
|
269
|
+
)
|
|
270
|
+
continue
|
|
116
271
|
let message: string
|
|
117
|
-
if (to.zone === "error")
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
272
|
+
if (to.zone === "error") {
|
|
273
|
+
if (to.package !== undefined) {
|
|
274
|
+
if (reportedPackages.has(to.package)) continue
|
|
275
|
+
reportedPackages.add(to.package)
|
|
276
|
+
} else if (to.entry === undefined) {
|
|
277
|
+
// A file in no zone loads what it imports too: the build refuses each one it reaches.
|
|
278
|
+
follow({ file: targetName, app, zone: "error" })
|
|
279
|
+
}
|
|
280
|
+
message = `${file} imports ${targetName}: ${to.reason}`
|
|
281
|
+
} else if (
|
|
282
|
+
from !== "error" &&
|
|
283
|
+
((SERVER.has(from) && FRONTEND.has(to.zone)) ||
|
|
284
|
+
// Shared code reaching backend code is NF-C004's, reported there with the chain.
|
|
285
|
+
(from === "shared" && !importAllowed(from, to.zone) && !BACKEND.has(to.zone)))
|
|
122
286
|
) {
|
|
123
|
-
message = importRuleMessage(file, from
|
|
287
|
+
message = importRuleMessage(file, from, targetName, to.zone)
|
|
124
288
|
} else continue
|
|
289
|
+
if (!inWebApp(app)) message = withApiKindHint(message)
|
|
125
290
|
findings.push(
|
|
126
291
|
diagnostic({
|
|
127
292
|
code: "NF-C028",
|
|
@@ -129,7 +294,7 @@ export const zoneImportRule: CheckRule = {
|
|
|
129
294
|
file,
|
|
130
295
|
line,
|
|
131
296
|
message,
|
|
132
|
-
evidence: [`import: ${specifier}`, `from zone: ${from
|
|
297
|
+
evidence: [`import: ${specifier}`, `from zone: ${from}`, `to zone: ${to.zone}`],
|
|
133
298
|
verify: "nifra check",
|
|
134
299
|
}),
|
|
135
300
|
)
|
|
@@ -141,6 +306,31 @@ export const zoneImportRule: CheckRule = {
|
|
|
141
306
|
},
|
|
142
307
|
}
|
|
143
308
|
|
|
309
|
+
/** NF-C037: `"nifra": { "kind": "api" }` governing a directory that holds web config. */
|
|
310
|
+
export const apiKindRule: CheckRule = {
|
|
311
|
+
code: "NF-C037",
|
|
312
|
+
title: "API-only declaration over a web app",
|
|
313
|
+
async scan(ctx) {
|
|
314
|
+
const kindOf = createKindProbe(ctx.root)
|
|
315
|
+
const findings: Diagnostic[] = []
|
|
316
|
+
for (const app of appDirs(ctx.sources.files)) {
|
|
317
|
+
const declared = kindOf(app)
|
|
318
|
+
if (declared.kind !== "api" || !hasWebConfig(ctx.root, app)) continue
|
|
319
|
+
const where = app === "" ? "the project root" : `${app}/`
|
|
320
|
+
findings.push(
|
|
321
|
+
diagnostic({
|
|
322
|
+
code: "NF-C037",
|
|
323
|
+
severity: "error",
|
|
324
|
+
file: declared.manifest ?? "package.json",
|
|
325
|
+
message: `"nifra": { "kind": "api" } in ${declared.manifest ?? "package.json"} governs ${where}, which has web config (${app === "" ? "" : `${app}/`}${existsSync(join(ctx.root, app, FRAMEWORK_FILE)) ? FRAMEWORK_FILE : CONFIG_FILE}). An app that serves pages keeps its zone checks; remove "kind", or give the web app its own package.json`,
|
|
326
|
+
verify: "nifra check",
|
|
327
|
+
}),
|
|
328
|
+
)
|
|
329
|
+
}
|
|
330
|
+
return findings
|
|
331
|
+
},
|
|
332
|
+
}
|
|
333
|
+
|
|
144
334
|
const PREFIX_EXPORT =
|
|
145
335
|
/\bexport\s+const\s+publicEnvPrefix\s*(?::\s*string\s*)?=\s*(?:"([^"\\\r\n]*)"|'([^'\\\r\n]*)')/
|
|
146
336
|
|
|
@@ -164,7 +354,7 @@ export const privateEnvRule: CheckRule = {
|
|
|
164
354
|
const findings: Diagnostic[] = []
|
|
165
355
|
for (const { file, app, classification } of files) {
|
|
166
356
|
if (!BROWSER_CODE.has(classification.zone)) continue
|
|
167
|
-
const content = ctx
|
|
357
|
+
const content = buildText(ctx, file)
|
|
168
358
|
if (content === undefined) continue
|
|
169
359
|
let prefix = prefixes.get(app)
|
|
170
360
|
if (prefix === undefined) {
|
|
@@ -192,4 +382,4 @@ export const privateEnvRule: CheckRule = {
|
|
|
192
382
|
},
|
|
193
383
|
}
|
|
194
384
|
|
|
195
|
-
export const zoneRules = Object.freeze([zoneImportRule, privateEnvRule])
|
|
385
|
+
export const zoneRules = Object.freeze([zoneImportRule, privateEnvRule, apiKindRule])
|
package/src/scaffold.ts
CHANGED
|
@@ -29,7 +29,7 @@ const EXT: Record<Framework, string> = {
|
|
|
29
29
|
solid: "tsx",
|
|
30
30
|
vue: "vue",
|
|
31
31
|
svelte: "svelte",
|
|
32
|
-
vanilla: "
|
|
32
|
+
vanilla: "tsx",
|
|
33
33
|
}
|
|
34
34
|
|
|
35
35
|
/** Derive the framework from a `clientModule` like `@nifrajs/web-react/client`. Defaults to react. */
|
|
@@ -93,7 +93,7 @@ export function routePathToFile(urlPath: string, ext: string): string {
|
|
|
93
93
|
|
|
94
94
|
const ROUTE_CONTRACT = `A route is two files:
|
|
95
95
|
- The page, \`routes/x.<ext>\` - ships to the browser: \`export default function Page({ data }: Route.ComponentProps)\` and \`export const meta = { title, meta: […] }\`. It never imports backend code.
|
|
96
|
-
- Its backend half, \`routes/x.backend.ts\` - never reaches the browser: \`export const loaderOutput = t.object({…})\` with \`export async
|
|
96
|
+
- Its backend half, \`routes/x.backend.ts\` - never reaches the browser: \`export const loaderOutput = t.object({…})\` with \`export const loader: Route.Loader = async ({ params, api }) => …\` (data for SSR - only what the schema declares is sent, and \`Route.Loader\` type-checks what it returns against that schema), \`actionOutput\` with \`export const action: Route.Action\` (the form POST), and \`hydrate\`, \`islandScripts\`, \`revalidate\`, \`middleware\`. Reach the backend through the typed \`api\`.
|
|
97
97
|
Both import \`type { Route } from "./+types/<name>"\`, which \`nifra dev\`, \`nifra build\`, \`nifra check\` and \`nifra types\` generate. Path params are typed on \`params\`.`
|
|
98
98
|
|
|
99
99
|
interface RouteParam {
|
|
@@ -120,11 +120,13 @@ function backendStub(file: string, params: readonly RouteParam[], vanilla: boole
|
|
|
120
120
|
const last = params.at(-1)
|
|
121
121
|
const loader =
|
|
122
122
|
last === undefined
|
|
123
|
-
?
|
|
123
|
+
? `// Route.Loader checks what it returns against loaderOutput.
|
|
124
|
+
export const loader: Route.Loader = async () => {
|
|
124
125
|
// Load through the typed backend client, \`api\` in these args; see nifra_example("loader").
|
|
125
126
|
return { title: "TODO" }
|
|
126
127
|
}`
|
|
127
|
-
:
|
|
128
|
+
: `// Route.Loader checks what it returns against loaderOutput.
|
|
129
|
+
export const loader: Route.Loader = async ({ params }) => {
|
|
128
130
|
// Load by params.${last.name} through the typed backend client, \`api\` in these args; see nifra_example("loader").
|
|
129
131
|
return { title: \`TODO: \${params.${last.name}${last.optional ? ' ?? ""' : ""}}\` }
|
|
130
132
|
}`
|
package/src/sdk.ts
CHANGED
|
@@ -381,7 +381,10 @@ function pythonModelFields(
|
|
|
381
381
|
if (properties === undefined) return " pass"
|
|
382
382
|
const required = schemaRequired(schema)
|
|
383
383
|
const used = new Set<string>()
|
|
384
|
+
// A dataclass rejects a field without a default after one with a default, so required fields
|
|
385
|
+
// lead and each group keeps the schema's order.
|
|
384
386
|
const fields: string[] = []
|
|
387
|
+
const optionalFields: string[] = []
|
|
385
388
|
for (const [jsonName, property] of Object.entries(properties)) {
|
|
386
389
|
let fieldName = pythonIdentifier(jsonName, "field")
|
|
387
390
|
while (used.has(fieldName)) fieldName += "_"
|
|
@@ -396,11 +399,12 @@ function pythonModelFields(
|
|
|
396
399
|
)
|
|
397
400
|
const optional = !required.has(jsonName)
|
|
398
401
|
if (optional && !type.includes("None")) type += " | None"
|
|
402
|
+
const group = optional ? optionalFields : fields
|
|
399
403
|
if (fieldName === jsonName) {
|
|
400
|
-
|
|
404
|
+
group.push(` ${fieldName}: ${type}${optional ? " = None" : ""}`)
|
|
401
405
|
} else {
|
|
402
406
|
const metadata = `{"json_name": ${JSON.stringify(jsonName)}}`
|
|
403
|
-
|
|
407
|
+
group.push(
|
|
404
408
|
" " +
|
|
405
409
|
fieldName +
|
|
406
410
|
": " +
|
|
@@ -413,6 +417,7 @@ function pythonModelFields(
|
|
|
413
417
|
)
|
|
414
418
|
}
|
|
415
419
|
}
|
|
420
|
+
fields.push(...optionalFields)
|
|
416
421
|
return fields.length === 0 ? " pass" : fields.join("\n")
|
|
417
422
|
}
|
|
418
423
|
|
|
@@ -430,9 +435,11 @@ function renderPythonModels(catalog: SchemaCatalog, tracker: OpaqueTracker): str
|
|
|
430
435
|
pythonModelFields(schema, name, catalog, tracker),
|
|
431
436
|
)
|
|
432
437
|
} else {
|
|
438
|
+
// A `type` statement evaluates lazily, so an alias may name a model declared below it.
|
|
433
439
|
rendered.push(
|
|
434
|
-
|
|
435
|
-
|
|
440
|
+
"type " +
|
|
441
|
+
name +
|
|
442
|
+
" = " +
|
|
436
443
|
pythonType(schema, name, catalog, tracker, "schema", `model ${name}`),
|
|
437
444
|
)
|
|
438
445
|
}
|
|
@@ -628,7 +635,7 @@ function pythonSdk(document: OpenAPIDocument, options: SdkRenderOptions): string
|
|
|
628
635
|
"import urllib.parse",
|
|
629
636
|
"import urllib.request",
|
|
630
637
|
"from dataclasses import dataclass, field, fields, is_dataclass",
|
|
631
|
-
"from typing import Any, Generic, Literal, Mapping, Never,
|
|
638
|
+
"from typing import Any, Generic, Literal, Mapping, Never, TypeAliasType, TypeVar, get_args, get_origin, get_type_hints",
|
|
632
639
|
"",
|
|
633
640
|
'T = TypeVar("T")',
|
|
634
641
|
"",
|
|
@@ -639,6 +646,8 @@ function pythonSdk(document: OpenAPIDocument, options: SdkRenderOptions): string
|
|
|
639
646
|
" self.body = body",
|
|
640
647
|
"",
|
|
641
648
|
"def _decode(value: Any, target: Any) -> Any:",
|
|
649
|
+
" if isinstance(target, TypeAliasType):",
|
|
650
|
+
" return _decode(value, target.__value__)",
|
|
642
651
|
" if target is None or target is Any or target is object:",
|
|
643
652
|
" return value",
|
|
644
653
|
" origin = get_origin(target)",
|