@pikku/inspector 0.12.21 → 0.12.23
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 +30 -0
- package/dist/add/add-addon-bans.d.ts +7 -0
- package/dist/add/add-addon-bans.js +65 -0
- package/dist/add/add-channel.js +47 -6
- package/dist/add/add-cli.js +17 -0
- package/dist/add/add-functions.js +16 -8
- package/dist/add/add-http-route.d.ts +11 -1
- package/dist/add/add-http-route.js +37 -0
- package/dist/add/add-http-routes.d.ts +0 -3
- package/dist/add/add-http-routes.js +179 -36
- package/dist/add/add-workflow.js +16 -2
- package/dist/error-codes.d.ts +15 -1
- package/dist/error-codes.js +3 -0
- package/dist/index.d.ts +1 -0
- package/dist/inspector.js +22 -6
- package/dist/types.d.ts +53 -2
- package/dist/utils/extract-node-value.js +19 -2
- package/dist/utils/get-exported-variable-name.d.ts +2 -0
- package/dist/utils/get-exported-variable-name.js +34 -0
- package/dist/utils/load-addon-functions-meta.js +98 -0
- package/dist/utils/resolve-addon-package.js +3 -1
- package/dist/utils/resolve-ref-contract.d.ts +21 -0
- package/dist/utils/resolve-ref-contract.js +46 -0
- package/dist/utils/serialize-inspector-state.d.ts +1 -0
- package/dist/utils/serialize-inspector-state.js +9 -0
- package/dist/utils/workflow/dsl/extract-dsl-workflow.js +15 -0
- package/dist/visit.js +24 -19
- package/package.json +2 -2
- package/src/add/add-addon-bans.ts +84 -0
- package/src/add/add-auth.test.ts +3 -0
- package/src/add/add-channel.ts +66 -7
- package/src/add/add-cli-renderers.test.ts +1 -0
- package/src/add/add-cli.ts +30 -0
- package/src/add/add-functions.test.ts +13 -0
- package/src/add/add-functions.ts +14 -10
- package/src/add/add-http-route.ts +75 -1
- package/src/add/add-http-routes.ts +283 -41
- package/src/add/add-workflow-fanout.test.ts +106 -0
- package/src/add/add-workflow.test.ts +3 -0
- package/src/add/add-workflow.ts +16 -2
- package/src/add/addon-bans.test.ts +121 -0
- package/src/add/addon-contracts.test.ts +221 -0
- package/src/add/pii-check.test.ts +4 -0
- package/src/add/wire-name-literal.test.ts +3 -0
- package/src/error-codes.ts +18 -0
- package/src/index.ts +1 -0
- package/src/inspector.ts +25 -6
- package/src/types.ts +75 -2
- package/src/utils/extract-node-value.test.ts +49 -1
- package/src/utils/extract-node-value.ts +19 -2
- package/src/utils/filter-inspector-state.test.ts +1 -0
- package/src/utils/filter-utils.test.ts +1 -0
- package/src/utils/get-exported-variable-name.ts +48 -0
- package/src/utils/load-addon-functions-meta.ts +164 -0
- package/src/utils/resolve-addon-package.ts +6 -1
- package/src/utils/resolve-ref-contract.ts +71 -0
- package/src/utils/resolve-versions.test.ts +1 -0
- package/src/utils/serialize-inspector-state.ts +10 -0
- package/src/utils/workflow/dsl/extract-dsl-workflow.ts +16 -0
- package/src/visit.ts +26 -19
- package/tsconfig.tsbuildinfo +1 -1
package/dist/add/add-workflow.js
CHANGED
|
@@ -5,6 +5,20 @@ import { ErrorCode } from '../error-codes.js';
|
|
|
5
5
|
import { extractStringLiteral, isStringLike, isFunctionLike, extractDescription, extractDuration, } from '../utils/extract-node-value.js';
|
|
6
6
|
import { getCommonWireMetaData, getPropertyValue, } from '../utils/get-property-value.js';
|
|
7
7
|
import { extractDSLWorkflow } from '../utils/workflow/dsl/extract-dsl-workflow.js';
|
|
8
|
+
import { getSourceText } from '../utils/workflow/dsl/patterns.js';
|
|
9
|
+
/**
|
|
10
|
+
* Extract a workflow step's display name without letting a non-static name
|
|
11
|
+
* (e.g. a function call) abort the scan. The step name is cosmetic, so a
|
|
12
|
+
* resolution failure must never prevent the RPC from being registered.
|
|
13
|
+
*/
|
|
14
|
+
function extractStepName(node, checker) {
|
|
15
|
+
try {
|
|
16
|
+
return extractStringLiteral(node, checker);
|
|
17
|
+
}
|
|
18
|
+
catch {
|
|
19
|
+
return getSourceText(node);
|
|
20
|
+
}
|
|
21
|
+
}
|
|
8
22
|
/**
|
|
9
23
|
* Recursively check if any step has inline type (non-serializable)
|
|
10
24
|
*/
|
|
@@ -89,7 +103,7 @@ function getWorkflowInvocations(node, checker, state, workflowName, steps) {
|
|
|
89
103
|
const stepNameArg = args[0];
|
|
90
104
|
const secondArg = args[1];
|
|
91
105
|
const optionsArg = args.length >= 4 ? args[args.length - 1] : undefined;
|
|
92
|
-
const stepName =
|
|
106
|
+
const stepName = extractStepName(stepNameArg, checker);
|
|
93
107
|
const description = extractDescription(optionsArg, checker) ?? undefined;
|
|
94
108
|
// Determine form by checking 2nd argument type
|
|
95
109
|
if (isStringLike(secondArg, checker)) {
|
|
@@ -115,7 +129,7 @@ function getWorkflowInvocations(node, checker, state, workflowName, steps) {
|
|
|
115
129
|
// workflow.sleep(stepName, duration)
|
|
116
130
|
const stepNameArg = args[0];
|
|
117
131
|
const durationArg = args[1];
|
|
118
|
-
const stepName =
|
|
132
|
+
const stepName = extractStepName(stepNameArg, checker);
|
|
119
133
|
const duration = extractDuration(durationArg, checker);
|
|
120
134
|
steps.push({
|
|
121
135
|
type: 'sleep',
|
package/dist/error-codes.d.ts
CHANGED
|
@@ -57,5 +57,19 @@ export declare enum ErrorCode {
|
|
|
57
57
|
SERVICES_NOT_DESTRUCTURED = "PKU410",
|
|
58
58
|
WIRES_NOT_DESTRUCTURED = "PKU411",
|
|
59
59
|
WORKFLOW_MULTI_QUEUE_NOT_SUPPORTED = "PKU901",
|
|
60
|
-
PII_IN_OUTPUT = "PKU910"
|
|
60
|
+
PII_IN_OUTPUT = "PKU910",
|
|
61
|
+
ADDON_WIRING_NOT_ALLOWED = "PKU920",
|
|
62
|
+
ADDON_CONTRACT_HANDLERS_NOT_ALLOWED = "PKU921"
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Severity of a tracked, coded diagnostic. `critical` always blocks the build;
|
|
66
|
+
* `error`/`warn` only block when the CLI is told to via `--fail-on-error` /
|
|
67
|
+
* `--fail-on-warn` (default: critical only). All severities are still printed.
|
|
68
|
+
*/
|
|
69
|
+
export type DiagnosticSeverity = 'warn' | 'error' | 'critical';
|
|
70
|
+
/** A coded diagnostic emitted via `logger.diagnostic(...)`. */
|
|
71
|
+
export interface CodedDiagnostic {
|
|
72
|
+
severity: DiagnosticSeverity;
|
|
73
|
+
code: ErrorCode;
|
|
74
|
+
message: string;
|
|
61
75
|
}
|
package/dist/error-codes.js
CHANGED
|
@@ -72,4 +72,7 @@ export var ErrorCode;
|
|
|
72
72
|
ErrorCode["WORKFLOW_MULTI_QUEUE_NOT_SUPPORTED"] = "PKU901";
|
|
73
73
|
// Data classification errors
|
|
74
74
|
ErrorCode["PII_IN_OUTPUT"] = "PKU910";
|
|
75
|
+
// Addon authoring errors
|
|
76
|
+
ErrorCode["ADDON_WIRING_NOT_ALLOWED"] = "PKU920";
|
|
77
|
+
ErrorCode["ADDON_CONTRACT_HANDLERS_NOT_ALLOWED"] = "PKU921";
|
|
75
78
|
})(ErrorCode || (ErrorCode = {}));
|
package/dist/index.d.ts
CHANGED
|
@@ -3,6 +3,7 @@ export type { TypesMap } from './types-map.js';
|
|
|
3
3
|
export type * from './types.js';
|
|
4
4
|
export type { FilesAndMethodsErrors } from './utils/get-files-and-methods.js';
|
|
5
5
|
export { ErrorCode } from './error-codes.js';
|
|
6
|
+
export type { DiagnosticSeverity, CodedDiagnostic } from './error-codes.js';
|
|
6
7
|
export { AUTH_HANDLER_FUNC_ID } from './add/add-auth.js';
|
|
7
8
|
export { serializeInspectorState, deserializeInspectorState, } from './utils/serialize-inspector-state.js';
|
|
8
9
|
export type { SerializableInspectorState } from './utils/serialize-inspector-state.js';
|
package/dist/inspector.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import * as ts from 'typescript';
|
|
2
2
|
import { performance } from 'perf_hooks';
|
|
3
|
+
import { resolve } from 'path';
|
|
3
4
|
import { visitSetup, visitRoutes } from './visit.js';
|
|
4
5
|
import { TypesMap } from './types-map.js';
|
|
5
6
|
import { getFilesAndMethods } from './utils/get-files-and-methods.js';
|
|
@@ -185,10 +186,19 @@ export function getInitialInspectorState(rootDir) {
|
|
|
185
186
|
openAPISpec: null,
|
|
186
187
|
diagnostics: [],
|
|
187
188
|
addonFunctions: {},
|
|
189
|
+
exportedContracts: {
|
|
190
|
+
http: {},
|
|
191
|
+
cli: {},
|
|
192
|
+
channel: {},
|
|
193
|
+
addonHttp: {},
|
|
194
|
+
addonCli: {},
|
|
195
|
+
addonChannel: {},
|
|
196
|
+
},
|
|
188
197
|
program: null,
|
|
189
198
|
};
|
|
190
199
|
}
|
|
191
200
|
export const inspect = async (logger, routeFiles, options = {}) => {
|
|
201
|
+
const normalizedRouteFiles = routeFiles.map((file) => resolve(file));
|
|
192
202
|
const compilerOptions = {
|
|
193
203
|
target: ts.ScriptTarget.ESNext,
|
|
194
204
|
module: ts.ModuleKind.Node16,
|
|
@@ -200,24 +210,29 @@ export const inspect = async (logger, routeFiles, options = {}) => {
|
|
|
200
210
|
checkJs: false,
|
|
201
211
|
};
|
|
202
212
|
const startProgram = performance.now();
|
|
203
|
-
const program = ts.createProgram(
|
|
213
|
+
const program = ts.createProgram(normalizedRouteFiles, compilerOptions, undefined, // host
|
|
204
214
|
options.oldProgram);
|
|
205
|
-
logger.debug(`Created program in ${(performance.now() - startProgram).toFixed(0)}ms (${
|
|
215
|
+
logger.debug(`Created program in ${(performance.now() - startProgram).toFixed(0)}ms (${normalizedRouteFiles.length} files${options.oldProgram ? ', incremental' : ''})`);
|
|
206
216
|
const startChecker = performance.now();
|
|
207
217
|
const checker = program.getTypeChecker();
|
|
208
218
|
logger.debug(`Got type checker in ${(performance.now() - startChecker).toFixed(2)}ms`);
|
|
209
219
|
// Use provided rootDir or infer from source files
|
|
210
|
-
const rootDir = options.rootDir || findCommonAncestor(
|
|
220
|
+
const rootDir = options.rootDir || findCommonAncestor(normalizedRouteFiles);
|
|
211
221
|
const startSourceFiles = performance.now();
|
|
222
|
+
// node_modules under rootDir (e.g. a locally-installed addon) is a
|
|
223
|
+
// dependency, not project source — scanning it double-counts the addon's
|
|
224
|
+
// own application types (CoreConfig/Services/SingletonServices).
|
|
212
225
|
const sourceFiles = program
|
|
213
226
|
.getSourceFiles()
|
|
214
|
-
.filter((sf) => sf.fileName.startsWith(rootDir)
|
|
227
|
+
.filter((sf) => sf.fileName.startsWith(rootDir) &&
|
|
228
|
+
!sf.fileName.includes('/node_modules/'));
|
|
215
229
|
logger.debug(`Got source files in ${(performance.now() - startSourceFiles).toFixed(2)}ms`);
|
|
216
230
|
const state = getInitialInspectorState(rootDir);
|
|
217
231
|
// First sweep: add all functions
|
|
218
232
|
const startSetup = performance.now();
|
|
219
233
|
for (const sourceFile of sourceFiles) {
|
|
220
|
-
|
|
234
|
+
const sourceOptions = { ...options, sourceFile };
|
|
235
|
+
ts.forEachChild(sourceFile, (child) => visitSetup(logger, checker, child, state, sourceOptions));
|
|
221
236
|
}
|
|
222
237
|
logger.debug(`Visit setup phase completed in ${(performance.now() - startSetup).toFixed(0)}ms`);
|
|
223
238
|
// Load addon function metadata so wirings can reference addon functions
|
|
@@ -226,7 +241,8 @@ export const inspect = async (logger, routeFiles, options = {}) => {
|
|
|
226
241
|
// Second sweep: add all transports
|
|
227
242
|
const startRoutes = performance.now();
|
|
228
243
|
for (const sourceFile of sourceFiles) {
|
|
229
|
-
|
|
244
|
+
const sourceOptions = { ...options, sourceFile };
|
|
245
|
+
ts.forEachChild(sourceFile, (child) => visitRoutes(logger, checker, child, state, sourceOptions));
|
|
230
246
|
}
|
|
231
247
|
logger.debug(`Visit routes phase completed in ${(performance.now() - startRoutes).toFixed(0)}ms`);
|
|
232
248
|
resolveLatestVersions(state, logger);
|
package/dist/types.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type * as ts from 'typescript';
|
|
2
|
-
import type { ChannelsMeta } from '@pikku/core/channel';
|
|
2
|
+
import type { ChannelMessageMeta, ChannelsMeta } from '@pikku/core/channel';
|
|
3
3
|
import type { GatewaysMeta } from '@pikku/core/gateway';
|
|
4
4
|
import type { HTTPWiringsMeta } from '@pikku/core/http';
|
|
5
5
|
import type { ScheduledTasksMeta } from '@pikku/core/scheduler';
|
|
@@ -9,6 +9,7 @@ import type { WorkflowsMeta } from '@pikku/core/workflow';
|
|
|
9
9
|
import type { MCPResourceMeta, MCPToolMeta, MCPPromptMeta } from '@pikku/core/mcp';
|
|
10
10
|
import type { AIAgentMeta } from '@pikku/core/ai-agent';
|
|
11
11
|
import type { CLIMeta } from '@pikku/core/cli';
|
|
12
|
+
import type { CLICommandMeta } from '@pikku/core/cli';
|
|
12
13
|
import type { NodesMeta } from '@pikku/core/node';
|
|
13
14
|
import type { SecretDefinitions } from '@pikku/core/secret';
|
|
14
15
|
import type { CredentialDefinitions } from '@pikku/core/credential';
|
|
@@ -16,7 +17,7 @@ import type { VariableDefinitions } from '@pikku/core/variable';
|
|
|
16
17
|
import type { TypesMap } from './types-map.js';
|
|
17
18
|
import type { FunctionsMeta, FunctionServicesMeta, FunctionWiresMeta, JSONValue } from '@pikku/core';
|
|
18
19
|
import type { OpenAPISpecInfo } from './utils/serialize-openapi-json.js';
|
|
19
|
-
import type { ErrorCode } from './error-codes.js';
|
|
20
|
+
import type { ErrorCode, CodedDiagnostic } from './error-codes.js';
|
|
20
21
|
import type { VersionManifest, VersionValidateError } from './utils/contract-hashes.js';
|
|
21
22
|
import type { SerializedWorkflowGraphs } from './utils/workflow/graph/workflow-graph.types.js';
|
|
22
23
|
export type PathToNameAndType = Map<string, {
|
|
@@ -80,6 +81,45 @@ export interface InspectorChannelState {
|
|
|
80
81
|
meta: ChannelsMeta;
|
|
81
82
|
files: Set<string>;
|
|
82
83
|
}
|
|
84
|
+
export interface ExportedHTTPRouteFunctionMeta {
|
|
85
|
+
pikkuFuncId: string;
|
|
86
|
+
packageName?: string;
|
|
87
|
+
}
|
|
88
|
+
export interface ExportedHTTPRouteConfigMeta {
|
|
89
|
+
method: string;
|
|
90
|
+
route: string;
|
|
91
|
+
func: ExportedHTTPRouteFunctionMeta;
|
|
92
|
+
auth?: boolean;
|
|
93
|
+
tags?: string[];
|
|
94
|
+
sse?: boolean;
|
|
95
|
+
contentType?: string;
|
|
96
|
+
timeout?: number;
|
|
97
|
+
headers?: Record<string, string>;
|
|
98
|
+
}
|
|
99
|
+
export interface ExportedHTTPRoutesGroupMeta {
|
|
100
|
+
basePath?: string;
|
|
101
|
+
tags?: string[];
|
|
102
|
+
auth?: boolean;
|
|
103
|
+
routes: ExportedHTTPRouteMapMeta;
|
|
104
|
+
}
|
|
105
|
+
export type ExportedHTTPRouteEntryMeta = ExportedHTTPRouteConfigMeta | ExportedHTTPRoutesGroupMeta | ExportedHTTPRouteMapMeta;
|
|
106
|
+
export interface ExportedHTTPRouteMapMeta {
|
|
107
|
+
[key: string]: ExportedHTTPRouteEntryMeta;
|
|
108
|
+
}
|
|
109
|
+
export type ExportedHTTPContractsMeta = Record<string, ExportedHTTPRoutesGroupMeta>;
|
|
110
|
+
export interface ExportedChannelRouteMeta extends ChannelMessageMeta {
|
|
111
|
+
auth?: boolean;
|
|
112
|
+
}
|
|
113
|
+
export type ExportedChannelContractsMeta = Record<string, Record<string, ExportedChannelRouteMeta>>;
|
|
114
|
+
export type ExportedCLIContractsMeta = Record<string, Record<string, CLICommandMeta>>;
|
|
115
|
+
export interface InspectorExportedContractsState {
|
|
116
|
+
http: ExportedHTTPContractsMeta;
|
|
117
|
+
cli: ExportedCLIContractsMeta;
|
|
118
|
+
channel: ExportedChannelContractsMeta;
|
|
119
|
+
addonHttp: Record<string, ExportedHTTPContractsMeta>;
|
|
120
|
+
addonCli: Record<string, ExportedCLIContractsMeta>;
|
|
121
|
+
addonChannel: Record<string, ExportedChannelContractsMeta>;
|
|
122
|
+
}
|
|
83
123
|
export interface InspectorMiddlewareDefinition {
|
|
84
124
|
services: FunctionServicesMeta;
|
|
85
125
|
wires?: FunctionWiresMeta;
|
|
@@ -167,6 +207,7 @@ export type InspectorOptions = Partial<{
|
|
|
167
207
|
setupOnly: boolean;
|
|
168
208
|
rootDir: string;
|
|
169
209
|
isAddon: boolean;
|
|
210
|
+
sourceFile: ts.SourceFile;
|
|
170
211
|
types: Partial<{
|
|
171
212
|
configFileType: string;
|
|
172
213
|
userSessionType: string;
|
|
@@ -192,6 +233,15 @@ export interface InspectorLogger {
|
|
|
192
233
|
error: (message: string) => void;
|
|
193
234
|
warn: (message: string) => void;
|
|
194
235
|
debug: (message: string) => void;
|
|
236
|
+
/**
|
|
237
|
+
* Emit a tracked, coded diagnostic. It is recorded and printed; `error`/`warn`
|
|
238
|
+
* only block the build when the CLI is run with `--fail-on-error` /
|
|
239
|
+
* `--fail-on-warn` (default: critical only). Use this for issues worth
|
|
240
|
+
* surfacing (e.g. data-classification leaks) that should not stop the dev
|
|
241
|
+
* server from starting.
|
|
242
|
+
*/
|
|
243
|
+
diagnostic: (diagnostic: CodedDiagnostic) => void;
|
|
244
|
+
/** Sugar for `diagnostic({ severity: 'critical', code, message })`. */
|
|
195
245
|
critical: (code: ErrorCode, message: string) => void;
|
|
196
246
|
hasCriticalErrors: () => boolean;
|
|
197
247
|
}
|
|
@@ -435,5 +485,6 @@ export interface InspectorState {
|
|
|
435
485
|
openAPISpec: Record<string, any> | null;
|
|
436
486
|
diagnostics: InspectorDiagnostic[];
|
|
437
487
|
addonFunctions: Record<string, FunctionsMeta>;
|
|
488
|
+
exportedContracts: InspectorExportedContractsState;
|
|
438
489
|
program: ts.Program | null;
|
|
439
490
|
}
|
|
@@ -26,8 +26,8 @@ export function extractStringLiteral(node, checker) {
|
|
|
26
26
|
}
|
|
27
27
|
if (ts.isBinaryExpression(node) &&
|
|
28
28
|
node.operatorToken.kind === ts.SyntaxKind.PlusToken) {
|
|
29
|
-
return (
|
|
30
|
-
|
|
29
|
+
return (extractConcatOperand(node.left, checker) +
|
|
30
|
+
extractConcatOperand(node.right, checker));
|
|
31
31
|
}
|
|
32
32
|
// Try to evaluate constant identifiers
|
|
33
33
|
if (ts.isIdentifier(node)) {
|
|
@@ -42,6 +42,23 @@ export function extractStringLiteral(node, checker) {
|
|
|
42
42
|
}
|
|
43
43
|
throw new Error('Unable to extract string literal from node');
|
|
44
44
|
}
|
|
45
|
+
/**
|
|
46
|
+
* Resolve one operand of a `+` string concatenation.
|
|
47
|
+
*
|
|
48
|
+
* An operand that can't be statically resolved (e.g. `a ?? b`) becomes a
|
|
49
|
+
* `${...}` placeholder rather than throwing — mirroring the TemplateExpression
|
|
50
|
+
* branch above, so `'x ' + expr` and `` `x ${expr}` `` produce the same string.
|
|
51
|
+
* This keeps an unresolvable display name from aborting the whole extraction.
|
|
52
|
+
*/
|
|
53
|
+
function extractConcatOperand(node, checker) {
|
|
54
|
+
try {
|
|
55
|
+
return extractStringLiteral(node, checker);
|
|
56
|
+
}
|
|
57
|
+
catch {
|
|
58
|
+
const inner = ts.isParenthesizedExpression(node) ? node.expression : node;
|
|
59
|
+
return '${' + inner.getText() + '}';
|
|
60
|
+
}
|
|
61
|
+
}
|
|
45
62
|
/**
|
|
46
63
|
* Check if node is string-like (string literal or template expression)
|
|
47
64
|
*/
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import * as ts from 'typescript';
|
|
2
|
+
const isExportedVariableStatement = (statement) => ts.isVariableStatement(statement) &&
|
|
3
|
+
(statement.modifiers?.some((modifier) => modifier.kind === ts.SyntaxKind.ExportKeyword) ??
|
|
4
|
+
false);
|
|
5
|
+
export const getExportedVariableName = (node, sourceFile) => {
|
|
6
|
+
if (!sourceFile) {
|
|
7
|
+
return null;
|
|
8
|
+
}
|
|
9
|
+
if (ts.isVariableDeclaration(node) && ts.isIdentifier(node.name)) {
|
|
10
|
+
for (const statement of sourceFile.statements) {
|
|
11
|
+
if (!isExportedVariableStatement(statement))
|
|
12
|
+
continue;
|
|
13
|
+
for (const declaration of statement.declarationList.declarations) {
|
|
14
|
+
if (declaration === node) {
|
|
15
|
+
return node.name.text;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
if (!ts.isCallExpression(node)) {
|
|
21
|
+
return null;
|
|
22
|
+
}
|
|
23
|
+
for (const statement of sourceFile.statements) {
|
|
24
|
+
if (!isExportedVariableStatement(statement))
|
|
25
|
+
continue;
|
|
26
|
+
for (const declaration of statement.declarationList.declarations) {
|
|
27
|
+
if (ts.isIdentifier(declaration.name) &&
|
|
28
|
+
declaration.initializer === node) {
|
|
29
|
+
return declaration.name.text;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
return null;
|
|
34
|
+
};
|
|
@@ -1,6 +1,74 @@
|
|
|
1
1
|
import { readFile, readdir } from 'fs/promises';
|
|
2
2
|
import { createRequire } from 'module';
|
|
3
3
|
import { join, dirname } from 'path';
|
|
4
|
+
const isHTTPRouteConfig = (value) => typeof value === 'object' &&
|
|
5
|
+
value !== null &&
|
|
6
|
+
'method' in value &&
|
|
7
|
+
'func' in value &&
|
|
8
|
+
'route' in value;
|
|
9
|
+
const isHTTPRouteGroup = (value) => typeof value === 'object' &&
|
|
10
|
+
value !== null &&
|
|
11
|
+
'routes' in value &&
|
|
12
|
+
!('method' in value);
|
|
13
|
+
const applyPackageToHTTPRouteMap = (routes, packageName, namespace) => {
|
|
14
|
+
for (const value of Object.values(routes)) {
|
|
15
|
+
if (!value || typeof value !== 'object')
|
|
16
|
+
continue;
|
|
17
|
+
if (isHTTPRouteConfig(value)) {
|
|
18
|
+
if (!value.func.packageName) {
|
|
19
|
+
value.func.packageName = packageName;
|
|
20
|
+
}
|
|
21
|
+
if (namespace && !value.func.pikkuFuncId.includes(':')) {
|
|
22
|
+
value.func.pikkuFuncId = `${namespace}:${value.func.pikkuFuncId}`;
|
|
23
|
+
}
|
|
24
|
+
continue;
|
|
25
|
+
}
|
|
26
|
+
if (isHTTPRouteGroup(value)) {
|
|
27
|
+
applyPackageToHTTPRouteMap(value.routes, packageName, namespace);
|
|
28
|
+
continue;
|
|
29
|
+
}
|
|
30
|
+
applyPackageToHTTPRouteMap(value, packageName, namespace);
|
|
31
|
+
}
|
|
32
|
+
};
|
|
33
|
+
const applyPackageToHTTPContracts = (contracts, packageName, namespace) => {
|
|
34
|
+
for (const contract of Object.values(contracts)) {
|
|
35
|
+
applyPackageToHTTPRouteMap(contract.routes, packageName, namespace);
|
|
36
|
+
}
|
|
37
|
+
};
|
|
38
|
+
const applyPackageToCLICommands = (commands, packageName, namespace) => {
|
|
39
|
+
for (const command of Object.values(commands)) {
|
|
40
|
+
if (command && typeof command === 'object') {
|
|
41
|
+
if (!command.packageName && command.pikkuFuncId) {
|
|
42
|
+
command.packageName = packageName;
|
|
43
|
+
}
|
|
44
|
+
if (namespace &&
|
|
45
|
+
typeof command.pikkuFuncId === 'string' &&
|
|
46
|
+
!command.pikkuFuncId.includes(':')) {
|
|
47
|
+
command.pikkuFuncId = `${namespace}:${command.pikkuFuncId}`;
|
|
48
|
+
}
|
|
49
|
+
if (command.subcommands) {
|
|
50
|
+
applyPackageToCLICommands(command.subcommands, packageName, namespace);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
};
|
|
55
|
+
const applyPackageToCLIContracts = (contracts, packageName, namespace) => {
|
|
56
|
+
for (const commands of Object.values(contracts)) {
|
|
57
|
+
applyPackageToCLICommands(commands, packageName, namespace);
|
|
58
|
+
}
|
|
59
|
+
};
|
|
60
|
+
const applyPackageToChannelContracts = (contracts, packageName, namespace) => {
|
|
61
|
+
for (const routes of Object.values(contracts)) {
|
|
62
|
+
for (const route of Object.values(routes)) {
|
|
63
|
+
if (!route.packageName) {
|
|
64
|
+
route.packageName = packageName;
|
|
65
|
+
}
|
|
66
|
+
if (!route.pikkuFuncId.includes(':')) {
|
|
67
|
+
route.pikkuFuncId = `${namespace}:${route.pikkuFuncId}`;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
};
|
|
4
72
|
/**
|
|
5
73
|
* After the setup sweep discovers wireAddon() declarations, load each addon
|
|
6
74
|
* package's function metadata so that wiring handlers (channels, HTTP routes,
|
|
@@ -81,6 +149,36 @@ export async function loadAddonFunctionsMeta(logger, state) {
|
|
|
81
149
|
catch {
|
|
82
150
|
// No services gen — addon may not have requiredParentServices
|
|
83
151
|
}
|
|
152
|
+
try {
|
|
153
|
+
const httpContractsPath = require.resolve(`${decl.package}/.pikku/http/pikku-http-contracts-meta.gen.json`);
|
|
154
|
+
const httpContractsRaw = await readFile(httpContractsPath, 'utf-8');
|
|
155
|
+
const httpContracts = JSON.parse(httpContractsRaw);
|
|
156
|
+
applyPackageToHTTPContracts(httpContracts, decl.package, namespace);
|
|
157
|
+
state.exportedContracts.addonHttp[namespace] = httpContracts;
|
|
158
|
+
}
|
|
159
|
+
catch {
|
|
160
|
+
// No addon HTTP contracts metadata
|
|
161
|
+
}
|
|
162
|
+
try {
|
|
163
|
+
const cliContractsPath = require.resolve(`${decl.package}/.pikku/cli/pikku-cli-contracts-meta.gen.json`);
|
|
164
|
+
const cliContractsRaw = await readFile(cliContractsPath, 'utf-8');
|
|
165
|
+
const cliContracts = JSON.parse(cliContractsRaw);
|
|
166
|
+
applyPackageToCLIContracts(cliContracts, decl.package, namespace);
|
|
167
|
+
state.exportedContracts.addonCli[namespace] = cliContracts;
|
|
168
|
+
}
|
|
169
|
+
catch {
|
|
170
|
+
// No addon CLI contracts metadata
|
|
171
|
+
}
|
|
172
|
+
try {
|
|
173
|
+
const channelContractsPath = require.resolve(`${decl.package}/.pikku/channel/pikku-channel-contracts-meta.gen.json`);
|
|
174
|
+
const channelContractsRaw = await readFile(channelContractsPath, 'utf-8');
|
|
175
|
+
const channelContracts = JSON.parse(channelContractsRaw);
|
|
176
|
+
applyPackageToChannelContracts(channelContracts, decl.package, namespace);
|
|
177
|
+
state.exportedContracts.addonChannel[namespace] = channelContracts;
|
|
178
|
+
}
|
|
179
|
+
catch {
|
|
180
|
+
// No addon channel contracts metadata
|
|
181
|
+
}
|
|
84
182
|
}
|
|
85
183
|
catch (error) {
|
|
86
184
|
logger.warn(`Failed to load addon function metadata for '${namespace}' (${decl.package}): ${error.message}`);
|
|
@@ -61,8 +61,10 @@ export const resolveAddonName = (identifier, checker, wireAddonDeclarations) =>
|
|
|
61
61
|
// Bare package import path
|
|
62
62
|
if (candidatePackage && !candidatePackage.startsWith('.')) {
|
|
63
63
|
for (const addonDecl of wireAddonDeclarations.values()) {
|
|
64
|
-
if (addonDecl.package === candidatePackage
|
|
64
|
+
if (addonDecl.package === candidatePackage ||
|
|
65
|
+
candidatePackage.startsWith(`${addonDecl.package}/`)) {
|
|
65
66
|
return addonDecl.package;
|
|
67
|
+
}
|
|
66
68
|
}
|
|
67
69
|
}
|
|
68
70
|
// Fall back to package.json lookup based on the declaration's source file.
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import * as ts from 'typescript';
|
|
2
|
+
export interface RefContractResolution<T> {
|
|
3
|
+
contract: T;
|
|
4
|
+
/**
|
|
5
|
+
* Optional basePath override supplied by the consumer via the second
|
|
6
|
+
* argument, e.g. refHTTP('ns:routes', { basePath: '/ext' }). When undefined
|
|
7
|
+
* the addon contract's own basePath is preserved.
|
|
8
|
+
*/
|
|
9
|
+
basePath?: string;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Resolve a refHTTP / refChannel / refCLI call expression against the addon
|
|
13
|
+
* contracts already loaded (and namespaced) by loadAddonFunctionsMeta.
|
|
14
|
+
*
|
|
15
|
+
* The first string argument has the form 'namespace:contractName', mirroring
|
|
16
|
+
* how ref('namespace:fn') references an addon function. Detection is purely
|
|
17
|
+
* syntactic — no import resolution is required because the namespace and
|
|
18
|
+
* contract name are carried in the string literal. An optional second object
|
|
19
|
+
* argument may override mount details such as basePath.
|
|
20
|
+
*/
|
|
21
|
+
export declare const resolveRefContract: <T>(node: ts.Node, helperName: "refHTTP" | "refChannel" | "refCLI", addonContracts: Record<string, Record<string, T>>) => RefContractResolution<T> | null;
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import * as ts from 'typescript';
|
|
2
|
+
const getStringProperty = (obj, name) => {
|
|
3
|
+
for (const prop of obj.properties) {
|
|
4
|
+
if (ts.isPropertyAssignment(prop) &&
|
|
5
|
+
(ts.isIdentifier(prop.name) || ts.isStringLiteral(prop.name)) &&
|
|
6
|
+
prop.name.text === name &&
|
|
7
|
+
ts.isStringLiteral(prop.initializer)) {
|
|
8
|
+
return prop.initializer.text;
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
return undefined;
|
|
12
|
+
};
|
|
13
|
+
/**
|
|
14
|
+
* Resolve a refHTTP / refChannel / refCLI call expression against the addon
|
|
15
|
+
* contracts already loaded (and namespaced) by loadAddonFunctionsMeta.
|
|
16
|
+
*
|
|
17
|
+
* The first string argument has the form 'namespace:contractName', mirroring
|
|
18
|
+
* how ref('namespace:fn') references an addon function. Detection is purely
|
|
19
|
+
* syntactic — no import resolution is required because the namespace and
|
|
20
|
+
* contract name are carried in the string literal. An optional second object
|
|
21
|
+
* argument may override mount details such as basePath.
|
|
22
|
+
*/
|
|
23
|
+
export const resolveRefContract = (node, helperName, addonContracts) => {
|
|
24
|
+
if (!ts.isCallExpression(node))
|
|
25
|
+
return null;
|
|
26
|
+
if (!ts.isIdentifier(node.expression) ||
|
|
27
|
+
node.expression.text !== helperName) {
|
|
28
|
+
return null;
|
|
29
|
+
}
|
|
30
|
+
const [arg, optionsArg] = node.arguments;
|
|
31
|
+
if (!arg || !ts.isStringLiteral(arg))
|
|
32
|
+
return null;
|
|
33
|
+
const separator = arg.text.indexOf(':');
|
|
34
|
+
if (separator === -1)
|
|
35
|
+
return null;
|
|
36
|
+
const namespace = arg.text.slice(0, separator);
|
|
37
|
+
const contractName = arg.text.slice(separator + 1);
|
|
38
|
+
const contract = addonContracts[namespace]?.[contractName];
|
|
39
|
+
if (contract === undefined)
|
|
40
|
+
return null;
|
|
41
|
+
let basePath;
|
|
42
|
+
if (optionsArg && ts.isObjectLiteralExpression(optionsArg)) {
|
|
43
|
+
basePath = getStringProperty(optionsArg, 'basePath');
|
|
44
|
+
}
|
|
45
|
+
return { contract, basePath };
|
|
46
|
+
};
|
|
@@ -267,6 +267,7 @@ export interface SerializableInspectorState {
|
|
|
267
267
|
openAPISpec: Record<string, any> | null;
|
|
268
268
|
diagnostics: InspectorDiagnostic[];
|
|
269
269
|
addonFunctions: InspectorState['addonFunctions'];
|
|
270
|
+
exportedContracts: InspectorState['exportedContracts'];
|
|
270
271
|
}
|
|
271
272
|
/**
|
|
272
273
|
* Serializes InspectorState to a JSON-compatible format
|
|
@@ -150,6 +150,7 @@ export function serializeInspectorState(state) {
|
|
|
150
150
|
openAPISpec: state.openAPISpec,
|
|
151
151
|
diagnostics: state.diagnostics,
|
|
152
152
|
addonFunctions: state.addonFunctions,
|
|
153
|
+
exportedContracts: state.exportedContracts,
|
|
153
154
|
};
|
|
154
155
|
}
|
|
155
156
|
/**
|
|
@@ -314,6 +315,14 @@ export function deserializeInspectorState(data) {
|
|
|
314
315
|
openAPISpec: data.openAPISpec || null,
|
|
315
316
|
diagnostics: data.diagnostics || [],
|
|
316
317
|
addonFunctions: data.addonFunctions || {},
|
|
318
|
+
exportedContracts: data.exportedContracts || {
|
|
319
|
+
http: {},
|
|
320
|
+
cli: {},
|
|
321
|
+
channel: {},
|
|
322
|
+
addonHttp: {},
|
|
323
|
+
addonCli: {},
|
|
324
|
+
addonChannel: {},
|
|
325
|
+
},
|
|
317
326
|
program: null,
|
|
318
327
|
};
|
|
319
328
|
}
|
|
@@ -255,6 +255,21 @@ function extractVariableDeclaration(statement, context) {
|
|
|
255
255
|
return step;
|
|
256
256
|
}
|
|
257
257
|
}
|
|
258
|
+
// Promise.all fanout/group captured into a variable
|
|
259
|
+
// (const results = await Promise.all(array.map(...)))
|
|
260
|
+
if (isParallelFanout(call) || isParallelGroup(call)) {
|
|
261
|
+
const step = isParallelFanout(call)
|
|
262
|
+
? extractParallelFanout(call, context)
|
|
263
|
+
: extractParallelGroup(call, context);
|
|
264
|
+
if (step) {
|
|
265
|
+
const type = context.checker.getTypeAtLocation(decl);
|
|
266
|
+
context.outputVars.set(varName, { type, node: decl });
|
|
267
|
+
if (isArrayType(type, context.checker)) {
|
|
268
|
+
context.arrayVars.add(varName);
|
|
269
|
+
}
|
|
270
|
+
return step;
|
|
271
|
+
}
|
|
272
|
+
}
|
|
258
273
|
}
|
|
259
274
|
// Check for array.filter(...)
|
|
260
275
|
if (ts.isCallExpression(init)) {
|
package/dist/visit.js
CHANGED
|
@@ -3,6 +3,7 @@ import { addFileWithFactory } from './add/add-file-with-factory.js';
|
|
|
3
3
|
import { addFileExtendsCoreType } from './add/add-file-extends-core-type.js';
|
|
4
4
|
import { addHTTPRoute } from './add/add-http-route.js';
|
|
5
5
|
import { addHTTPRoutes } from './add/add-http-routes.js';
|
|
6
|
+
import { checkAddonBans } from './add/add-addon-bans.js';
|
|
6
7
|
import { addSchedule } from './add/add-schedule.js';
|
|
7
8
|
import { addTrigger } from './add/add-trigger.js';
|
|
8
9
|
import { addQueueWorker } from './add/add-queue-worker.js';
|
|
@@ -41,23 +42,27 @@ export const visitSetup = (logger, checker, node, state, options) => {
|
|
|
41
42
|
ts.forEachChild(node, (child) => visitSetup(logger, checker, child, state, options));
|
|
42
43
|
};
|
|
43
44
|
export const visitRoutes = (logger, checker, node, state, options) => {
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
45
|
+
const nextOptions = ts.isSourceFile(node)
|
|
46
|
+
? { ...options, sourceFile: node }
|
|
47
|
+
: options;
|
|
48
|
+
checkAddonBans(logger, node, checker, state, nextOptions);
|
|
49
|
+
addFunctions(logger, node, checker, state, nextOptions);
|
|
50
|
+
addAuth(logger, node, checker, state, nextOptions);
|
|
51
|
+
addSecret(logger, node, checker, state, nextOptions);
|
|
52
|
+
addCredential(logger, node, checker, state, nextOptions);
|
|
53
|
+
addVariable(logger, node, checker, state, nextOptions);
|
|
54
|
+
addHTTPRoute(logger, node, checker, state, nextOptions);
|
|
55
|
+
addHTTPRoutes(logger, node, checker, state, nextOptions);
|
|
56
|
+
addSchedule(logger, node, checker, state, nextOptions);
|
|
57
|
+
addTrigger(logger, node, checker, state, nextOptions);
|
|
58
|
+
addQueueWorker(logger, node, checker, state, nextOptions);
|
|
59
|
+
addChannel(logger, node, checker, state, nextOptions);
|
|
60
|
+
addGateway(logger, node, checker, state, nextOptions);
|
|
61
|
+
addCLI(logger, node, checker, state, nextOptions);
|
|
62
|
+
addCLIRenderers(logger, node, checker, state, nextOptions);
|
|
63
|
+
addMCPResource(logger, node, checker, state, nextOptions);
|
|
64
|
+
addMCPPrompt(logger, node, checker, state, nextOptions);
|
|
65
|
+
addWorkflowGraph(logger, node, checker, state, nextOptions);
|
|
66
|
+
addAIAgent(logger, node, checker, state, nextOptions);
|
|
67
|
+
ts.forEachChild(node, (child) => visitRoutes(logger, checker, child, state, nextOptions));
|
|
63
68
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pikku/inspector",
|
|
3
|
-
"version": "0.12.
|
|
3
|
+
"version": "0.12.23",
|
|
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.35",
|
|
39
39
|
"openapi-types": "^12.1.3",
|
|
40
40
|
"path-to-regexp": "^8.3.0",
|
|
41
41
|
"ts-json-schema-generator": "^2.5.0",
|