@pikku/inspector 0.12.11 → 0.12.13
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-cli.js +10 -3
- package/dist/add/add-credential.js +2 -1
- package/dist/add/add-functions.js +48 -1
- package/dist/add/add-http-route.js +24 -5
- package/dist/add/add-keyed-wiring.js +3 -1
- package/dist/add/add-middleware.js +33 -4
- package/dist/add/add-permission.js +7 -7
- package/dist/add/add-workflow-graph.js +20 -1
- package/dist/error-codes.d.ts +3 -1
- package/dist/error-codes.js +3 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/inspector.js +2 -5
- package/dist/types.d.ts +10 -19
- package/dist/utils/check-pii-output.d.ts +14 -0
- package/dist/utils/check-pii-output.js +63 -0
- package/dist/utils/extract-function-name.js +6 -0
- package/dist/utils/filter-inspector-state.js +187 -59
- package/dist/utils/filter-utils.js +13 -5
- package/dist/utils/get-property-value.d.ts +10 -0
- package/dist/utils/get-property-value.js +30 -0
- package/dist/utils/post-process.d.ts +2 -3
- package/dist/utils/post-process.js +3 -23
- package/dist/utils/resolve-addon-package.d.ts +4 -5
- package/dist/utils/resolve-addon-package.js +64 -16
- package/dist/utils/resolve-deploy-target.d.ts +28 -0
- package/dist/utils/resolve-deploy-target.js +56 -0
- package/dist/utils/resolve-versions.js +79 -0
- package/dist/utils/schema-generator.js +31 -12
- package/package.json +2 -2
- package/src/add/add-cli.ts +10 -3
- package/src/add/add-credential.ts +3 -0
- package/src/add/add-functions.test.ts +149 -0
- package/src/add/add-functions.ts +61 -1
- package/src/add/add-gateway.ts +5 -1
- package/src/add/add-http-route.ts +26 -6
- package/src/add/add-keyed-wiring.ts +7 -1
- package/src/add/add-mcp-prompt.ts +5 -1
- package/src/add/add-mcp-resource.ts +5 -1
- package/src/add/add-middleware.ts +42 -4
- package/src/add/add-permission.ts +7 -7
- package/src/add/add-schedule.ts +5 -1
- package/src/add/add-workflow-graph.ts +19 -1
- package/src/add/pii-check.test.ts +197 -0
- package/src/add/wire-name-literal.test.ts +114 -0
- package/src/error-codes.ts +4 -0
- package/src/index.ts +1 -0
- package/src/inspector.ts +1 -5
- package/src/types.ts +19 -15
- package/src/utils/check-pii-output.ts +76 -0
- package/src/utils/extract-function-name.ts +8 -0
- package/src/utils/filter-inspector-state.test.ts +168 -64
- package/src/utils/filter-inspector-state.ts +290 -64
- package/src/utils/filter-utils.test.ts +30 -15
- package/src/utils/filter-utils.ts +14 -5
- package/src/utils/get-property-value.ts +40 -0
- package/src/utils/post-process.ts +3 -38
- package/src/utils/resolve-addon-package.ts +65 -14
- package/src/utils/resolve-deploy-target.test.ts +105 -0
- package/src/utils/resolve-deploy-target.ts +63 -0
- package/src/utils/resolve-versions.test.ts +108 -0
- package/src/utils/resolve-versions.ts +86 -0
- package/src/utils/schema-generator.ts +37 -13
- package/tsconfig.tsbuildinfo +1 -1
|
@@ -1,12 +1,42 @@
|
|
|
1
1
|
import * as ts from 'typescript';
|
|
2
|
+
import { existsSync, readFileSync } from 'fs';
|
|
3
|
+
import { dirname, join, parse } from 'path';
|
|
4
|
+
const packageNameCache = new Map();
|
|
5
|
+
const findPackageNameForFile = (filePath) => {
|
|
6
|
+
if (packageNameCache.has(filePath)) {
|
|
7
|
+
return packageNameCache.get(filePath);
|
|
8
|
+
}
|
|
9
|
+
const root = parse(filePath).root;
|
|
10
|
+
let dir = dirname(filePath);
|
|
11
|
+
while (dir && dir !== root) {
|
|
12
|
+
const pkgPath = join(dir, 'package.json');
|
|
13
|
+
if (existsSync(pkgPath)) {
|
|
14
|
+
try {
|
|
15
|
+
const pkg = JSON.parse(readFileSync(pkgPath, 'utf-8'));
|
|
16
|
+
const name = typeof pkg.name === 'string' ? pkg.name : null;
|
|
17
|
+
packageNameCache.set(filePath, name);
|
|
18
|
+
return name;
|
|
19
|
+
}
|
|
20
|
+
catch {
|
|
21
|
+
packageNameCache.set(filePath, null);
|
|
22
|
+
return null;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
const parent = dirname(dir);
|
|
26
|
+
if (parent === dir)
|
|
27
|
+
break;
|
|
28
|
+
dir = parent;
|
|
29
|
+
}
|
|
30
|
+
packageNameCache.set(filePath, null);
|
|
31
|
+
return null;
|
|
32
|
+
};
|
|
2
33
|
/**
|
|
3
34
|
* Resolve the addon package name from an imported identifier.
|
|
4
35
|
* Checks if the identifier's import module specifier matches any
|
|
5
|
-
* configured addon package
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
* addon package.
|
|
36
|
+
* configured addon package — and if the import is relative (because
|
|
37
|
+
* the identifier resolves to a source file inside the addon package
|
|
38
|
+
* itself), walks up to the nearest package.json to obtain the real
|
|
39
|
+
* package name.
|
|
10
40
|
*/
|
|
11
41
|
export const resolveAddonName = (identifier, checker, wireAddonDeclarations) => {
|
|
12
42
|
if (!wireAddonDeclarations || wireAddonDeclarations.size === 0) {
|
|
@@ -16,18 +46,36 @@ export const resolveAddonName = (identifier, checker, wireAddonDeclarations) =>
|
|
|
16
46
|
if (!sym)
|
|
17
47
|
return null;
|
|
18
48
|
const decl = sym.declarations?.[0];
|
|
19
|
-
if (!decl
|
|
20
|
-
return null;
|
|
21
|
-
// ImportSpecifier -> NamedImports -> ImportClause -> ImportDeclaration
|
|
22
|
-
const importDecl = decl.parent?.parent?.parent;
|
|
23
|
-
if (!importDecl || !ts.isImportDeclaration(importDecl))
|
|
49
|
+
if (!decl)
|
|
24
50
|
return null;
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
if (
|
|
30
|
-
|
|
51
|
+
let candidatePackage = null;
|
|
52
|
+
if (ts.isImportSpecifier(decl)) {
|
|
53
|
+
// ImportSpecifier -> NamedImports -> ImportClause -> ImportDeclaration
|
|
54
|
+
const importDecl = decl.parent?.parent?.parent;
|
|
55
|
+
if (importDecl &&
|
|
56
|
+
ts.isImportDeclaration(importDecl) &&
|
|
57
|
+
ts.isStringLiteral(importDecl.moduleSpecifier)) {
|
|
58
|
+
candidatePackage = importDecl.moduleSpecifier.text;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
// Bare package import path
|
|
62
|
+
if (candidatePackage && !candidatePackage.startsWith('.')) {
|
|
63
|
+
for (const addonDecl of wireAddonDeclarations.values()) {
|
|
64
|
+
if (addonDecl.package === candidatePackage)
|
|
65
|
+
return addonDecl.package;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
// Fall back to package.json lookup based on the declaration's source file.
|
|
69
|
+
// This catches the case where the identifier resolves into an addon
|
|
70
|
+
// package's own internal source (relative import inside that package).
|
|
71
|
+
const declFile = decl.getSourceFile()?.fileName;
|
|
72
|
+
if (declFile) {
|
|
73
|
+
const pkgName = findPackageNameForFile(declFile);
|
|
74
|
+
if (pkgName) {
|
|
75
|
+
for (const addonDecl of wireAddonDeclarations.values()) {
|
|
76
|
+
if (addonDecl.package === pkgName)
|
|
77
|
+
return addonDecl.package;
|
|
78
|
+
}
|
|
31
79
|
}
|
|
32
80
|
}
|
|
33
81
|
return null;
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import type { FunctionMeta } from '@pikku/core';
|
|
2
|
+
/**
|
|
3
|
+
* Thrown when a function's explicit `deploy: 'serverless'` conflicts
|
|
4
|
+
* with one of its services being declared `serverlessIncompatible`.
|
|
5
|
+
* The user has to either remove the explicit flag (let it auto-resolve
|
|
6
|
+
* to 'server'), or set `deploy: 'server'` explicitly.
|
|
7
|
+
*/
|
|
8
|
+
export declare class IncompatibleDeployTargetError extends Error {
|
|
9
|
+
readonly functionName: string;
|
|
10
|
+
readonly incompatibleServices: string[];
|
|
11
|
+
constructor(functionName: string, incompatibleServices: string[]);
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Determine the effective deploy target for a function.
|
|
15
|
+
*
|
|
16
|
+
* Resolution order:
|
|
17
|
+
* 1. If any of the function's services is in `serverlessIncompatible`:
|
|
18
|
+
* - throw if the function explicitly declares `deploy: 'serverless'`
|
|
19
|
+
* - otherwise target is 'server'
|
|
20
|
+
* 2. Explicit `funcMeta.deploy: 'serverless' | 'server'`
|
|
21
|
+
* 3. Default 'serverless'
|
|
22
|
+
*
|
|
23
|
+
* Used both by the per-unit deploy analyzer (when bucketing functions
|
|
24
|
+
* into deployment units) and by `filterInspectorState` (when
|
|
25
|
+
* `pikku all --deploy <target>` is used to emit a target-scoped set
|
|
26
|
+
* of gen files).
|
|
27
|
+
*/
|
|
28
|
+
export declare function resolveDeployTarget(funcMeta: Pick<FunctionMeta, 'deploy' | 'services'>, serverlessIncompatible: Set<string>, functionName?: string): 'serverless' | 'server';
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Thrown when a function's explicit `deploy: 'serverless'` conflicts
|
|
3
|
+
* with one of its services being declared `serverlessIncompatible`.
|
|
4
|
+
* The user has to either remove the explicit flag (let it auto-resolve
|
|
5
|
+
* to 'server'), or set `deploy: 'server'` explicitly.
|
|
6
|
+
*/
|
|
7
|
+
export class IncompatibleDeployTargetError extends Error {
|
|
8
|
+
functionName;
|
|
9
|
+
incompatibleServices;
|
|
10
|
+
constructor(functionName, incompatibleServices) {
|
|
11
|
+
super(`Function '${functionName}' is declared deploy: 'serverless' but uses ` +
|
|
12
|
+
`serverless-incompatible service(s) [${incompatibleServices.join(', ')}]. ` +
|
|
13
|
+
`Either remove deploy: 'serverless' (will auto-resolve to 'server'), ` +
|
|
14
|
+
`or set deploy: 'server' explicitly.`);
|
|
15
|
+
this.functionName = functionName;
|
|
16
|
+
this.incompatibleServices = incompatibleServices;
|
|
17
|
+
this.name = 'IncompatibleDeployTargetError';
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Determine the effective deploy target for a function.
|
|
22
|
+
*
|
|
23
|
+
* Resolution order:
|
|
24
|
+
* 1. If any of the function's services is in `serverlessIncompatible`:
|
|
25
|
+
* - throw if the function explicitly declares `deploy: 'serverless'`
|
|
26
|
+
* - otherwise target is 'server'
|
|
27
|
+
* 2. Explicit `funcMeta.deploy: 'serverless' | 'server'`
|
|
28
|
+
* 3. Default 'serverless'
|
|
29
|
+
*
|
|
30
|
+
* Used both by the per-unit deploy analyzer (when bucketing functions
|
|
31
|
+
* into deployment units) and by `filterInspectorState` (when
|
|
32
|
+
* `pikku all --deploy <target>` is used to emit a target-scoped set
|
|
33
|
+
* of gen files).
|
|
34
|
+
*/
|
|
35
|
+
export function resolveDeployTarget(funcMeta, serverlessIncompatible, functionName = '<unknown>') {
|
|
36
|
+
// Service compatibility wins over the explicit flag — a serverless
|
|
37
|
+
// bundle of a function that needs (e.g.) node:fs would crash at runtime.
|
|
38
|
+
const incompatibleHits = [];
|
|
39
|
+
if (funcMeta.services?.services) {
|
|
40
|
+
for (const svc of funcMeta.services.services) {
|
|
41
|
+
if (serverlessIncompatible.has(svc))
|
|
42
|
+
incompatibleHits.push(svc);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
if (incompatibleHits.length > 0) {
|
|
46
|
+
if (funcMeta.deploy === 'serverless') {
|
|
47
|
+
throw new IncompatibleDeployTargetError(functionName, incompatibleHits);
|
|
48
|
+
}
|
|
49
|
+
return 'server';
|
|
50
|
+
}
|
|
51
|
+
if (funcMeta.deploy === 'server')
|
|
52
|
+
return 'server';
|
|
53
|
+
if (funcMeta.deploy === 'serverless')
|
|
54
|
+
return 'serverless';
|
|
55
|
+
return 'serverless';
|
|
56
|
+
}
|
|
@@ -96,6 +96,7 @@ export function resolveLatestVersions(state, logger) {
|
|
|
96
96
|
}
|
|
97
97
|
}
|
|
98
98
|
function updateWiringReferences(state, oldId, newId) {
|
|
99
|
+
// HTTP routes
|
|
99
100
|
if (state.http) {
|
|
100
101
|
for (const methods of Object.values(state.http.meta)) {
|
|
101
102
|
for (const meta of Object.values(methods)) {
|
|
@@ -105,4 +106,82 @@ function updateWiringReferences(state, oldId, newId) {
|
|
|
105
106
|
}
|
|
106
107
|
}
|
|
107
108
|
}
|
|
109
|
+
// Channels: connect/disconnect/message slots + action-routed message wirings
|
|
110
|
+
if (state.channels) {
|
|
111
|
+
for (const channel of Object.values(state.channels.meta)) {
|
|
112
|
+
for (const slot of [
|
|
113
|
+
channel.connect,
|
|
114
|
+
channel.disconnect,
|
|
115
|
+
channel.message,
|
|
116
|
+
]) {
|
|
117
|
+
if (slot && slot.pikkuFuncId === oldId) {
|
|
118
|
+
slot.pikkuFuncId = newId;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
for (const routes of Object.values(channel.messageWirings)) {
|
|
122
|
+
for (const message of Object.values(routes)) {
|
|
123
|
+
if (message.pikkuFuncId === oldId) {
|
|
124
|
+
message.pikkuFuncId = newId;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
// CLI programs: commands and nested subcommands. This also covers
|
|
131
|
+
// CLI-over-channel generation, which reads command funcIds from this meta.
|
|
132
|
+
if (state.cli) {
|
|
133
|
+
const updateCommands = (commands) => {
|
|
134
|
+
for (const command of Object.values(commands)) {
|
|
135
|
+
if (command.pikkuFuncId === oldId) {
|
|
136
|
+
command.pikkuFuncId = newId;
|
|
137
|
+
}
|
|
138
|
+
if (command.subcommands) {
|
|
139
|
+
updateCommands(command.subcommands);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
};
|
|
143
|
+
for (const program of Object.values(state.cli.meta.programs)) {
|
|
144
|
+
updateCommands(program.commands);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
// Scheduled tasks
|
|
148
|
+
if (state.scheduledTasks) {
|
|
149
|
+
for (const task of Object.values(state.scheduledTasks.meta)) {
|
|
150
|
+
if (task.pikkuFuncId === oldId) {
|
|
151
|
+
task.pikkuFuncId = newId;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
// Queue workers
|
|
156
|
+
if (state.queueWorkers) {
|
|
157
|
+
for (const worker of Object.values(state.queueWorkers.meta)) {
|
|
158
|
+
if (worker.pikkuFuncId === oldId) {
|
|
159
|
+
worker.pikkuFuncId = newId;
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
// Trigger sources (TriggerSourceMeta carries the handler's pikkuFuncId).
|
|
164
|
+
// Gateways/workflows/agents reference functions by bare rpc name and are
|
|
165
|
+
// resolved at runtime via state.rpc.internalMeta, so they need no rewrite here.
|
|
166
|
+
if (state.triggers) {
|
|
167
|
+
for (const source of Object.values(state.triggers.sourceMeta)) {
|
|
168
|
+
if (source.pikkuFuncId === oldId) {
|
|
169
|
+
source.pikkuFuncId = newId;
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
// MCP resources, tools, and prompts
|
|
174
|
+
if (state.mcpEndpoints) {
|
|
175
|
+
for (const collection of [
|
|
176
|
+
state.mcpEndpoints.resourcesMeta,
|
|
177
|
+
state.mcpEndpoints.toolsMeta,
|
|
178
|
+
state.mcpEndpoints.promptsMeta,
|
|
179
|
+
]) {
|
|
180
|
+
for (const endpoint of Object.values(collection)) {
|
|
181
|
+
if (endpoint.pikkuFuncId === oldId) {
|
|
182
|
+
endpoint.pikkuFuncId = newId;
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
}
|
|
108
187
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import * as ts from 'typescript';
|
|
2
2
|
import { dirname, join, resolve } from 'path';
|
|
3
3
|
import { createGenerator, RootlessError } from 'ts-json-schema-generator';
|
|
4
|
-
import { register
|
|
4
|
+
import { register } from 'tsx/esm/api';
|
|
5
5
|
import * as z from 'zod';
|
|
6
6
|
import { zodToTs, createAuxiliaryTypeStore } from 'zod-to-ts';
|
|
7
7
|
import { ErrorCode } from '../error-codes.js';
|
|
@@ -212,7 +212,16 @@ async function batchImportWithRegister(logger, sourceFiles) {
|
|
|
212
212
|
return null;
|
|
213
213
|
}
|
|
214
214
|
finally {
|
|
215
|
-
unregister?.();
|
|
215
|
+
await unregister?.();
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
async function importWithRegister(sourceFile) {
|
|
219
|
+
const unregister = register();
|
|
220
|
+
try {
|
|
221
|
+
return await import(sourceFile);
|
|
222
|
+
}
|
|
223
|
+
finally {
|
|
224
|
+
await unregister();
|
|
216
225
|
}
|
|
217
226
|
}
|
|
218
227
|
function processZodSchema(schemaName, zodSchema, schemas, typesMap, auxiliaryTypeStore, printer, fakeSourceFile, logger) {
|
|
@@ -243,6 +252,7 @@ function processZodSchema(schemaName, zodSchema, schemas, typesMap, auxiliaryTyp
|
|
|
243
252
|
}
|
|
244
253
|
async function generateZodSchemas(logger, schemaLookup, typesMap) {
|
|
245
254
|
const schemas = {};
|
|
255
|
+
const errors = [];
|
|
246
256
|
const auxiliaryTypeStore = createAuxiliaryTypeStore();
|
|
247
257
|
const printer = ts.createPrinter();
|
|
248
258
|
const fakeSourceFile = ts.createSourceFile('zod-types.ts', '', ts.ScriptTarget.ESNext, false, ts.ScriptKind.TS);
|
|
@@ -258,10 +268,10 @@ async function generateZodSchemas(logger, schemaLookup, typesMap) {
|
|
|
258
268
|
const uniqueSourceFiles = [
|
|
259
269
|
...new Set([...schemaLookup.values()].map((ref) => ref.sourceFile)),
|
|
260
270
|
];
|
|
261
|
-
logger.
|
|
271
|
+
logger.debug(`[TIMING] Zod schemas: ${schemaLookup.size} schemas from ${uniqueSourceFiles.length} files`);
|
|
262
272
|
const importStart = performance.now();
|
|
263
273
|
const importedModules = await batchImportWithRegister(logger, uniqueSourceFiles);
|
|
264
|
-
logger.
|
|
274
|
+
logger.debug(`[TIMING] Batch import: ${(performance.now() - importStart).toFixed(0)}ms`);
|
|
265
275
|
const processStart = performance.now();
|
|
266
276
|
// Track schemas that need per-file tsImport fallback
|
|
267
277
|
const fallbackSchemas = [];
|
|
@@ -270,39 +280,48 @@ async function generateZodSchemas(logger, schemaLookup, typesMap) {
|
|
|
270
280
|
if (mod) {
|
|
271
281
|
const zodSchema = mod[ref.variableName];
|
|
272
282
|
if (!zodSchema) {
|
|
273
|
-
|
|
283
|
+
errors.push(`Could not find exported schema '${ref.variableName}' in ${ref.sourceFile} for ${schemaName}. Available exports: ${Object.keys(mod).join(', ')}`);
|
|
274
284
|
continue;
|
|
275
285
|
}
|
|
276
286
|
try {
|
|
277
287
|
processZodSchema(schemaName, zodSchema, schemas, typesMap, auxiliaryTypeStore, printer, fakeSourceFile, logger);
|
|
278
288
|
}
|
|
279
289
|
catch (e) {
|
|
280
|
-
|
|
290
|
+
errors.push(`Could not convert Zod schema '${schemaName}': ${e instanceof Error ? e.message : e}`);
|
|
281
291
|
}
|
|
282
292
|
}
|
|
283
293
|
else {
|
|
284
294
|
fallbackSchemas.push([schemaName, ref]);
|
|
285
295
|
}
|
|
286
296
|
}
|
|
287
|
-
// Fallback: use
|
|
297
|
+
// Fallback: use a scoped tsx register/import cycle for any schemas that
|
|
298
|
+
// batch import couldn't handle. Avoid tsImport() here because its ESM path
|
|
299
|
+
// can leave loader plumbing alive after failed imports, which prevents the
|
|
300
|
+
// CLI process from exiting on schema errors.
|
|
288
301
|
if (fallbackSchemas.length > 0) {
|
|
289
|
-
logger.debug(`Falling back to
|
|
302
|
+
logger.debug(`Falling back to register() import for ${fallbackSchemas.length} schema(s)`);
|
|
290
303
|
for (const [schemaName, ref] of fallbackSchemas) {
|
|
291
304
|
try {
|
|
292
|
-
const module = await
|
|
305
|
+
const module = await importWithRegister(ref.sourceFile);
|
|
293
306
|
const zodSchema = module[ref.variableName];
|
|
294
307
|
if (!zodSchema) {
|
|
295
|
-
|
|
308
|
+
errors.push(`Could not find exported schema '${ref.variableName}' in ${ref.sourceFile} for ${schemaName}. Available exports: ${Object.keys(module).join(', ')}`);
|
|
296
309
|
continue;
|
|
297
310
|
}
|
|
298
311
|
processZodSchema(schemaName, zodSchema, schemas, typesMap, auxiliaryTypeStore, printer, fakeSourceFile, logger);
|
|
299
312
|
}
|
|
300
313
|
catch (e) {
|
|
301
|
-
|
|
314
|
+
errors.push(`Could not convert Zod schema '${schemaName}': ${e instanceof Error ? e.message : e}`);
|
|
302
315
|
}
|
|
303
316
|
}
|
|
304
317
|
}
|
|
305
|
-
|
|
318
|
+
if (errors.length > 0) {
|
|
319
|
+
for (const message of errors) {
|
|
320
|
+
logger.error(message);
|
|
321
|
+
}
|
|
322
|
+
throw new Error(`Schema generation failed for ${errors.length} schema${errors.length === 1 ? '' : 's'}`);
|
|
323
|
+
}
|
|
324
|
+
logger.debug(`[TIMING] Process schemas: ${(performance.now() - processStart).toFixed(0)}ms (${Object.keys(schemas).length} generated)`);
|
|
306
325
|
return schemas;
|
|
307
326
|
}
|
|
308
327
|
export async function generateAllSchemas(logger, config, state) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pikku/inspector",
|
|
3
|
-
"version": "0.12.
|
|
3
|
+
"version": "0.12.13",
|
|
4
4
|
"author": "yasser.fadl@gmail.com",
|
|
5
5
|
"license": "BUSL-1.1",
|
|
6
6
|
"type": "module",
|
|
@@ -35,7 +35,7 @@
|
|
|
35
35
|
},
|
|
36
36
|
"dependencies": {
|
|
37
37
|
"@openapi-contrib/json-schema-to-openapi-schema": "^4.3.1",
|
|
38
|
-
"@pikku/core": "^0.12.
|
|
38
|
+
"@pikku/core": "^0.12.25",
|
|
39
39
|
"path-to-regexp": "^8.3.0",
|
|
40
40
|
"ts-json-schema-generator": "^2.5.0",
|
|
41
41
|
"tsx": "^4.21.0",
|
package/src/add/add-cli.ts
CHANGED
|
@@ -479,10 +479,16 @@ function processCommand(
|
|
|
479
479
|
}
|
|
480
480
|
break
|
|
481
481
|
|
|
482
|
-
case 'subcommands':
|
|
483
|
-
|
|
482
|
+
case 'subcommands': {
|
|
483
|
+
let subcommandsNode: ts.Node | undefined = prop.initializer
|
|
484
|
+
if (ts.isIdentifier(prop.initializer)) {
|
|
485
|
+
subcommandsNode = resolveIdentifier(prop.initializer, typeChecker, [
|
|
486
|
+
'defineCLICommands',
|
|
487
|
+
])
|
|
488
|
+
}
|
|
489
|
+
if (subcommandsNode && ts.isObjectLiteralExpression(subcommandsNode)) {
|
|
484
490
|
meta.subcommands = {}
|
|
485
|
-
for (const subProp of
|
|
491
|
+
for (const subProp of subcommandsNode.properties) {
|
|
486
492
|
if (!ts.isPropertyAssignment(subProp)) continue
|
|
487
493
|
|
|
488
494
|
const subName = getPropertyName(subProp)
|
|
@@ -507,6 +513,7 @@ function processCommand(
|
|
|
507
513
|
}
|
|
508
514
|
}
|
|
509
515
|
break
|
|
516
|
+
}
|
|
510
517
|
|
|
511
518
|
case 'isDefault':
|
|
512
519
|
if (
|
|
@@ -2,6 +2,7 @@ import * as ts from 'typescript'
|
|
|
2
2
|
import {
|
|
3
3
|
getPropertyValue,
|
|
4
4
|
getArrayPropertyValue,
|
|
5
|
+
assertStringLiteralProperty,
|
|
5
6
|
} from '../utils/get-property-value.js'
|
|
6
7
|
import type { AddWiring } from '../types.js'
|
|
7
8
|
import { ErrorCode } from '../error-codes.js'
|
|
@@ -33,6 +34,8 @@ export const addCredential: AddWiring = (
|
|
|
33
34
|
if (ts.isObjectLiteralExpression(firstArg)) {
|
|
34
35
|
const obj = firstArg
|
|
35
36
|
|
|
37
|
+
assertStringLiteralProperty(obj, 'name', 'Credential', logger)
|
|
38
|
+
|
|
36
39
|
const nameValue = getPropertyValue(obj, 'name') as string | null
|
|
37
40
|
const displayNameValue = getPropertyValue(obj, 'displayName') as
|
|
38
41
|
| string
|
|
@@ -111,6 +111,64 @@ describe('addFunctions duplicate name handling', () => {
|
|
|
111
111
|
}
|
|
112
112
|
})
|
|
113
113
|
|
|
114
|
+
test('strips VN suffix so createUserV1 + version:1 groups with createUser as createUser@v1', async () => {
|
|
115
|
+
const rootDir = await mkdtemp(join(tmpdir(), 'pikku-vn-suffix-'))
|
|
116
|
+
const fileV1 = join(rootDir, 'create-user-v1.ts')
|
|
117
|
+
const fileLatest = join(rootDir, 'create-user.ts')
|
|
118
|
+
|
|
119
|
+
await writeFile(
|
|
120
|
+
fileV1,
|
|
121
|
+
[
|
|
122
|
+
"import { pikkuFunc } from '@pikku/core'",
|
|
123
|
+
'export const createUserV1 = pikkuFunc({',
|
|
124
|
+
' version: 1,',
|
|
125
|
+
' func: async () => ({ ok: true })',
|
|
126
|
+
'})',
|
|
127
|
+
].join('\n')
|
|
128
|
+
)
|
|
129
|
+
|
|
130
|
+
await writeFile(
|
|
131
|
+
fileLatest,
|
|
132
|
+
[
|
|
133
|
+
"import { pikkuFunc } from '@pikku/core'",
|
|
134
|
+
'export const createUser = pikkuFunc({',
|
|
135
|
+
' func: async () => ({ ok: true })',
|
|
136
|
+
'})',
|
|
137
|
+
].join('\n')
|
|
138
|
+
)
|
|
139
|
+
|
|
140
|
+
const logger: InspectorLogger = {
|
|
141
|
+
debug: () => {},
|
|
142
|
+
info: () => {},
|
|
143
|
+
warn: () => {},
|
|
144
|
+
error: () => {},
|
|
145
|
+
critical: () => {},
|
|
146
|
+
hasCriticalErrors: () => false,
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
try {
|
|
150
|
+
const state = await inspect(logger, [fileV1, fileLatest], { rootDir })
|
|
151
|
+
// V1 suffix stripped: createUserV1 + version:1 → createUser@v1
|
|
152
|
+
assert.ok(
|
|
153
|
+
state.functions.meta['createUser@v1'],
|
|
154
|
+
'createUser@v1 should exist'
|
|
155
|
+
)
|
|
156
|
+
assert.strictEqual(state.functions.meta['createUser@v1']!.version, 1)
|
|
157
|
+
// Unversioned createUser auto-promoted to createUser@v2
|
|
158
|
+
assert.ok(
|
|
159
|
+
state.functions.meta['createUser@v2'],
|
|
160
|
+
'createUser@v2 should exist'
|
|
161
|
+
)
|
|
162
|
+
assert.strictEqual(state.functions.meta['createUser@v2']!.version, 2)
|
|
163
|
+
// No stale createUserV1@v1 entry
|
|
164
|
+
assert.strictEqual(state.functions.meta['createUserV1@v1'], undefined)
|
|
165
|
+
// Latest alias points to v2
|
|
166
|
+
assert.strictEqual(state.rpc.internalMeta['createUser'], 'createUser@v2')
|
|
167
|
+
} finally {
|
|
168
|
+
await rm(rootDir, { recursive: true, force: true })
|
|
169
|
+
}
|
|
170
|
+
})
|
|
171
|
+
|
|
114
172
|
test('logs a critical error when exposed function name is duplicated across files', async () => {
|
|
115
173
|
const rootDir = await mkdtemp(
|
|
116
174
|
join(tmpdir(), 'pikku-exposed-duplicate-function-')
|
|
@@ -167,3 +225,94 @@ describe('addFunctions duplicate name handling', () => {
|
|
|
167
225
|
}
|
|
168
226
|
})
|
|
169
227
|
})
|
|
228
|
+
|
|
229
|
+
describe('addFunctions implementationHash', () => {
|
|
230
|
+
test('records a stable implementation hash for an inline function', async () => {
|
|
231
|
+
const rootDir = await mkdtemp(join(tmpdir(), 'pikku-function-hash-inline-'))
|
|
232
|
+
const file = join(rootDir, 'inline.ts')
|
|
233
|
+
|
|
234
|
+
await writeFile(
|
|
235
|
+
file,
|
|
236
|
+
[
|
|
237
|
+
"import { pikkuFunc } from '@pikku/core'",
|
|
238
|
+
'export const createUser = pikkuFunc({',
|
|
239
|
+
' expose: true,',
|
|
240
|
+
' func: async ({ logger }, input: { name: string }) => {',
|
|
241
|
+
" logger.info('create user')",
|
|
242
|
+
' return { ok: true, name: input.name }',
|
|
243
|
+
' }',
|
|
244
|
+
'})',
|
|
245
|
+
].join('\n')
|
|
246
|
+
)
|
|
247
|
+
|
|
248
|
+
const logger: InspectorLogger = {
|
|
249
|
+
debug: () => {},
|
|
250
|
+
info: () => {},
|
|
251
|
+
warn: () => {},
|
|
252
|
+
error: () => {},
|
|
253
|
+
critical: () => {},
|
|
254
|
+
hasCriticalErrors: () => false,
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
try {
|
|
258
|
+
const first = await inspect(logger, [file], { rootDir })
|
|
259
|
+
const second = await inspect(logger, [file], { rootDir })
|
|
260
|
+
const firstHash = first.functions.meta['createUser']?.implementationHash
|
|
261
|
+
const secondHash = second.functions.meta['createUser']?.implementationHash
|
|
262
|
+
|
|
263
|
+
assert.match(firstHash ?? '', /^[0-9a-f]{16}$/)
|
|
264
|
+
assert.strictEqual(firstHash, secondHash)
|
|
265
|
+
} finally {
|
|
266
|
+
await rm(rootDir, { recursive: true, force: true })
|
|
267
|
+
}
|
|
268
|
+
})
|
|
269
|
+
|
|
270
|
+
test('changes when a referenced handler implementation changes', async () => {
|
|
271
|
+
const rootDir = await mkdtemp(
|
|
272
|
+
join(tmpdir(), 'pikku-function-hash-referenced-')
|
|
273
|
+
)
|
|
274
|
+
const file = join(rootDir, 'referenced.ts')
|
|
275
|
+
|
|
276
|
+
const writeSource = async (bodyLine: string) => {
|
|
277
|
+
await writeFile(
|
|
278
|
+
file,
|
|
279
|
+
[
|
|
280
|
+
"import { pikkuFunc } from '@pikku/core'",
|
|
281
|
+
'',
|
|
282
|
+
'const handler = async () => {',
|
|
283
|
+
` ${bodyLine}`,
|
|
284
|
+
' return { ok: true }',
|
|
285
|
+
'}',
|
|
286
|
+
'',
|
|
287
|
+
'export const createUser = pikkuFunc({',
|
|
288
|
+
' func: handler,',
|
|
289
|
+
'})',
|
|
290
|
+
].join('\n')
|
|
291
|
+
)
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
const logger: InspectorLogger = {
|
|
295
|
+
debug: () => {},
|
|
296
|
+
info: () => {},
|
|
297
|
+
warn: () => {},
|
|
298
|
+
error: () => {},
|
|
299
|
+
critical: () => {},
|
|
300
|
+
hasCriticalErrors: () => false,
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
try {
|
|
304
|
+
await writeSource("console.log('first')")
|
|
305
|
+
const first = await inspect(logger, [file], { rootDir })
|
|
306
|
+
|
|
307
|
+
await writeSource("console.log('second')")
|
|
308
|
+
const second = await inspect(logger, [file], { rootDir })
|
|
309
|
+
|
|
310
|
+
assert.notStrictEqual(
|
|
311
|
+
first.functions.meta['createUser']?.implementationHash,
|
|
312
|
+
second.functions.meta['createUser']?.implementationHash
|
|
313
|
+
)
|
|
314
|
+
} finally {
|
|
315
|
+
await rm(rootDir, { recursive: true, force: true })
|
|
316
|
+
}
|
|
317
|
+
})
|
|
318
|
+
})
|