@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
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import * as ts from 'typescript';
|
|
2
|
+
/** The helpers that define or register middleware. */
|
|
3
|
+
export declare const MIDDLEWARE_HELPERS: ReadonlySet<string>;
|
|
4
|
+
/** The helpers that define or register permissions. */
|
|
5
|
+
export declare const PERMISSION_HELPERS: ReadonlySet<string>;
|
|
6
|
+
type NameMatcher = ReadonlySet<string> | ((name: string) => boolean);
|
|
7
|
+
export declare const isNamespaceReceiver: (receiver: ts.Expression, checker: ts.TypeChecker) => boolean;
|
|
8
|
+
/**
|
|
9
|
+
* Every name a callee answers to, from its spelling at the call site to the
|
|
10
|
+
* declaration it resolves to: through import and re-export aliases, members
|
|
11
|
+
* of a namespace import, and `const` bindings of another name.
|
|
12
|
+
*/
|
|
13
|
+
export declare const calleeNames: (expression: ts.Expression, checker: ts.TypeChecker) => readonly string[];
|
|
14
|
+
/**
|
|
15
|
+
* The pikku helper a call invokes, identified by what its callee resolves to
|
|
16
|
+
* rather than how it is spelled, so `import { addTagMiddleware as guard }` and
|
|
17
|
+
* `mw.addTagMiddleware(...)` are seen for what they are.
|
|
18
|
+
*
|
|
19
|
+
* The deepest known name wins, because it is the declaration's own: an alias
|
|
20
|
+
* that happens to look like another helper (`pikkuFunc as pikkuOpenFunc`) is
|
|
21
|
+
* still the helper it imports. A call inside the body of a helper's own
|
|
22
|
+
* declaration — the generated `#pikku` wrappers forwarding to `@pikku/core` —
|
|
23
|
+
* is the helper's implementation, not a use of it, and keeps the meaning its
|
|
24
|
+
* literal spelling gives it.
|
|
25
|
+
*/
|
|
26
|
+
export declare const resolveCalleeName: (call: ts.CallExpression, checker: ts.TypeChecker, known: NameMatcher) => string | undefined;
|
|
27
|
+
export {};
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
import * as ts from 'typescript';
|
|
2
|
+
const MAX_DEPTH = 8;
|
|
3
|
+
/** The helpers that define or register middleware. */
|
|
4
|
+
export const MIDDLEWARE_HELPERS = new Set([
|
|
5
|
+
'pikkuMiddleware',
|
|
6
|
+
'pikkuMiddlewareFactory',
|
|
7
|
+
'addTagMiddleware',
|
|
8
|
+
'addGlobalMiddleware',
|
|
9
|
+
'addHTTPMiddleware',
|
|
10
|
+
'pikkuChannelMiddleware',
|
|
11
|
+
'pikkuChannelMiddlewareFactory',
|
|
12
|
+
'pikkuAgentMiddleware',
|
|
13
|
+
'addChannelMiddleware',
|
|
14
|
+
]);
|
|
15
|
+
/** The helpers that define or register permissions. */
|
|
16
|
+
export const PERMISSION_HELPERS = new Set([
|
|
17
|
+
'pikkuPermission',
|
|
18
|
+
'pikkuAuth',
|
|
19
|
+
'pikkuPermissionFactory',
|
|
20
|
+
'addGlobalPermission',
|
|
21
|
+
]);
|
|
22
|
+
const matcher = (known) => typeof known === 'function' ? known : (name) => known.has(name);
|
|
23
|
+
const namesCache = new WeakMap();
|
|
24
|
+
const unwrap = (expression) => {
|
|
25
|
+
let current = expression;
|
|
26
|
+
while (ts.isParenthesizedExpression(current) ||
|
|
27
|
+
ts.isAsExpression(current) ||
|
|
28
|
+
ts.isSatisfiesExpression(current) ||
|
|
29
|
+
ts.isNonNullExpression(current) ||
|
|
30
|
+
ts.isTypeAssertionExpression(current)) {
|
|
31
|
+
current = current.expression;
|
|
32
|
+
}
|
|
33
|
+
return current;
|
|
34
|
+
};
|
|
35
|
+
export const isNamespaceReceiver = (receiver, checker) => {
|
|
36
|
+
if (!ts.isIdentifier(receiver))
|
|
37
|
+
return false;
|
|
38
|
+
const symbol = checker.getSymbolAtLocation(receiver);
|
|
39
|
+
if (!symbol)
|
|
40
|
+
return false;
|
|
41
|
+
if (symbol.declarations?.some(ts.isNamespaceImport))
|
|
42
|
+
return true;
|
|
43
|
+
const target = symbol.flags & ts.SymbolFlags.Alias
|
|
44
|
+
? checker.getAliasedSymbol(symbol)
|
|
45
|
+
: symbol;
|
|
46
|
+
return (target.flags & ts.SymbolFlags.ValueModule) !== 0;
|
|
47
|
+
};
|
|
48
|
+
const followSymbol = (symbol, checker, names, depth) => {
|
|
49
|
+
if (!symbol || depth > MAX_DEPTH)
|
|
50
|
+
return;
|
|
51
|
+
let target = symbol;
|
|
52
|
+
if (symbol.flags & ts.SymbolFlags.Alias) {
|
|
53
|
+
const declaration = symbol.declarations?.[0];
|
|
54
|
+
if (declaration &&
|
|
55
|
+
(ts.isImportSpecifier(declaration) || ts.isExportSpecifier(declaration))) {
|
|
56
|
+
names.push((declaration.propertyName ?? declaration.name).text);
|
|
57
|
+
}
|
|
58
|
+
target = checker.getAliasedSymbol(symbol);
|
|
59
|
+
// An import from a module the program cannot resolve: the specifier's own
|
|
60
|
+
// exported name is the only identity there is.
|
|
61
|
+
if (!target.declarations?.length)
|
|
62
|
+
return;
|
|
63
|
+
names.push(target.getName());
|
|
64
|
+
}
|
|
65
|
+
const declaration = target.valueDeclaration;
|
|
66
|
+
if (declaration &&
|
|
67
|
+
ts.isVariableDeclaration(declaration) &&
|
|
68
|
+
declaration.initializer &&
|
|
69
|
+
ts.getCombinedNodeFlags(declaration) & ts.NodeFlags.Const) {
|
|
70
|
+
collectNames(declaration.initializer, checker, names, depth + 1);
|
|
71
|
+
}
|
|
72
|
+
};
|
|
73
|
+
const collectNames = (expression, checker, names, depth) => {
|
|
74
|
+
const callee = unwrap(expression);
|
|
75
|
+
if (ts.isIdentifier(callee)) {
|
|
76
|
+
names.push(callee.text);
|
|
77
|
+
followSymbol(checker.getSymbolAtLocation(callee), checker, names, depth);
|
|
78
|
+
}
|
|
79
|
+
else if (ts.isPropertyAccessExpression(callee) &&
|
|
80
|
+
isNamespaceReceiver(unwrap(callee.expression), checker)) {
|
|
81
|
+
names.push(callee.name.text);
|
|
82
|
+
followSymbol(checker.getSymbolAtLocation(callee.name), checker, names, depth);
|
|
83
|
+
}
|
|
84
|
+
};
|
|
85
|
+
/**
|
|
86
|
+
* Every name a callee answers to, from its spelling at the call site to the
|
|
87
|
+
* declaration it resolves to: through import and re-export aliases, members
|
|
88
|
+
* of a namespace import, and `const` bindings of another name.
|
|
89
|
+
*/
|
|
90
|
+
export const calleeNames = (expression, checker) => {
|
|
91
|
+
let cache = namesCache.get(checker);
|
|
92
|
+
if (!cache) {
|
|
93
|
+
cache = new WeakMap();
|
|
94
|
+
namesCache.set(checker, cache);
|
|
95
|
+
}
|
|
96
|
+
const cached = cache.get(expression);
|
|
97
|
+
if (cached)
|
|
98
|
+
return cached;
|
|
99
|
+
const names = [];
|
|
100
|
+
collectNames(expression, checker, names, 0);
|
|
101
|
+
cache.set(expression, names);
|
|
102
|
+
return names;
|
|
103
|
+
};
|
|
104
|
+
const isInsideDeclarationOf = (node, isKnown) => {
|
|
105
|
+
for (let current = node.parent; current; current = current.parent) {
|
|
106
|
+
if ((ts.isVariableDeclaration(current) ||
|
|
107
|
+
ts.isFunctionDeclaration(current)) &&
|
|
108
|
+
current.name &&
|
|
109
|
+
ts.isIdentifier(current.name) &&
|
|
110
|
+
isKnown(current.name.text)) {
|
|
111
|
+
return true;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
return false;
|
|
115
|
+
};
|
|
116
|
+
/**
|
|
117
|
+
* The pikku helper a call invokes, identified by what its callee resolves to
|
|
118
|
+
* rather than how it is spelled, so `import { addTagMiddleware as guard }` and
|
|
119
|
+
* `mw.addTagMiddleware(...)` are seen for what they are.
|
|
120
|
+
*
|
|
121
|
+
* The deepest known name wins, because it is the declaration's own: an alias
|
|
122
|
+
* that happens to look like another helper (`pikkuFunc as pikkuOpenFunc`) is
|
|
123
|
+
* still the helper it imports. A call inside the body of a helper's own
|
|
124
|
+
* declaration — the generated `#pikku` wrappers forwarding to `@pikku/core` —
|
|
125
|
+
* is the helper's implementation, not a use of it, and keeps the meaning its
|
|
126
|
+
* literal spelling gives it.
|
|
127
|
+
*/
|
|
128
|
+
export const resolveCalleeName = (call, checker, known) => {
|
|
129
|
+
const isKnown = matcher(known);
|
|
130
|
+
const names = calleeNames(call.expression, checker);
|
|
131
|
+
let resolved;
|
|
132
|
+
for (let i = names.length - 1; i >= 0; i--) {
|
|
133
|
+
if (isKnown(names[i])) {
|
|
134
|
+
resolved = names[i];
|
|
135
|
+
break;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
if (!resolved)
|
|
139
|
+
return undefined;
|
|
140
|
+
const spelled = ts.isIdentifier(call.expression)
|
|
141
|
+
? call.expression.text
|
|
142
|
+
: undefined;
|
|
143
|
+
if (resolved !== spelled && isInsideDeclarationOf(call, isKnown)) {
|
|
144
|
+
return spelled !== undefined && isKnown(spelled) ? spelled : undefined;
|
|
145
|
+
}
|
|
146
|
+
return resolved;
|
|
147
|
+
};
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type ts from 'typescript';
|
|
2
|
+
import type { InspectorServiceTypes, InspectorState } from '../types.js';
|
|
3
|
+
/**
|
|
4
|
+
* Reads the checker's view of the SingletonServices and Services types into
|
|
5
|
+
* plain names, so the shared finalize phase can aggregate services and refuse
|
|
6
|
+
* secret aliases without a `ts.Type` in hand.
|
|
7
|
+
*/
|
|
8
|
+
export declare const resolveServiceTypes: (state: Pick<InspectorState, "typesLookup" | "singletonServicesTypeImportMap" | "wireServicesTypeImportMap">, checker: ts.TypeChecker) => InspectorServiceTypes;
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { extractTypeKeys } from './type-utils.js';
|
|
2
|
+
import { resolveCoreType } from './resolve-core-type.js';
|
|
3
|
+
import { findSecretAliasServices } from './secret-alias-services.js';
|
|
4
|
+
/**
|
|
5
|
+
* Reads the checker's view of the SingletonServices and Services types into
|
|
6
|
+
* plain names, so the shared finalize phase can aggregate services and refuse
|
|
7
|
+
* secret aliases without a `ts.Type` in hand.
|
|
8
|
+
*/
|
|
9
|
+
export const resolveServiceTypes = (state, checker) => {
|
|
10
|
+
const singletonServicesType = resolveCoreType(state.typesLookup, state.singletonServicesTypeImportMap);
|
|
11
|
+
const servicesType = resolveCoreType(state.typesLookup, state.wireServicesTypeImportMap);
|
|
12
|
+
return {
|
|
13
|
+
singletonServices: singletonServicesType
|
|
14
|
+
? extractTypeKeys(singletonServicesType).sort()
|
|
15
|
+
: null,
|
|
16
|
+
services: servicesType ? extractTypeKeys(servicesType) : null,
|
|
17
|
+
secretAliases: singletonServicesType
|
|
18
|
+
? findSecretAliasServices(singletonServicesType, checker)
|
|
19
|
+
: [],
|
|
20
|
+
};
|
|
21
|
+
};
|
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
import * as ts from 'typescript';
|
|
2
2
|
import { createHash } from 'crypto';
|
|
3
3
|
import { existsSync, mkdirSync, readFileSync, statSync, writeFileSync, } from 'fs';
|
|
4
|
-
import { dirname, join, resolve } from 'path';
|
|
4
|
+
import { basename, dirname, join, resolve } from 'path';
|
|
5
5
|
import { createFormatter, createParser, DEFAULT_CONFIG, RootlessError, SchemaGenerator, } from 'ts-json-schema-generator';
|
|
6
6
|
import { register } from 'tsx/esm/api';
|
|
7
7
|
import * as z from 'zod';
|
|
8
8
|
import { findZodTransform } from './find-zod-transform.js';
|
|
9
9
|
import { zodToTypeText } from './zod-to-type-text.js';
|
|
10
10
|
import { ErrorCode } from '../error-codes.js';
|
|
11
|
+
import { getCachedSourceFile } from '../source-file-cache.js';
|
|
11
12
|
import { generateCustomTypes } from './custom-types-generator.js';
|
|
12
13
|
const PRIMITIVE_TYPES = new Set([
|
|
13
14
|
'boolean',
|
|
@@ -212,6 +213,21 @@ const createIndexedGenerator = (program, config) => {
|
|
|
212
213
|
const completedConfig = { ...DEFAULT_CONFIG, ...config };
|
|
213
214
|
return new IndexedSchemaGenerator(program, createParser(program, completedConfig), createFormatter(completedConfig), completedConfig);
|
|
214
215
|
};
|
|
216
|
+
/**
|
|
217
|
+
* Parse at the inspector's target so the schema program reuses the inspector's
|
|
218
|
+
* SourceFiles instead of re-parsing the project; `target` is part of the
|
|
219
|
+
* parse cache key. The lib the project's own target implies is pinned, so type
|
|
220
|
+
* resolution is unchanged.
|
|
221
|
+
*/
|
|
222
|
+
const parseAtInspectorTarget = (options) => {
|
|
223
|
+
if (options.target === ts.ScriptTarget.ESNext)
|
|
224
|
+
return options;
|
|
225
|
+
return {
|
|
226
|
+
...options,
|
|
227
|
+
target: ts.ScriptTarget.ESNext,
|
|
228
|
+
lib: options.lib ?? [basename(ts.getDefaultLibFilePath(options))],
|
|
229
|
+
};
|
|
230
|
+
};
|
|
215
231
|
function createProgramWithVirtualFile(logger, tsconfig, virtualFilePath, virtualFileContent) {
|
|
216
232
|
const configPath = resolve(tsconfig);
|
|
217
233
|
// Cache the parsed tsconfig — it doesn't change between runs
|
|
@@ -219,6 +235,7 @@ function createProgramWithVirtualFile(logger, tsconfig, virtualFilePath, virtual
|
|
|
219
235
|
const configFile = ts.readConfigFile(configPath, ts.sys.readFile);
|
|
220
236
|
const basePath = dirname(configPath);
|
|
221
237
|
cachedParsedConfig = ts.parseJsonConfigFileContent(configFile.config, ts.sys, basePath);
|
|
238
|
+
cachedParsedConfig.options = parseAtInspectorTarget(cachedParsedConfig.options);
|
|
222
239
|
cachedTsconfigPath = configPath;
|
|
223
240
|
cachedSchemaProgram = undefined;
|
|
224
241
|
}
|
|
@@ -235,7 +252,17 @@ function createProgramWithVirtualFile(logger, tsconfig, virtualFilePath, virtual
|
|
|
235
252
|
if (resolve(fileName) === resolvedVirtualPath) {
|
|
236
253
|
return ts.createSourceFile(fileName, virtualFileContent, languageVersionOrOptions);
|
|
237
254
|
}
|
|
238
|
-
|
|
255
|
+
if (shouldCreateNewSourceFile) {
|
|
256
|
+
return defaultHost.getSourceFile(fileName, languageVersionOrOptions, onError, shouldCreateNewSourceFile);
|
|
257
|
+
}
|
|
258
|
+
// Shares the inspector's parsed files rather than parsing the project a
|
|
259
|
+
// second time.
|
|
260
|
+
const text = defaultHost.readFile(fileName);
|
|
261
|
+
if (text === undefined)
|
|
262
|
+
return undefined;
|
|
263
|
+
return getCachedSourceFile(fileName, text, languageVersionOrOptions, {
|
|
264
|
+
populate: false,
|
|
265
|
+
}).file;
|
|
239
266
|
},
|
|
240
267
|
fileExists(fileName) {
|
|
241
268
|
if (resolve(fileName) === resolvedVirtualPath)
|
|
@@ -60,6 +60,8 @@ export interface SerializableInspectorState {
|
|
|
60
60
|
addonRequiredParentServices: string[];
|
|
61
61
|
addonCreatedServices: string[];
|
|
62
62
|
addonServicesFactorySeen: boolean;
|
|
63
|
+
analytics?: InspectorState['analytics'];
|
|
64
|
+
outgoingWebhooks?: InspectorState['outgoingWebhooks'];
|
|
63
65
|
addonServerlessIncompatible: Array<[string, string[]]>;
|
|
64
66
|
configFactories: Array<[
|
|
65
67
|
string,
|
|
@@ -115,6 +117,7 @@ export interface SerializableInspectorState {
|
|
|
115
117
|
exportedName: string;
|
|
116
118
|
}]>;
|
|
117
119
|
approvalDescriptions: InspectorState['functions']['approvalDescriptions'];
|
|
120
|
+
dynamicImportIds?: string[];
|
|
118
121
|
};
|
|
119
122
|
http: {
|
|
120
123
|
metaInputTypes: Array<[
|
|
@@ -260,6 +263,7 @@ export interface SerializableInspectorState {
|
|
|
260
263
|
scopes: {
|
|
261
264
|
definitions: InspectorState['scopes']['definitions'];
|
|
262
265
|
files: string[];
|
|
266
|
+
declared?: Array<[string, string]>;
|
|
263
267
|
};
|
|
264
268
|
systemRoles: {
|
|
265
269
|
definitions: InspectorState['systemRoles']['definitions'];
|
|
@@ -269,8 +273,8 @@ export interface SerializableInspectorState {
|
|
|
269
273
|
definitions: InspectorState['featureFlags']['definitions'];
|
|
270
274
|
files: string[];
|
|
271
275
|
};
|
|
272
|
-
|
|
273
|
-
definitions: InspectorState['
|
|
276
|
+
virtualUsers: {
|
|
277
|
+
definitions: InspectorState['virtualUsers']['definitions'];
|
|
274
278
|
files: string[];
|
|
275
279
|
};
|
|
276
280
|
variables: {
|
|
@@ -301,6 +305,7 @@ export interface SerializableInspectorState {
|
|
|
301
305
|
permissions: {
|
|
302
306
|
definitions: InspectorState['permissions']['definitions'];
|
|
303
307
|
instances: InspectorState['permissions']['instances'];
|
|
308
|
+
global?: InspectorState['permissions']['global'];
|
|
304
309
|
};
|
|
305
310
|
serviceAggregation: {
|
|
306
311
|
requiredServices: string[];
|
|
@@ -310,6 +315,7 @@ export interface SerializableInspectorState {
|
|
|
310
315
|
allSingletonServices: string[];
|
|
311
316
|
allWireServices: string[];
|
|
312
317
|
};
|
|
318
|
+
serviceTypes?: InspectorState['serviceTypes'];
|
|
313
319
|
resolvedIOTypes: Record<string, {
|
|
314
320
|
inputType: string;
|
|
315
321
|
outputType: string;
|
|
@@ -328,7 +334,7 @@ export interface SerializableInspectorState {
|
|
|
328
334
|
* Serializes InspectorState to a JSON-compatible format
|
|
329
335
|
* Omits typesLookup (not needed for filtering/generation) and converts Maps/Sets to arrays
|
|
330
336
|
*/
|
|
331
|
-
export declare function serializeInspectorState(state: InspectorState): SerializableInspectorState;
|
|
337
|
+
export declare function serializeInspectorState(state: Omit<InspectorState, 'typesLookup'>): SerializableInspectorState;
|
|
332
338
|
/**
|
|
333
339
|
* Deserializes JSON data back to InspectorState
|
|
334
340
|
* Creates a partial state suitable for filtering/generation (without typesLookup)
|
|
@@ -26,6 +26,8 @@ export function serializeInspectorState(state) {
|
|
|
26
26
|
addonRequiredParentServices: state.addonRequiredParentServices,
|
|
27
27
|
addonCreatedServices: state.addonCreatedServices,
|
|
28
28
|
addonServicesFactorySeen: state.addonServicesFactorySeen,
|
|
29
|
+
analytics: state.analytics,
|
|
30
|
+
outgoingWebhooks: state.outgoingWebhooks,
|
|
29
31
|
addonServerlessIncompatible: Array.from(state.addonServerlessIncompatible.entries()),
|
|
30
32
|
configFactories: Array.from(state.configFactories.entries()),
|
|
31
33
|
serverLifecycleFactories: Array.from(state.serverLifecycleFactories.entries()),
|
|
@@ -38,6 +40,7 @@ export function serializeInspectorState(state) {
|
|
|
38
40
|
meta: state.functions.meta,
|
|
39
41
|
files: Array.from(state.functions.files.entries()),
|
|
40
42
|
approvalDescriptions: state.functions.approvalDescriptions,
|
|
43
|
+
dynamicImportIds: Array.from(state.functions.dynamicImportIds ?? []),
|
|
41
44
|
},
|
|
42
45
|
http: {
|
|
43
46
|
metaInputTypes: Array.from(state.http.metaInputTypes.entries()),
|
|
@@ -131,6 +134,9 @@ export function serializeInspectorState(state) {
|
|
|
131
134
|
scopes: {
|
|
132
135
|
definitions: state.scopes.definitions,
|
|
133
136
|
files: Array.from(state.scopes.files),
|
|
137
|
+
declared: state.scopes.declared
|
|
138
|
+
? Array.from(state.scopes.declared.entries())
|
|
139
|
+
: undefined,
|
|
134
140
|
},
|
|
135
141
|
systemRoles: {
|
|
136
142
|
definitions: state.systemRoles.definitions,
|
|
@@ -140,9 +146,9 @@ export function serializeInspectorState(state) {
|
|
|
140
146
|
definitions: state.featureFlags.definitions,
|
|
141
147
|
files: Array.from(state.featureFlags.files),
|
|
142
148
|
},
|
|
143
|
-
|
|
144
|
-
definitions: state.
|
|
145
|
-
files: Array.from(state.
|
|
149
|
+
virtualUsers: {
|
|
150
|
+
definitions: state.virtualUsers.definitions,
|
|
151
|
+
files: Array.from(state.virtualUsers.files),
|
|
146
152
|
},
|
|
147
153
|
variables: {
|
|
148
154
|
definitions: state.variables.definitions,
|
|
@@ -166,6 +172,7 @@ export function serializeInspectorState(state) {
|
|
|
166
172
|
permissions: {
|
|
167
173
|
definitions: state.permissions.definitions,
|
|
168
174
|
instances: state.permissions.instances,
|
|
175
|
+
global: state.permissions.global,
|
|
169
176
|
},
|
|
170
177
|
serviceAggregation: {
|
|
171
178
|
requiredServices: Array.from(state.serviceAggregation.requiredServices),
|
|
@@ -175,6 +182,7 @@ export function serializeInspectorState(state) {
|
|
|
175
182
|
allSingletonServices: state.serviceAggregation.allSingletonServices,
|
|
176
183
|
allWireServices: state.serviceAggregation.allWireServices,
|
|
177
184
|
},
|
|
185
|
+
serviceTypes: state.serviceTypes,
|
|
178
186
|
resolvedIOTypes: state.resolvedIOTypes,
|
|
179
187
|
middlewareGroupsMeta: state.middlewareGroupsMeta,
|
|
180
188
|
permissionsGroupsMeta: state.permissionsGroupsMeta,
|
|
@@ -212,6 +220,8 @@ export function deserializeInspectorState(data) {
|
|
|
212
220
|
addonRequiredParentServices: data.addonRequiredParentServices || [],
|
|
213
221
|
addonCreatedServices: data.addonCreatedServices || [],
|
|
214
222
|
addonServicesFactorySeen: data.addonServicesFactorySeen ?? false,
|
|
223
|
+
analytics: data.analytics,
|
|
224
|
+
outgoingWebhooks: data.outgoingWebhooks,
|
|
215
225
|
addonServerlessIncompatible: new Map(data.addonServerlessIncompatible || []),
|
|
216
226
|
configFactories: new Map(data.configFactories),
|
|
217
227
|
serverLifecycleFactories: new Map(data.serverLifecycleFactories),
|
|
@@ -227,7 +237,7 @@ export function deserializeInspectorState(data) {
|
|
|
227
237
|
meta: data.functions.meta,
|
|
228
238
|
files: new Map(data.functions.files),
|
|
229
239
|
approvalDescriptions: data.functions.approvalDescriptions || {},
|
|
230
|
-
dynamicImportIds: new Set(),
|
|
240
|
+
dynamicImportIds: new Set(data.functions.dynamicImportIds ?? []),
|
|
231
241
|
},
|
|
232
242
|
http: {
|
|
233
243
|
metaInputTypes: new Map(data.http.metaInputTypes),
|
|
@@ -321,6 +331,9 @@ export function deserializeInspectorState(data) {
|
|
|
321
331
|
scopes: {
|
|
322
332
|
definitions: data.scopes?.definitions || [],
|
|
323
333
|
files: new Set(data.scopes?.files || []),
|
|
334
|
+
declared: data.scopes?.declared
|
|
335
|
+
? new Map(data.scopes.declared)
|
|
336
|
+
: undefined,
|
|
324
337
|
},
|
|
325
338
|
systemRoles: {
|
|
326
339
|
definitions: data.systemRoles?.definitions || [],
|
|
@@ -330,9 +343,9 @@ export function deserializeInspectorState(data) {
|
|
|
330
343
|
definitions: data.featureFlags?.definitions || [],
|
|
331
344
|
files: new Set(data.featureFlags?.files || []),
|
|
332
345
|
},
|
|
333
|
-
|
|
334
|
-
definitions: data.
|
|
335
|
-
files: new Set(data.
|
|
346
|
+
virtualUsers: {
|
|
347
|
+
definitions: data.virtualUsers?.definitions || [],
|
|
348
|
+
files: new Set(data.virtualUsers?.files || []),
|
|
336
349
|
},
|
|
337
350
|
variables: {
|
|
338
351
|
definitions: data.variables?.definitions || [],
|
|
@@ -356,6 +369,7 @@ export function deserializeInspectorState(data) {
|
|
|
356
369
|
permissions: {
|
|
357
370
|
definitions: data.permissions.definitions,
|
|
358
371
|
instances: data.permissions.instances || {},
|
|
372
|
+
global: data.permissions.global || [],
|
|
359
373
|
},
|
|
360
374
|
serviceAggregation: {
|
|
361
375
|
requiredServices: new Set(data.serviceAggregation.requiredServices),
|
|
@@ -365,6 +379,7 @@ export function deserializeInspectorState(data) {
|
|
|
365
379
|
allSingletonServices: data.serviceAggregation.allSingletonServices,
|
|
366
380
|
allWireServices: data.serviceAggregation.allWireServices,
|
|
367
381
|
},
|
|
382
|
+
serviceTypes: data.serviceTypes,
|
|
368
383
|
resolvedIOTypes: data.resolvedIOTypes || {},
|
|
369
384
|
middlewareGroupsMeta: data.middlewareGroupsMeta || {
|
|
370
385
|
definitions: {},
|
|
@@ -3,18 +3,18 @@ import type { InspectorLogger } from '../types.js';
|
|
|
3
3
|
/**
|
|
4
4
|
* Claims the one call site a single-declaration construct is allowed.
|
|
5
5
|
*
|
|
6
|
-
* `
|
|
6
|
+
* `defineVirtualUsers` takes a keyed object, so one call already declares as many
|
|
7
7
|
* entries as you like. Letting the call itself repeat buys nothing and costs
|
|
8
8
|
* the thing that matters: a single place to read the declared set from, and a
|
|
9
9
|
* single place to add to it. The same rule `pikkuBetterAuth` has had all along.
|
|
10
10
|
*
|
|
11
11
|
* A second call in the SAME file is refused too — "the file" is not an answer
|
|
12
|
-
* to "where do I add a
|
|
12
|
+
* to "where do I add a virtual user?" when the file holds two calls, so allowing it
|
|
13
13
|
* would keep the ambiguity the rule exists to remove.
|
|
14
14
|
*
|
|
15
15
|
* A generated file is exempt, and never claims the slot either. The rule exists
|
|
16
16
|
* to name the one place a person adds to, and nobody adds to a file the CLI
|
|
17
|
-
* rewrites on the next run — `pikku-
|
|
17
|
+
* rewrites on the next run — `pikku-virtual-users.gen.ts` would otherwise make every
|
|
18
18
|
* project that scaffolds one unbuildable, and take the app's own declaration
|
|
19
19
|
* down with it.
|
|
20
20
|
*
|
|
@@ -1,18 +1,18 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Claims the one call site a single-declaration construct is allowed.
|
|
3
3
|
*
|
|
4
|
-
* `
|
|
4
|
+
* `defineVirtualUsers` takes a keyed object, so one call already declares as many
|
|
5
5
|
* entries as you like. Letting the call itself repeat buys nothing and costs
|
|
6
6
|
* the thing that matters: a single place to read the declared set from, and a
|
|
7
7
|
* single place to add to it. The same rule `pikkuBetterAuth` has had all along.
|
|
8
8
|
*
|
|
9
9
|
* A second call in the SAME file is refused too — "the file" is not an answer
|
|
10
|
-
* to "where do I add a
|
|
10
|
+
* to "where do I add a virtual user?" when the file holds two calls, so allowing it
|
|
11
11
|
* would keep the ambiguity the rule exists to remove.
|
|
12
12
|
*
|
|
13
13
|
* A generated file is exempt, and never claims the slot either. The rule exists
|
|
14
14
|
* to name the one place a person adds to, and nobody adds to a file the CLI
|
|
15
|
-
* rewrites on the next run — `pikku-
|
|
15
|
+
* rewrites on the next run — `pikku-virtual-users.gen.ts` would otherwise make every
|
|
16
16
|
* project that scaffolds one unbuildable, and take the app's own declaration
|
|
17
17
|
* down with it.
|
|
18
18
|
*
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
import type { InspectorLogger, InspectorState } from '../types.js';
|
|
2
|
+
/** Warns about a workflow with at most one RPC step and nothing to wait on or fan out: it is a function or a queue job wearing a workflow's cost. */
|
|
3
|
+
export declare function validateSingleStepWorkflows(logger: InspectorLogger, state: InspectorState): void;
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { ErrorCode } from '../error-codes.js';
|
|
2
|
+
const DURABLE_FLOWS = new Set([
|
|
3
|
+
'sleep',
|
|
4
|
+
'suspend',
|
|
5
|
+
'approval',
|
|
6
|
+
'parallel',
|
|
7
|
+
'fanout',
|
|
8
|
+
]);
|
|
9
|
+
/** Warns about a workflow with at most one RPC step and nothing to wait on or fan out: it is a function or a queue job wearing a workflow's cost. */
|
|
10
|
+
export function validateSingleStepWorkflows(logger, state) {
|
|
11
|
+
for (const [name, graph] of Object.entries(state.workflows.graphMeta)) {
|
|
12
|
+
if (graph.source === 'scenario')
|
|
13
|
+
continue;
|
|
14
|
+
const file = state.workflows.files.get(graph.pikkuFuncId)?.path ??
|
|
15
|
+
state.workflows.graphFiles.get(name)?.path;
|
|
16
|
+
if (!file)
|
|
17
|
+
continue;
|
|
18
|
+
const nodes = Object.values(graph.nodes);
|
|
19
|
+
const rpcSteps = nodes.filter((n) => 'rpcName' in n && n.rpcName);
|
|
20
|
+
if (rpcSteps.length > 1)
|
|
21
|
+
continue;
|
|
22
|
+
if (nodes.some((n) => 'flow' in n && DURABLE_FLOWS.has(n.flow)))
|
|
23
|
+
continue;
|
|
24
|
+
logger.diagnostic({
|
|
25
|
+
severity: 'warn',
|
|
26
|
+
code: ErrorCode.SINGLE_STEP_WORKFLOW,
|
|
27
|
+
message: `Workflow '${name}' (${file}) runs ${rpcSteps.length === 0 ? 'no RPC step' : 'a single RPC step'} ` +
|
|
28
|
+
`and never sleeps, suspends or fans out, so it buys nothing a workflow is for. ` +
|
|
29
|
+
`Call the function directly, or enqueue it on a queue worker if it must survive a restart.`,
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import * as ts from 'typescript';
|
|
2
|
-
import { isWorkflowDoCall, isWorkflowExpectEventuallyCall, isScenarioStepCall, isScenarioExpectationCall, getScenarioStepPhase, DYNAMIC_SCENARIO_STEP_TARGET,
|
|
2
|
+
import { isWorkflowDoCall, isWorkflowExpectEventuallyCall, isScenarioStepCall, isScenarioExpectationCall, getScenarioStepPhase, DYNAMIC_SCENARIO_STEP_TARGET, extractVirtualUserFromOptions, isWorkflowSleepCall, isWorkflowSuspendCall, isWorkflowMilestoneCall, isWorkflowApprovalCall, isThrowCancelException, extractCancelReason, isParallelFanout, isParallelGroup, isSequentialFanout, isArrayFilter, isArraySome, isArrayEvery, extractForOfVariable, isArrayType, getSourceText, extractSourcePath, } from './patterns.js';
|
|
3
3
|
import { validateNoDisallowedPatterns, validateAwaitedCalls, formatValidationErrors, } from './validation.js';
|
|
4
4
|
import { extractStringLiteral, extractNumberLiteral, } from '../../extract-node-value.js';
|
|
5
5
|
/**
|
|
@@ -571,7 +571,7 @@ function extractRpcStep(call, context, outputVar) {
|
|
|
571
571
|
outputVar,
|
|
572
572
|
inputs,
|
|
573
573
|
options,
|
|
574
|
-
|
|
574
|
+
virtualUser: extractVirtualUserFromOptions(optionsArg),
|
|
575
575
|
expectEventually: expectEventually || undefined,
|
|
576
576
|
};
|
|
577
577
|
}
|
|
@@ -623,7 +623,7 @@ function extractScenarioStep(call, context, outputVar) {
|
|
|
623
623
|
outputVar,
|
|
624
624
|
inputs,
|
|
625
625
|
options,
|
|
626
|
-
|
|
626
|
+
virtualUser: extractVirtualUserFromOptions(optionsArg),
|
|
627
627
|
};
|
|
628
628
|
}
|
|
629
629
|
catch (error) {
|
|
@@ -13,13 +13,7 @@ export declare function isWorkflowWireIdentifier(expr: ts.Expression): boolean;
|
|
|
13
13
|
* (both are RPC steps; expectEventually is the polling variant used by scenarios)
|
|
14
14
|
*/
|
|
15
15
|
export declare function isWorkflowDoCall(node: ts.CallExpression, checker: ts.TypeChecker): boolean;
|
|
16
|
-
|
|
17
|
-
* Placeholder recorded when a scenario step's target isn't a static string
|
|
18
|
-
* literal. The step is kept rather than dropped so post-processing can raise
|
|
19
|
-
* PKU678 against it — a dropped step is the silent-failure mode this repo has
|
|
20
|
-
* hit repeatedly on workflow graph round-trips.
|
|
21
|
-
*/
|
|
22
|
-
export declare const DYNAMIC_SCENARIO_STEP_TARGET = "<dynamic>";
|
|
16
|
+
export { DYNAMIC_SCENARIO_STEP_TARGET } from '../../inspector-constants.js';
|
|
23
17
|
declare const SCENARIO_STEP_PHASES: readonly ["given", "when", "then"];
|
|
24
18
|
export type ScenarioStepPhaseName = (typeof SCENARIO_STEP_PHASES)[number];
|
|
25
19
|
/**
|
|
@@ -44,11 +38,11 @@ export declare function isScenarioExpectationCall(node: ts.CallExpression): bool
|
|
|
44
38
|
*/
|
|
45
39
|
export declare function isWorkflowExpectEventuallyCall(node: ts.CallExpression): boolean;
|
|
46
40
|
/**
|
|
47
|
-
* Extract the
|
|
48
|
-
* `{
|
|
41
|
+
* Extract the virtual user NAME from a step options object literal:
|
|
42
|
+
* `{ virtualUser: virtualUsers.x }` → 'x' (also `virtualUsers['x']`). Undefined when absent
|
|
49
43
|
* or not statically resolvable.
|
|
50
44
|
*/
|
|
51
|
-
export declare function
|
|
45
|
+
export declare function extractVirtualUserFromOptions(optionsArg: ts.Expression | undefined): string | undefined;
|
|
52
46
|
/**
|
|
53
47
|
* Check if a call expression is workflow.sleep()
|
|
54
48
|
*/
|
|
@@ -117,4 +111,3 @@ export declare function isArrayType(type: ts.Type, checker: ts.TypeChecker): boo
|
|
|
117
111
|
* Get the source text of a node (for condition expressions)
|
|
118
112
|
*/
|
|
119
113
|
export declare function getSourceText(node: ts.Node): string;
|
|
120
|
-
export {};
|
|
@@ -24,13 +24,7 @@ export function isWorkflowDoCall(node, checker) {
|
|
|
24
24
|
propAccess.name.text === 'expectEventually') &&
|
|
25
25
|
isWorkflowWireIdentifier(propAccess.expression));
|
|
26
26
|
}
|
|
27
|
-
|
|
28
|
-
* Placeholder recorded when a scenario step's target isn't a static string
|
|
29
|
-
* literal. The step is kept rather than dropped so post-processing can raise
|
|
30
|
-
* PKU678 against it — a dropped step is the silent-failure mode this repo has
|
|
31
|
-
* hit repeatedly on workflow graph round-trips.
|
|
32
|
-
*/
|
|
33
|
-
export const DYNAMIC_SCENARIO_STEP_TARGET = '<dynamic>';
|
|
27
|
+
export { DYNAMIC_SCENARIO_STEP_TARGET } from '../../inspector-constants.js';
|
|
34
28
|
const SCENARIO_STEP_PHASES = ['given', 'when', 'then'];
|
|
35
29
|
/**
|
|
36
30
|
* Check if a call expression is scenario.given()/when()/then() — a declared
|
|
@@ -88,17 +82,17 @@ export function isWorkflowExpectEventuallyCall(node) {
|
|
|
88
82
|
isWorkflowWireIdentifier(node.expression.expression));
|
|
89
83
|
}
|
|
90
84
|
/**
|
|
91
|
-
* Extract the
|
|
92
|
-
* `{
|
|
85
|
+
* Extract the virtual user NAME from a step options object literal:
|
|
86
|
+
* `{ virtualUser: virtualUsers.x }` → 'x' (also `virtualUsers['x']`). Undefined when absent
|
|
93
87
|
* or not statically resolvable.
|
|
94
88
|
*/
|
|
95
|
-
export function
|
|
89
|
+
export function extractVirtualUserFromOptions(optionsArg) {
|
|
96
90
|
if (!optionsArg || !ts.isObjectLiteralExpression(optionsArg))
|
|
97
91
|
return;
|
|
98
92
|
for (const prop of optionsArg.properties) {
|
|
99
93
|
if (ts.isPropertyAssignment(prop) &&
|
|
100
94
|
ts.isIdentifier(prop.name) &&
|
|
101
|
-
prop.name.text === '
|
|
95
|
+
prop.name.text === 'virtualUser') {
|
|
102
96
|
const init = prop.initializer;
|
|
103
97
|
if (ts.isPropertyAccessExpression(init)) {
|
|
104
98
|
return init.name.text;
|