@pikku/inspector 0.12.75 → 0.12.81

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.
Files changed (219) hide show
  1. package/CHANGELOG.md +330 -0
  2. package/dist/add/add-analytics.d.ts +9 -0
  3. package/dist/add/add-analytics.js +178 -0
  4. package/dist/add/add-auth.js +0 -54
  5. package/dist/add/add-feature-flag.d.ts +2 -0
  6. package/dist/add/add-feature-flag.js +162 -0
  7. package/dist/add/add-wire-addon.js +1 -0
  8. package/dist/add/add-wire-remote-addon.js +1 -0
  9. package/dist/index.d.ts +3 -1
  10. package/dist/index.js +3 -1
  11. package/dist/inspector.js +4 -0
  12. package/dist/surface/collect-surface.d.ts +58 -0
  13. package/dist/surface/collect-surface.js +613 -0
  14. package/dist/types.d.ts +20 -11
  15. package/dist/utils/filter-inspector-state.js +14 -0
  16. package/dist/utils/post-process.js +15 -0
  17. package/dist/utils/read-module-specifiers.d.ts +8 -0
  18. package/dist/utils/read-module-specifiers.js +23 -0
  19. package/dist/utils/read-tsconfig-out-dir.d.ts +5 -0
  20. package/dist/utils/read-tsconfig-out-dir.js +12 -0
  21. package/dist/utils/resolve-deploy-target.d.ts +7 -0
  22. package/dist/utils/resolve-deploy-target.js +15 -7
  23. package/dist/utils/serialize-inspector-state.d.ts +5 -0
  24. package/dist/utils/serialize-inspector-state.js +8 -0
  25. package/dist/visit.js +4 -0
  26. package/package.json +16 -6
  27. package/run-tests.sh +21 -6
  28. package/src/add/add-addon-bans.ts +0 -85
  29. package/src/add/add-agent-scorer.test.ts +0 -197
  30. package/src/add/add-agent-scorer.ts +0 -83
  31. package/src/add/add-agent.test.ts +0 -87
  32. package/src/add/add-agent.ts +0 -638
  33. package/src/add/add-approval-description.ts +0 -76
  34. package/src/add/add-auth.test.ts +0 -791
  35. package/src/add/add-auth.ts +0 -424
  36. package/src/add/add-channel.ts +0 -778
  37. package/src/add/add-cli-renderers.test.ts +0 -74
  38. package/src/add/add-cli.ts +0 -1006
  39. package/src/add/add-credential.ts +0 -186
  40. package/src/add/add-feature.test.ts +0 -306
  41. package/src/add/add-feature.ts +0 -253
  42. package/src/add/add-file-extends-core-type.ts +0 -66
  43. package/src/add/add-file-with-config.ts +0 -45
  44. package/src/add/add-file-with-factory.test.ts +0 -134
  45. package/src/add/add-file-with-factory.ts +0 -288
  46. package/src/add/add-functions.test.ts +0 -743
  47. package/src/add/add-functions.ts +0 -1548
  48. package/src/add/add-gateway.ts +0 -143
  49. package/src/add/add-http-route-ref.test.ts +0 -149
  50. package/src/add/add-http-route.ts +0 -530
  51. package/src/add/add-http-routes.ts +0 -471
  52. package/src/add/add-keyed-wiring.ts +0 -180
  53. package/src/add/add-mcp-prompt.ts +0 -157
  54. package/src/add/add-mcp-resource.ts +0 -175
  55. package/src/add/add-middleware.test.ts +0 -196
  56. package/src/add/add-middleware.ts +0 -879
  57. package/src/add/add-permission-auth.test.ts +0 -59
  58. package/src/add/add-permission.ts +0 -311
  59. package/src/add/add-personas.test.ts +0 -406
  60. package/src/add/add-personas.ts +0 -287
  61. package/src/add/add-queue-worker.ts +0 -117
  62. package/src/add/add-rpc-invocations.test.ts +0 -287
  63. package/src/add/add-rpc-invocations.ts +0 -221
  64. package/src/add/add-scenario-step.test.ts +0 -509
  65. package/src/add/add-schedule.ts +0 -120
  66. package/src/add/add-scope.test.ts +0 -419
  67. package/src/add/add-scope.ts +0 -221
  68. package/src/add/add-secret.ts +0 -10
  69. package/src/add/add-subject-scenario-steps.test.ts +0 -161
  70. package/src/add/add-system-role.test.ts +0 -297
  71. package/src/add/add-system-role.ts +0 -162
  72. package/src/add/add-trigger.ts +0 -165
  73. package/src/add/add-variable.ts +0 -10
  74. package/src/add/add-wire-addon.test.ts +0 -185
  75. package/src/add/add-wire-addon.ts +0 -146
  76. package/src/add/add-wire-remote-addon.ts +0 -80
  77. package/src/add/add-workflow-edge-cases.test.ts +0 -201
  78. package/src/add/add-workflow-fanout.test.ts +0 -166
  79. package/src/add/add-workflow-graph-foreach.test.ts +0 -174
  80. package/src/add/add-workflow-graph-input.test.ts +0 -94
  81. package/src/add/add-workflow-graph-notes.test.ts +0 -74
  82. package/src/add/add-workflow-graph.ts +0 -644
  83. package/src/add/add-workflow-on-error.test.ts +0 -118
  84. package/src/add/add-workflow-return-switch.test.ts +0 -121
  85. package/src/add/add-workflow-silent-drops.test.ts +0 -261
  86. package/src/add/add-workflow-sleep-expression.test.ts +0 -96
  87. package/src/add/add-workflow.test.ts +0 -155
  88. package/src/add/add-workflow.ts +0 -482
  89. package/src/add/addon-bans.test.ts +0 -121
  90. package/src/add/addon-contracts.test.ts +0 -221
  91. package/src/add/allow-escape-hatches.test.ts +0 -144
  92. package/src/add/audit-meta.test.ts +0 -75
  93. package/src/add/classified-sink-check.test.ts +0 -347
  94. package/src/add/config-value-metadata.test.ts +0 -263
  95. package/src/add/dynamic-import-check.test.ts +0 -119
  96. package/src/add/expect-eventually-scenario-only.test.ts +0 -108
  97. package/src/add/function-body-span.test.ts +0 -96
  98. package/src/add/inline-wiring-function.test.ts +0 -104
  99. package/src/add/pii-check.test.ts +0 -258
  100. package/src/add/rpc-type-cast.test.ts +0 -123
  101. package/src/add/scenario-instrumentation.test.ts +0 -89
  102. package/src/add/scenario-instrumentation.ts +0 -42
  103. package/src/add/scenario-marker.test.ts +0 -89
  104. package/src/add/scenario-not-extractable.test.ts +0 -159
  105. package/src/add/scenario-services.test.ts +0 -103
  106. package/src/add/scenario-skip.test.ts +0 -83
  107. package/src/add/validate-workflow-graph-addons.test.ts +0 -102
  108. package/src/add/wire-name-literal.test.ts +0 -117
  109. package/src/error-codes-emitted.test.ts +0 -76
  110. package/src/error-codes.ts +0 -145
  111. package/src/index.ts +0 -66
  112. package/src/inspector.ts +0 -575
  113. package/src/schema-generator.ts +0 -1
  114. package/src/source-file-cache.ts +0 -73
  115. package/src/tsconfig-paths.test.ts +0 -90
  116. package/src/types-map.ts +0 -141
  117. package/src/types.ts +0 -775
  118. package/src/utils/annotate-http-route-auth.test.ts +0 -161
  119. package/src/utils/annotate-http-route-auth.ts +0 -53
  120. package/src/utils/check-classified-sinks.ts +0 -215
  121. package/src/utils/check-pii-output.ts +0 -120
  122. package/src/utils/compute-diagnostics.test.ts +0 -106
  123. package/src/utils/compute-required-schemas.ts +0 -48
  124. package/src/utils/contract-hashes.test.ts +0 -553
  125. package/src/utils/contract-hashes.ts +0 -387
  126. package/src/utils/custom-types-generator.test.ts +0 -124
  127. package/src/utils/custom-types-generator.ts +0 -150
  128. package/src/utils/detect-schema-vendor.test.ts +0 -123
  129. package/src/utils/detect-schema-vendor.ts +0 -120
  130. package/src/utils/does-type-extend-core-type.ts +0 -53
  131. package/src/utils/ensure-function-metadata.ts +0 -385
  132. package/src/utils/extract-function-name.ts +0 -612
  133. package/src/utils/extract-function-node.ts +0 -70
  134. package/src/utils/extract-node-value.test.ts +0 -117
  135. package/src/utils/extract-node-value.ts +0 -172
  136. package/src/utils/extract-secret-usage.test.ts +0 -81
  137. package/src/utils/extract-secret-usage.ts +0 -73
  138. package/src/utils/extract-services.ts +0 -70
  139. package/src/utils/extract-surface-usage.test.ts +0 -147
  140. package/src/utils/extract-surface-usage.ts +0 -121
  141. package/src/utils/filter-inspector-state.test.ts +0 -2232
  142. package/src/utils/filter-inspector-state.ts +0 -1298
  143. package/src/utils/filter-utils.test.ts +0 -843
  144. package/src/utils/filter-utils.ts +0 -161
  145. package/src/utils/find-root-dir.ts +0 -68
  146. package/src/utils/find-zod-transform.test.ts +0 -70
  147. package/src/utils/find-zod-transform.ts +0 -103
  148. package/src/utils/get-exported-variable-name.ts +0 -48
  149. package/src/utils/get-files-and-methods.ts +0 -157
  150. package/src/utils/get-property-value.test.ts +0 -141
  151. package/src/utils/get-property-value.ts +0 -331
  152. package/src/utils/hash.ts +0 -26
  153. package/src/utils/literal-properties.ts +0 -97
  154. package/src/utils/load-addon-functions-meta.test.ts +0 -157
  155. package/src/utils/load-addon-functions-meta.ts +0 -508
  156. package/src/utils/load-addon-scopes.test.ts +0 -138
  157. package/src/utils/middleware.test.ts +0 -204
  158. package/src/utils/middleware.ts +0 -303
  159. package/src/utils/nested-project-filter.test.ts +0 -53
  160. package/src/utils/nested-project-filter.ts +0 -28
  161. package/src/utils/permissions-shorthand.test.ts +0 -79
  162. package/src/utils/permissions.test.ts +0 -125
  163. package/src/utils/permissions.ts +0 -129
  164. package/src/utils/post-process.test.ts +0 -1094
  165. package/src/utils/post-process.ts +0 -1604
  166. package/src/utils/resolve-addon-package.ts +0 -106
  167. package/src/utils/resolve-core-type.test.ts +0 -142
  168. package/src/utils/resolve-core-type.ts +0 -35
  169. package/src/utils/resolve-deploy-target.test.ts +0 -135
  170. package/src/utils/resolve-deploy-target.ts +0 -65
  171. package/src/utils/resolve-function-meta.ts +0 -25
  172. package/src/utils/resolve-function-types.ts +0 -42
  173. package/src/utils/resolve-identifier.ts +0 -46
  174. package/src/utils/resolve-ref-contract.ts +0 -71
  175. package/src/utils/resolve-versions.test.ts +0 -499
  176. package/src/utils/resolve-versions.ts +0 -228
  177. package/src/utils/schema-cache.test.ts +0 -124
  178. package/src/utils/schema-generator.test.ts +0 -272
  179. package/src/utils/schema-generator.ts +0 -780
  180. package/src/utils/secret-alias-services.test.ts +0 -146
  181. package/src/utils/secret-alias-services.ts +0 -58
  182. package/src/utils/serialize-inspector-state.ts +0 -742
  183. package/src/utils/serialize-mcp-json.ts +0 -150
  184. package/src/utils/serialize-middleware-groups-meta.ts +0 -36
  185. package/src/utils/serialize-openapi-json.ts +0 -277
  186. package/src/utils/serialize-permissions-groups-meta.ts +0 -7
  187. package/src/utils/single-declaration.ts +0 -57
  188. package/src/utils/type-utils.ts +0 -145
  189. package/src/utils/validate-auth-sessionless.ts +0 -30
  190. package/src/utils/validate-exposed-functions-gated.test.ts +0 -230
  191. package/src/utils/validate-exposed-functions-gated.ts +0 -103
  192. package/src/utils/validate-tags-resolve-to-middleware.test.ts +0 -108
  193. package/src/utils/validate-tags-resolve-to-middleware.ts +0 -48
  194. package/src/utils/workflow/derive-workflow-plan.test.ts +0 -178
  195. package/src/utils/workflow/derive-workflow-plan.ts +0 -117
  196. package/src/utils/workflow/dsl/deserialize-dsl-workflow.ts +0 -1465
  197. package/src/utils/workflow/dsl/deserialize-fanout-body.test.ts +0 -71
  198. package/src/utils/workflow/dsl/deserialize-fanout-fidelity.test.ts +0 -99
  199. package/src/utils/workflow/dsl/deserialize-graph-next.test.ts +0 -161
  200. package/src/utils/workflow/dsl/deserialize-roundtrip-bindings.test.ts +0 -121
  201. package/src/utils/workflow/dsl/deserialize-roundtrip-values.test.ts +0 -116
  202. package/src/utils/workflow/dsl/deserialize-roundtrip.test.ts +0 -145
  203. package/src/utils/workflow/dsl/extract-approval-policy.test.ts +0 -71
  204. package/src/utils/workflow/dsl/extract-dsl-workflow.ts +0 -2254
  205. package/src/utils/workflow/dsl/index.ts +0 -11
  206. package/src/utils/workflow/dsl/patterns.ts +0 -440
  207. package/src/utils/workflow/dsl/validation.ts +0 -203
  208. package/src/utils/workflow/graph/convert-dsl-to-graph-skip.test.ts +0 -35
  209. package/src/utils/workflow/graph/convert-dsl-to-graph.ts +0 -543
  210. package/src/utils/workflow/graph/fanout-node-identity.test.ts +0 -105
  211. package/src/utils/workflow/graph/finalize-workflow-wires.ts +0 -310
  212. package/src/utils/workflow/graph/finalize-workflows.test.ts +0 -50
  213. package/src/utils/workflow/graph/finalize-workflows.ts +0 -130
  214. package/src/utils/workflow/graph/index.ts +0 -28
  215. package/src/utils/workflow/graph/serialize-workflow-graph.ts +0 -240
  216. package/src/utils/workflow/graph/workflow-graph.types.ts +0 -281
  217. package/src/visit.ts +0 -181
  218. package/tsconfig.json +0 -19
  219. package/tsconfig.tsbuildinfo +0 -1
@@ -1,780 +0,0 @@
1
- import * as ts from 'typescript'
2
- import { createHash } from 'crypto'
3
- import {
4
- existsSync,
5
- mkdirSync,
6
- readFileSync,
7
- statSync,
8
- writeFileSync,
9
- } from 'fs'
10
- import { dirname, join, resolve } from 'path'
11
- import { createGenerator, RootlessError } from 'ts-json-schema-generator'
12
- import { register } from 'tsx/esm/api'
13
- import * as z from 'zod'
14
- import { zodToTs, createAuxiliaryTypeStore } from 'zod-to-ts'
15
- import { findZodTransform } from './find-zod-transform.js'
16
- import type { FunctionsMeta } from '@pikku/core/services'
17
- import type { JSONValue } from '@pikku/core/utils'
18
- import type { HTTPWiringsMeta } from '@pikku/core/http'
19
- import type { TypesMap } from '../types-map.js'
20
- import { ErrorCode } from '../error-codes.js'
21
- import type { InspectorLogger, InspectorState, SchemaRef } from '../types.js'
22
- import { generateCustomTypes } from './custom-types-generator.js'
23
-
24
- const PRIMITIVE_TYPES = new Set([
25
- 'boolean',
26
- 'string',
27
- 'number',
28
- 'null',
29
- 'undefined',
30
- 'void',
31
- 'any',
32
- 'unknown',
33
- 'never',
34
- ])
35
-
36
- function primitiveTypeToSchema(typeStr: string): JSONValue | null {
37
- const normalized = typeStr.trim()
38
-
39
- if (
40
- normalized === 'void' ||
41
- normalized === 'undefined' ||
42
- normalized === 'never'
43
- ) {
44
- return null
45
- }
46
-
47
- if (
48
- normalized === 'boolean' ||
49
- normalized === 'false | true' ||
50
- normalized === 'true | false'
51
- ) {
52
- return { type: 'boolean' }
53
- }
54
- if (normalized === 'true') {
55
- return { const: true }
56
- }
57
- if (normalized === 'false') {
58
- return { const: false }
59
- }
60
-
61
- if (normalized === 'string') {
62
- return { type: 'string' }
63
- }
64
-
65
- if (normalized === 'number') {
66
- return { type: 'number' }
67
- }
68
-
69
- if (normalized === 'null') {
70
- return { type: 'null' }
71
- }
72
-
73
- if (normalized === 'any' || normalized === 'unknown') {
74
- return {}
75
- }
76
-
77
- return null
78
- }
79
-
80
- // Cached state for schema program reuse across inspect() calls
81
- let cachedSchemaProgram: ts.Program | undefined
82
- let cachedParsedConfig: ts.ParsedCommandLine | undefined
83
- let cachedTsconfigPath: string | undefined
84
- let cachedCustomTypesContent: string | undefined
85
- let cachedTSSchemas: Record<string, JSONValue> | undefined
86
- let cachedTSSchemaDeps: SchemaDep[] | undefined
87
-
88
- const SCHEMA_CACHE_VERSION = 1
89
-
90
- // This package's own version — folded into the cache key so that upgrading
91
- // @pikku/inspector (the channel through which a schema-format change ships)
92
- // auto-invalidates every on-disk cache, without relying on someone remembering
93
- // to bump SCHEMA_CACHE_VERSION. Read once; falls back to the constant if the
94
- // package.json can't be located (e.g. an unexpected bundling layout).
95
- const inspectorVersion: string = (() => {
96
- try {
97
- const pkgUrl = new URL('../../package.json', import.meta.url)
98
- const pkg = JSON.parse(readFileSync(pkgUrl, 'utf-8'))
99
- return typeof pkg.version === 'string'
100
- ? pkg.version
101
- : `v${SCHEMA_CACHE_VERSION}`
102
- } catch {
103
- return `v${SCHEMA_CACHE_VERSION}`
104
- }
105
- })()
106
-
107
- // Key the TS-schema cache on everything that affects its output: the generated
108
- // custom-types source, the generator options that change schema shape, and the
109
- // inspector version (schema-format changes ship with a version bump).
110
- function tsSchemaCacheKey(
111
- customTypesContent: string,
112
- config: {
113
- schemasFromTypes?: string[]
114
- schema?: { additionalProperties?: boolean }
115
- }
116
- ): string {
117
- return createHash('sha1')
118
- .update(`v${SCHEMA_CACHE_VERSION}\0`)
119
- .update(`pkg:${inspectorVersion}\0`)
120
- .update(`ap:${config.schema?.additionalProperties ? 1 : 0}\0`)
121
- .update(`ft:${(config.schemasFromTypes ?? []).join(',')}\0`)
122
- .update(customTypesContent)
123
- .digest('hex')
124
- }
125
-
126
- function schemaCacheFile(cacheDir: string): string {
127
- return join(cacheDir, 'ts-schemas.json')
128
- }
129
-
130
- /** One file the generated schemas were derived from, as of when they were generated. */
131
- export type SchemaDep = { path: string; mtimeMs: number; size: number }
132
-
133
- /**
134
- * The files a set of generated schemas was actually derived from.
135
- *
136
- * The cache key alone cannot answer whether the schemas are stale. It hashes the
137
- * *synthesized* custom-types source, but a named type — `KnowledgeBundle`,
138
- * `DbSchema`, anything a function declares as its input or output — appears
139
- * there only as a name; its definition is resolved out of a project file or a
140
- * dependency's `.d.ts`. Edit one of those and the key is unchanged, so a stale
141
- * schema is served for a type that no longer looks like that. The failure is
142
- * silent and outlives `rm -rf .pikku`, because this cache lives in
143
- * `node_modules/.cache`.
144
- *
145
- * The program is rooted at the virtual file, so its source files are exactly the
146
- * transitive closure the schemas depend on — nothing wider, nothing missed.
147
- * Default-lib files are skipped: they change only when TypeScript itself does,
148
- * which the inspector version in the key already covers.
149
- */
150
- function fingerprintSchemaDeps(program: ts.Program): SchemaDep[] {
151
- const deps: SchemaDep[] = []
152
- for (const sourceFile of program.getSourceFiles()) {
153
- if (program.isSourceFileDefaultLibrary(sourceFile)) continue
154
- try {
155
- const stat = statSync(sourceFile.fileName)
156
- deps.push({
157
- path: sourceFile.fileName,
158
- mtimeMs: stat.mtimeMs,
159
- size: stat.size,
160
- })
161
- } catch {
162
- // The virtual root has no file on disk. Its content is the custom-types
163
- // source, which the cache key already hashes.
164
- }
165
- }
166
- return deps
167
- }
168
-
169
- /**
170
- * Whether every file the schemas were derived from is still exactly as it was.
171
- *
172
- * mtime-and-size rather than a content hash: this runs on the cache-hit path,
173
- * where the whole point is to answer in milliseconds without building a TS
174
- * program. Anything that rewrites a file — a dependency rebuild, a checkout, an
175
- * edit — moves its mtime, and a missing file reads as changed, so the answer errs
176
- * toward regenerating.
177
- */
178
- function schemaDepsUnchanged(deps: unknown): boolean {
179
- if (!Array.isArray(deps)) return false
180
- for (const dep of deps as SchemaDep[]) {
181
- if (!dep || typeof dep.path !== 'string') return false
182
- try {
183
- const stat = statSync(dep.path)
184
- if (stat.mtimeMs !== dep.mtimeMs || stat.size !== dep.size) return false
185
- } catch {
186
- return false
187
- }
188
- }
189
- return true
190
- }
191
-
192
- export function readDiskTSSchemas(
193
- logger: InspectorLogger,
194
- cacheDir: string,
195
- key: string
196
- ): { schemas: Record<string, JSONValue>; deps: SchemaDep[] } | null {
197
- const file = schemaCacheFile(cacheDir)
198
- if (!existsSync(file)) return null
199
- try {
200
- const parsed = JSON.parse(readFileSync(file, 'utf-8'))
201
- if (parsed?.key !== key || !parsed.schemas) return null
202
- if (!schemaDepsUnchanged(parsed.deps)) {
203
- logger.debug(
204
- 'Discarding TS-schema cache: a type it was built from changed'
205
- )
206
- return null
207
- }
208
- return { schemas: parsed.schemas, deps: parsed.deps }
209
- } catch (e) {
210
- logger.debug(`Ignoring unreadable TS-schema cache: ${(e as Error).message}`)
211
- }
212
- return null
213
- }
214
-
215
- export function writeDiskTSSchemas(
216
- logger: InspectorLogger,
217
- cacheDir: string,
218
- key: string,
219
- schemas: Record<string, JSONValue>,
220
- deps: SchemaDep[]
221
- ): void {
222
- const file = schemaCacheFile(cacheDir)
223
- try {
224
- mkdirSync(dirname(file), { recursive: true })
225
- writeFileSync(file, JSON.stringify({ key, schemas, deps }))
226
- } catch (e) {
227
- logger.debug(`Failed to persist TS-schema cache: ${(e as Error).message}`)
228
- }
229
- }
230
-
231
- function createProgramWithVirtualFile(
232
- logger: InspectorLogger,
233
- tsconfig: string,
234
- virtualFilePath: string,
235
- virtualFileContent: string
236
- ): ts.Program {
237
- const configPath = resolve(tsconfig)
238
-
239
- // Cache the parsed tsconfig — it doesn't change between runs
240
- if (!cachedParsedConfig || cachedTsconfigPath !== configPath) {
241
- const configFile = ts.readConfigFile(configPath, ts.sys.readFile)
242
- const basePath = dirname(configPath)
243
- cachedParsedConfig = ts.parseJsonConfigFileContent(
244
- configFile.config,
245
- ts.sys,
246
- basePath
247
- )
248
- cachedTsconfigPath = configPath
249
- cachedSchemaProgram = undefined
250
- }
251
-
252
- const resolvedVirtualPath = resolve(virtualFilePath)
253
- // The virtual file imports every type it references, so it is a complete root
254
- // on its own and TypeScript pulls in exactly the transitive closure it needs.
255
- // Rooting at cachedParsedConfig.fileNames instead loaded the whole project
256
- // (2572 files vs 870 on a real tree) without widening what could be resolved.
257
- const fileNames = [resolvedVirtualPath]
258
-
259
- const defaultHost = ts.createCompilerHost(cachedParsedConfig.options)
260
- const customHost: ts.CompilerHost = {
261
- ...defaultHost,
262
- getSourceFile(
263
- fileName,
264
- languageVersionOrOptions,
265
- onError,
266
- shouldCreateNewSourceFile
267
- ) {
268
- if (resolve(fileName) === resolvedVirtualPath) {
269
- return ts.createSourceFile(
270
- fileName,
271
- virtualFileContent,
272
- languageVersionOrOptions
273
- )
274
- }
275
- return defaultHost.getSourceFile(
276
- fileName,
277
- languageVersionOrOptions,
278
- onError,
279
- shouldCreateNewSourceFile
280
- )
281
- },
282
- fileExists(fileName) {
283
- if (resolve(fileName) === resolvedVirtualPath) return true
284
- return defaultHost.fileExists(fileName)
285
- },
286
- readFile(fileName) {
287
- if (resolve(fileName) === resolvedVirtualPath) return virtualFileContent
288
- return defaultHost.readFile(fileName)
289
- },
290
- }
291
-
292
- const startProgram = performance.now()
293
- const program = ts.createProgram(
294
- fileNames,
295
- cachedParsedConfig.options,
296
- customHost,
297
- cachedSchemaProgram // reuse previous program for incremental compilation
298
- )
299
- cachedSchemaProgram = program
300
- logger.debug(
301
- `Created schema program in ${(performance.now() - startProgram).toFixed(0)}ms (${program.getSourceFiles().length} files)`
302
- )
303
- return program
304
- }
305
-
306
- function generateTSSchemas(
307
- logger: InspectorLogger,
308
- tsconfig: string,
309
- customTypesContent: string,
310
- typesMap: TypesMap,
311
- functionMeta: FunctionsMeta,
312
- httpWiringsMeta: HTTPWiringsMeta,
313
- additionalTypes?: string[],
314
- additionalProperties: boolean = false,
315
- generatedZodSchemas?: Record<string, JSONValue>
316
- ): { schemas: Record<string, JSONValue>; deps: SchemaDep[] } {
317
- const schemasSet = new Set(typesMap.customTypes.keys())
318
- for (const { inputs, outputs } of Object.values(functionMeta)) {
319
- const types = [...(inputs || []), ...(outputs || [])]
320
- for (const type of types) {
321
- try {
322
- const uniqueName = typesMap.getUniqueName(type)
323
- if (uniqueName) {
324
- schemasSet.add(uniqueName)
325
- }
326
- } catch {
327
- // Skip types not in typesMap (e.g., inline types in generated workflow workers)
328
- }
329
- }
330
- }
331
-
332
- for (const wiringRoutes of Object.values(httpWiringsMeta)) {
333
- for (const { inputTypes } of Object.values(wiringRoutes)) {
334
- if (inputTypes?.body) {
335
- schemasSet.add(inputTypes.body)
336
- }
337
- if (inputTypes?.query) {
338
- schemasSet.add(inputTypes.query)
339
- }
340
- if (inputTypes?.params) {
341
- schemasSet.add(inputTypes.params)
342
- }
343
- }
344
- }
345
-
346
- if (additionalTypes) {
347
- for (const type of additionalTypes) {
348
- schemasSet.add(type)
349
- }
350
- }
351
-
352
- // Skip ts-json-schema-generator if all schemas are already covered by Zod/primitives.
353
- // Use generatedZodSchemas (actually converted) rather than schemaLookup (all attempted)
354
- // so that failed Zod conversions fall through to TS schema generation.
355
- const uncoveredSchemas = [...schemasSet].filter(
356
- (s) => !PRIMITIVE_TYPES.has(s) && !generatedZodSchemas?.[s]
357
- )
358
- if (uncoveredSchemas.length === 0) {
359
- return { schemas: {}, deps: [] }
360
- }
361
- logger.debug(
362
- `generateTSSchemas needed for ${uncoveredSchemas.length} types: ${uncoveredSchemas.slice(0, 3).join(', ')}${uncoveredSchemas.length > 3 ? '...' : ''}`
363
- )
364
-
365
- const virtualFilePath = join(
366
- dirname(resolve(tsconfig)),
367
- '__pikku_virtual_types__.ts'
368
- )
369
- const program = createProgramWithVirtualFile(
370
- logger,
371
- tsconfig,
372
- virtualFilePath,
373
- customTypesContent
374
- )
375
-
376
- const generator = createGenerator({
377
- tsProgram: program,
378
- skipTypeCheck: true,
379
- topRef: false,
380
- discriminatorType: 'open-api',
381
- expose: 'export',
382
- jsDoc: 'extended',
383
- sortProps: true,
384
- strictTuples: false,
385
- encodeRefs: false,
386
- additionalProperties,
387
- })
388
- const schemas: Record<string, JSONValue> = {}
389
-
390
- schemasSet.forEach((schema) => {
391
- if (PRIMITIVE_TYPES.has(schema)) {
392
- return
393
- }
394
- if (generatedZodSchemas?.[schema]) {
395
- return
396
- }
397
- try {
398
- schemas[schema] = generator.createSchema(schema) as JSONValue
399
- } catch (e) {
400
- if (e instanceof RootlessError) {
401
- const customType = typesMap.customTypes.get(schema)
402
- if (customType) {
403
- const primitiveSchema = primitiveTypeToSchema(customType.type)
404
- if (primitiveSchema) {
405
- schemas[schema] = primitiveSchema
406
- }
407
- }
408
- return
409
- }
410
- const customType = typesMap.customTypes.get(schema)
411
- logger.error(
412
- `[${ErrorCode.SCHEMA_GENERATION_ERROR}] Error generating schema: ${schema}. Message: ${(e as Error).message}. Type info: ${customType ? `type=${customType.type}` : 'not in typesMap'}`
413
- )
414
- }
415
- })
416
-
417
- // Fingerprinted here, while the program is still alive — it is released as soon
418
- // as the schemas are out of it.
419
- return { schemas, deps: fingerprintSchemaDeps(program) }
420
- }
421
-
422
- /**
423
- * Where the RUNTIME value of a schema lives, given where TypeScript resolved it.
424
- *
425
- * A schema imported from a built workspace package resolves to that package's
426
- * declaration file, and importing a `.d.ts` yields a module with no exports at
427
- * all — every one of them is a type. The value is in the emitted JS beside it, so
428
- * that is what gets imported.
429
- *
430
- * Falls back to the original path when no sibling JS exists, which keeps the
431
- * "could not find exported schema" error pointing at the file the developer
432
- * actually wrote.
433
- */
434
- export const schemaRuntimeFile = (sourceFile: string): string => {
435
- const match = /\.d\.([cm]?)ts$/.exec(sourceFile)
436
- if (!match) return sourceFile
437
- const emitted = sourceFile.replace(/\.d\.([cm]?)ts$/, '.$1js')
438
- return existsSync(emitted) ? emitted : sourceFile
439
- }
440
-
441
- /**
442
- * Import all source files in parallel using tsx's register() API.
443
- *
444
- * tsx's register() sets up the TypeScript loader once, then all subsequent
445
- * import() calls reuse that loader. This is dramatically faster than calling
446
- * tsImport() per-file because tsImport() sets up and tears down a fresh
447
- * compilation context for each call (~170ms each).
448
- *
449
- * With register() + parallel import():
450
- * - 71 files: ~350ms total
451
- * - vs tsImport loop: ~12,000ms (71 * 170ms)
452
- *
453
- * Falls back to serial tsImport() per-file if register() is unavailable.
454
- */
455
- async function batchImportWithRegister(
456
- logger: InspectorLogger,
457
- sourceFiles: string[]
458
- ): Promise<Map<string, Record<string, any>> | null> {
459
- if (sourceFiles.length === 0) return new Map()
460
-
461
- let unregister: (() => void | Promise<void>) | undefined
462
- try {
463
- unregister = register()
464
-
465
- const modules = new Map<string, Record<string, any>>()
466
- const results = await Promise.allSettled(
467
- sourceFiles.map(async (srcPath) => {
468
- const mod = await import(srcPath)
469
- modules.set(srcPath, mod)
470
- })
471
- )
472
-
473
- const failures = results.filter((r) => r.status === 'rejected')
474
- if (failures.length > 0) {
475
- logger.debug(
476
- `${failures.length}/${sourceFiles.length} files failed to import via register()`
477
- )
478
- }
479
-
480
- return modules
481
- } catch (e) {
482
- logger.debug(`tsx register() batch import failed: ${(e as Error).message}`)
483
- return null
484
- } finally {
485
- void Promise.resolve(unregister?.()).catch((e) => {
486
- logger.debug(`tsx unregister() failed: ${(e as Error).message}`)
487
- })
488
- }
489
- }
490
-
491
- async function importWithRegister(
492
- sourceFile: string
493
- ): Promise<Record<string, any>> {
494
- const unregister = register()
495
- try {
496
- return await import(sourceFile)
497
- } finally {
498
- void Promise.resolve(unregister()).catch(() => {})
499
- }
500
- }
501
-
502
- function processZodSchema(
503
- schemaName: string,
504
- zodSchema: any,
505
- schemas: Record<string, JSONValue>,
506
- typesMap: TypesMap,
507
- auxiliaryTypeStore: ReturnType<typeof createAuxiliaryTypeStore>,
508
- printer: ts.Printer,
509
- fakeSourceFile: ts.SourceFile,
510
- logger: InspectorLogger
511
- ): void {
512
- const transformPath = findZodTransform(zodSchema)
513
- if (transformPath !== undefined) {
514
- const where = transformPath === '' ? 'the schema itself' : transformPath
515
- throw new Error(
516
- `[${ErrorCode.SCHEMA_TRANSFORM_NOT_SUPPORTED}] '${schemaName}' contains a .transform() at ${where}.\n` +
517
- 'A transform makes the value sent and the value received two different ' +
518
- 'shapes, and a wire contract describes one: it cannot be expressed in ' +
519
- 'JSON Schema or in the generated TypeScript.\n' +
520
- 'Validate with the plain schema and apply the transform inside the ' +
521
- 'function, or move it to the output type.'
522
- )
523
- }
524
-
525
- const schema = z.toJSONSchema(zodSchema, {
526
- unrepresentable: 'any',
527
- override: ({ zodSchema, jsonSchema }) => {
528
- if ((zodSchema as any)._zod?.def?.type === 'date') {
529
- ;(jsonSchema as any).type = 'string'
530
- ;(jsonSchema as any).format = 'date-time'
531
- }
532
- },
533
- }) as any
534
-
535
- if (schema.required && schema.properties) {
536
- schema.required = schema.required.filter((fieldName: string) => {
537
- const prop = schema.properties[fieldName]
538
- return prop && prop.default === undefined
539
- })
540
- if (schema.required.length === 0) {
541
- delete schema.required
542
- }
543
- }
544
-
545
- const { node: tsType } = zodToTs(zodSchema, { auxiliaryTypeStore })
546
-
547
- const typeText = printer.printNode(
548
- ts.EmitHint.Unspecified,
549
- tsType,
550
- fakeSourceFile
551
- )
552
-
553
- typesMap.addCustomType(schemaName, typeText, [])
554
- schemas[schemaName] = schema
555
- logger.debug(`• Generated schema from Zod: ${schemaName}`)
556
- }
557
-
558
- async function generateZodSchemas(
559
- logger: InspectorLogger,
560
- schemaLookup: Map<string, SchemaRef>,
561
- typesMap: TypesMap
562
- ): Promise<Record<string, JSONValue>> {
563
- const schemas: Record<string, JSONValue> = {}
564
- const errors: string[] = []
565
- const auxiliaryTypeStore = createAuxiliaryTypeStore()
566
- const printer = ts.createPrinter()
567
- const fakeSourceFile = ts.createSourceFile(
568
- 'zod-types.ts',
569
- '',
570
- ts.ScriptTarget.ESNext,
571
- false,
572
- ts.ScriptKind.TS
573
- )
574
-
575
- // Validate all schemas are zod (or unspecified vendor)
576
- for (const [schemaName, ref] of schemaLookup.entries()) {
577
- if (ref.vendor && ref.vendor !== 'zod') {
578
- throw new Error(
579
- `Schema '${schemaName}' uses ${ref.vendor} which is not yet supported for JSON Schema generation. ` +
580
- `Currently only Zod schemas can be converted to JSON Schema. ` +
581
- `Please use Zod or contribute support for ${ref.vendor}.`
582
- )
583
- }
584
- }
585
-
586
- // Collect unique source files and batch-import them in parallel
587
- const uniqueSourceFiles = [
588
- ...new Set(
589
- [...schemaLookup.values()].map((ref) => schemaRuntimeFile(ref.sourceFile))
590
- ),
591
- ]
592
- logger.debug(
593
- `[TIMING] Zod schemas: ${schemaLookup.size} schemas from ${uniqueSourceFiles.length} files`
594
- )
595
-
596
- const importStart = performance.now()
597
- const importedModules = await batchImportWithRegister(
598
- logger,
599
- uniqueSourceFiles
600
- )
601
- logger.debug(
602
- `[TIMING] Batch import: ${(performance.now() - importStart).toFixed(0)}ms`
603
- )
604
-
605
- const processStart = performance.now()
606
- // Track schemas that need per-file tsImport fallback
607
- const fallbackSchemas: [string, SchemaRef][] = []
608
-
609
- for (const [schemaName, ref] of schemaLookup.entries()) {
610
- const runtimeFile = schemaRuntimeFile(ref.sourceFile)
611
- const mod = importedModules?.get(runtimeFile)
612
- if (mod) {
613
- const zodSchema = mod[ref.variableName]
614
- if (!zodSchema) {
615
- errors.push(
616
- `Could not find exported schema '${ref.variableName}' in ${runtimeFile} for ${schemaName}. Available exports: ${Object.keys(mod).join(', ')}`
617
- )
618
- continue
619
- }
620
- try {
621
- processZodSchema(
622
- schemaName,
623
- zodSchema,
624
- schemas,
625
- typesMap,
626
- auxiliaryTypeStore,
627
- printer,
628
- fakeSourceFile,
629
- logger
630
- )
631
- } catch (e) {
632
- errors.push(
633
- `Could not convert Zod schema '${schemaName}': ${e instanceof Error ? e.message : e}`
634
- )
635
- }
636
- } else {
637
- fallbackSchemas.push([schemaName, ref])
638
- }
639
- }
640
-
641
- // Fallback: use a scoped tsx register/import cycle for any schemas that
642
- // batch import couldn't handle. Avoid tsImport() here because its ESM path
643
- // can leave loader plumbing alive after failed imports, which prevents the
644
- // CLI process from exiting on schema errors.
645
- if (fallbackSchemas.length > 0) {
646
- logger.debug(
647
- `Falling back to register() import for ${fallbackSchemas.length} schema(s)`
648
- )
649
- for (const [schemaName, ref] of fallbackSchemas) {
650
- const runtimeFile = schemaRuntimeFile(ref.sourceFile)
651
- try {
652
- const module = await importWithRegister(runtimeFile)
653
- const zodSchema = module[ref.variableName]
654
- if (!zodSchema) {
655
- errors.push(
656
- `Could not find exported schema '${ref.variableName}' in ${runtimeFile} for ${schemaName}. Available exports: ${Object.keys(module).join(', ')}`
657
- )
658
- continue
659
- }
660
- processZodSchema(
661
- schemaName,
662
- zodSchema,
663
- schemas,
664
- typesMap,
665
- auxiliaryTypeStore,
666
- printer,
667
- fakeSourceFile,
668
- logger
669
- )
670
- } catch (e) {
671
- errors.push(
672
- `Could not convert Zod schema '${schemaName}': ${e instanceof Error ? e.message : e}`
673
- )
674
- }
675
- }
676
- }
677
-
678
- if (errors.length > 0) {
679
- for (const message of errors) {
680
- logger.error(message)
681
- }
682
- throw new Error(
683
- `Schema generation failed for ${errors.length} schema${errors.length === 1 ? '' : 's'}`
684
- )
685
- }
686
-
687
- logger.debug(
688
- `[TIMING] Process schemas: ${(performance.now() - processStart).toFixed(0)}ms (${Object.keys(schemas).length} generated)`
689
- )
690
- return schemas
691
- }
692
-
693
- export async function generateAllSchemas(
694
- logger: InspectorLogger,
695
- config: {
696
- tsconfig: string
697
- schemasFromTypes?: string[]
698
- schema?: { additionalProperties?: boolean }
699
- cacheDir?: string
700
- },
701
- state: InspectorState
702
- ): Promise<Record<string, JSONValue>> {
703
- const zodSchemas = await generateZodSchemas(
704
- logger,
705
- state.schemaLookup,
706
- state.functions.typesMap
707
- )
708
-
709
- const requiredTypes = new Set<string>()
710
- const customTypesContent = generateCustomTypes(
711
- state.functions.typesMap,
712
- requiredTypes
713
- )
714
-
715
- // Fast path: same process, types unchanged — reuse the in-memory result. The
716
- // dep check is what makes this correct under `pikku dev`, where the process
717
- // outlives the edits: the custom-types source is identical whenever a named
718
- // type changes shape without changing its name.
719
- if (
720
- cachedTSSchemas &&
721
- cachedCustomTypesContent === customTypesContent &&
722
- schemaDepsUnchanged(cachedTSSchemaDeps)
723
- ) {
724
- logger.debug('Reusing cached TS schemas (types unchanged)')
725
- return { ...cachedTSSchemas, ...zodSchemas }
726
- }
727
-
728
- // Disk path: a prior `pikku all` left a cache whose key matches the current
729
- // custom types — load it and skip ts-json-schema-generator (the dominant
730
- // cold-run cost). Zod schemas are always regenerated (cheap, ~1ms/schema).
731
- const cacheKey = config.cacheDir
732
- ? tsSchemaCacheKey(customTypesContent, config)
733
- : null
734
- if (config.cacheDir && cacheKey) {
735
- const disk = readDiskTSSchemas(logger, config.cacheDir, cacheKey)
736
- if (disk) {
737
- logger.debug('Reusing on-disk TS schemas (types unchanged across runs)')
738
- cachedCustomTypesContent = customTypesContent
739
- cachedTSSchemas = disk.schemas
740
- cachedTSSchemaDeps = disk.deps
741
- return { ...disk.schemas, ...zodSchemas }
742
- }
743
- }
744
-
745
- const { schemas: tsSchemas, deps: tsSchemaDeps } = generateTSSchemas(
746
- logger,
747
- config.tsconfig,
748
- customTypesContent,
749
- state.functions.typesMap,
750
- state.functions.meta,
751
- state.http.meta,
752
- config.schemasFromTypes,
753
- config.schema?.additionalProperties,
754
- zodSchemas
755
- )
756
-
757
- // Release the program once the schemas are out of it. It spans the whole
758
- // tsconfig (~2.5k files, ~600MB on a large tree) and was previously pinned at
759
- // module scope for the life of the process, so it stacked on top of the main
760
- // inspector program and pushed `pikku all` past a 2GB heap (#982). The result
761
- // cache below is what the same-process fast path actually needs; the program
762
- // only bought incremental reuse on a rebuild, at a cost that dwarfs it.
763
- cachedSchemaProgram = undefined
764
-
765
- cachedCustomTypesContent = customTypesContent
766
- cachedTSSchemas = tsSchemas
767
- cachedTSSchemaDeps = tsSchemaDeps
768
-
769
- if (config.cacheDir && cacheKey) {
770
- writeDiskTSSchemas(
771
- logger,
772
- config.cacheDir,
773
- cacheKey,
774
- tsSchemas,
775
- tsSchemaDeps
776
- )
777
- }
778
-
779
- return { ...tsSchemas, ...zodSchemas }
780
- }