@pikku/inspector 0.12.10 → 0.12.11
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 +8 -9
- package/dist/add/add-functions.js +71 -4
- package/dist/add/add-http-route.js +20 -1
- package/dist/error-codes.d.ts +1 -0
- package/dist/error-codes.js +1 -0
- package/dist/utils/schema-generator.js +3 -3
- package/dist/utils/validate-auth-sessionless.d.ts +1 -1
- package/package.json +2 -2
- package/src/add/add-functions.test.ts +169 -0
- package/src/add/add-functions.ts +127 -5
- package/src/add/add-http-route.ts +28 -1
- package/src/error-codes.ts +1 -0
- package/src/utils/schema-generator.ts +3 -3
- package/src/utils/validate-auth-sessionless.ts +1 -1
- package/tsconfig.tsbuildinfo +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,11 @@
|
|
|
1
|
+
## 0.12.11
|
|
2
|
+
|
|
3
|
+
### Patch Changes
|
|
4
|
+
|
|
5
|
+
- 033d172: Log a critical inspector error when multiple functions resolve to the same `pikku` function name, instead of silently allowing routing map collisions. This may cause builds to fail if multiple functions previously resolved to the same `pikku` function name.
|
|
6
|
+
- Updated dependencies [b9ed73e]
|
|
7
|
+
- @pikku/core@0.12.19
|
|
8
|
+
|
|
1
9
|
## 0.12.0
|
|
2
10
|
|
|
3
11
|
## 0.12.10
|
|
@@ -32,7 +40,6 @@
|
|
|
32
40
|
### Patch Changes
|
|
33
41
|
|
|
34
42
|
- 624097e: Add deploy pipeline with provider-agnostic architecture
|
|
35
|
-
|
|
36
43
|
- Add MetaService with explicit typed API, absorb WiringService reads
|
|
37
44
|
- Add deployment service, traceId propagation, scoped logger
|
|
38
45
|
- Rewrite analyzer: one function = one worker, gateways dispatch via RPC
|
|
@@ -79,7 +86,6 @@
|
|
|
79
86
|
|
|
80
87
|
- 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.
|
|
81
88
|
- e412b4d: Optimize CLI codegen performance: 12x faster `pikku all`
|
|
82
|
-
|
|
83
89
|
- Reuse schemas across re-inspections (skip redundant `ts-json-schema-generator` runs)
|
|
84
90
|
- Cache TS schemas to disk (`.pikku/schema-cache.json`) for cross-run reuse
|
|
85
91
|
- Pass `oldProgram` to `ts.createProgram` for incremental TS compilation
|
|
@@ -208,14 +214,12 @@
|
|
|
208
214
|
- 1967172: Update code generation to support channel middleware enhancements
|
|
209
215
|
|
|
210
216
|
**Code Generation Updates:**
|
|
211
|
-
|
|
212
217
|
- Update channel type serialization to include middleware support
|
|
213
218
|
- Improve WebSocket wrapper generation for middleware handling
|
|
214
219
|
- Update CLI channel client generation with better type support
|
|
215
220
|
- Enhance services and schema generation for channel configurations
|
|
216
221
|
|
|
217
222
|
**Inspector Updates:**
|
|
218
|
-
|
|
219
223
|
- Improve channel metadata extraction for middleware
|
|
220
224
|
- Better type analysis for channel lifecycle functions
|
|
221
225
|
- Enhanced post-processing for channel configurations
|
|
@@ -223,19 +227,16 @@
|
|
|
223
227
|
- 753481a: Add bootstrap command, performance optimizations, and CLI improvements
|
|
224
228
|
|
|
225
229
|
**New Features:**
|
|
226
|
-
|
|
227
230
|
- Add `pikku bootstrap` command for type-only generation (~13.5% faster than `pikku all`)
|
|
228
231
|
- Add configurable `ignoreFiles` option to pikku.config.json with sensible defaults (_.gen.ts, _.test.ts, \*.spec.ts)
|
|
229
232
|
- Export pikkuCLIRender helper from serialize-cli-types.ts with JSDoc documentation
|
|
230
233
|
|
|
231
234
|
**Performance Improvements:**
|
|
232
|
-
|
|
233
235
|
- Add aggressive TypeScript compiler options (skipDefaultLibCheck, types: []) - ~37% faster TypeScript setup
|
|
234
236
|
- Add detailed performance timing to inspector phases (--logLevel=debug)
|
|
235
237
|
- Optimize file inspection with ignore patterns - ~10-20% faster overall
|
|
236
238
|
|
|
237
239
|
**Enhancements:**
|
|
238
|
-
|
|
239
240
|
- Fix --logLevel flag to properly apply log level to logger
|
|
240
241
|
- Update middleware logging to use structured log format
|
|
241
242
|
- Improve CLI renderers to consistently use destructured logger service
|
|
@@ -336,7 +337,6 @@ For complete details, see https://pikku.dev/changelogs/0_10_0.md
|
|
|
336
337
|
### Patch Changes
|
|
337
338
|
|
|
338
339
|
- 44e3ff4: feat: enhance CLI filtering with type and directory filters
|
|
339
|
-
|
|
340
340
|
- Add --types filter to filter by PikkuEventTypes (http, channel, queue, scheduler, rpc, mcp)
|
|
341
341
|
- Add --directories filter to filter by file paths/directories
|
|
342
342
|
- All filters (tags, types, directories) now work together with AND logic
|
|
@@ -347,7 +347,6 @@ For complete details, see https://pikku.dev/changelogs/0_10_0.md
|
|
|
347
347
|
- 7c592b8: feat: support for required services and improved service configuration
|
|
348
348
|
|
|
349
349
|
This release includes several enhancements to service management and configuration:
|
|
350
|
-
|
|
351
350
|
- Added support for required services configuration
|
|
352
351
|
- Improved service discovery and registration
|
|
353
352
|
- Added typed RPC clients for service communication
|
|
@@ -3,7 +3,7 @@ import { detectSchemaVendorOrError } from '../utils/detect-schema-vendor.js';
|
|
|
3
3
|
import { extractFunctionName, funcIdToTypeName, } from '../utils/extract-function-name.js';
|
|
4
4
|
import { extractFunctionNode } from '../utils/extract-function-node.js';
|
|
5
5
|
import { extractUsedWires } from '../utils/extract-services.js';
|
|
6
|
-
import { formatVersionedId } from '@pikku/core';
|
|
6
|
+
import { formatVersionedId, parseVersionedId } from '@pikku/core';
|
|
7
7
|
import { getPropertyValue, getCommonWireMetaData, } from '../utils/get-property-value.js';
|
|
8
8
|
import { resolveMiddleware } from '../utils/middleware.js';
|
|
9
9
|
import { resolvePermissions } from '../utils/permissions.js';
|
|
@@ -209,6 +209,19 @@ function unwrapPromise(checker, type) {
|
|
|
209
209
|
}
|
|
210
210
|
return type;
|
|
211
211
|
}
|
|
212
|
+
const resolveExistingFunctionSource = (state, pikkuFuncId) => {
|
|
213
|
+
return (state.functions.meta[pikkuFuncId]?.sourceFile ||
|
|
214
|
+
state.rpc.internalFiles.get(pikkuFuncId)?.path ||
|
|
215
|
+
null);
|
|
216
|
+
};
|
|
217
|
+
const areCompatibleFunctionIds = (existingId, incomingId) => {
|
|
218
|
+
if (existingId === incomingId) {
|
|
219
|
+
return true;
|
|
220
|
+
}
|
|
221
|
+
const existingParsed = parseVersionedId(existingId);
|
|
222
|
+
const incomingParsed = parseVersionedId(incomingId);
|
|
223
|
+
return existingParsed.baseName === incomingParsed.baseName;
|
|
224
|
+
};
|
|
212
225
|
/**
|
|
213
226
|
* Inspect pikkuFunc calls, extract input/output and first-arg destructuring,
|
|
214
227
|
* then push into state.functions.meta.
|
|
@@ -400,6 +413,7 @@ export const addFunctions = (logger, node, checker, state, options) => {
|
|
|
400
413
|
pikkuFuncId = formatVersionedId(baseName, version);
|
|
401
414
|
}
|
|
402
415
|
const isMCPToolFunc = expression.text === 'pikkuMCPToolFunc';
|
|
416
|
+
const isListFunc = expression.text === 'pikkuListFunc';
|
|
403
417
|
const mcpEnabled = mcp || isMCPToolFunc;
|
|
404
418
|
// Pick the handler: use resolvedFunc when it exists and is a function, otherwise fall back to handlerNode
|
|
405
419
|
const handler = resolvedFunc &&
|
|
@@ -461,7 +475,22 @@ export const addFunctions = (logger, node, checker, state, options) => {
|
|
|
461
475
|
state.schemaLookup.set(schemaName, inputSchemaRef);
|
|
462
476
|
state.functions.typesMap.addCustomType(schemaName, 'unknown', []);
|
|
463
477
|
}
|
|
464
|
-
else if (genericTypes.length >= 1 && genericTypes[0]) {
|
|
478
|
+
else if (isListFunc && genericTypes.length >= 1 && genericTypes[0]) {
|
|
479
|
+
const inputAliasName = `${capitalizedName}Input`;
|
|
480
|
+
const filterType = genericTypes[0];
|
|
481
|
+
const filterTypeText = checker.typeToString(filterType, undefined, ts.TypeFormatFlags.NoTruncation);
|
|
482
|
+
const refs = resolveTypeImports(filterType, state.functions.typesMap, true, checker);
|
|
483
|
+
state.functions.typesMap.addCustomType(inputAliasName, `{ cursor?: string; limit?: number; sort?: Array<{ column: string; direction: 'asc' | 'desc' }>; filter?: unknown; search?: string; } & ${filterTypeText}`, [...new Set(refs)]);
|
|
484
|
+
inputNames = [inputAliasName];
|
|
485
|
+
const secondParam = handler.parameters[1];
|
|
486
|
+
if (secondParam) {
|
|
487
|
+
inputTypes = [checker.getTypeAtLocation(secondParam)];
|
|
488
|
+
}
|
|
489
|
+
else {
|
|
490
|
+
inputTypes = [filterType];
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
else if (!isListFunc && genericTypes.length >= 1 && genericTypes[0]) {
|
|
465
494
|
// Fall back to extracting from generic type arguments
|
|
466
495
|
const result = getNamesAndTypes(checker, state.functions.typesMap, 'Input', name, genericTypes[0]);
|
|
467
496
|
inputNames = result.names;
|
|
@@ -485,7 +514,15 @@ export const addFunctions = (logger, node, checker, state, options) => {
|
|
|
485
514
|
state.schemaLookup.set(schemaName, outputSchemaRef);
|
|
486
515
|
state.functions.typesMap.addCustomType(schemaName, 'unknown', []);
|
|
487
516
|
}
|
|
488
|
-
else if (genericTypes.length >= 2) {
|
|
517
|
+
else if (isListFunc && genericTypes.length >= 2 && genericTypes[1]) {
|
|
518
|
+
const outputAliasName = `${capitalizedName}Output`;
|
|
519
|
+
const rowType = genericTypes[1];
|
|
520
|
+
const rowTypeText = checker.typeToString(rowType, undefined, ts.TypeFormatFlags.NoTruncation);
|
|
521
|
+
const refs = resolveTypeImports(rowType, state.functions.typesMap, true, checker);
|
|
522
|
+
state.functions.typesMap.addCustomType(outputAliasName, `{ rows: Array<${rowTypeText}>; nextCursor: string | null; totalCount?: number; }`, [...new Set(refs)]);
|
|
523
|
+
outputNames = [outputAliasName];
|
|
524
|
+
}
|
|
525
|
+
else if (!isListFunc && genericTypes.length >= 2) {
|
|
489
526
|
outputNames = getNamesAndTypes(checker, state.functions.typesMap, 'Output', name, genericTypes[1]).names;
|
|
490
527
|
}
|
|
491
528
|
else {
|
|
@@ -539,6 +576,36 @@ export const addFunctions = (logger, node, checker, state, options) => {
|
|
|
539
576
|
if (inputTypes.length > 0) {
|
|
540
577
|
state.typesLookup.set(pikkuFuncId, inputTypes);
|
|
541
578
|
}
|
|
579
|
+
const sourceFile = node.getSourceFile().fileName;
|
|
580
|
+
const existingFunction = state.functions.meta[pikkuFuncId];
|
|
581
|
+
if (existingFunction &&
|
|
582
|
+
existingFunction.sourceFile &&
|
|
583
|
+
existingFunction.sourceFile !== sourceFile) {
|
|
584
|
+
logger.critical(ErrorCode.DUPLICATE_FUNCTION_NAME, `Function name '${name}' is not unique. ` +
|
|
585
|
+
`'${pikkuFuncId}' is already defined in '${existingFunction.sourceFile}' and cannot be redefined in '${sourceFile}'.`);
|
|
586
|
+
return;
|
|
587
|
+
}
|
|
588
|
+
if (exportedName || explicitName) {
|
|
589
|
+
const existingInternal = state.rpc.internalMeta[name];
|
|
590
|
+
if (existingInternal &&
|
|
591
|
+
!areCompatibleFunctionIds(existingInternal, pikkuFuncId)) {
|
|
592
|
+
const existingSource = resolveExistingFunctionSource(state, existingInternal) || 'unknown file';
|
|
593
|
+
logger.critical(ErrorCode.DUPLICATE_FUNCTION_NAME, `Function name '${name}' is not unique. ` +
|
|
594
|
+
`It already points to '${existingInternal}' in '${existingSource}', but '${pikkuFuncId}' in '${sourceFile}' tried to use the same name.`);
|
|
595
|
+
return;
|
|
596
|
+
}
|
|
597
|
+
if (expose) {
|
|
598
|
+
const existingExposed = state.rpc.exposedMeta[name];
|
|
599
|
+
if (existingExposed &&
|
|
600
|
+
!areCompatibleFunctionIds(existingExposed, pikkuFuncId)) {
|
|
601
|
+
const existingSource = resolveExistingFunctionSource(state, existingExposed) ||
|
|
602
|
+
'unknown file';
|
|
603
|
+
logger.critical(ErrorCode.DUPLICATE_FUNCTION_NAME, `Exposed function name '${name}' is not unique. ` +
|
|
604
|
+
`It already points to '${existingExposed}' in '${existingSource}', but '${pikkuFuncId}' in '${sourceFile}' tried to use the same name.`);
|
|
605
|
+
return;
|
|
606
|
+
}
|
|
607
|
+
}
|
|
608
|
+
}
|
|
542
609
|
// --- resolve middleware ---
|
|
543
610
|
let middleware = objectNode
|
|
544
611
|
? resolveMiddleware(state, objectNode, tags, checker)
|
|
@@ -597,7 +664,7 @@ export const addFunctions = (logger, node, checker, state, options) => {
|
|
|
597
664
|
middleware,
|
|
598
665
|
permissions,
|
|
599
666
|
isDirectFunction,
|
|
600
|
-
sourceFile
|
|
667
|
+
sourceFile,
|
|
601
668
|
exportedName: exportedName || undefined,
|
|
602
669
|
};
|
|
603
670
|
// Populate node metadata if node config is present
|
|
@@ -7,6 +7,7 @@ import { resolveHTTPMiddlewareFromObject } from '../utils/middleware.js';
|
|
|
7
7
|
import { resolveHTTPPermissionsFromObject } from '../utils/permissions.js';
|
|
8
8
|
import { extractWireNames } from '../utils/post-process.js';
|
|
9
9
|
import { ensureFunctionMetadata } from '../utils/ensure-function-metadata.js';
|
|
10
|
+
import { resolveFunctionMeta } from '../utils/resolve-function-meta.js';
|
|
10
11
|
import { ErrorCode } from '../error-codes.js';
|
|
11
12
|
import { validateAuthSessionless } from '../utils/validate-auth-sessionless.js';
|
|
12
13
|
import { detectSchemaVendorOrError } from '../utils/detect-schema-vendor.js';
|
|
@@ -113,12 +114,30 @@ export function registerHTTPRoute({ obj, state, checker, logger, sourceFile, bas
|
|
|
113
114
|
if (funcName.startsWith('__temp_')) {
|
|
114
115
|
funcName = makeContextBasedId('http', method, fullRoute);
|
|
115
116
|
}
|
|
117
|
+
let refAddonTarget = null;
|
|
118
|
+
if (ts.isCallExpression(funcInitializer) &&
|
|
119
|
+
ts.isIdentifier(funcInitializer.expression) &&
|
|
120
|
+
funcInitializer.expression.text === 'ref') {
|
|
121
|
+
const [firstArg] = funcInitializer.arguments;
|
|
122
|
+
if (firstArg &&
|
|
123
|
+
ts.isStringLiteral(firstArg) &&
|
|
124
|
+
firstArg.text.includes(':')) {
|
|
125
|
+
refAddonTarget = firstArg.text;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
116
128
|
const packageName = ts.isIdentifier(funcInitializer)
|
|
117
129
|
? resolveAddonName(funcInitializer, checker, state.rpc.wireAddonDeclarations)
|
|
118
130
|
: null;
|
|
131
|
+
if (refAddonTarget) {
|
|
132
|
+
const targetMeta = resolveFunctionMeta(state, refAddonTarget);
|
|
133
|
+
if (!targetMeta) {
|
|
134
|
+
logger.warn(`Skipping route '${fullRoute}': addon function metadata for '${refAddonTarget}' is not available yet.`);
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
119
138
|
ensureFunctionMetadata(state, funcName, fullRoute, funcInitializer, checker, extracted.isHelper);
|
|
120
139
|
// Lookup existing function metadata
|
|
121
|
-
const fnMeta = state
|
|
140
|
+
const fnMeta = resolveFunctionMeta(state, funcName);
|
|
122
141
|
if (!fnMeta) {
|
|
123
142
|
logger.critical(ErrorCode.FUNCTION_METADATA_NOT_FOUND, `No function metadata found for '${funcName}'.`);
|
|
124
143
|
return;
|
package/dist/error-codes.d.ts
CHANGED
|
@@ -41,6 +41,7 @@ export declare enum ErrorCode {
|
|
|
41
41
|
PERMISSION_EMPTY_ARRAY = "PKU937",
|
|
42
42
|
PERMISSION_PATTERN_INVALID = "PKU975",
|
|
43
43
|
DUPLICATE_FUNCTION_VERSION = "PKU850",
|
|
44
|
+
DUPLICATE_FUNCTION_NAME = "PKU851",
|
|
44
45
|
MANIFEST_MISSING = "PKU860",
|
|
45
46
|
FUNCTION_VERSION_MODIFIED = "PKU861",
|
|
46
47
|
CONTRACT_CHANGED_REQUIRES_BUMP = "PKU862",
|
package/dist/error-codes.js
CHANGED
|
@@ -48,6 +48,7 @@ export var ErrorCode;
|
|
|
48
48
|
ErrorCode["PERMISSION_PATTERN_INVALID"] = "PKU975";
|
|
49
49
|
// Versioning errors
|
|
50
50
|
ErrorCode["DUPLICATE_FUNCTION_VERSION"] = "PKU850";
|
|
51
|
+
ErrorCode["DUPLICATE_FUNCTION_NAME"] = "PKU851";
|
|
51
52
|
// Contract versioning errors
|
|
52
53
|
ErrorCode["MANIFEST_MISSING"] = "PKU860";
|
|
53
54
|
ErrorCode["FUNCTION_VERSION_MODIFIED"] = "PKU861";
|
|
@@ -258,10 +258,10 @@ async function generateZodSchemas(logger, schemaLookup, typesMap) {
|
|
|
258
258
|
const uniqueSourceFiles = [
|
|
259
259
|
...new Set([...schemaLookup.values()].map((ref) => ref.sourceFile)),
|
|
260
260
|
];
|
|
261
|
-
|
|
261
|
+
logger.info(`[TIMING] Zod schemas: ${schemaLookup.size} schemas from ${uniqueSourceFiles.length} files`);
|
|
262
262
|
const importStart = performance.now();
|
|
263
263
|
const importedModules = await batchImportWithRegister(logger, uniqueSourceFiles);
|
|
264
|
-
|
|
264
|
+
logger.info(`[TIMING] Batch import: ${(performance.now() - importStart).toFixed(0)}ms`);
|
|
265
265
|
const processStart = performance.now();
|
|
266
266
|
// Track schemas that need per-file tsImport fallback
|
|
267
267
|
const fallbackSchemas = [];
|
|
@@ -302,7 +302,7 @@ async function generateZodSchemas(logger, schemaLookup, typesMap) {
|
|
|
302
302
|
}
|
|
303
303
|
}
|
|
304
304
|
}
|
|
305
|
-
|
|
305
|
+
logger.info(`[TIMING] Process schemas: ${(performance.now() - processStart).toFixed(0)}ms (${Object.keys(schemas).length} generated)`);
|
|
306
306
|
return schemas;
|
|
307
307
|
}
|
|
308
308
|
export async function generateAllSchemas(logger, config, state) {
|
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
import * as ts from 'typescript';
|
|
1
|
+
import type * as ts from 'typescript';
|
|
2
2
|
import type { InspectorLogger, InspectorState } from '../types.js';
|
|
3
3
|
export declare function validateAuthSessionless(logger: InspectorLogger, obj: ts.ObjectLiteralExpression, state: InspectorState, funcName: string, wireDescription: string, inheritedAuth?: boolean): boolean;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pikku/inspector",
|
|
3
|
-
"version": "0.12.
|
|
3
|
+
"version": "0.12.11",
|
|
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.19",
|
|
39
39
|
"path-to-regexp": "^8.3.0",
|
|
40
40
|
"ts-json-schema-generator": "^2.5.0",
|
|
41
41
|
"tsx": "^4.21.0",
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
import { strict as assert } from 'assert'
|
|
2
|
+
import { describe, test } from 'node:test'
|
|
3
|
+
import { mkdtemp, rm, writeFile } from 'node:fs/promises'
|
|
4
|
+
import { tmpdir } from 'node:os'
|
|
5
|
+
import { join } from 'node:path'
|
|
6
|
+
import { inspect } from '../inspector.js'
|
|
7
|
+
import { ErrorCode } from '../error-codes.js'
|
|
8
|
+
import type { InspectorLogger } from '../types.js'
|
|
9
|
+
|
|
10
|
+
describe('addFunctions duplicate name handling', () => {
|
|
11
|
+
test('logs a critical error when function name is duplicated across files', async () => {
|
|
12
|
+
const rootDir = await mkdtemp(join(tmpdir(), 'pikku-duplicate-function-'))
|
|
13
|
+
const fileA = join(rootDir, 'a.ts')
|
|
14
|
+
const fileB = join(rootDir, 'b.ts')
|
|
15
|
+
|
|
16
|
+
await writeFile(
|
|
17
|
+
fileA,
|
|
18
|
+
[
|
|
19
|
+
"import { pikkuFunc } from '@pikku/core'",
|
|
20
|
+
'export const createUser = pikkuFunc({',
|
|
21
|
+
' func: async () => ({ ok: true })',
|
|
22
|
+
'})',
|
|
23
|
+
].join('\n')
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
await writeFile(
|
|
27
|
+
fileB,
|
|
28
|
+
[
|
|
29
|
+
"import { pikkuFunc } from '@pikku/core'",
|
|
30
|
+
'export const createUser = pikkuFunc({',
|
|
31
|
+
' func: async () => ({ ok: true })',
|
|
32
|
+
'})',
|
|
33
|
+
].join('\n')
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
const criticals: Array<{ code: ErrorCode; message: string }> = []
|
|
37
|
+
const logger: InspectorLogger = {
|
|
38
|
+
debug: () => {},
|
|
39
|
+
info: () => {},
|
|
40
|
+
warn: () => {},
|
|
41
|
+
error: () => {},
|
|
42
|
+
critical: (code: ErrorCode, message: string) => {
|
|
43
|
+
criticals.push({ code, message })
|
|
44
|
+
},
|
|
45
|
+
hasCriticalErrors: () => criticals.length > 0,
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
try {
|
|
49
|
+
const state = await inspect(logger, [fileA, fileB], { rootDir })
|
|
50
|
+
const nameCollision = criticals.find(
|
|
51
|
+
(entry) => entry.code === ErrorCode.DUPLICATE_FUNCTION_NAME
|
|
52
|
+
)
|
|
53
|
+
assert.ok(nameCollision)
|
|
54
|
+
assert.match(nameCollision!.message, /createUser/)
|
|
55
|
+
assert.strictEqual(state.rpc.internalMeta['createUser'], 'createUser')
|
|
56
|
+
} finally {
|
|
57
|
+
await rm(rootDir, { recursive: true, force: true })
|
|
58
|
+
}
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
test('allows same base function name across files when versions differ', async () => {
|
|
62
|
+
const rootDir = await mkdtemp(join(tmpdir(), 'pikku-versioned-function-'))
|
|
63
|
+
const fileA = join(rootDir, 'a.ts')
|
|
64
|
+
const fileB = join(rootDir, 'b.ts')
|
|
65
|
+
|
|
66
|
+
await writeFile(
|
|
67
|
+
fileA,
|
|
68
|
+
[
|
|
69
|
+
"import { pikkuFunc } from '@pikku/core'",
|
|
70
|
+
'export const createUser = pikkuFunc({',
|
|
71
|
+
' version: 1,',
|
|
72
|
+
' func: async () => ({ ok: true })',
|
|
73
|
+
'})',
|
|
74
|
+
].join('\n')
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
await writeFile(
|
|
78
|
+
fileB,
|
|
79
|
+
[
|
|
80
|
+
"import { pikkuFunc } from '@pikku/core'",
|
|
81
|
+
'export const createUser = pikkuFunc({',
|
|
82
|
+
' version: 2,',
|
|
83
|
+
' func: async () => ({ ok: true })',
|
|
84
|
+
'})',
|
|
85
|
+
].join('\n')
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
const criticals: Array<{ code: ErrorCode; message: string }> = []
|
|
89
|
+
const logger: InspectorLogger = {
|
|
90
|
+
debug: () => {},
|
|
91
|
+
info: () => {},
|
|
92
|
+
warn: () => {},
|
|
93
|
+
error: () => {},
|
|
94
|
+
critical: (code: ErrorCode, message: string) => {
|
|
95
|
+
criticals.push({ code, message })
|
|
96
|
+
},
|
|
97
|
+
hasCriticalErrors: () => criticals.length > 0,
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
try {
|
|
101
|
+
const state = await inspect(logger, [fileA, fileB], { rootDir })
|
|
102
|
+
const nameCollision = criticals.find(
|
|
103
|
+
(entry) => entry.code === ErrorCode.DUPLICATE_FUNCTION_NAME
|
|
104
|
+
)
|
|
105
|
+
assert.equal(nameCollision, undefined)
|
|
106
|
+
assert.strictEqual(state.rpc.internalMeta['createUser'], 'createUser@v2')
|
|
107
|
+
assert.ok(state.functions.meta['createUser@v1'])
|
|
108
|
+
assert.ok(state.functions.meta['createUser@v2'])
|
|
109
|
+
} finally {
|
|
110
|
+
await rm(rootDir, { recursive: true, force: true })
|
|
111
|
+
}
|
|
112
|
+
})
|
|
113
|
+
|
|
114
|
+
test('logs a critical error when exposed function name is duplicated across files', async () => {
|
|
115
|
+
const rootDir = await mkdtemp(
|
|
116
|
+
join(tmpdir(), 'pikku-exposed-duplicate-function-')
|
|
117
|
+
)
|
|
118
|
+
const fileA = join(rootDir, 'a.ts')
|
|
119
|
+
const fileB = join(rootDir, 'b.ts')
|
|
120
|
+
|
|
121
|
+
await writeFile(
|
|
122
|
+
fileA,
|
|
123
|
+
[
|
|
124
|
+
"import { pikkuFunc } from '@pikku/core'",
|
|
125
|
+
'export const createUser = pikkuFunc({',
|
|
126
|
+
' expose: true,',
|
|
127
|
+
' func: async () => ({ ok: true })',
|
|
128
|
+
'})',
|
|
129
|
+
].join('\n')
|
|
130
|
+
)
|
|
131
|
+
|
|
132
|
+
await writeFile(
|
|
133
|
+
fileB,
|
|
134
|
+
[
|
|
135
|
+
"import { pikkuFunc } from '@pikku/core'",
|
|
136
|
+
'export const createUser = pikkuFunc({',
|
|
137
|
+
' expose: true,',
|
|
138
|
+
' func: async () => ({ ok: true })',
|
|
139
|
+
'})',
|
|
140
|
+
].join('\n')
|
|
141
|
+
)
|
|
142
|
+
|
|
143
|
+
const criticals: Array<{ code: ErrorCode; message: string }> = []
|
|
144
|
+
const logger: InspectorLogger = {
|
|
145
|
+
debug: () => {},
|
|
146
|
+
info: () => {},
|
|
147
|
+
warn: () => {},
|
|
148
|
+
error: () => {},
|
|
149
|
+
critical: (code: ErrorCode, message: string) => {
|
|
150
|
+
criticals.push({ code, message })
|
|
151
|
+
},
|
|
152
|
+
hasCriticalErrors: () => criticals.length > 0,
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
try {
|
|
156
|
+
await inspect(logger, [fileA, fileB], { rootDir })
|
|
157
|
+
const nameCollision = criticals.find(
|
|
158
|
+
(entry) => entry.code === ErrorCode.DUPLICATE_FUNCTION_NAME
|
|
159
|
+
)
|
|
160
|
+
assert.ok(nameCollision)
|
|
161
|
+
assert.match(
|
|
162
|
+
nameCollision!.message,
|
|
163
|
+
/Function name 'createUser' is not unique/
|
|
164
|
+
)
|
|
165
|
+
} finally {
|
|
166
|
+
await rm(rootDir, { recursive: true, force: true })
|
|
167
|
+
}
|
|
168
|
+
})
|
|
169
|
+
})
|
package/src/add/add-functions.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import * as ts from 'typescript'
|
|
2
|
-
import type { AddWiring, SchemaRef } from '../types.js'
|
|
2
|
+
import type { AddWiring, InspectorState, SchemaRef } from '../types.js'
|
|
3
3
|
import { detectSchemaVendorOrError } from '../utils/detect-schema-vendor.js'
|
|
4
4
|
import type { TypesMap } from '../types-map.js'
|
|
5
5
|
import {
|
|
@@ -9,7 +9,7 @@ import {
|
|
|
9
9
|
import { extractFunctionNode } from '../utils/extract-function-node.js'
|
|
10
10
|
import { extractUsedWires } from '../utils/extract-services.js'
|
|
11
11
|
import type { FunctionServicesMeta } from '@pikku/core'
|
|
12
|
-
import { formatVersionedId } from '@pikku/core'
|
|
12
|
+
import { formatVersionedId, parseVersionedId } from '@pikku/core'
|
|
13
13
|
import {
|
|
14
14
|
getPropertyValue,
|
|
15
15
|
getCommonWireMetaData,
|
|
@@ -287,6 +287,31 @@ function unwrapPromise(checker: ts.TypeChecker, type: ts.Type): ts.Type {
|
|
|
287
287
|
return type
|
|
288
288
|
}
|
|
289
289
|
|
|
290
|
+
const resolveExistingFunctionSource = (
|
|
291
|
+
state: InspectorState,
|
|
292
|
+
pikkuFuncId: string
|
|
293
|
+
): string | null => {
|
|
294
|
+
return (
|
|
295
|
+
state.functions.meta[pikkuFuncId]?.sourceFile ||
|
|
296
|
+
state.rpc.internalFiles.get(pikkuFuncId)?.path ||
|
|
297
|
+
null
|
|
298
|
+
)
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
const areCompatibleFunctionIds = (
|
|
302
|
+
existingId: string,
|
|
303
|
+
incomingId: string
|
|
304
|
+
): boolean => {
|
|
305
|
+
if (existingId === incomingId) {
|
|
306
|
+
return true
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
const existingParsed = parseVersionedId(existingId)
|
|
310
|
+
const incomingParsed = parseVersionedId(incomingId)
|
|
311
|
+
|
|
312
|
+
return existingParsed.baseName === incomingParsed.baseName
|
|
313
|
+
}
|
|
314
|
+
|
|
290
315
|
/**
|
|
291
316
|
* Inspect pikkuFunc calls, extract input/output and first-arg destructuring,
|
|
292
317
|
* then push into state.functions.meta.
|
|
@@ -546,6 +571,7 @@ export const addFunctions: AddWiring = (
|
|
|
546
571
|
}
|
|
547
572
|
|
|
548
573
|
const isMCPToolFunc = expression.text === 'pikkuMCPToolFunc'
|
|
574
|
+
const isListFunc = expression.text === 'pikkuListFunc'
|
|
549
575
|
const mcpEnabled = mcp || isMCPToolFunc
|
|
550
576
|
|
|
551
577
|
// Pick the handler: use resolvedFunc when it exists and is a function, otherwise fall back to handlerNode
|
|
@@ -618,7 +644,33 @@ export const addFunctions: AddWiring = (
|
|
|
618
644
|
inputNames = [schemaName]
|
|
619
645
|
state.schemaLookup.set(schemaName, inputSchemaRef)
|
|
620
646
|
state.functions.typesMap.addCustomType(schemaName, 'unknown', [])
|
|
621
|
-
} else if (genericTypes.length >= 1 && genericTypes[0]) {
|
|
647
|
+
} else if (isListFunc && genericTypes.length >= 1 && genericTypes[0]) {
|
|
648
|
+
const inputAliasName = `${capitalizedName}Input`
|
|
649
|
+
const filterType = genericTypes[0]
|
|
650
|
+
const filterTypeText = checker.typeToString(
|
|
651
|
+
filterType,
|
|
652
|
+
undefined,
|
|
653
|
+
ts.TypeFormatFlags.NoTruncation
|
|
654
|
+
)
|
|
655
|
+
const refs = resolveTypeImports(
|
|
656
|
+
filterType,
|
|
657
|
+
state.functions.typesMap,
|
|
658
|
+
true,
|
|
659
|
+
checker
|
|
660
|
+
)
|
|
661
|
+
state.functions.typesMap.addCustomType(
|
|
662
|
+
inputAliasName,
|
|
663
|
+
`{ cursor?: string; limit?: number; sort?: Array<{ column: string; direction: 'asc' | 'desc' }>; filter?: unknown; search?: string; } & ${filterTypeText}`,
|
|
664
|
+
[...new Set(refs)]
|
|
665
|
+
)
|
|
666
|
+
inputNames = [inputAliasName]
|
|
667
|
+
const secondParam = handler.parameters[1]
|
|
668
|
+
if (secondParam) {
|
|
669
|
+
inputTypes = [checker.getTypeAtLocation(secondParam)]
|
|
670
|
+
} else {
|
|
671
|
+
inputTypes = [filterType]
|
|
672
|
+
}
|
|
673
|
+
} else if (!isListFunc && genericTypes.length >= 1 && genericTypes[0]) {
|
|
622
674
|
// Fall back to extracting from generic type arguments
|
|
623
675
|
const result = getNamesAndTypes(
|
|
624
676
|
checker,
|
|
@@ -654,7 +706,27 @@ export const addFunctions: AddWiring = (
|
|
|
654
706
|
outputNames = [schemaName]
|
|
655
707
|
state.schemaLookup.set(schemaName, outputSchemaRef)
|
|
656
708
|
state.functions.typesMap.addCustomType(schemaName, 'unknown', [])
|
|
657
|
-
} else if (genericTypes.length >= 2) {
|
|
709
|
+
} else if (isListFunc && genericTypes.length >= 2 && genericTypes[1]) {
|
|
710
|
+
const outputAliasName = `${capitalizedName}Output`
|
|
711
|
+
const rowType = genericTypes[1]
|
|
712
|
+
const rowTypeText = checker.typeToString(
|
|
713
|
+
rowType,
|
|
714
|
+
undefined,
|
|
715
|
+
ts.TypeFormatFlags.NoTruncation
|
|
716
|
+
)
|
|
717
|
+
const refs = resolveTypeImports(
|
|
718
|
+
rowType,
|
|
719
|
+
state.functions.typesMap,
|
|
720
|
+
true,
|
|
721
|
+
checker
|
|
722
|
+
)
|
|
723
|
+
state.functions.typesMap.addCustomType(
|
|
724
|
+
outputAliasName,
|
|
725
|
+
`{ rows: Array<${rowTypeText}>; nextCursor: string | null; totalCount?: number; }`,
|
|
726
|
+
[...new Set(refs)]
|
|
727
|
+
)
|
|
728
|
+
outputNames = [outputAliasName]
|
|
729
|
+
} else if (!isListFunc && genericTypes.length >= 2) {
|
|
658
730
|
outputNames = getNamesAndTypes(
|
|
659
731
|
checker,
|
|
660
732
|
state.functions.typesMap,
|
|
@@ -731,6 +803,56 @@ export const addFunctions: AddWiring = (
|
|
|
731
803
|
state.typesLookup.set(pikkuFuncId, inputTypes)
|
|
732
804
|
}
|
|
733
805
|
|
|
806
|
+
const sourceFile = node.getSourceFile().fileName
|
|
807
|
+
const existingFunction = state.functions.meta[pikkuFuncId]
|
|
808
|
+
if (
|
|
809
|
+
existingFunction &&
|
|
810
|
+
existingFunction.sourceFile &&
|
|
811
|
+
existingFunction.sourceFile !== sourceFile
|
|
812
|
+
) {
|
|
813
|
+
logger.critical(
|
|
814
|
+
ErrorCode.DUPLICATE_FUNCTION_NAME,
|
|
815
|
+
`Function name '${name}' is not unique. ` +
|
|
816
|
+
`'${pikkuFuncId}' is already defined in '${existingFunction.sourceFile}' and cannot be redefined in '${sourceFile}'.`
|
|
817
|
+
)
|
|
818
|
+
return
|
|
819
|
+
}
|
|
820
|
+
|
|
821
|
+
if (exportedName || explicitName) {
|
|
822
|
+
const existingInternal = state.rpc.internalMeta[name]
|
|
823
|
+
if (
|
|
824
|
+
existingInternal &&
|
|
825
|
+
!areCompatibleFunctionIds(existingInternal, pikkuFuncId)
|
|
826
|
+
) {
|
|
827
|
+
const existingSource =
|
|
828
|
+
resolveExistingFunctionSource(state, existingInternal) || 'unknown file'
|
|
829
|
+
logger.critical(
|
|
830
|
+
ErrorCode.DUPLICATE_FUNCTION_NAME,
|
|
831
|
+
`Function name '${name}' is not unique. ` +
|
|
832
|
+
`It already points to '${existingInternal}' in '${existingSource}', but '${pikkuFuncId}' in '${sourceFile}' tried to use the same name.`
|
|
833
|
+
)
|
|
834
|
+
return
|
|
835
|
+
}
|
|
836
|
+
|
|
837
|
+
if (expose) {
|
|
838
|
+
const existingExposed = state.rpc.exposedMeta[name]
|
|
839
|
+
if (
|
|
840
|
+
existingExposed &&
|
|
841
|
+
!areCompatibleFunctionIds(existingExposed, pikkuFuncId)
|
|
842
|
+
) {
|
|
843
|
+
const existingSource =
|
|
844
|
+
resolveExistingFunctionSource(state, existingExposed) ||
|
|
845
|
+
'unknown file'
|
|
846
|
+
logger.critical(
|
|
847
|
+
ErrorCode.DUPLICATE_FUNCTION_NAME,
|
|
848
|
+
`Exposed function name '${name}' is not unique. ` +
|
|
849
|
+
`It already points to '${existingExposed}' in '${existingSource}', but '${pikkuFuncId}' in '${sourceFile}' tried to use the same name.`
|
|
850
|
+
)
|
|
851
|
+
return
|
|
852
|
+
}
|
|
853
|
+
}
|
|
854
|
+
}
|
|
855
|
+
|
|
734
856
|
// --- resolve middleware ---
|
|
735
857
|
let middleware = objectNode
|
|
736
858
|
? resolveMiddleware(state, objectNode, tags, checker)
|
|
@@ -801,7 +923,7 @@ export const addFunctions: AddWiring = (
|
|
|
801
923
|
middleware,
|
|
802
924
|
permissions,
|
|
803
925
|
isDirectFunction,
|
|
804
|
-
sourceFile
|
|
926
|
+
sourceFile,
|
|
805
927
|
exportedName: exportedName || undefined,
|
|
806
928
|
}
|
|
807
929
|
|