@pikku/inspector 0.12.106 → 0.12.108
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 +4 -0
- package/dist/add/add-functions.js +27 -31
- 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/check-indirect-registrations.d.ts +9 -0
- package/dist/add/check-indirect-registrations.js +114 -0
- package/dist/error-codes.d.ts +1 -0
- package/dist/error-codes.js +1 -0
- 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 -302
- 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 +32 -0
- 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 +1 -3
- 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 +14 -0
- package/dist/utils/inspector-constants.d.ts +16 -0
- package/dist/utils/inspector-constants.js +16 -0
- package/dist/utils/post-process.d.ts +1 -2
- package/dist/utils/post-process.js +27 -27
- package/dist/utils/read-jsx-literal-text.js +31 -1
- 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 +7 -1
- package/dist/utils/serialize-inspector-state.js +16 -1
- package/dist/utils/workflow/dsl/patterns.d.ts +1 -8
- package/dist/utils/workflow/dsl/patterns.js +1 -7
- package/dist/visit.js +2 -0
- package/package.json +25 -9
- package/CHANGELOG.md +0 -4368
- package/OPTIMIZATION-PLAN.md +0 -201
- package/knowledge/decisions/a-channel-with-auth-false-marks-its-connect-and-disconnect-sessionless.md +0 -22
- package/knowledge/decisions/a-declared-scope-id-never-shadows-a-definition.md +0 -20
- package/knowledge/decisions/a-dsl-step-virtual-user-is-the-literal-name-in-its-options.md +0 -27
- package/knowledge/decisions/a-feature-flags-anyof-is-read-from-the-ast-and-an-empty-list-is-refused.md +0 -20
- package/knowledge/decisions/a-non-literal-string-array-reads-as-absent.md +0 -25
- package/knowledge/decisions/a-permission-with-an-underscore-data-parameter-takes-no-data.md +0 -20
- package/knowledge/decisions/a-provider-login-virtual-user-is-never-runnable.md +0 -27
- package/knowledge/decisions/a-remote-addon-bound-credential-or-secret-must-be-wired.md +0 -27
- package/knowledge/decisions/a-route-auth-overrides-its-groups-auth.md +0 -22
- package/knowledge/decisions/a-route-requires-a-session-when-any-auth-signal-says-so.md +0 -29
- package/knowledge/decisions/a-webhook-source-with-verify-gets-a-singleton-secret-credential.md +0 -21
- package/knowledge/decisions/a-wire-with-auth-disabled-cannot-point-at-a-pikkufunc.md +0 -21
- package/knowledge/decisions/addon-contracts-cannot-wire-or-declare-handlers.md +0 -25
- package/knowledge/decisions/addon-declarations-name-the-secrets-and-credentials-the-app-lends-it.md +0 -19
- package/knowledge/decisions/an-inline-wiring-function-is-sessionless-unless-built-with-pikkufunc.md +0 -23
- package/knowledge/decisions/better-auth-admin-plugin-is-refused.md +0 -24
- package/knowledge/decisions/define-screens-scopes-must-be-string-literals.md +0 -18
- package/knowledge/decisions/index.md +0 -38
- package/knowledge/decisions/inspector-records-a-non-literal-global-grant-as-source-text.md +0 -20
- package/knowledge/decisions/only-one-pikku-better-auth-export-per-codebase.md +0 -19
- package/knowledge/decisions/scope-references-and-secret-aliases-are-critical-diagnostics.md +0 -23
- package/knowledge/decisions/scope-segments-may-not-be-wildcards-or-contain-colons.md +0 -23
- package/knowledge/decisions/shorthand-permission-entries-are-extracted.md +0 -22
- package/knowledge/decisions/system-roles-declare-literal-unqualified-scopes.md +0 -28
- package/knowledge/decisions/the-inspector-records-agent-permissions-only-to-aggregate-services.md +0 -14
- package/knowledge/decisions/vault-secrets-reach-no-sink-and-pii-stays-out-of-logs-and-webhooks.md +0 -30
- package/knowledge/decisions/webhook-gateway-wrappers-are-sessionless-and-gateway-auth-runs-inside-the-handler.md +0 -26
- package/knowledge/index.md +0 -14
- package/knowledge/questions/does-a-non-literal-remote-addon-auth-get-checked.md +0 -26
- package/knowledge/questions/index.md +0 -16
- package/knowledge/questions/is-an-incomplete-oauth2-block-dropped-without-a-log.md +0 -23
- package/knowledge/questions/should-app-functions-named-like-scenario-instrumentation-be-refused.md +0 -28
- package/knowledge/questions/should-the-openapi-document-default-every-operation-to-a-bearer-or-api-key.md +0 -22
- package/run-tests.sh +0 -68
- package/src/utils/test-data/inspector-state.json +0 -1687
|
@@ -1,15 +1,11 @@
|
|
|
1
1
|
import { existsSync, readFileSync } from 'node:fs';
|
|
2
2
|
import { createRequire } from 'node:module';
|
|
3
3
|
import { join } from 'node:path';
|
|
4
|
-
import { extractTypeKeys } from './type-utils.js';
|
|
5
4
|
import { ErrorCode } from '../error-codes.js';
|
|
6
|
-
import { findSecretAliasServices } from './secret-alias-services.js';
|
|
7
5
|
import { deriveOAuth2AppSecrets } from '@pikku/core/secret';
|
|
8
|
-
import { resolveCoreType } from './resolve-core-type.js';
|
|
9
6
|
import { relative } from 'node:path';
|
|
10
|
-
import { AUTH_HANDLER_FUNC_ID } from '
|
|
7
|
+
import { AUTH_HANDLER_FUNC_ID, DYNAMIC_SCENARIO_STEP_TARGET, } from './inspector-constants.js';
|
|
11
8
|
import { flattenScopeDefinitions } from '@pikku/core/scope';
|
|
12
|
-
import { DYNAMIC_SCENARIO_STEP_TARGET } from './workflow/dsl/patterns.js';
|
|
13
9
|
import { addonResolutionDirs } from './addon-resolution.js';
|
|
14
10
|
/**
|
|
15
11
|
* Stamp the inspected authorize/callbacks service set onto the generated auth
|
|
@@ -72,26 +68,25 @@ function expandAndAddGroupServices(list, state, addServices) {
|
|
|
72
68
|
}
|
|
73
69
|
}
|
|
74
70
|
/**
|
|
75
|
-
*
|
|
76
|
-
*
|
|
77
|
-
*
|
|
71
|
+
* Applies the service names of the SingletonServices and Services types, which
|
|
72
|
+
* `resolveServiceTypes` read off the checker during analysis. This provides the
|
|
73
|
+
* complete list of available services for code generation.
|
|
74
|
+
* Skipped when analysis did not resolve them (e.g. an older serialized state).
|
|
78
75
|
*/
|
|
79
76
|
function extractAllServices(state) {
|
|
80
|
-
|
|
81
|
-
if (!
|
|
77
|
+
const resolved = state.serviceTypes;
|
|
78
|
+
if (!resolved) {
|
|
82
79
|
return;
|
|
83
80
|
}
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
81
|
+
if (resolved.singletonServices) {
|
|
82
|
+
state.serviceAggregation.allSingletonServices = [
|
|
83
|
+
...resolved.singletonServices,
|
|
84
|
+
];
|
|
88
85
|
}
|
|
89
|
-
|
|
90
|
-
const servicesType = resolveCoreType(state.typesLookup, state.wireServicesTypeImportMap);
|
|
91
|
-
if (servicesType) {
|
|
86
|
+
if (resolved.services) {
|
|
92
87
|
// Wire services are those in Services but not in SingletonServices
|
|
93
88
|
const singletonSet = new Set(state.serviceAggregation.allSingletonServices);
|
|
94
|
-
state.serviceAggregation.allWireServices =
|
|
89
|
+
state.serviceAggregation.allWireServices = resolved.services
|
|
95
90
|
.filter((name) => !singletonSet.has(name))
|
|
96
91
|
.sort();
|
|
97
92
|
markSingletonServicesOnly(state, new Set(state.serviceAggregation.allWireServices));
|
|
@@ -170,6 +165,18 @@ export function aggregateRequiredServices(state) {
|
|
|
170
165
|
addServices(permissionMeta.services);
|
|
171
166
|
}
|
|
172
167
|
});
|
|
168
|
+
// 3b. Global permissions and global middleware apply to every function, so
|
|
169
|
+
// whatever they touch is required wherever any function runs.
|
|
170
|
+
for (const registration of state.permissions?.global ?? []) {
|
|
171
|
+
for (const definitionId of registration.definitionIds) {
|
|
172
|
+
addServices(state.permissions.definitions[definitionId]?.services);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
for (const [instanceId, instance] of Object.entries(state.middleware?.instances ?? {})) {
|
|
176
|
+
if (instanceId.startsWith('global:')) {
|
|
177
|
+
addServices(state.middleware.definitions[instance.definitionId]?.services);
|
|
178
|
+
}
|
|
179
|
+
}
|
|
173
180
|
// 4. Services from middleware/permission groups used in wirings
|
|
174
181
|
// We need to check all wirings and expand any tag/HTTP-pattern groups they use
|
|
175
182
|
for (const method of [
|
|
@@ -1172,15 +1179,8 @@ export function validateSchemaWiringSeparation(logger, state) {
|
|
|
1172
1179
|
* a name the omit does not cover. A name-based rule misses this; the type does
|
|
1173
1180
|
* not.
|
|
1174
1181
|
*/
|
|
1175
|
-
export function validateNoSecretAliasServices(logger,
|
|
1176
|
-
|
|
1177
|
-
return;
|
|
1178
|
-
}
|
|
1179
|
-
const singletonServicesType = resolveCoreType(state.typesLookup, state.singletonServicesTypeImportMap);
|
|
1180
|
-
if (!singletonServicesType) {
|
|
1181
|
-
return;
|
|
1182
|
-
}
|
|
1183
|
-
const aliases = findSecretAliasServices(singletonServicesType, checker);
|
|
1182
|
+
export function validateNoSecretAliasServices(logger, state) {
|
|
1183
|
+
const aliases = state.serviceTypes?.secretAliases ?? [];
|
|
1184
1184
|
if (aliases.length === 0) {
|
|
1185
1185
|
return;
|
|
1186
1186
|
}
|
|
@@ -42,6 +42,34 @@ const NON_TEXT_ATTRIBUTES = new Set([
|
|
|
42
42
|
'value',
|
|
43
43
|
'key',
|
|
44
44
|
]);
|
|
45
|
+
/**
|
|
46
|
+
* Calls whose string arguments are class lists. A literal anywhere inside one —
|
|
47
|
+
* `cva('…', { variants: { size: { heading: 'font-heading text-xl' } } })` — is
|
|
48
|
+
* styling, even under a key like `heading` that would otherwise read as text.
|
|
49
|
+
*/
|
|
50
|
+
const CLASS_BUILDERS = new Set([
|
|
51
|
+
'cn',
|
|
52
|
+
'cva',
|
|
53
|
+
'clsx',
|
|
54
|
+
'cx',
|
|
55
|
+
'classNames',
|
|
56
|
+
'classnames',
|
|
57
|
+
'tv',
|
|
58
|
+
'twMerge',
|
|
59
|
+
'twJoin',
|
|
60
|
+
]);
|
|
61
|
+
const isInsideClassBuilder = (node) => {
|
|
62
|
+
for (let at = node.parent; at; at = at.parent) {
|
|
63
|
+
if (ts.isJsxElement(at) || ts.isJsxSelfClosingElement(at))
|
|
64
|
+
return false;
|
|
65
|
+
if (ts.isCallExpression(at) &&
|
|
66
|
+
ts.isIdentifier(at.expression) &&
|
|
67
|
+
CLASS_BUILDERS.has(at.expression.text)) {
|
|
68
|
+
return true;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
return false;
|
|
72
|
+
};
|
|
45
73
|
const isProse = (text) => /^\p{Lu}[^\s]*(\s+[^\s]+)+/u.test(text.trim()) &&
|
|
46
74
|
!/[:/[\]=_#]/.test(text.split(/\s+/)[0] ?? '');
|
|
47
75
|
const isSentence = (text) => /\p{L}+\s+\p{L}+/u.test(text);
|
|
@@ -72,7 +100,9 @@ export const readJsxLiteralText = (fileName, content) => {
|
|
|
72
100
|
const found = [];
|
|
73
101
|
const seen = new Set();
|
|
74
102
|
const add = (node, text) => {
|
|
75
|
-
if (!hasLetter(text) ||
|
|
103
|
+
if (!hasLetter(text) ||
|
|
104
|
+
seen.has(node.getStart(source)) ||
|
|
105
|
+
isInsideClassBuilder(node))
|
|
76
106
|
return;
|
|
77
107
|
seen.add(node.getStart(source));
|
|
78
108
|
const { line } = source.getLineAndCharacterOfPosition(node.getStart(source));
|
|
@@ -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'];
|
|
@@ -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,
|
|
@@ -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 || [],
|
|
@@ -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: {},
|
|
@@ -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
|
/**
|
|
@@ -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
|
package/dist/visit.js
CHANGED
|
@@ -35,6 +35,7 @@ import { addAnalytics } from './add/add-analytics.js';
|
|
|
35
35
|
import { addOutgoingWebhook } from './add/add-outgoing-webhook.js';
|
|
36
36
|
import { addAgentScorer } from './add/add-agent-scorer.js';
|
|
37
37
|
import { addApprovalDescription } from './add/add-approval-description.js';
|
|
38
|
+
import { checkIndirectRegistrations } from './add/check-indirect-registrations.js';
|
|
38
39
|
export const visitSetup = (logger, checker, node, state, options) => {
|
|
39
40
|
addFileExtendsCoreType(node, checker, state.singletonServicesTypeImportMap, 'CoreSingletonServices', state);
|
|
40
41
|
addFileExtendsCoreType(node, checker, state.wireServicesTypeImportMap, 'CoreServices', state);
|
|
@@ -52,6 +53,7 @@ export const visitSetup = (logger, checker, node, state, options) => {
|
|
|
52
53
|
addWireRemoteAddon(node, state, logger);
|
|
53
54
|
addMiddleware(logger, node, checker, state, options);
|
|
54
55
|
addPermission(logger, node, checker, state, options);
|
|
56
|
+
checkIndirectRegistrations(logger, node, checker, state, options);
|
|
55
57
|
addApprovalDescription(logger, node, checker, state, options);
|
|
56
58
|
addWorkflow(logger, node, checker, state, options);
|
|
57
59
|
ts.forEachChild(node, (child) => visitSetup(logger, checker, child, state, options));
|