@nifrajs/cli 3.1.0 → 3.2.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 -0
- package/dist/assure-hydration.d.ts.map +1 -1
- package/dist/assure-hydration.js +6 -1
- package/dist/assure-hydration.js.map +1 -1
- package/dist/assure.d.ts.map +1 -1
- package/dist/assure.js +3 -2
- package/dist/assure.js.map +1 -1
- package/dist/check-diagnostics.d.ts +165 -0
- package/dist/check-diagnostics.d.ts.map +1 -0
- package/dist/check-diagnostics.js +1009 -0
- package/dist/check-diagnostics.js.map +1 -0
- package/dist/check-scan.d.ts +245 -0
- package/dist/check-scan.d.ts.map +1 -0
- package/dist/check-scan.js +1599 -0
- package/dist/check-scan.js.map +1 -0
- package/dist/check.d.ts +4 -324
- package/dist/check.d.ts.map +1 -1
- package/dist/check.js +8 -2366
- package/dist/check.js.map +1 -1
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +247 -10
- package/dist/cli.js.map +1 -1
- package/dist/command-catalog.d.ts +21 -1
- package/dist/command-catalog.d.ts.map +1 -1
- package/dist/command-catalog.js +122 -0
- package/dist/command-catalog.js.map +1 -1
- package/dist/env-file.js +5 -5
- package/dist/env-file.js.map +1 -1
- package/dist/frontend-guidance.d.ts +57 -0
- package/dist/frontend-guidance.d.ts.map +1 -0
- package/dist/frontend-guidance.js +195 -0
- package/dist/frontend-guidance.js.map +1 -0
- package/dist/levels-tool.d.ts.map +1 -1
- package/dist/levels-tool.js +6 -0
- package/dist/levels-tool.js.map +1 -1
- package/dist/load.d.ts.map +1 -1
- package/dist/load.js +4 -1
- package/dist/load.js.map +1 -1
- package/dist/mcp-context.d.ts +48 -0
- package/dist/mcp-context.d.ts.map +1 -0
- package/dist/mcp-context.js +215 -0
- package/dist/mcp-context.js.map +1 -0
- package/dist/mcp-docs-tools.d.ts.map +1 -1
- package/dist/mcp-docs-tools.js +31 -0
- package/dist/mcp-docs-tools.js.map +1 -1
- package/dist/mcp-exec.d.ts +74 -0
- package/dist/mcp-exec.d.ts.map +1 -0
- package/dist/mcp-exec.js +844 -0
- package/dist/mcp-exec.js.map +1 -0
- package/dist/mcp-http.js +1 -1
- package/dist/mcp-io.d.ts +26 -0
- package/dist/mcp-io.d.ts.map +1 -0
- package/dist/mcp-io.js +100 -0
- package/dist/mcp-io.js.map +1 -0
- package/dist/mcp-reflect.d.ts +16 -0
- package/dist/mcp-reflect.d.ts.map +1 -0
- package/dist/mcp-reflect.js +101 -0
- package/dist/mcp-reflect.js.map +1 -0
- package/dist/mcp-root.d.ts +7 -0
- package/dist/mcp-root.d.ts.map +1 -1
- package/dist/mcp-root.js +51 -3
- package/dist/mcp-root.js.map +1 -1
- package/dist/mcp.d.ts +10 -92
- package/dist/mcp.d.ts.map +1 -1
- package/dist/mcp.js +13 -1232
- package/dist/mcp.js.map +1 -1
- package/dist/openapi-tool.d.ts +5 -10
- package/dist/openapi-tool.d.ts.map +1 -1
- package/dist/openapi-tool.js +21 -2
- package/dist/openapi-tool.js.map +1 -1
- package/dist/openapi-types.d.ts +21 -0
- package/dist/openapi-types.d.ts.map +1 -0
- package/dist/openapi-types.js +214 -0
- package/dist/openapi-types.js.map +1 -0
- package/dist/port.d.ts.map +1 -1
- package/dist/port.js +2 -1
- package/dist/port.js.map +1 -1
- package/dist/project-facts.d.ts +2 -1
- package/dist/project-facts.d.ts.map +1 -1
- package/dist/project-facts.js.map +1 -1
- package/dist/release-verification.d.ts +4 -1
- package/dist/release-verification.d.ts.map +1 -1
- package/dist/release-verification.js +6 -138
- package/dist/release-verification.js.map +1 -1
- package/dist/rules/codes.d.ts +4 -0
- package/dist/rules/codes.d.ts.map +1 -1
- package/dist/rules/codes.js +4 -0
- package/dist/rules/codes.js.map +1 -1
- package/dist/rules/islands.d.ts +4 -0
- package/dist/rules/islands.d.ts.map +1 -0
- package/dist/rules/islands.js +155 -0
- package/dist/rules/islands.js.map +1 -0
- package/dist/rules/nano.d.ts +6 -0
- package/dist/rules/nano.d.ts.map +1 -0
- package/dist/rules/nano.js +264 -0
- package/dist/rules/nano.js.map +1 -0
- package/dist/rules/security.d.ts.map +1 -1
- package/dist/rules/security.js +38 -4
- package/dist/rules/security.js.map +1 -1
- package/dist/scaffold.d.ts +12 -6
- package/dist/scaffold.d.ts.map +1 -1
- package/dist/scaffold.js +149 -10
- package/dist/scaffold.js.map +1 -1
- package/dist/search-terms.d.ts.map +1 -1
- package/dist/search-terms.js +5 -3
- package/dist/search-terms.js.map +1 -1
- package/dist/stylex-migrate.d.ts +38 -0
- package/dist/stylex-migrate.d.ts.map +1 -0
- package/dist/stylex-migrate.js +1052 -0
- package/dist/stylex-migrate.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/verification-plan.d.ts +24 -0
- package/dist/verification-plan.d.ts.map +1 -0
- package/dist/verification-plan.js +116 -0
- package/dist/verification-plan.js.map +1 -0
- package/dist/work-graph.d.ts.map +1 -1
- package/dist/work-graph.js +6 -3
- package/dist/work-graph.js.map +1 -1
- package/docs/examples.json +36 -0
- package/docs/llms-full.txt +1226 -14
- package/docs/types.json +4185 -87
- package/package.json +9 -9
- package/src/assure-hydration.ts +6 -1
- package/src/assure.ts +4 -2
- package/src/check-diagnostics.ts +1219 -0
- package/src/check-scan.ts +1825 -0
- package/src/check.ts +47 -2752
- package/src/cli.ts +252 -10
- package/src/command-catalog.ts +155 -0
- package/src/env-file.ts +5 -5
- package/src/frontend-guidance.ts +259 -0
- package/src/levels-tool.ts +6 -0
- package/src/load.ts +4 -1
- package/src/mcp-context.ts +266 -0
- package/src/mcp-docs-tools.ts +37 -0
- package/src/mcp-exec.ts +1040 -0
- package/src/mcp-http.ts +1 -1
- package/src/mcp-io.ts +118 -0
- package/src/mcp-reflect.ts +140 -0
- package/src/mcp-root.ts +62 -4
- package/src/mcp.ts +61 -1511
- package/src/openapi-tool.ts +32 -2
- package/src/openapi-types.ts +261 -0
- package/src/port.ts +2 -1
- package/src/project-facts.ts +2 -4
- package/src/release-verification.ts +22 -164
- package/src/rules/codes.ts +4 -0
- package/src/rules/islands.ts +171 -0
- package/src/rules/nano.ts +295 -0
- package/src/rules/security.ts +40 -4
- package/src/scaffold.ts +169 -9
- package/src/search-terms.ts +7 -3
- package/src/stylex-migrate.ts +1139 -0
- package/src/sync-manifest.ts +2 -1
- package/src/verification-plan.ts +242 -0
- package/src/work-graph.ts +6 -3
package/src/openapi-tool.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { basename } from "node:path"
|
|
2
2
|
import { toOpenAPI } from "@nifrajs/schema/openapi"
|
|
3
3
|
import type { LoadedApp } from "./load.ts"
|
|
4
|
+
import { type InferredOpenAPIResponses, inferOpenAPIResponses } from "./openapi-types.ts"
|
|
4
5
|
|
|
5
6
|
export type OpenApiFormat = "json" | "yaml"
|
|
6
7
|
|
|
@@ -52,10 +53,19 @@ function toYaml(value: unknown, depth = 0): string {
|
|
|
52
53
|
* the trimmed document still references those names, and recomputing the exact reachable subset isn't
|
|
53
54
|
* worth the risk of emitting a `$ref` with no target.
|
|
54
55
|
*/
|
|
55
|
-
|
|
56
|
+
function renderOpenApiWithResponses(
|
|
57
|
+
app: LoadedApp,
|
|
58
|
+
format: OpenApiFormat,
|
|
59
|
+
pathPrefix: string | undefined,
|
|
60
|
+
inferredResponses: InferredOpenAPIResponses | undefined,
|
|
61
|
+
): string {
|
|
56
62
|
const title = `${basename(app.cwd)} API`
|
|
57
63
|
const input = hasRoutesMethod(app.backend) ? app.backend : {}
|
|
58
|
-
const generated = toOpenAPI(input, {
|
|
64
|
+
const generated = toOpenAPI(input, {
|
|
65
|
+
title,
|
|
66
|
+
version: "1.0.0",
|
|
67
|
+
...(inferredResponses === undefined ? {} : { inferredResponses }),
|
|
68
|
+
})
|
|
59
69
|
// Narrow to operations under the prefix, rebuilding the doc (its `paths` is readonly). `components`
|
|
60
70
|
// stays - see the doc comment for why the schema set isn't recomputed.
|
|
61
71
|
const document =
|
|
@@ -69,3 +79,23 @@ export function renderOpenApi(app: LoadedApp, format: OpenApiFormat, pathPrefix?
|
|
|
69
79
|
: generated
|
|
70
80
|
return format === "yaml" ? toYaml(document) : JSON.stringify(document, null, 2)
|
|
71
81
|
}
|
|
82
|
+
|
|
83
|
+
/** Render the current runtime route table without loading the TypeScript compiler. */
|
|
84
|
+
export function renderOpenApi(app: LoadedApp, format: OpenApiFormat, pathPrefix?: string): string {
|
|
85
|
+
return renderOpenApiWithResponses(app, format, pathPrefix, undefined)
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Render OpenAPI with optional build-time response-type inference. The TypeScript compiler is loaded
|
|
90
|
+
* only in this explicit CLI path; the server and schema runtime packages remain compiler-free.
|
|
91
|
+
*/
|
|
92
|
+
export async function renderOpenApiWithTypes(
|
|
93
|
+
app: LoadedApp,
|
|
94
|
+
format: OpenApiFormat,
|
|
95
|
+
pathPrefix?: string,
|
|
96
|
+
): Promise<string> {
|
|
97
|
+
const inferred = await inferOpenAPIResponses(app.cwd)
|
|
98
|
+
for (const warning of inferred.warnings)
|
|
99
|
+
console.warn(`[nifra] OpenAPI type inference: ${warning}`)
|
|
100
|
+
return renderOpenApiWithResponses(app, format, pathPrefix, inferred.responses)
|
|
101
|
+
}
|
|
@@ -0,0 +1,261 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Build-time response-type reflection for OpenAPI.
|
|
3
|
+
*
|
|
4
|
+
* This module is CLI-only. It loads the project's TypeScript compiler on demand, inspects the
|
|
5
|
+
* exported `backend` type, and returns inert JSON Schema metadata. It never imports or executes the
|
|
6
|
+
* generated schema at request time, and unsupported/opaque types are omitted rather than guessed.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { existsSync } from "node:fs"
|
|
10
|
+
import { mkdtemp, rm, writeFile } from "node:fs/promises"
|
|
11
|
+
import { tmpdir } from "node:os"
|
|
12
|
+
import { dirname, join, resolve } from "node:path"
|
|
13
|
+
import type { JsonSchema } from "@nifrajs/core/reflection"
|
|
14
|
+
import type * as TSApi from "typescript"
|
|
15
|
+
import { importProjectTypeScript, type TypeScriptApi } from "./internal/typescript-import.ts"
|
|
16
|
+
|
|
17
|
+
export interface InferredOpenAPIResponse {
|
|
18
|
+
readonly description?: string
|
|
19
|
+
readonly schema?: JsonSchema
|
|
20
|
+
readonly contentType?: string
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export type InferredOpenAPIResponses = Readonly<
|
|
24
|
+
Record<string, Readonly<Record<string, InferredOpenAPIResponse>>>
|
|
25
|
+
>
|
|
26
|
+
|
|
27
|
+
export interface OpenAPITypeInferenceResult {
|
|
28
|
+
readonly responses: InferredOpenAPIResponses
|
|
29
|
+
readonly warnings: readonly string[]
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
type Type = TSApi.Type
|
|
33
|
+
type TypeChecker = TSApi.TypeChecker
|
|
34
|
+
|
|
35
|
+
const hasFlag = (type: Type, flag: number): boolean => (type.flags & flag) !== 0
|
|
36
|
+
|
|
37
|
+
function schemaForType(
|
|
38
|
+
ts: TypeScriptApi,
|
|
39
|
+
checker: TypeChecker,
|
|
40
|
+
input: Type,
|
|
41
|
+
seen: Set<Type>,
|
|
42
|
+
depth: number,
|
|
43
|
+
): JsonSchema | undefined {
|
|
44
|
+
if (depth > 20 || seen.has(input)) return undefined
|
|
45
|
+
// Keep `null` in unions: OpenAPI distinguishes `string | null` from `string`. Undefined members
|
|
46
|
+
// are omitted below because JSON has no undefined value; optional object properties are represented
|
|
47
|
+
// by the property's optional symbol flag instead.
|
|
48
|
+
const type = input
|
|
49
|
+
if (hasFlag(type, ts.TypeFlags.Any) || hasFlag(type, ts.TypeFlags.Unknown)) return undefined
|
|
50
|
+
if (hasFlag(type, ts.TypeFlags.Never)) return undefined
|
|
51
|
+
if (hasFlag(type, ts.TypeFlags.Undefined) || hasFlag(type, ts.TypeFlags.Void)) return undefined
|
|
52
|
+
if (hasFlag(type, ts.TypeFlags.StringLiteral)) {
|
|
53
|
+
return { const: (type as TSApi.StringLiteralType).value }
|
|
54
|
+
}
|
|
55
|
+
if (hasFlag(type, ts.TypeFlags.NumberLiteral)) {
|
|
56
|
+
return { const: (type as TSApi.NumberLiteralType).value }
|
|
57
|
+
}
|
|
58
|
+
if (hasFlag(type, ts.TypeFlags.BooleanLiteral)) {
|
|
59
|
+
return { const: checker.typeToString(type) === "true" }
|
|
60
|
+
}
|
|
61
|
+
if (hasFlag(type, ts.TypeFlags.StringLike)) return { type: "string" }
|
|
62
|
+
if (hasFlag(type, ts.TypeFlags.NumberLike)) return { type: "number" }
|
|
63
|
+
if (hasFlag(type, ts.TypeFlags.BooleanLike)) return { type: "boolean" }
|
|
64
|
+
if (hasFlag(type, ts.TypeFlags.BigIntLike)) return { type: "integer" }
|
|
65
|
+
if (hasFlag(type, ts.TypeFlags.Null)) return { type: "null" }
|
|
66
|
+
|
|
67
|
+
if (type.isUnion()) {
|
|
68
|
+
const members = type.types
|
|
69
|
+
.map((member) => schemaForType(ts, checker, member, seen, depth + 1))
|
|
70
|
+
.filter((schema): schema is JsonSchema => schema !== undefined)
|
|
71
|
+
if (members.length === 0) return undefined
|
|
72
|
+
if (members.length === 1) return members[0]
|
|
73
|
+
return { anyOf: members }
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
if (checker.isTupleType(type)) {
|
|
77
|
+
const elements = checker
|
|
78
|
+
.getTypeArguments(type as TSApi.TypeReference)
|
|
79
|
+
.map((member) => schemaForType(ts, checker, member, seen, depth + 1))
|
|
80
|
+
.filter((schema): schema is JsonSchema => schema !== undefined)
|
|
81
|
+
return elements.length === 0 ? undefined : { type: "array", prefixItems: elements }
|
|
82
|
+
}
|
|
83
|
+
if (checker.isArrayType(type)) {
|
|
84
|
+
const [element] = checker.getTypeArguments(type as TSApi.TypeReference)
|
|
85
|
+
const items =
|
|
86
|
+
element === undefined ? undefined : schemaForType(ts, checker, element, seen, depth + 1)
|
|
87
|
+
return items === undefined ? { type: "array" } : { type: "array", items }
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
if (type.symbol?.name === "Date") return { type: "string", format: "date-time" }
|
|
91
|
+
if ((type.flags & ts.TypeFlags.Object) === 0) return undefined
|
|
92
|
+
|
|
93
|
+
seen.add(type)
|
|
94
|
+
try {
|
|
95
|
+
const properties: Record<string, JsonSchema> = {}
|
|
96
|
+
const required: string[] = []
|
|
97
|
+
for (const property of checker.getPropertiesOfType(type)) {
|
|
98
|
+
const declaration = property.valueDeclaration ?? property.declarations?.[0]
|
|
99
|
+
if (declaration === undefined) continue
|
|
100
|
+
const propertyType = checker.getTypeOfSymbolAtLocation(property, declaration)
|
|
101
|
+
// Methods are implementation details, not JSON fields. Other unsupported property types make
|
|
102
|
+
// the whole response opaque instead of emitting a partial schema that could falsely promise a
|
|
103
|
+
// contract to generated clients.
|
|
104
|
+
if (checker.getSignaturesOfType(propertyType, ts.SignatureKind.Call).length > 0) continue
|
|
105
|
+
const propertySchema = schemaForType(ts, checker, propertyType, seen, depth + 1)
|
|
106
|
+
if (propertySchema === undefined) return undefined
|
|
107
|
+
properties[property.name] = propertySchema
|
|
108
|
+
if ((property.flags & ts.SymbolFlags.Optional) === 0) required.push(property.name)
|
|
109
|
+
}
|
|
110
|
+
if (Object.keys(properties).length === 0) return { type: "object" }
|
|
111
|
+
return {
|
|
112
|
+
type: "object",
|
|
113
|
+
properties,
|
|
114
|
+
...(required.length > 0 ? { required } : {}),
|
|
115
|
+
}
|
|
116
|
+
} finally {
|
|
117
|
+
seen.delete(type)
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function symbolNamed(
|
|
122
|
+
checker: TypeChecker,
|
|
123
|
+
source: TSApi.SourceFile,
|
|
124
|
+
name: string,
|
|
125
|
+
flags: number,
|
|
126
|
+
): TSApi.Symbol | undefined {
|
|
127
|
+
return checker.getSymbolsInScope(source, flags).find((symbol) => symbol.name === name)
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function propertyType(checker: TypeChecker, type: Type, name: string): Type | undefined {
|
|
131
|
+
const property = type.getProperty(name)
|
|
132
|
+
if (property === undefined) return undefined
|
|
133
|
+
const declaration =
|
|
134
|
+
property.valueDeclaration ?? property.declarations?.[0] ?? type.symbol?.valueDeclaration
|
|
135
|
+
return declaration === undefined
|
|
136
|
+
? checker.getDeclaredTypeOfSymbol(property)
|
|
137
|
+
: checker.getTypeOfSymbolAtLocation(property, declaration)
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function responseEntries(
|
|
141
|
+
ts: TypeScriptApi,
|
|
142
|
+
checker: TypeChecker,
|
|
143
|
+
location: TSApi.Node,
|
|
144
|
+
responseMap: Type,
|
|
145
|
+
warnings: string[],
|
|
146
|
+
): Record<string, InferredOpenAPIResponse> {
|
|
147
|
+
const output: Record<string, InferredOpenAPIResponse> = {}
|
|
148
|
+
for (const property of responseMap.getProperties()) {
|
|
149
|
+
if (!/^[1-5][0-9]{2}$/.test(property.name)) continue
|
|
150
|
+
const bodyType = checker.getTypeOfSymbolAtLocation(property, location)
|
|
151
|
+
const schema = schemaForType(ts, checker, bodyType, new Set(), 0)
|
|
152
|
+
if (schema === undefined) {
|
|
153
|
+
const bodylessStatus =
|
|
154
|
+
property.name === "204" || property.name === "205" || property.name === "304"
|
|
155
|
+
const bodylessType =
|
|
156
|
+
hasFlag(bodyType, ts.TypeFlags.Undefined) || hasFlag(bodyType, ts.TypeFlags.Void)
|
|
157
|
+
if (bodylessStatus && bodylessType) {
|
|
158
|
+
output[property.name] = {}
|
|
159
|
+
continue
|
|
160
|
+
}
|
|
161
|
+
warnings.push(
|
|
162
|
+
`response ${property.name} has an unsupported or opaque TypeScript body (${checker.typeToString(bodyType)})`,
|
|
163
|
+
)
|
|
164
|
+
continue
|
|
165
|
+
}
|
|
166
|
+
output[property.name] = { schema }
|
|
167
|
+
}
|
|
168
|
+
return output
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/** Infer response schemas from `<root>/backend.ts` without affecting runtime application loading. */
|
|
172
|
+
export async function inferOpenAPIResponses(root: string): Promise<OpenAPITypeInferenceResult> {
|
|
173
|
+
const warnings: string[] = []
|
|
174
|
+
const backendPath = resolve(root, "backend.ts")
|
|
175
|
+
if (!existsSync(backendPath)) return { responses: {}, warnings }
|
|
176
|
+
|
|
177
|
+
const ts = await importProjectTypeScript(root)
|
|
178
|
+
if (ts === undefined) {
|
|
179
|
+
warnings.push("TypeScript is not installed; response inference was skipped")
|
|
180
|
+
return { responses: {}, warnings }
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
const temp = await mkdtemp(join(tmpdir(), "nifra-openapi-"))
|
|
184
|
+
const probePath = join(temp, "probe.ts")
|
|
185
|
+
const probe = [
|
|
186
|
+
'import type { Server as __NifraServer } from "@nifrajs/core/server"',
|
|
187
|
+
`import { backend as __nifra_backend } from ${JSON.stringify(backendPath)}`,
|
|
188
|
+
"type __NifraRegistry = typeof __nifra_backend extends __NifraServer<infer R, any, any> ? R : never",
|
|
189
|
+
"declare const __nifra_registry: __NifraRegistry",
|
|
190
|
+
].join("\n")
|
|
191
|
+
|
|
192
|
+
try {
|
|
193
|
+
await writeFile(probePath, probe, "utf8")
|
|
194
|
+
const configPath = ts.findConfigFile(root, ts.sys.fileExists, "tsconfig.json")
|
|
195
|
+
let options: TSApi.CompilerOptions = {
|
|
196
|
+
noEmit: true,
|
|
197
|
+
skipLibCheck: true,
|
|
198
|
+
moduleResolution: ts.ModuleResolutionKind.Bundler,
|
|
199
|
+
}
|
|
200
|
+
let fileNames = [backendPath]
|
|
201
|
+
if (configPath !== undefined) {
|
|
202
|
+
const config = ts.readConfigFile(configPath, ts.sys.readFile)
|
|
203
|
+
if (config.error) {
|
|
204
|
+
warnings.push(
|
|
205
|
+
`could not read tsconfig.json: ${ts.flattenDiagnosticMessageText(config.error.messageText, " ")}`,
|
|
206
|
+
)
|
|
207
|
+
} else {
|
|
208
|
+
const parsed = ts.parseJsonConfigFileContent(config.config, ts.sys, dirname(configPath))
|
|
209
|
+
options = { ...parsed.options, noEmit: true, skipLibCheck: true }
|
|
210
|
+
fileNames = parsed.fileNames.length > 0 ? parsed.fileNames : fileNames
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
const host = ts.createCompilerHost(options, true)
|
|
214
|
+
const program = ts.createProgram(
|
|
215
|
+
[...new Set([...fileNames, backendPath, probePath])],
|
|
216
|
+
options,
|
|
217
|
+
host,
|
|
218
|
+
)
|
|
219
|
+
const source = program.getSourceFile(probePath)
|
|
220
|
+
if (source === undefined) {
|
|
221
|
+
warnings.push("could not create the TypeScript response-inference probe")
|
|
222
|
+
return { responses: {}, warnings }
|
|
223
|
+
}
|
|
224
|
+
const checker = program.getTypeChecker()
|
|
225
|
+
const registrySymbol = symbolNamed(
|
|
226
|
+
checker,
|
|
227
|
+
source,
|
|
228
|
+
"__nifra_registry",
|
|
229
|
+
ts.SymbolFlags.BlockScopedVariable,
|
|
230
|
+
)
|
|
231
|
+
if (registrySymbol === undefined) {
|
|
232
|
+
warnings.push("backend does not expose a typed Nifra server registry")
|
|
233
|
+
return { responses: {}, warnings }
|
|
234
|
+
}
|
|
235
|
+
const registry = checker.getTypeOfSymbolAtLocation(registrySymbol, source)
|
|
236
|
+
const responses: Record<string, Record<string, InferredOpenAPIResponse>> = {}
|
|
237
|
+
for (const pathSymbol of registry.getProperties()) {
|
|
238
|
+
const routePath = pathSymbol.name
|
|
239
|
+
const methods = checker.getTypeOfSymbolAtLocation(pathSymbol, source)
|
|
240
|
+
for (const methodSymbol of methods.getProperties()) {
|
|
241
|
+
if (methodSymbol.name === "WS") continue
|
|
242
|
+
const info = checker.getTypeOfSymbolAtLocation(methodSymbol, source)
|
|
243
|
+
const responseType = propertyType(checker, info, "responses")
|
|
244
|
+
if (responseType === undefined) continue
|
|
245
|
+
const routeResponses = responseEntries(
|
|
246
|
+
ts,
|
|
247
|
+
checker,
|
|
248
|
+
source,
|
|
249
|
+
checker.getNonNullableType(responseType),
|
|
250
|
+
warnings,
|
|
251
|
+
)
|
|
252
|
+
if (Object.keys(routeResponses).length > 0) {
|
|
253
|
+
responses[`${methodSymbol.name.toUpperCase()} ${routePath}`] = routeResponses
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
return { responses, warnings }
|
|
258
|
+
} finally {
|
|
259
|
+
await rm(temp, { recursive: true, force: true })
|
|
260
|
+
}
|
|
261
|
+
}
|
package/src/port.ts
CHANGED
|
@@ -312,10 +312,11 @@ async function walkPortSource(
|
|
|
312
312
|
cwd: string,
|
|
313
313
|
visit: (rel: string, content: string) => void,
|
|
314
314
|
): Promise<void> {
|
|
315
|
-
for await (const
|
|
315
|
+
for await (const rawRel of new Glob("**/*.{ts,tsx,mts,cts,js,jsx,mjs,cjs}").scan({
|
|
316
316
|
cwd,
|
|
317
317
|
dot: false,
|
|
318
318
|
})) {
|
|
319
|
+
const rel = rawRel.replaceAll("\\", "/")
|
|
319
320
|
if (IGNORED.test(rel)) continue
|
|
320
321
|
visit(rel, await Bun.file(join(cwd, rel)).text())
|
|
321
322
|
}
|
package/src/project-facts.ts
CHANGED
|
@@ -1,13 +1,11 @@
|
|
|
1
1
|
import type { CapabilityProjectReport } from "./capabilities-tool.ts"
|
|
2
|
+
import type { CheckAssuranceContext, CheckConfig, CheckDiagnostic } from "./check-diagnostics.ts"
|
|
2
3
|
import type {
|
|
3
|
-
CheckAssuranceContext,
|
|
4
|
-
CheckConfig,
|
|
5
|
-
CheckDiagnostic,
|
|
6
4
|
ManifestDriftFinding,
|
|
7
5
|
SourceFinding,
|
|
8
6
|
StaticRouteFinding,
|
|
9
7
|
TransitiveServerImportFinding,
|
|
10
|
-
} from "./check.ts"
|
|
8
|
+
} from "./check-scan.ts"
|
|
11
9
|
import type { DoctorResult } from "./doctor.ts"
|
|
12
10
|
import type { PipelineReport } from "./pipeline-report.ts"
|
|
13
11
|
import type { RulePack, SourceIndex } from "./rules/index.ts"
|
|
@@ -8,8 +8,14 @@
|
|
|
8
8
|
import { mkdtempSync, readFileSync, realpathSync, rmSync } from "node:fs"
|
|
9
9
|
import { tmpdir } from "node:os"
|
|
10
10
|
import { dirname, join, relative, resolve, sep } from "node:path"
|
|
11
|
-
|
|
12
|
-
|
|
11
|
+
import {
|
|
12
|
+
omittedVerificationGateIds,
|
|
13
|
+
type VerificationGateSpec,
|
|
14
|
+
type VerificationPlanMode,
|
|
15
|
+
verificationPlan,
|
|
16
|
+
} from "./verification-plan.ts"
|
|
17
|
+
|
|
18
|
+
export type ReleaseVerificationMode = VerificationPlanMode
|
|
13
19
|
export type ReleaseGateStatus = "pass" | "fail" | "skipped"
|
|
14
20
|
|
|
15
21
|
export interface ReleaseGateResult {
|
|
@@ -25,6 +31,8 @@ export interface ReleaseVerificationResult {
|
|
|
25
31
|
readonly ok: boolean
|
|
26
32
|
readonly mode: ReleaseVerificationMode
|
|
27
33
|
readonly gates: readonly ReleaseGateResult[]
|
|
34
|
+
/** Release gates intentionally absent from the selected shorter plan. */
|
|
35
|
+
readonly omittedReleaseGateIds: readonly string[]
|
|
28
36
|
}
|
|
29
37
|
|
|
30
38
|
export interface ReleaseCommandResult {
|
|
@@ -44,164 +52,6 @@ export interface ReleaseVerificationOptions {
|
|
|
44
52
|
readonly runCommand?: (spec: ReleaseCommandSpec) => Promise<ReleaseCommandResult>
|
|
45
53
|
}
|
|
46
54
|
|
|
47
|
-
interface ReleaseGateSpec {
|
|
48
|
-
readonly id: string
|
|
49
|
-
readonly commands: readonly (readonly string[])[]
|
|
50
|
-
readonly remediation: string
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
const DEFAULT_GATES: readonly ReleaseGateSpec[] = [
|
|
54
|
-
{
|
|
55
|
-
id: "lint",
|
|
56
|
-
commands: [["run", "lint"]],
|
|
57
|
-
remediation: "Run `bun run lint` and fix the reported lint findings.",
|
|
58
|
-
},
|
|
59
|
-
{
|
|
60
|
-
id: "typecheck",
|
|
61
|
-
commands: [["run", "typecheck"]],
|
|
62
|
-
remediation: "Run `bun run typecheck` and fix the reported TypeScript errors.",
|
|
63
|
-
},
|
|
64
|
-
{
|
|
65
|
-
id: "tests",
|
|
66
|
-
commands: [["run", "test"]],
|
|
67
|
-
remediation: "Run `bun run test` and fix the first failing test.",
|
|
68
|
-
},
|
|
69
|
-
{
|
|
70
|
-
id: "docs",
|
|
71
|
-
commands: [["run", "check:docs"]],
|
|
72
|
-
remediation: "Run `bun run check:docs` and update the failing documentation example.",
|
|
73
|
-
},
|
|
74
|
-
{
|
|
75
|
-
id: "api-corpus",
|
|
76
|
-
commands: [["run", "check:api"]],
|
|
77
|
-
remediation: "Run `bun run gen:api` and review the generated API reference.",
|
|
78
|
-
},
|
|
79
|
-
{
|
|
80
|
-
id: "cards-corpus",
|
|
81
|
-
commands: [["run", "check:cards"]],
|
|
82
|
-
remediation: "Run `bun run gen:cards` and review the generated package cards.",
|
|
83
|
-
},
|
|
84
|
-
{
|
|
85
|
-
id: "node-outcome-corpus",
|
|
86
|
-
commands: [["run", "check:node-outcome"]],
|
|
87
|
-
remediation: "Run `bun run gen:node-outcome` and review the generated Node outcome contract.",
|
|
88
|
-
},
|
|
89
|
-
{
|
|
90
|
-
id: "sitemap",
|
|
91
|
-
commands: [["run", "check:sitemap"]],
|
|
92
|
-
remediation: "Run `bun run gen:sitemap` and review the generated sitemap.",
|
|
93
|
-
},
|
|
94
|
-
{
|
|
95
|
-
id: "public-boundary",
|
|
96
|
-
commands: [["run", "check:public-boundary"]],
|
|
97
|
-
remediation:
|
|
98
|
-
"Run `bun run check:public-boundary` and remove the reported public-boundary violation.",
|
|
99
|
-
},
|
|
100
|
-
{
|
|
101
|
-
id: "size",
|
|
102
|
-
commands: [["run", "check:size"]],
|
|
103
|
-
remediation:
|
|
104
|
-
"Run `bun run check:size` and either reduce the bundle or update the reviewed budget.",
|
|
105
|
-
},
|
|
106
|
-
// Appended, not inserted: RELEASE_GATES below composes this list BY INDEX
|
|
107
|
-
// (`slice(0, 3)`, `[3]`), so a gate added in the middle would silently re-point those.
|
|
108
|
-
{
|
|
109
|
-
id: "changesets",
|
|
110
|
-
commands: [["run", "check:changesets"]],
|
|
111
|
-
remediation:
|
|
112
|
-
"Run `bun run changeset` and name every package whose source changed, so the release documents it.",
|
|
113
|
-
},
|
|
114
|
-
]
|
|
115
|
-
|
|
116
|
-
const RELEASE_GATES: readonly ReleaseGateSpec[] = [
|
|
117
|
-
{
|
|
118
|
-
id: "build",
|
|
119
|
-
commands: [["run", "build"]],
|
|
120
|
-
remediation: "Run `bun run build` and fix the first package build failure.",
|
|
121
|
-
},
|
|
122
|
-
...DEFAULT_GATES.slice(0, 3),
|
|
123
|
-
{
|
|
124
|
-
id: "coverage",
|
|
125
|
-
commands: [
|
|
126
|
-
["run", "test:coverage"],
|
|
127
|
-
["run", "check:coverage"],
|
|
128
|
-
],
|
|
129
|
-
remediation:
|
|
130
|
-
"Run `bun run test:coverage` first, then `bun run check:coverage`, and fix the reported coverage regression.",
|
|
131
|
-
},
|
|
132
|
-
{
|
|
133
|
-
id: "corpus",
|
|
134
|
-
commands: [["run", "check:corpus"]],
|
|
135
|
-
remediation:
|
|
136
|
-
"Run `bun run gen:llms`, `bun run gen:api`, and `bun run gen:cards`, then rerun the corpus gate.",
|
|
137
|
-
},
|
|
138
|
-
DEFAULT_GATES[3] as ReleaseGateSpec,
|
|
139
|
-
{
|
|
140
|
-
id: "public-boundary",
|
|
141
|
-
commands: [["run", "check:public-boundary"]],
|
|
142
|
-
remediation:
|
|
143
|
-
"Run `bun run check:public-boundary` and remove the reported public-boundary violation.",
|
|
144
|
-
},
|
|
145
|
-
{
|
|
146
|
-
id: "size",
|
|
147
|
-
commands: [["run", "check:size"]],
|
|
148
|
-
remediation:
|
|
149
|
-
"Run `bun run check:size` and either reduce the bundle or update the reviewed budget.",
|
|
150
|
-
},
|
|
151
|
-
{
|
|
152
|
-
id: "core-performance",
|
|
153
|
-
commands: [["run", "check:core-performance"]],
|
|
154
|
-
remediation:
|
|
155
|
-
"Run `bun run check:core-performance` and investigate the measured performance regression.",
|
|
156
|
-
},
|
|
157
|
-
{
|
|
158
|
-
id: "publish",
|
|
159
|
-
commands: [["run", "check:publish"]],
|
|
160
|
-
remediation:
|
|
161
|
-
"Run `bun run check:publish` and fix the publish-consumer metadata or type-surface failure.",
|
|
162
|
-
},
|
|
163
|
-
{
|
|
164
|
-
id: "consumer",
|
|
165
|
-
commands: [["run", "check:consumers"]],
|
|
166
|
-
remediation: "Run `bun run check:consumers` and fix the isolated consumer failure.",
|
|
167
|
-
},
|
|
168
|
-
{
|
|
169
|
-
id: "cold-start",
|
|
170
|
-
commands: [["run", "check:cold-start"]],
|
|
171
|
-
remediation:
|
|
172
|
-
"Run `bun run check:cold-start` and fix the fresh scaffold install or build failure.",
|
|
173
|
-
},
|
|
174
|
-
{
|
|
175
|
-
id: "cross-runtime-deno",
|
|
176
|
-
commands: [
|
|
177
|
-
["run", "test:deno"],
|
|
178
|
-
["run", "check:deno-tarball"],
|
|
179
|
-
],
|
|
180
|
-
remediation:
|
|
181
|
-
"Run `bun run test:deno` and `bun run check:deno-tarball`, then fix the first Deno compatibility failure.",
|
|
182
|
-
},
|
|
183
|
-
{
|
|
184
|
-
id: "cross-runtime-node",
|
|
185
|
-
commands: [["run", "test:node"]],
|
|
186
|
-
remediation: "Run `bun run test:node` and fix the Node runtime adapter failure.",
|
|
187
|
-
},
|
|
188
|
-
{
|
|
189
|
-
id: "pipeline-parity",
|
|
190
|
-
commands: [["run", "check:pipeline-parity"]],
|
|
191
|
-
remediation:
|
|
192
|
-
"Run `bun run check:pipeline-parity` and fix the development and production manifest drift.",
|
|
193
|
-
},
|
|
194
|
-
// Last in release mode on purpose: this is the gate on what the release SAYS, and it is the only
|
|
195
|
-
// one whose failure is invisible afterwards - a shipped version cannot grow the changelog line it
|
|
196
|
-
// never had.
|
|
197
|
-
{
|
|
198
|
-
id: "changesets",
|
|
199
|
-
commands: [["run", "check:changesets"]],
|
|
200
|
-
remediation:
|
|
201
|
-
"Run `bun run changeset` and name every package whose source changed, so the release documents it.",
|
|
202
|
-
},
|
|
203
|
-
]
|
|
204
|
-
|
|
205
55
|
const runBunCommand = async (spec: ReleaseCommandSpec): Promise<ReleaseCommandResult> => {
|
|
206
56
|
const child = Bun.spawn([process.execPath, ...spec.args], {
|
|
207
57
|
cwd: spec.cwd,
|
|
@@ -278,14 +128,14 @@ export async function resolveVerificationRoot(start: string): Promise<string> {
|
|
|
278
128
|
}
|
|
279
129
|
}
|
|
280
130
|
|
|
281
|
-
const gatePlan = (mode: ReleaseVerificationMode): readonly
|
|
282
|
-
mode
|
|
131
|
+
const gatePlan = (mode: ReleaseVerificationMode): readonly VerificationGateSpec[] =>
|
|
132
|
+
verificationPlan(mode)
|
|
283
133
|
|
|
284
134
|
const commandLabel = (args: readonly string[]): string => `bun ${args.join(" ")}`
|
|
285
135
|
|
|
286
136
|
const runGate = async (
|
|
287
137
|
root: string,
|
|
288
|
-
gate:
|
|
138
|
+
gate: VerificationGateSpec,
|
|
289
139
|
runCommand: (spec: ReleaseCommandSpec) => Promise<ReleaseCommandResult>,
|
|
290
140
|
fixtureRoot: string,
|
|
291
141
|
): Promise<ReleaseGateResult> => {
|
|
@@ -322,7 +172,7 @@ const runGate = async (
|
|
|
322
172
|
}
|
|
323
173
|
}
|
|
324
174
|
|
|
325
|
-
const skippedGate = (gate:
|
|
175
|
+
const skippedGate = (gate: VerificationGateSpec, failedId: string): ReleaseGateResult => ({
|
|
326
176
|
id: gate.id,
|
|
327
177
|
status: "skipped",
|
|
328
178
|
commands: gate.commands.map(commandLabel),
|
|
@@ -358,6 +208,7 @@ export async function collectReleaseVerification(
|
|
|
358
208
|
ok: gates.every((gate) => gate.status === "pass"),
|
|
359
209
|
mode,
|
|
360
210
|
gates,
|
|
211
|
+
omittedReleaseGateIds: omittedVerificationGateIds(mode),
|
|
361
212
|
}
|
|
362
213
|
}
|
|
363
214
|
|
|
@@ -369,6 +220,13 @@ export function renderReleaseVerification(result: ReleaseVerificationResult): st
|
|
|
369
220
|
if (gate.message !== undefined) lines.push(` ${gate.message}`)
|
|
370
221
|
if (gate.status !== "pass") lines.push(` fix: ${gate.remediation}`)
|
|
371
222
|
}
|
|
223
|
+
if (result.omittedReleaseGateIds.length > 0) {
|
|
224
|
+
lines.push(
|
|
225
|
+
"",
|
|
226
|
+
`note: default verification omits release gates: ${result.omittedReleaseGateIds.join(", ")}`,
|
|
227
|
+
" run `bun run check:release` for the full release gate.",
|
|
228
|
+
)
|
|
229
|
+
}
|
|
372
230
|
lines.push("", result.ok ? "✓ verification passed" : "✗ verification failed")
|
|
373
231
|
return lines.join("\n")
|
|
374
232
|
}
|
package/src/rules/codes.ts
CHANGED
|
@@ -20,6 +20,10 @@ export const RULE_CODES = Object.freeze({
|
|
|
20
20
|
"NF-C017": "Rule pack validation",
|
|
21
21
|
"NF-C018": "Reserved client segment check",
|
|
22
22
|
"NF-C019": "Duplicate route registration check",
|
|
23
|
+
"NF-C020": "Island enhancer cleanup check",
|
|
24
|
+
"NF-C021": "nano binding cleanup check",
|
|
25
|
+
"NF-C022": "nano bindList key check",
|
|
26
|
+
"NF-C023": "nano computed deps check",
|
|
23
27
|
"NF-D001": "Doctor check",
|
|
24
28
|
"NF-A001": "Assurance bundle check",
|
|
25
29
|
"NF-H001": "Hydration mismatch",
|