@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
package/src/add/add-functions.ts
DELETED
|
@@ -1,1548 +0,0 @@
|
|
|
1
|
-
import * as ts from 'typescript'
|
|
2
|
-
import type { AddWiring, InspectorState, SchemaRef } from '../types.js'
|
|
3
|
-
import { detectSchemaVendorOrError } from '../utils/detect-schema-vendor.js'
|
|
4
|
-
import type { TypesMap } from '../types-map.js'
|
|
5
|
-
import {
|
|
6
|
-
extractFunctionName,
|
|
7
|
-
funcIdToTypeName,
|
|
8
|
-
} from '../utils/extract-function-name.js'
|
|
9
|
-
import { extractFunctionNode } from '../utils/extract-function-node.js'
|
|
10
|
-
import { extractUsedWires } from '../utils/extract-services.js'
|
|
11
|
-
import type { AuditDurability } from '@pikku/core/services'
|
|
12
|
-
import type { FunctionServicesMeta } from '@pikku/core/function'
|
|
13
|
-
import type { ScenarioSurface, ScenarioStepKind } from '@pikku/core/scenario'
|
|
14
|
-
import { formatVersionedId, parseVersionedId } from '@pikku/core/utils'
|
|
15
|
-
|
|
16
|
-
/** Binding keys a `pikkuScenarioStep` may declare, in report order. */
|
|
17
|
-
const SCENARIO_SURFACE_KEYS: readonly ScenarioSurface[] = [
|
|
18
|
-
'browser',
|
|
19
|
-
'cli',
|
|
20
|
-
'default',
|
|
21
|
-
]
|
|
22
|
-
|
|
23
|
-
/**
|
|
24
|
-
* The step wrappers, and who acts in each.
|
|
25
|
-
*
|
|
26
|
-
* Which function was called *is* the kind. That is the whole reason these are
|
|
27
|
-
* three declarations rather than a `kind:` field on one: a string literal can be
|
|
28
|
-
* copied into an addon package by a consumer, a called identifier cannot.
|
|
29
|
-
*/
|
|
30
|
-
const SCENARIO_STEP_KINDS = new Map<string, ScenarioStepKind>([
|
|
31
|
-
['pikkuScenarioStep', 'persona'],
|
|
32
|
-
['pikkuPlatformScenarioStep', 'platform'],
|
|
33
|
-
['pikkuAddonScenarioStep', 'addon'],
|
|
34
|
-
])
|
|
35
|
-
import {
|
|
36
|
-
getPropertyValue,
|
|
37
|
-
getArrayPropertyValue,
|
|
38
|
-
getCommonWireMetaData,
|
|
39
|
-
} from '../utils/get-property-value.js'
|
|
40
|
-
import { canonicalJSON, hashString } from '../utils/hash.js'
|
|
41
|
-
import { resolveMiddleware } from '../utils/middleware.js'
|
|
42
|
-
import { resolvePermissions } from '../utils/permissions.js'
|
|
43
|
-
import { extractWireNames } from '../utils/post-process.js'
|
|
44
|
-
import { ErrorCode } from '../error-codes.js'
|
|
45
|
-
import { findPiiPaths } from '../utils/check-pii-output.js'
|
|
46
|
-
import { findClassifiedSinks } from '../utils/check-classified-sinks.js'
|
|
47
|
-
import { isScenarioInstrumentationFunction } from './scenario-instrumentation.js'
|
|
48
|
-
import type { NodeType } from '@pikku/core/node'
|
|
49
|
-
|
|
50
|
-
const isValidVariableName = (name: string) => {
|
|
51
|
-
const regex = /^[a-zA-Z_$][a-zA-Z0-9_$]*$/
|
|
52
|
-
return regex.test(name)
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
/**
|
|
56
|
-
* True when the handler body contains a runtime dynamic `import(...)` call
|
|
57
|
-
* anywhere (including nested callbacks). Type-only positions like
|
|
58
|
-
* `import('x').Foo` are `ImportTypeNode`s, not `CallExpression`s with an
|
|
59
|
-
* `ImportKeyword` callee, so they are intentionally not matched.
|
|
60
|
-
*/
|
|
61
|
-
const handlerHasDynamicImport = (
|
|
62
|
-
handler: ts.ArrowFunction | ts.FunctionExpression
|
|
63
|
-
): boolean => {
|
|
64
|
-
let found = false
|
|
65
|
-
const visit = (node: ts.Node) => {
|
|
66
|
-
if (found) {
|
|
67
|
-
return
|
|
68
|
-
}
|
|
69
|
-
if (
|
|
70
|
-
ts.isCallExpression(node) &&
|
|
71
|
-
node.expression.kind === ts.SyntaxKind.ImportKeyword
|
|
72
|
-
) {
|
|
73
|
-
found = true
|
|
74
|
-
return
|
|
75
|
-
}
|
|
76
|
-
ts.forEachChild(node, visit)
|
|
77
|
-
}
|
|
78
|
-
visit(handler.body)
|
|
79
|
-
return found
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
const nullifyTypes = (type: string | null) => {
|
|
83
|
-
if (
|
|
84
|
-
type === 'void' ||
|
|
85
|
-
type === 'undefined' ||
|
|
86
|
-
type === 'unknown' ||
|
|
87
|
-
type === 'any' ||
|
|
88
|
-
type === 'null'
|
|
89
|
-
) {
|
|
90
|
-
return null
|
|
91
|
-
}
|
|
92
|
-
return type
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
const resolveTypeImports = (
|
|
96
|
-
type: ts.Type,
|
|
97
|
-
resolvedTypes: TypesMap,
|
|
98
|
-
isCustom: boolean,
|
|
99
|
-
checker: ts.TypeChecker
|
|
100
|
-
): string[] => {
|
|
101
|
-
const types: string[] = []
|
|
102
|
-
|
|
103
|
-
const visitType = (currentType: ts.Type) => {
|
|
104
|
-
const symbol = currentType.aliasSymbol || currentType.getSymbol()
|
|
105
|
-
|
|
106
|
-
if (symbol) {
|
|
107
|
-
const declarations = symbol.getDeclarations()
|
|
108
|
-
const declaration = declarations?.[0]
|
|
109
|
-
if (declaration) {
|
|
110
|
-
const sourceFile = declaration.getSourceFile()
|
|
111
|
-
const path = sourceFile.fileName
|
|
112
|
-
|
|
113
|
-
// Skip built-in utility types or TypeScript lib types
|
|
114
|
-
// Skip enum members (but not the enum type itself)
|
|
115
|
-
const isEnumMember = declaration && ts.isEnumMember(declaration)
|
|
116
|
-
|
|
117
|
-
if (
|
|
118
|
-
!path.includes('node_modules/typescript') &&
|
|
119
|
-
symbol.getName() !== '__type' &&
|
|
120
|
-
!isPrimitiveType(currentType) &&
|
|
121
|
-
!isEnumMember
|
|
122
|
-
) {
|
|
123
|
-
const originalName = symbol.getName()
|
|
124
|
-
// Check if the type is already in the map
|
|
125
|
-
let uniqueName = resolvedTypes.exists(originalName, path)
|
|
126
|
-
if (!uniqueName) {
|
|
127
|
-
if (isCustom) {
|
|
128
|
-
uniqueName = resolvedTypes.addUniqueType(originalName, path)
|
|
129
|
-
} else {
|
|
130
|
-
resolvedTypes.addType(originalName, path)
|
|
131
|
-
uniqueName = originalName
|
|
132
|
-
}
|
|
133
|
-
}
|
|
134
|
-
types.push(uniqueName)
|
|
135
|
-
}
|
|
136
|
-
}
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
if (isCustom) {
|
|
140
|
-
// Handle nested utility types like Partial, Pick, etc.
|
|
141
|
-
if (currentType.aliasTypeArguments) {
|
|
142
|
-
currentType.aliasTypeArguments.forEach(visitType)
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
// Handle intersections and unions
|
|
146
|
-
if (currentType.isUnionOrIntersection()) {
|
|
147
|
-
currentType.types.forEach(visitType)
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
// Handle object types with type arguments
|
|
151
|
-
if (
|
|
152
|
-
currentType.flags & ts.TypeFlags.Object &&
|
|
153
|
-
(currentType as ts.ObjectType).objectFlags & ts.ObjectFlags.Reference
|
|
154
|
-
) {
|
|
155
|
-
const typeRef = currentType as ts.TypeReference
|
|
156
|
-
typeRef.typeArguments?.forEach(visitType)
|
|
157
|
-
}
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
// Always traverse type arguments for thorough type collection
|
|
161
|
-
if (currentType.aliasTypeArguments) {
|
|
162
|
-
currentType.aliasTypeArguments.forEach(visitType)
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
// Always handle intersections and unions
|
|
166
|
-
if (currentType.isUnionOrIntersection()) {
|
|
167
|
-
currentType.types.forEach(visitType)
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
// Always handle object types with type arguments
|
|
171
|
-
if (
|
|
172
|
-
currentType.flags & ts.TypeFlags.Object &&
|
|
173
|
-
(currentType as ts.ObjectType).objectFlags & ts.ObjectFlags.Reference
|
|
174
|
-
) {
|
|
175
|
-
const typeRef = currentType as ts.TypeReference
|
|
176
|
-
typeRef.typeArguments?.forEach(visitType)
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
// Handle anonymous object types with enum properties (e.g., { userType: UserType })
|
|
180
|
-
// Only traverse into enum property types to avoid over-importing other named types
|
|
181
|
-
if (currentType.flags & ts.TypeFlags.Object) {
|
|
182
|
-
const objectType = currentType as ts.ObjectType
|
|
183
|
-
const typeSymbol = objectType.getSymbol()
|
|
184
|
-
|
|
185
|
-
// Only traverse properties for anonymous object types (no symbol or __type symbol)
|
|
186
|
-
// Skip named types, interfaces, and enums to avoid over-importing
|
|
187
|
-
const isAnonymousObject = !typeSymbol || typeSymbol.getName() === '__type'
|
|
188
|
-
|
|
189
|
-
if (isAnonymousObject) {
|
|
190
|
-
const properties = objectType.getProperties()
|
|
191
|
-
for (const prop of properties) {
|
|
192
|
-
if (prop.valueDeclaration) {
|
|
193
|
-
const propType = checker.getTypeOfSymbolAtLocation(
|
|
194
|
-
prop,
|
|
195
|
-
prop.valueDeclaration
|
|
196
|
-
)
|
|
197
|
-
if (propType) {
|
|
198
|
-
visitType(propType)
|
|
199
|
-
}
|
|
200
|
-
}
|
|
201
|
-
}
|
|
202
|
-
}
|
|
203
|
-
}
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
visitType(type)
|
|
207
|
-
return types
|
|
208
|
-
}
|
|
209
|
-
|
|
210
|
-
const resolveUnionTypes = (
|
|
211
|
-
checker: ts.TypeChecker,
|
|
212
|
-
type: ts.Type
|
|
213
|
-
): { types: ts.Type[]; names: string[] } => {
|
|
214
|
-
const types: ts.Type[] = []
|
|
215
|
-
const names: string[] = []
|
|
216
|
-
|
|
217
|
-
// Check if it's a union type AND not part of an intersection
|
|
218
|
-
if (type.isUnion() && !(type.flags & ts.TypeFlags.Intersection)) {
|
|
219
|
-
for (const t of type.types) {
|
|
220
|
-
const name = nullifyTypes(
|
|
221
|
-
checker.typeToString(t, undefined, ts.TypeFormatFlags.NoTruncation)
|
|
222
|
-
)
|
|
223
|
-
if (name) {
|
|
224
|
-
types.push(t)
|
|
225
|
-
names.push(name)
|
|
226
|
-
}
|
|
227
|
-
}
|
|
228
|
-
} else {
|
|
229
|
-
const name = nullifyTypes(
|
|
230
|
-
checker.typeToString(type, undefined, ts.TypeFormatFlags.NoTruncation)
|
|
231
|
-
)
|
|
232
|
-
if (name) {
|
|
233
|
-
types.push(type)
|
|
234
|
-
names.push(name)
|
|
235
|
-
}
|
|
236
|
-
}
|
|
237
|
-
|
|
238
|
-
return { types, names }
|
|
239
|
-
}
|
|
240
|
-
|
|
241
|
-
const getNamesAndTypes = (
|
|
242
|
-
checker: ts.TypeChecker,
|
|
243
|
-
typesMap: TypesMap,
|
|
244
|
-
direction: 'Input' | 'Output',
|
|
245
|
-
funcName: string,
|
|
246
|
-
type?: ts.Type
|
|
247
|
-
) => {
|
|
248
|
-
if (!type) {
|
|
249
|
-
return { names: [], types: [] }
|
|
250
|
-
}
|
|
251
|
-
|
|
252
|
-
// 1) Handle an explicit void (or undefined or null) type up front
|
|
253
|
-
if (type.flags & (ts.TypeFlags.VoidLike | ts.TypeFlags.Null)) {
|
|
254
|
-
return {
|
|
255
|
-
names: [],
|
|
256
|
-
types: [],
|
|
257
|
-
}
|
|
258
|
-
}
|
|
259
|
-
|
|
260
|
-
// 2) For unions, resolve all member names/types
|
|
261
|
-
const { names: rawNames, types: rawTypes } = resolveUnionTypes(checker, type)
|
|
262
|
-
|
|
263
|
-
// If the union is exactly [void], we'd have caught it above.
|
|
264
|
-
// If it's e.g. [string, void], rawNames should already include 'void'.
|
|
265
|
-
|
|
266
|
-
// 3) If multiple names or the single name isn't a valid identifier,
|
|
267
|
-
// we emit an alias type.
|
|
268
|
-
const firstName = rawNames[0]
|
|
269
|
-
if (rawNames.length > 1 || (firstName && !isValidVariableName(firstName))) {
|
|
270
|
-
const aliasType = rawNames.join(' | ')
|
|
271
|
-
const aliasName = funcIdToTypeName(funcName) + direction
|
|
272
|
-
|
|
273
|
-
// record the alias in your TypesMap
|
|
274
|
-
const references = rawTypes.flatMap((t) =>
|
|
275
|
-
resolveTypeImports(t, typesMap, true, checker)
|
|
276
|
-
)
|
|
277
|
-
|
|
278
|
-
typesMap.addCustomType(aliasName, aliasType, references)
|
|
279
|
-
|
|
280
|
-
return {
|
|
281
|
-
names: [aliasName],
|
|
282
|
-
types: rawTypes,
|
|
283
|
-
}
|
|
284
|
-
}
|
|
285
|
-
|
|
286
|
-
// 4) Single, valid name → inline it
|
|
287
|
-
const uniqueNames = rawNames.flatMap((name, i) => {
|
|
288
|
-
const t = rawTypes[i]
|
|
289
|
-
if (!t) {
|
|
290
|
-
throw new Error(`Expected type for name "${name}" in ${funcName}`)
|
|
291
|
-
}
|
|
292
|
-
if (isPrimitiveType(t)) {
|
|
293
|
-
return name
|
|
294
|
-
}
|
|
295
|
-
// non-primitive: import/alias it inline
|
|
296
|
-
return resolveTypeImports(t, typesMap, false, checker)
|
|
297
|
-
})
|
|
298
|
-
|
|
299
|
-
return {
|
|
300
|
-
names: uniqueNames,
|
|
301
|
-
types: rawTypes,
|
|
302
|
-
}
|
|
303
|
-
}
|
|
304
|
-
|
|
305
|
-
const isPrimitiveType = (type: ts.Type): boolean => {
|
|
306
|
-
const primitiveFlags =
|
|
307
|
-
ts.TypeFlags.Number |
|
|
308
|
-
ts.TypeFlags.String |
|
|
309
|
-
ts.TypeFlags.Boolean |
|
|
310
|
-
ts.TypeFlags.BigInt |
|
|
311
|
-
ts.TypeFlags.ESSymbol |
|
|
312
|
-
ts.TypeFlags.Void |
|
|
313
|
-
ts.TypeFlags.Undefined |
|
|
314
|
-
ts.TypeFlags.Null |
|
|
315
|
-
ts.TypeFlags.Unknown |
|
|
316
|
-
ts.TypeFlags.VoidLike
|
|
317
|
-
|
|
318
|
-
return (type.flags & primitiveFlags) !== 0
|
|
319
|
-
}
|
|
320
|
-
|
|
321
|
-
/**
|
|
322
|
-
* If `type` is a `Promise<T>`, return `T`, otherwise return `type` itself.
|
|
323
|
-
*/
|
|
324
|
-
function unwrapPromise(checker: ts.TypeChecker, type: ts.Type): ts.Type {
|
|
325
|
-
if (!type?.symbol) return type
|
|
326
|
-
|
|
327
|
-
const isPromise =
|
|
328
|
-
type.symbol.name === 'Promise' &&
|
|
329
|
-
checker.getFullyQualifiedName(type.symbol).includes('Promise')
|
|
330
|
-
|
|
331
|
-
// aliasTypeArguments covers most Promise<T> cases
|
|
332
|
-
if (isPromise && type.aliasTypeArguments?.length === 1) {
|
|
333
|
-
return type.aliasTypeArguments[0]!
|
|
334
|
-
}
|
|
335
|
-
|
|
336
|
-
// fallback for raw TypeReference
|
|
337
|
-
if (isPromise && (type as ts.TypeReference).typeArguments?.length === 1) {
|
|
338
|
-
return (type as ts.TypeReference).typeArguments![0]!
|
|
339
|
-
}
|
|
340
|
-
|
|
341
|
-
return type
|
|
342
|
-
}
|
|
343
|
-
|
|
344
|
-
const resolveExistingFunctionSource = (
|
|
345
|
-
state: InspectorState,
|
|
346
|
-
pikkuFuncId: string
|
|
347
|
-
): string | null => {
|
|
348
|
-
return (
|
|
349
|
-
state.functions.meta[pikkuFuncId]?.sourceFile ||
|
|
350
|
-
state.rpc.internalFiles.get(pikkuFuncId)?.path ||
|
|
351
|
-
null
|
|
352
|
-
)
|
|
353
|
-
}
|
|
354
|
-
|
|
355
|
-
const areCompatibleFunctionIds = (
|
|
356
|
-
existingId: string,
|
|
357
|
-
incomingId: string
|
|
358
|
-
): boolean => {
|
|
359
|
-
if (existingId === incomingId) {
|
|
360
|
-
return true
|
|
361
|
-
}
|
|
362
|
-
|
|
363
|
-
const existingParsed = parseVersionedId(existingId)
|
|
364
|
-
const incomingParsed = parseVersionedId(incomingId)
|
|
365
|
-
|
|
366
|
-
return existingParsed.baseName === incomingParsed.baseName
|
|
367
|
-
}
|
|
368
|
-
|
|
369
|
-
function printNode(node: ts.Node): string {
|
|
370
|
-
return ts
|
|
371
|
-
.createPrinter({ removeComments: true })
|
|
372
|
-
.printNode(ts.EmitHint.Unspecified, node, node.getSourceFile())
|
|
373
|
-
}
|
|
374
|
-
|
|
375
|
-
function computeImplementationHash(args: {
|
|
376
|
-
wrapper: string
|
|
377
|
-
handler: ts.ArrowFunction | ts.FunctionExpression
|
|
378
|
-
objectNode?: ts.ObjectLiteralExpression
|
|
379
|
-
isDirectFunction: boolean
|
|
380
|
-
}): string {
|
|
381
|
-
const { wrapper, handler, objectNode, isDirectFunction } = args
|
|
382
|
-
return hashString(
|
|
383
|
-
canonicalJSON({
|
|
384
|
-
wrapper,
|
|
385
|
-
isDirectFunction,
|
|
386
|
-
handler: printNode(handler),
|
|
387
|
-
config: objectNode ? printNode(objectNode) : null,
|
|
388
|
-
})
|
|
389
|
-
)
|
|
390
|
-
}
|
|
391
|
-
|
|
392
|
-
/**
|
|
393
|
-
* Registers a `pikkuRemoteChannelFunc` declaration: a name, its schemas and its
|
|
394
|
-
* description, with no body to inspect. The name has to reach `rpc.internalMeta`
|
|
395
|
-
* for `channel.remote` to resolve the schemas it validates each call against.
|
|
396
|
-
*/
|
|
397
|
-
const addRemoteChannelFunc = ({
|
|
398
|
-
state,
|
|
399
|
-
logger,
|
|
400
|
-
node,
|
|
401
|
-
pikkuFuncId,
|
|
402
|
-
name,
|
|
403
|
-
exportedName,
|
|
404
|
-
inputSchemaRef,
|
|
405
|
-
outputSchemaRef,
|
|
406
|
-
meta,
|
|
407
|
-
}: {
|
|
408
|
-
state: InspectorState
|
|
409
|
-
logger: Parameters<AddWiring>[0]
|
|
410
|
-
node: ts.CallExpression
|
|
411
|
-
pikkuFuncId: string
|
|
412
|
-
name: string
|
|
413
|
-
exportedName?: string | null
|
|
414
|
-
inputSchemaRef: SchemaRef | null
|
|
415
|
-
outputSchemaRef: SchemaRef | null
|
|
416
|
-
meta: {
|
|
417
|
-
version?: number
|
|
418
|
-
title?: string
|
|
419
|
-
tags?: string[]
|
|
420
|
-
summary?: string
|
|
421
|
-
description?: string
|
|
422
|
-
errors?: string[]
|
|
423
|
-
}
|
|
424
|
-
}) => {
|
|
425
|
-
const sourceFile = node.getSourceFile().fileName
|
|
426
|
-
|
|
427
|
-
if (!exportedName) {
|
|
428
|
-
logger.error(
|
|
429
|
-
`• Remote channel function '${name}' is not exported, this is not allowed.`
|
|
430
|
-
)
|
|
431
|
-
return
|
|
432
|
-
}
|
|
433
|
-
|
|
434
|
-
// Two files declaring the same capability is the collision that matters:
|
|
435
|
-
// both ids are the bare name, so the id-compatibility check below reads them
|
|
436
|
-
// as the same function and lets the second overwrite the first.
|
|
437
|
-
const existingFunction = state.functions.meta[pikkuFuncId]
|
|
438
|
-
if (
|
|
439
|
-
existingFunction &&
|
|
440
|
-
existingFunction.sourceFile &&
|
|
441
|
-
existingFunction.sourceFile !== sourceFile
|
|
442
|
-
) {
|
|
443
|
-
logger.critical(
|
|
444
|
-
ErrorCode.DUPLICATE_FUNCTION_NAME,
|
|
445
|
-
`Function name '${name}' is not unique. ` +
|
|
446
|
-
`'${pikkuFuncId}' is already defined in '${existingFunction.sourceFile}' and cannot be redefined in '${sourceFile}'.`
|
|
447
|
-
)
|
|
448
|
-
return
|
|
449
|
-
}
|
|
450
|
-
|
|
451
|
-
const existing = state.rpc.internalMeta[name]
|
|
452
|
-
if (existing && !areCompatibleFunctionIds(existing, pikkuFuncId)) {
|
|
453
|
-
logger.critical(
|
|
454
|
-
ErrorCode.DUPLICATE_FUNCTION_NAME,
|
|
455
|
-
`Function name '${name}' is not unique. ` +
|
|
456
|
-
`It already points to '${existing}' in '${resolveExistingFunctionSource(state, existing) || 'unknown file'}', but '${pikkuFuncId}' in '${sourceFile}' tried to use the same name.`
|
|
457
|
-
)
|
|
458
|
-
return
|
|
459
|
-
}
|
|
460
|
-
|
|
461
|
-
const capitalizedName = funcIdToTypeName(name)
|
|
462
|
-
const registerSchema = (
|
|
463
|
-
ref: SchemaRef | null,
|
|
464
|
-
suffix: 'Input' | 'Output'
|
|
465
|
-
) => {
|
|
466
|
-
if (!ref) return null
|
|
467
|
-
const schemaName = `${capitalizedName}${suffix}`
|
|
468
|
-
state.schemaLookup.set(schemaName, ref)
|
|
469
|
-
state.functions.typesMap.addCustomType(schemaName, 'unknown', [])
|
|
470
|
-
return schemaName
|
|
471
|
-
}
|
|
472
|
-
const inputSchemaName = registerSchema(inputSchemaRef, 'Input')
|
|
473
|
-
const outputSchemaName = registerSchema(outputSchemaRef, 'Output')
|
|
474
|
-
|
|
475
|
-
state.functions.meta[pikkuFuncId] = {
|
|
476
|
-
pikkuFuncId,
|
|
477
|
-
functionType: 'remote',
|
|
478
|
-
funcWrapper: 'pikkuRemoteChannelFunc',
|
|
479
|
-
sessionless: true,
|
|
480
|
-
name,
|
|
481
|
-
services: { optimized: true, services: [] },
|
|
482
|
-
inputSchemaName,
|
|
483
|
-
outputSchemaName,
|
|
484
|
-
inputs: inputSchemaName ? [inputSchemaName] : [],
|
|
485
|
-
outputs: outputSchemaName ? [outputSchemaName] : [],
|
|
486
|
-
sourceFile,
|
|
487
|
-
exportedName,
|
|
488
|
-
...meta,
|
|
489
|
-
}
|
|
490
|
-
|
|
491
|
-
state.functions.files.set(pikkuFuncId, { path: sourceFile, exportedName })
|
|
492
|
-
state.rpc.internalMeta[name] = pikkuFuncId
|
|
493
|
-
// Imported so a local call gets the factory's "this runs on the client"
|
|
494
|
-
// error rather than a bare lookup miss.
|
|
495
|
-
state.rpc.internalFiles.set(pikkuFuncId, { path: sourceFile, exportedName })
|
|
496
|
-
}
|
|
497
|
-
|
|
498
|
-
/**
|
|
499
|
-
* Inspect pikkuFunc calls, extract input/output and first-arg destructuring,
|
|
500
|
-
* then push into state.functions.meta.
|
|
501
|
-
*/
|
|
502
|
-
export const addFunctions: AddWiring = (
|
|
503
|
-
logger,
|
|
504
|
-
node,
|
|
505
|
-
checker,
|
|
506
|
-
state,
|
|
507
|
-
options
|
|
508
|
-
) => {
|
|
509
|
-
if (!ts.isCallExpression(node)) return
|
|
510
|
-
|
|
511
|
-
const { expression, arguments: args, typeArguments } = node
|
|
512
|
-
|
|
513
|
-
// only handle calls like pikkuFunc(...)
|
|
514
|
-
if (!ts.isIdentifier(expression)) {
|
|
515
|
-
return
|
|
516
|
-
}
|
|
517
|
-
|
|
518
|
-
// Match identifiers that contain both "pikku" and "func" (case insensitive),
|
|
519
|
-
// plus pikkuScenario and the three scenario step wrappers (workflow wrappers
|
|
520
|
-
// without "func" in their names)
|
|
521
|
-
const pikkuFuncPattern = /pikku.*func/i
|
|
522
|
-
if (
|
|
523
|
-
!pikkuFuncPattern.test(expression.text) &&
|
|
524
|
-
expression.text !== 'pikkuScenario' &&
|
|
525
|
-
!SCENARIO_STEP_KINDS.has(expression.text)
|
|
526
|
-
) {
|
|
527
|
-
return
|
|
528
|
-
}
|
|
529
|
-
|
|
530
|
-
// A scenario step is bundled like any invoked function, but is deliberately
|
|
531
|
-
// never RPC-registered: a step may drive a browser, so exposing it over the
|
|
532
|
-
// network would put a test harness on the public API surface.
|
|
533
|
-
const scenarioStepKind = SCENARIO_STEP_KINDS.get(expression.text)
|
|
534
|
-
const isScenarioStep = scenarioStepKind !== undefined
|
|
535
|
-
|
|
536
|
-
// A scenario's own body. Marked so the codegen partitions and the deploy
|
|
537
|
-
// analyzer can tell it apart from an application workflow without having to
|
|
538
|
-
// reach into the workflow graph.
|
|
539
|
-
const isScenario = expression.text === 'pikkuScenario'
|
|
540
|
-
|
|
541
|
-
// only handle calls like pikkuFunc(...)
|
|
542
|
-
if (!ts.isIdentifier(expression) || !expression.text.startsWith('pikku')) {
|
|
543
|
-
return
|
|
544
|
-
}
|
|
545
|
-
|
|
546
|
-
if (args.length === 0) return
|
|
547
|
-
|
|
548
|
-
let { pikkuFuncId, name, explicitName, exportedName } = extractFunctionName(
|
|
549
|
-
node,
|
|
550
|
-
checker,
|
|
551
|
-
state.rootDir
|
|
552
|
-
)
|
|
553
|
-
|
|
554
|
-
if (!pikkuFuncId || pikkuFuncId.startsWith('__temp_')) {
|
|
555
|
-
return
|
|
556
|
-
}
|
|
557
|
-
|
|
558
|
-
// pikku's own scenario instrumentation, which older projects still carry as a
|
|
559
|
-
// scaffolded file in their source. It is not application code and must not be
|
|
560
|
-
// registered, metadata'd or deployed as if it were — `pikku dev` registers the
|
|
561
|
-
// implementations itself, and that is the only server that should have them.
|
|
562
|
-
if (
|
|
563
|
-
isScenarioInstrumentationFunction(pikkuFuncId) ||
|
|
564
|
-
isScenarioInstrumentationFunction(name)
|
|
565
|
-
) {
|
|
566
|
-
return
|
|
567
|
-
}
|
|
568
|
-
|
|
569
|
-
let title: string | undefined
|
|
570
|
-
let tags: string[] | undefined
|
|
571
|
-
let summary: string | undefined
|
|
572
|
-
let description: string | undefined
|
|
573
|
-
let errors: string[] | undefined
|
|
574
|
-
let expose: boolean | undefined
|
|
575
|
-
let remote: boolean | undefined
|
|
576
|
-
let mcp: boolean | undefined
|
|
577
|
-
/**
|
|
578
|
-
* A sessionless function's own `auth: true`. `sessionless` records the
|
|
579
|
-
* baseline — a `pikkuFunc` always needs a session — while this records the
|
|
580
|
-
* tightening a `pikkuSessionlessFunc` applies to itself, which is otherwise
|
|
581
|
-
* invisible to anything reading meta.
|
|
582
|
-
*/
|
|
583
|
-
let auth: boolean | undefined
|
|
584
|
-
/** The author's claim that the body authorizes its own callers. */
|
|
585
|
-
let permissionsInBody: boolean | undefined
|
|
586
|
-
/** The function's `audit` config, resolved to its object form. */
|
|
587
|
-
let audit: { durability: AuditDurability } | undefined
|
|
588
|
-
let readonly_: boolean | undefined
|
|
589
|
-
let deploy: 'serverless' | 'server' | 'auto' | undefined
|
|
590
|
-
let approvalRequired: boolean | undefined
|
|
591
|
-
let approvalDescription: string | undefined
|
|
592
|
-
let workflowQueued: boolean | undefined
|
|
593
|
-
let workflowRetries: number | undefined
|
|
594
|
-
let workflowTimeout: string | undefined
|
|
595
|
-
let scenarioStepSurfaces: ScenarioSurface[] | undefined
|
|
596
|
-
let scenarioStepRequiresActor: boolean | undefined
|
|
597
|
-
let scenarioStepAddon: string | undefined
|
|
598
|
-
let scenarioStepTemplate: string | undefined
|
|
599
|
-
let scopes: string[] | undefined
|
|
600
|
-
let version: number | undefined
|
|
601
|
-
let objectNode: ts.ObjectLiteralExpression | undefined
|
|
602
|
-
let nodeDisplayName: string | null = null
|
|
603
|
-
let nodeCategory: string | null = null
|
|
604
|
-
let nodeType: NodeType | null = null
|
|
605
|
-
let nodeErrorOutput: boolean | null = null
|
|
606
|
-
|
|
607
|
-
// Extract the function node using shared utility
|
|
608
|
-
const firstArg = args[0]!
|
|
609
|
-
const {
|
|
610
|
-
funcNode: handlerNode,
|
|
611
|
-
resolvedFunc,
|
|
612
|
-
isDirectFunction,
|
|
613
|
-
} = extractFunctionNode(firstArg, checker)
|
|
614
|
-
|
|
615
|
-
// Variables to hold schema references if provided
|
|
616
|
-
let inputSchemaRef: SchemaRef | null = null
|
|
617
|
-
let outputSchemaRef: SchemaRef | null = null
|
|
618
|
-
|
|
619
|
-
// Helper to resolve schema identifier to its actual source file and detect vendor.
|
|
620
|
-
// Logs a fatal error and returns null if vendor cannot be determined.
|
|
621
|
-
const resolveSchemaRef = (
|
|
622
|
-
identifier: ts.Identifier,
|
|
623
|
-
context: string
|
|
624
|
-
): SchemaRef | null => {
|
|
625
|
-
const symbol = checker.getSymbolAtLocation(identifier)
|
|
626
|
-
if (!symbol) return null
|
|
627
|
-
|
|
628
|
-
const decl = symbol.valueDeclaration || symbol.declarations?.[0]
|
|
629
|
-
if (!decl) return null
|
|
630
|
-
|
|
631
|
-
let sourceFile: string
|
|
632
|
-
|
|
633
|
-
// If it's an import specifier, resolve the aliased symbol to get the actual source
|
|
634
|
-
if (ts.isImportSpecifier(decl)) {
|
|
635
|
-
const aliasedSymbol = checker.getAliasedSymbol(symbol)
|
|
636
|
-
if (aliasedSymbol) {
|
|
637
|
-
const aliasedDecl =
|
|
638
|
-
aliasedSymbol.valueDeclaration || aliasedSymbol.declarations?.[0]
|
|
639
|
-
if (aliasedDecl) {
|
|
640
|
-
sourceFile = aliasedDecl.getSourceFile().fileName
|
|
641
|
-
} else {
|
|
642
|
-
return null
|
|
643
|
-
}
|
|
644
|
-
} else {
|
|
645
|
-
return null
|
|
646
|
-
}
|
|
647
|
-
} else {
|
|
648
|
-
sourceFile = decl.getSourceFile().fileName
|
|
649
|
-
}
|
|
650
|
-
|
|
651
|
-
const vendor = detectSchemaVendorOrError(
|
|
652
|
-
identifier,
|
|
653
|
-
checker,
|
|
654
|
-
logger,
|
|
655
|
-
context,
|
|
656
|
-
sourceFile
|
|
657
|
-
)
|
|
658
|
-
if (!vendor) return null
|
|
659
|
-
|
|
660
|
-
return {
|
|
661
|
-
variableName: identifier.text,
|
|
662
|
-
sourceFile,
|
|
663
|
-
vendor,
|
|
664
|
-
}
|
|
665
|
-
}
|
|
666
|
-
|
|
667
|
-
// Extract config properties if using object form
|
|
668
|
-
if (ts.isObjectLiteralExpression(firstArg)) {
|
|
669
|
-
objectNode = firstArg
|
|
670
|
-
const metadata = getCommonWireMetaData(
|
|
671
|
-
firstArg,
|
|
672
|
-
'Function',
|
|
673
|
-
name,
|
|
674
|
-
logger,
|
|
675
|
-
checker
|
|
676
|
-
)
|
|
677
|
-
if (metadata.disabled) return
|
|
678
|
-
title = metadata.title
|
|
679
|
-
tags = metadata.tags
|
|
680
|
-
summary = metadata.summary
|
|
681
|
-
description = metadata.description
|
|
682
|
-
errors = metadata.errors
|
|
683
|
-
expose = getPropertyValue(firstArg, 'expose') as boolean | undefined
|
|
684
|
-
remote = getPropertyValue(firstArg, 'remote') as boolean | undefined
|
|
685
|
-
mcp = getPropertyValue(firstArg, 'mcp') as boolean | undefined
|
|
686
|
-
auth = getPropertyValue(firstArg, 'auth') as boolean | undefined
|
|
687
|
-
permissionsInBody = getPropertyValue(firstArg, 'permissionsInBody') as
|
|
688
|
-
boolean | undefined
|
|
689
|
-
if (permissionsInBody && !options.allow?.permissionsInBody) {
|
|
690
|
-
logger.critical(
|
|
691
|
-
ErrorCode.PERMISSIONS_IN_BODY_NOT_ALLOWED,
|
|
692
|
-
`'${name}' declares \`permissionsInBody: true\`, which this project has not enabled. ` +
|
|
693
|
-
`It silences the exposed-and-ungated audit without gating anything, so it is opt-in: ` +
|
|
694
|
-
`set \`allow: { permissionsInBody: true }\` in pikku.config.json if the project accepts ` +
|
|
695
|
-
`permission checks that only a reader can verify. Otherwise express the check as a ` +
|
|
696
|
-
`\`permissions\` entry, or drop \`expose: true\` if the function was not meant to be ` +
|
|
697
|
-
`callable from outside.`
|
|
698
|
-
)
|
|
699
|
-
return
|
|
700
|
-
}
|
|
701
|
-
// `audit` is either `true` or `{ durability }`, so it needs the nested read:
|
|
702
|
-
// getPropertyValue stringifies an object initializer rather than parsing it.
|
|
703
|
-
// Mirrors core's `resolveAuditConfig` — the two must agree, or meta would
|
|
704
|
-
// claim a durability the runner does not use.
|
|
705
|
-
const auditProp = firstArg.properties.find(
|
|
706
|
-
(p) =>
|
|
707
|
-
ts.isPropertyAssignment(p) &&
|
|
708
|
-
ts.isIdentifier(p.name) &&
|
|
709
|
-
p.name.text === 'audit'
|
|
710
|
-
)
|
|
711
|
-
if (auditProp && ts.isPropertyAssignment(auditProp)) {
|
|
712
|
-
const auditInit = auditProp.initializer
|
|
713
|
-
if (auditInit.kind === ts.SyntaxKind.TrueKeyword) {
|
|
714
|
-
audit = { durability: 'best-effort' }
|
|
715
|
-
} else if (ts.isObjectLiteralExpression(auditInit)) {
|
|
716
|
-
const durability = getPropertyValue(
|
|
717
|
-
auditInit,
|
|
718
|
-
'durability'
|
|
719
|
-
) as AuditDurability | null
|
|
720
|
-
audit = { durability: durability ?? 'best-effort' }
|
|
721
|
-
}
|
|
722
|
-
}
|
|
723
|
-
readonly_ = getPropertyValue(firstArg, 'readonly') as boolean | undefined
|
|
724
|
-
deploy = getPropertyValue(firstArg, 'deploy') as
|
|
725
|
-
'serverless' | 'server' | 'auto' | undefined
|
|
726
|
-
approvalRequired = getPropertyValue(firstArg, 'approvalRequired') as
|
|
727
|
-
boolean | undefined
|
|
728
|
-
workflowQueued = getPropertyValue(firstArg, 'workflowQueued') as
|
|
729
|
-
boolean | undefined
|
|
730
|
-
workflowRetries = getPropertyValue(firstArg, 'workflowRetries') as
|
|
731
|
-
number | undefined
|
|
732
|
-
workflowTimeout = getPropertyValue(firstArg, 'workflowTimeout') as
|
|
733
|
-
string | undefined
|
|
734
|
-
// A step declares one implementation per surface it supports; the binding
|
|
735
|
-
// keys that are present ARE the declaration, so there is nothing to read a
|
|
736
|
-
// value from — only names to collect.
|
|
737
|
-
const declaredSurfaces = SCENARIO_SURFACE_KEYS.filter((surface) =>
|
|
738
|
-
firstArg.properties.some(
|
|
739
|
-
(prop) =>
|
|
740
|
-
(ts.isPropertyAssignment(prop) ||
|
|
741
|
-
ts.isMethodDeclaration(prop) ||
|
|
742
|
-
ts.isShorthandPropertyAssignment(prop)) &&
|
|
743
|
-
ts.isIdentifier(prop.name) &&
|
|
744
|
-
prop.name.text === surface
|
|
745
|
-
)
|
|
746
|
-
)
|
|
747
|
-
if (scenarioStepKind === 'platform' || scenarioStepKind === 'addon') {
|
|
748
|
-
// Neither kind has surfaces. Nobody clicks "Stripe's webhook arrives", and
|
|
749
|
-
// there is no human behind "the platform has expired the trial" — so both
|
|
750
|
-
// take a single `func`, and a binding key here is a coded error rather
|
|
751
|
-
// than a convention nobody reads.
|
|
752
|
-
if (declaredSurfaces.length > 0) {
|
|
753
|
-
logger.error(
|
|
754
|
-
`• ${expression.text}('${name}') declares ${declaredSurfaces.join('/')} — a ${scenarioStepKind} step has one implementation, declared as \`func\`. Surfaces are how a person drives the system, and nobody drives this one.`
|
|
755
|
-
)
|
|
756
|
-
return
|
|
757
|
-
}
|
|
758
|
-
// The phase rule depends on it: an assertion runs every witness it has and
|
|
759
|
-
// fails if they disagree, so a step with exactly one witness by
|
|
760
|
-
// construction has to be unable to look like it has several.
|
|
761
|
-
scenarioStepSurfaces = ['default']
|
|
762
|
-
if (scenarioStepKind === 'addon') {
|
|
763
|
-
scenarioStepAddon = getPropertyValue(firstArg, 'addon') as
|
|
764
|
-
string | undefined
|
|
765
|
-
if (!scenarioStepAddon) {
|
|
766
|
-
logger.error(
|
|
767
|
-
`• pikkuAddonScenarioStep('${name}') needs a string literal \`addon\` naming the addon whose system acts — the same name its wireAddon declares.`
|
|
768
|
-
)
|
|
769
|
-
return
|
|
770
|
-
}
|
|
771
|
-
}
|
|
772
|
-
} else {
|
|
773
|
-
scenarioStepSurfaces = declaredSurfaces.length
|
|
774
|
-
? declaredSurfaces
|
|
775
|
-
: undefined
|
|
776
|
-
// A browser binding cannot be provisioned without a window to open, so it
|
|
777
|
-
// requires an actor whether or not the step said so.
|
|
778
|
-
scenarioStepRequiresActor =
|
|
779
|
-
getPropertyValue(firstArg, 'actor') === true ||
|
|
780
|
-
declaredSurfaces.includes('browser')
|
|
781
|
-
? true
|
|
782
|
-
: undefined
|
|
783
|
-
}
|
|
784
|
-
scenarioStepTemplate = getPropertyValue(firstArg, 'template') as
|
|
785
|
-
string | undefined
|
|
786
|
-
scopes = getArrayPropertyValue(firstArg, 'scopes') ?? undefined
|
|
787
|
-
|
|
788
|
-
// Extract approvalDescription identifier reference
|
|
789
|
-
for (const prop of firstArg.properties) {
|
|
790
|
-
if (
|
|
791
|
-
ts.isPropertyAssignment(prop) &&
|
|
792
|
-
ts.isIdentifier(prop.name) &&
|
|
793
|
-
prop.name.text === 'approvalDescription' &&
|
|
794
|
-
ts.isIdentifier(prop.initializer)
|
|
795
|
-
) {
|
|
796
|
-
const { pikkuFuncId: descId } = extractFunctionName(
|
|
797
|
-
prop.initializer,
|
|
798
|
-
checker,
|
|
799
|
-
state.rootDir
|
|
800
|
-
)
|
|
801
|
-
if (descId && !descId.startsWith('__temp_')) {
|
|
802
|
-
approvalDescription = descId
|
|
803
|
-
} else {
|
|
804
|
-
// Try resolving the identifier directly
|
|
805
|
-
approvalDescription = prop.initializer.text
|
|
806
|
-
}
|
|
807
|
-
break
|
|
808
|
-
}
|
|
809
|
-
}
|
|
810
|
-
|
|
811
|
-
const versionRaw = getPropertyValue(firstArg, 'version')
|
|
812
|
-
if (versionRaw !== null && versionRaw !== undefined) {
|
|
813
|
-
const parsed = Number(versionRaw)
|
|
814
|
-
if (Number.isInteger(parsed) && parsed >= 1) {
|
|
815
|
-
version = parsed
|
|
816
|
-
}
|
|
817
|
-
}
|
|
818
|
-
|
|
819
|
-
// Extract node config from nested object
|
|
820
|
-
for (const prop of firstArg.properties) {
|
|
821
|
-
if (
|
|
822
|
-
ts.isPropertyAssignment(prop) &&
|
|
823
|
-
ts.isIdentifier(prop.name) &&
|
|
824
|
-
prop.name.text === 'node' &&
|
|
825
|
-
ts.isObjectLiteralExpression(prop.initializer)
|
|
826
|
-
) {
|
|
827
|
-
const nodeObj = prop.initializer
|
|
828
|
-
nodeDisplayName = getPropertyValue(nodeObj, 'displayName') as
|
|
829
|
-
string | null
|
|
830
|
-
nodeCategory = getPropertyValue(nodeObj, 'category') as string | null
|
|
831
|
-
nodeType = getPropertyValue(nodeObj, 'type') as NodeType | null
|
|
832
|
-
nodeErrorOutput = getPropertyValue(nodeObj, 'errorOutput') as
|
|
833
|
-
boolean | null
|
|
834
|
-
|
|
835
|
-
if (!nodeDisplayName) {
|
|
836
|
-
logger.critical(
|
|
837
|
-
ErrorCode.MISSING_NAME,
|
|
838
|
-
`Function '${name}' node config is missing the required 'displayName' property.`
|
|
839
|
-
)
|
|
840
|
-
}
|
|
841
|
-
if (!nodeCategory) {
|
|
842
|
-
logger.critical(
|
|
843
|
-
ErrorCode.MISSING_NAME,
|
|
844
|
-
`Function '${name}' node config is missing the required 'category' property.`
|
|
845
|
-
)
|
|
846
|
-
}
|
|
847
|
-
if (!nodeType) {
|
|
848
|
-
logger.critical(
|
|
849
|
-
ErrorCode.MISSING_NAME,
|
|
850
|
-
`Function '${name}' node config is missing the required 'type' property.`
|
|
851
|
-
)
|
|
852
|
-
} else if (!['trigger', 'action', 'end'].includes(nodeType)) {
|
|
853
|
-
logger.critical(
|
|
854
|
-
ErrorCode.INVALID_VALUE,
|
|
855
|
-
`Function '${name}' node config has invalid type '${nodeType}'. Must be 'trigger', 'action', or 'end'.`
|
|
856
|
-
)
|
|
857
|
-
}
|
|
858
|
-
break
|
|
859
|
-
}
|
|
860
|
-
}
|
|
861
|
-
|
|
862
|
-
// Extract schema variable names from input/output properties
|
|
863
|
-
for (const prop of firstArg.properties) {
|
|
864
|
-
if (ts.isPropertyAssignment(prop) && ts.isIdentifier(prop.name)) {
|
|
865
|
-
const propName = prop.name.text
|
|
866
|
-
if (propName === 'input' || propName === 'output') {
|
|
867
|
-
if (ts.isIdentifier(prop.initializer)) {
|
|
868
|
-
// Good - it's a variable reference, resolve its actual source file and vendor
|
|
869
|
-
const context = `Function '${name}' ${propName}`
|
|
870
|
-
const ref = resolveSchemaRef(prop.initializer, context)
|
|
871
|
-
if (ref) {
|
|
872
|
-
if (propName === 'input') {
|
|
873
|
-
inputSchemaRef = ref
|
|
874
|
-
} else {
|
|
875
|
-
outputSchemaRef = ref
|
|
876
|
-
}
|
|
877
|
-
}
|
|
878
|
-
} else if (ts.isCallExpression(prop.initializer)) {
|
|
879
|
-
// Bad - it's an inline expression
|
|
880
|
-
const schemaName = `${funcIdToTypeName(name)}${propName.charAt(0).toUpperCase() + propName.slice(1)}`
|
|
881
|
-
logger.critical(
|
|
882
|
-
ErrorCode.INLINE_SCHEMA,
|
|
883
|
-
`Inline schemas are not supported for '${propName}' in '${name}'.\n` +
|
|
884
|
-
` Extract to an exported variable:\n` +
|
|
885
|
-
` export const ${schemaName} = ${prop.initializer.getText()}\n` +
|
|
886
|
-
` Then use: ${propName}: ${schemaName}`
|
|
887
|
-
)
|
|
888
|
-
}
|
|
889
|
-
}
|
|
890
|
-
}
|
|
891
|
-
}
|
|
892
|
-
}
|
|
893
|
-
|
|
894
|
-
if (version !== undefined) {
|
|
895
|
-
let baseName = explicitName || exportedName || pikkuFuncId
|
|
896
|
-
// Strip trailing VN suffix if it matches the version (e.g. getDataV1 + version:1 → getData@v1)
|
|
897
|
-
const vSuffix = `V${version}`
|
|
898
|
-
if (baseName.endsWith(vSuffix) && baseName.length > vSuffix.length) {
|
|
899
|
-
baseName = baseName.slice(0, -vSuffix.length)
|
|
900
|
-
}
|
|
901
|
-
pikkuFuncId = formatVersionedId(baseName, version)
|
|
902
|
-
}
|
|
903
|
-
|
|
904
|
-
// A remote channel capability is a contract with no body: the connected
|
|
905
|
-
// client answers it. Everything past this point reads the handler, so it gets
|
|
906
|
-
// its own path rather than a nullable handler threaded through all of it.
|
|
907
|
-
if (expression.text === 'pikkuRemoteChannelFunc') {
|
|
908
|
-
addRemoteChannelFunc({
|
|
909
|
-
state,
|
|
910
|
-
logger,
|
|
911
|
-
node,
|
|
912
|
-
pikkuFuncId,
|
|
913
|
-
name,
|
|
914
|
-
exportedName,
|
|
915
|
-
inputSchemaRef,
|
|
916
|
-
outputSchemaRef,
|
|
917
|
-
meta: { version, title, tags, summary, description, errors },
|
|
918
|
-
})
|
|
919
|
-
return
|
|
920
|
-
}
|
|
921
|
-
|
|
922
|
-
const isMCPToolFunc = expression.text === 'pikkuMCPToolFunc'
|
|
923
|
-
const isListFunc = expression.text === 'pikkuListFunc'
|
|
924
|
-
const mcpEnabled = mcp || isMCPToolFunc
|
|
925
|
-
|
|
926
|
-
// Pick the handler: use resolvedFunc when it exists and is a function, otherwise fall back to handlerNode
|
|
927
|
-
const handler =
|
|
928
|
-
resolvedFunc &&
|
|
929
|
-
(ts.isArrowFunction(resolvedFunc) || ts.isFunctionExpression(resolvedFunc))
|
|
930
|
-
? resolvedFunc
|
|
931
|
-
: handlerNode
|
|
932
|
-
|
|
933
|
-
// Validate that we got a valid function
|
|
934
|
-
if (!ts.isArrowFunction(handler) && !ts.isFunctionExpression(handler)) {
|
|
935
|
-
logger.error(`• No valid 'func' property found for ${pikkuFuncId}.`)
|
|
936
|
-
// Create stub metadata to prevent "function not found" errors in wirings
|
|
937
|
-
state.functions.meta[pikkuFuncId] = {
|
|
938
|
-
pikkuFuncId,
|
|
939
|
-
functionType: 'user',
|
|
940
|
-
name,
|
|
941
|
-
services: { optimized: false, services: [] },
|
|
942
|
-
inputSchemaName: null,
|
|
943
|
-
outputSchemaName: null,
|
|
944
|
-
inputs: [],
|
|
945
|
-
outputs: [],
|
|
946
|
-
middleware: undefined,
|
|
947
|
-
}
|
|
948
|
-
return
|
|
949
|
-
}
|
|
950
|
-
|
|
951
|
-
const services: FunctionServicesMeta = {
|
|
952
|
-
optimized: true,
|
|
953
|
-
services: [],
|
|
954
|
-
}
|
|
955
|
-
|
|
956
|
-
const firstParam = handler.parameters[0]
|
|
957
|
-
if (firstParam) {
|
|
958
|
-
if (ts.isObjectBindingPattern(firstParam.name)) {
|
|
959
|
-
for (const elem of firstParam.name.elements) {
|
|
960
|
-
const original =
|
|
961
|
-
elem.propertyName && ts.isIdentifier(elem.propertyName)
|
|
962
|
-
? elem.propertyName.text
|
|
963
|
-
: ts.isIdentifier(elem.name)
|
|
964
|
-
? elem.name.text
|
|
965
|
-
: undefined
|
|
966
|
-
if (original) {
|
|
967
|
-
services.services.push(original)
|
|
968
|
-
}
|
|
969
|
-
}
|
|
970
|
-
} else if (
|
|
971
|
-
ts.isIdentifier(firstParam.name) &&
|
|
972
|
-
!firstParam.name.text.startsWith('_')
|
|
973
|
-
) {
|
|
974
|
-
services.optimized = false
|
|
975
|
-
}
|
|
976
|
-
}
|
|
977
|
-
|
|
978
|
-
const wires = extractUsedWires(handler, 2)
|
|
979
|
-
|
|
980
|
-
// --- Generics → ts.Type[], unwrapped from Promise ---
|
|
981
|
-
const genericTypes: ts.Type[] = (typeArguments ?? [])
|
|
982
|
-
.map((tn) => checker.getTypeFromTypeNode(tn))
|
|
983
|
-
.map((t) => unwrapPromise(checker, t))
|
|
984
|
-
|
|
985
|
-
// pikkuChannelConnectionFunc<Out> declares a single generic that is the
|
|
986
|
-
// OUTPUT type — its input is always void (PikkuFunctionSessionless<void, Out>).
|
|
987
|
-
// Every other wrapper reads generic[0] as INPUT, so without this guard the
|
|
988
|
-
// connect handler's output generic is mis-recorded as inputSchemaName and the
|
|
989
|
-
// empty WS handshake fails input validation at connect (1008/403).
|
|
990
|
-
const isChannelConnectionFunc = /ChannelConnection/i.test(expression.text)
|
|
991
|
-
|
|
992
|
-
const capitalizedName = funcIdToTypeName(name)
|
|
993
|
-
|
|
994
|
-
// --- Input Extraction ---
|
|
995
|
-
let inputNames: string[] = []
|
|
996
|
-
let inputTypes: ts.Type[] = []
|
|
997
|
-
|
|
998
|
-
if (inputSchemaRef) {
|
|
999
|
-
const schemaName = `${capitalizedName}Input`
|
|
1000
|
-
inputNames = [schemaName]
|
|
1001
|
-
state.schemaLookup.set(schemaName, inputSchemaRef)
|
|
1002
|
-
state.functions.typesMap.addCustomType(schemaName, 'unknown', [])
|
|
1003
|
-
} else if (isListFunc && genericTypes.length >= 1 && genericTypes[0]) {
|
|
1004
|
-
const inputAliasName = `${capitalizedName}Input`
|
|
1005
|
-
const filterType = genericTypes[0]
|
|
1006
|
-
const filterTypeText = checker.typeToString(
|
|
1007
|
-
filterType,
|
|
1008
|
-
undefined,
|
|
1009
|
-
ts.TypeFormatFlags.NoTruncation
|
|
1010
|
-
)
|
|
1011
|
-
const refs = resolveTypeImports(
|
|
1012
|
-
filterType,
|
|
1013
|
-
state.functions.typesMap,
|
|
1014
|
-
true,
|
|
1015
|
-
checker
|
|
1016
|
-
)
|
|
1017
|
-
state.functions.typesMap.addCustomType(
|
|
1018
|
-
inputAliasName,
|
|
1019
|
-
`{ cursor?: string; limit?: number; sort?: Array<{ column: string; direction: 'asc' | 'desc' }>; filter?: unknown; search?: string; } & ${filterTypeText}`,
|
|
1020
|
-
[...new Set(refs)]
|
|
1021
|
-
)
|
|
1022
|
-
inputNames = [inputAliasName]
|
|
1023
|
-
const secondParam = handler.parameters[1]
|
|
1024
|
-
if (secondParam) {
|
|
1025
|
-
inputTypes = [checker.getTypeAtLocation(secondParam)]
|
|
1026
|
-
} else {
|
|
1027
|
-
inputTypes = [filterType]
|
|
1028
|
-
}
|
|
1029
|
-
} else if (
|
|
1030
|
-
!isChannelConnectionFunc &&
|
|
1031
|
-
!isListFunc &&
|
|
1032
|
-
genericTypes.length >= 1 &&
|
|
1033
|
-
genericTypes[0]
|
|
1034
|
-
) {
|
|
1035
|
-
// Fall back to extracting from generic type arguments
|
|
1036
|
-
const result = getNamesAndTypes(
|
|
1037
|
-
checker,
|
|
1038
|
-
state.functions.typesMap,
|
|
1039
|
-
'Input',
|
|
1040
|
-
name,
|
|
1041
|
-
genericTypes[0]
|
|
1042
|
-
)
|
|
1043
|
-
inputNames = result.names
|
|
1044
|
-
inputTypes = result.types
|
|
1045
|
-
} else {
|
|
1046
|
-
// Fall back to extracting from the function's second parameter type
|
|
1047
|
-
const secondParam = handler.parameters[1]
|
|
1048
|
-
if (secondParam) {
|
|
1049
|
-
const paramType = checker.getTypeAtLocation(secondParam)
|
|
1050
|
-
const result = getNamesAndTypes(
|
|
1051
|
-
checker,
|
|
1052
|
-
state.functions.typesMap,
|
|
1053
|
-
'Input',
|
|
1054
|
-
pikkuFuncId,
|
|
1055
|
-
paramType
|
|
1056
|
-
)
|
|
1057
|
-
inputNames = result.names
|
|
1058
|
-
inputTypes = result.types
|
|
1059
|
-
}
|
|
1060
|
-
}
|
|
1061
|
-
|
|
1062
|
-
// --- Output Extraction ---
|
|
1063
|
-
let outputNames: string[] = []
|
|
1064
|
-
|
|
1065
|
-
if (outputSchemaRef) {
|
|
1066
|
-
const schemaName = `${capitalizedName}Output`
|
|
1067
|
-
outputNames = [schemaName]
|
|
1068
|
-
state.schemaLookup.set(schemaName, outputSchemaRef)
|
|
1069
|
-
state.functions.typesMap.addCustomType(schemaName, 'unknown', [])
|
|
1070
|
-
} else if (isListFunc && genericTypes.length >= 2 && genericTypes[1]) {
|
|
1071
|
-
const outputAliasName = `${capitalizedName}Output`
|
|
1072
|
-
const rowType = genericTypes[1]
|
|
1073
|
-
const rowTypeText = checker.typeToString(
|
|
1074
|
-
rowType,
|
|
1075
|
-
undefined,
|
|
1076
|
-
ts.TypeFormatFlags.NoTruncation
|
|
1077
|
-
)
|
|
1078
|
-
const refs = resolveTypeImports(
|
|
1079
|
-
rowType,
|
|
1080
|
-
state.functions.typesMap,
|
|
1081
|
-
true,
|
|
1082
|
-
checker
|
|
1083
|
-
)
|
|
1084
|
-
state.functions.typesMap.addCustomType(
|
|
1085
|
-
outputAliasName,
|
|
1086
|
-
`{ rows: Array<${rowTypeText}>; nextCursor: string | null; totalCount?: number; }`,
|
|
1087
|
-
[...new Set(refs)]
|
|
1088
|
-
)
|
|
1089
|
-
outputNames = [outputAliasName]
|
|
1090
|
-
} else if (!isListFunc && genericTypes.length >= 2) {
|
|
1091
|
-
outputNames = getNamesAndTypes(
|
|
1092
|
-
checker,
|
|
1093
|
-
state.functions.typesMap,
|
|
1094
|
-
'Output',
|
|
1095
|
-
name,
|
|
1096
|
-
genericTypes[1]
|
|
1097
|
-
).names
|
|
1098
|
-
} else {
|
|
1099
|
-
const sig = checker.getSignatureFromDeclaration(handler)
|
|
1100
|
-
if (sig) {
|
|
1101
|
-
const rawRet = checker.getReturnTypeOfSignature(sig)
|
|
1102
|
-
const unwrapped = unwrapPromise(checker, rawRet)
|
|
1103
|
-
outputNames = getNamesAndTypes(
|
|
1104
|
-
checker,
|
|
1105
|
-
state.functions.typesMap,
|
|
1106
|
-
'Output',
|
|
1107
|
-
pikkuFuncId,
|
|
1108
|
-
unwrapped
|
|
1109
|
-
).names
|
|
1110
|
-
}
|
|
1111
|
-
}
|
|
1112
|
-
|
|
1113
|
-
const mcpOutputTypes: Record<string, string> = {
|
|
1114
|
-
pikkuMCPResourceFunc: 'MCPResourceResponse',
|
|
1115
|
-
pikkuMCPToolFunc: 'MCPToolResponse',
|
|
1116
|
-
pikkuMCPPromptFunc: 'MCPPromptResponse',
|
|
1117
|
-
}
|
|
1118
|
-
const mcpOutputType = mcpOutputTypes[expression.text]
|
|
1119
|
-
if (mcpOutputType && outputNames[0] !== mcpOutputType) {
|
|
1120
|
-
let resolved = false
|
|
1121
|
-
const rawSymbol = checker.getSymbolAtLocation(expression)
|
|
1122
|
-
const funcSymbol =
|
|
1123
|
-
rawSymbol && rawSymbol.flags & ts.SymbolFlags.Alias
|
|
1124
|
-
? checker.getAliasedSymbol(rawSymbol)
|
|
1125
|
-
: rawSymbol
|
|
1126
|
-
const funcDecls = funcSymbol?.getDeclarations() || []
|
|
1127
|
-
for (const funcDecl of funcDecls) {
|
|
1128
|
-
if (resolved) break
|
|
1129
|
-
const mcpTypeSymbol = checker.resolveName(
|
|
1130
|
-
mcpOutputType,
|
|
1131
|
-
funcDecl,
|
|
1132
|
-
ts.SymbolFlags.Type,
|
|
1133
|
-
false
|
|
1134
|
-
)
|
|
1135
|
-
if (mcpTypeSymbol) {
|
|
1136
|
-
const aliased =
|
|
1137
|
-
mcpTypeSymbol.flags & ts.SymbolFlags.Alias
|
|
1138
|
-
? checker.getAliasedSymbol(mcpTypeSymbol)
|
|
1139
|
-
: mcpTypeSymbol
|
|
1140
|
-
const typeDecl = aliased?.getDeclarations()?.[0]
|
|
1141
|
-
if (typeDecl) {
|
|
1142
|
-
const path = typeDecl.getSourceFile().fileName
|
|
1143
|
-
if (!state.functions.typesMap.exists(mcpOutputType, path)) {
|
|
1144
|
-
state.functions.typesMap.addType(mcpOutputType, path)
|
|
1145
|
-
}
|
|
1146
|
-
resolved = true
|
|
1147
|
-
}
|
|
1148
|
-
}
|
|
1149
|
-
}
|
|
1150
|
-
if (!resolved) {
|
|
1151
|
-
state.functions.typesMap.addCustomType(mcpOutputType, mcpOutputType, [])
|
|
1152
|
-
}
|
|
1153
|
-
outputNames = [mcpOutputType]
|
|
1154
|
-
}
|
|
1155
|
-
|
|
1156
|
-
if (inputNames.length > 1) {
|
|
1157
|
-
logger.warn(
|
|
1158
|
-
'More than one input type detected, only the first one will be used as a schema.'
|
|
1159
|
-
)
|
|
1160
|
-
}
|
|
1161
|
-
|
|
1162
|
-
// Store the input type for later use
|
|
1163
|
-
if (inputTypes.length > 0) {
|
|
1164
|
-
state.typesLookup.set(pikkuFuncId, inputTypes)
|
|
1165
|
-
}
|
|
1166
|
-
|
|
1167
|
-
const sourceFile = node.getSourceFile().fileName
|
|
1168
|
-
const existingFunction = state.functions.meta[pikkuFuncId]
|
|
1169
|
-
if (
|
|
1170
|
-
existingFunction &&
|
|
1171
|
-
existingFunction.sourceFile &&
|
|
1172
|
-
existingFunction.sourceFile !== sourceFile
|
|
1173
|
-
) {
|
|
1174
|
-
logger.critical(
|
|
1175
|
-
ErrorCode.DUPLICATE_FUNCTION_NAME,
|
|
1176
|
-
`Function name '${name}' is not unique. ` +
|
|
1177
|
-
`'${pikkuFuncId}' is already defined in '${existingFunction.sourceFile}' and cannot be redefined in '${sourceFile}'.`
|
|
1178
|
-
)
|
|
1179
|
-
return
|
|
1180
|
-
}
|
|
1181
|
-
|
|
1182
|
-
if (exportedName || explicitName) {
|
|
1183
|
-
const existingInternal = state.rpc.internalMeta[name]
|
|
1184
|
-
if (
|
|
1185
|
-
existingInternal &&
|
|
1186
|
-
!areCompatibleFunctionIds(existingInternal, pikkuFuncId)
|
|
1187
|
-
) {
|
|
1188
|
-
const existingSource =
|
|
1189
|
-
resolveExistingFunctionSource(state, existingInternal) || 'unknown file'
|
|
1190
|
-
logger.critical(
|
|
1191
|
-
ErrorCode.DUPLICATE_FUNCTION_NAME,
|
|
1192
|
-
`Function name '${name}' is not unique. ` +
|
|
1193
|
-
`It already points to '${existingInternal}' in '${existingSource}', but '${pikkuFuncId}' in '${sourceFile}' tried to use the same name.`
|
|
1194
|
-
)
|
|
1195
|
-
return
|
|
1196
|
-
}
|
|
1197
|
-
|
|
1198
|
-
if (expose) {
|
|
1199
|
-
const existingExposed = state.rpc.exposedMeta[name]
|
|
1200
|
-
if (
|
|
1201
|
-
existingExposed &&
|
|
1202
|
-
!areCompatibleFunctionIds(existingExposed, pikkuFuncId)
|
|
1203
|
-
) {
|
|
1204
|
-
const existingSource =
|
|
1205
|
-
resolveExistingFunctionSource(state, existingExposed) ||
|
|
1206
|
-
'unknown file'
|
|
1207
|
-
logger.critical(
|
|
1208
|
-
ErrorCode.DUPLICATE_FUNCTION_NAME,
|
|
1209
|
-
`Exposed function name '${name}' is not unique. ` +
|
|
1210
|
-
`It already points to '${existingExposed}' in '${existingSource}', but '${pikkuFuncId}' in '${sourceFile}' tried to use the same name.`
|
|
1211
|
-
)
|
|
1212
|
-
return
|
|
1213
|
-
}
|
|
1214
|
-
}
|
|
1215
|
-
}
|
|
1216
|
-
|
|
1217
|
-
const sessionless = expression.text !== 'pikkuFunc'
|
|
1218
|
-
|
|
1219
|
-
// ── Classification brand check ─────────────────────────────────────────────
|
|
1220
|
-
// Walk the function body's ACTUAL inferred return type looking for classification
|
|
1221
|
-
// brands (__classification__ property on Private<T>, Pii<T>, Secret<T>).
|
|
1222
|
-
//
|
|
1223
|
-
// Semantics:
|
|
1224
|
-
// secret → never returned by any exposed function (sessioned or not)
|
|
1225
|
-
// private → only visible to authenticated (sessioned) users; ok for pikkuFunc
|
|
1226
|
-
// public → safe for sessionless functions
|
|
1227
|
-
// Opt-in only: inferring every handler's return type (getReturnTypeOfSignature)
|
|
1228
|
-
// is the single most expensive checker operation and dominates `pikku all`
|
|
1229
|
-
// wall-clock. The classification leak scan is a security lint, not codegen, so
|
|
1230
|
-
// it runs ONLY when explicitly requested (`pikku all --security`) — see the
|
|
1231
|
-
// classificationCheck option. Default codegen skips it entirely.
|
|
1232
|
-
if (options.classificationCheck) {
|
|
1233
|
-
// A revealed vault secret — or a PII column — is invisible to the return-type
|
|
1234
|
-
// scan below when it is written out mid-body rather than returned, so walk
|
|
1235
|
-
// the sinks too.
|
|
1236
|
-
for (const violation of findClassifiedSinks(checker, handler)) {
|
|
1237
|
-
logger.diagnostic({
|
|
1238
|
-
severity: 'error',
|
|
1239
|
-
code:
|
|
1240
|
-
violation.classification === 'secret'
|
|
1241
|
-
? ErrorCode.SECRET_REVEALED_INTO_SINK
|
|
1242
|
-
: ErrorCode.PII_INTO_SINK,
|
|
1243
|
-
message:
|
|
1244
|
-
violation.classification === 'secret'
|
|
1245
|
-
? `Function '${name}' passes a revealed secret to '${violation.sink}' ` +
|
|
1246
|
-
`at line ${violation.line} (path '${violation.path}').\n ` +
|
|
1247
|
-
`A vault secret must not be written to a log, an audit, a queue, an email or a webhook. ` +
|
|
1248
|
-
`Pass the value only to the client that needs it, and reveal it there.`
|
|
1249
|
-
: `Function '${name}' passes personal data to '${violation.sink}' ` +
|
|
1250
|
-
`at line ${violation.line} (path '${violation.path}').\n ` +
|
|
1251
|
-
`A log, the console and a webhook all leave your control — they are shipped to third parties and retained ` +
|
|
1252
|
-
`long past the request. Log an id instead, or narrow the value to a non-identifying field. ` +
|
|
1253
|
-
`An email to the data subject, an audit in your own database and a queue on your own infrastructure are fine.`,
|
|
1254
|
-
})
|
|
1255
|
-
}
|
|
1256
|
-
|
|
1257
|
-
const sig = checker.getSignatureFromDeclaration(handler)
|
|
1258
|
-
if (sig) {
|
|
1259
|
-
const rawRet = checker.getReturnTypeOfSignature(sig)
|
|
1260
|
-
const unwrapped = unwrapPromise(checker, rawRet)
|
|
1261
|
-
const classifiedFields = findPiiPaths(checker, unwrapped)
|
|
1262
|
-
|
|
1263
|
-
const secretPaths = classifiedFields
|
|
1264
|
-
.filter((f) => f.classification === 'secret')
|
|
1265
|
-
.map((f) => f.path)
|
|
1266
|
-
|
|
1267
|
-
const privatePaths = classifiedFields
|
|
1268
|
-
.filter(
|
|
1269
|
-
(f) => f.classification === 'private' || f.classification === 'pii'
|
|
1270
|
-
)
|
|
1271
|
-
.map((f) => f.path)
|
|
1272
|
-
|
|
1273
|
-
if (secretPaths.length > 0) {
|
|
1274
|
-
logger.diagnostic({
|
|
1275
|
-
severity: 'error',
|
|
1276
|
-
code: ErrorCode.PII_IN_OUTPUT,
|
|
1277
|
-
message:
|
|
1278
|
-
`Function '${name}' exposes secret-classified field(s) in its return type: ` +
|
|
1279
|
-
secretPaths.map((p) => `'${p}'`).join(', ') +
|
|
1280
|
-
`.\n Secret fields must never appear in function output. ` +
|
|
1281
|
-
`Strip these fields before returning or change the column classification.`,
|
|
1282
|
-
})
|
|
1283
|
-
}
|
|
1284
|
-
|
|
1285
|
-
if (sessionless && privatePaths.length > 0) {
|
|
1286
|
-
logger.diagnostic({
|
|
1287
|
-
severity: 'error',
|
|
1288
|
-
code: ErrorCode.PII_IN_OUTPUT,
|
|
1289
|
-
message:
|
|
1290
|
-
`Sessionless function '${name}' exposes private-classified field(s) in its return type: ` +
|
|
1291
|
-
privatePaths.map((p) => `'${p}'`).join(', ') +
|
|
1292
|
-
`.\n Private fields are only safe to return from authenticated (sessioned) functions. ` +
|
|
1293
|
-
`Either require a session (use pikkuFunc) or set the column's security to 'public' in db/annotations.ts if it is safe to expose publicly.`,
|
|
1294
|
-
})
|
|
1295
|
-
}
|
|
1296
|
-
}
|
|
1297
|
-
}
|
|
1298
|
-
|
|
1299
|
-
// --- resolve middleware ---
|
|
1300
|
-
let middleware = objectNode
|
|
1301
|
-
? resolveMiddleware(state, objectNode, tags, checker)
|
|
1302
|
-
: undefined
|
|
1303
|
-
|
|
1304
|
-
// --- resolve permissions ---
|
|
1305
|
-
let permissions = objectNode
|
|
1306
|
-
? resolvePermissions(state, objectNode, tags, checker)
|
|
1307
|
-
: undefined
|
|
1308
|
-
|
|
1309
|
-
if (options.tags?.length) {
|
|
1310
|
-
tags = [...new Set([...(tags || []), ...options.tags])]
|
|
1311
|
-
const tagEntries = options.tags.map((tag) => ({
|
|
1312
|
-
type: 'tag' as const,
|
|
1313
|
-
tag,
|
|
1314
|
-
}))
|
|
1315
|
-
const existingMiddlewareTags = new Set(
|
|
1316
|
-
(middleware || [])
|
|
1317
|
-
.filter((m) => m.type === 'tag')
|
|
1318
|
-
.map((m) => (m as any).tag)
|
|
1319
|
-
)
|
|
1320
|
-
const newMiddleware = tagEntries.filter(
|
|
1321
|
-
(e) => !existingMiddlewareTags.has(e.tag)
|
|
1322
|
-
)
|
|
1323
|
-
if (newMiddleware.length > 0) {
|
|
1324
|
-
middleware = [...(middleware || []), ...newMiddleware]
|
|
1325
|
-
}
|
|
1326
|
-
}
|
|
1327
|
-
|
|
1328
|
-
const implementationHash = computeImplementationHash({
|
|
1329
|
-
wrapper: expression.text,
|
|
1330
|
-
handler,
|
|
1331
|
-
objectNode,
|
|
1332
|
-
isDirectFunction,
|
|
1333
|
-
})
|
|
1334
|
-
|
|
1335
|
-
const handlerSourceFile = handler.getSourceFile()
|
|
1336
|
-
const bodySourceFile =
|
|
1337
|
-
handlerSourceFile.fileName !== sourceFile
|
|
1338
|
-
? handlerSourceFile.fileName
|
|
1339
|
-
: undefined
|
|
1340
|
-
const lineOf = (pos: number) =>
|
|
1341
|
-
handlerSourceFile.getLineAndCharacterOfPosition(pos).line + 1
|
|
1342
|
-
const handlerBody = handler.body
|
|
1343
|
-
const bodySpan =
|
|
1344
|
-
ts.isBlock(handlerBody) && handlerBody.statements.length > 0
|
|
1345
|
-
? {
|
|
1346
|
-
bodyStart: lineOf(
|
|
1347
|
-
handlerBody.statements[0].getStart(handlerSourceFile)
|
|
1348
|
-
),
|
|
1349
|
-
bodyEnd: lineOf(
|
|
1350
|
-
handlerBody.statements[handlerBody.statements.length - 1].getEnd()
|
|
1351
|
-
),
|
|
1352
|
-
}
|
|
1353
|
-
: {
|
|
1354
|
-
bodyStart: lineOf(handlerBody.getStart(handlerSourceFile)),
|
|
1355
|
-
bodyEnd: lineOf(handlerBody.getEnd()),
|
|
1356
|
-
}
|
|
1357
|
-
|
|
1358
|
-
state.functions.meta[pikkuFuncId] = {
|
|
1359
|
-
pikkuFuncId,
|
|
1360
|
-
functionType: 'user',
|
|
1361
|
-
funcWrapper: expression.text,
|
|
1362
|
-
sessionless,
|
|
1363
|
-
name,
|
|
1364
|
-
services,
|
|
1365
|
-
wires: wires.wires.length > 0 || !wires.optimized ? wires : undefined,
|
|
1366
|
-
inputSchemaName: inputNames[0] ?? null,
|
|
1367
|
-
outputSchemaName: outputNames[0] ?? null,
|
|
1368
|
-
inputs: inputNames.filter((n) => n !== 'void') ?? null,
|
|
1369
|
-
outputs: outputNames.filter((n) => n !== 'void') ?? null,
|
|
1370
|
-
expose: expose || undefined,
|
|
1371
|
-
auth: auth || undefined,
|
|
1372
|
-
permissionsInBody: permissionsInBody || undefined,
|
|
1373
|
-
audit,
|
|
1374
|
-
remote: remote || undefined,
|
|
1375
|
-
scenarioStep: isScenarioStep || undefined,
|
|
1376
|
-
scenario: isScenario || undefined,
|
|
1377
|
-
mcp: mcpEnabled || undefined,
|
|
1378
|
-
readonly: readonly_ || undefined,
|
|
1379
|
-
deploy: deploy || undefined,
|
|
1380
|
-
approvalRequired: approvalRequired || undefined,
|
|
1381
|
-
approvalDescription: approvalDescription || undefined,
|
|
1382
|
-
workflowQueued: workflowQueued === true ? true : undefined,
|
|
1383
|
-
workflowRetries: workflowRetries ?? undefined,
|
|
1384
|
-
workflowTimeout: workflowTimeout ?? undefined,
|
|
1385
|
-
scenarioStepSurfaces,
|
|
1386
|
-
scenarioStepRequiresActor,
|
|
1387
|
-
// `persona` is the default reading of an unmarked step, so it is left off
|
|
1388
|
-
// rather than written into every step meta a project already has.
|
|
1389
|
-
scenarioStepKind:
|
|
1390
|
-
scenarioStepKind && scenarioStepKind !== 'persona'
|
|
1391
|
-
? scenarioStepKind
|
|
1392
|
-
: undefined,
|
|
1393
|
-
scenarioStepAddon,
|
|
1394
|
-
scenarioStepTemplate: scenarioStepTemplate || undefined,
|
|
1395
|
-
scopes: scopes ?? undefined,
|
|
1396
|
-
implementationHash,
|
|
1397
|
-
version,
|
|
1398
|
-
title,
|
|
1399
|
-
tags: tags || undefined,
|
|
1400
|
-
summary,
|
|
1401
|
-
description,
|
|
1402
|
-
errors,
|
|
1403
|
-
middleware,
|
|
1404
|
-
permissions,
|
|
1405
|
-
isDirectFunction,
|
|
1406
|
-
sourceFile,
|
|
1407
|
-
bodySourceFile,
|
|
1408
|
-
exportedName: exportedName || undefined,
|
|
1409
|
-
...bodySpan,
|
|
1410
|
-
}
|
|
1411
|
-
|
|
1412
|
-
if (handlerHasDynamicImport(handler)) {
|
|
1413
|
-
state.functions.dynamicImportIds.add(pikkuFuncId)
|
|
1414
|
-
}
|
|
1415
|
-
|
|
1416
|
-
// Populate node metadata if node config is present
|
|
1417
|
-
if (nodeDisplayName && nodeCategory && nodeType) {
|
|
1418
|
-
state.nodes.files.add(node.getSourceFile().fileName)
|
|
1419
|
-
state.nodes.meta[pikkuFuncId] = {
|
|
1420
|
-
name: pikkuFuncId,
|
|
1421
|
-
displayName: nodeDisplayName,
|
|
1422
|
-
category: nodeCategory,
|
|
1423
|
-
type: nodeType,
|
|
1424
|
-
rpc: pikkuFuncId,
|
|
1425
|
-
description,
|
|
1426
|
-
errorOutput: nodeErrorOutput ?? false,
|
|
1427
|
-
inputSchemaName: inputNames[0] ?? null,
|
|
1428
|
-
outputSchemaName: outputNames[0] ?? null,
|
|
1429
|
-
tags,
|
|
1430
|
-
}
|
|
1431
|
-
}
|
|
1432
|
-
|
|
1433
|
-
if (mcpEnabled) {
|
|
1434
|
-
// `pikkuMCPToolFunc`'s config accepts `name`, and it is the name an AI client
|
|
1435
|
-
// calls the tool by — `runMCPTool` looks the tool up in `toolsMeta` by exactly
|
|
1436
|
-
// this key and then dispatches through `pikkuFuncId`, so it is a public label
|
|
1437
|
-
// rather than an identity. It was never read here, so a declared `name` was
|
|
1438
|
-
// silently dropped and the tool published under whatever the export happened to
|
|
1439
|
-
// be called. It does NOT become the `pikkuFuncId`: renaming an export's rpc id
|
|
1440
|
-
// is a different decision, and the scenario-step wrappers are the only place
|
|
1441
|
-
// `name` carries that weight.
|
|
1442
|
-
const declaredName = objectNode
|
|
1443
|
-
? getPropertyValue(objectNode, 'name')
|
|
1444
|
-
: undefined
|
|
1445
|
-
const toolName =
|
|
1446
|
-
typeof declaredName === 'string' && declaredName ? declaredName : name
|
|
1447
|
-
if (!description) {
|
|
1448
|
-
logger.warn(`MCP tool '${toolName}' is missing a description.`)
|
|
1449
|
-
}
|
|
1450
|
-
state.mcpEndpoints.files.add(node.getSourceFile().fileName)
|
|
1451
|
-
state.mcpEndpoints.toolsMeta[toolName] = {
|
|
1452
|
-
pikkuFuncId,
|
|
1453
|
-
name: toolName,
|
|
1454
|
-
title: title || undefined,
|
|
1455
|
-
description: description || undefined,
|
|
1456
|
-
summary,
|
|
1457
|
-
errors,
|
|
1458
|
-
tags,
|
|
1459
|
-
inputSchema: inputNames[0] ?? null,
|
|
1460
|
-
outputSchema: outputNames[0] ?? null,
|
|
1461
|
-
middleware,
|
|
1462
|
-
}
|
|
1463
|
-
state.serviceAggregation.usedFunctions.add(pikkuFuncId)
|
|
1464
|
-
extractWireNames(middleware).forEach((n) =>
|
|
1465
|
-
state.serviceAggregation.usedMiddleware.add(n)
|
|
1466
|
-
)
|
|
1467
|
-
extractWireNames(permissions).forEach((n) =>
|
|
1468
|
-
state.serviceAggregation.usedPermissions.add(n)
|
|
1469
|
-
)
|
|
1470
|
-
}
|
|
1471
|
-
|
|
1472
|
-
// Workflow functions don't get registered as RPC functions,
|
|
1473
|
-
// they are their own type handled by add-workflow
|
|
1474
|
-
if (expression.text.includes('Workflow')) {
|
|
1475
|
-
return
|
|
1476
|
-
}
|
|
1477
|
-
|
|
1478
|
-
// Trigger and channel connect/disconnect functions are not callable via RPC
|
|
1479
|
-
const nonRPCPatterns = [
|
|
1480
|
-
/Trigger/i,
|
|
1481
|
-
/ChannelConnection/i,
|
|
1482
|
-
/ChannelDisconnection/i,
|
|
1483
|
-
]
|
|
1484
|
-
if (nonRPCPatterns.some((pattern) => pattern.test(expression.text))) {
|
|
1485
|
-
return
|
|
1486
|
-
}
|
|
1487
|
-
|
|
1488
|
-
// Store function file location for wiring generation
|
|
1489
|
-
if (exportedName) {
|
|
1490
|
-
state.functions.files.set(pikkuFuncId, {
|
|
1491
|
-
path: node.getSourceFile().fileName,
|
|
1492
|
-
exportedName,
|
|
1493
|
-
})
|
|
1494
|
-
}
|
|
1495
|
-
|
|
1496
|
-
if (exportedName || explicitName) {
|
|
1497
|
-
if (!exportedName) {
|
|
1498
|
-
logger.error(
|
|
1499
|
-
`• Function with explicit name '${name}' is not exported, this is not allowed.`
|
|
1500
|
-
)
|
|
1501
|
-
return
|
|
1502
|
-
}
|
|
1503
|
-
|
|
1504
|
-
if (remote && !isScenarioStep) {
|
|
1505
|
-
state.rpc.invokedFunctions.add(pikkuFuncId)
|
|
1506
|
-
// The consumer-facing surface a wireRemoteAddon imports (mirrors exposedMeta)
|
|
1507
|
-
state.rpc.remoteMeta[name] = pikkuFuncId
|
|
1508
|
-
state.rpc.remoteFiles.set(name, {
|
|
1509
|
-
path: node.getSourceFile().fileName,
|
|
1510
|
-
exportedName,
|
|
1511
|
-
})
|
|
1512
|
-
}
|
|
1513
|
-
|
|
1514
|
-
if (expose && !isScenarioStep) {
|
|
1515
|
-
state.rpc.exposedMeta[name] = pikkuFuncId
|
|
1516
|
-
state.rpc.exposedFiles.set(name, {
|
|
1517
|
-
path: node.getSourceFile().fileName,
|
|
1518
|
-
exportedName,
|
|
1519
|
-
})
|
|
1520
|
-
// Track exposed RPC function for service aggregation
|
|
1521
|
-
state.serviceAggregation.usedFunctions.add(pikkuFuncId)
|
|
1522
|
-
}
|
|
1523
|
-
|
|
1524
|
-
if (!isScenarioStep) {
|
|
1525
|
-
// We add it to internal meta to allow autocomplete for everything
|
|
1526
|
-
state.rpc.internalMeta[name] = pikkuFuncId
|
|
1527
|
-
|
|
1528
|
-
if (version !== undefined) {
|
|
1529
|
-
state.rpc.internalMeta[pikkuFuncId] = pikkuFuncId
|
|
1530
|
-
state.rpc.invokedFunctions.add(pikkuFuncId)
|
|
1531
|
-
}
|
|
1532
|
-
}
|
|
1533
|
-
|
|
1534
|
-
// But we only import the actual function if it's actually invoked to keep
|
|
1535
|
-
// bundle size down
|
|
1536
|
-
if (
|
|
1537
|
-
state.rpc.invokedFunctions.has(pikkuFuncId) ||
|
|
1538
|
-
expose ||
|
|
1539
|
-
remote ||
|
|
1540
|
-
mcpEnabled
|
|
1541
|
-
) {
|
|
1542
|
-
state.rpc.internalFiles.set(pikkuFuncId, {
|
|
1543
|
-
path: node.getSourceFile().fileName,
|
|
1544
|
-
exportedName,
|
|
1545
|
-
})
|
|
1546
|
-
}
|
|
1547
|
-
}
|
|
1548
|
-
}
|