@pikku/inspector 0.12.22 → 0.12.24
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 +40 -0
- package/dist/add/add-addon-bans.d.ts +7 -0
- package/dist/add/add-addon-bans.js +65 -0
- package/dist/add/add-auth.js +43 -0
- package/dist/add/add-channel.js +47 -6
- package/dist/add/add-cli.js +17 -0
- 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/error-codes.d.ts +3 -1
- package/dist/error-codes.js +3 -0
- package/dist/inspector.js +17 -5
- package/dist/types.d.ts +48 -1
- 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/post-process.js +16 -3
- 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/visit.js +24 -19
- package/package.json +1 -1
- package/src/add/add-addon-bans.ts +84 -0
- package/src/add/add-auth.test.ts +94 -0
- package/src/add/add-auth.ts +46 -0
- package/src/add/add-channel.ts +66 -7
- package/src/add/add-cli.ts +30 -0
- package/src/add/add-http-route.ts +75 -1
- package/src/add/add-http-routes.ts +283 -41
- package/src/add/addon-bans.test.ts +121 -0
- package/src/add/addon-contracts.test.ts +221 -0
- package/src/error-codes.ts +4 -0
- package/src/inspector.ts +17 -5
- package/src/types.ts +70 -1
- package/src/utils/get-exported-variable-name.ts +48 -0
- package/src/utils/load-addon-functions-meta.ts +164 -0
- package/src/utils/post-process.ts +17 -3
- package/src/utils/resolve-addon-package.ts +6 -1
- package/src/utils/resolve-ref-contract.ts +71 -0
- package/src/utils/serialize-inspector-state.ts +10 -0
- package/src/visit.ts +26 -19
- package/tsconfig.tsbuildinfo +1 -1
|
@@ -1,31 +1,40 @@
|
|
|
1
1
|
import * as ts from 'typescript';
|
|
2
2
|
import { getPropertyValue } from '../utils/get-property-value.js';
|
|
3
|
-
import { registerHTTPRoute } from './add-http-route.js';
|
|
3
|
+
import { registerHTTPRoute, registerHTTPRouteMeta } from './add-http-route.js';
|
|
4
4
|
import { resolveIdentifier } from '../utils/resolve-identifier.js';
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
5
|
+
import { extractFunctionName } from '../utils/extract-function-name.js';
|
|
6
|
+
import { getPropertyAssignmentInitializer } from '../utils/type-utils.js';
|
|
7
|
+
import { resolveAddonName } from '../utils/resolve-addon-package.js';
|
|
8
|
+
import { resolveRefContract, } from '../utils/resolve-ref-contract.js';
|
|
9
|
+
import { getExportedVariableName } from '../utils/get-exported-variable-name.js';
|
|
10
|
+
export const addHTTPRoutes = (logger, node, checker, state, options) => {
|
|
9
11
|
if (!ts.isCallExpression(node))
|
|
10
12
|
return;
|
|
11
13
|
const { expression, arguments: args } = node;
|
|
12
|
-
if (!ts.isIdentifier(expression)
|
|
14
|
+
if (!ts.isIdentifier(expression))
|
|
15
|
+
return;
|
|
16
|
+
if (expression.text === 'defineHTTPRoutes') {
|
|
17
|
+
const exportName = getExportedVariableName(node, options.sourceFile);
|
|
18
|
+
const firstArg = args[0];
|
|
19
|
+
if (exportName && firstArg && ts.isObjectLiteralExpression(firstArg)) {
|
|
20
|
+
const contract = serializeHTTPRoutesContract(firstArg, checker, state);
|
|
21
|
+
if (contract) {
|
|
22
|
+
state.exportedContracts.http[exportName] = contract;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
if (expression.text !== 'wireHTTPRoutes')
|
|
13
28
|
return;
|
|
14
29
|
const firstArg = args[0];
|
|
15
30
|
if (!firstArg || !ts.isObjectLiteralExpression(firstArg))
|
|
16
31
|
return;
|
|
17
|
-
// Extract group config
|
|
18
32
|
const groupConfig = extractGroupConfig(firstArg);
|
|
19
|
-
// Get routes property
|
|
20
33
|
const routesProp = getPropertyAssignment(firstArg, 'routes');
|
|
21
34
|
if (!routesProp)
|
|
22
35
|
return;
|
|
23
|
-
// Process routes recursively
|
|
24
36
|
processRoutes(routesProp.initializer, groupConfig, state, checker, logger, node.getSourceFile());
|
|
25
37
|
};
|
|
26
|
-
/**
|
|
27
|
-
* Get a property assignment from an object literal
|
|
28
|
-
*/
|
|
29
38
|
function getPropertyAssignment(obj, propName) {
|
|
30
39
|
for (const prop of obj.properties) {
|
|
31
40
|
if (ts.isPropertyAssignment(prop) &&
|
|
@@ -36,9 +45,6 @@ function getPropertyAssignment(obj, propName) {
|
|
|
36
45
|
}
|
|
37
46
|
return undefined;
|
|
38
47
|
}
|
|
39
|
-
/**
|
|
40
|
-
* Extract group configuration from an object literal
|
|
41
|
-
*/
|
|
42
48
|
function extractGroupConfig(obj) {
|
|
43
49
|
const basePath = getPropertyValue(obj, 'basePath') || '';
|
|
44
50
|
const tags = getPropertyValue(obj, 'tags') || [];
|
|
@@ -49,9 +55,6 @@ function extractGroupConfig(obj) {
|
|
|
49
55
|
auth: auth === true ? true : auth === false ? false : undefined,
|
|
50
56
|
};
|
|
51
57
|
}
|
|
52
|
-
/**
|
|
53
|
-
* Merge two group configs following cascading rules
|
|
54
|
-
*/
|
|
55
58
|
function mergeConfigs(parent, child) {
|
|
56
59
|
return {
|
|
57
60
|
basePath: parent.basePath + child.basePath,
|
|
@@ -59,9 +62,6 @@ function mergeConfigs(parent, child) {
|
|
|
59
62
|
auth: child.auth ?? parent.auth,
|
|
60
63
|
};
|
|
61
64
|
}
|
|
62
|
-
/**
|
|
63
|
-
* Check if a value is a route config (has method, func, and route)
|
|
64
|
-
*/
|
|
65
65
|
function isRouteConfig(obj) {
|
|
66
66
|
let hasMethod = false;
|
|
67
67
|
let hasFunc = false;
|
|
@@ -78,9 +78,6 @@ function isRouteConfig(obj) {
|
|
|
78
78
|
}
|
|
79
79
|
return hasMethod && hasFunc && hasRoute;
|
|
80
80
|
}
|
|
81
|
-
/**
|
|
82
|
-
* Check if a value is a route contract (has routes property but no method/func)
|
|
83
|
-
*/
|
|
84
81
|
function isRouteContract(obj) {
|
|
85
82
|
let hasRoutes = false;
|
|
86
83
|
let hasMethod = false;
|
|
@@ -97,11 +94,7 @@ function isRouteContract(obj) {
|
|
|
97
94
|
}
|
|
98
95
|
return hasRoutes && !hasMethod && !hasFunc;
|
|
99
96
|
}
|
|
100
|
-
/**
|
|
101
|
-
* Recursively process routes - handles nested maps, contracts, and identifiers
|
|
102
|
-
*/
|
|
103
97
|
function processRoutes(node, parentConfig, state, checker, logger, sourceFile) {
|
|
104
|
-
// Handle array of routes
|
|
105
98
|
if (ts.isArrayLiteralExpression(node)) {
|
|
106
99
|
for (const element of node.elements) {
|
|
107
100
|
if (ts.isObjectLiteralExpression(element) && isRouteConfig(element)) {
|
|
@@ -110,14 +103,11 @@ function processRoutes(node, parentConfig, state, checker, logger, sourceFile) {
|
|
|
110
103
|
}
|
|
111
104
|
return;
|
|
112
105
|
}
|
|
113
|
-
// Handle object literal
|
|
114
106
|
if (ts.isObjectLiteralExpression(node)) {
|
|
115
|
-
// Check if this is a route config
|
|
116
107
|
if (isRouteConfig(node)) {
|
|
117
108
|
processRoute(node, parentConfig, state, checker, logger, sourceFile);
|
|
118
109
|
return;
|
|
119
110
|
}
|
|
120
|
-
// Check if this is a route contract
|
|
121
111
|
if (isRouteContract(node)) {
|
|
122
112
|
const contractConfig = extractGroupConfig(node);
|
|
123
113
|
const mergedConfig = mergeConfigs(parentConfig, contractConfig);
|
|
@@ -127,15 +117,25 @@ function processRoutes(node, parentConfig, state, checker, logger, sourceFile) {
|
|
|
127
117
|
}
|
|
128
118
|
return;
|
|
129
119
|
}
|
|
130
|
-
// Otherwise it's a nested map - process each property
|
|
131
120
|
for (const prop of node.properties) {
|
|
132
121
|
if (ts.isPropertyAssignment(prop)) {
|
|
122
|
+
const ref = resolveRefContract(prop.initializer, 'refHTTP', state.exportedContracts.addonHttp);
|
|
123
|
+
if (ref) {
|
|
124
|
+
processRefHTTPContract(ref, parentConfig, state, logger, sourceFile);
|
|
125
|
+
continue;
|
|
126
|
+
}
|
|
133
127
|
processRoutes(prop.initializer, parentConfig, state, checker, logger, sourceFile);
|
|
134
128
|
}
|
|
135
129
|
}
|
|
136
130
|
return;
|
|
137
131
|
}
|
|
138
|
-
|
|
132
|
+
if (ts.isCallExpression(node)) {
|
|
133
|
+
const ref = resolveRefContract(node, 'refHTTP', state.exportedContracts.addonHttp);
|
|
134
|
+
if (ref) {
|
|
135
|
+
processRefHTTPContract(ref, parentConfig, state, logger, sourceFile);
|
|
136
|
+
}
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
139
|
if (ts.isIdentifier(node)) {
|
|
140
140
|
const resolved = resolveIdentifier(node, checker, ['defineHTTPRoutes']);
|
|
141
141
|
if (resolved) {
|
|
@@ -143,9 +143,152 @@ function processRoutes(node, parentConfig, state, checker, logger, sourceFile) {
|
|
|
143
143
|
}
|
|
144
144
|
}
|
|
145
145
|
}
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
146
|
+
function processRefHTTPContract(ref, parentConfig, state, logger, sourceFile) {
|
|
147
|
+
const basePath = ref.basePath !== undefined ? ref.basePath : ref.contract.basePath || '';
|
|
148
|
+
processExportedRouteMap(ref.contract.routes, mergeConfigs(parentConfig, {
|
|
149
|
+
basePath,
|
|
150
|
+
tags: ref.contract.tags || [],
|
|
151
|
+
auth: ref.contract.auth,
|
|
152
|
+
}), state, logger, sourceFile);
|
|
153
|
+
}
|
|
154
|
+
function processExportedRouteMap(routes, parentConfig, state, logger, sourceFile) {
|
|
155
|
+
for (const value of Object.values(routes)) {
|
|
156
|
+
if (isExportedRouteConfig(value)) {
|
|
157
|
+
registerHTTPRouteMeta({
|
|
158
|
+
route: value,
|
|
159
|
+
state,
|
|
160
|
+
logger,
|
|
161
|
+
sourceFile,
|
|
162
|
+
basePath: parentConfig.basePath,
|
|
163
|
+
inheritedTags: parentConfig.tags,
|
|
164
|
+
inheritedAuth: parentConfig.auth,
|
|
165
|
+
});
|
|
166
|
+
continue;
|
|
167
|
+
}
|
|
168
|
+
if (isExportedRouteContract(value)) {
|
|
169
|
+
processExportedRouteMap(value.routes, mergeConfigs(parentConfig, {
|
|
170
|
+
basePath: value.basePath || '',
|
|
171
|
+
tags: value.tags || [],
|
|
172
|
+
auth: value.auth,
|
|
173
|
+
}), state, logger, sourceFile);
|
|
174
|
+
continue;
|
|
175
|
+
}
|
|
176
|
+
processExportedRouteMap(value, parentConfig, state, logger, sourceFile);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
function isExportedRouteConfig(value) {
|
|
180
|
+
return (typeof value === 'object' &&
|
|
181
|
+
value !== null &&
|
|
182
|
+
'method' in value &&
|
|
183
|
+
'route' in value &&
|
|
184
|
+
'func' in value);
|
|
185
|
+
}
|
|
186
|
+
function isExportedRouteContract(value) {
|
|
187
|
+
return (typeof value === 'object' &&
|
|
188
|
+
value !== null &&
|
|
189
|
+
'routes' in value &&
|
|
190
|
+
!('method' in value));
|
|
191
|
+
}
|
|
192
|
+
function serializeHTTPRoutesContract(node, checker, state) {
|
|
193
|
+
if (isRouteContract(node)) {
|
|
194
|
+
const routesProp = getPropertyAssignment(node, 'routes');
|
|
195
|
+
if (!routesProp || !ts.isObjectLiteralExpression(routesProp.initializer)) {
|
|
196
|
+
return null;
|
|
197
|
+
}
|
|
198
|
+
return {
|
|
199
|
+
...extractGroupConfig(node),
|
|
200
|
+
routes: serializeHTTPRouteMap(routesProp.initializer, checker, state),
|
|
201
|
+
};
|
|
202
|
+
}
|
|
203
|
+
return {
|
|
204
|
+
routes: serializeHTTPRouteMap(node, checker, state),
|
|
205
|
+
};
|
|
206
|
+
}
|
|
207
|
+
function serializeHTTPRouteMap(node, checker, state) {
|
|
208
|
+
const result = {};
|
|
209
|
+
for (const prop of node.properties) {
|
|
210
|
+
if (!ts.isPropertyAssignment(prop))
|
|
211
|
+
continue;
|
|
212
|
+
const key = prop.name.getText().replace(/^['"]|['"]$/g, '');
|
|
213
|
+
const value = prop.initializer;
|
|
214
|
+
if (ts.isObjectLiteralExpression(value)) {
|
|
215
|
+
if (isRouteConfig(value)) {
|
|
216
|
+
const route = serializeHTTPRouteConfig(value, checker, state);
|
|
217
|
+
if (route) {
|
|
218
|
+
result[key] = route;
|
|
219
|
+
}
|
|
220
|
+
continue;
|
|
221
|
+
}
|
|
222
|
+
if (isRouteContract(value)) {
|
|
223
|
+
const routeContract = serializeHTTPRoutesContract(value, checker, state);
|
|
224
|
+
if (routeContract) {
|
|
225
|
+
result[key] = routeContract;
|
|
226
|
+
}
|
|
227
|
+
continue;
|
|
228
|
+
}
|
|
229
|
+
result[key] = serializeHTTPRouteMap(value, checker, state);
|
|
230
|
+
continue;
|
|
231
|
+
}
|
|
232
|
+
if (ts.isIdentifier(value)) {
|
|
233
|
+
const resolved = resolveIdentifier(value, checker, ['defineHTTPRoutes']);
|
|
234
|
+
if (resolved && ts.isObjectLiteralExpression(resolved)) {
|
|
235
|
+
if (isRouteContract(resolved)) {
|
|
236
|
+
const routeContract = serializeHTTPRoutesContract(resolved, checker, state);
|
|
237
|
+
if (routeContract) {
|
|
238
|
+
result[key] = routeContract;
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
else {
|
|
242
|
+
result[key] = serializeHTTPRouteMap(resolved, checker, state);
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
return result;
|
|
248
|
+
}
|
|
249
|
+
function serializeHTTPRouteConfig(obj, checker, state) {
|
|
250
|
+
const method = getPropertyValue(obj, 'method');
|
|
251
|
+
const route = getPropertyValue(obj, 'route');
|
|
252
|
+
const funcInitializer = getPropertyAssignmentInitializer(obj, 'func', true, checker);
|
|
253
|
+
if (!method || !route || !funcInitializer) {
|
|
254
|
+
return null;
|
|
255
|
+
}
|
|
256
|
+
let pikkuFuncId = extractFunctionName(funcInitializer, checker, state.rootDir).pikkuFuncId;
|
|
257
|
+
let packageName;
|
|
258
|
+
if (ts.isCallExpression(funcInitializer) &&
|
|
259
|
+
ts.isIdentifier(funcInitializer.expression) &&
|
|
260
|
+
funcInitializer.expression.text === 'ref') {
|
|
261
|
+
const [firstArg] = funcInitializer.arguments;
|
|
262
|
+
if (firstArg && ts.isStringLiteral(firstArg)) {
|
|
263
|
+
pikkuFuncId = firstArg.text;
|
|
264
|
+
const addonNamespace = pikkuFuncId.includes(':')
|
|
265
|
+
? pikkuFuncId.split(':')[0]
|
|
266
|
+
: null;
|
|
267
|
+
packageName = addonNamespace
|
|
268
|
+
? state.rpc.wireAddonDeclarations.get(addonNamespace)?.package
|
|
269
|
+
: undefined;
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
else if (ts.isIdentifier(funcInitializer)) {
|
|
273
|
+
packageName =
|
|
274
|
+
resolveAddonName(funcInitializer, checker, state.rpc.wireAddonDeclarations) || undefined;
|
|
275
|
+
}
|
|
276
|
+
return {
|
|
277
|
+
auth: getPropertyValue(obj, 'auth'),
|
|
278
|
+
contentType: getPropertyValue(obj, 'contentType'),
|
|
279
|
+
headers: getPropertyValue(obj, 'headers') ||
|
|
280
|
+
undefined,
|
|
281
|
+
method,
|
|
282
|
+
route,
|
|
283
|
+
sse: getPropertyValue(obj, 'sse'),
|
|
284
|
+
tags: getPropertyValue(obj, 'tags') || undefined,
|
|
285
|
+
timeout: getPropertyValue(obj, 'timeout'),
|
|
286
|
+
func: {
|
|
287
|
+
pikkuFuncId,
|
|
288
|
+
...(packageName && { packageName }),
|
|
289
|
+
},
|
|
290
|
+
};
|
|
291
|
+
}
|
|
149
292
|
function processRoute(obj, groupConfig, state, checker, logger, sourceFile) {
|
|
150
293
|
registerHTTPRoute({
|
|
151
294
|
obj,
|
package/dist/error-codes.d.ts
CHANGED
|
@@ -57,7 +57,9 @@ 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"
|
|
61
63
|
}
|
|
62
64
|
/**
|
|
63
65
|
* Severity of a tracked, coded diagnostic. `critical` always blocks the build;
|
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/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,14 +210,14 @@ 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();
|
|
212
222
|
// node_modules under rootDir (e.g. a locally-installed addon) is a
|
|
213
223
|
// dependency, not project source — scanning it double-counts the addon's
|
|
@@ -221,7 +231,8 @@ export const inspect = async (logger, routeFiles, options = {}) => {
|
|
|
221
231
|
// First sweep: add all functions
|
|
222
232
|
const startSetup = performance.now();
|
|
223
233
|
for (const sourceFile of sourceFiles) {
|
|
224
|
-
|
|
234
|
+
const sourceOptions = { ...options, sourceFile };
|
|
235
|
+
ts.forEachChild(sourceFile, (child) => visitSetup(logger, checker, child, state, sourceOptions));
|
|
225
236
|
}
|
|
226
237
|
logger.debug(`Visit setup phase completed in ${(performance.now() - startSetup).toFixed(0)}ms`);
|
|
227
238
|
// Load addon function metadata so wirings can reference addon functions
|
|
@@ -230,7 +241,8 @@ export const inspect = async (logger, routeFiles, options = {}) => {
|
|
|
230
241
|
// Second sweep: add all transports
|
|
231
242
|
const startRoutes = performance.now();
|
|
232
243
|
for (const sourceFile of sourceFiles) {
|
|
233
|
-
|
|
244
|
+
const sourceOptions = { ...options, sourceFile };
|
|
245
|
+
ts.forEachChild(sourceFile, (child) => visitRoutes(logger, checker, child, state, sourceOptions));
|
|
234
246
|
}
|
|
235
247
|
logger.debug(`Visit routes phase completed in ${(performance.now() - startRoutes).toFixed(0)}ms`);
|
|
236
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';
|
|
@@ -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;
|
|
@@ -390,6 +431,11 @@ export interface InspectorState {
|
|
|
390
431
|
* codebase, if any. The CLI generates the `/auth/*` HTTP wiring from it.
|
|
391
432
|
* More than one `pikkuBetterAuth` is a critical error. */
|
|
392
433
|
definition: AuthDefinition | null;
|
|
434
|
+
/** True when a user (non-generated) file already registers
|
|
435
|
+
* `betterAuthStatelessSession(...)`. The CLI then skips auto-generating its
|
|
436
|
+
* own default-map stateless middleware, which would otherwise pre-empt the
|
|
437
|
+
* user's custom mapSession (pikkujs/pikku#754). */
|
|
438
|
+
userStatelessSession?: boolean;
|
|
393
439
|
};
|
|
394
440
|
secrets: {
|
|
395
441
|
definitions: SecretDefinitions;
|
|
@@ -444,5 +490,6 @@ export interface InspectorState {
|
|
|
444
490
|
openAPISpec: Record<string, any> | null;
|
|
445
491
|
diagnostics: InspectorDiagnostic[];
|
|
446
492
|
addonFunctions: Record<string, FunctionsMeta>;
|
|
493
|
+
exportedContracts: InspectorExportedContractsState;
|
|
447
494
|
program: ts.Program | null;
|
|
448
495
|
}
|
|
@@ -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}`);
|
|
@@ -245,9 +245,22 @@ export function aggregateRequiredServices(state) {
|
|
|
245
245
|
if (Object.keys(state.channels.meta).length > 0) {
|
|
246
246
|
requiredServices.add('eventHub');
|
|
247
247
|
}
|
|
248
|
-
// 7. Services that addons need from the parent project
|
|
249
|
-
|
|
250
|
-
|
|
248
|
+
// 7. Services that consumed addons need from the parent project.
|
|
249
|
+
// These are required ONLY by units that actually deploy an addon function;
|
|
250
|
+
// a unit that merely calls the addon over RPC (or never touches it) must not
|
|
251
|
+
// carry them, or every per-unit bundle would over-include the addon's
|
|
252
|
+
// parent-service dependencies (e.g. aiAgentRunner, deploymentService) and
|
|
253
|
+
// defeat per-unit tree-shaking.
|
|
254
|
+
const addonFuncIds = new Set();
|
|
255
|
+
for (const fns of Object.values(state.addonFunctions ?? {})) {
|
|
256
|
+
for (const id of Object.keys(fns))
|
|
257
|
+
addonFuncIds.add(id);
|
|
258
|
+
}
|
|
259
|
+
const unitDeploysAddonFn = [...usedFunctions].some((fn) => addonFuncIds.has(fn));
|
|
260
|
+
if (unitDeploysAddonFn) {
|
|
261
|
+
for (const service of state.addonRequiredParentServices ?? []) {
|
|
262
|
+
requiredServices.add(service);
|
|
263
|
+
}
|
|
251
264
|
}
|
|
252
265
|
}
|
|
253
266
|
export function validateSecretOverrides(logger, state) {
|
|
@@ -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.
|