@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/CHANGELOG.md CHANGED
@@ -1,5 +1,37 @@
1
1
  ## 0.12.0
2
2
 
3
+ ## 0.12.4
4
+
5
+ ### Patch Changes
6
+
7
+ - 5866b66: Add critical error (PKU490) when Zod schemas and wiring calls (wireHTTPRoutes, addPermission, addHTTPMiddleware) coexist in the same file. The CLI uses tsImport to extract Zod schemas at runtime, which executes all top-level code — wiring side-effects crash in this context because pikku state metadata doesn't exist. Schemas and wirings must be in separate files.
8
+ - e412b4d: Optimize CLI codegen performance: 12x faster `pikku all`
9
+
10
+ - Reuse schemas across re-inspections (skip redundant `ts-json-schema-generator` runs)
11
+ - Cache TS schemas to disk (`.pikku/schema-cache.json`) for cross-run reuse
12
+ - Pass `oldProgram` to `ts.createProgram` for incremental TS compilation
13
+ - Cache parsed tsconfig in schema generator between runs
14
+ - Auto-include direct `addPermission`/`addHTTPMiddleware` in bootstrap via side-effect imports
15
+ - Skip `pikkuAuth()` errors when nested inside `addPermission`/`addHTTPPermission`
16
+
17
+ - Updated dependencies [e412b4d]
18
+ - Updated dependencies [53dc8c8]
19
+ - Updated dependencies [0a1cc51]
20
+ - Updated dependencies [0a1cc51]
21
+ - Updated dependencies [0a1cc51]
22
+ - Updated dependencies [0a1cc51]
23
+ - Updated dependencies [0a1cc51]
24
+ - Updated dependencies [0a1cc51]
25
+ - Updated dependencies [0a1cc51]
26
+ - Updated dependencies [0a1cc51]
27
+ - Updated dependencies [0a1cc51]
28
+ - Updated dependencies [8b9b2e9]
29
+ - Updated dependencies [8b9b2e9]
30
+ - Updated dependencies [b973d44]
31
+ - Updated dependencies [8b9b2e9]
32
+ - Updated dependencies [8b9b2e9]
33
+ - @pikku/core@0.12.9
34
+
3
35
  ## 0.12.3
4
36
 
5
37
  ### Patch Changes
@@ -189,12 +189,10 @@ export const addMiddleware = (logger, node, checker, state) => {
189
189
  return;
190
190
  }
191
191
  const refs = extractMiddlewareRefs(middlewareArrayArg, checker, state.rootDir);
192
- if (refs.length === 0) {
193
- logger.warn(`• addMiddleware('${tag}', ...) has empty middleware array`);
194
- return;
195
- }
196
192
  const definitionIds = refs.map((r) => r.definitionId);
197
- renameTempDefinitions(state, definitionIds, 'tag', tag);
193
+ if (definitionIds.length > 0) {
194
+ renameTempDefinitions(state, definitionIds, 'tag', tag);
195
+ }
198
196
  const sourceFile = node.getSourceFile().fileName;
199
197
  const instanceIds = [];
200
198
  for (let i = 0; i < refs.length; i++) {
@@ -273,12 +271,10 @@ export const addMiddleware = (logger, node, checker, state) => {
273
271
  return;
274
272
  }
275
273
  const refs = extractMiddlewareRefs(middlewareArrayArg, checker, state.rootDir);
276
- if (refs.length === 0) {
277
- logger.warn(`• addHTTPMiddleware('${pattern}', ...) has empty middleware array`);
278
- return;
279
- }
280
274
  const definitionIds = refs.map((r) => r.definitionId);
281
- renameTempDefinitions(state, definitionIds, 'http', pattern);
275
+ if (definitionIds.length > 0) {
276
+ renameTempDefinitions(state, definitionIds, 'http', pattern);
277
+ }
282
278
  const sourceFile = node.getSourceFile().fileName;
283
279
  const instanceIds = [];
284
280
  for (let i = 0; i < refs.length; i++) {
@@ -21,12 +21,14 @@ function renameTempDefinitions(state, definitionIds, groupType, groupKey) {
21
21
  definitionIds[idx] = newId;
22
22
  }
23
23
  }
24
- function isInsidePermissionFactory(node) {
24
+ function isInsidePermissionContainer(node) {
25
25
  let current = node.parent;
26
26
  while (current) {
27
27
  if (ts.isCallExpression(current) &&
28
28
  ts.isIdentifier(current.expression) &&
29
- current.expression.text === 'pikkuPermissionFactory') {
29
+ (current.expression.text === 'pikkuPermissionFactory' ||
30
+ current.expression.text === 'addPermission' ||
31
+ current.expression.text === 'addHTTPPermission')) {
30
32
  return true;
31
33
  }
32
34
  current = current.parent;
@@ -47,7 +49,7 @@ export const addPermission = (logger, node, checker, state) => {
47
49
  // Handle pikkuPermission(...) - individual permission function definition
48
50
  if (expression.text === 'pikkuPermission') {
49
51
  // Skip if nested inside pikkuPermissionFactory — the factory handler extracts services itself
50
- if (isInsidePermissionFactory(node))
52
+ if (isInsidePermissionContainer(node))
51
53
  return;
52
54
  const arg = args[0];
53
55
  if (!arg)
@@ -110,7 +112,7 @@ export const addPermission = (logger, node, checker, state) => {
110
112
  return;
111
113
  }
112
114
  if (expression.text === 'pikkuAuth') {
113
- if (isInsidePermissionFactory(node))
115
+ if (isInsidePermissionContainer(node))
114
116
  return;
115
117
  const arg = args[0];
116
118
  if (!arg)
@@ -269,11 +271,9 @@ export const addPermission = (logger, node, checker, state) => {
269
271
  }
270
272
  // Extract permission pikkuFuncIds from array
271
273
  const permissionNames = extractPermissionPikkuNames(permissionsArrayArg, checker, state.rootDir);
272
- if (permissionNames.length === 0) {
273
- logger.warn(`• addPermission('${tag}', ...) has empty permissions array`);
274
- return;
274
+ if (permissionNames.length > 0) {
275
+ renameTempDefinitions(state, permissionNames, 'tag', tag);
275
276
  }
276
- renameTempDefinitions(state, permissionNames, 'tag', tag);
277
277
  const allServices = new Set();
278
278
  for (const permissionName of permissionNames) {
279
279
  const permissionMeta = state.permissions.definitions[permissionName];
@@ -348,11 +348,9 @@ export const addPermission = (logger, node, checker, state) => {
348
348
  }
349
349
  // Extract permission pikkuFuncIds from array
350
350
  const permissionNames = extractPermissionPikkuNames(permissionsArrayArg, checker, state.rootDir);
351
- if (permissionNames.length === 0) {
352
- logger.warn(`• addHTTPPermission('${pattern}', ...) has empty permissions array`);
353
- return;
351
+ if (permissionNames.length > 0) {
352
+ renameTempDefinitions(state, permissionNames, 'http', pattern);
354
353
  }
355
- renameTempDefinitions(state, permissionNames, 'http', pattern);
356
354
  const allServices = new Set();
357
355
  for (const permissionName of permissionNames) {
358
356
  const permissionMeta = state.permissions.definitions[permissionName];
@@ -3,7 +3,7 @@ import { extractFunctionName } from '../utils/extract-function-name.js';
3
3
  import { extractFunctionNode } from '../utils/extract-function-node.js';
4
4
  import { ErrorCode } from '../error-codes.js';
5
5
  import { extractStringLiteral, isStringLike, isFunctionLike, extractDescription, extractDuration, } from '../utils/extract-node-value.js';
6
- import { getCommonWireMetaData } from '../utils/get-property-value.js';
6
+ import { getCommonWireMetaData, getPropertyValue, } from '../utils/get-property-value.js';
7
7
  import { extractDSLWorkflow } from '../utils/workflow/dsl/extract-dsl-workflow.js';
8
8
  /**
9
9
  * Recursively check if any step has inline type (non-serializable)
@@ -182,6 +182,7 @@ export const addWorkflow = (logger, node, checker, state) => {
182
182
  let summary;
183
183
  let description;
184
184
  let errors;
185
+ let inline;
185
186
  if (ts.isObjectLiteralExpression(firstArg)) {
186
187
  const metadata = getCommonWireMetaData(firstArg, 'Workflow', workflowName, logger);
187
188
  if (metadata.disabled)
@@ -190,6 +191,10 @@ export const addWorkflow = (logger, node, checker, state) => {
190
191
  summary = metadata.summary;
191
192
  description = metadata.description;
192
193
  errors = metadata.errors;
194
+ const inlineProp = getPropertyValue(firstArg, 'inline');
195
+ if (inlineProp === true) {
196
+ inline = true;
197
+ }
193
198
  }
194
199
  // Validate that we got a valid function
195
200
  if (ts.isObjectLiteralExpression(firstArg) &&
@@ -272,5 +277,6 @@ export const addWorkflow = (logger, node, checker, state) => {
272
277
  description,
273
278
  errors,
274
279
  tags,
280
+ inline,
275
281
  };
276
282
  };
@@ -49,6 +49,7 @@ export declare enum ErrorCode {
49
49
  MANIFEST_INTEGRITY_ERROR = "PKU865",
50
50
  MISSING_MODEL = "PKU145",
51
51
  INVALID_MODEL = "PKU146",
52
+ SCHEMA_AND_WIRING_COLOCATED = "PKU490",
52
53
  SERVICES_NOT_DESTRUCTURED = "PKU410",
53
54
  WIRES_NOT_DESTRUCTURED = "PKU411",
54
55
  WORKFLOW_MULTI_QUEUE_NOT_SUPPORTED = "PKU901"
@@ -58,6 +58,8 @@ export var ErrorCode;
58
58
  // Model configuration errors
59
59
  ErrorCode["MISSING_MODEL"] = "PKU145";
60
60
  ErrorCode["INVALID_MODEL"] = "PKU146";
61
+ // File structure errors
62
+ ErrorCode["SCHEMA_AND_WIRING_COLOCATED"] = "PKU490";
61
63
  // Optimization diagnostics
62
64
  ErrorCode["SERVICES_NOT_DESTRUCTURED"] = "PKU410";
63
65
  ErrorCode["WIRES_NOT_DESTRUCTURED"] = "PKU411";
package/dist/inspector.js CHANGED
@@ -4,7 +4,7 @@ import { visitSetup, visitRoutes } from './visit.js';
4
4
  import { TypesMap } from './types-map.js';
5
5
  import { getFilesAndMethods } from './utils/get-files-and-methods.js';
6
6
  import { findCommonAncestor } from './utils/find-root-dir.js';
7
- import { aggregateRequiredServices, validateAgentModels, validateAgentOverrides, validateSecretOverrides, validateVariableOverrides, computeResolvedIOTypes, computeMiddlewareGroupsMeta, computePermissionsGroupsMeta, computeRequiredSchemas, computeDiagnostics, } from './utils/post-process.js';
7
+ import { aggregateRequiredServices, validateAgentModels, validateAgentOverrides, validateSecretOverrides, validateVariableOverrides, computeResolvedIOTypes, computeMiddlewareGroupsMeta, computePermissionsGroupsMeta, computeRequiredSchemas, computeDiagnostics, validateSchemaWiringSeparation, } from './utils/post-process.js';
8
8
  import { generateOpenAPISpec } from './utils/serialize-openapi-json.js';
9
9
  import { pikkuState } from '@pikku/core/internal';
10
10
  import { resolveLatestVersions } from './utils/resolve-versions.js';
@@ -174,11 +174,11 @@ export function getInitialInspectorState(rootDir) {
174
174
  openAPISpec: null,
175
175
  diagnostics: [],
176
176
  addonFunctions: {},
177
+ program: null,
177
178
  };
178
179
  }
179
180
  export const inspect = async (logger, routeFiles, options = {}) => {
180
- const startProgram = performance.now();
181
- const program = ts.createProgram(routeFiles, {
181
+ const compilerOptions = {
182
182
  target: ts.ScriptTarget.ESNext,
183
183
  module: ts.ModuleKind.Node16,
184
184
  skipLibCheck: true,
@@ -187,8 +187,11 @@ export const inspect = async (logger, routeFiles, options = {}) => {
187
187
  types: [],
188
188
  allowJs: false,
189
189
  checkJs: false,
190
- });
191
- logger.debug(`Created program in ${(performance.now() - startProgram).toFixed(2)}ms`);
190
+ };
191
+ const startProgram = performance.now();
192
+ const program = ts.createProgram(routeFiles, compilerOptions, undefined, // host
193
+ options.oldProgram);
194
+ logger.debug(`Created program in ${(performance.now() - startProgram).toFixed(0)}ms (${routeFiles.length} files${options.oldProgram ? ', incremental' : ''})`);
192
195
  const startChecker = performance.now();
193
196
  const checker = program.getTypeChecker();
194
197
  logger.debug(`Got type checker in ${(performance.now() - startChecker).toFixed(2)}ms`);
@@ -207,7 +210,7 @@ export const inspect = async (logger, routeFiles, options = {}) => {
207
210
  for (const sourceFile of sourceFiles) {
208
211
  ts.forEachChild(sourceFile, (child) => visitSetup(logger, checker, child, state, options));
209
212
  }
210
- logger.debug(`Visit setup phase completed in ${(performance.now() - startSetup).toFixed(2)}ms`);
213
+ logger.debug(`Visit setup phase completed in ${(performance.now() - startSetup).toFixed(0)}ms`);
211
214
  // Load addon function metadata so wirings can reference addon functions
212
215
  await loadAddonFunctionsMeta(logger, state);
213
216
  if (!options.setupOnly) {
@@ -216,10 +219,12 @@ export const inspect = async (logger, routeFiles, options = {}) => {
216
219
  for (const sourceFile of sourceFiles) {
217
220
  ts.forEachChild(sourceFile, (child) => visitRoutes(logger, checker, child, state, options));
218
221
  }
219
- logger.debug(`Visit routes phase completed in ${(performance.now() - startRoutes).toFixed(2)}ms`);
222
+ logger.debug(`Visit routes phase completed in ${(performance.now() - startRoutes).toFixed(0)}ms`);
220
223
  resolveLatestVersions(state, logger);
221
224
  if (options.schemaConfig) {
225
+ const startSchemas = performance.now();
222
226
  state.schemas = await generateAllSchemas(logger, options.schemaConfig, state);
227
+ logger.debug(`generateAllSchemas took ${(performance.now() - startSchemas).toFixed(0)}ms`);
223
228
  computeContractHashes(state.schemas, state.functions.typesMap, state.functions.meta);
224
229
  computeRequiredSchemas(state, options);
225
230
  }
@@ -248,6 +253,7 @@ export const inspect = async (logger, routeFiles, options = {}) => {
248
253
  computeMiddlewareGroupsMeta(state);
249
254
  computePermissionsGroupsMeta(state);
250
255
  computeDiagnostics(state);
256
+ validateSchemaWiringSeparation(logger, state);
251
257
  if (options.openAPI) {
252
258
  state.openAPISpec = await generateOpenAPISpec(logger, state.functions.meta, state.http.meta, state.schemas, options.openAPI.additionalInfo, pikkuState(null, 'misc', 'errors'));
253
259
  }
@@ -256,5 +262,6 @@ export const inspect = async (logger, routeFiles, options = {}) => {
256
262
  validateSecretOverrides(logger, state);
257
263
  validateVariableOverrides(logger, state);
258
264
  }
265
+ state.program = program;
259
266
  return state;
260
267
  };
package/dist/types.d.ts CHANGED
@@ -193,6 +193,7 @@ export type InspectorOptions = Partial<{
193
193
  tags: string[];
194
194
  manifest: VersionManifest;
195
195
  modelConfig: InspectorModelConfig;
196
+ oldProgram: ts.Program | undefined;
196
197
  }>;
197
198
  export interface InspectorLogger {
198
199
  info: (message: string) => void;
@@ -393,4 +394,5 @@ export interface InspectorState {
393
394
  openAPISpec: Record<string, any> | null;
394
395
  diagnostics: InspectorDiagnostic[];
395
396
  addonFunctions: Record<string, FunctionsMeta>;
397
+ program: ts.Program | null;
396
398
  }
@@ -9,6 +9,7 @@ export function sanitizeTypeName(name) {
9
9
  }
10
10
  export function generateCustomTypes(typesMap, requiredTypes) {
11
11
  const typeDeclarations = Array.from(typesMap.customTypes.entries())
12
+ .sort(([a], [b]) => a.localeCompare(b))
12
13
  .filter(([_name, { type }]) => {
13
14
  const hasUndefinedGeneric = /\b(Name|In|Out|Key)\b/.test(type) && /\[.*\]/.test(type);
14
15
  return !hasUndefinedGeneric;
@@ -22,4 +22,13 @@ export declare function computePermissionsGroupsMeta(state: InspectorState): voi
22
22
  export declare function computeRequiredSchemas(state: InspectorState, options: InspectorOptions): void;
23
23
  export declare function validateAgentModels(logger: InspectorLogger, state: InspectorState | Omit<InspectorState, 'typesLookup'>, modelConfig?: InspectorModelConfig): void;
24
24
  export declare function validateAgentOverrides(logger: InspectorLogger, state: InspectorState | Omit<InspectorState, 'typesLookup'>, modelConfig?: InspectorModelConfig): void;
25
+ /**
26
+ * Validates that Zod schemas and wiring side-effects (wireHTTPRoutes,
27
+ * addPermission, addHTTPMiddleware, etc.) do not coexist in the same file.
28
+ *
29
+ * The CLI uses tsImport to extract Zod schemas at runtime, which executes
30
+ * all top-level code in the file. Wiring calls crash during this process
31
+ * because the pikku state metadata doesn't exist in the CLI context.
32
+ */
33
+ export declare function validateSchemaWiringSeparation(logger: InspectorLogger, state: InspectorState): void;
25
34
  export declare function computeDiagnostics(state: InspectorState): void;
@@ -378,6 +378,52 @@ export function validateAgentOverrides(logger, state, modelConfig) {
378
378
  }
379
379
  }
380
380
  }
381
+ /**
382
+ * Validates that Zod schemas and wiring side-effects (wireHTTPRoutes,
383
+ * addPermission, addHTTPMiddleware, etc.) do not coexist in the same file.
384
+ *
385
+ * The CLI uses tsImport to extract Zod schemas at runtime, which executes
386
+ * all top-level code in the file. Wiring calls crash during this process
387
+ * because the pikku state metadata doesn't exist in the CLI context.
388
+ */
389
+ export function validateSchemaWiringSeparation(logger, state) {
390
+ // Collect files that contain schemas
391
+ const schemaFiles = new Set();
392
+ for (const ref of state.schemaLookup.values()) {
393
+ schemaFiles.add(ref.sourceFile);
394
+ }
395
+ // Collect files that contain wiring side-effects
396
+ const wiringFiles = new Set();
397
+ // HTTP route wirings
398
+ for (const file of state.http.files) {
399
+ wiringFiles.add(file);
400
+ }
401
+ // Permission wirings (addPermission calls)
402
+ for (const meta of state.permissions.tagPermissions.values()) {
403
+ wiringFiles.add(meta.sourceFile);
404
+ }
405
+ for (const meta of state.http.routePermissions.values()) {
406
+ wiringFiles.add(meta.sourceFile);
407
+ }
408
+ // Middleware wirings (addHTTPMiddleware calls)
409
+ for (const meta of state.http.routeMiddleware.values()) {
410
+ wiringFiles.add(meta.sourceFile);
411
+ }
412
+ for (const meta of state.middleware.tagMiddleware.values()) {
413
+ wiringFiles.add(meta.sourceFile);
414
+ }
415
+ // Check for overlap
416
+ for (const file of schemaFiles) {
417
+ if (wiringFiles.has(file)) {
418
+ const schemas = Array.from(state.schemaLookup.entries())
419
+ .filter(([, ref]) => ref.sourceFile === file)
420
+ .map(([name]) => name);
421
+ logger.critical(ErrorCode.SCHEMA_AND_WIRING_COLOCATED, `File '${file}' contains both Zod schemas (${schemas.join(', ')}) and wiring calls (wireHTTPRoutes, addPermission, etc.). ` +
422
+ `These must be in separate files because the CLI imports schema files at runtime, which triggers wiring side-effects that crash without server context. ` +
423
+ `Move the route/wiring definitions to a dedicated wiring file.`);
424
+ }
425
+ }
426
+ }
381
427
  export function computeDiagnostics(state) {
382
428
  const diagnostics = [];
383
429
  for (const [id, meta] of Object.entries(state.functions.meta)) {
@@ -46,14 +46,25 @@ function primitiveTypeToSchema(typeStr) {
46
46
  }
47
47
  return null;
48
48
  }
49
+ // Cached state for schema program reuse across inspect() calls
50
+ let cachedSchemaProgram;
51
+ let cachedParsedConfig;
52
+ let cachedTsconfigPath;
53
+ let cachedCustomTypesContent;
54
+ let cachedTSSchemas;
49
55
  function createProgramWithVirtualFile(tsconfig, virtualFilePath, virtualFileContent) {
50
56
  const configPath = resolve(tsconfig);
51
- const configFile = ts.readConfigFile(configPath, ts.sys.readFile);
52
- const basePath = dirname(configPath);
53
- const parsedConfig = ts.parseJsonConfigFileContent(configFile.config, ts.sys, basePath);
57
+ // Cache the parsed tsconfig — it doesn't change between runs
58
+ if (!cachedParsedConfig || cachedTsconfigPath !== configPath) {
59
+ const configFile = ts.readConfigFile(configPath, ts.sys.readFile);
60
+ const basePath = dirname(configPath);
61
+ cachedParsedConfig = ts.parseJsonConfigFileContent(configFile.config, ts.sys, basePath);
62
+ cachedTsconfigPath = configPath;
63
+ cachedSchemaProgram = undefined;
64
+ }
54
65
  const resolvedVirtualPath = resolve(virtualFilePath);
55
- const fileNames = [...parsedConfig.fileNames, resolvedVirtualPath];
56
- const defaultHost = ts.createCompilerHost(parsedConfig.options);
66
+ const fileNames = [...cachedParsedConfig.fileNames, resolvedVirtualPath];
67
+ const defaultHost = ts.createCompilerHost(cachedParsedConfig.options);
57
68
  const customHost = {
58
69
  ...defaultHost,
59
70
  getSourceFile(fileName, languageVersionOrOptions, onError, shouldCreateNewSourceFile) {
@@ -73,7 +84,10 @@ function createProgramWithVirtualFile(tsconfig, virtualFilePath, virtualFileCont
73
84
  return defaultHost.readFile(fileName);
74
85
  },
75
86
  };
76
- return ts.createProgram(fileNames, parsedConfig.options, customHost);
87
+ const program = ts.createProgram(fileNames, cachedParsedConfig.options, customHost, cachedSchemaProgram // reuse previous program for incremental compilation
88
+ );
89
+ cachedSchemaProgram = program;
90
+ return program;
77
91
  }
78
92
  function generateTSSchemas(logger, tsconfig, customTypesContent, typesMap, functionMeta, httpWiringsMeta, additionalTypes, additionalProperties = false, schemaLookup) {
79
93
  const schemasSet = new Set(typesMap.customTypes.keys());
@@ -204,6 +218,12 @@ export async function generateAllSchemas(logger, config, state) {
204
218
  const zodSchemas = await generateZodSchemas(logger, state.schemaLookup, state.functions.typesMap);
205
219
  const requiredTypes = new Set();
206
220
  const customTypesContent = generateCustomTypes(state.functions.typesMap, requiredTypes);
221
+ if (cachedTSSchemas && cachedCustomTypesContent === customTypesContent) {
222
+ logger.debug('Reusing cached TS schemas (types unchanged)');
223
+ return { ...cachedTSSchemas, ...zodSchemas };
224
+ }
207
225
  const tsSchemas = generateTSSchemas(logger, config.tsconfig, customTypesContent, state.functions.typesMap, state.functions.meta, state.http.meta, config.schemasFromTypes, config.schema?.additionalProperties, state.schemaLookup);
226
+ cachedCustomTypesContent = customTypesContent;
227
+ cachedTSSchemas = tsSchemas;
208
228
  return { ...tsSchemas, ...zodSchemas };
209
229
  }
@@ -292,5 +292,6 @@ export function deserializeInspectorState(data) {
292
292
  openAPISpec: data.openAPISpec || null,
293
293
  diagnostics: data.diagnostics || [],
294
294
  addonFunctions: data.addonFunctions || {},
295
+ program: null,
295
296
  };
296
297
  }
@@ -301,6 +301,7 @@ export function convertDslToGraph(workflowName, meta) {
301
301
  source,
302
302
  description: meta.description,
303
303
  tags: meta.tags,
304
+ inline: meta.inline,
304
305
  context: meta.context,
305
306
  nodes: nodesRecord,
306
307
  entryNodeIds,
@@ -141,6 +141,8 @@ export interface SerializedWorkflowGraph {
141
141
  description?: string;
142
142
  /** Tags for organization */
143
143
  tags?: string[];
144
+ /** If true, workflow always executes inline without queues */
145
+ inline?: boolean;
144
146
  /** Workflow context/state variables (from Zod schema) */
145
147
  context?: WorkflowContext;
146
148
  /** Serialized nodes */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pikku/inspector",
3
- "version": "0.12.3",
3
+ "version": "0.12.4",
4
4
  "author": "yasser.fadl@gmail.com",
5
5
  "license": "BUSL-1.1",
6
6
  "type": "module",
@@ -30,11 +30,12 @@
30
30
  "release": "yarn build && npm test",
31
31
  "test": "bash run-tests.sh",
32
32
  "test:watch": "bash run-tests.sh --watch",
33
- "test:coverage": "bash run-tests.sh --coverage"
33
+ "test:coverage": "bash run-tests.sh --coverage",
34
+ "prepublishOnly": "yarn build"
34
35
  },
35
36
  "dependencies": {
36
37
  "@openapi-contrib/json-schema-to-openapi-schema": "^4.3.1",
37
- "@pikku/core": "^0.12.3",
38
+ "@pikku/core": "^0.12.9",
38
39
  "path-to-regexp": "^8.3.0",
39
40
  "ts-json-schema-generator": "^2.5.0",
40
41
  "tsx": "^4.21.0",
@@ -274,13 +274,10 @@ export const addMiddleware: AddWiring = (logger, node, checker, state) => {
274
274
  state.rootDir
275
275
  )
276
276
 
277
- if (refs.length === 0) {
278
- logger.warn(`• addMiddleware('${tag}', ...) has empty middleware array`)
279
- return
280
- }
281
-
282
277
  const definitionIds = refs.map((r) => r.definitionId)
283
- renameTempDefinitions(state, definitionIds, 'tag', tag)
278
+ if (definitionIds.length > 0) {
279
+ renameTempDefinitions(state, definitionIds, 'tag', tag)
280
+ }
284
281
 
285
282
  const sourceFile = node.getSourceFile().fileName
286
283
  const instanceIds: string[] = []
@@ -384,15 +381,10 @@ export const addMiddleware: AddWiring = (logger, node, checker, state) => {
384
381
  state.rootDir
385
382
  )
386
383
 
387
- if (refs.length === 0) {
388
- logger.warn(
389
- `• addHTTPMiddleware('${pattern}', ...) has empty middleware array`
390
- )
391
- return
392
- }
393
-
394
384
  const definitionIds = refs.map((r) => r.definitionId)
395
- renameTempDefinitions(state, definitionIds, 'http', pattern)
385
+ if (definitionIds.length > 0) {
386
+ renameTempDefinitions(state, definitionIds, 'http', pattern)
387
+ }
396
388
 
397
389
  const sourceFile = node.getSourceFile().fileName
398
390
  const instanceIds: string[] = []
@@ -38,13 +38,15 @@ function renameTempDefinitions(
38
38
  }
39
39
  }
40
40
 
41
- function isInsidePermissionFactory(node: ts.Node): boolean {
41
+ function isInsidePermissionContainer(node: ts.Node): boolean {
42
42
  let current = node.parent
43
43
  while (current) {
44
44
  if (
45
45
  ts.isCallExpression(current) &&
46
46
  ts.isIdentifier(current.expression) &&
47
- current.expression.text === 'pikkuPermissionFactory'
47
+ (current.expression.text === 'pikkuPermissionFactory' ||
48
+ current.expression.text === 'addPermission' ||
49
+ current.expression.text === 'addHTTPPermission')
48
50
  ) {
49
51
  return true
50
52
  }
@@ -69,7 +71,7 @@ export const addPermission: AddWiring = (logger, node, checker, state) => {
69
71
  // Handle pikkuPermission(...) - individual permission function definition
70
72
  if (expression.text === 'pikkuPermission') {
71
73
  // Skip if nested inside pikkuPermissionFactory — the factory handler extracts services itself
72
- if (isInsidePermissionFactory(node)) return
74
+ if (isInsidePermissionContainer(node)) return
73
75
 
74
76
  const arg = args[0]
75
77
  if (!arg) return
@@ -156,7 +158,7 @@ export const addPermission: AddWiring = (logger, node, checker, state) => {
156
158
  }
157
159
 
158
160
  if (expression.text === 'pikkuAuth') {
159
- if (isInsidePermissionFactory(node)) return
161
+ if (isInsidePermissionContainer(node)) return
160
162
 
161
163
  const arg = args[0]
162
164
  if (!arg) return
@@ -373,13 +375,10 @@ export const addPermission: AddWiring = (logger, node, checker, state) => {
373
375
  state.rootDir
374
376
  )
375
377
 
376
- if (permissionNames.length === 0) {
377
- logger.warn(`• addPermission('${tag}', ...) has empty permissions array`)
378
- return
378
+ if (permissionNames.length > 0) {
379
+ renameTempDefinitions(state, permissionNames, 'tag', tag)
379
380
  }
380
381
 
381
- renameTempDefinitions(state, permissionNames, 'tag', tag)
382
-
383
382
  const allServices = new Set<string>()
384
383
  for (const permissionName of permissionNames) {
385
384
  const permissionMeta = state.permissions.definitions[permissionName]
@@ -479,15 +478,10 @@ export const addPermission: AddWiring = (logger, node, checker, state) => {
479
478
  state.rootDir
480
479
  )
481
480
 
482
- if (permissionNames.length === 0) {
483
- logger.warn(
484
- `• addHTTPPermission('${pattern}', ...) has empty permissions array`
485
- )
486
- return
481
+ if (permissionNames.length > 0) {
482
+ renameTempDefinitions(state, permissionNames, 'http', pattern)
487
483
  }
488
484
 
489
- renameTempDefinitions(state, permissionNames, 'http', pattern)
490
-
491
485
  const allServices = new Set<string>()
492
486
  for (const permissionName of permissionNames) {
493
487
  const permissionMeta = state.permissions.definitions[permissionName]
@@ -11,7 +11,10 @@ import {
11
11
  extractDescription,
12
12
  extractDuration,
13
13
  } from '../utils/extract-node-value.js'
14
- import { getCommonWireMetaData } from '../utils/get-property-value.js'
14
+ import {
15
+ getCommonWireMetaData,
16
+ getPropertyValue,
17
+ } from '../utils/get-property-value.js'
15
18
  import { extractDSLWorkflow } from '../utils/workflow/dsl/extract-dsl-workflow.js'
16
19
 
17
20
  /**
@@ -206,6 +209,7 @@ export const addWorkflow: AddWiring = (logger, node, checker, state) => {
206
209
  let summary: string | undefined
207
210
  let description: string | undefined
208
211
  let errors: string[] | undefined
212
+ let inline: boolean | undefined
209
213
 
210
214
  if (ts.isObjectLiteralExpression(firstArg)) {
211
215
  const metadata = getCommonWireMetaData(
@@ -219,6 +223,11 @@ export const addWorkflow: AddWiring = (logger, node, checker, state) => {
219
223
  summary = metadata.summary
220
224
  description = metadata.description
221
225
  errors = metadata.errors
226
+
227
+ const inlineProp = getPropertyValue(firstArg, 'inline')
228
+ if (inlineProp === true) {
229
+ inline = true
230
+ }
222
231
  }
223
232
 
224
233
  // Validate that we got a valid function
@@ -324,5 +333,6 @@ export const addWorkflow: AddWiring = (logger, node, checker, state) => {
324
333
  description,
325
334
  errors,
326
335
  tags,
336
+ inline,
327
337
  }
328
338
  }
@@ -66,6 +66,9 @@ export enum ErrorCode {
66
66
  MISSING_MODEL = 'PKU145',
67
67
  INVALID_MODEL = 'PKU146',
68
68
 
69
+ // File structure errors
70
+ SCHEMA_AND_WIRING_COLOCATED = 'PKU490',
71
+
69
72
  // Optimization diagnostics
70
73
  SERVICES_NOT_DESTRUCTURED = 'PKU410',
71
74
  WIRES_NOT_DESTRUCTURED = 'PKU411',