@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 +32 -0
- package/dist/add/add-middleware.js +6 -10
- package/dist/add/add-permission.js +10 -12
- package/dist/add/add-workflow.js +7 -1
- package/dist/error-codes.d.ts +1 -0
- package/dist/error-codes.js +2 -0
- package/dist/inspector.js +14 -7
- package/dist/types.d.ts +2 -0
- package/dist/utils/custom-types-generator.js +1 -0
- package/dist/utils/post-process.d.ts +9 -0
- package/dist/utils/post-process.js +46 -0
- package/dist/utils/schema-generator.js +26 -6
- package/dist/utils/serialize-inspector-state.js +1 -0
- package/dist/utils/workflow/graph/convert-dsl-to-graph.js +1 -0
- package/dist/utils/workflow/graph/workflow-graph.types.d.ts +2 -0
- package/package.json +4 -3
- package/src/add/add-middleware.ts +6 -14
- package/src/add/add-permission.ts +10 -16
- package/src/add/add-workflow.ts +11 -1
- package/src/error-codes.ts +3 -0
- package/src/inspector.ts +21 -6
- package/src/types.ts +2 -0
- package/src/utils/custom-types-generator.ts +1 -0
- package/src/utils/post-process.ts +59 -0
- package/src/utils/schema-generator.ts +38 -10
- package/src/utils/serialize-inspector-state.ts +1 -0
- package/src/utils/workflow/graph/convert-dsl-to-graph.ts +1 -0
- package/src/utils/workflow/graph/workflow-graph.types.ts +2 -0
- package/tsconfig.tsbuildinfo +1 -1
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
|
-
|
|
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
|
-
|
|
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
|
|
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 (
|
|
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 (
|
|
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
|
|
273
|
-
|
|
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
|
|
352
|
-
|
|
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];
|
package/dist/add/add-workflow.js
CHANGED
|
@@ -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
|
};
|
package/dist/error-codes.d.ts
CHANGED
|
@@ -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"
|
package/dist/error-codes.js
CHANGED
|
@@ -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
|
|
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
|
-
|
|
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(
|
|
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(
|
|
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
|
-
|
|
52
|
-
|
|
53
|
-
|
|
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 = [...
|
|
56
|
-
const defaultHost = ts.createCompilerHost(
|
|
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
|
-
|
|
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
|
}
|
|
@@ -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
|
+
"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.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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 (
|
|
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 (
|
|
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
|
|
377
|
-
|
|
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
|
|
483
|
-
|
|
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]
|
package/src/add/add-workflow.ts
CHANGED
|
@@ -11,7 +11,10 @@ import {
|
|
|
11
11
|
extractDescription,
|
|
12
12
|
extractDuration,
|
|
13
13
|
} from '../utils/extract-node-value.js'
|
|
14
|
-
import {
|
|
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
|
}
|
package/src/error-codes.ts
CHANGED
|
@@ -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',
|