ts-procedures 9.1.0 → 9.3.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/CHANGELOG.md +55 -0
- package/agent_config/claude-code/skills/ts-procedures/api-reference.md +29 -22
- package/agent_config/copilot/copilot-instructions.md +2 -0
- package/agent_config/cursor/cursorrules +2 -0
- package/build/codegen/bin/cli.d.ts +4 -0
- package/build/codegen/bin/cli.js +19 -0
- package/build/codegen/bin/cli.js.map +1 -1
- package/build/codegen/bin/cli.test.js +21 -0
- package/build/codegen/bin/cli.test.js.map +1 -1
- package/build/codegen/bin/flag-specs.js +3 -0
- package/build/codegen/bin/flag-specs.js.map +1 -1
- package/build/codegen/check-output.d.ts +28 -0
- package/build/codegen/check-output.js +216 -0
- package/build/codegen/check-output.js.map +1 -0
- package/build/codegen/check-output.test.d.ts +1 -0
- package/build/codegen/check-output.test.js +160 -0
- package/build/codegen/check-output.test.js.map +1 -0
- package/build/codegen/emit/api-route.js +1 -1
- package/build/codegen/emit/api-route.js.map +1 -1
- package/build/codegen/emit/context.d.ts +9 -0
- package/build/codegen/emit/http-stream-route.js +1 -1
- package/build/codegen/emit/http-stream-route.js.map +1 -1
- package/build/codegen/emit/rpc-route.js +1 -1
- package/build/codegen/emit/rpc-route.js.map +1 -1
- package/build/codegen/emit/scope-file.js +14 -0
- package/build/codegen/emit/scope-file.js.map +1 -1
- package/build/codegen/emit/stream-route.js +1 -1
- package/build/codegen/emit/stream-route.js.map +1 -1
- package/build/codegen/emit-scope.test.js +55 -0
- package/build/codegen/emit-scope.test.js.map +1 -1
- package/build/codegen/emit-types.js +19 -0
- package/build/codegen/emit-types.js.map +1 -1
- package/build/codegen/emit-types.test.js +34 -0
- package/build/codegen/emit-types.test.js.map +1 -1
- package/build/codegen/index.d.ts +14 -0
- package/build/codegen/index.js +11 -1
- package/build/codegen/index.js.map +1 -1
- package/build/codegen/targets/ts/name-collision.test.d.ts +1 -0
- package/build/codegen/targets/ts/name-collision.test.js +30 -0
- package/build/codegen/targets/ts/name-collision.test.js.map +1 -0
- package/build/codegen/targets/ts/run.js +13 -0
- package/build/codegen/targets/ts/run.js.map +1 -1
- package/build/core/create-http-stream.d.ts +3 -14
- package/build/core/create-http-stream.js.map +1 -1
- package/build/core/create-http.d.ts +2 -9
- package/build/core/create-http.js.map +1 -1
- package/build/core/create-http.test-d.d.ts +1 -0
- package/build/core/create-http.test-d.js +104 -0
- package/build/core/create-http.test-d.js.map +1 -0
- package/build/core/procedures.d.ts +6 -16
- package/build/core/types.d.ts +49 -4
- package/build/errors.d.ts +33 -0
- package/build/errors.js +91 -0
- package/build/errors.js.map +1 -0
- package/build/errors.test.d.ts +1 -0
- package/build/errors.test.js +122 -0
- package/build/errors.test.js.map +1 -0
- package/build/exports.d.ts +1 -1
- package/build/exports.js.map +1 -1
- package/build/implementations/http/doc-registry.d.ts +28 -0
- package/build/implementations/http/doc-registry.js +96 -0
- package/build/implementations/http/doc-registry.js.map +1 -0
- package/build/implementations/http/doc-registry.test.d.ts +1 -0
- package/build/implementations/http/doc-registry.test.js +516 -0
- package/build/implementations/http/doc-registry.test.js.map +1 -0
- package/build/implementations/http/error-taxonomy.d.ts +249 -0
- package/build/implementations/http/error-taxonomy.js +252 -0
- package/build/implementations/http/error-taxonomy.js.map +1 -0
- package/build/implementations/http/error-taxonomy.test.d.ts +1 -0
- package/build/implementations/http/error-taxonomy.test.js +399 -0
- package/build/implementations/http/error-taxonomy.test.js.map +1 -0
- package/build/implementations/http/express-rpc/error-taxonomy.test.d.ts +1 -0
- package/build/implementations/http/express-rpc/error-taxonomy.test.js +83 -0
- package/build/implementations/http/express-rpc/error-taxonomy.test.js.map +1 -0
- package/build/implementations/http/express-rpc/index.d.ts +125 -0
- package/build/implementations/http/express-rpc/index.js +216 -0
- package/build/implementations/http/express-rpc/index.js.map +1 -0
- package/build/implementations/http/express-rpc/index.test.d.ts +1 -0
- package/build/implementations/http/express-rpc/index.test.js +684 -0
- package/build/implementations/http/express-rpc/index.test.js.map +1 -0
- package/build/implementations/http/express-rpc/types.d.ts +11 -0
- package/build/implementations/http/express-rpc/types.js +2 -0
- package/build/implementations/http/express-rpc/types.js.map +1 -0
- package/build/implementations/http/hono-api/error-taxonomy.test.d.ts +1 -0
- package/build/implementations/http/hono-api/error-taxonomy.test.js +137 -0
- package/build/implementations/http/hono-api/error-taxonomy.test.js.map +1 -0
- package/build/implementations/http/hono-api/index.d.ts +151 -0
- package/build/implementations/http/hono-api/index.js +344 -0
- package/build/implementations/http/hono-api/index.js.map +1 -0
- package/build/implementations/http/hono-api/index.test.d.ts +1 -0
- package/build/implementations/http/hono-api/index.test.js +992 -0
- package/build/implementations/http/hono-api/index.test.js.map +1 -0
- package/build/implementations/http/hono-api/types.d.ts +13 -0
- package/build/implementations/http/hono-api/types.js +2 -0
- package/build/implementations/http/hono-api/types.js.map +1 -0
- package/build/implementations/http/hono-rpc/error-taxonomy.test.d.ts +1 -0
- package/build/implementations/http/hono-rpc/error-taxonomy.test.js +64 -0
- package/build/implementations/http/hono-rpc/error-taxonomy.test.js.map +1 -0
- package/build/implementations/http/hono-rpc/index.d.ts +130 -0
- package/build/implementations/http/hono-rpc/index.js +209 -0
- package/build/implementations/http/hono-rpc/index.js.map +1 -0
- package/build/implementations/http/hono-rpc/index.test.d.ts +1 -0
- package/build/implementations/http/hono-rpc/index.test.js +828 -0
- package/build/implementations/http/hono-rpc/index.test.js.map +1 -0
- package/build/implementations/http/hono-rpc/types.d.ts +11 -0
- package/build/implementations/http/hono-rpc/types.js +2 -0
- package/build/implementations/http/hono-rpc/types.js.map +1 -0
- package/build/implementations/http/hono-stream/error-taxonomy.test.d.ts +1 -0
- package/build/implementations/http/hono-stream/error-taxonomy.test.js +159 -0
- package/build/implementations/http/hono-stream/error-taxonomy.test.js.map +1 -0
- package/build/implementations/http/hono-stream/index.d.ts +171 -0
- package/build/implementations/http/hono-stream/index.js +415 -0
- package/build/implementations/http/hono-stream/index.js.map +1 -0
- package/build/implementations/http/hono-stream/index.test.d.ts +1 -0
- package/build/implementations/http/hono-stream/index.test.js +1383 -0
- package/build/implementations/http/hono-stream/index.test.js.map +1 -0
- package/build/implementations/http/hono-stream/types.d.ts +15 -0
- package/build/implementations/http/hono-stream/types.js +2 -0
- package/build/implementations/http/hono-stream/types.js.map +1 -0
- package/build/implementations/http/on-request-error.test.d.ts +1 -0
- package/build/implementations/http/on-request-error.test.js +173 -0
- package/build/implementations/http/on-request-error.test.js.map +1 -0
- package/build/implementations/http/route-errors.test.d.ts +1 -0
- package/build/implementations/http/route-errors.test.js +139 -0
- package/build/implementations/http/route-errors.test.js.map +1 -0
- package/build/implementations/types.d.ts +192 -0
- package/build/implementations/types.js +2 -0
- package/build/implementations/types.js.map +1 -0
- package/build/index.d.ts +165 -0
- package/build/index.js +258 -0
- package/build/index.js.map +1 -0
- package/build/index.test.d.ts +1 -0
- package/build/index.test.js +920 -0
- package/build/index.test.js.map +1 -0
- package/build/schema/extract-json-schema.d.ts +2 -0
- package/build/schema/extract-json-schema.js +12 -0
- package/build/schema/extract-json-schema.js.map +1 -0
- package/build/schema/extract-json-schema.test.d.ts +1 -0
- package/build/schema/extract-json-schema.test.js +23 -0
- package/build/schema/extract-json-schema.test.js.map +1 -0
- package/build/schema/parser.d.ts +28 -0
- package/build/schema/parser.js +170 -0
- package/build/schema/parser.js.map +1 -0
- package/build/schema/parser.test.d.ts +1 -0
- package/build/schema/parser.test.js +120 -0
- package/build/schema/parser.test.js.map +1 -0
- package/build/schema/resolve-schema-lib.d.ts +12 -0
- package/build/schema/resolve-schema-lib.js +11 -0
- package/build/schema/resolve-schema-lib.js.map +1 -0
- package/build/schema/resolve-schema-lib.test.d.ts +1 -0
- package/build/schema/resolve-schema-lib.test.js +17 -0
- package/build/schema/resolve-schema-lib.test.js.map +1 -0
- package/build/schema/types.d.ts +8 -0
- package/build/schema/types.js +2 -0
- package/build/schema/types.js.map +1 -0
- package/build/server/types.d.ts +6 -12
- package/build/stack-utils.d.ts +25 -0
- package/build/stack-utils.js +95 -0
- package/build/stack-utils.js.map +1 -0
- package/build/stack-utils.test.d.ts +1 -0
- package/build/stack-utils.test.js +80 -0
- package/build/stack-utils.test.js.map +1 -0
- package/docs/client-and-codegen.md +17 -0
- package/docs/core.md +1 -0
- package/docs/migration-v8-to-v9.md +12 -4
- package/package.json +1 -1
- package/src/codegen/bin/cli.test.ts +28 -0
- package/src/codegen/bin/cli.ts +26 -0
- package/src/codegen/bin/flag-specs.ts +3 -0
- package/src/codegen/check-output.test.ts +177 -0
- package/src/codegen/check-output.ts +262 -0
- package/src/codegen/emit/api-route.ts +1 -1
- package/src/codegen/emit/context.ts +9 -0
- package/src/codegen/emit/http-stream-route.ts +1 -1
- package/src/codegen/emit/rpc-route.ts +1 -1
- package/src/codegen/emit/scope-file.ts +17 -0
- package/src/codegen/emit/stream-route.ts +1 -1
- package/src/codegen/emit-scope.test.ts +60 -0
- package/src/codegen/emit-types.test.ts +36 -0
- package/src/codegen/emit-types.ts +20 -0
- package/src/codegen/index.ts +23 -1
- package/src/codegen/targets/ts/name-collision.test.ts +34 -0
- package/src/codegen/targets/ts/run.ts +16 -0
- package/src/core/create-http-stream.ts +11 -12
- package/src/core/create-http.test-d.ts +154 -0
- package/src/core/create-http.ts +11 -7
- package/src/core/types.ts +63 -4
- package/src/exports.ts +5 -0
- package/src/server/types.ts +8 -12
|
@@ -0,0 +1,262 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Opt-in self-validation of generated output (`--check`).
|
|
3
|
+
*
|
|
4
|
+
* Codegen is otherwise throw-to-fail: a bug that emits syntactically-written
|
|
5
|
+
* but non-compiling output (a dangling type reference, a duplicate identifier)
|
|
6
|
+
* exits 0 and silently overwrites a previously-working client. `--check` turns
|
|
7
|
+
* that into a loud, non-zero failure.
|
|
8
|
+
*
|
|
9
|
+
* Two modes:
|
|
10
|
+
* - `'scan'` (default): a dependency-free, in-memory scan for *dangling type
|
|
11
|
+
* references* — a type alias whose body is a bare identifier that is never
|
|
12
|
+
* declared or imported (the exact shape of the namespace-mode root-union
|
|
13
|
+
* bug, `export type Params = Root`). It deliberately does NOT try to detect
|
|
14
|
+
* duplicate identifiers: that needs an accurate model of TypeScript's
|
|
15
|
+
* declaration-merging rules to avoid false positives (a value and a type
|
|
16
|
+
* legally share a name), the duplicate-from-name-collision case is already
|
|
17
|
+
* caught deterministically at the source (see `emitScopeFile`), and `'tsc'`
|
|
18
|
+
* mode catches the rest. Keeping the scan to one high-confidence check makes
|
|
19
|
+
* it reliable and trivial to reason about.
|
|
20
|
+
* - `'tsc'`: a real `tsc --noEmit` over the generated code via the programmatic
|
|
21
|
+
* `typescript` API — authoritative, catches everything. `typescript` is a
|
|
22
|
+
* dev/peer dependency that some consumers won't have installed, so it
|
|
23
|
+
* gracefully falls back to `'scan'` (with a warning) when it can't be
|
|
24
|
+
* resolved.
|
|
25
|
+
*/
|
|
26
|
+
import { dirname, resolve } from 'node:path'
|
|
27
|
+
import type { GeneratedFile } from './targets/_shared/write-files.js'
|
|
28
|
+
|
|
29
|
+
export type CheckMode = 'scan' | 'tsc'
|
|
30
|
+
|
|
31
|
+
export interface CheckIssue {
|
|
32
|
+
/** Absolute path of the offending generated file. */
|
|
33
|
+
file: string
|
|
34
|
+
/** 1-based line number, when known. */
|
|
35
|
+
line?: number
|
|
36
|
+
message: string
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export interface CheckResult {
|
|
40
|
+
ok: boolean
|
|
41
|
+
/** The mode actually run — may differ from the requested mode after a fallback. */
|
|
42
|
+
mode: CheckMode
|
|
43
|
+
issues: CheckIssue[]
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// ---------------------------------------------------------------------------
|
|
47
|
+
// Scan mode
|
|
48
|
+
// ---------------------------------------------------------------------------
|
|
49
|
+
|
|
50
|
+
// Any exported declaration that introduces an identifier the file can later
|
|
51
|
+
// reference (so a `export type X = Foo` whose `Foo` is declared lower down
|
|
52
|
+
// doesn't read as dangling).
|
|
53
|
+
const DECL_RE =
|
|
54
|
+
/^\s*export\s+(?:declare\s+)?(?:type|interface|class|const|let|var|function|enum|namespace)\s+([A-Za-z_$][\w$]*)/
|
|
55
|
+
|
|
56
|
+
// `export type X = <body>` — body captured for the bare-identifier test.
|
|
57
|
+
const TYPE_ALIAS_RE = /^\s*export\s+type\s+([A-Za-z_$][\w$]*)\s*=\s*(.+?);?\s*$/
|
|
58
|
+
|
|
59
|
+
const IMPORT_NAMES_RE = /import\s+(?:type\s+)?(?:\*\s+as\s+\w+|(\w+)\s*,?\s*)?(?:\{([^}]*)\})?\s*from/g
|
|
60
|
+
|
|
61
|
+
const BARE_IDENT_RE = /^[A-Za-z_$][\w$]*$/
|
|
62
|
+
|
|
63
|
+
// TS lib globals / primitives a bare type-alias body may legitimately resolve
|
|
64
|
+
// to without being declared or imported in the file.
|
|
65
|
+
const BUILTIN_TYPES = new Set([
|
|
66
|
+
'string', 'number', 'boolean', 'void', 'undefined', 'null', 'unknown', 'any',
|
|
67
|
+
'never', 'object', 'symbol', 'bigint', 'this', 'true', 'false', 'Date',
|
|
68
|
+
'Array', 'ReadonlyArray', 'Record', 'Promise', 'Map', 'Set', 'Partial',
|
|
69
|
+
'Required', 'Readonly', 'Pick', 'Omit', 'Exclude', 'Extract', 'NonNullable',
|
|
70
|
+
'ReturnType', 'Parameters', 'Uint8Array', 'ArrayBuffer', 'Blob',
|
|
71
|
+
])
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Collects every identifier the file declares or imports — the set of names a
|
|
75
|
+
* bare type-alias body is allowed to reference. Declarations at any nesting are
|
|
76
|
+
* counted: the check only asks "does this name exist in the file at all", not
|
|
77
|
+
* "is it in scope here", so it never false-positives on a valid reference.
|
|
78
|
+
*/
|
|
79
|
+
function collectKnownNames(code: string): Set<string> {
|
|
80
|
+
const names = new Set<string>()
|
|
81
|
+
for (const line of code.split('\n')) {
|
|
82
|
+
const decl = DECL_RE.exec(line)
|
|
83
|
+
if (decl) names.add(decl[1]!)
|
|
84
|
+
}
|
|
85
|
+
for (const m of code.matchAll(IMPORT_NAMES_RE)) {
|
|
86
|
+
if (m[1]) names.add(m[1])
|
|
87
|
+
if (m[2]) {
|
|
88
|
+
for (const part of m[2].split(',')) {
|
|
89
|
+
const name = part.split(/\s+as\s+/).pop()?.trim()
|
|
90
|
+
if (name) names.add(name)
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
return names
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Flags `export type X = <Bare>` where `<Bare>` is a lone identifier that is
|
|
99
|
+
* neither declared nor imported in the file nor a TS built-in — the exact shape
|
|
100
|
+
* of the namespace-mode root-union bug (`export type Params = Root`). Bodies
|
|
101
|
+
* that are anything other than a single bare identifier (object literals,
|
|
102
|
+
* unions, generics, qualified names) are left alone: they're either valid or
|
|
103
|
+
* the kind of error only a real type-check can adjudicate.
|
|
104
|
+
*/
|
|
105
|
+
function scanFile(file: GeneratedFile, known: Set<string>): CheckIssue[] {
|
|
106
|
+
const issues: CheckIssue[] = []
|
|
107
|
+
const lines = file.code.split('\n')
|
|
108
|
+
for (let i = 0; i < lines.length; i += 1) {
|
|
109
|
+
const alias = TYPE_ALIAS_RE.exec(lines[i]!)
|
|
110
|
+
if (alias == null) continue
|
|
111
|
+
const rhs = alias[2]!.trim()
|
|
112
|
+
if (BARE_IDENT_RE.test(rhs) && !BUILTIN_TYPES.has(rhs) && !known.has(rhs)) {
|
|
113
|
+
issues.push({
|
|
114
|
+
file: file.path,
|
|
115
|
+
line: i + 1,
|
|
116
|
+
message: `Type alias "${alias[1]}" references "${rhs}", which is never declared or imported in this file (dangling reference).`,
|
|
117
|
+
})
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
return issues
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
export function scanGeneratedFiles(files: GeneratedFile[]): CheckIssue[] {
|
|
124
|
+
return files
|
|
125
|
+
.filter((f) => f.path.endsWith('.ts'))
|
|
126
|
+
.flatMap((f) => scanFile(f, collectKnownNames(f.code)))
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// ---------------------------------------------------------------------------
|
|
130
|
+
// tsc mode
|
|
131
|
+
// ---------------------------------------------------------------------------
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Type-checks the generated files in-process with the programmatic `typescript`
|
|
135
|
+
* API. Builds a `Program` over the in-memory `code` via a `CompilerHost` that
|
|
136
|
+
* serves generated content for generated paths and delegates everything else
|
|
137
|
+
* (the default lib + `node_modules`) to the real filesystem. Files are anchored
|
|
138
|
+
* at `outDir`, so extensionless/bare imports (`./_models`, `ts-procedures/client`)
|
|
139
|
+
* resolve under Bundler resolution exactly as the consumer's tooling resolves
|
|
140
|
+
* them: self-contained output resolves entirely within the in-memory set;
|
|
141
|
+
* non-self-contained output finds the installed `ts-procedures` by walking up
|
|
142
|
+
* from `outDir` to the nearest `node_modules`. It reads no bytes it didn't emit
|
|
143
|
+
* and writes nothing, so it can't perturb the frozen codegen output. Works in
|
|
144
|
+
* dry-run too (nothing needs to be on disk). Returns `null` to signal "couldn't
|
|
145
|
+
* run, fall back to scan" when `typescript` can't be resolved.
|
|
146
|
+
*
|
|
147
|
+
* The in-memory host approach mirrors the sibling ts-channels generator's
|
|
148
|
+
* `assertGeneratedFilesCompile`, keeping the two generators' tsc checks aligned.
|
|
149
|
+
*/
|
|
150
|
+
async function tscCheck(files: GeneratedFile[]): Promise<CheckIssue[] | null> {
|
|
151
|
+
let ts: typeof import('typescript')
|
|
152
|
+
try {
|
|
153
|
+
const mod = await import('typescript')
|
|
154
|
+
ts = (mod as { default?: typeof import('typescript') }).default ?? mod
|
|
155
|
+
} catch {
|
|
156
|
+
return null
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
const tsFiles = files.filter((f) => f.path.endsWith('.ts'))
|
|
160
|
+
if (tsFiles.length === 0) return []
|
|
161
|
+
|
|
162
|
+
// ts-procedures emits extensionless relative imports (`from './_models'`),
|
|
163
|
+
// which are illegal under NodeNext — so type-check under Bundler resolution to
|
|
164
|
+
// match how that output is actually consumed (tsx / bundlers).
|
|
165
|
+
const options: import('typescript').CompilerOptions = {
|
|
166
|
+
target: ts.ScriptTarget.ES2022,
|
|
167
|
+
module: ts.ModuleKind.ESNext,
|
|
168
|
+
moduleResolution: ts.ModuleResolutionKind.Bundler,
|
|
169
|
+
strict: true,
|
|
170
|
+
noEmit: true,
|
|
171
|
+
skipLibCheck: true,
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
const generated = new Map<string, string>()
|
|
175
|
+
const generatedDirs = new Set<string>()
|
|
176
|
+
for (const f of tsFiles) {
|
|
177
|
+
const abs = resolve(f.path)
|
|
178
|
+
generated.set(abs, f.code)
|
|
179
|
+
generatedDirs.add(dirname(abs))
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
const host = ts.createCompilerHost(options, true)
|
|
183
|
+
const baseGetSourceFile = host.getSourceFile.bind(host)
|
|
184
|
+
const baseReadFile = host.readFile.bind(host)
|
|
185
|
+
const baseFileExists = host.fileExists.bind(host)
|
|
186
|
+
const baseDirectoryExists = host.directoryExists?.bind(host)
|
|
187
|
+
host.getSourceFile = (fileName, languageVersionOrOptions, onError, shouldCreate) => {
|
|
188
|
+
const content = generated.get(resolve(fileName))
|
|
189
|
+
if (content !== undefined) return ts.createSourceFile(fileName, content, languageVersionOrOptions, true)
|
|
190
|
+
return baseGetSourceFile(fileName, languageVersionOrOptions, onError, shouldCreate)
|
|
191
|
+
}
|
|
192
|
+
host.readFile = (fileName) => {
|
|
193
|
+
const content = generated.get(resolve(fileName))
|
|
194
|
+
return content !== undefined ? content : baseReadFile(fileName)
|
|
195
|
+
}
|
|
196
|
+
host.fileExists = (fileName) => generated.has(resolve(fileName)) || baseFileExists(fileName)
|
|
197
|
+
// Without this, Bundler resolution of `./_types` etc. bails when the virtual
|
|
198
|
+
// outDir doesn't exist on disk (e.g. --dry-run) and every relative import
|
|
199
|
+
// reports TS2307, cascading into TS7006 implicit-any noise.
|
|
200
|
+
host.directoryExists = (d) => generatedDirs.has(resolve(d)) || (baseDirectoryExists?.(d) ?? false)
|
|
201
|
+
|
|
202
|
+
const program = ts.createProgram([...generated.keys()], options, host)
|
|
203
|
+
const diagnostics = ts.getPreEmitDiagnostics(program).filter((d) => d.file != null && generated.has(resolve(d.file.fileName)))
|
|
204
|
+
|
|
205
|
+
return diagnostics.map((d) => {
|
|
206
|
+
const message = ts.flattenDiagnosticMessageText(d.messageText, '\n')
|
|
207
|
+
const issue: CheckIssue = {
|
|
208
|
+
file: d.file!.fileName,
|
|
209
|
+
message: `TS${d.code}: ${message}`,
|
|
210
|
+
}
|
|
211
|
+
if (d.start != null) {
|
|
212
|
+
issue.line = d.file!.getLineAndCharacterOfPosition(d.start).line + 1
|
|
213
|
+
}
|
|
214
|
+
return issue
|
|
215
|
+
})
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// ---------------------------------------------------------------------------
|
|
219
|
+
// Orchestration
|
|
220
|
+
// ---------------------------------------------------------------------------
|
|
221
|
+
|
|
222
|
+
export interface CheckOptions {
|
|
223
|
+
mode: CheckMode
|
|
224
|
+
/** Surface fallback warnings to the user. */
|
|
225
|
+
logger?: (message: string) => void
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
export async function checkGeneratedFiles(
|
|
229
|
+
files: GeneratedFile[],
|
|
230
|
+
options: CheckOptions,
|
|
231
|
+
): Promise<CheckResult> {
|
|
232
|
+
if (options.mode === 'tsc') {
|
|
233
|
+
// The in-memory type-check works from the emitted code, so it runs even in
|
|
234
|
+
// --dry-run. The only reason to fall back is a missing `typescript` peer.
|
|
235
|
+
const tscIssues = await tscCheck(files)
|
|
236
|
+
if (tscIssues != null) {
|
|
237
|
+
return { ok: tscIssues.length === 0, mode: 'tsc', issues: tscIssues }
|
|
238
|
+
}
|
|
239
|
+
options.logger?.(
|
|
240
|
+
'[ts-procedures-codegen] --check-mode tsc requested but `typescript` could not be resolved. Falling back to the scan check.',
|
|
241
|
+
)
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
const issues = scanGeneratedFiles(files)
|
|
245
|
+
return { ok: issues.length === 0, mode: 'scan', issues }
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
/**
|
|
249
|
+
* Renders a failed {@link CheckResult} as a multi-line, human-readable report.
|
|
250
|
+
* Shared by the CLI (stderr) and `generateClient({ validate })` (thrown error)
|
|
251
|
+
* so both surface findings identically.
|
|
252
|
+
*/
|
|
253
|
+
export function formatCheckResult(result: CheckResult): string {
|
|
254
|
+
const lines = [
|
|
255
|
+
`[ts-procedures-codegen] Self-check (${result.mode}) found ${result.issues.length} problem(s) in the generated output:`,
|
|
256
|
+
]
|
|
257
|
+
for (const issue of result.issues) {
|
|
258
|
+
const loc = issue.line != null ? `:${issue.line}` : ''
|
|
259
|
+
lines.push(` ${issue.file}${loc} — ${issue.message}`)
|
|
260
|
+
}
|
|
261
|
+
return lines.join('\n')
|
|
262
|
+
}
|
|
@@ -180,5 +180,5 @@ export async function emitApiRoute(route: APIHttpRouteDoc, ctx: EmitRouteContext
|
|
|
180
180
|
? injectRouteErrors(declarations, pascal, errorUnion, ctx.namespaceTypes)
|
|
181
181
|
: errorUnion !== null // flat mode already emitted errors above
|
|
182
182
|
|
|
183
|
-
return { typeDeclarations: declarations, callable, hasStream: false, hasErrors: hasErrorsInjected }
|
|
183
|
+
return { typeDeclarations: declarations, callable, hasStream: false, hasErrors: hasErrorsInjected, routePascal: pascal }
|
|
184
184
|
}
|
|
@@ -13,6 +13,15 @@ export interface RouteChunks {
|
|
|
13
13
|
hasStream: boolean
|
|
14
14
|
/** True when this route emitted an `Errors` type (drives the `_errors` import at the top of the scope file). */
|
|
15
15
|
hasErrors: boolean
|
|
16
|
+
/**
|
|
17
|
+
* The PascalCase name this route emits its types/namespace under (e.g.
|
|
18
|
+
* `GetTask`). `emitScopeFile` checks these for collisions across routes in
|
|
19
|
+
* the scope — two routes whose names pascalize to the same value (e.g.
|
|
20
|
+
* `get-task` and `getTask`, or `Foo`@v2 and `FooV2`@v1) would otherwise emit
|
|
21
|
+
* merged namespaces / duplicate `export type` declarations and a duplicated
|
|
22
|
+
* callable key, none of which compile.
|
|
23
|
+
*/
|
|
24
|
+
routePascal: string
|
|
16
25
|
}
|
|
17
26
|
|
|
18
27
|
export interface EmitRouteContext {
|
|
@@ -158,5 +158,5 @@ export async function emitHttpStreamRoute(route: HttpStreamRouteDoc, ctx: EmitRo
|
|
|
158
158
|
ctx.namespaceTypes
|
|
159
159
|
)
|
|
160
160
|
|
|
161
|
-
return { typeDeclarations: declarations, callable, hasStream: true, hasErrors }
|
|
161
|
+
return { typeDeclarations: declarations, callable, hasStream: true, hasErrors, routePascal: pascal }
|
|
162
162
|
}
|
|
@@ -45,5 +45,5 @@ export async function emitRpcRoute(route: RPCHttpRouteDoc, ctx: EmitRouteContext
|
|
|
45
45
|
|
|
46
46
|
const hasErrorsInjected = injectRouteErrors(declarations, pascal, errorUnion, ctx.namespaceTypes)
|
|
47
47
|
|
|
48
|
-
return { typeDeclarations: declarations, callable, hasStream: false, hasErrors: hasErrorsInjected }
|
|
48
|
+
return { typeDeclarations: declarations, callable, hasStream: false, hasErrors: hasErrorsInjected, routePascal: pascal }
|
|
49
49
|
}
|
|
@@ -105,6 +105,13 @@ export async function emitScopeFile(
|
|
|
105
105
|
const callables: string[] = []
|
|
106
106
|
let hasStream = false
|
|
107
107
|
let scopeHasErrors = false
|
|
108
|
+
// routePascal → the first route name that claimed it. Two routes whose names
|
|
109
|
+
// pascalize to the same value (e.g. `get-task`/`getTask`, or `Foo`@v2 and
|
|
110
|
+
// `FooV2`@v1) would emit merged namespaces, duplicate `export type`
|
|
111
|
+
// declarations, and a duplicated callable key — none of which compile. We
|
|
112
|
+
// fail fast with an actionable message rather than silently emit a broken
|
|
113
|
+
// scope file.
|
|
114
|
+
const pascalToName = new Map<string, string>()
|
|
108
115
|
|
|
109
116
|
for (const route of group.routes) {
|
|
110
117
|
let chunks: RouteChunks
|
|
@@ -129,6 +136,16 @@ export async function emitScopeFile(
|
|
|
129
136
|
)
|
|
130
137
|
}
|
|
131
138
|
|
|
139
|
+
const prior = pascalToName.get(chunks.routePascal)
|
|
140
|
+
if (prior != null) {
|
|
141
|
+
throw new Error(
|
|
142
|
+
`[ts-procedures-codegen] Scope "${group.scopeKey}": routes "${prior}" and "${route.name}" both generate the name "${chunks.routePascal}", ` +
|
|
143
|
+
`whose generated types and callable would collide (duplicate identifier / duplicate object key). ` +
|
|
144
|
+
`Rename one of the routes so they pascalize to distinct names.`,
|
|
145
|
+
)
|
|
146
|
+
}
|
|
147
|
+
pascalToName.set(chunks.routePascal, route.name)
|
|
148
|
+
|
|
132
149
|
allTypeDeclarations.push(...chunks.typeDeclarations)
|
|
133
150
|
callables.push(chunks.callable)
|
|
134
151
|
if (chunks.hasStream) hasStream = true
|
|
@@ -77,5 +77,5 @@ export async function emitStreamRoute(route: StreamHttpRouteDoc, ctx: EmitRouteC
|
|
|
77
77
|
ctx.namespaceTypes
|
|
78
78
|
)
|
|
79
79
|
|
|
80
|
-
return { typeDeclarations: declarations, callable, hasStream: true, hasErrors }
|
|
80
|
+
return { typeDeclarations: declarations, callable, hasStream: true, hasErrors, routePascal: pascal }
|
|
81
81
|
}
|
|
@@ -1748,4 +1748,64 @@ describe('emitScopeFile errors-only routes (DX #6)', () => {
|
|
|
1748
1748
|
expect(out).toContain('export type LogoutErrors = _errors.Unauthorized')
|
|
1749
1749
|
expect(out).toContain('client.bindCallableTyped<void, void, LogoutErrors>')
|
|
1750
1750
|
})
|
|
1751
|
+
|
|
1752
|
+
// Bug repro (downstream): two routes whose names pascalize to the same value
|
|
1753
|
+
// (`get-task`/`getTask`) emit merged namespaces with duplicate `export type`
|
|
1754
|
+
// declarations (the `Ref`/`Params` the downstream saw) AND a duplicated
|
|
1755
|
+
// callable key — none of which compile. We fail fast with both route names.
|
|
1756
|
+
it('throws when two routes in a scope pascalize to the same name', async () => {
|
|
1757
|
+
const group: ScopeGroup = {
|
|
1758
|
+
scopeKey: 'tasks',
|
|
1759
|
+
camelCase: 'tasks',
|
|
1760
|
+
routes: [
|
|
1761
|
+
{
|
|
1762
|
+
kind: 'rpc', name: 'get-task', path: '/t/1', method: 'post', scope: 'tasks', version: 1,
|
|
1763
|
+
jsonSchema: { body: { type: 'object', properties: { ref: { type: 'object', properties: { a: { type: 'string' } }, required: ['a'] } }, required: ['ref'] } },
|
|
1764
|
+
} satisfies RPCHttpRouteDoc,
|
|
1765
|
+
{
|
|
1766
|
+
kind: 'rpc', name: 'getTask', path: '/t/2', method: 'post', scope: 'tasks', version: 1,
|
|
1767
|
+
jsonSchema: { body: { type: 'object', properties: { ref: { type: 'object', properties: { b: { type: 'number' } }, required: ['b'] } }, required: ['ref'] } },
|
|
1768
|
+
} satisfies RPCHttpRouteDoc,
|
|
1769
|
+
],
|
|
1770
|
+
}
|
|
1771
|
+
await expect(emitScopeFile(group, { namespaceTypes: true })).rejects.toThrow(
|
|
1772
|
+
/routes "get-task" and "getTask" both generate the name "GetTask"/,
|
|
1773
|
+
)
|
|
1774
|
+
})
|
|
1775
|
+
|
|
1776
|
+
// Version-suffix collision: `Foo`@v2 → `FooV2` and `FooV2`@v1 → `FooV2`.
|
|
1777
|
+
it('throws when a version suffix collides with a literal route name', async () => {
|
|
1778
|
+
const group: ScopeGroup = {
|
|
1779
|
+
scopeKey: 'tasks',
|
|
1780
|
+
camelCase: 'tasks',
|
|
1781
|
+
routes: [
|
|
1782
|
+
{ kind: 'rpc', name: 'Foo', path: '/t/1', method: 'post', scope: 'tasks', version: 2, jsonSchema: {} } satisfies RPCHttpRouteDoc,
|
|
1783
|
+
{ kind: 'rpc', name: 'FooV2', path: '/t/2', method: 'post', scope: 'tasks', version: 1, jsonSchema: {} } satisfies RPCHttpRouteDoc,
|
|
1784
|
+
],
|
|
1785
|
+
}
|
|
1786
|
+
await expect(emitScopeFile(group, { namespaceTypes: true })).rejects.toThrow(/both generate the name "FooV2"/)
|
|
1787
|
+
})
|
|
1788
|
+
|
|
1789
|
+
// Distinct pascal names — including the same name across DIFFERENT scopes and
|
|
1790
|
+
// same-name-different-version within a scope — must NOT trip the guard.
|
|
1791
|
+
it('allows two routes with the same property-derived sub-type in distinct route namespaces', async () => {
|
|
1792
|
+
const group: ScopeGroup = {
|
|
1793
|
+
scopeKey: 'tasks',
|
|
1794
|
+
camelCase: 'tasks',
|
|
1795
|
+
routes: [
|
|
1796
|
+
{
|
|
1797
|
+
kind: 'rpc', name: 'CreateTask', path: '/t/1', method: 'post', scope: 'tasks', version: 1,
|
|
1798
|
+
jsonSchema: { body: { type: 'object', properties: { ref: { type: 'object', properties: { a: { type: 'string' } }, required: ['a'] } }, required: ['ref'] } },
|
|
1799
|
+
} satisfies RPCHttpRouteDoc,
|
|
1800
|
+
{
|
|
1801
|
+
kind: 'rpc', name: 'UpdateTask', path: '/t/2', method: 'post', scope: 'tasks', version: 1,
|
|
1802
|
+
jsonSchema: { body: { type: 'object', properties: { ref: { type: 'object', properties: { b: { type: 'number' } }, required: ['b'] } }, required: ['ref'] } },
|
|
1803
|
+
} satisfies RPCHttpRouteDoc,
|
|
1804
|
+
],
|
|
1805
|
+
}
|
|
1806
|
+
const out = await emitScopeFile(group, { namespaceTypes: true })
|
|
1807
|
+
// Both Refs survive, each fenced inside its own route namespace.
|
|
1808
|
+
expect(out).toContain('export namespace CreateTask')
|
|
1809
|
+
expect(out).toContain('export namespace UpdateTask')
|
|
1810
|
+
})
|
|
1751
1811
|
})
|
|
@@ -179,6 +179,42 @@ describe('jsonSchemaToExtractedTypes', () => {
|
|
|
179
179
|
expect(result!.body).toContain('id: string')
|
|
180
180
|
})
|
|
181
181
|
|
|
182
|
+
// Bug repro (downstream): a ROOT-LEVEL union (anyOf/oneOf, no top-level
|
|
183
|
+
// `type`) whose arms ajsc can merge makes ajsc's inlineTypes:false path emit
|
|
184
|
+
// `export type Root = Root;` — a self-reference to a name it never declares.
|
|
185
|
+
// Stripping the prefix left `body === 'Root'`, which the namespace emitter
|
|
186
|
+
// wrapped as `export type Params = Root` → TS2304. The body must instead be
|
|
187
|
+
// inlined exactly as flat mode produces it.
|
|
188
|
+
it('inlines a root-level mergeable union instead of emitting a dangling Root self-reference', async () => {
|
|
189
|
+
const schema = {
|
|
190
|
+
anyOf: [
|
|
191
|
+
{ type: 'object', properties: { a: { type: 'string' } }, required: ['a'] },
|
|
192
|
+
{ type: 'object', properties: { a: { type: 'string' } }, required: ['a'] },
|
|
193
|
+
],
|
|
194
|
+
}
|
|
195
|
+
const result = await jsonSchemaToExtractedTypes(schema)
|
|
196
|
+
expect(result).not.toBeUndefined()
|
|
197
|
+
expect(result!.body).not.toBe('Root')
|
|
198
|
+
expect(result!.declarations.some((d) => /^export\s+type\s+Root\b/.test(d))).toBe(false)
|
|
199
|
+
expect(result!.body).toContain('a: string')
|
|
200
|
+
})
|
|
201
|
+
|
|
202
|
+
// A genuinely distinct root union must keep working (it never produced the
|
|
203
|
+
// degenerate `Root` body — guard against the fix over-reaching).
|
|
204
|
+
it('keeps a root-level union of distinct objects inlined', async () => {
|
|
205
|
+
const schema = {
|
|
206
|
+
anyOf: [
|
|
207
|
+
{ type: 'object', properties: { a: { type: 'string' } }, required: ['a'] },
|
|
208
|
+
{ type: 'object', properties: { b: { type: 'number' } }, required: ['b'] },
|
|
209
|
+
],
|
|
210
|
+
}
|
|
211
|
+
const result = await jsonSchemaToExtractedTypes(schema)
|
|
212
|
+
expect(result).not.toBeUndefined()
|
|
213
|
+
expect(result!.body).not.toBe('Root')
|
|
214
|
+
expect(result!.body).toContain('a: string')
|
|
215
|
+
expect(result!.body).toContain('b: number')
|
|
216
|
+
})
|
|
217
|
+
|
|
182
218
|
// Bug repro (downstream): codegen emits `Cannot find name 'RootType'`.
|
|
183
219
|
// ajsc with inlineTypes:false renders an array-root schema as TWO blocks:
|
|
184
220
|
// export type RootType = { ... };
|
|
@@ -241,6 +241,26 @@ export async function jsonSchemaToExtractedTypes(
|
|
|
241
241
|
.trim()
|
|
242
242
|
}
|
|
243
243
|
|
|
244
|
+
// Degenerate self-reference guard. When a ROOT-LEVEL union (`anyOf`/`oneOf`
|
|
245
|
+
// with no top-level `type`) has arms ajsc can merge, ajsc's inlineTypes:false
|
|
246
|
+
// path emits `export type Root = Root;` — a self-reference to a name it never
|
|
247
|
+
// declares. Stripping the `Root =` prefix above leaves `body === 'Root'` with
|
|
248
|
+
// no `Root` declaration, so the namespace emitter would wrap it as
|
|
249
|
+
// `export type Params = Root` and the output wouldn't compile (TS2304). Flat
|
|
250
|
+
// mode never hits this because it force-inlines everything; here we do the
|
|
251
|
+
// same — recompute the body via the inline path so the union is materialised
|
|
252
|
+
// as `{ … } | { … }` exactly as flat mode produces it.
|
|
253
|
+
const hasRootDecl = declarations.some((d) => extractedDeclName(d) === 'Root')
|
|
254
|
+
if (body === 'Root' && !hasRootDecl) {
|
|
255
|
+
const inline = await resolveTypeBody(schema, options)
|
|
256
|
+
return {
|
|
257
|
+
declarations: [],
|
|
258
|
+
body: inline.body,
|
|
259
|
+
referencedNamedTypes: inline.referencedNamedTypes,
|
|
260
|
+
extractedTypeNames: [],
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
|
|
244
264
|
return { declarations, body, referencedNamedTypes, extractedTypeNames }
|
|
245
265
|
}
|
|
246
266
|
|
package/src/codegen/index.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { resolveEnvelope, type ResolveInput } from './resolve-envelope.js'
|
|
2
2
|
import { runPipeline, type GeneratedFile } from './pipeline.js'
|
|
3
|
+
import { checkGeneratedFiles, formatCheckResult, type CheckMode } from './check-output.js'
|
|
3
4
|
import type { AjscOptions } from './emit-types.js'
|
|
4
5
|
import type { SharedTypesImportMap } from './collect-models.js'
|
|
5
6
|
import type { KotlinEmitter } from './targets/kotlin/ajsc-adapter.js'
|
|
@@ -22,6 +23,15 @@ export interface GenerateClientOptions extends ResolveInput {
|
|
|
22
23
|
sharedModelsModule?: string
|
|
23
24
|
/** Hard-fail codegen if any $id-bearing model would be generated as a local structural twin. */
|
|
24
25
|
strictSharedModels?: boolean
|
|
26
|
+
/**
|
|
27
|
+
* Self-validate the generated output and throw if it won't compile, instead of
|
|
28
|
+
* silently returning a broken client (the programmatic peer of the CLI's
|
|
29
|
+
* `--check`). `true` runs the dependency-free `'scan'`; pass `{ mode: 'tsc' }`
|
|
30
|
+
* for a full type-check via the optional `typescript` peer (falls back to the
|
|
31
|
+
* scan if it isn't installed). The check runs on the in-memory output, so it
|
|
32
|
+
* works under `dryRun` too. TS target only. (default: off)
|
|
33
|
+
*/
|
|
34
|
+
validate?: boolean | { mode?: CheckMode }
|
|
25
35
|
/**
|
|
26
36
|
* Sink for non-error progress messages (e.g. the shared-models summary).
|
|
27
37
|
* Omitted by default so programmatic callers produce no console output; pass
|
|
@@ -42,7 +52,7 @@ export interface GenerateClientOptions extends ResolveInput {
|
|
|
42
52
|
|
|
43
53
|
export async function generateClient(options: GenerateClientOptions): Promise<GeneratedFile[]> {
|
|
44
54
|
const envelope = await resolveEnvelope(options)
|
|
45
|
-
|
|
55
|
+
const files = await runPipeline({
|
|
46
56
|
envelope,
|
|
47
57
|
outDir: options.outDir,
|
|
48
58
|
ajsc: options.ajsc,
|
|
@@ -66,10 +76,22 @@ export async function generateClient(options: GenerateClientOptions): Promise<Ge
|
|
|
66
76
|
swiftAccessLevel: options.swiftAccessLevel,
|
|
67
77
|
swiftEmitter: options.swiftEmitter,
|
|
68
78
|
})
|
|
79
|
+
|
|
80
|
+
if (options.validate) {
|
|
81
|
+
const mode: CheckMode = options.validate === true ? 'scan' : options.validate.mode ?? 'scan'
|
|
82
|
+
const result = await checkGeneratedFiles(files, { mode, logger: options.logger })
|
|
83
|
+
if (!result.ok) {
|
|
84
|
+
throw new Error(`${formatCheckResult(result)}\nThe generated client may not compile.`)
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
return files
|
|
69
89
|
}
|
|
70
90
|
|
|
71
91
|
export type { AjscOptions } from './emit-types.js'
|
|
72
92
|
export type { ResolveInput } from './resolve-envelope.js'
|
|
93
|
+
export { checkGeneratedFiles, scanGeneratedFiles, formatCheckResult } from './check-output.js'
|
|
94
|
+
export type { CheckMode, CheckIssue, CheckResult } from './check-output.js'
|
|
73
95
|
|
|
74
96
|
/**
|
|
75
97
|
* @internal Subject to change with ajsc minor versions. Use for test injection
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest'
|
|
2
|
+
import { runPipeline } from '../../pipeline.js'
|
|
3
|
+
import { makeRpcRoute, makeEnvelope } from '../../__fixtures__/make-envelope.js'
|
|
4
|
+
|
|
5
|
+
// Bug repro (downstream): `--service-name Godmode` together with a scope named
|
|
6
|
+
// `godmode` emitted two `export type GodmodeClient = …` declarations (the
|
|
7
|
+
// aggregate client type AND the scope port type) → duplicate identifier. We now
|
|
8
|
+
// fail fast at generation time with both knobs the user can turn.
|
|
9
|
+
describe('ts target: --service-name vs scope-name collision', () => {
|
|
10
|
+
const envelope = makeEnvelope([
|
|
11
|
+
makeRpcRoute({
|
|
12
|
+
name: 'DoThing',
|
|
13
|
+
scope: 'godmode',
|
|
14
|
+
jsonSchema: { body: { type: 'object', properties: { id: { type: 'string' } }, required: ['id'] } },
|
|
15
|
+
}),
|
|
16
|
+
])
|
|
17
|
+
|
|
18
|
+
it('throws when a scope pascalizes to the same name as --service-name', async () => {
|
|
19
|
+
await expect(
|
|
20
|
+
runPipeline({ envelope, outDir: 'out', dryRun: true, serviceName: 'Godmode', namespaceTypes: true }),
|
|
21
|
+
).rejects.toThrow(/Scope "godmode" produces client port type "GodmodeClient" which collides/)
|
|
22
|
+
})
|
|
23
|
+
|
|
24
|
+
it('does not throw when --service-name differs from every scope', async () => {
|
|
25
|
+
const files = await runPipeline({
|
|
26
|
+
envelope,
|
|
27
|
+
outDir: 'out',
|
|
28
|
+
dryRun: true,
|
|
29
|
+
serviceName: 'GodmodeApi',
|
|
30
|
+
namespaceTypes: true,
|
|
31
|
+
})
|
|
32
|
+
expect(files.length).toBeGreaterThan(0)
|
|
33
|
+
})
|
|
34
|
+
})
|
|
@@ -16,6 +16,7 @@ import { emitClientTypesFile } from '../../emit-client-types.js'
|
|
|
16
16
|
import { emitClientRuntimeFile } from '../../emit-client-runtime.js'
|
|
17
17
|
import { collectModels, resolveModelImports } from '../../collect-models.js'
|
|
18
18
|
import { emitModelsFile } from '../../emit-models.js'
|
|
19
|
+
import { toPascalCase } from '../../naming.js'
|
|
19
20
|
|
|
20
21
|
export type TsRunInput = TargetRunInput
|
|
21
22
|
|
|
@@ -48,6 +49,21 @@ export async function runTsPipeline(input: TsRunInput): Promise<GeneratedFile[]>
|
|
|
48
49
|
envelope.errors.filter((e) => e.schema != null).map((e) => e.name),
|
|
49
50
|
)
|
|
50
51
|
|
|
52
|
+
// The index emits one aggregate `${ServiceName}Client` type AND one
|
|
53
|
+
// `${ScopePascal}Client` port type per scope. When a scope's PascalCase name
|
|
54
|
+
// equals the service name's, the two declarations collide (duplicate
|
|
55
|
+
// identifier). Fail fast with both knobs the user can turn, rather than emit
|
|
56
|
+
// a non-compiling index.
|
|
57
|
+
const servicePascal = toPascalCase(serviceName)
|
|
58
|
+
for (const group of groups) {
|
|
59
|
+
if (toPascalCase(group.camelCase) === servicePascal) {
|
|
60
|
+
throw new Error(
|
|
61
|
+
`[ts-procedures-codegen] Scope "${group.scopeKey}" produces client port type "${servicePascal}Client" which collides with the aggregate client type "${servicePascal}Client" derived from --service-name "${serviceName}". ` +
|
|
62
|
+
`Rename the scope or change --service-name so they differ.`,
|
|
63
|
+
)
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
51
67
|
if (selfContained) {
|
|
52
68
|
for (const group of groups) {
|
|
53
69
|
if (group.scopeKey === '_types' || group.scopeKey === '_client') {
|
|
@@ -11,11 +11,15 @@ import {
|
|
|
11
11
|
validateReqChannels,
|
|
12
12
|
} from './internal.js'
|
|
13
13
|
import type { FactoryRuntime } from './internal.js'
|
|
14
|
-
import type {
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
14
|
+
import type {
|
|
15
|
+
HttpMethod,
|
|
16
|
+
HttpReq,
|
|
17
|
+
ProcedureResult,
|
|
18
|
+
THttpReqInput,
|
|
19
|
+
THttpStreamSchema,
|
|
20
|
+
TStreamContext,
|
|
21
|
+
THttpStreamProcedureRegistration,
|
|
22
|
+
} from './types.js'
|
|
19
23
|
|
|
20
24
|
export function makeCreateHttpStream<TContext>(runtime: FactoryRuntime<TContext, any>) {
|
|
21
25
|
/**
|
|
@@ -29,7 +33,7 @@ export function makeCreateHttpStream<TContext>(runtime: FactoryRuntime<TContext,
|
|
|
29
33
|
*/
|
|
30
34
|
return function CreateHttpStream<
|
|
31
35
|
TName extends string,
|
|
32
|
-
TReq extends
|
|
36
|
+
TReq extends THttpReqInput | undefined,
|
|
33
37
|
TYieldType,
|
|
34
38
|
TReturnType = void,
|
|
35
39
|
TResHeaders = undefined,
|
|
@@ -42,12 +46,7 @@ export function makeCreateHttpStream<TContext>(runtime: FactoryRuntime<TContext,
|
|
|
42
46
|
scope?: string
|
|
43
47
|
errors?: TErrorKey[]
|
|
44
48
|
description?: string
|
|
45
|
-
schema:
|
|
46
|
-
req?: TReq
|
|
47
|
-
yield?: TYieldType
|
|
48
|
-
returnType?: TReturnType
|
|
49
|
-
res?: TResHeaders extends undefined ? undefined : { headers: TResHeaders }
|
|
50
|
-
}
|
|
49
|
+
schema: THttpStreamSchema<TReq, TYieldType, TReturnType, TResHeaders>
|
|
51
50
|
validateYields?: boolean
|
|
52
51
|
},
|
|
53
52
|
handler: TResHeaders extends undefined
|