@pikku/inspector 0.12.75 → 0.12.81
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +330 -0
- package/dist/add/add-analytics.d.ts +9 -0
- package/dist/add/add-analytics.js +178 -0
- package/dist/add/add-auth.js +0 -54
- package/dist/add/add-feature-flag.d.ts +2 -0
- package/dist/add/add-feature-flag.js +162 -0
- package/dist/add/add-wire-addon.js +1 -0
- package/dist/add/add-wire-remote-addon.js +1 -0
- package/dist/index.d.ts +3 -1
- package/dist/index.js +3 -1
- package/dist/inspector.js +4 -0
- package/dist/surface/collect-surface.d.ts +58 -0
- package/dist/surface/collect-surface.js +613 -0
- package/dist/types.d.ts +20 -11
- package/dist/utils/filter-inspector-state.js +14 -0
- package/dist/utils/post-process.js +15 -0
- package/dist/utils/read-module-specifiers.d.ts +8 -0
- package/dist/utils/read-module-specifiers.js +23 -0
- package/dist/utils/read-tsconfig-out-dir.d.ts +5 -0
- package/dist/utils/read-tsconfig-out-dir.js +12 -0
- package/dist/utils/resolve-deploy-target.d.ts +7 -0
- package/dist/utils/resolve-deploy-target.js +15 -7
- package/dist/utils/serialize-inspector-state.d.ts +5 -0
- package/dist/utils/serialize-inspector-state.js +8 -0
- package/dist/visit.js +4 -0
- package/package.json +16 -6
- package/run-tests.sh +21 -6
- package/src/add/add-addon-bans.ts +0 -85
- package/src/add/add-agent-scorer.test.ts +0 -197
- package/src/add/add-agent-scorer.ts +0 -83
- package/src/add/add-agent.test.ts +0 -87
- package/src/add/add-agent.ts +0 -638
- package/src/add/add-approval-description.ts +0 -76
- package/src/add/add-auth.test.ts +0 -791
- package/src/add/add-auth.ts +0 -424
- package/src/add/add-channel.ts +0 -778
- package/src/add/add-cli-renderers.test.ts +0 -74
- package/src/add/add-cli.ts +0 -1006
- package/src/add/add-credential.ts +0 -186
- package/src/add/add-feature.test.ts +0 -306
- package/src/add/add-feature.ts +0 -253
- package/src/add/add-file-extends-core-type.ts +0 -66
- package/src/add/add-file-with-config.ts +0 -45
- package/src/add/add-file-with-factory.test.ts +0 -134
- package/src/add/add-file-with-factory.ts +0 -288
- package/src/add/add-functions.test.ts +0 -743
- package/src/add/add-functions.ts +0 -1548
- package/src/add/add-gateway.ts +0 -143
- package/src/add/add-http-route-ref.test.ts +0 -149
- package/src/add/add-http-route.ts +0 -530
- package/src/add/add-http-routes.ts +0 -471
- package/src/add/add-keyed-wiring.ts +0 -180
- package/src/add/add-mcp-prompt.ts +0 -157
- package/src/add/add-mcp-resource.ts +0 -175
- package/src/add/add-middleware.test.ts +0 -196
- package/src/add/add-middleware.ts +0 -879
- package/src/add/add-permission-auth.test.ts +0 -59
- package/src/add/add-permission.ts +0 -311
- package/src/add/add-personas.test.ts +0 -406
- package/src/add/add-personas.ts +0 -287
- package/src/add/add-queue-worker.ts +0 -117
- package/src/add/add-rpc-invocations.test.ts +0 -287
- package/src/add/add-rpc-invocations.ts +0 -221
- package/src/add/add-scenario-step.test.ts +0 -509
- package/src/add/add-schedule.ts +0 -120
- package/src/add/add-scope.test.ts +0 -419
- package/src/add/add-scope.ts +0 -221
- package/src/add/add-secret.ts +0 -10
- package/src/add/add-subject-scenario-steps.test.ts +0 -161
- package/src/add/add-system-role.test.ts +0 -297
- package/src/add/add-system-role.ts +0 -162
- package/src/add/add-trigger.ts +0 -165
- package/src/add/add-variable.ts +0 -10
- package/src/add/add-wire-addon.test.ts +0 -185
- package/src/add/add-wire-addon.ts +0 -146
- package/src/add/add-wire-remote-addon.ts +0 -80
- package/src/add/add-workflow-edge-cases.test.ts +0 -201
- package/src/add/add-workflow-fanout.test.ts +0 -166
- package/src/add/add-workflow-graph-foreach.test.ts +0 -174
- package/src/add/add-workflow-graph-input.test.ts +0 -94
- package/src/add/add-workflow-graph-notes.test.ts +0 -74
- package/src/add/add-workflow-graph.ts +0 -644
- package/src/add/add-workflow-on-error.test.ts +0 -118
- package/src/add/add-workflow-return-switch.test.ts +0 -121
- package/src/add/add-workflow-silent-drops.test.ts +0 -261
- package/src/add/add-workflow-sleep-expression.test.ts +0 -96
- package/src/add/add-workflow.test.ts +0 -155
- package/src/add/add-workflow.ts +0 -482
- package/src/add/addon-bans.test.ts +0 -121
- package/src/add/addon-contracts.test.ts +0 -221
- package/src/add/allow-escape-hatches.test.ts +0 -144
- package/src/add/audit-meta.test.ts +0 -75
- package/src/add/classified-sink-check.test.ts +0 -347
- package/src/add/config-value-metadata.test.ts +0 -263
- package/src/add/dynamic-import-check.test.ts +0 -119
- package/src/add/expect-eventually-scenario-only.test.ts +0 -108
- package/src/add/function-body-span.test.ts +0 -96
- package/src/add/inline-wiring-function.test.ts +0 -104
- package/src/add/pii-check.test.ts +0 -258
- package/src/add/rpc-type-cast.test.ts +0 -123
- package/src/add/scenario-instrumentation.test.ts +0 -89
- package/src/add/scenario-instrumentation.ts +0 -42
- package/src/add/scenario-marker.test.ts +0 -89
- package/src/add/scenario-not-extractable.test.ts +0 -159
- package/src/add/scenario-services.test.ts +0 -103
- package/src/add/scenario-skip.test.ts +0 -83
- package/src/add/validate-workflow-graph-addons.test.ts +0 -102
- package/src/add/wire-name-literal.test.ts +0 -117
- package/src/error-codes-emitted.test.ts +0 -76
- package/src/error-codes.ts +0 -145
- package/src/index.ts +0 -66
- package/src/inspector.ts +0 -575
- package/src/schema-generator.ts +0 -1
- package/src/source-file-cache.ts +0 -73
- package/src/tsconfig-paths.test.ts +0 -90
- package/src/types-map.ts +0 -141
- package/src/types.ts +0 -775
- package/src/utils/annotate-http-route-auth.test.ts +0 -161
- package/src/utils/annotate-http-route-auth.ts +0 -53
- package/src/utils/check-classified-sinks.ts +0 -215
- package/src/utils/check-pii-output.ts +0 -120
- package/src/utils/compute-diagnostics.test.ts +0 -106
- package/src/utils/compute-required-schemas.ts +0 -48
- package/src/utils/contract-hashes.test.ts +0 -553
- package/src/utils/contract-hashes.ts +0 -387
- package/src/utils/custom-types-generator.test.ts +0 -124
- package/src/utils/custom-types-generator.ts +0 -150
- package/src/utils/detect-schema-vendor.test.ts +0 -123
- package/src/utils/detect-schema-vendor.ts +0 -120
- package/src/utils/does-type-extend-core-type.ts +0 -53
- package/src/utils/ensure-function-metadata.ts +0 -385
- package/src/utils/extract-function-name.ts +0 -612
- package/src/utils/extract-function-node.ts +0 -70
- package/src/utils/extract-node-value.test.ts +0 -117
- package/src/utils/extract-node-value.ts +0 -172
- package/src/utils/extract-secret-usage.test.ts +0 -81
- package/src/utils/extract-secret-usage.ts +0 -73
- package/src/utils/extract-services.ts +0 -70
- package/src/utils/extract-surface-usage.test.ts +0 -147
- package/src/utils/extract-surface-usage.ts +0 -121
- package/src/utils/filter-inspector-state.test.ts +0 -2232
- package/src/utils/filter-inspector-state.ts +0 -1298
- package/src/utils/filter-utils.test.ts +0 -843
- package/src/utils/filter-utils.ts +0 -161
- package/src/utils/find-root-dir.ts +0 -68
- package/src/utils/find-zod-transform.test.ts +0 -70
- package/src/utils/find-zod-transform.ts +0 -103
- package/src/utils/get-exported-variable-name.ts +0 -48
- package/src/utils/get-files-and-methods.ts +0 -157
- package/src/utils/get-property-value.test.ts +0 -141
- package/src/utils/get-property-value.ts +0 -331
- package/src/utils/hash.ts +0 -26
- package/src/utils/literal-properties.ts +0 -97
- package/src/utils/load-addon-functions-meta.test.ts +0 -157
- package/src/utils/load-addon-functions-meta.ts +0 -508
- package/src/utils/load-addon-scopes.test.ts +0 -138
- package/src/utils/middleware.test.ts +0 -204
- package/src/utils/middleware.ts +0 -303
- package/src/utils/nested-project-filter.test.ts +0 -53
- package/src/utils/nested-project-filter.ts +0 -28
- package/src/utils/permissions-shorthand.test.ts +0 -79
- package/src/utils/permissions.test.ts +0 -125
- package/src/utils/permissions.ts +0 -129
- package/src/utils/post-process.test.ts +0 -1094
- package/src/utils/post-process.ts +0 -1604
- package/src/utils/resolve-addon-package.ts +0 -106
- package/src/utils/resolve-core-type.test.ts +0 -142
- package/src/utils/resolve-core-type.ts +0 -35
- package/src/utils/resolve-deploy-target.test.ts +0 -135
- package/src/utils/resolve-deploy-target.ts +0 -65
- package/src/utils/resolve-function-meta.ts +0 -25
- package/src/utils/resolve-function-types.ts +0 -42
- package/src/utils/resolve-identifier.ts +0 -46
- package/src/utils/resolve-ref-contract.ts +0 -71
- package/src/utils/resolve-versions.test.ts +0 -499
- package/src/utils/resolve-versions.ts +0 -228
- package/src/utils/schema-cache.test.ts +0 -124
- package/src/utils/schema-generator.test.ts +0 -272
- package/src/utils/schema-generator.ts +0 -780
- package/src/utils/secret-alias-services.test.ts +0 -146
- package/src/utils/secret-alias-services.ts +0 -58
- package/src/utils/serialize-inspector-state.ts +0 -742
- package/src/utils/serialize-mcp-json.ts +0 -150
- package/src/utils/serialize-middleware-groups-meta.ts +0 -36
- package/src/utils/serialize-openapi-json.ts +0 -277
- package/src/utils/serialize-permissions-groups-meta.ts +0 -7
- package/src/utils/single-declaration.ts +0 -57
- package/src/utils/type-utils.ts +0 -145
- package/src/utils/validate-auth-sessionless.ts +0 -30
- package/src/utils/validate-exposed-functions-gated.test.ts +0 -230
- package/src/utils/validate-exposed-functions-gated.ts +0 -103
- package/src/utils/validate-tags-resolve-to-middleware.test.ts +0 -108
- package/src/utils/validate-tags-resolve-to-middleware.ts +0 -48
- package/src/utils/workflow/derive-workflow-plan.test.ts +0 -178
- package/src/utils/workflow/derive-workflow-plan.ts +0 -117
- package/src/utils/workflow/dsl/deserialize-dsl-workflow.ts +0 -1465
- package/src/utils/workflow/dsl/deserialize-fanout-body.test.ts +0 -71
- package/src/utils/workflow/dsl/deserialize-fanout-fidelity.test.ts +0 -99
- package/src/utils/workflow/dsl/deserialize-graph-next.test.ts +0 -161
- package/src/utils/workflow/dsl/deserialize-roundtrip-bindings.test.ts +0 -121
- package/src/utils/workflow/dsl/deserialize-roundtrip-values.test.ts +0 -116
- package/src/utils/workflow/dsl/deserialize-roundtrip.test.ts +0 -145
- package/src/utils/workflow/dsl/extract-approval-policy.test.ts +0 -71
- package/src/utils/workflow/dsl/extract-dsl-workflow.ts +0 -2254
- package/src/utils/workflow/dsl/index.ts +0 -11
- package/src/utils/workflow/dsl/patterns.ts +0 -440
- package/src/utils/workflow/dsl/validation.ts +0 -203
- package/src/utils/workflow/graph/convert-dsl-to-graph-skip.test.ts +0 -35
- package/src/utils/workflow/graph/convert-dsl-to-graph.ts +0 -543
- package/src/utils/workflow/graph/fanout-node-identity.test.ts +0 -105
- package/src/utils/workflow/graph/finalize-workflow-wires.ts +0 -310
- package/src/utils/workflow/graph/finalize-workflows.test.ts +0 -50
- package/src/utils/workflow/graph/finalize-workflows.ts +0 -130
- package/src/utils/workflow/graph/index.ts +0 -28
- package/src/utils/workflow/graph/serialize-workflow-graph.ts +0 -240
- package/src/utils/workflow/graph/workflow-graph.types.ts +0 -281
- package/src/visit.ts +0 -181
- package/tsconfig.json +0 -19
- package/tsconfig.tsbuildinfo +0 -1
|
@@ -1,117 +0,0 @@
|
|
|
1
|
-
import { test, describe } from 'node:test'
|
|
2
|
-
import { strict as assert } from 'node:assert'
|
|
3
|
-
import * as ts from 'typescript'
|
|
4
|
-
import { extractDescription, extractStringLiteral } from './extract-node-value'
|
|
5
|
-
|
|
6
|
-
const createChecker = (source: string) => {
|
|
7
|
-
const sourceFile = ts.createSourceFile(
|
|
8
|
-
'test.ts',
|
|
9
|
-
source,
|
|
10
|
-
ts.ScriptTarget.Latest,
|
|
11
|
-
true,
|
|
12
|
-
ts.ScriptKind.TS
|
|
13
|
-
)
|
|
14
|
-
const host = ts.createCompilerHost({})
|
|
15
|
-
const originalGetSourceFile = host.getSourceFile
|
|
16
|
-
host.getSourceFile = (fileName, target) => {
|
|
17
|
-
if (fileName === 'test.ts') return sourceFile
|
|
18
|
-
return originalGetSourceFile.call(host, fileName, target)
|
|
19
|
-
}
|
|
20
|
-
const program = ts.createProgram(['test.ts'], {}, host)
|
|
21
|
-
return { checker: program.getTypeChecker(), sourceFile }
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
const findObjectLiteral = (
|
|
25
|
-
node: ts.Node
|
|
26
|
-
): ts.ObjectLiteralExpression | undefined => {
|
|
27
|
-
if (ts.isObjectLiteralExpression(node)) return node
|
|
28
|
-
let result: ts.ObjectLiteralExpression | undefined
|
|
29
|
-
ts.forEachChild(node, (child) => {
|
|
30
|
-
if (!result) result = findObjectLiteral(child)
|
|
31
|
-
})
|
|
32
|
-
return result
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
describe('extractDescription', () => {
|
|
36
|
-
test('returns null when node is undefined', () => {
|
|
37
|
-
const { checker } = createChecker('')
|
|
38
|
-
assert.equal(extractDescription(undefined, checker), null)
|
|
39
|
-
})
|
|
40
|
-
|
|
41
|
-
test('extracts string literal description', () => {
|
|
42
|
-
const { checker, sourceFile } = createChecker(
|
|
43
|
-
`const opts = { description: 'my step' }`
|
|
44
|
-
)
|
|
45
|
-
const obj = findObjectLiteral(sourceFile)!
|
|
46
|
-
assert.equal(extractDescription(obj, checker), 'my step')
|
|
47
|
-
})
|
|
48
|
-
|
|
49
|
-
test('extracts concatenated string literals in description', () => {
|
|
50
|
-
const { checker, sourceFile } = createChecker(
|
|
51
|
-
`const data = { description: 'line one ' + 'line two' }`
|
|
52
|
-
)
|
|
53
|
-
const obj = findObjectLiteral(sourceFile)!
|
|
54
|
-
assert.equal(extractDescription(obj, checker), 'line one line two')
|
|
55
|
-
})
|
|
56
|
-
|
|
57
|
-
test('extracts deeply nested concatenation in description', () => {
|
|
58
|
-
const { checker, sourceFile } = createChecker(
|
|
59
|
-
`const data = { description: 'a' + 'b' + 'c' }`
|
|
60
|
-
)
|
|
61
|
-
const obj = findObjectLiteral(sourceFile)!
|
|
62
|
-
assert.equal(extractDescription(obj, checker), 'abc')
|
|
63
|
-
})
|
|
64
|
-
|
|
65
|
-
test('returns null for non-object node', () => {
|
|
66
|
-
const { checker, sourceFile } = createChecker(`const x = 42`)
|
|
67
|
-
assert.equal(extractDescription(sourceFile, checker), null)
|
|
68
|
-
})
|
|
69
|
-
})
|
|
70
|
-
|
|
71
|
-
describe('extractStringLiteral — concatenation/template symmetry', () => {
|
|
72
|
-
const findInitializer = (node: ts.Node): ts.Expression | undefined => {
|
|
73
|
-
if (ts.isVariableDeclaration(node) && node.initializer) {
|
|
74
|
-
return node.initializer
|
|
75
|
-
}
|
|
76
|
-
let result: ts.Expression | undefined
|
|
77
|
-
ts.forEachChild(node, (child) => {
|
|
78
|
-
if (!result) result = findInitializer(child)
|
|
79
|
-
})
|
|
80
|
-
return result
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
test('a `+` operand that cannot be statically resolved becomes a ${...} placeholder', () => {
|
|
84
|
-
const { checker, sourceFile } = createChecker(
|
|
85
|
-
`const x = 'Enrich event ' + (event.id ?? event.name)`
|
|
86
|
-
)
|
|
87
|
-
const init = findInitializer(sourceFile)!
|
|
88
|
-
assert.equal(
|
|
89
|
-
extractStringLiteral(init, checker),
|
|
90
|
-
'Enrich event ${event.id ?? event.name}'
|
|
91
|
-
)
|
|
92
|
-
})
|
|
93
|
-
|
|
94
|
-
test('`+` concatenation and template literal produce the same display string', () => {
|
|
95
|
-
const concat = createChecker(
|
|
96
|
-
`const x = 'Enrich event ' + (event.id ?? event.name)`
|
|
97
|
-
)
|
|
98
|
-
const template = createChecker(
|
|
99
|
-
'const x = `Enrich event ${event.id ?? event.name}`'
|
|
100
|
-
)
|
|
101
|
-
const concatValue = extractStringLiteral(
|
|
102
|
-
findInitializer(concat.sourceFile)!,
|
|
103
|
-
concat.checker
|
|
104
|
-
)
|
|
105
|
-
const templateValue = extractStringLiteral(
|
|
106
|
-
findInitializer(template.sourceFile)!,
|
|
107
|
-
template.checker
|
|
108
|
-
)
|
|
109
|
-
assert.equal(concatValue, templateValue)
|
|
110
|
-
})
|
|
111
|
-
|
|
112
|
-
test('still resolves fully-static concatenation exactly', () => {
|
|
113
|
-
const { checker, sourceFile } = createChecker(`const x = 'a' + 'b' + 'c'`)
|
|
114
|
-
const init = findInitializer(sourceFile)!
|
|
115
|
-
assert.equal(extractStringLiteral(init, checker), 'abc')
|
|
116
|
-
})
|
|
117
|
-
})
|
|
@@ -1,172 +0,0 @@
|
|
|
1
|
-
import * as ts from 'typescript'
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* Extract string literal value from a TypeScript node.
|
|
5
|
-
* Handles string literals, template literals (including placeholders),
|
|
6
|
-
* and constant variable references.
|
|
7
|
-
*/
|
|
8
|
-
export function extractStringLiteral(
|
|
9
|
-
node: ts.Node,
|
|
10
|
-
checker: ts.TypeChecker
|
|
11
|
-
): string {
|
|
12
|
-
if (ts.isStringLiteral(node)) {
|
|
13
|
-
return node.text
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
if (ts.isNoSubstitutionTemplateLiteral(node)) {
|
|
17
|
-
return node.text
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
if (ts.isTemplateExpression(node)) {
|
|
21
|
-
// reconstruct: `head + ${expr} + middle + ${expr} + tail`
|
|
22
|
-
let result = node.head.text
|
|
23
|
-
for (const span of node.templateSpans) {
|
|
24
|
-
const exprText = span.expression.getText()
|
|
25
|
-
result += '${' + exprText + '}' + span.literal.text
|
|
26
|
-
}
|
|
27
|
-
return result
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
// Unwrap type assertions: `expr as Type` or `<Type>expr`
|
|
31
|
-
if (ts.isAsExpression(node) || ts.isTypeAssertionExpression(node)) {
|
|
32
|
-
return extractStringLiteral(node.expression, checker)
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
if (
|
|
36
|
-
ts.isBinaryExpression(node) &&
|
|
37
|
-
node.operatorToken.kind === ts.SyntaxKind.PlusToken
|
|
38
|
-
) {
|
|
39
|
-
return (
|
|
40
|
-
extractConcatOperand(node.left, checker) +
|
|
41
|
-
extractConcatOperand(node.right, checker)
|
|
42
|
-
)
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
// Try to evaluate constant identifiers
|
|
46
|
-
if (ts.isIdentifier(node)) {
|
|
47
|
-
const symbol = checker.getSymbolAtLocation(node)
|
|
48
|
-
if (
|
|
49
|
-
symbol?.valueDeclaration &&
|
|
50
|
-
ts.isVariableDeclaration(symbol.valueDeclaration)
|
|
51
|
-
) {
|
|
52
|
-
const init = symbol.valueDeclaration.initializer
|
|
53
|
-
if (init) {
|
|
54
|
-
return extractStringLiteral(init, checker)
|
|
55
|
-
}
|
|
56
|
-
}
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
throw new Error('Unable to extract string literal from node')
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
/**
|
|
63
|
-
* Resolve one operand of a `+` string concatenation.
|
|
64
|
-
*
|
|
65
|
-
* An operand that can't be statically resolved (e.g. `a ?? b`) becomes a
|
|
66
|
-
* `${...}` placeholder rather than throwing — mirroring the TemplateExpression
|
|
67
|
-
* branch above, so `'x ' + expr` and `` `x ${expr}` `` produce the same string.
|
|
68
|
-
* This keeps an unresolvable display name from aborting the whole extraction.
|
|
69
|
-
*/
|
|
70
|
-
function extractConcatOperand(node: ts.Node, checker: ts.TypeChecker): string {
|
|
71
|
-
try {
|
|
72
|
-
return extractStringLiteral(node, checker)
|
|
73
|
-
} catch {
|
|
74
|
-
const inner = ts.isParenthesizedExpression(node) ? node.expression : node
|
|
75
|
-
return '${' + inner.getText() + '}'
|
|
76
|
-
}
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
/**
|
|
80
|
-
* Check if node is string-like (string literal or template expression)
|
|
81
|
-
*/
|
|
82
|
-
export function isStringLike(node: ts.Node, checker: ts.TypeChecker): boolean {
|
|
83
|
-
if (ts.isStringLiteral(node) || ts.isNoSubstitutionTemplateLiteral(node)) {
|
|
84
|
-
return true
|
|
85
|
-
}
|
|
86
|
-
// Check if it's a template string with substitutions
|
|
87
|
-
if (ts.isTemplateExpression(node)) {
|
|
88
|
-
return true
|
|
89
|
-
}
|
|
90
|
-
// Unwrap type assertions: `expr as Type` or `<Type>expr`
|
|
91
|
-
if (ts.isAsExpression(node) || ts.isTypeAssertionExpression(node)) {
|
|
92
|
-
return isStringLike(node.expression, checker)
|
|
93
|
-
}
|
|
94
|
-
return false
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
/**
|
|
98
|
-
* Check if node is function-like (arrow, function expression, or function declaration)
|
|
99
|
-
*/
|
|
100
|
-
export function isFunctionLike(node: ts.Node): boolean {
|
|
101
|
-
return (
|
|
102
|
-
ts.isArrowFunction(node) ||
|
|
103
|
-
ts.isFunctionExpression(node) ||
|
|
104
|
-
ts.isFunctionDeclaration(node)
|
|
105
|
-
)
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
/**
|
|
109
|
-
* Extract number literal value from a node
|
|
110
|
-
*/
|
|
111
|
-
export function extractNumberLiteral(node: ts.Node): number | null {
|
|
112
|
-
if (ts.isNumericLiteral(node)) {
|
|
113
|
-
return Number(node.text)
|
|
114
|
-
}
|
|
115
|
-
return null
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
/**
|
|
119
|
-
* Extract a property value from an object literal expression
|
|
120
|
-
* Returns the extracted value or null if not found/cannot extract
|
|
121
|
-
*/
|
|
122
|
-
export function extractPropertyString(
|
|
123
|
-
objNode: ts.ObjectLiteralExpression,
|
|
124
|
-
propertyName: string,
|
|
125
|
-
checker: ts.TypeChecker
|
|
126
|
-
): string | null {
|
|
127
|
-
for (const prop of objNode.properties) {
|
|
128
|
-
if (
|
|
129
|
-
ts.isPropertyAssignment(prop) &&
|
|
130
|
-
ts.isIdentifier(prop.name) &&
|
|
131
|
-
prop.name.text === propertyName
|
|
132
|
-
) {
|
|
133
|
-
return extractStringLiteral(prop.initializer, checker)
|
|
134
|
-
}
|
|
135
|
-
}
|
|
136
|
-
return null
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
/**
|
|
140
|
-
* Extract description from options object
|
|
141
|
-
*/
|
|
142
|
-
export function extractDescription(
|
|
143
|
-
optionsNode: ts.Node | undefined,
|
|
144
|
-
checker: ts.TypeChecker
|
|
145
|
-
): string | null {
|
|
146
|
-
if (!optionsNode || !ts.isObjectLiteralExpression(optionsNode)) {
|
|
147
|
-
return null
|
|
148
|
-
}
|
|
149
|
-
try {
|
|
150
|
-
return extractPropertyString(optionsNode, 'description', checker)
|
|
151
|
-
} catch {
|
|
152
|
-
return null
|
|
153
|
-
}
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
/**
|
|
157
|
-
* Extract duration value (number or string)
|
|
158
|
-
*/
|
|
159
|
-
export function extractDuration(
|
|
160
|
-
node: ts.Node,
|
|
161
|
-
checker: ts.TypeChecker
|
|
162
|
-
): string | number | null {
|
|
163
|
-
const numValue = extractNumberLiteral(node)
|
|
164
|
-
if (numValue !== null) {
|
|
165
|
-
return numValue
|
|
166
|
-
}
|
|
167
|
-
try {
|
|
168
|
-
return extractStringLiteral(node, checker)
|
|
169
|
-
} catch {
|
|
170
|
-
return null
|
|
171
|
-
}
|
|
172
|
-
}
|
|
@@ -1,81 +0,0 @@
|
|
|
1
|
-
import { describe, test } from 'node:test'
|
|
2
|
-
import assert from 'node:assert/strict'
|
|
3
|
-
import ts from 'typescript'
|
|
4
|
-
import { extractSecretUsage } from './extract-secret-usage.js'
|
|
5
|
-
|
|
6
|
-
const usageOf = (source: string) =>
|
|
7
|
-
extractSecretUsage(
|
|
8
|
-
ts.createSourceFile('/services.ts', source, ts.ScriptTarget.ESNext, true)
|
|
9
|
-
)
|
|
10
|
-
|
|
11
|
-
describe('extractSecretUsage', () => {
|
|
12
|
-
test('collects a literal getSecret key', () => {
|
|
13
|
-
const usage = usageOf(`await secrets.getSecret('STRIPE_KEY')`)
|
|
14
|
-
assert.deepEqual(usage.keys, ['STRIPE_KEY'])
|
|
15
|
-
assert.deepEqual(usage.dynamic, [])
|
|
16
|
-
})
|
|
17
|
-
|
|
18
|
-
test('collects hasSecret and template literals with no substitutions', () => {
|
|
19
|
-
const usage = usageOf(
|
|
20
|
-
'await secrets.hasSecret(`NOTION_TOKEN`)\nawait secrets.getSecret("A")'
|
|
21
|
-
)
|
|
22
|
-
assert.deepEqual(usage.keys, ['A', 'NOTION_TOKEN'])
|
|
23
|
-
})
|
|
24
|
-
|
|
25
|
-
test('collects every key of a getSecrets array', () => {
|
|
26
|
-
const usage = usageOf(`await secrets.getSecrets(['B', 'A'])`)
|
|
27
|
-
assert.deepEqual(usage.keys, ['A', 'B'])
|
|
28
|
-
})
|
|
29
|
-
|
|
30
|
-
test('flags a computed key as dynamic', () => {
|
|
31
|
-
const usage = usageOf(`await secrets.getSecret(name)`)
|
|
32
|
-
assert.deepEqual(usage.keys, [])
|
|
33
|
-
assert.equal(usage.dynamic.length, 1)
|
|
34
|
-
assert.match(usage.dynamic[0]!, /^1:name$/)
|
|
35
|
-
})
|
|
36
|
-
|
|
37
|
-
test('flags a template literal with a substitution as dynamic', () => {
|
|
38
|
-
const usage = usageOf('await secrets.getSecret(`PREFIX_${suffix}`)')
|
|
39
|
-
assert.deepEqual(usage.keys, [])
|
|
40
|
-
assert.equal(usage.dynamic.length, 1)
|
|
41
|
-
})
|
|
42
|
-
|
|
43
|
-
test('flags a non-array getSecrets argument as dynamic', () => {
|
|
44
|
-
const usage = usageOf(`await secrets.getSecrets(keys)`)
|
|
45
|
-
assert.equal(usage.dynamic.length, 1)
|
|
46
|
-
})
|
|
47
|
-
|
|
48
|
-
test('records both the literal and the dynamic keys of one call', () => {
|
|
49
|
-
const usage = usageOf(`await secrets.getSecrets(['A', other])`)
|
|
50
|
-
assert.deepEqual(usage.keys, ['A'])
|
|
51
|
-
assert.equal(usage.dynamic.length, 1)
|
|
52
|
-
})
|
|
53
|
-
|
|
54
|
-
test('finds reads nested inside a factory body', () => {
|
|
55
|
-
const usage = usageOf(`
|
|
56
|
-
export const createSingletonServices = async ({ secrets }) => {
|
|
57
|
-
const key = await secrets.getSecret('DEEP_KEY')
|
|
58
|
-
return { thing: new Thing(key) }
|
|
59
|
-
}
|
|
60
|
-
`)
|
|
61
|
-
assert.deepEqual(usage.keys, ['DEEP_KEY'])
|
|
62
|
-
})
|
|
63
|
-
|
|
64
|
-
test('deduplicates a key read twice', () => {
|
|
65
|
-
const usage = usageOf(
|
|
66
|
-
`await secrets.getSecret('A'); await secrets.getSecret('A')`
|
|
67
|
-
)
|
|
68
|
-
assert.deepEqual(usage.keys, ['A'])
|
|
69
|
-
})
|
|
70
|
-
|
|
71
|
-
test('ignores unrelated calls', () => {
|
|
72
|
-
const usage = usageOf(`await variables.get('HOME'); logger.info('hi')`)
|
|
73
|
-
assert.deepEqual(usage.keys, [])
|
|
74
|
-
assert.deepEqual(usage.dynamic, [])
|
|
75
|
-
})
|
|
76
|
-
|
|
77
|
-
test('reports the line a dynamic read is on', () => {
|
|
78
|
-
const usage = usageOf(`const a = 1\nconst b = 2\nsecrets.getSecret(name)`)
|
|
79
|
-
assert.match(usage.dynamic[0]!, /^3:/)
|
|
80
|
-
})
|
|
81
|
-
})
|
|
@@ -1,73 +0,0 @@
|
|
|
1
|
-
import ts from 'typescript'
|
|
2
|
-
|
|
3
|
-
export type SecretUsage = {
|
|
4
|
-
/** Secret ids read with a string literal, so statically known. */
|
|
5
|
-
keys: string[]
|
|
6
|
-
/** Reads whose key could not be resolved statically, as `line:source`. */
|
|
7
|
-
dynamic: string[]
|
|
8
|
-
}
|
|
9
|
-
|
|
10
|
-
const SINGLE_KEY_METHODS = new Set(['getSecret', 'hasSecret'])
|
|
11
|
-
const KEY_LIST_METHODS = new Set(['getSecrets'])
|
|
12
|
-
|
|
13
|
-
const literalKey = (node: ts.Expression): string | undefined => {
|
|
14
|
-
if (ts.isStringLiteral(node) || ts.isNoSubstitutionTemplateLiteral(node)) {
|
|
15
|
-
return node.text
|
|
16
|
-
}
|
|
17
|
-
return undefined
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
/** Secret reads in a file, whether the key is statically known or not. */
|
|
21
|
-
export const extractSecretUsage = (sourceFile: ts.SourceFile): SecretUsage => {
|
|
22
|
-
const keys = new Set<string>()
|
|
23
|
-
const dynamic = new Set<string>()
|
|
24
|
-
|
|
25
|
-
const record = (node: ts.CallExpression, argument?: ts.Expression) => {
|
|
26
|
-
const { line } = sourceFile.getLineAndCharacterOfPosition(node.getStart())
|
|
27
|
-
dynamic.add(`${line + 1}:${(argument ?? node).getText()}`)
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
const visit = (node: ts.Node): void => {
|
|
31
|
-
if (
|
|
32
|
-
ts.isCallExpression(node) &&
|
|
33
|
-
ts.isPropertyAccessExpression(node.expression)
|
|
34
|
-
) {
|
|
35
|
-
const method = node.expression.name.text
|
|
36
|
-
const [firstArgument] = node.arguments
|
|
37
|
-
|
|
38
|
-
if (SINGLE_KEY_METHODS.has(method)) {
|
|
39
|
-
if (!firstArgument) {
|
|
40
|
-
record(node)
|
|
41
|
-
} else {
|
|
42
|
-
const key = literalKey(firstArgument)
|
|
43
|
-
if (key === undefined) {
|
|
44
|
-
record(node, firstArgument)
|
|
45
|
-
} else {
|
|
46
|
-
keys.add(key)
|
|
47
|
-
}
|
|
48
|
-
}
|
|
49
|
-
} else if (KEY_LIST_METHODS.has(method)) {
|
|
50
|
-
if (firstArgument && ts.isArrayLiteralExpression(firstArgument)) {
|
|
51
|
-
for (const element of firstArgument.elements) {
|
|
52
|
-
const key = literalKey(element)
|
|
53
|
-
if (key === undefined) {
|
|
54
|
-
record(node, element)
|
|
55
|
-
} else {
|
|
56
|
-
keys.add(key)
|
|
57
|
-
}
|
|
58
|
-
}
|
|
59
|
-
} else {
|
|
60
|
-
record(node, firstArgument)
|
|
61
|
-
}
|
|
62
|
-
}
|
|
63
|
-
}
|
|
64
|
-
ts.forEachChild(node, visit)
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
visit(sourceFile)
|
|
68
|
-
|
|
69
|
-
return {
|
|
70
|
-
keys: [...keys].sort(),
|
|
71
|
-
dynamic: [...dynamic].sort(),
|
|
72
|
-
}
|
|
73
|
-
}
|
|
@@ -1,70 +0,0 @@
|
|
|
1
|
-
import * as ts from 'typescript'
|
|
2
|
-
import type {
|
|
3
|
-
FunctionServicesMeta,
|
|
4
|
-
FunctionWiresMeta,
|
|
5
|
-
} from '@pikku/core/function'
|
|
6
|
-
/**
|
|
7
|
-
* Extract services from a function's first parameter destructuring pattern
|
|
8
|
-
*/
|
|
9
|
-
export function extractServicesFromFunction(
|
|
10
|
-
handlerNode: ts.FunctionExpression | ts.ArrowFunction
|
|
11
|
-
): FunctionServicesMeta {
|
|
12
|
-
const services: FunctionServicesMeta = {
|
|
13
|
-
optimized: true,
|
|
14
|
-
services: [],
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
const firstParam = handlerNode.parameters[0]
|
|
18
|
-
if (firstParam) {
|
|
19
|
-
if (ts.isObjectBindingPattern(firstParam.name)) {
|
|
20
|
-
for (const elem of firstParam.name.elements) {
|
|
21
|
-
const original =
|
|
22
|
-
elem.propertyName && ts.isIdentifier(elem.propertyName)
|
|
23
|
-
? elem.propertyName.text
|
|
24
|
-
: ts.isIdentifier(elem.name)
|
|
25
|
-
? elem.name.text
|
|
26
|
-
: undefined
|
|
27
|
-
if (original) {
|
|
28
|
-
services.services.push(original)
|
|
29
|
-
}
|
|
30
|
-
}
|
|
31
|
-
} else if (
|
|
32
|
-
ts.isIdentifier(firstParam.name) &&
|
|
33
|
-
!firstParam.name.text.startsWith('_')
|
|
34
|
-
) {
|
|
35
|
-
services.optimized = false
|
|
36
|
-
}
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
return services
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
export function extractUsedWires(
|
|
43
|
-
handlerNode: ts.FunctionExpression | ts.ArrowFunction,
|
|
44
|
-
paramIndex: number
|
|
45
|
-
): FunctionWiresMeta {
|
|
46
|
-
const param = handlerNode.parameters[paramIndex]
|
|
47
|
-
if (param && ts.isObjectBindingPattern(param.name)) {
|
|
48
|
-
const wires: string[] = []
|
|
49
|
-
for (const elem of param.name.elements) {
|
|
50
|
-
const propertyName =
|
|
51
|
-
elem.propertyName && ts.isIdentifier(elem.propertyName)
|
|
52
|
-
? elem.propertyName.text
|
|
53
|
-
: ts.isIdentifier(elem.name)
|
|
54
|
-
? elem.name.text
|
|
55
|
-
: undefined
|
|
56
|
-
if (propertyName) {
|
|
57
|
-
wires.push(propertyName)
|
|
58
|
-
}
|
|
59
|
-
}
|
|
60
|
-
return { optimized: true, wires }
|
|
61
|
-
}
|
|
62
|
-
if (
|
|
63
|
-
param &&
|
|
64
|
-
ts.isIdentifier(param.name) &&
|
|
65
|
-
!param.name.text.startsWith('_')
|
|
66
|
-
) {
|
|
67
|
-
return { optimized: false, wires: [] }
|
|
68
|
-
}
|
|
69
|
-
return { optimized: true, wires: [] }
|
|
70
|
-
}
|
|
@@ -1,147 +0,0 @@
|
|
|
1
|
-
import assert from 'node:assert'
|
|
2
|
-
import { describe, test } from 'node:test'
|
|
3
|
-
import ts from 'typescript'
|
|
4
|
-
|
|
5
|
-
import {
|
|
6
|
-
accumulateSurfaceUsage,
|
|
7
|
-
surfaceUsageArea,
|
|
8
|
-
type SurfaceUsageCounts,
|
|
9
|
-
} from './extract-surface-usage.js'
|
|
10
|
-
|
|
11
|
-
const countsFor = (
|
|
12
|
-
files: Array<{ area: string; source: string }>
|
|
13
|
-
): SurfaceUsageCounts => {
|
|
14
|
-
const counts: SurfaceUsageCounts = {}
|
|
15
|
-
files.forEach(({ area, source }, index) => {
|
|
16
|
-
const file = `src/file-${index}.ts`
|
|
17
|
-
accumulateSurfaceUsage(
|
|
18
|
-
ts.createSourceFile(
|
|
19
|
-
`/project/${file}`,
|
|
20
|
-
source,
|
|
21
|
-
ts.ScriptTarget.Latest,
|
|
22
|
-
true
|
|
23
|
-
),
|
|
24
|
-
{ area, file },
|
|
25
|
-
counts
|
|
26
|
-
)
|
|
27
|
-
})
|
|
28
|
-
return counts
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
describe('accumulateSurfaceUsage', () => {
|
|
32
|
-
test('counts an import site per named import', () => {
|
|
33
|
-
const counts = countsFor([
|
|
34
|
-
{
|
|
35
|
-
area: 'src',
|
|
36
|
-
source:
|
|
37
|
-
"import { pikkuFunc, pikkuSessionlessFunc } from '#pikku/function'\n",
|
|
38
|
-
},
|
|
39
|
-
{ area: 'src', source: "import { pikkuFunc } from '#pikku/function'\n" },
|
|
40
|
-
])
|
|
41
|
-
|
|
42
|
-
assert.deepStrictEqual(counts['#pikku/function'], {
|
|
43
|
-
pikkuFunc: {
|
|
44
|
-
imports: 2,
|
|
45
|
-
seenIn: ['src'],
|
|
46
|
-
files: ['src/file-0.ts', 'src/file-1.ts'],
|
|
47
|
-
},
|
|
48
|
-
pikkuSessionlessFunc: {
|
|
49
|
-
imports: 1,
|
|
50
|
-
seenIn: ['src'],
|
|
51
|
-
files: ['src/file-0.ts'],
|
|
52
|
-
},
|
|
53
|
-
})
|
|
54
|
-
})
|
|
55
|
-
|
|
56
|
-
test('counts a deep specifier against the leaf it belongs to', () => {
|
|
57
|
-
const counts = countsFor([
|
|
58
|
-
{
|
|
59
|
-
area: 'src',
|
|
60
|
-
source:
|
|
61
|
-
"import { pikkuWorkflowFunc } from '#pikku/workflow/pikku-workflow-types.gen.js'\n",
|
|
62
|
-
},
|
|
63
|
-
{
|
|
64
|
-
area: 'workers',
|
|
65
|
-
source: "import { pikkuWorkflowFunc } from '#pikku/workflow'\n",
|
|
66
|
-
},
|
|
67
|
-
])
|
|
68
|
-
|
|
69
|
-
assert.deepStrictEqual(counts['#pikku/workflow'], {
|
|
70
|
-
pikkuWorkflowFunc: {
|
|
71
|
-
imports: 2,
|
|
72
|
-
seenIn: ['src', 'workers'],
|
|
73
|
-
files: ['src/file-0.ts', 'src/file-1.ts'],
|
|
74
|
-
},
|
|
75
|
-
})
|
|
76
|
-
})
|
|
77
|
-
|
|
78
|
-
test('records the aliased name, not the local one', () => {
|
|
79
|
-
const counts = countsFor([
|
|
80
|
-
{
|
|
81
|
-
area: 'src',
|
|
82
|
-
source: "import { cors as allowAll } from '#pikku/http'\n",
|
|
83
|
-
},
|
|
84
|
-
])
|
|
85
|
-
|
|
86
|
-
assert.deepStrictEqual(counts['#pikku/http'], {
|
|
87
|
-
cors: { imports: 1, seenIn: ['src'], files: ['src/file-0.ts'] },
|
|
88
|
-
})
|
|
89
|
-
})
|
|
90
|
-
|
|
91
|
-
test('counts a re-export as a use of the leaf', () => {
|
|
92
|
-
const counts = countsFor([
|
|
93
|
-
{ area: 'src', source: "export { wireHTTP } from '#pikku/http'\n" },
|
|
94
|
-
])
|
|
95
|
-
|
|
96
|
-
assert.deepStrictEqual(counts['#pikku/http']!.wireHTTP!.imports, 1)
|
|
97
|
-
})
|
|
98
|
-
|
|
99
|
-
test('ignores imports from anywhere else', () => {
|
|
100
|
-
const counts = countsFor([
|
|
101
|
-
{
|
|
102
|
-
area: 'src',
|
|
103
|
-
source: [
|
|
104
|
-
"import { z } from 'zod'",
|
|
105
|
-
"import { thing } from './local.js'",
|
|
106
|
-
"import * as everything from '#pikku/function'",
|
|
107
|
-
"import bootstrap from '#pikku/pikku-bootstrap.gen.js'",
|
|
108
|
-
].join('\n'),
|
|
109
|
-
},
|
|
110
|
-
])
|
|
111
|
-
|
|
112
|
-
assert.deepStrictEqual(counts, {})
|
|
113
|
-
})
|
|
114
|
-
})
|
|
115
|
-
|
|
116
|
-
describe('surfaceUsageArea', () => {
|
|
117
|
-
test('falls back to the top-level source directory in a single package', () => {
|
|
118
|
-
assert.strictEqual(
|
|
119
|
-
surfaceUsageArea('/project/src/wirings/todo.ts', '/project', new Map()),
|
|
120
|
-
'src'
|
|
121
|
-
)
|
|
122
|
-
})
|
|
123
|
-
|
|
124
|
-
test('does not label everything with the root package name', () => {
|
|
125
|
-
const cache = new Map<string, string | null>([
|
|
126
|
-
['/project/src/wirings', null],
|
|
127
|
-
['/project/src', null],
|
|
128
|
-
])
|
|
129
|
-
|
|
130
|
-
assert.strictEqual(
|
|
131
|
-
surfaceUsageArea('/project/src/wirings/todo.ts', '/project', cache),
|
|
132
|
-
'src'
|
|
133
|
-
)
|
|
134
|
-
})
|
|
135
|
-
|
|
136
|
-
test('prefers the workspace package a file belongs to', () => {
|
|
137
|
-
const cache = new Map<string, string | null>([
|
|
138
|
-
['/project/packages/api/src', null],
|
|
139
|
-
['/project/packages/api', '@app/api'],
|
|
140
|
-
])
|
|
141
|
-
|
|
142
|
-
assert.strictEqual(
|
|
143
|
-
surfaceUsageArea('/project/packages/api/src/todo.ts', '/project', cache),
|
|
144
|
-
'@app/api'
|
|
145
|
-
)
|
|
146
|
-
})
|
|
147
|
-
})
|