@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,106 +0,0 @@
|
|
|
1
|
-
import * as ts from 'typescript'
|
|
2
|
-
import { existsSync, readFileSync } from 'fs'
|
|
3
|
-
import { dirname, join, parse } from 'path'
|
|
4
|
-
|
|
5
|
-
const packageNameCache = new Map<string, string | null>()
|
|
6
|
-
|
|
7
|
-
const findPackageNameForFile = (filePath: string): string | null => {
|
|
8
|
-
if (packageNameCache.has(filePath)) {
|
|
9
|
-
return packageNameCache.get(filePath)!
|
|
10
|
-
}
|
|
11
|
-
const root = parse(filePath).root
|
|
12
|
-
let dir = dirname(filePath)
|
|
13
|
-
while (dir && dir !== root) {
|
|
14
|
-
const pkgPath = join(dir, 'package.json')
|
|
15
|
-
if (existsSync(pkgPath)) {
|
|
16
|
-
try {
|
|
17
|
-
const pkg = JSON.parse(readFileSync(pkgPath, 'utf-8'))
|
|
18
|
-
const name = typeof pkg.name === 'string' ? pkg.name : null
|
|
19
|
-
packageNameCache.set(filePath, name)
|
|
20
|
-
return name
|
|
21
|
-
} catch {
|
|
22
|
-
packageNameCache.set(filePath, null)
|
|
23
|
-
return null
|
|
24
|
-
}
|
|
25
|
-
}
|
|
26
|
-
const parent = dirname(dir)
|
|
27
|
-
if (parent === dir) break
|
|
28
|
-
dir = parent
|
|
29
|
-
}
|
|
30
|
-
packageNameCache.set(filePath, null)
|
|
31
|
-
return null
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
/**
|
|
35
|
-
* Resolve the addon package name from an imported identifier.
|
|
36
|
-
* Checks if the identifier's import module specifier matches any
|
|
37
|
-
* configured addon package — and if the import is relative (because
|
|
38
|
-
* the identifier resolves to a source file inside the addon package
|
|
39
|
-
* itself), walks up to the nearest package.json to obtain the real
|
|
40
|
-
* package name.
|
|
41
|
-
*/
|
|
42
|
-
export const resolveAddonName = (
|
|
43
|
-
identifier: ts.Identifier,
|
|
44
|
-
checker: ts.TypeChecker,
|
|
45
|
-
wireAddonDeclarations?: Map<
|
|
46
|
-
string,
|
|
47
|
-
{
|
|
48
|
-
package: string
|
|
49
|
-
rpcEndpoint?: string
|
|
50
|
-
secretOverrides?: Record<string, string>
|
|
51
|
-
variableOverrides?: Record<string, string>
|
|
52
|
-
credentialOverrides?: Record<string, string>
|
|
53
|
-
}
|
|
54
|
-
>
|
|
55
|
-
): string | null => {
|
|
56
|
-
if (!wireAddonDeclarations || wireAddonDeclarations.size === 0) {
|
|
57
|
-
return null
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
const sym = checker.getSymbolAtLocation(identifier)
|
|
61
|
-
if (!sym) return null
|
|
62
|
-
|
|
63
|
-
const decl = sym.declarations?.[0]
|
|
64
|
-
if (!decl) return null
|
|
65
|
-
|
|
66
|
-
let candidatePackage: string | null = null
|
|
67
|
-
|
|
68
|
-
if (ts.isImportSpecifier(decl)) {
|
|
69
|
-
// ImportSpecifier -> NamedImports -> ImportClause -> ImportDeclaration
|
|
70
|
-
const importDecl = decl.parent?.parent?.parent
|
|
71
|
-
if (
|
|
72
|
-
importDecl &&
|
|
73
|
-
ts.isImportDeclaration(importDecl) &&
|
|
74
|
-
ts.isStringLiteral(importDecl.moduleSpecifier)
|
|
75
|
-
) {
|
|
76
|
-
candidatePackage = importDecl.moduleSpecifier.text
|
|
77
|
-
}
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
// Bare package import path
|
|
81
|
-
if (candidatePackage && !candidatePackage.startsWith('.')) {
|
|
82
|
-
for (const addonDecl of wireAddonDeclarations.values()) {
|
|
83
|
-
if (
|
|
84
|
-
addonDecl.package === candidatePackage ||
|
|
85
|
-
candidatePackage.startsWith(`${addonDecl.package}/`)
|
|
86
|
-
) {
|
|
87
|
-
return addonDecl.package
|
|
88
|
-
}
|
|
89
|
-
}
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
// Fall back to package.json lookup based on the declaration's source file.
|
|
93
|
-
// This catches the case where the identifier resolves into an addon
|
|
94
|
-
// package's own internal source (relative import inside that package).
|
|
95
|
-
const declFile = decl.getSourceFile()?.fileName
|
|
96
|
-
if (declFile) {
|
|
97
|
-
const pkgName = findPackageNameForFile(declFile)
|
|
98
|
-
if (pkgName) {
|
|
99
|
-
for (const addonDecl of wireAddonDeclarations.values()) {
|
|
100
|
-
if (addonDecl.package === pkgName) return addonDecl.package
|
|
101
|
-
}
|
|
102
|
-
}
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
return null
|
|
106
|
-
}
|
|
@@ -1,142 +0,0 @@
|
|
|
1
|
-
import { describe, test } from 'node:test'
|
|
2
|
-
import assert from 'node:assert/strict'
|
|
3
|
-
import ts from 'typescript'
|
|
4
|
-
import { aggregateRequiredServices } from './post-process.js'
|
|
5
|
-
import type { InspectorState, PathToNameAndType } from '../types.js'
|
|
6
|
-
|
|
7
|
-
const SOURCE_FILE = '/application-types.ts'
|
|
8
|
-
|
|
9
|
-
/**
|
|
10
|
-
* Builds the slice of inspector state that service extraction reads: the
|
|
11
|
-
* `typesLookup` entry the AST walk records under whatever the project named its
|
|
12
|
-
* interface, and the import map that points at that name.
|
|
13
|
-
*/
|
|
14
|
-
const stateWithCoreTypes = (
|
|
15
|
-
source: string,
|
|
16
|
-
singletonName: string,
|
|
17
|
-
wireName: string
|
|
18
|
-
): InspectorState => {
|
|
19
|
-
const host: ts.CompilerHost = {
|
|
20
|
-
...ts.createCompilerHost({}),
|
|
21
|
-
getSourceFile: (name, languageVersion) =>
|
|
22
|
-
name === SOURCE_FILE
|
|
23
|
-
? ts.createSourceFile(name, source, languageVersion, true)
|
|
24
|
-
: undefined,
|
|
25
|
-
fileExists: (name) => name === SOURCE_FILE,
|
|
26
|
-
readFile: (name) => (name === SOURCE_FILE ? source : undefined),
|
|
27
|
-
}
|
|
28
|
-
const program = ts.createProgram([SOURCE_FILE], { noLib: true }, host)
|
|
29
|
-
const checker = program.getTypeChecker()
|
|
30
|
-
const sourceFile = program.getSourceFile(SOURCE_FILE)!
|
|
31
|
-
|
|
32
|
-
const typesLookup = new Map<string, ts.Type[]>()
|
|
33
|
-
const importMapFor = (name: string): PathToNameAndType => {
|
|
34
|
-
const declaration = sourceFile.statements.find(
|
|
35
|
-
(statement): statement is ts.InterfaceDeclaration =>
|
|
36
|
-
ts.isInterfaceDeclaration(statement) && statement.name.text === name
|
|
37
|
-
)!
|
|
38
|
-
typesLookup.set(name, [checker.getTypeAtLocation(declaration)])
|
|
39
|
-
return new Map([
|
|
40
|
-
[SOURCE_FILE, [{ variable: name, type: name, typePath: SOURCE_FILE }]],
|
|
41
|
-
])
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
return {
|
|
45
|
-
typesLookup,
|
|
46
|
-
singletonServicesTypeImportMap: importMapFor(singletonName),
|
|
47
|
-
wireServicesTypeImportMap: importMapFor(wireName),
|
|
48
|
-
serviceAggregation: {
|
|
49
|
-
requiredServices: new Set<string>(),
|
|
50
|
-
usedFunctions: new Set<string>(),
|
|
51
|
-
usedMiddleware: new Set<string>(),
|
|
52
|
-
usedPermissions: new Set<string>(),
|
|
53
|
-
allSingletonServices: [],
|
|
54
|
-
allWireServices: [],
|
|
55
|
-
},
|
|
56
|
-
functions: { meta: {} },
|
|
57
|
-
middleware: { definitions: {}, tagMiddleware: new Map() },
|
|
58
|
-
permissions: { definitions: {} },
|
|
59
|
-
http: {
|
|
60
|
-
meta: {
|
|
61
|
-
get: {},
|
|
62
|
-
post: {},
|
|
63
|
-
put: {},
|
|
64
|
-
patch: {},
|
|
65
|
-
delete: {},
|
|
66
|
-
head: {},
|
|
67
|
-
options: {},
|
|
68
|
-
},
|
|
69
|
-
routeMiddleware: new Map(),
|
|
70
|
-
},
|
|
71
|
-
channels: { meta: {} },
|
|
72
|
-
queueWorkers: { meta: {} },
|
|
73
|
-
scheduledTasks: { meta: {} },
|
|
74
|
-
mcpEndpoints: { toolsMeta: {}, promptsMeta: {}, resourcesMeta: {} },
|
|
75
|
-
agents: { agentsMeta: {} },
|
|
76
|
-
workflows: { meta: {}, graphMeta: {} },
|
|
77
|
-
wireServicesMeta: new Map(),
|
|
78
|
-
rpc: { internalMeta: {}, exposedMeta: {} },
|
|
79
|
-
scopes: { definitions: [] },
|
|
80
|
-
addonFunctions: {},
|
|
81
|
-
addonRequiredParentServices: [],
|
|
82
|
-
} as unknown as InspectorState
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
const CONVENTIONAL = `
|
|
86
|
-
interface SingletonServices { logger: string; kysely: string }
|
|
87
|
-
interface Services { logger: string; kysely: string; http: string }
|
|
88
|
-
`
|
|
89
|
-
|
|
90
|
-
const RENAMED = `
|
|
91
|
-
interface AppSingletonServices { logger: string; kysely: string }
|
|
92
|
-
interface AppServices { logger: string; kysely: string; http: string }
|
|
93
|
-
`
|
|
94
|
-
|
|
95
|
-
describe('service extraction resolves core types by declaration, not by name', () => {
|
|
96
|
-
test('the conventional names still resolve', () => {
|
|
97
|
-
const state = stateWithCoreTypes(
|
|
98
|
-
CONVENTIONAL,
|
|
99
|
-
'SingletonServices',
|
|
100
|
-
'Services'
|
|
101
|
-
)
|
|
102
|
-
aggregateRequiredServices(state)
|
|
103
|
-
assert.deepEqual(state.serviceAggregation.allSingletonServices, [
|
|
104
|
-
'kysely',
|
|
105
|
-
'logger',
|
|
106
|
-
])
|
|
107
|
-
assert.deepEqual(state.serviceAggregation.allWireServices, ['http'])
|
|
108
|
-
})
|
|
109
|
-
|
|
110
|
-
test('a project that renamed its interfaces resolves the same way', () => {
|
|
111
|
-
const state = stateWithCoreTypes(
|
|
112
|
-
RENAMED,
|
|
113
|
-
'AppSingletonServices',
|
|
114
|
-
'AppServices'
|
|
115
|
-
)
|
|
116
|
-
aggregateRequiredServices(state)
|
|
117
|
-
assert.deepEqual(
|
|
118
|
-
state.serviceAggregation.allSingletonServices,
|
|
119
|
-
['kysely', 'logger'],
|
|
120
|
-
'a renamed SingletonServices must not silently resolve to no services'
|
|
121
|
-
)
|
|
122
|
-
assert.deepEqual(state.serviceAggregation.allWireServices, ['http'])
|
|
123
|
-
})
|
|
124
|
-
|
|
125
|
-
test('no declaration at all leaves the lists empty for PKU724 to catch', () => {
|
|
126
|
-
const state = stateWithCoreTypes(
|
|
127
|
-
CONVENTIONAL,
|
|
128
|
-
'SingletonServices',
|
|
129
|
-
'Services'
|
|
130
|
-
)
|
|
131
|
-
state.singletonServicesTypeImportMap = new Map()
|
|
132
|
-
state.wireServicesTypeImportMap = new Map()
|
|
133
|
-
// Left at their initial empty value on purpose: `extractAllServices` only
|
|
134
|
-
// assigns when a type resolves, so seeding them here would make the
|
|
135
|
-
// assertions below true no matter what it did. `typesLookup` still holds
|
|
136
|
-
// both interfaces under their conventional names — resolving one from
|
|
137
|
-
// there is exactly the bug this guards.
|
|
138
|
-
aggregateRequiredServices(state)
|
|
139
|
-
assert.deepEqual(state.serviceAggregation.allSingletonServices, [])
|
|
140
|
-
assert.deepEqual(state.serviceAggregation.allWireServices, [])
|
|
141
|
-
})
|
|
142
|
-
})
|
|
@@ -1,35 +0,0 @@
|
|
|
1
|
-
import type ts from 'typescript'
|
|
2
|
-
import type { PathToNameAndType } from '../types.js'
|
|
3
|
-
|
|
4
|
-
/**
|
|
5
|
-
* Resolves the one interface extending a core type (`CoreConfig`,
|
|
6
|
-
* `CoreSingletonServices`, `CoreServices`) to the `ts.Type` the AST walk
|
|
7
|
-
* recorded for it.
|
|
8
|
-
*
|
|
9
|
-
* `typesLookup` is keyed by whatever the project named the interface, so
|
|
10
|
-
* reading it under a hardcoded `'SingletonServices'` only works for projects
|
|
11
|
-
* that follow the scaffold's naming. A project that renamed the interface still
|
|
12
|
-
* satisfies every required-type check and then resolves to no services at all,
|
|
13
|
-
* which surfaces far downstream as PKU724 or as every service turning optional.
|
|
14
|
-
* The import map carries the real name, so no name matching is needed.
|
|
15
|
-
*
|
|
16
|
-
* A project is only ever meant to declare one of each: `getMetaTypes` records
|
|
17
|
-
* `More than one ... found` when it sees a second, which `checkRequiredTypes`
|
|
18
|
-
* then reports. That check is the caller's to run, and extraction happens
|
|
19
|
-
* first, so where two declarations do exist this takes the first in traversal
|
|
20
|
-
* order rather than diagnosing the ambiguity itself.
|
|
21
|
-
*/
|
|
22
|
-
export const resolveCoreType = (
|
|
23
|
-
typesLookup: Map<string, ts.Type[]>,
|
|
24
|
-
importMap: PathToNameAndType
|
|
25
|
-
): ts.Type | undefined => {
|
|
26
|
-
for (const entries of importMap.values()) {
|
|
27
|
-
for (const { type } of entries) {
|
|
28
|
-
const resolved = type ? typesLookup.get(type)?.[0] : undefined
|
|
29
|
-
if (resolved) {
|
|
30
|
-
return resolved
|
|
31
|
-
}
|
|
32
|
-
}
|
|
33
|
-
}
|
|
34
|
-
return undefined
|
|
35
|
-
}
|
|
@@ -1,135 +0,0 @@
|
|
|
1
|
-
import { describe, test } from 'node:test'
|
|
2
|
-
import assert from 'node:assert'
|
|
3
|
-
import {
|
|
4
|
-
IncompatibleDeployTargetError,
|
|
5
|
-
resolveDeployTarget,
|
|
6
|
-
} from './resolve-deploy-target.js'
|
|
7
|
-
|
|
8
|
-
describe('resolveDeployTarget', () => {
|
|
9
|
-
test('default → serverless', () => {
|
|
10
|
-
assert.strictEqual(resolveDeployTarget({}, new Set()), 'serverless')
|
|
11
|
-
})
|
|
12
|
-
|
|
13
|
-
test('explicit deploy: server → server', () => {
|
|
14
|
-
assert.strictEqual(
|
|
15
|
-
resolveDeployTarget({ deploy: 'server' }, new Set()),
|
|
16
|
-
'server'
|
|
17
|
-
)
|
|
18
|
-
})
|
|
19
|
-
|
|
20
|
-
test('explicit deploy: serverless with no incompatible svc → serverless', () => {
|
|
21
|
-
assert.strictEqual(
|
|
22
|
-
resolveDeployTarget(
|
|
23
|
-
{ deploy: 'serverless', services: { services: ['kysely'] } as any },
|
|
24
|
-
new Set(['fs'])
|
|
25
|
-
),
|
|
26
|
-
'serverless'
|
|
27
|
-
)
|
|
28
|
-
})
|
|
29
|
-
|
|
30
|
-
test('serverlessIncompatible service forces server even without deploy flag', () => {
|
|
31
|
-
assert.strictEqual(
|
|
32
|
-
resolveDeployTarget(
|
|
33
|
-
{ services: { services: ['metaService'] } as any },
|
|
34
|
-
new Set(['metaService'])
|
|
35
|
-
),
|
|
36
|
-
'server'
|
|
37
|
-
)
|
|
38
|
-
})
|
|
39
|
-
|
|
40
|
-
test('serverlessIncompatible takes precedence over explicit deploy: server', () => {
|
|
41
|
-
assert.strictEqual(
|
|
42
|
-
resolveDeployTarget(
|
|
43
|
-
{
|
|
44
|
-
deploy: 'server',
|
|
45
|
-
services: { services: ['metaService'] } as any,
|
|
46
|
-
},
|
|
47
|
-
new Set(['metaService'])
|
|
48
|
-
),
|
|
49
|
-
'server'
|
|
50
|
-
)
|
|
51
|
-
})
|
|
52
|
-
|
|
53
|
-
test('explicit deploy: serverless + incompatible svc → throws', () => {
|
|
54
|
-
assert.throws(
|
|
55
|
-
() =>
|
|
56
|
-
resolveDeployTarget(
|
|
57
|
-
{
|
|
58
|
-
deploy: 'serverless',
|
|
59
|
-
services: { services: ['metaService', 'unrelated'] } as any,
|
|
60
|
-
},
|
|
61
|
-
new Set(['metaService']),
|
|
62
|
-
'myFunction'
|
|
63
|
-
),
|
|
64
|
-
(err: unknown) => {
|
|
65
|
-
assert.ok(err instanceof IncompatibleDeployTargetError)
|
|
66
|
-
assert.strictEqual(err.functionName, 'myFunction')
|
|
67
|
-
assert.deepStrictEqual(err.incompatibleServices, ['metaService'])
|
|
68
|
-
assert.match(err.message, /serverless-incompatible/)
|
|
69
|
-
assert.match(err.message, /myFunction/)
|
|
70
|
-
return true
|
|
71
|
-
}
|
|
72
|
-
)
|
|
73
|
-
})
|
|
74
|
-
|
|
75
|
-
test('multiple incompatible services are all reported', () => {
|
|
76
|
-
assert.throws(
|
|
77
|
-
() =>
|
|
78
|
-
resolveDeployTarget(
|
|
79
|
-
{
|
|
80
|
-
deploy: 'serverless',
|
|
81
|
-
services: {
|
|
82
|
-
services: ['metaService', 'localContent'],
|
|
83
|
-
} as any,
|
|
84
|
-
},
|
|
85
|
-
new Set(['metaService', 'localContent']),
|
|
86
|
-
'multiSvc'
|
|
87
|
-
),
|
|
88
|
-
(err: unknown) => {
|
|
89
|
-
assert.ok(err instanceof IncompatibleDeployTargetError)
|
|
90
|
-
assert.deepStrictEqual(err.incompatibleServices, [
|
|
91
|
-
'metaService',
|
|
92
|
-
'localContent',
|
|
93
|
-
])
|
|
94
|
-
return true
|
|
95
|
-
}
|
|
96
|
-
)
|
|
97
|
-
})
|
|
98
|
-
|
|
99
|
-
test('handles funcMeta with no services field', () => {
|
|
100
|
-
assert.strictEqual(
|
|
101
|
-
resolveDeployTarget({ deploy: 'server' }, new Set(['metaService'])),
|
|
102
|
-
'server'
|
|
103
|
-
)
|
|
104
|
-
})
|
|
105
|
-
|
|
106
|
-
test('defaultTarget: server overrides the serverless default', () => {
|
|
107
|
-
assert.strictEqual(
|
|
108
|
-
resolveDeployTarget({}, new Set(), '<unknown>', 'server'),
|
|
109
|
-
'server'
|
|
110
|
-
)
|
|
111
|
-
})
|
|
112
|
-
|
|
113
|
-
test('explicit deploy flag wins over defaultTarget', () => {
|
|
114
|
-
assert.strictEqual(
|
|
115
|
-
resolveDeployTarget({ deploy: 'serverless' }, new Set(), 'fn', 'server'),
|
|
116
|
-
'serverless'
|
|
117
|
-
)
|
|
118
|
-
assert.strictEqual(
|
|
119
|
-
resolveDeployTarget({ deploy: 'server' }, new Set(), 'fn', 'serverless'),
|
|
120
|
-
'server'
|
|
121
|
-
)
|
|
122
|
-
})
|
|
123
|
-
|
|
124
|
-
test('serverlessIncompatible still forces server regardless of defaultTarget', () => {
|
|
125
|
-
assert.strictEqual(
|
|
126
|
-
resolveDeployTarget(
|
|
127
|
-
{ services: { services: ['metaService'] } as any },
|
|
128
|
-
new Set(['metaService']),
|
|
129
|
-
'fn',
|
|
130
|
-
'serverless'
|
|
131
|
-
),
|
|
132
|
-
'server'
|
|
133
|
-
)
|
|
134
|
-
})
|
|
135
|
-
})
|
|
@@ -1,65 +0,0 @@
|
|
|
1
|
-
import type { FunctionMeta } from '@pikku/core/services'
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* Thrown when a function's explicit `deploy: 'serverless'` conflicts
|
|
5
|
-
* with one of its services being declared `serverlessIncompatible`.
|
|
6
|
-
* The user has to either remove the explicit flag (let it auto-resolve
|
|
7
|
-
* to 'server'), or set `deploy: 'server'` explicitly.
|
|
8
|
-
*/
|
|
9
|
-
export class IncompatibleDeployTargetError extends Error {
|
|
10
|
-
constructor(
|
|
11
|
-
public readonly functionName: string,
|
|
12
|
-
public readonly incompatibleServices: string[]
|
|
13
|
-
) {
|
|
14
|
-
super(
|
|
15
|
-
`Function '${functionName}' is declared deploy: 'serverless' but uses ` +
|
|
16
|
-
`serverless-incompatible service(s) [${incompatibleServices.join(', ')}]. ` +
|
|
17
|
-
`Either remove deploy: 'serverless' (will auto-resolve to 'server'), ` +
|
|
18
|
-
`or set deploy: 'server' explicitly.`
|
|
19
|
-
)
|
|
20
|
-
this.name = 'IncompatibleDeployTargetError'
|
|
21
|
-
}
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
/**
|
|
25
|
-
* Determine the effective deploy target for a function.
|
|
26
|
-
*
|
|
27
|
-
* Resolution order:
|
|
28
|
-
* 1. If any of the function's services is in `serverlessIncompatible`:
|
|
29
|
-
* - throw if the function explicitly declares `deploy: 'serverless'`
|
|
30
|
-
* - otherwise target is 'server'
|
|
31
|
-
* 2. Explicit `funcMeta.deploy: 'serverless' | 'server'`
|
|
32
|
-
* 3. `defaultTarget` (sourced from `pikku.config.json` →
|
|
33
|
-
* `deploy.defaultTarget`, falling back to 'serverless')
|
|
34
|
-
*
|
|
35
|
-
* Used both by the per-unit deploy analyzer (when bucketing functions
|
|
36
|
-
* into deployment units) and by `filterInspectorState` (when
|
|
37
|
-
* `pikku all --deploy <target>` is used to emit a target-scoped set
|
|
38
|
-
* of gen files).
|
|
39
|
-
*/
|
|
40
|
-
export function resolveDeployTarget(
|
|
41
|
-
funcMeta: Pick<FunctionMeta, 'deploy' | 'services'>,
|
|
42
|
-
serverlessIncompatible: Set<string>,
|
|
43
|
-
functionName = '<unknown>',
|
|
44
|
-
defaultTarget: 'serverless' | 'server' = 'serverless'
|
|
45
|
-
): 'serverless' | 'server' {
|
|
46
|
-
// Service compatibility wins over the explicit flag — a serverless
|
|
47
|
-
// bundle of a function that needs (e.g.) node:fs would crash at runtime.
|
|
48
|
-
const incompatibleHits: string[] = []
|
|
49
|
-
if (funcMeta.services?.services) {
|
|
50
|
-
for (const svc of funcMeta.services.services) {
|
|
51
|
-
if (serverlessIncompatible.has(svc)) incompatibleHits.push(svc)
|
|
52
|
-
}
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
if (incompatibleHits.length > 0) {
|
|
56
|
-
if (funcMeta.deploy === 'serverless') {
|
|
57
|
-
throw new IncompatibleDeployTargetError(functionName, incompatibleHits)
|
|
58
|
-
}
|
|
59
|
-
return 'server'
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
if (funcMeta.deploy === 'server') return 'server'
|
|
63
|
-
if (funcMeta.deploy === 'serverless') return 'serverless'
|
|
64
|
-
return defaultTarget
|
|
65
|
-
}
|
|
@@ -1,25 +0,0 @@
|
|
|
1
|
-
import type { FunctionMeta, FunctionsMeta } from '@pikku/core/services'
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* Look up function metadata by pikkuFuncId, checking both local functions
|
|
5
|
-
* and addon functions. Addon functions use namespaced IDs like 'namespace:funcName'.
|
|
6
|
-
*/
|
|
7
|
-
export function resolveFunctionMeta(
|
|
8
|
-
state: {
|
|
9
|
-
functions: { meta: FunctionsMeta }
|
|
10
|
-
addonFunctions: Record<string, FunctionsMeta>
|
|
11
|
-
},
|
|
12
|
-
pikkuFuncId: string
|
|
13
|
-
): FunctionMeta | undefined {
|
|
14
|
-
// Check local functions first
|
|
15
|
-
const local = state.functions.meta[pikkuFuncId]
|
|
16
|
-
if (local) return local
|
|
17
|
-
|
|
18
|
-
// Check addon functions (namespaced like 'swaggerPetstore:addPet')
|
|
19
|
-
const colonIndex = pikkuFuncId.indexOf(':')
|
|
20
|
-
if (colonIndex === -1) return undefined
|
|
21
|
-
|
|
22
|
-
const namespace = pikkuFuncId.substring(0, colonIndex)
|
|
23
|
-
const funcName = pikkuFuncId.substring(colonIndex + 1)
|
|
24
|
-
return state.addonFunctions[namespace]?.[funcName]
|
|
25
|
-
}
|
|
@@ -1,42 +0,0 @@
|
|
|
1
|
-
import type { TypesMap } from '../types-map.js'
|
|
2
|
-
import type { FunctionsMeta } from '@pikku/core/services'
|
|
3
|
-
|
|
4
|
-
export function resolveFunctionIOTypes(
|
|
5
|
-
pikkuFuncId: string,
|
|
6
|
-
functionsMeta: FunctionsMeta,
|
|
7
|
-
typesMap: TypesMap,
|
|
8
|
-
requiredTypes: Set<string>
|
|
9
|
-
): { inputType: string; outputType: string } {
|
|
10
|
-
const functionMeta = functionsMeta[pikkuFuncId]
|
|
11
|
-
if (!functionMeta) {
|
|
12
|
-
throw new Error(
|
|
13
|
-
`Function ${pikkuFuncId} not found in functionsMeta. Please check your configuration.`
|
|
14
|
-
)
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
const input = functionMeta.inputs ? functionMeta.inputs[0] : undefined
|
|
18
|
-
const output = functionMeta.outputs ? functionMeta.outputs[0] : undefined
|
|
19
|
-
|
|
20
|
-
let inputType = 'null'
|
|
21
|
-
if (input) {
|
|
22
|
-
try {
|
|
23
|
-
inputType = typesMap.getTypeMeta(input).uniqueName
|
|
24
|
-
} catch {
|
|
25
|
-
inputType = input
|
|
26
|
-
}
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
let outputType = 'null'
|
|
30
|
-
if (output) {
|
|
31
|
-
try {
|
|
32
|
-
outputType = typesMap.getTypeMeta(output).uniqueName
|
|
33
|
-
} catch {
|
|
34
|
-
outputType = output
|
|
35
|
-
}
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
requiredTypes.add(inputType)
|
|
39
|
-
requiredTypes.add(outputType)
|
|
40
|
-
|
|
41
|
-
return { inputType, outputType }
|
|
42
|
-
}
|
|
@@ -1,46 +0,0 @@
|
|
|
1
|
-
import * as ts from 'typescript'
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* Resolve an identifier to its definition, optionally unwrapping
|
|
5
|
-
* known "define" wrapper functions (e.g. defineHTTPRoutes, defineCLICommands).
|
|
6
|
-
*
|
|
7
|
-
* When the identifier resolves to a variable whose initializer is a call
|
|
8
|
-
* to one of the `unwrapFunctionNames`, the first argument of that call
|
|
9
|
-
* is returned instead.
|
|
10
|
-
*/
|
|
11
|
-
export function resolveIdentifier(
|
|
12
|
-
node: ts.Identifier,
|
|
13
|
-
checker: ts.TypeChecker,
|
|
14
|
-
unwrapFunctionNames?: string[]
|
|
15
|
-
): ts.Node | undefined {
|
|
16
|
-
const symbol = checker.getSymbolAtLocation(node)
|
|
17
|
-
if (!symbol) return undefined
|
|
18
|
-
|
|
19
|
-
// Handle aliased symbols (imports)
|
|
20
|
-
let resolvedSymbol = symbol
|
|
21
|
-
if (resolvedSymbol.flags & ts.SymbolFlags.Alias) {
|
|
22
|
-
resolvedSymbol = checker.getAliasedSymbol(resolvedSymbol) ?? resolvedSymbol
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
const decl =
|
|
26
|
-
resolvedSymbol.valueDeclaration || resolvedSymbol.declarations?.[0]
|
|
27
|
-
if (!decl) return undefined
|
|
28
|
-
|
|
29
|
-
// Follow to the actual value (handles imports, variable declarations)
|
|
30
|
-
if (ts.isVariableDeclaration(decl) && decl.initializer) {
|
|
31
|
-
// Check if it's a call to one of the unwrap functions
|
|
32
|
-
if (
|
|
33
|
-
ts.isCallExpression(decl.initializer) &&
|
|
34
|
-
unwrapFunctionNames &&
|
|
35
|
-
unwrapFunctionNames.length > 0
|
|
36
|
-
) {
|
|
37
|
-
const expr = decl.initializer.expression
|
|
38
|
-
if (ts.isIdentifier(expr) && unwrapFunctionNames.includes(expr.text)) {
|
|
39
|
-
return decl.initializer.arguments[0]
|
|
40
|
-
}
|
|
41
|
-
}
|
|
42
|
-
return decl.initializer
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
return undefined
|
|
46
|
-
}
|
|
@@ -1,71 +0,0 @@
|
|
|
1
|
-
import * as ts from 'typescript'
|
|
2
|
-
|
|
3
|
-
export interface RefContractResolution<T> {
|
|
4
|
-
contract: T
|
|
5
|
-
/**
|
|
6
|
-
* Optional basePath override supplied by the consumer via the second
|
|
7
|
-
* argument, e.g. refHTTP('ns:routes', { basePath: '/ext' }). When undefined
|
|
8
|
-
* the addon contract's own basePath is preserved.
|
|
9
|
-
*/
|
|
10
|
-
basePath?: string
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
const getStringProperty = (
|
|
14
|
-
obj: ts.ObjectLiteralExpression,
|
|
15
|
-
name: string
|
|
16
|
-
): string | undefined => {
|
|
17
|
-
for (const prop of obj.properties) {
|
|
18
|
-
if (
|
|
19
|
-
ts.isPropertyAssignment(prop) &&
|
|
20
|
-
(ts.isIdentifier(prop.name) || ts.isStringLiteral(prop.name)) &&
|
|
21
|
-
prop.name.text === name &&
|
|
22
|
-
ts.isStringLiteral(prop.initializer)
|
|
23
|
-
) {
|
|
24
|
-
return prop.initializer.text
|
|
25
|
-
}
|
|
26
|
-
}
|
|
27
|
-
return undefined
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
/**
|
|
31
|
-
* Resolve a refHTTP / refChannel / refCLI call expression against the addon
|
|
32
|
-
* contracts already loaded (and namespaced) by loadAddonFunctionsMeta.
|
|
33
|
-
*
|
|
34
|
-
* The first string argument has the form 'namespace:contractName', mirroring
|
|
35
|
-
* how ref('namespace:fn') references an addon function. Detection is purely
|
|
36
|
-
* syntactic — no import resolution is required because the namespace and
|
|
37
|
-
* contract name are carried in the string literal. An optional second object
|
|
38
|
-
* argument may override mount details such as basePath.
|
|
39
|
-
*/
|
|
40
|
-
export const resolveRefContract = <T>(
|
|
41
|
-
node: ts.Node,
|
|
42
|
-
helperName: 'refHTTP' | 'refChannel' | 'refCLI',
|
|
43
|
-
addonContracts: Record<string, Record<string, T>>
|
|
44
|
-
): RefContractResolution<T> | null => {
|
|
45
|
-
if (!ts.isCallExpression(node)) return null
|
|
46
|
-
if (
|
|
47
|
-
!ts.isIdentifier(node.expression) ||
|
|
48
|
-
node.expression.text !== helperName
|
|
49
|
-
) {
|
|
50
|
-
return null
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
const [arg, optionsArg] = node.arguments
|
|
54
|
-
if (!arg || !ts.isStringLiteral(arg)) return null
|
|
55
|
-
|
|
56
|
-
const separator = arg.text.indexOf(':')
|
|
57
|
-
if (separator === -1) return null
|
|
58
|
-
|
|
59
|
-
const namespace = arg.text.slice(0, separator)
|
|
60
|
-
const contractName = arg.text.slice(separator + 1)
|
|
61
|
-
|
|
62
|
-
const contract = addonContracts[namespace]?.[contractName]
|
|
63
|
-
if (contract === undefined) return null
|
|
64
|
-
|
|
65
|
-
let basePath: string | undefined
|
|
66
|
-
if (optionsArg && ts.isObjectLiteralExpression(optionsArg)) {
|
|
67
|
-
basePath = getStringProperty(optionsArg, 'basePath')
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
return { contract, basePath }
|
|
71
|
-
}
|