@pikku/inspector 0.12.3 → 0.12.4

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/src/inspector.ts CHANGED
@@ -20,6 +20,7 @@ import {
20
20
  computePermissionsGroupsMeta,
21
21
  computeRequiredSchemas,
22
22
  computeDiagnostics,
23
+ validateSchemaWiringSeparation,
23
24
  } from './utils/post-process.js'
24
25
  import { generateOpenAPISpec } from './utils/serialize-openapi-json.js'
25
26
  import { pikkuState } from '@pikku/core/internal'
@@ -203,6 +204,7 @@ export function getInitialInspectorState(rootDir: string): InspectorState {
203
204
  openAPISpec: null,
204
205
  diagnostics: [],
205
206
  addonFunctions: {},
207
+ program: null,
206
208
  }
207
209
  }
208
210
 
@@ -211,8 +213,7 @@ export const inspect = async (
211
213
  routeFiles: string[],
212
214
  options: InspectorOptions = {}
213
215
  ): Promise<InspectorState> => {
214
- const startProgram = performance.now()
215
- const program = ts.createProgram(routeFiles, {
216
+ const compilerOptions: ts.CompilerOptions = {
216
217
  target: ts.ScriptTarget.ESNext,
217
218
  module: ts.ModuleKind.Node16,
218
219
  skipLibCheck: true,
@@ -221,9 +222,16 @@ export const inspect = async (
221
222
  types: [],
222
223
  allowJs: false,
223
224
  checkJs: false,
224
- })
225
+ }
226
+ const startProgram = performance.now()
227
+ const program = ts.createProgram(
228
+ routeFiles,
229
+ compilerOptions,
230
+ undefined, // host
231
+ options.oldProgram
232
+ )
225
233
  logger.debug(
226
- `Created program in ${(performance.now() - startProgram).toFixed(2)}ms`
234
+ `Created program in ${(performance.now() - startProgram).toFixed(0)}ms (${routeFiles.length} files${options.oldProgram ? ', incremental' : ''})`
227
235
  )
228
236
 
229
237
  const startChecker = performance.now()
@@ -255,7 +263,7 @@ export const inspect = async (
255
263
  )
256
264
  }
257
265
  logger.debug(
258
- `Visit setup phase completed in ${(performance.now() - startSetup).toFixed(2)}ms`
266
+ `Visit setup phase completed in ${(performance.now() - startSetup).toFixed(0)}ms`
259
267
  )
260
268
 
261
269
  // Load addon function metadata so wirings can reference addon functions
@@ -270,17 +278,21 @@ export const inspect = async (
270
278
  )
271
279
  }
272
280
  logger.debug(
273
- `Visit routes phase completed in ${(performance.now() - startRoutes).toFixed(2)}ms`
281
+ `Visit routes phase completed in ${(performance.now() - startRoutes).toFixed(0)}ms`
274
282
  )
275
283
 
276
284
  resolveLatestVersions(state, logger)
277
285
 
278
286
  if (options.schemaConfig) {
287
+ const startSchemas = performance.now()
279
288
  state.schemas = await generateAllSchemas(
280
289
  logger,
281
290
  options.schemaConfig,
282
291
  state
283
292
  )
293
+ logger.debug(
294
+ `generateAllSchemas took ${(performance.now() - startSchemas).toFixed(0)}ms`
295
+ )
284
296
  computeContractHashes(
285
297
  state.schemas,
286
298
  state.functions.typesMap,
@@ -323,6 +335,7 @@ export const inspect = async (
323
335
  computeMiddlewareGroupsMeta(state)
324
336
  computePermissionsGroupsMeta(state)
325
337
  computeDiagnostics(state)
338
+ validateSchemaWiringSeparation(logger, state)
326
339
 
327
340
  if (options.openAPI) {
328
341
  state.openAPISpec = await generateOpenAPISpec(
@@ -341,5 +354,7 @@ export const inspect = async (
341
354
  validateVariableOverrides(logger, state)
342
355
  }
343
356
 
357
+ state.program = program
358
+
344
359
  return state
345
360
  }
package/src/types.ts CHANGED
@@ -225,6 +225,7 @@ export type InspectorOptions = Partial<{
225
225
  tags: string[]
226
226
  manifest: VersionManifest
227
227
  modelConfig: InspectorModelConfig
228
+ oldProgram: ts.Program | undefined
228
229
  }>
229
230
 
230
231
  export interface InspectorLogger {
@@ -420,4 +421,5 @@ export interface InspectorState {
420
421
  openAPISpec: Record<string, any> | null
421
422
  diagnostics: InspectorDiagnostic[]
422
423
  addonFunctions: Record<string, FunctionsMeta> // namespace -> addon's function metadata
424
+ program: ts.Program | null // Retained for incremental re-inspection
423
425
  }
@@ -16,6 +16,7 @@ export function generateCustomTypes(
16
16
  requiredTypes: Set<string>
17
17
  ) {
18
18
  const typeDeclarations = Array.from(typesMap.customTypes.entries())
19
+ .sort(([a], [b]) => a.localeCompare(b))
19
20
  .filter(([_name, { type }]) => {
20
21
  const hasUndefinedGeneric =
21
22
  /\b(Name|In|Out|Key)\b/.test(type) && /\[.*\]/.test(type)
@@ -492,6 +492,65 @@ export function validateAgentOverrides(
492
492
  }
493
493
  }
494
494
 
495
+ /**
496
+ * Validates that Zod schemas and wiring side-effects (wireHTTPRoutes,
497
+ * addPermission, addHTTPMiddleware, etc.) do not coexist in the same file.
498
+ *
499
+ * The CLI uses tsImport to extract Zod schemas at runtime, which executes
500
+ * all top-level code in the file. Wiring calls crash during this process
501
+ * because the pikku state metadata doesn't exist in the CLI context.
502
+ */
503
+ export function validateSchemaWiringSeparation(
504
+ logger: InspectorLogger,
505
+ state: InspectorState
506
+ ): void {
507
+ // Collect files that contain schemas
508
+ const schemaFiles = new Set<string>()
509
+ for (const ref of state.schemaLookup.values()) {
510
+ schemaFiles.add(ref.sourceFile)
511
+ }
512
+
513
+ // Collect files that contain wiring side-effects
514
+ const wiringFiles = new Set<string>()
515
+
516
+ // HTTP route wirings
517
+ for (const file of state.http.files) {
518
+ wiringFiles.add(file)
519
+ }
520
+
521
+ // Permission wirings (addPermission calls)
522
+ for (const meta of state.permissions.tagPermissions.values()) {
523
+ wiringFiles.add(meta.sourceFile)
524
+ }
525
+ for (const meta of state.http.routePermissions.values()) {
526
+ wiringFiles.add(meta.sourceFile)
527
+ }
528
+
529
+ // Middleware wirings (addHTTPMiddleware calls)
530
+ for (const meta of state.http.routeMiddleware.values()) {
531
+ wiringFiles.add(meta.sourceFile)
532
+ }
533
+ for (const meta of state.middleware.tagMiddleware.values()) {
534
+ wiringFiles.add(meta.sourceFile)
535
+ }
536
+
537
+ // Check for overlap
538
+ for (const file of schemaFiles) {
539
+ if (wiringFiles.has(file)) {
540
+ const schemas = Array.from(state.schemaLookup.entries())
541
+ .filter(([, ref]) => ref.sourceFile === file)
542
+ .map(([name]) => name)
543
+
544
+ logger.critical(
545
+ ErrorCode.SCHEMA_AND_WIRING_COLOCATED,
546
+ `File '${file}' contains both Zod schemas (${schemas.join(', ')}) and wiring calls (wireHTTPRoutes, addPermission, etc.). ` +
547
+ `These must be in separate files because the CLI imports schema files at runtime, which triggers wiring side-effects that crash without server context. ` +
548
+ `Move the route/wiring definitions to a dedicated wiring file.`
549
+ )
550
+ }
551
+ }
552
+ }
553
+
495
554
  export function computeDiagnostics(state: InspectorState): void {
496
555
  const diagnostics: InspectorDiagnostic[] = []
497
556
 
@@ -63,24 +63,37 @@ function primitiveTypeToSchema(typeStr: string): JSONValue | null {
63
63
  return null
64
64
  }
65
65
 
66
+ // Cached state for schema program reuse across inspect() calls
67
+ let cachedSchemaProgram: ts.Program | undefined
68
+ let cachedParsedConfig: ts.ParsedCommandLine | undefined
69
+ let cachedTsconfigPath: string | undefined
70
+ let cachedCustomTypesContent: string | undefined
71
+ let cachedTSSchemas: Record<string, JSONValue> | undefined
72
+
66
73
  function createProgramWithVirtualFile(
67
74
  tsconfig: string,
68
75
  virtualFilePath: string,
69
76
  virtualFileContent: string
70
77
  ): ts.Program {
71
78
  const configPath = resolve(tsconfig)
72
- const configFile = ts.readConfigFile(configPath, ts.sys.readFile)
73
- const basePath = dirname(configPath)
74
- const parsedConfig = ts.parseJsonConfigFileContent(
75
- configFile.config,
76
- ts.sys,
77
- basePath
78
- )
79
+
80
+ // Cache the parsed tsconfig — it doesn't change between runs
81
+ if (!cachedParsedConfig || cachedTsconfigPath !== configPath) {
82
+ const configFile = ts.readConfigFile(configPath, ts.sys.readFile)
83
+ const basePath = dirname(configPath)
84
+ cachedParsedConfig = ts.parseJsonConfigFileContent(
85
+ configFile.config,
86
+ ts.sys,
87
+ basePath
88
+ )
89
+ cachedTsconfigPath = configPath
90
+ cachedSchemaProgram = undefined
91
+ }
79
92
 
80
93
  const resolvedVirtualPath = resolve(virtualFilePath)
81
- const fileNames = [...parsedConfig.fileNames, resolvedVirtualPath]
94
+ const fileNames = [...cachedParsedConfig.fileNames, resolvedVirtualPath]
82
95
 
83
- const defaultHost = ts.createCompilerHost(parsedConfig.options)
96
+ const defaultHost = ts.createCompilerHost(cachedParsedConfig.options)
84
97
  const customHost: ts.CompilerHost = {
85
98
  ...defaultHost,
86
99
  getSourceFile(
@@ -113,7 +126,14 @@ function createProgramWithVirtualFile(
113
126
  },
114
127
  }
115
128
 
116
- return ts.createProgram(fileNames, parsedConfig.options, customHost)
129
+ const program = ts.createProgram(
130
+ fileNames,
131
+ cachedParsedConfig.options,
132
+ customHost,
133
+ cachedSchemaProgram // reuse previous program for incremental compilation
134
+ )
135
+ cachedSchemaProgram = program
136
+ return program
117
137
  }
118
138
 
119
139
  function generateTSSchemas(
@@ -313,6 +333,11 @@ export async function generateAllSchemas(
313
333
  requiredTypes
314
334
  )
315
335
 
336
+ if (cachedTSSchemas && cachedCustomTypesContent === customTypesContent) {
337
+ logger.debug('Reusing cached TS schemas (types unchanged)')
338
+ return { ...cachedTSSchemas, ...zodSchemas }
339
+ }
340
+
316
341
  const tsSchemas = generateTSSchemas(
317
342
  logger,
318
343
  config.tsconfig,
@@ -325,5 +350,8 @@ export async function generateAllSchemas(
325
350
  state.schemaLookup
326
351
  )
327
352
 
353
+ cachedCustomTypesContent = customTypesContent
354
+ cachedTSSchemas = tsSchemas
355
+
328
356
  return { ...tsSchemas, ...zodSchemas }
329
357
  }
@@ -596,5 +596,6 @@ export function deserializeInspectorState(
596
596
  openAPISpec: data.openAPISpec || null,
597
597
  diagnostics: data.diagnostics || [],
598
598
  addonFunctions: data.addonFunctions || {},
599
+ program: null,
599
600
  }
600
601
  }
@@ -400,6 +400,7 @@ export function convertDslToGraph(
400
400
  source,
401
401
  description: meta.description,
402
402
  tags: meta.tags,
403
+ inline: meta.inline,
403
404
  context: meta.context,
404
405
  nodes: nodesRecord,
405
406
  entryNodeIds,
@@ -192,6 +192,8 @@ export interface SerializedWorkflowGraph {
192
192
  description?: string
193
193
  /** Tags for organization */
194
194
  tags?: string[]
195
+ /** If true, workflow always executes inline without queues */
196
+ inline?: boolean
195
197
  /** Workflow context/state variables (from Zod schema) */
196
198
  context?: WorkflowContext
197
199
  /** Serialized nodes */