@pikku/inspector 0.12.103 → 0.12.107
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/dist/add/add-auth.d.ts +1 -9
- package/dist/add/add-auth.js +64 -17
- package/dist/add/add-cli.js +25 -11
- package/dist/add/add-functions.js +35 -39
- package/dist/add/add-keyed-wiring.js +22 -2
- package/dist/add/add-middleware.js +17 -16
- package/dist/add/add-permission.d.ts +0 -3
- package/dist/add/add-permission.js +213 -22
- package/dist/add/add-scope.d.ts +1 -7
- package/dist/add/add-scope.js +1 -21
- package/dist/add/add-virtual-users.d.ts +13 -0
- package/dist/add/{add-personas.js → add-virtual-users.js} +33 -33
- package/dist/add/add-workflow.js +14 -10
- package/dist/add/check-indirect-registrations.d.ts +9 -0
- package/dist/add/check-indirect-registrations.js +114 -0
- package/dist/error-codes.d.ts +5 -3
- package/dist/error-codes.js +5 -3
- package/dist/finalize.d.ts +10 -0
- package/dist/finalize.js +102 -0
- package/dist/index.d.ts +8 -6
- package/dist/index.js +6 -5
- package/dist/initial-state.d.ts +7 -0
- package/dist/initial-state.js +216 -0
- package/dist/inspector.d.ts +6 -4
- package/dist/inspector.js +28 -300
- package/dist/lite.d.ts +26 -0
- package/dist/lite.js +25 -0
- package/dist/source-file-cache.d.ts +11 -0
- package/dist/source-file-cache.js +34 -10
- package/dist/types-map.js +2 -1
- package/dist/types.d.ts +35 -3
- package/dist/utils/apply-declared-scopes.d.ts +8 -0
- package/dist/utils/apply-declared-scopes.js +21 -0
- package/dist/utils/canonical-type-string.d.ts +14 -0
- package/dist/utils/canonical-type-string.js +162 -0
- package/dist/utils/context-based-id.d.ts +7 -0
- package/dist/utils/context-based-id.js +9 -0
- package/dist/utils/custom-types-generator.d.ts +15 -1
- package/dist/utils/custom-types-generator.js +17 -5
- package/dist/utils/ensure-function-metadata.d.ts +0 -14
- package/dist/utils/ensure-function-metadata.js +4 -1
- package/dist/utils/extract-function-name.d.ts +1 -1
- package/dist/utils/extract-function-name.js +2 -4
- package/dist/utils/filter-inspector-state.js +31 -1
- package/dist/utils/get-property-value.d.ts +5 -0
- package/dist/utils/get-property-value.js +15 -1
- package/dist/utils/inspector-constants.d.ts +16 -0
- package/dist/utils/inspector-constants.js +16 -0
- package/dist/utils/load-addon-functions-meta.js +1 -0
- package/dist/utils/post-process.d.ts +35 -36
- package/dist/utils/post-process.js +218 -193
- package/dist/utils/resolve-callee-name.d.ts +27 -0
- package/dist/utils/resolve-callee-name.js +147 -0
- package/dist/utils/resolve-service-types.d.ts +8 -0
- package/dist/utils/resolve-service-types.js +21 -0
- package/dist/utils/schema-generator.js +29 -2
- package/dist/utils/serialize-inspector-state.d.ts +9 -3
- package/dist/utils/serialize-inspector-state.js +22 -7
- package/dist/utils/single-declaration.d.ts +3 -3
- package/dist/utils/single-declaration.js +3 -3
- package/dist/utils/validate-single-step-workflows.d.ts +3 -0
- package/dist/utils/validate-single-step-workflows.js +32 -0
- package/dist/utils/workflow/dsl/extract-dsl-workflow.js +3 -3
- package/dist/utils/workflow/dsl/patterns.d.ts +4 -11
- package/dist/utils/workflow/dsl/patterns.js +5 -11
- package/dist/utils/workflow/graph/convert-dsl-to-graph.js +6 -6
- package/dist/utils/workflow/graph/workflow-graph.types.d.ts +5 -5
- package/dist/visit.js +4 -2
- package/package.json +25 -9
- package/CHANGELOG.md +0 -4318
- package/OPTIMIZATION-PLAN.md +0 -201
- package/dist/add/add-personas.d.ts +0 -13
- package/run-tests.sh +0 -68
- package/src/utils/test-data/inspector-state.json +0 -1687
|
@@ -1,14 +1,16 @@
|
|
|
1
1
|
import * as ts from 'typescript';
|
|
2
|
-
import {
|
|
2
|
+
import { relative } from 'node:path';
|
|
3
|
+
import { extractFunctionName, isNamedExport, makeContextBasedId, } from '../utils/extract-function-name.js';
|
|
3
4
|
import { extractServicesFromFunction, extractUsedWires, } from '../utils/extract-services.js';
|
|
4
5
|
import { getPropertyValue } from '../utils/get-property-value.js';
|
|
5
6
|
import { getPropertyAssignmentInitializer } from '../utils/type-utils.js';
|
|
6
|
-
|
|
7
|
+
import { PERMISSION_HELPERS, resolveCalleeName, } from '../utils/resolve-callee-name.js';
|
|
8
|
+
function isInsidePermissionContainer(node, checker) {
|
|
7
9
|
let current = node.parent;
|
|
8
10
|
while (current) {
|
|
9
11
|
if (ts.isCallExpression(current) &&
|
|
10
|
-
|
|
11
|
-
|
|
12
|
+
resolveCalleeName(current, checker, PERMISSION_HELPERS) ===
|
|
13
|
+
'pikkuPermissionFactory') {
|
|
12
14
|
return true;
|
|
13
15
|
}
|
|
14
16
|
current = current.parent;
|
|
@@ -16,20 +18,144 @@ function isInsidePermissionContainer(node) {
|
|
|
16
18
|
return false;
|
|
17
19
|
}
|
|
18
20
|
/**
|
|
19
|
-
*
|
|
21
|
+
* The id of a permission written inline in an `addGlobalPermission([...])`
|
|
22
|
+
* array, which has no variable to be named after. Derived from where it sits,
|
|
23
|
+
* so the registration and the definition agree on it without either having to
|
|
24
|
+
* be visited first.
|
|
20
25
|
*/
|
|
26
|
+
function inlineGlobalPermissionId(node, checker, rootDir) {
|
|
27
|
+
const array = node.parent;
|
|
28
|
+
if (!array || !ts.isArrayLiteralExpression(array))
|
|
29
|
+
return undefined;
|
|
30
|
+
const call = array.parent;
|
|
31
|
+
if (!call ||
|
|
32
|
+
!ts.isCallExpression(call) ||
|
|
33
|
+
call.arguments[0] !== array ||
|
|
34
|
+
resolveCalleeName(call, checker, PERMISSION_HELPERS) !==
|
|
35
|
+
'addGlobalPermission') {
|
|
36
|
+
return undefined;
|
|
37
|
+
}
|
|
38
|
+
const sourceFile = node.getSourceFile();
|
|
39
|
+
return makeContextBasedId('global', 'permission', relative(rootDir, sourceFile.fileName), String(node.getStart()));
|
|
40
|
+
}
|
|
41
|
+
const unwrapExpression = (expression) => {
|
|
42
|
+
let current = expression;
|
|
43
|
+
while (ts.isParenthesizedExpression(current) ||
|
|
44
|
+
ts.isAsExpression(current) ||
|
|
45
|
+
ts.isSatisfiesExpression(current) ||
|
|
46
|
+
ts.isNonNullExpression(current) ||
|
|
47
|
+
ts.isTypeAssertionExpression(current)) {
|
|
48
|
+
current = current.expression;
|
|
49
|
+
}
|
|
50
|
+
return current;
|
|
51
|
+
};
|
|
52
|
+
/**
|
|
53
|
+
* Follows `addGlobalPermission`'s argument to the array or object literal it was
|
|
54
|
+
* written as: a literal itself, or a `const` (declared here or imported) that is
|
|
55
|
+
* initialised with one. `undefined` when it is built any other way.
|
|
56
|
+
*/
|
|
57
|
+
const resolvePermissionLiteral = (argument, checker) => {
|
|
58
|
+
let current = unwrapExpression(argument);
|
|
59
|
+
for (let hops = 0; hops < 8; hops++) {
|
|
60
|
+
if (ts.isArrayLiteralExpression(current) ||
|
|
61
|
+
ts.isObjectLiteralExpression(current)) {
|
|
62
|
+
return current;
|
|
63
|
+
}
|
|
64
|
+
if (!ts.isIdentifier(current))
|
|
65
|
+
return undefined;
|
|
66
|
+
let symbol = checker.getSymbolAtLocation(current);
|
|
67
|
+
if (symbol && symbol.flags & ts.SymbolFlags.Alias) {
|
|
68
|
+
symbol = checker.getAliasedSymbol(symbol);
|
|
69
|
+
}
|
|
70
|
+
const declaration = symbol?.valueDeclaration;
|
|
71
|
+
if (!declaration ||
|
|
72
|
+
!ts.isVariableDeclaration(declaration) ||
|
|
73
|
+
!declaration.initializer) {
|
|
74
|
+
return undefined;
|
|
75
|
+
}
|
|
76
|
+
current = unwrapExpression(declaration.initializer);
|
|
77
|
+
}
|
|
78
|
+
return undefined;
|
|
79
|
+
};
|
|
80
|
+
const isNamedExportedFunction = (declaration, checker) => {
|
|
81
|
+
if (!declaration.name)
|
|
82
|
+
return false;
|
|
83
|
+
const flags = ts.getCombinedModifierFlags(declaration);
|
|
84
|
+
if (flags & ts.ModifierFlags.Default)
|
|
85
|
+
return false;
|
|
86
|
+
if (flags & ts.ModifierFlags.Export)
|
|
87
|
+
return true;
|
|
88
|
+
const moduleSymbol = checker.getSymbolAtLocation(declaration.getSourceFile());
|
|
89
|
+
const symbol = checker.getSymbolAtLocation(declaration.name);
|
|
90
|
+
return (!!moduleSymbol &&
|
|
91
|
+
!!symbol &&
|
|
92
|
+
checker.getExportsOfModule(moduleSymbol).some((exported) => {
|
|
93
|
+
const resolved = exported.flags & ts.SymbolFlags.Alias
|
|
94
|
+
? checker.getAliasedSymbol(exported)
|
|
95
|
+
: exported;
|
|
96
|
+
return resolved === symbol;
|
|
97
|
+
}));
|
|
98
|
+
};
|
|
99
|
+
const isImmediatelyInvoked = (fn) => {
|
|
100
|
+
let current = fn;
|
|
101
|
+
while (current.parent && ts.isParenthesizedExpression(current.parent)) {
|
|
102
|
+
current = current.parent;
|
|
103
|
+
}
|
|
104
|
+
return (!!current.parent &&
|
|
105
|
+
ts.isCallExpression(current.parent) &&
|
|
106
|
+
current.parent.expression === current);
|
|
107
|
+
};
|
|
108
|
+
/**
|
|
109
|
+
* How the generated permissions file makes this `addGlobalPermission` call run.
|
|
110
|
+
* Importing a file only evaluates its top level, so a call at the top level (or
|
|
111
|
+
* in a function invoked right where it is written) runs on import. A call inside
|
|
112
|
+
* an exported zero-argument function runs when the generated file calls that
|
|
113
|
+
* function. Anywhere else nothing ever calls it, so the gate would not exist.
|
|
114
|
+
*/
|
|
115
|
+
const globalPermissionActivation = (call, checker) => {
|
|
116
|
+
let current = call.parent;
|
|
117
|
+
while (current && !ts.isSourceFile(current)) {
|
|
118
|
+
if (ts.isArrowFunction(current) ||
|
|
119
|
+
ts.isFunctionExpression(current) ||
|
|
120
|
+
ts.isFunctionDeclaration(current) ||
|
|
121
|
+
ts.isMethodDeclaration(current) ||
|
|
122
|
+
ts.isConstructorDeclaration(current) ||
|
|
123
|
+
ts.isGetAccessorDeclaration(current) ||
|
|
124
|
+
ts.isSetAccessorDeclaration(current)) {
|
|
125
|
+
if (isImmediatelyInvoked(current)) {
|
|
126
|
+
current = current.parent;
|
|
127
|
+
continue;
|
|
128
|
+
}
|
|
129
|
+
if (current.parameters.length > 0)
|
|
130
|
+
return { kind: 'unreachable' };
|
|
131
|
+
if ((ts.isArrowFunction(current) || ts.isFunctionExpression(current)) &&
|
|
132
|
+
ts.isVariableDeclaration(current.parent) &&
|
|
133
|
+
ts.isIdentifier(current.parent.name) &&
|
|
134
|
+
isNamedExport(current.parent, checker)) {
|
|
135
|
+
return { kind: 'factory', exportName: current.parent.name.text };
|
|
136
|
+
}
|
|
137
|
+
if (ts.isFunctionDeclaration(current) &&
|
|
138
|
+
current.name &&
|
|
139
|
+
isNamedExportedFunction(current, checker)) {
|
|
140
|
+
return { kind: 'factory', exportName: current.name.text };
|
|
141
|
+
}
|
|
142
|
+
return { kind: 'unreachable' };
|
|
143
|
+
}
|
|
144
|
+
current = current.parent;
|
|
145
|
+
}
|
|
146
|
+
return { kind: 'import' };
|
|
147
|
+
};
|
|
21
148
|
export const addPermission = (logger, node, checker, state) => {
|
|
22
149
|
if (!ts.isCallExpression(node))
|
|
23
150
|
return;
|
|
24
|
-
const {
|
|
25
|
-
|
|
26
|
-
if (!
|
|
151
|
+
const { arguments: args } = node;
|
|
152
|
+
const callee = resolveCalleeName(node, checker, PERMISSION_HELPERS);
|
|
153
|
+
if (!callee)
|
|
27
154
|
return;
|
|
28
|
-
}
|
|
29
155
|
// Handle pikkuPermission(...) - individual permission function definition
|
|
30
|
-
if (
|
|
156
|
+
if (callee === 'pikkuPermission') {
|
|
31
157
|
// Skip if nested inside pikkuPermissionFactory — the factory handler extracts services itself
|
|
32
|
-
if (isInsidePermissionContainer(node))
|
|
158
|
+
if (isInsidePermissionContainer(node, checker))
|
|
33
159
|
return;
|
|
34
160
|
const arg = args[0];
|
|
35
161
|
if (!arg)
|
|
@@ -70,9 +196,13 @@ export const addPermission = (logger, node, checker, state) => {
|
|
|
70
196
|
pikkuFuncId = node.parent.name.text;
|
|
71
197
|
}
|
|
72
198
|
else {
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
199
|
+
const globalId = inlineGlobalPermissionId(node, checker, state.rootDir);
|
|
200
|
+
if (!globalId) {
|
|
201
|
+
logger.error(`• pikkuPermission() must be assigned to a variable or object property. ` +
|
|
202
|
+
`Extract it to a const: const myPermission = pikkuPermission(...)`);
|
|
203
|
+
return;
|
|
204
|
+
}
|
|
205
|
+
pikkuFuncId = globalId;
|
|
76
206
|
}
|
|
77
207
|
}
|
|
78
208
|
const dataParam = actualHandler.parameters[1];
|
|
@@ -91,8 +221,8 @@ export const addPermission = (logger, node, checker, state) => {
|
|
|
91
221
|
logger.debug(`• Found permission with services: ${services.services.join(', ')}${name ? ` (name: ${name})` : ''}${description ? ` (description: ${description})` : ''}${!requiresData ? ' (auth-only)' : ''}`);
|
|
92
222
|
return;
|
|
93
223
|
}
|
|
94
|
-
if (
|
|
95
|
-
if (isInsidePermissionContainer(node))
|
|
224
|
+
if (callee === 'pikkuAuth') {
|
|
225
|
+
if (isInsidePermissionContainer(node, checker))
|
|
96
226
|
return;
|
|
97
227
|
const arg = args[0];
|
|
98
228
|
if (!arg)
|
|
@@ -137,9 +267,13 @@ export const addPermission = (logger, node, checker, state) => {
|
|
|
137
267
|
pikkuFuncId = node.parent.name.text;
|
|
138
268
|
}
|
|
139
269
|
else {
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
270
|
+
const globalId = inlineGlobalPermissionId(node, checker, state.rootDir);
|
|
271
|
+
if (!globalId) {
|
|
272
|
+
logger.error(`• pikkuAuth() must be assigned to a variable or object property. ` +
|
|
273
|
+
`Extract it to a const: const myAuth = pikkuAuth(...)`);
|
|
274
|
+
return;
|
|
275
|
+
}
|
|
276
|
+
pikkuFuncId = globalId;
|
|
143
277
|
}
|
|
144
278
|
}
|
|
145
279
|
state.permissions.definitions[pikkuFuncId] = {
|
|
@@ -156,7 +290,7 @@ export const addPermission = (logger, node, checker, state) => {
|
|
|
156
290
|
return;
|
|
157
291
|
}
|
|
158
292
|
// Handle pikkuPermissionFactory(...) - permission factory function
|
|
159
|
-
if (
|
|
293
|
+
if (callee === 'pikkuPermissionFactory') {
|
|
160
294
|
const factoryNode = args[0];
|
|
161
295
|
if (!factoryNode)
|
|
162
296
|
return;
|
|
@@ -175,8 +309,8 @@ export const addPermission = (logger, node, checker, state) => {
|
|
|
175
309
|
};
|
|
176
310
|
const findPikkuPermissionCall = (node) => {
|
|
177
311
|
if (ts.isCallExpression(node)) {
|
|
178
|
-
|
|
179
|
-
|
|
312
|
+
if (resolveCalleeName(node, checker, PERMISSION_HELPERS) ===
|
|
313
|
+
'pikkuPermission') {
|
|
180
314
|
return node;
|
|
181
315
|
}
|
|
182
316
|
}
|
|
@@ -229,4 +363,61 @@ export const addPermission = (logger, node, checker, state) => {
|
|
|
229
363
|
logger.debug(`• Found permission factory with services: ${services.services.join(', ')}`);
|
|
230
364
|
return;
|
|
231
365
|
}
|
|
366
|
+
if (callee === 'addGlobalPermission') {
|
|
367
|
+
const permissionsArg = args[0];
|
|
368
|
+
const sourceFile = node.getSourceFile().fileName;
|
|
369
|
+
let entries;
|
|
370
|
+
const literal = permissionsArg && resolvePermissionLiteral(permissionsArg, checker);
|
|
371
|
+
if (literal && ts.isArrayLiteralExpression(literal)) {
|
|
372
|
+
entries = literal.elements;
|
|
373
|
+
}
|
|
374
|
+
else if (literal) {
|
|
375
|
+
entries = literal.properties.flatMap((p) => ts.isPropertyAssignment(p)
|
|
376
|
+
? [p.initializer]
|
|
377
|
+
: ts.isShorthandPropertyAssignment(p)
|
|
378
|
+
? [p.name]
|
|
379
|
+
: []);
|
|
380
|
+
}
|
|
381
|
+
if (!entries) {
|
|
382
|
+
logger.warn(`• addGlobalPermission(...) in ${sourceFile} is not given an array or object it can follow to a literal, so the services its permissions use cannot be seen.`);
|
|
383
|
+
}
|
|
384
|
+
const definitionIds = (entries ?? []).flatMap((entry) => {
|
|
385
|
+
if (ts.isIdentifier(entry)) {
|
|
386
|
+
const { pikkuFuncId } = extractFunctionName(entry, checker, state.rootDir);
|
|
387
|
+
return [pikkuFuncId.startsWith('__temp_') ? entry.text : pikkuFuncId];
|
|
388
|
+
}
|
|
389
|
+
if (ts.isCallExpression(entry)) {
|
|
390
|
+
const inline = inlineGlobalPermissionId(entry, checker, state.rootDir);
|
|
391
|
+
if (inline)
|
|
392
|
+
return [inline];
|
|
393
|
+
if (ts.isPropertyAssignment(entry.parent) &&
|
|
394
|
+
ts.isIdentifier(entry.parent.name) &&
|
|
395
|
+
resolveCalleeName(entry, checker, PERMISSION_HELPERS)) {
|
|
396
|
+
return [entry.parent.name.text];
|
|
397
|
+
}
|
|
398
|
+
const { pikkuFuncId } = extractFunctionName(entry.expression, checker, state.rootDir);
|
|
399
|
+
return pikkuFuncId.startsWith('__temp_') &&
|
|
400
|
+
ts.isIdentifier(entry.expression)
|
|
401
|
+
? [entry.expression.text]
|
|
402
|
+
: [pikkuFuncId];
|
|
403
|
+
}
|
|
404
|
+
return [];
|
|
405
|
+
});
|
|
406
|
+
const activation = globalPermissionActivation(node, checker);
|
|
407
|
+
if (activation.kind === 'unreachable') {
|
|
408
|
+
logger.warn(`• addGlobalPermission(...) in ${sourceFile} sits inside a function that nothing calls: only a call at the top level, or inside an exported function with no parameters, is run by the generated permissions file. This global permission is NOT registered. Export it as \`export const name = () => { addGlobalPermission(...) }\` or move it to the top level.`);
|
|
409
|
+
return;
|
|
410
|
+
}
|
|
411
|
+
const exportName = activation.kind === 'factory' ? activation.exportName : null;
|
|
412
|
+
const position = node.getStart();
|
|
413
|
+
if (!state.permissions.global.some((g) => g.sourceFile === sourceFile && g.position === position)) {
|
|
414
|
+
state.permissions.global.push({
|
|
415
|
+
sourceFile,
|
|
416
|
+
position,
|
|
417
|
+
exportName,
|
|
418
|
+
definitionIds,
|
|
419
|
+
});
|
|
420
|
+
}
|
|
421
|
+
logger.debug(`• Found global permissions in ${sourceFile}: [${definitionIds.join(', ')}]`);
|
|
422
|
+
}
|
|
232
423
|
};
|
package/dist/add/add-scope.d.ts
CHANGED
|
@@ -1,9 +1,3 @@
|
|
|
1
1
|
import type { AddWiring } from '../types.js';
|
|
2
|
-
|
|
3
|
-
* Folds `declareScopes` ids into `state.scopes.definitions`. An id the
|
|
4
|
-
* definitions already hold (an addon's tree, a `defineScope`) is left alone;
|
|
5
|
-
* a missing one is added as a bare node under its root, creating the root when
|
|
6
|
-
* nothing declared it.
|
|
7
|
-
*/
|
|
8
|
-
export declare const applyDeclaredScopes: (state: Parameters<AddWiring>[3]) => void;
|
|
2
|
+
export { applyDeclaredScopes } from '../utils/apply-declared-scopes.js';
|
|
9
3
|
export declare const addScope: AddWiring;
|
package/dist/add/add-scope.js
CHANGED
|
@@ -111,27 +111,7 @@ const addDeclareScopes = (logger, node, state) => {
|
|
|
111
111
|
}
|
|
112
112
|
state.scopes.files.add(sourceFile);
|
|
113
113
|
};
|
|
114
|
-
|
|
115
|
-
* Folds `declareScopes` ids into `state.scopes.definitions`. An id the
|
|
116
|
-
* definitions already hold (an addon's tree, a `defineScope`) is left alone;
|
|
117
|
-
* a missing one is added as a bare node under its root, creating the root when
|
|
118
|
-
* nothing declared it.
|
|
119
|
-
*/
|
|
120
|
-
export const applyDeclaredScopes = (state) => {
|
|
121
|
-
for (const [id, sourceFile] of state.scopes.declared ?? []) {
|
|
122
|
-
const [rootName, ...rest] = id.split(SEPARATOR);
|
|
123
|
-
let root = state.scopes.definitions.find((d) => d.name === rootName);
|
|
124
|
-
if (!root) {
|
|
125
|
-
root = { name: rootName, sourceFile };
|
|
126
|
-
state.scopes.definitions.push(root);
|
|
127
|
-
}
|
|
128
|
-
let level = root;
|
|
129
|
-
for (const segment of rest) {
|
|
130
|
-
level.scopes ??= {};
|
|
131
|
-
level = level.scopes[segment] ??= {};
|
|
132
|
-
}
|
|
133
|
-
}
|
|
134
|
-
};
|
|
114
|
+
export { applyDeclaredScopes } from '../utils/apply-declared-scopes.js';
|
|
135
115
|
export const addScope = (logger, node, checker, state, _options) => {
|
|
136
116
|
if (!ts.isCallExpression(node)) {
|
|
137
117
|
return;
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { AddWiring } from '../types.js';
|
|
2
|
+
/**
|
|
3
|
+
* Inspector for `defineVirtualUsers()` calls.
|
|
4
|
+
*
|
|
5
|
+
* A virtual user is entirely declaration — a person, their roles, what they want —
|
|
6
|
+
* so there is no code body and nothing to resolve at runtime. What is read here
|
|
7
|
+
* is the whole thing, and it is generated straight to `virtual-users.gen.json` for
|
|
8
|
+
* the CLI and the console to read without loading the app.
|
|
9
|
+
*
|
|
10
|
+
* `name` is the one field that cannot be defaulted. A virtual user without one is
|
|
11
|
+
* not a person, and the console has nothing to show.
|
|
12
|
+
*/
|
|
13
|
+
export declare const addVirtualUsers: AddWiring;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import * as ts from 'typescript';
|
|
2
2
|
import { DISPOSITIONS } from '@pikku/core/virtual-user';
|
|
3
|
-
import {
|
|
3
|
+
import { isRunnableVirtualUser } from '@pikku/core/virtual-user';
|
|
4
4
|
import { ErrorCode } from '../error-codes.js';
|
|
5
5
|
import { claimSingleDeclaration } from '../utils/single-declaration.js';
|
|
6
6
|
import { booleanProperty, getProperty, numberProperty, stringArrayProperty, stringProperty, unwrapAs, } from '../utils/literal-properties.js';
|
|
@@ -15,7 +15,7 @@ const RATES = [
|
|
|
15
15
|
*
|
|
16
16
|
* The ranges are checked here rather than at run time because these are the
|
|
17
17
|
* kind of typo that does not throw: `repeatRate: 18` meaning 18% would silently
|
|
18
|
-
* double every call the
|
|
18
|
+
* double every call the virtual user makes, and you would read the run as a product
|
|
19
19
|
* bug.
|
|
20
20
|
*/
|
|
21
21
|
const tuningProperty = (config, onError) => {
|
|
@@ -72,7 +72,7 @@ const accountProperty = (value) => {
|
|
|
72
72
|
const provider = stringProperty(value, 'provider');
|
|
73
73
|
return provider === undefined ? {} : { provider };
|
|
74
74
|
};
|
|
75
|
-
const linkedAccountsProperty = (config,
|
|
75
|
+
const linkedAccountsProperty = (config, virtualUserId, logger) => {
|
|
76
76
|
const value = getProperty(config, 'linkedAccounts');
|
|
77
77
|
if (!value || !ts.isObjectLiteralExpression(value)) {
|
|
78
78
|
return undefined;
|
|
@@ -83,12 +83,12 @@ const linkedAccountsProperty = (config, personaId, logger) => {
|
|
|
83
83
|
continue;
|
|
84
84
|
}
|
|
85
85
|
if (!ts.isIdentifier(prop.name) && !ts.isStringLiteral(prop.name)) {
|
|
86
|
-
logger.critical(ErrorCode.NON_LITERAL_WIRE_NAME, `
|
|
86
|
+
logger.critical(ErrorCode.NON_LITERAL_WIRE_NAME, `VirtualUser '${virtualUserId}' has a linked account whose key is not a literal.`);
|
|
87
87
|
continue;
|
|
88
88
|
}
|
|
89
89
|
const account = accountProperty(prop.initializer);
|
|
90
90
|
if (!account) {
|
|
91
|
-
logger.critical(ErrorCode.INVALID_VALUE, `
|
|
91
|
+
logger.critical(ErrorCode.INVALID_VALUE, `VirtualUser '${virtualUserId}' linked account '${prop.name.text}' must be an object literal.`);
|
|
92
92
|
continue;
|
|
93
93
|
}
|
|
94
94
|
accounts[prop.name.text] = account;
|
|
@@ -96,22 +96,22 @@ const linkedAccountsProperty = (config, personaId, logger) => {
|
|
|
96
96
|
return Object.keys(accounts).length ? accounts : undefined;
|
|
97
97
|
};
|
|
98
98
|
/**
|
|
99
|
-
* Inspector for `
|
|
99
|
+
* Inspector for `defineVirtualUsers()` calls.
|
|
100
100
|
*
|
|
101
|
-
* A
|
|
101
|
+
* A virtual user is entirely declaration — a person, their roles, what they want —
|
|
102
102
|
* so there is no code body and nothing to resolve at runtime. What is read here
|
|
103
|
-
* is the whole thing, and it is generated straight to `
|
|
103
|
+
* is the whole thing, and it is generated straight to `virtual-users.gen.json` for
|
|
104
104
|
* the CLI and the console to read without loading the app.
|
|
105
105
|
*
|
|
106
|
-
* `name` is the one field that cannot be defaulted. A
|
|
106
|
+
* `name` is the one field that cannot be defaulted. A virtual user without one is
|
|
107
107
|
* not a person, and the console has nothing to show.
|
|
108
108
|
*/
|
|
109
|
-
export const
|
|
109
|
+
export const addVirtualUsers = (logger, node, _checker, state) => {
|
|
110
110
|
if (!ts.isCallExpression(node)) {
|
|
111
111
|
return;
|
|
112
112
|
}
|
|
113
113
|
const expression = node.expression;
|
|
114
|
-
if (!ts.isIdentifier(expression) || expression.text !== '
|
|
114
|
+
if (!ts.isIdentifier(expression) || expression.text !== 'defineVirtualUsers') {
|
|
115
115
|
return;
|
|
116
116
|
}
|
|
117
117
|
const firstArg = node.arguments[0];
|
|
@@ -120,11 +120,11 @@ export const addPersonas = (logger, node, _checker, state) => {
|
|
|
120
120
|
}
|
|
121
121
|
const unwrapped = unwrapAs(firstArg);
|
|
122
122
|
if (!ts.isObjectLiteralExpression(unwrapped)) {
|
|
123
|
-
logger.critical(ErrorCode.INVALID_VALUE, '
|
|
123
|
+
logger.critical(ErrorCode.INVALID_VALUE, 'defineVirtualUsers() needs an inline object — virtual users are read statically, never evaluated.');
|
|
124
124
|
return;
|
|
125
125
|
}
|
|
126
126
|
const sourceFile = node.getSourceFile().fileName;
|
|
127
|
-
if (!claimSingleDeclaration(logger, state.
|
|
127
|
+
if (!claimSingleDeclaration(logger, state.virtualUsers.files, ErrorCode.DUPLICATE_VIRTUAL_USERS_DEFINITION, 'defineVirtualUsers', sourceFile)) {
|
|
128
128
|
return;
|
|
129
129
|
}
|
|
130
130
|
const known = Object.keys(DISPOSITIONS);
|
|
@@ -133,46 +133,46 @@ export const addPersonas = (logger, node, _checker, state) => {
|
|
|
133
133
|
continue;
|
|
134
134
|
}
|
|
135
135
|
if (!ts.isIdentifier(prop.name) && !ts.isStringLiteral(prop.name)) {
|
|
136
|
-
logger.critical(ErrorCode.NON_LITERAL_WIRE_NAME, 'A
|
|
136
|
+
logger.critical(ErrorCode.NON_LITERAL_WIRE_NAME, 'A virtual user is declared with a key that is not a literal.');
|
|
137
137
|
continue;
|
|
138
138
|
}
|
|
139
139
|
const id = prop.name.text;
|
|
140
140
|
if (!ts.isObjectLiteralExpression(prop.initializer)) {
|
|
141
|
-
logger.critical(ErrorCode.INVALID_VALUE, `
|
|
141
|
+
logger.critical(ErrorCode.INVALID_VALUE, `VirtualUser '${id}' must be an object literal.`);
|
|
142
142
|
continue;
|
|
143
143
|
}
|
|
144
144
|
const config = prop.initializer;
|
|
145
145
|
const name = stringProperty(config, 'name');
|
|
146
146
|
if (!name) {
|
|
147
|
-
logger.critical(ErrorCode.INVALID_VALUE, `
|
|
147
|
+
logger.critical(ErrorCode.INVALID_VALUE, `VirtualUser '${id}' needs a literal 'name' — a virtual user is a person, and the console has nothing to show without one.`);
|
|
148
148
|
continue;
|
|
149
149
|
}
|
|
150
|
-
// The generated `
|
|
150
|
+
// The generated `defineVirtualUsers` types this as a union, so a bad value only
|
|
151
151
|
// reaches here in source that does not compile. Caught anyway: a
|
|
152
152
|
// disposition is the entire behaviour, and defaulting a typo to `realistic`
|
|
153
153
|
// would run a different person than the one that was written.
|
|
154
154
|
const disposition = stringProperty(config, 'disposition');
|
|
155
155
|
if (disposition !== undefined && !known.includes(disposition)) {
|
|
156
|
-
logger.critical(ErrorCode.INVALID_VALUE, `
|
|
156
|
+
logger.critical(ErrorCode.INVALID_VALUE, `VirtualUser '${id}' has an unknown disposition '${disposition}'. One of: ${known.join(', ')}`);
|
|
157
157
|
continue;
|
|
158
158
|
}
|
|
159
159
|
let tuningFailed = false;
|
|
160
160
|
const tuning = tuningProperty(config, (message) => {
|
|
161
161
|
tuningFailed = true;
|
|
162
|
-
logger.critical(ErrorCode.INVALID_VALUE, `
|
|
162
|
+
logger.critical(ErrorCode.INVALID_VALUE, `VirtualUser '${id}': ${message}`);
|
|
163
163
|
});
|
|
164
164
|
if (tuningFailed) {
|
|
165
165
|
continue;
|
|
166
166
|
}
|
|
167
167
|
const account = accountProperty(getProperty(config, 'account'));
|
|
168
168
|
const declaredRunnable = booleanProperty(config, 'runnable');
|
|
169
|
-
const
|
|
169
|
+
const virtualUser = {
|
|
170
170
|
id,
|
|
171
171
|
name,
|
|
172
172
|
roles: stringArrayProperty(config, 'roles') ?? [],
|
|
173
173
|
goals: stringArrayProperty(config, 'goals') ?? [],
|
|
174
174
|
tags: stringArrayProperty(config, 'tags') ?? [],
|
|
175
|
-
runnable:
|
|
175
|
+
runnable: isRunnableVirtualUser({
|
|
176
176
|
...(declaredRunnable !== undefined
|
|
177
177
|
? { runnable: declaredRunnable }
|
|
178
178
|
: {}),
|
|
@@ -182,38 +182,38 @@ export const addPersonas = (logger, node, _checker, state) => {
|
|
|
182
182
|
};
|
|
183
183
|
const jobTitle = stringProperty(config, 'jobTitle');
|
|
184
184
|
if (jobTitle !== undefined)
|
|
185
|
-
|
|
185
|
+
virtualUser.jobTitle = jobTitle;
|
|
186
186
|
const description = stringProperty(config, 'description');
|
|
187
187
|
if (description !== undefined)
|
|
188
|
-
|
|
188
|
+
virtualUser.description = description;
|
|
189
189
|
const avatarUrl = stringProperty(config, 'avatarUrl');
|
|
190
190
|
if (avatarUrl !== undefined)
|
|
191
|
-
|
|
191
|
+
virtualUser.avatarUrl = avatarUrl;
|
|
192
192
|
const personality = stringProperty(config, 'personality');
|
|
193
193
|
if (personality !== undefined)
|
|
194
|
-
|
|
194
|
+
virtualUser.personality = personality;
|
|
195
195
|
const app = stringProperty(config, 'app');
|
|
196
196
|
if (app !== undefined)
|
|
197
|
-
|
|
197
|
+
virtualUser.app = app;
|
|
198
198
|
if (disposition !== undefined) {
|
|
199
|
-
|
|
199
|
+
virtualUser.disposition = disposition;
|
|
200
200
|
}
|
|
201
201
|
if (tuning !== undefined)
|
|
202
|
-
|
|
202
|
+
virtualUser.tuning = tuning;
|
|
203
203
|
const fixtures = stringArrayProperty(config, 'fixtures');
|
|
204
204
|
if (fixtures !== undefined)
|
|
205
|
-
|
|
205
|
+
virtualUser.fixtures = fixtures;
|
|
206
206
|
// Left absent rather than resolved here: "everywhere but production" is a
|
|
207
207
|
// fact about the config, and the inspector has no business baking today's
|
|
208
208
|
// environment list into a meta file that outlives it.
|
|
209
209
|
const environments = stringArrayProperty(config, 'environments');
|
|
210
210
|
if (environments !== undefined)
|
|
211
|
-
|
|
211
|
+
virtualUser.environments = environments;
|
|
212
212
|
if (account !== undefined)
|
|
213
|
-
|
|
213
|
+
virtualUser.account = account;
|
|
214
214
|
const linkedAccounts = linkedAccountsProperty(config, id, logger);
|
|
215
215
|
if (linkedAccounts !== undefined)
|
|
216
|
-
|
|
217
|
-
state.
|
|
216
|
+
virtualUser.linkedAccounts = linkedAccounts;
|
|
217
|
+
state.virtualUsers.definitions.push(virtualUser);
|
|
218
218
|
}
|
|
219
219
|
};
|
package/dist/add/add-workflow.js
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
import * as ts from 'typescript';
|
|
2
|
+
import { relative } from 'node:path';
|
|
2
3
|
import { extractFunctionName } from '../utils/extract-function-name.js';
|
|
3
4
|
import { extractFunctionNode } from '../utils/extract-function-node.js';
|
|
4
5
|
import { ErrorCode } from '../error-codes.js';
|
|
5
6
|
import { extractStringLiteral, isStringLike, isFunctionLike, extractDescription, extractDuration, } from '../utils/extract-node-value.js';
|
|
6
7
|
import { getCommonWireMetaData, getPropertyValue, } from '../utils/get-property-value.js';
|
|
7
8
|
import { extractDSLWorkflow } from '../utils/workflow/dsl/extract-dsl-workflow.js';
|
|
8
|
-
import { getSourceText,
|
|
9
|
+
import { getSourceText, extractVirtualUserFromOptions, } from '../utils/workflow/dsl/patterns.js';
|
|
9
10
|
/**
|
|
10
11
|
* Extract a workflow step's display name without letting a non-static name
|
|
11
12
|
* (e.g. a function call) abort the scan. The step name is cosmetic, so a
|
|
@@ -141,7 +142,7 @@ function getWorkflowInvocations(node, checker, state, workflowName, steps) {
|
|
|
141
142
|
type: 'rpc',
|
|
142
143
|
stepName,
|
|
143
144
|
rpcName,
|
|
144
|
-
|
|
145
|
+
virtualUser: extractVirtualUserFromOptions(optionsArg),
|
|
145
146
|
});
|
|
146
147
|
state.rpc.invokedFunctions.add(rpcName);
|
|
147
148
|
}
|
|
@@ -164,7 +165,7 @@ function getWorkflowInvocations(node, checker, state, workflowName, steps) {
|
|
|
164
165
|
stepName,
|
|
165
166
|
rpcName,
|
|
166
167
|
expectEventually: true,
|
|
167
|
-
|
|
168
|
+
virtualUser: extractVirtualUserFromOptions(args.length >= 5 ? args[4] : undefined),
|
|
168
169
|
});
|
|
169
170
|
state.rpc.invokedFunctions.add(rpcName);
|
|
170
171
|
}
|
|
@@ -229,7 +230,7 @@ export const addWorkflow = (logger, node, checker, state, options) => {
|
|
|
229
230
|
wrapperType = 'complex';
|
|
230
231
|
}
|
|
231
232
|
else if (expression.text === 'pikkuScenario') {
|
|
232
|
-
// A scenario is a complex workflow whose steps run as
|
|
233
|
+
// A scenario is a complex workflow whose steps run as virtual users over the
|
|
233
234
|
// real transport — same extraction rules as complex, distinct meta.
|
|
234
235
|
wrapperType = 'scenario';
|
|
235
236
|
}
|
|
@@ -243,7 +244,10 @@ export const addWorkflow = (logger, node, checker, state, options) => {
|
|
|
243
244
|
const { pikkuFuncId, name, exportedName } = extractFunctionName(node, checker, state.rootDir);
|
|
244
245
|
const workflowName = exportedName || name;
|
|
245
246
|
if (!workflowName) {
|
|
246
|
-
|
|
247
|
+
const sourceFile = node.getSourceFile();
|
|
248
|
+
const { line } = sourceFile.getLineAndCharacterOfPosition(node.getStart());
|
|
249
|
+
logger.critical(ErrorCode.MISSING_NAME, `Could not determine workflow name from export at ${relative(state.rootDir, sourceFile.fileName)}:${line + 1}. ` +
|
|
250
|
+
`Assign the \`${expression.text}(...)\` call to an exported const; that name is how it is registered.`);
|
|
247
251
|
return;
|
|
248
252
|
}
|
|
249
253
|
// Extract the function node (either direct function or from config.func)
|
|
@@ -358,11 +362,11 @@ export const addWorkflow = (logger, node, checker, state, options) => {
|
|
|
358
362
|
const sink = steps.length > 0 ? [] : steps;
|
|
359
363
|
getWorkflowInvocations(resolvedFunc, checker, state, workflowName, sink);
|
|
360
364
|
}
|
|
361
|
-
//
|
|
362
|
-
//
|
|
363
|
-
const
|
|
365
|
+
// Virtual user names the flow's steps run as (scenarios) — powers the console
|
|
366
|
+
// virtual users view and the Scenario graph badges.
|
|
367
|
+
const virtualUserNames = [
|
|
364
368
|
...new Set(steps
|
|
365
|
-
.map((s) => ('
|
|
369
|
+
.map((s) => ('virtualUser' in s ? s.virtualUser : undefined))
|
|
366
370
|
.filter((a) => typeof a === 'string')),
|
|
367
371
|
];
|
|
368
372
|
state.workflows.meta[workflowName] = {
|
|
@@ -380,7 +384,7 @@ export const addWorkflow = (logger, node, checker, state, options) => {
|
|
|
380
384
|
skip,
|
|
381
385
|
scenario: wrapperType === 'scenario' ? true : undefined,
|
|
382
386
|
asserts,
|
|
383
|
-
|
|
387
|
+
virtualUsers: virtualUserNames.length > 0 ? virtualUserNames : undefined,
|
|
384
388
|
};
|
|
385
389
|
// The scenario "logger/config only" rule (PKU673) and the implicit
|
|
386
390
|
// workflow platform-service injection both used to live here, reading
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { AddWiring } from '../types.js';
|
|
2
|
+
/**
|
|
3
|
+
* A middleware or permission helper the inspector can only see by its direct
|
|
4
|
+
* calls. Passed around as a value — into an array, through a ternary, as an
|
|
5
|
+
* argument — whatever it registers is invisible to codegen, and a gate that
|
|
6
|
+
* codegen never imports is a gate that never runs. Say so rather than
|
|
7
|
+
* generate an app that is silently open.
|
|
8
|
+
*/
|
|
9
|
+
export declare const checkIndirectRegistrations: AddWiring;
|