@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.
Files changed (74) hide show
  1. package/dist/add/add-auth.d.ts +1 -9
  2. package/dist/add/add-auth.js +64 -17
  3. package/dist/add/add-cli.js +25 -11
  4. package/dist/add/add-functions.js +35 -39
  5. package/dist/add/add-keyed-wiring.js +22 -2
  6. package/dist/add/add-middleware.js +17 -16
  7. package/dist/add/add-permission.d.ts +0 -3
  8. package/dist/add/add-permission.js +213 -22
  9. package/dist/add/add-scope.d.ts +1 -7
  10. package/dist/add/add-scope.js +1 -21
  11. package/dist/add/add-virtual-users.d.ts +13 -0
  12. package/dist/add/{add-personas.js → add-virtual-users.js} +33 -33
  13. package/dist/add/add-workflow.js +14 -10
  14. package/dist/add/check-indirect-registrations.d.ts +9 -0
  15. package/dist/add/check-indirect-registrations.js +114 -0
  16. package/dist/error-codes.d.ts +5 -3
  17. package/dist/error-codes.js +5 -3
  18. package/dist/finalize.d.ts +10 -0
  19. package/dist/finalize.js +102 -0
  20. package/dist/index.d.ts +8 -6
  21. package/dist/index.js +6 -5
  22. package/dist/initial-state.d.ts +7 -0
  23. package/dist/initial-state.js +216 -0
  24. package/dist/inspector.d.ts +6 -4
  25. package/dist/inspector.js +28 -300
  26. package/dist/lite.d.ts +26 -0
  27. package/dist/lite.js +25 -0
  28. package/dist/source-file-cache.d.ts +11 -0
  29. package/dist/source-file-cache.js +34 -10
  30. package/dist/types-map.js +2 -1
  31. package/dist/types.d.ts +35 -3
  32. package/dist/utils/apply-declared-scopes.d.ts +8 -0
  33. package/dist/utils/apply-declared-scopes.js +21 -0
  34. package/dist/utils/canonical-type-string.d.ts +14 -0
  35. package/dist/utils/canonical-type-string.js +162 -0
  36. package/dist/utils/context-based-id.d.ts +7 -0
  37. package/dist/utils/context-based-id.js +9 -0
  38. package/dist/utils/custom-types-generator.d.ts +15 -1
  39. package/dist/utils/custom-types-generator.js +17 -5
  40. package/dist/utils/ensure-function-metadata.d.ts +0 -14
  41. package/dist/utils/ensure-function-metadata.js +4 -1
  42. package/dist/utils/extract-function-name.d.ts +1 -1
  43. package/dist/utils/extract-function-name.js +2 -4
  44. package/dist/utils/filter-inspector-state.js +31 -1
  45. package/dist/utils/get-property-value.d.ts +5 -0
  46. package/dist/utils/get-property-value.js +15 -1
  47. package/dist/utils/inspector-constants.d.ts +16 -0
  48. package/dist/utils/inspector-constants.js +16 -0
  49. package/dist/utils/load-addon-functions-meta.js +1 -0
  50. package/dist/utils/post-process.d.ts +35 -36
  51. package/dist/utils/post-process.js +218 -193
  52. package/dist/utils/resolve-callee-name.d.ts +27 -0
  53. package/dist/utils/resolve-callee-name.js +147 -0
  54. package/dist/utils/resolve-service-types.d.ts +8 -0
  55. package/dist/utils/resolve-service-types.js +21 -0
  56. package/dist/utils/schema-generator.js +29 -2
  57. package/dist/utils/serialize-inspector-state.d.ts +9 -3
  58. package/dist/utils/serialize-inspector-state.js +22 -7
  59. package/dist/utils/single-declaration.d.ts +3 -3
  60. package/dist/utils/single-declaration.js +3 -3
  61. package/dist/utils/validate-single-step-workflows.d.ts +3 -0
  62. package/dist/utils/validate-single-step-workflows.js +32 -0
  63. package/dist/utils/workflow/dsl/extract-dsl-workflow.js +3 -3
  64. package/dist/utils/workflow/dsl/patterns.d.ts +4 -11
  65. package/dist/utils/workflow/dsl/patterns.js +5 -11
  66. package/dist/utils/workflow/graph/convert-dsl-to-graph.js +6 -6
  67. package/dist/utils/workflow/graph/workflow-graph.types.d.ts +5 -5
  68. package/dist/visit.js +4 -2
  69. package/package.json +25 -9
  70. package/CHANGELOG.md +0 -4318
  71. package/OPTIMIZATION-PLAN.md +0 -201
  72. package/dist/add/add-personas.d.ts +0 -13
  73. package/run-tests.sh +0 -68
  74. package/src/utils/test-data/inspector-state.json +0 -1687
@@ -1,14 +1,16 @@
1
1
  import * as ts from 'typescript';
2
- import { extractFunctionName } from '../utils/extract-function-name.js';
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
- function isInsidePermissionContainer(node) {
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
- ts.isIdentifier(current.expression) &&
11
- current.expression.text === 'pikkuPermissionFactory') {
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
- * Inspect pikkuPermission, pikkuAuth, and pikkuPermissionFactory definitions.
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 { expression, arguments: args } = node;
25
- // only handle specific function calls
26
- if (!ts.isIdentifier(expression)) {
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 (expression.text === 'pikkuPermission') {
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
- logger.error(`• pikkuPermission() must be assigned to a variable or object property. ` +
74
- `Extract it to a const: const myPermission = pikkuPermission(...)`);
75
- return;
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 (expression.text === 'pikkuAuth') {
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
- logger.error(`• pikkuAuth() must be assigned to a variable or object property. ` +
141
- `Extract it to a const: const myAuth = pikkuAuth(...)`);
142
- return;
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 (expression.text === 'pikkuPermissionFactory') {
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
- const expr = node.expression;
179
- if (ts.isIdentifier(expr) && expr.text === 'pikkuPermission') {
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
  };
@@ -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;
@@ -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 { isRunnablePersona } from '@pikku/core/persona';
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 persona makes, and you would read the run as a product
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, personaId, logger) => {
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, `Persona '${personaId}' has a linked account whose key is not a literal.`);
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, `Persona '${personaId}' linked account '${prop.name.text}' must be an object literal.`);
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 `definePersonas()` calls.
99
+ * Inspector for `defineVirtualUsers()` calls.
100
100
  *
101
- * A persona is entirely declaration — a person, their roles, what they want —
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 `personas.gen.json` for
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 persona without one is
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 addPersonas = (logger, node, _checker, state) => {
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 !== 'definePersonas') {
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, 'definePersonas() needs an inline object — personas are read statically, never evaluated.');
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.personas.files, ErrorCode.DUPLICATE_PERSONAS_DEFINITION, 'definePersonas', sourceFile)) {
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 persona is declared with a key that is not a literal.');
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, `Persona '${id}' must be an object literal.`);
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, `Persona '${id}' needs a literal 'name' — a persona is a person, and the console has nothing to show without one.`);
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 `definePersonas` types this as a union, so a bad value only
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, `Persona '${id}' has an unknown disposition '${disposition}'. One of: ${known.join(', ')}`);
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, `Persona '${id}': ${message}`);
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 persona = {
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: isRunnablePersona({
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
- persona.jobTitle = jobTitle;
185
+ virtualUser.jobTitle = jobTitle;
186
186
  const description = stringProperty(config, 'description');
187
187
  if (description !== undefined)
188
- persona.description = description;
188
+ virtualUser.description = description;
189
189
  const avatarUrl = stringProperty(config, 'avatarUrl');
190
190
  if (avatarUrl !== undefined)
191
- persona.avatarUrl = avatarUrl;
191
+ virtualUser.avatarUrl = avatarUrl;
192
192
  const personality = stringProperty(config, 'personality');
193
193
  if (personality !== undefined)
194
- persona.personality = personality;
194
+ virtualUser.personality = personality;
195
195
  const app = stringProperty(config, 'app');
196
196
  if (app !== undefined)
197
- persona.app = app;
197
+ virtualUser.app = app;
198
198
  if (disposition !== undefined) {
199
- persona.disposition = disposition;
199
+ virtualUser.disposition = disposition;
200
200
  }
201
201
  if (tuning !== undefined)
202
- persona.tuning = tuning;
202
+ virtualUser.tuning = tuning;
203
203
  const fixtures = stringArrayProperty(config, 'fixtures');
204
204
  if (fixtures !== undefined)
205
- persona.fixtures = fixtures;
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
- persona.environments = environments;
211
+ virtualUser.environments = environments;
212
212
  if (account !== undefined)
213
- persona.account = account;
213
+ virtualUser.account = account;
214
214
  const linkedAccounts = linkedAccountsProperty(config, id, logger);
215
215
  if (linkedAccounts !== undefined)
216
- persona.linkedAccounts = linkedAccounts;
217
- state.personas.definitions.push(persona);
216
+ virtualUser.linkedAccounts = linkedAccounts;
217
+ state.virtualUsers.definitions.push(virtualUser);
218
218
  }
219
219
  };
@@ -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, extractActorFromOptions, } from '../utils/workflow/dsl/patterns.js';
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
- actor: extractActorFromOptions(optionsArg),
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
- actor: extractActorFromOptions(args.length >= 5 ? args[4] : undefined),
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 actors over the
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
- logger.critical(ErrorCode.MISSING_NAME, `Could not determine workflow name from export.`);
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
- // Actor names the flow's steps run as (scenarios) — powers the console
362
- // personas view and the Scenario graph badges.
363
- const actorNames = [
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) => ('actor' in s ? s.actor : undefined))
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
- actors: actorNames.length > 0 ? actorNames : undefined,
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;