@nifrajs/cli 4.0.0 → 5.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 +3 -0
- 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 +7 -2
- package/dist/assure-hydration.js.map +1 -1
- package/dist/capabilities-tool.d.ts.map +1 -1
- package/dist/capabilities-tool.js +13 -3
- package/dist/capabilities-tool.js.map +1 -1
- package/dist/check-diagnostics.d.ts +20 -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 +75 -22
- package/dist/check-scan.d.ts.map +1 -1
- package/dist/check-scan.js +1475 -310
- 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 +192 -65
- package/dist/check.js.map +1 -1
- package/dist/cli.d.ts +7 -0
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +112 -5
- package/dist/cli.js.map +1 -1
- package/dist/client-module.d.ts +18 -0
- package/dist/client-module.d.ts.map +1 -0
- package/dist/client-module.js +49 -0
- package/dist/client-module.js.map +1 -0
- package/dist/command-catalog.d.ts +21 -5
- package/dist/command-catalog.d.ts.map +1 -1
- package/dist/command-catalog.js +138 -38
- 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/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.map +1 -1
- package/dist/fix-recipes.js +28 -3
- 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 +2 -2
- package/dist/init-agents.d.ts.map +1 -1
- package/dist/init-agents.js +17 -11
- package/dist/init-agents.js.map +1 -1
- 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 +16 -0
- package/dist/internal/project-specifiers.d.ts.map +1 -0
- package/dist/internal/project-specifiers.js +396 -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 +2 -0
- package/dist/internal/source-facts.d.ts.map +1 -1
- package/dist/internal/source-facts.js +11 -0
- 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 +5 -3
- package/dist/internal/typescript-import.js.map +1 -1
- package/dist/introspect.d.ts +6 -6
- package/dist/introspect.d.ts.map +1 -1
- package/dist/introspect.js +55 -24
- package/dist/introspect.js.map +1 -1
- package/dist/levels-tool.d.ts.map +1 -1
- package/dist/levels-tool.js +93 -15
- package/dist/levels-tool.js.map +1 -1
- package/dist/load.d.ts +31 -8
- package/dist/load.d.ts.map +1 -1
- package/dist/load.js +37 -7
- 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 +2 -2
- package/dist/mcp-context.js.map +1 -1
- package/dist/mcp-exec.d.ts +12 -4
- package/dist/mcp-exec.d.ts.map +1 -1
- package/dist/mcp-exec.js +50 -13
- package/dist/mcp-exec.js.map +1 -1
- package/dist/mcp-http.js +1 -1
- package/dist/mcp-io.d.ts +17 -0
- package/dist/mcp-io.d.ts.map +1 -1
- package/dist/mcp-io.js +40 -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-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 +8 -2
- package/dist/mcp.js.map +1 -1
- package/dist/migrate-layout.d.ts +7 -0
- package/dist/migrate-layout.d.ts.map +1 -1
- package/dist/migrate-layout.js +958 -71
- package/dist/migrate-layout.js.map +1 -1
- package/dist/output-schemas.d.ts +94 -0
- package/dist/output-schemas.d.ts.map +1 -0
- package/dist/output-schemas.js +864 -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/recipes/4.0.0.d.ts.map +1 -1
- package/dist/recipes/4.0.0.js +2 -0
- package/dist/recipes/4.0.0.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/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 +38 -7
- package/dist/review.js.map +1 -1
- package/dist/rules/codes.d.ts +10 -0
- package/dist/rules/codes.d.ts.map +1 -1
- package/dist/rules/codes.js +10 -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 +7 -0
- package/dist/rules/data-guard.d.ts.map +1 -1
- package/dist/rules/data-guard.js +319 -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 +158 -43
- package/dist/rules/legacy.js.map +1 -1
- package/dist/rules/pages.d.ts +24 -0
- package/dist/rules/pages.d.ts.map +1 -1
- package/dist/rules/pages.js +221 -3
- 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 +12 -1
- package/dist/rules/security.d.ts.map +1 -1
- package/dist/rules/security.js +281 -68
- 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 +192 -40
- 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 +163 -7
- package/dist/stylex-migrate.js.map +1 -1
- package/dist/sync-manifest.d.ts +10 -2
- package/dist/sync-manifest.d.ts.map +1 -1
- package/dist/sync-manifest.js +42 -18
- package/dist/sync-manifest.js.map +1 -1
- package/dist/test-tool.d.ts.map +1 -1
- package/dist/test-tool.js +12 -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 +69 -0
- package/dist/upgrade.d.ts.map +1 -1
- package/dist/upgrade.js +420 -77
- package/dist/upgrade.js.map +1 -1
- package/dist/verification-plan.d.ts.map +1 -1
- package/dist/verification-plan.js +11 -0
- 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 +42 -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 +54 -6
- package/docs/llms-full.txt +1539 -138
- package/docs/types.json +1456 -206
- package/package.json +11 -12
- package/src/agent-files.ts +203 -0
- package/src/app-files.ts +34 -4
- package/src/assure-hydration.ts +15 -2
- package/src/capabilities-tool.ts +15 -3
- package/src/check-diagnostics.ts +73 -9
- package/src/check-scan.ts +1773 -381
- package/src/check.ts +237 -76
- package/src/cli.ts +141 -5
- package/src/client-module.ts +61 -0
- package/src/command-catalog.ts +181 -47
- 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/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 +34 -3
- package/src/hand-built-manifest.ts +144 -0
- package/src/hooks-report.ts +135 -0
- package/src/init-agents.ts +17 -11
- package/src/internal/nifra-project.ts +191 -0
- package/src/internal/project-specifiers.ts +424 -0
- package/src/internal/script-kind.ts +2 -1
- package/src/internal/source-facts.ts +25 -0
- package/src/internal/typescript-import.ts +10 -3
- package/src/introspect.ts +65 -27
- package/src/levels-tool.ts +117 -20
- package/src/load.ts +78 -14
- package/src/mcp-context.ts +7 -7
- package/src/mcp-exec.ts +60 -17
- package/src/mcp-http.ts +1 -1
- package/src/mcp-io.ts +41 -0
- package/src/mcp-isolate.ts +9 -2
- 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 +8 -3
- package/src/migrate-layout.ts +1109 -70
- package/src/output-schemas.ts +1084 -0
- package/src/port.ts +3 -2
- package/src/project-facts.ts +17 -3
- package/src/recipes/4.0.0.ts +2 -0
- package/src/recipes/5.0.0.ts +35 -0
- package/src/recipes/index.ts +10 -0
- package/src/replay.ts +14 -3
- package/src/review.ts +39 -10
- package/src/rules/codes.ts +10 -0
- package/src/rules/comment-markers.ts +57 -21
- package/src/rules/contract-visibility.ts +383 -0
- package/src/rules/data-guard.ts +351 -42
- package/src/rules/layout.ts +22 -0
- package/src/rules/legacy.ts +184 -51
- package/src/rules/pages.ts +238 -3
- package/src/rules/routes.ts +124 -16
- package/src/rules/secrets.ts +2 -2
- package/src/rules/security.ts +352 -71
- package/src/rules/zones.ts +205 -38
- package/src/scaffold.ts +6 -4
- package/src/sdk.ts +14 -5
- package/src/stylex-migrate.ts +134 -7
- package/src/sync-manifest.ts +48 -18
- package/src/test-tool.ts +18 -3
- package/src/typecheck-coverage.ts +134 -0
- package/src/upgrade.ts +487 -71
- package/src/verification-plan.ts +24 -0
- package/src/version-drift.ts +67 -0
- package/src/web-app-options.ts +2 -2
- package/src/work-graph.ts +17 -12
package/src/sync-manifest.ts
CHANGED
|
@@ -21,6 +21,14 @@ const GENERATED_MARKER = "GENERATED by @nifrajs/web generateServerManifest"
|
|
|
21
21
|
// routes dir relative to the manifest and strips imports to route-relative keys.
|
|
22
22
|
const ROUTES_PREFIX = /["'](\.{1,2}(?:\/[^"'/]+)*?\/routes\/)[^"']+["']/
|
|
23
23
|
const IGNORED = /(^|\/)(node_modules|dist(-[a-z0-9]+)?|build|\.nifra|\.git|\.wrangler|coverage)\//
|
|
24
|
+
// The fs codes that mean the routes dir is absent or unreadable. Every other throw is the route table
|
|
25
|
+
// refusing to build, which must fail the command rather than read as "no manifest found".
|
|
26
|
+
const ROUTES_DIR_UNAVAILABLE = new Set(["ENOENT", "ENOTDIR", "EACCES", "EPERM"])
|
|
27
|
+
|
|
28
|
+
function routesDirUnavailable(error: unknown): boolean {
|
|
29
|
+
if (typeof error !== "object" || error === null || !("code" in error)) return false
|
|
30
|
+
return typeof error.code === "string" && ROUTES_DIR_UNAVAILABLE.has(error.code)
|
|
31
|
+
}
|
|
24
32
|
|
|
25
33
|
export interface SyncManifestResult {
|
|
26
34
|
/** The committed server-manifest file (relative to cwd). */
|
|
@@ -34,12 +42,16 @@ export interface SyncManifestResult {
|
|
|
34
42
|
/** Set when the sync was REFUSED because discovery found zero routes for a manifest that had some
|
|
35
43
|
* (`routes/` likely emptied by mistake) - the manifest was left untouched rather than wiped. */
|
|
36
44
|
readonly refusedEmpty?: boolean
|
|
45
|
+
/** Set when the `routes/` tree could not be built into a route table (a duplicate route, two routes of
|
|
46
|
+
* one shape sharing a path, ...) - the manifest was left untouched and the sync failed. */
|
|
47
|
+
readonly error?: string
|
|
37
48
|
}
|
|
38
49
|
|
|
39
50
|
/**
|
|
40
51
|
* Discover every generated `server-manifest.ts` under `cwd` and re-sync its route table from the live
|
|
41
52
|
* `routes/` tree, preserving baked client assets. Returns one result per generated manifest. Never throws
|
|
42
|
-
* on a per-manifest
|
|
53
|
+
* on a per-manifest failure: an absent or unreadable routes dir is skipped, and a route table that fails
|
|
54
|
+
* to build is reported in that manifest's `error`.
|
|
43
55
|
*/
|
|
44
56
|
export async function syncServerManifests(cwd: string): Promise<SyncManifestResult[]> {
|
|
45
57
|
const { discoverRoutes } = await import("@nifrajs/web/fs")
|
|
@@ -60,8 +72,11 @@ export async function syncServerManifests(cwd: string): Promise<SyncManifestResu
|
|
|
60
72
|
let next: string
|
|
61
73
|
try {
|
|
62
74
|
next = resyncServerManifestSource(source, discoverRoutes(routesDir), routesPrefix)
|
|
63
|
-
} catch {
|
|
64
|
-
continue //
|
|
75
|
+
} catch (error) {
|
|
76
|
+
if (routesDirUnavailable(error)) continue // not a manifest we can sync
|
|
77
|
+
const message = error instanceof Error ? error.message : String(error)
|
|
78
|
+
results.push({ file: rel, changed: false, added: [], removed: [], error: message })
|
|
79
|
+
continue
|
|
65
80
|
}
|
|
66
81
|
const before = parseManifestRouteFiles(source, routesPrefix)
|
|
67
82
|
const after = parseManifestRouteFiles(next, routesPrefix)
|
|
@@ -81,33 +96,48 @@ export async function syncServerManifests(cwd: string): Promise<SyncManifestResu
|
|
|
81
96
|
return results.sort((a, b) => codeUnitOrder(a.file, b.file))
|
|
82
97
|
}
|
|
83
98
|
|
|
84
|
-
/**
|
|
85
|
-
export
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
99
|
+
/** True when no manifest's route table failed to build. */
|
|
100
|
+
export function syncManifestsOk(results: readonly SyncManifestResult[]): boolean {
|
|
101
|
+
return results.every((r) => r.error === undefined)
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/** Per-file summary lines + the client-rebuild caveat. */
|
|
105
|
+
export function renderSyncManifestResults(results: readonly SyncManifestResult[]): string[] {
|
|
106
|
+
if (results.length === 0)
|
|
107
|
+
return ["nifra sync-manifest: no generated server-manifest.ts found under this directory."]
|
|
108
|
+
const lines: string[] = []
|
|
91
109
|
let anyChanged = false
|
|
92
110
|
for (const r of results) {
|
|
93
|
-
if (r.
|
|
94
|
-
|
|
111
|
+
if (r.error !== undefined) {
|
|
112
|
+
lines.push(
|
|
113
|
+
`✖ ${r.file}: routes/ does not build a route table; left untouched.`,
|
|
114
|
+
` ${r.error}`,
|
|
115
|
+
)
|
|
116
|
+
} else if (r.refusedEmpty) {
|
|
117
|
+
lines.push(
|
|
95
118
|
`⚠ ${r.file}: refused - routes/ has zero routes but the manifest had some; left untouched (delete the manifest yourself if this is intentional).`,
|
|
96
119
|
)
|
|
97
120
|
} else if (r.changed) {
|
|
98
121
|
anyChanged = true
|
|
99
|
-
|
|
100
|
-
if (r.added.length > 0)
|
|
101
|
-
if (r.removed.length > 0)
|
|
122
|
+
lines.push(`✓ synced ${r.file}`)
|
|
123
|
+
if (r.added.length > 0) lines.push(` added: ${r.added.join(", ")}`)
|
|
124
|
+
if (r.removed.length > 0) lines.push(` removed: ${r.removed.join(", ")}`)
|
|
102
125
|
} else {
|
|
103
|
-
|
|
126
|
+
lines.push(`• ${r.file} already in sync`)
|
|
104
127
|
}
|
|
105
128
|
}
|
|
106
129
|
if (anyChanged) {
|
|
107
|
-
|
|
130
|
+
lines.push(
|
|
108
131
|
"\nRe-synced the server manifest's route table. If you added or changed a HYDRATING route\n" +
|
|
109
132
|
"component (not just a loader / SSR-only route), also run a full build so the client bundle matches.",
|
|
110
133
|
)
|
|
111
134
|
}
|
|
112
|
-
return
|
|
135
|
+
return lines
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/** CLI entry: sync every generated manifest and print the summary. False when a route table failed to build. */
|
|
139
|
+
export async function runSyncManifest(cwd: string): Promise<boolean> {
|
|
140
|
+
const results = await syncServerManifests(cwd)
|
|
141
|
+
console.log(renderSyncManifestResults(results).join("\n"))
|
|
142
|
+
return syncManifestsOk(results)
|
|
113
143
|
}
|
package/src/test-tool.ts
CHANGED
|
@@ -1,4 +1,9 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
type HeadTailOutput,
|
|
3
|
+
OWN_PROCESS_GROUP,
|
|
4
|
+
readHeadTail,
|
|
5
|
+
signalProcessGroup,
|
|
6
|
+
} from "./mcp-io.ts"
|
|
2
7
|
import { mcpProjectPathError, resolveMcpProjectPath } from "./mcp-path.ts"
|
|
3
8
|
|
|
4
9
|
export interface TestToolArgs {
|
|
@@ -58,6 +63,8 @@ function normalizeTimeout(value: unknown): number {
|
|
|
58
63
|
return Math.min(Math.max(Math.trunc(value), 1_000), MAX_TIMEOUT_MS)
|
|
59
64
|
}
|
|
60
65
|
|
|
66
|
+
const KILL_GRACE_MS = 2_000
|
|
67
|
+
|
|
61
68
|
const ANSI = new RegExp(`${String.fromCharCode(27)}\\[[0-?]*[ -/]*[@-~]`, "g")
|
|
62
69
|
|
|
63
70
|
// A UTF-8 character is at most four bytes, so these hold every character the result can show.
|
|
@@ -146,17 +153,24 @@ export async function collectTestResult(
|
|
|
146
153
|
env: process.env,
|
|
147
154
|
stdout: "pipe",
|
|
148
155
|
stderr: "pipe",
|
|
156
|
+
detached: OWN_PROCESS_GROUP,
|
|
149
157
|
})
|
|
150
158
|
let timedOut = false
|
|
151
159
|
let cancelled = false
|
|
160
|
+
let escalation: ReturnType<typeof setTimeout> | undefined
|
|
161
|
+
// A test can ignore SIGTERM, so the group gets SIGKILL once the grace period runs out.
|
|
162
|
+
const stop = (): void => {
|
|
163
|
+
signalProcessGroup(proc, "SIGTERM")
|
|
164
|
+
escalation ??= setTimeout(() => signalProcessGroup(proc, "SIGKILL"), KILL_GRACE_MS)
|
|
165
|
+
}
|
|
152
166
|
const abort = (): void => {
|
|
153
167
|
cancelled = true
|
|
154
|
-
|
|
168
|
+
stop()
|
|
155
169
|
}
|
|
156
170
|
opts.signal?.addEventListener("abort", abort, { once: true })
|
|
157
171
|
const timer = setTimeout(() => {
|
|
158
172
|
timedOut = true
|
|
159
|
-
|
|
173
|
+
stop()
|
|
160
174
|
}, timeoutMs)
|
|
161
175
|
const empty: HeadTailOutput = { head: "", tail: "", droppedBytes: 0 }
|
|
162
176
|
let stdoutRaw = empty
|
|
@@ -175,6 +189,7 @@ export async function collectTestResult(
|
|
|
175
189
|
exitCode = result[2]
|
|
176
190
|
} finally {
|
|
177
191
|
clearTimeout(timer)
|
|
192
|
+
clearTimeout(escalation)
|
|
178
193
|
opts.signal?.removeEventListener("abort", abort)
|
|
179
194
|
}
|
|
180
195
|
const stdout = clean(stdoutRaw)
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Which zoned files the typecheck never read. `tsc --noEmit` passes for code outside its program, so
|
|
3
|
+
* a stale tsconfig `include` (a folder moved under `backend/`, say) turns the contract gate into a
|
|
4
|
+
* pass that checked nothing there. The check runs tsc with `--listFiles` and compares the program
|
|
5
|
+
* against the app's zone folders.
|
|
6
|
+
*/
|
|
7
|
+
import { existsSync, readFileSync, realpathSync } from "node:fs"
|
|
8
|
+
import { dirname, isAbsolute, join, relative, resolve } from "node:path"
|
|
9
|
+
import { stripComments } from "./check-scan.ts"
|
|
10
|
+
|
|
11
|
+
const ZONE_FOLDER = new Set(["routes", "frontend", "backend", "shared"])
|
|
12
|
+
const TS_SOURCE = /\.(?:[cm]?ts|tsx)$/
|
|
13
|
+
const NOT_PROGRAM_CODE = /\.d\.[cm]?ts$|\.(?:test|spec)\.[cm]?tsx?$/
|
|
14
|
+
const TSC_DIAGNOSTIC = /\(\d+,\d+\):\s*(?:error|warning)\s+TS\d+:/
|
|
15
|
+
|
|
16
|
+
/** Split `tsc --listFiles` output into the program's files and the compiler's own output. */
|
|
17
|
+
export function splitListedFiles(output: string): {
|
|
18
|
+
readonly programFiles: readonly string[]
|
|
19
|
+
readonly output: string
|
|
20
|
+
} {
|
|
21
|
+
const programFiles: string[] = []
|
|
22
|
+
const rest: string[] = []
|
|
23
|
+
for (const line of output.split(/\r?\n/)) {
|
|
24
|
+
if (line !== "" && line === line.trim() && isAbsolute(line) && !TSC_DIAGNOSTIC.test(line))
|
|
25
|
+
programFiles.push(line)
|
|
26
|
+
else rest.push(line)
|
|
27
|
+
}
|
|
28
|
+
return { programFiles, output: rest.join("\n").trim() }
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const real = (path: string): string => {
|
|
32
|
+
try {
|
|
33
|
+
return realpathSync.native(path)
|
|
34
|
+
} catch {
|
|
35
|
+
return path
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// tsc prints forward slashes, and on Windows the drive letter's case is not stable.
|
|
40
|
+
const canonical = (path: string): string => {
|
|
41
|
+
const posix = path.replaceAll("\\", "/")
|
|
42
|
+
return process.platform === "win32" ? posix.toLowerCase() : posix
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** The zone folder a project file sits in (`backend`, `apps/web/routes`), or `undefined`. */
|
|
46
|
+
export function zoneFolderOf(file: string): string | undefined {
|
|
47
|
+
const parts = file.split("/")
|
|
48
|
+
const at = parts.findIndex((part, i) => i < parts.length - 1 && ZONE_FOLDER.has(part))
|
|
49
|
+
return at === -1 ? undefined : parts.slice(0, at + 1).join("/")
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** True when a tsconfig between the app root and `cwd` makes the app its own program. */
|
|
53
|
+
function hasOwnProgram(cwd: string, app: string): boolean {
|
|
54
|
+
let dir = app
|
|
55
|
+
while (dir !== "" && dir !== ".") {
|
|
56
|
+
if (existsSync(join(cwd, dir, "tsconfig.json"))) return true
|
|
57
|
+
const parent = dirname(dir)
|
|
58
|
+
if (parent === dir) return false
|
|
59
|
+
dir = parent
|
|
60
|
+
}
|
|
61
|
+
return false
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* The `exclude` patterns of `<cwd>/tsconfig.json`, inherited through relative `extends` as tsc does,
|
|
66
|
+
* rewritten relative to `cwd`. A file a tsconfig excludes is out of the program on purpose.
|
|
67
|
+
*/
|
|
68
|
+
export function tsconfigExcludes(cwd: string): string[] {
|
|
69
|
+
let path = join(cwd, "tsconfig.json")
|
|
70
|
+
for (let depth = 0; depth < 8 && existsSync(path); depth++) {
|
|
71
|
+
let config: unknown
|
|
72
|
+
try {
|
|
73
|
+
config = JSON.parse(stripComments(readFileSync(path, "utf8")).replace(/,(\s*[}\]])/g, "$1"))
|
|
74
|
+
} catch {
|
|
75
|
+
return []
|
|
76
|
+
}
|
|
77
|
+
if (typeof config !== "object" || config === null) return []
|
|
78
|
+
const exclude: unknown = Reflect.get(config, "exclude")
|
|
79
|
+
if (Array.isArray(exclude)) {
|
|
80
|
+
const from = dirname(path)
|
|
81
|
+
return exclude
|
|
82
|
+
.filter((pattern): pattern is string => typeof pattern === "string")
|
|
83
|
+
.map((pattern) => relative(cwd, resolve(from, pattern)).replaceAll("\\", "/"))
|
|
84
|
+
}
|
|
85
|
+
const base: unknown = Reflect.get(config, "extends")
|
|
86
|
+
if (typeof base !== "string" || !base.startsWith(".")) return []
|
|
87
|
+
path = resolve(dirname(path), base.endsWith(".json") ? base : `${base}.json`)
|
|
88
|
+
}
|
|
89
|
+
return []
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// tsc reads a pattern that names a folder as everything below it.
|
|
93
|
+
const excludedBy = (patterns: readonly string[], file: string): boolean =>
|
|
94
|
+
patterns.some(
|
|
95
|
+
(pattern) => new Bun.Glob(pattern).match(file) || new Bun.Glob(`${pattern}/**`).match(file),
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* The TypeScript files under a zone folder that `programFiles` does not contain, project-relative and
|
|
100
|
+
* in input order. Declaration files and tests are left out: a project may typecheck tests with a
|
|
101
|
+
* separate config. So is an app nested under its own tsconfig, which this program does not own, and a
|
|
102
|
+
* file the tsconfig's `exclude` names.
|
|
103
|
+
*/
|
|
104
|
+
export function filesOutsideProgram(
|
|
105
|
+
cwd: string,
|
|
106
|
+
files: readonly string[],
|
|
107
|
+
programFiles: readonly string[],
|
|
108
|
+
): string[] {
|
|
109
|
+
const program = new Set<string>()
|
|
110
|
+
for (const file of programFiles) {
|
|
111
|
+
program.add(canonical(file))
|
|
112
|
+
if (!file.includes("/node_modules/")) program.add(canonical(real(file)))
|
|
113
|
+
}
|
|
114
|
+
const ownProgram = new Map<string, boolean>()
|
|
115
|
+
const excluded = tsconfigExcludes(cwd)
|
|
116
|
+
const outside: string[] = []
|
|
117
|
+
for (const file of files) {
|
|
118
|
+
if (!TS_SOURCE.test(file) || NOT_PROGRAM_CODE.test(file)) continue
|
|
119
|
+
if (excludedBy(excluded, file)) continue
|
|
120
|
+
const folder = zoneFolderOf(file)
|
|
121
|
+
if (folder === undefined) continue
|
|
122
|
+
const app = dirname(folder)
|
|
123
|
+
let own = ownProgram.get(app)
|
|
124
|
+
if (own === undefined) {
|
|
125
|
+
own = hasOwnProgram(cwd, app)
|
|
126
|
+
ownProgram.set(app, own)
|
|
127
|
+
}
|
|
128
|
+
if (own) continue
|
|
129
|
+
const abs = join(cwd, file)
|
|
130
|
+
if (program.has(canonical(abs)) || program.has(canonical(real(abs)))) continue
|
|
131
|
+
outside.push(file)
|
|
132
|
+
}
|
|
133
|
+
return outside
|
|
134
|
+
}
|