@nifrajs/cli 2.14.1 → 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 -2346
- package/dist/check.js.map +1 -1
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +285 -10
- package/dist/cli.js.map +1 -1
- package/dist/command-catalog.d.ts +27 -1
- package/dist/command-catalog.d.ts.map +1 -1
- package/dist/command-catalog.js +142 -2
- package/dist/command-catalog.js.map +1 -1
- package/dist/doctor.d.ts +42 -0
- package/dist/doctor.d.ts.map +1 -1
- package/dist/doctor.js +64 -7
- package/dist/doctor.js.map +1 -1
- package/dist/env-file.d.ts +17 -0
- package/dist/env-file.d.ts.map +1 -0
- package/dist/env-file.js +113 -0
- package/dist/env-file.js.map +1 -0
- package/dist/fix-recipes.d.ts.map +1 -1
- package/dist/fix-recipes.js +91 -15
- package/dist/fix-recipes.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/internal/reserved-segment-codemod.d.ts +36 -0
- package/dist/internal/reserved-segment-codemod.d.ts.map +1 -0
- package/dist/internal/reserved-segment-codemod.js +134 -0
- package/dist/internal/reserved-segment-codemod.js.map +1 -0
- 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 +62 -11
- package/dist/internal/typescript-import.js.map +1 -1
- package/dist/introspect.d.ts +26 -1
- package/dist/introspect.d.ts.map +1 -1
- package/dist/introspect.js +74 -24
- package/dist/introspect.js.map +1 -1
- 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-http.js.map +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 -84
- package/dist/mcp.d.ts.map +1 -1
- package/dist/mcp.js +13 -1170
- 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/recipes/3.0.0.d.ts +10 -0
- package/dist/recipes/3.0.0.d.ts.map +1 -0
- package/dist/recipes/3.0.0.js +23 -0
- package/dist/recipes/3.0.0.js.map +1 -0
- package/dist/recipes/index.d.ts.map +1 -1
- package/dist/recipes/index.js +2 -0
- package/dist/recipes/index.js.map +1 -1
- package/dist/release-verification.d.ts +4 -1
- package/dist/release-verification.d.ts.map +1 -1
- package/dist/release-verification.js +6 -123
- 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/routes.d.ts.map +1 -1
- package/dist/rules/routes.js +17 -30
- package/dist/rules/routes.js.map +1 -1
- 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/dist/workspace-link.d.ts +37 -0
- package/dist/workspace-link.d.ts.map +1 -0
- package/dist/workspace-link.js +72 -0
- package/dist/workspace-link.js.map +1 -0
- package/docs/examples.json +37 -1
- package/docs/llms-full.txt +1598 -44
- package/docs/types.json +5157 -172
- 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 -2731
- package/src/cli.ts +291 -10
- package/src/command-catalog.ts +177 -2
- package/src/doctor.ts +99 -8
- package/src/env-file.ts +121 -0
- package/src/fix-recipes.ts +101 -15
- package/src/frontend-guidance.ts +259 -0
- package/src/internal/reserved-segment-codemod.ts +150 -0
- package/src/internal/typescript-import.ts +61 -11
- package/src/introspect.ts +82 -21
- 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 -1436
- 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/recipes/3.0.0.ts +24 -0
- package/src/recipes/index.ts +2 -0
- package/src/release-verification.ts +22 -147
- package/src/rules/codes.ts +4 -0
- package/src/rules/islands.ts +171 -0
- package/src/rules/nano.ts +295 -0
- package/src/rules/routes.ts +17 -30
- 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/workspace-link.ts +80 -0
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"
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { UpgradeRecipe } from "./index.ts"
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Upgrade recipe for the Nifra 3.0 fixed-group cutover.
|
|
5
|
+
*
|
|
6
|
+
* No package is removed and no import specifier moves this release, so the runner only pins the
|
|
7
|
+
* fixed version group. The two breaking changes are structural - a redirect return value and the
|
|
8
|
+
* reserved typed-client segment types - and stay as explicit notes, one of which points at a codemod.
|
|
9
|
+
*/
|
|
10
|
+
export const recipe: UpgradeRecipe = {
|
|
11
|
+
version: "3.0.0",
|
|
12
|
+
pins: [
|
|
13
|
+
{ match: "@nifrajs/", to: "3.0.0" },
|
|
14
|
+
{ match: "create-nifra", to: "3.0.0" },
|
|
15
|
+
{ match: "nifra", to: "3.0.0" },
|
|
16
|
+
],
|
|
17
|
+
importMoves: [],
|
|
18
|
+
notes: [
|
|
19
|
+
"Read the 2.x → 3.0 migration guide: https://nifra.dev/docs/migrate-3.",
|
|
20
|
+
"`redirect(...)` now returns plain render data, not a `Response`. Read it with `.plain` (`{ status, headers, body }`) or `toResponse()`, add headers with the second argument, and assert on `.plain` in tests. `return redirect()` / `throw redirect()` are unchanged, and cookies still apply.",
|
|
21
|
+
"Reserved typed-client segment keys are a frozen contract: a property or bracket access on a reserved-named route segment no longer type-checks. Run `nifra fix --code NF-C018` to rewrite the affected call sites; `nifra routes` annotates a colliding route with the spelling that reaches it.",
|
|
22
|
+
"On Node, `@nifrajs/proxy` defaults to the undici transport. Pass an explicit `transport` to `createProxy` to override it.",
|
|
23
|
+
],
|
|
24
|
+
}
|
package/src/recipes/index.ts
CHANGED
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
|
|
7
7
|
import { recipe as v1_8_0 } from "./1.8.0.ts"
|
|
8
8
|
import { recipe as v2_0_0 } from "./2.0.0.ts"
|
|
9
|
+
import { recipe as v3_0_0 } from "./3.0.0.ts"
|
|
9
10
|
|
|
10
11
|
/** A pin rule: every dependency whose name starts with `match` is set to version `to`. */
|
|
11
12
|
export interface PinRule {
|
|
@@ -40,6 +41,7 @@ export interface UpgradeRecipe {
|
|
|
40
41
|
const RECIPES: Record<string, UpgradeRecipe> = {
|
|
41
42
|
[v1_8_0.version]: v1_8_0,
|
|
42
43
|
[v2_0_0.version]: v2_0_0,
|
|
44
|
+
[v3_0_0.version]: v3_0_0,
|
|
43
45
|
}
|
|
44
46
|
|
|
45
47
|
export function getRecipe(version: string): UpgradeRecipe | undefined {
|
|
@@ -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,147 +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
|
-
]
|
|
107
|
-
|
|
108
|
-
const RELEASE_GATES: readonly ReleaseGateSpec[] = [
|
|
109
|
-
{
|
|
110
|
-
id: "build",
|
|
111
|
-
commands: [["run", "build"]],
|
|
112
|
-
remediation: "Run `bun run build` and fix the first package build failure.",
|
|
113
|
-
},
|
|
114
|
-
...DEFAULT_GATES.slice(0, 3),
|
|
115
|
-
{
|
|
116
|
-
id: "coverage",
|
|
117
|
-
commands: [
|
|
118
|
-
["run", "test:coverage"],
|
|
119
|
-
["run", "check:coverage"],
|
|
120
|
-
],
|
|
121
|
-
remediation:
|
|
122
|
-
"Run `bun run test:coverage` first, then `bun run check:coverage`, and fix the reported coverage regression.",
|
|
123
|
-
},
|
|
124
|
-
{
|
|
125
|
-
id: "corpus",
|
|
126
|
-
commands: [["run", "check:corpus"]],
|
|
127
|
-
remediation:
|
|
128
|
-
"Run `bun run gen:llms`, `bun run gen:api`, and `bun run gen:cards`, then rerun the corpus gate.",
|
|
129
|
-
},
|
|
130
|
-
DEFAULT_GATES[3] as ReleaseGateSpec,
|
|
131
|
-
{
|
|
132
|
-
id: "public-boundary",
|
|
133
|
-
commands: [["run", "check:public-boundary"]],
|
|
134
|
-
remediation:
|
|
135
|
-
"Run `bun run check:public-boundary` and remove the reported public-boundary violation.",
|
|
136
|
-
},
|
|
137
|
-
{
|
|
138
|
-
id: "size",
|
|
139
|
-
commands: [["run", "check:size"]],
|
|
140
|
-
remediation:
|
|
141
|
-
"Run `bun run check:size` and either reduce the bundle or update the reviewed budget.",
|
|
142
|
-
},
|
|
143
|
-
{
|
|
144
|
-
id: "core-performance",
|
|
145
|
-
commands: [["run", "check:core-performance"]],
|
|
146
|
-
remediation:
|
|
147
|
-
"Run `bun run check:core-performance` and investigate the measured performance regression.",
|
|
148
|
-
},
|
|
149
|
-
{
|
|
150
|
-
id: "publish",
|
|
151
|
-
commands: [["run", "check:publish"]],
|
|
152
|
-
remediation:
|
|
153
|
-
"Run `bun run check:publish` and fix the publish-consumer metadata or type-surface failure.",
|
|
154
|
-
},
|
|
155
|
-
{
|
|
156
|
-
id: "consumer",
|
|
157
|
-
commands: [["run", "check:consumers"]],
|
|
158
|
-
remediation: "Run `bun run check:consumers` and fix the isolated consumer failure.",
|
|
159
|
-
},
|
|
160
|
-
{
|
|
161
|
-
id: "cold-start",
|
|
162
|
-
commands: [["run", "check:cold-start"]],
|
|
163
|
-
remediation:
|
|
164
|
-
"Run `bun run check:cold-start` and fix the fresh scaffold install or build failure.",
|
|
165
|
-
},
|
|
166
|
-
{
|
|
167
|
-
id: "cross-runtime-deno",
|
|
168
|
-
commands: [
|
|
169
|
-
["run", "test:deno"],
|
|
170
|
-
["run", "check:deno-tarball"],
|
|
171
|
-
],
|
|
172
|
-
remediation:
|
|
173
|
-
"Run `bun run test:deno` and `bun run check:deno-tarball`, then fix the first Deno compatibility failure.",
|
|
174
|
-
},
|
|
175
|
-
{
|
|
176
|
-
id: "cross-runtime-node",
|
|
177
|
-
commands: [["run", "test:node"]],
|
|
178
|
-
remediation: "Run `bun run test:node` and fix the Node runtime adapter failure.",
|
|
179
|
-
},
|
|
180
|
-
{
|
|
181
|
-
id: "pipeline-parity",
|
|
182
|
-
commands: [["run", "check:pipeline-parity"]],
|
|
183
|
-
remediation:
|
|
184
|
-
"Run `bun run check:pipeline-parity` and fix the development and production manifest drift.",
|
|
185
|
-
},
|
|
186
|
-
]
|
|
187
|
-
|
|
188
55
|
const runBunCommand = async (spec: ReleaseCommandSpec): Promise<ReleaseCommandResult> => {
|
|
189
56
|
const child = Bun.spawn([process.execPath, ...spec.args], {
|
|
190
57
|
cwd: spec.cwd,
|
|
@@ -261,14 +128,14 @@ export async function resolveVerificationRoot(start: string): Promise<string> {
|
|
|
261
128
|
}
|
|
262
129
|
}
|
|
263
130
|
|
|
264
|
-
const gatePlan = (mode: ReleaseVerificationMode): readonly
|
|
265
|
-
mode
|
|
131
|
+
const gatePlan = (mode: ReleaseVerificationMode): readonly VerificationGateSpec[] =>
|
|
132
|
+
verificationPlan(mode)
|
|
266
133
|
|
|
267
134
|
const commandLabel = (args: readonly string[]): string => `bun ${args.join(" ")}`
|
|
268
135
|
|
|
269
136
|
const runGate = async (
|
|
270
137
|
root: string,
|
|
271
|
-
gate:
|
|
138
|
+
gate: VerificationGateSpec,
|
|
272
139
|
runCommand: (spec: ReleaseCommandSpec) => Promise<ReleaseCommandResult>,
|
|
273
140
|
fixtureRoot: string,
|
|
274
141
|
): Promise<ReleaseGateResult> => {
|
|
@@ -305,7 +172,7 @@ const runGate = async (
|
|
|
305
172
|
}
|
|
306
173
|
}
|
|
307
174
|
|
|
308
|
-
const skippedGate = (gate:
|
|
175
|
+
const skippedGate = (gate: VerificationGateSpec, failedId: string): ReleaseGateResult => ({
|
|
309
176
|
id: gate.id,
|
|
310
177
|
status: "skipped",
|
|
311
178
|
commands: gate.commands.map(commandLabel),
|
|
@@ -341,6 +208,7 @@ export async function collectReleaseVerification(
|
|
|
341
208
|
ok: gates.every((gate) => gate.status === "pass"),
|
|
342
209
|
mode,
|
|
343
210
|
gates,
|
|
211
|
+
omittedReleaseGateIds: omittedVerificationGateIds(mode),
|
|
344
212
|
}
|
|
345
213
|
}
|
|
346
214
|
|
|
@@ -352,6 +220,13 @@ export function renderReleaseVerification(result: ReleaseVerificationResult): st
|
|
|
352
220
|
if (gate.message !== undefined) lines.push(` ${gate.message}`)
|
|
353
221
|
if (gate.status !== "pass") lines.push(` fix: ${gate.remediation}`)
|
|
354
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
|
+
}
|
|
355
230
|
lines.push("", result.ok ? "✓ verification passed" : "✗ verification failed")
|
|
356
231
|
return lines.join("\n")
|
|
357
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",
|