@pikku/inspector 0.12.59 → 0.12.61
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 +373 -0
- package/OPTIMIZATION-PLAN.md +19 -19
- package/dist/add/add-agent-scorer.d.ts +2 -0
- package/dist/add/{add-ai-scorer.js → add-agent-scorer.js} +4 -4
- package/dist/add/{add-ai-agent.d.ts → add-agent.d.ts} +1 -1
- package/dist/add/{add-ai-agent.js → add-agent.js} +11 -11
- package/dist/add/add-auth.js +1 -1
- package/dist/add/add-cli.js +5 -0
- package/dist/add/add-functions.js +10 -1
- package/dist/add/add-http-route.js +18 -18
- package/dist/add/add-keyed-wiring.d.ts +2 -0
- package/dist/add/add-keyed-wiring.js +3 -1
- package/dist/add/add-middleware.js +5 -5
- package/dist/add/add-personas.js +3 -0
- package/dist/add/add-secret.js +1 -0
- package/dist/add/add-variable.js +1 -0
- package/dist/add/add-wire-remote-addon.js +2 -1
- package/dist/add/add-workflow-graph.js +63 -6
- package/dist/error-codes.d.ts +3 -1
- package/dist/error-codes.js +3 -1
- package/dist/index.d.ts +3 -1
- package/dist/index.js +1 -0
- package/dist/inspector.js +13 -3
- package/dist/types.d.ts +10 -5
- package/dist/utils/annotate-http-route-auth.js +2 -1
- package/dist/utils/check-secret-sinks.js +2 -1
- package/dist/utils/compute-required-schemas.d.ts +1 -1
- package/dist/utils/contract-hashes.d.ts +2 -1
- package/dist/utils/contract-hashes.js +1 -1
- package/dist/utils/extract-function-name.js +1 -1
- package/dist/utils/extract-services.d.ts +1 -1
- package/dist/utils/extract-surface-usage.d.ts +29 -0
- package/dist/utils/extract-surface-usage.js +97 -0
- package/dist/utils/filter-inspector-state.js +53 -16
- package/dist/utils/filter-utils.d.ts +1 -1
- package/dist/utils/get-property-value.js +42 -3
- package/dist/utils/load-addon-functions-meta.js +31 -0
- package/dist/utils/middleware.d.ts +2 -2
- package/dist/utils/middleware.js +5 -5
- package/dist/utils/permissions.d.ts +1 -1
- package/dist/utils/post-process.d.ts +28 -8
- package/dist/utils/post-process.js +83 -18
- package/dist/utils/resolve-deploy-target.d.ts +1 -1
- package/dist/utils/resolve-function-meta.d.ts +1 -1
- package/dist/utils/resolve-function-types.d.ts +1 -1
- package/dist/utils/resolve-versions.js +1 -1
- package/dist/utils/schema-generator.d.ts +1 -1
- package/dist/utils/serialize-inspector-state.d.ts +4 -3
- package/dist/utils/serialize-inspector-state.js +6 -4
- package/dist/utils/serialize-openapi-json.d.ts +1 -1
- package/dist/utils/validate-auth-sessionless.js +2 -1
- package/dist/utils/workflow/graph/finalize-workflows.js +1 -1
- package/dist/utils/workflow/graph/serialize-workflow-graph.d.ts +6 -1
- package/dist/utils/workflow/graph/serialize-workflow-graph.js +26 -2
- package/dist/utils/workflow/graph/workflow-graph.types.d.ts +9 -0
- package/dist/visit.js +4 -4
- package/package.json +10 -10
- package/run-tests.sh +0 -0
- package/src/add/{add-ai-scorer.test.ts → add-agent-scorer.test.ts} +11 -11
- package/src/add/{add-ai-scorer.ts → add-agent-scorer.ts} +4 -4
- package/src/add/{add-ai-agent.test.ts → add-agent.test.ts} +1 -1
- package/src/add/{add-ai-agent.ts → add-agent.ts} +13 -21
- package/src/add/add-auth.ts +2 -2
- package/src/add/add-cli.ts +6 -0
- package/src/add/add-credential.ts +3 -6
- package/src/add/add-file-with-factory.ts +2 -6
- package/src/add/add-functions.ts +23 -25
- package/src/add/add-http-route-ref.test.ts +149 -0
- package/src/add/add-http-route.ts +25 -28
- package/src/add/add-keyed-wiring.ts +8 -7
- package/src/add/add-middleware.ts +7 -5
- package/src/add/add-permission.ts +1 -1
- package/src/add/add-personas.test.ts +24 -0
- package/src/add/add-personas.ts +3 -1
- package/src/add/add-scenario-step.test.ts +35 -0
- package/src/add/add-scope.test.ts +4 -4
- package/src/add/add-scope.ts +4 -8
- package/src/add/add-secret.ts +1 -0
- package/src/add/add-system-role.test.ts +8 -8
- package/src/add/add-system-role.ts +2 -4
- package/src/add/add-variable.ts +1 -0
- package/src/add/add-wire-remote-addon.ts +4 -1
- package/src/add/add-workflow-graph-foreach.test.ts +174 -0
- package/src/add/add-workflow-graph.ts +78 -6
- package/src/add/add-workflow-sleep-expression.test.ts +3 -4
- package/src/add/addon-contracts.test.ts +1 -1
- package/src/add/config-value-metadata.test.ts +94 -0
- package/src/add/secret-sink-check.test.ts +4 -1
- package/src/error-codes.ts +3 -1
- package/src/index.ts +3 -4
- package/src/inspector.ts +20 -3
- package/src/types.ts +10 -7
- package/src/utils/annotate-http-route-auth.test.ts +5 -7
- package/src/utils/annotate-http-route-auth.ts +2 -1
- package/src/utils/check-secret-sinks.ts +4 -1
- package/src/utils/compute-required-schemas.ts +1 -1
- package/src/utils/contract-hashes.test.ts +1 -1
- package/src/utils/contract-hashes.ts +3 -2
- package/src/utils/extract-function-name.ts +1 -1
- package/src/utils/extract-services.ts +4 -2
- package/src/utils/extract-surface-usage.test.ts +147 -0
- package/src/utils/extract-surface-usage.ts +121 -0
- package/src/utils/filter-inspector-state.test.ts +138 -12
- package/src/utils/filter-inspector-state.ts +82 -21
- package/src/utils/filter-utils.ts +1 -1
- package/src/utils/get-property-value.ts +43 -5
- package/src/utils/load-addon-functions-meta.ts +40 -0
- package/src/utils/middleware.test.ts +23 -23
- package/src/utils/middleware.ts +6 -6
- package/src/utils/permissions.ts +1 -1
- package/src/utils/post-process.test.ts +68 -11
- package/src/utils/post-process.ts +91 -28
- package/src/utils/resolve-deploy-target.ts +1 -1
- package/src/utils/resolve-function-meta.ts +1 -1
- package/src/utils/resolve-function-types.ts +1 -1
- package/src/utils/resolve-versions.ts +1 -1
- package/src/utils/schema-generator.ts +2 -1
- package/src/utils/serialize-inspector-state.ts +10 -7
- package/src/utils/serialize-mcp-json.ts +1 -1
- package/src/utils/serialize-openapi-json.ts +1 -1
- package/src/utils/validate-auth-sessionless.ts +2 -1
- package/src/utils/validate-tags-resolve-to-middleware.test.ts +4 -1
- package/src/utils/workflow/dsl/extract-approval-policy.test.ts +4 -1
- package/src/utils/workflow/graph/convert-dsl-to-graph-skip.test.ts +1 -1
- package/src/utils/workflow/graph/finalize-workflows.ts +6 -2
- package/src/utils/workflow/graph/serialize-workflow-graph.ts +34 -3
- package/src/utils/workflow/graph/workflow-graph.types.ts +10 -0
- package/src/visit.ts +4 -4
- package/tsconfig.tsbuildinfo +1 -1
- package/dist/add/add-ai-scorer.d.ts +0 -2
- package/dist/utils/secret-brokers.d.ts +0 -2
- package/dist/utils/secret-brokers.js +0 -9
|
@@ -108,18 +108,16 @@ describe('annotateHttpRouteAuth', () => {
|
|
|
108
108
|
|
|
109
109
|
test('a ref route resolves against its target', () => {
|
|
110
110
|
const state = {
|
|
111
|
-
functions: {
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
target: { pikkuFuncId: 'target', sessionless: false },
|
|
115
|
-
},
|
|
111
|
+
functions: { meta: {} },
|
|
112
|
+
addonFunctions: {
|
|
113
|
+
console: { target: { pikkuFuncId: 'target', sessionless: false } },
|
|
116
114
|
},
|
|
117
115
|
http: {
|
|
118
116
|
meta: {
|
|
119
117
|
get: {
|
|
120
118
|
'/thing': {
|
|
121
|
-
pikkuFuncId: '
|
|
122
|
-
|
|
119
|
+
pikkuFuncId: 'console:target',
|
|
120
|
+
packageName: ADDON_PACKAGE,
|
|
123
121
|
route: '/thing',
|
|
124
122
|
method: 'get',
|
|
125
123
|
},
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { InspectorState } from '../types.js'
|
|
2
|
+
import { resolveFunctionMeta } from './resolve-function-meta.js'
|
|
2
3
|
|
|
3
4
|
/**
|
|
4
5
|
* Resolve, per HTTP route, whether reaching it requires a session.
|
|
@@ -25,7 +26,7 @@ export function annotateHttpRouteAuth(state: InspectorState): void {
|
|
|
25
26
|
|
|
26
27
|
for (const routes of Object.values(state.http?.meta ?? {})) {
|
|
27
28
|
for (const route of Object.values(routes)) {
|
|
28
|
-
const meta = state
|
|
29
|
+
const meta = resolveFunctionMeta(state, route.pikkuFuncId)
|
|
29
30
|
|
|
30
31
|
route.requiresSession =
|
|
31
32
|
// A pikkuFunc always requires a session, on every path.
|
|
@@ -53,7 +53,10 @@ export function findRevealedSecretSinks(
|
|
|
53
53
|
const violations: SecretSinkViolation[] = []
|
|
54
54
|
|
|
55
55
|
const visit = (node: ts.Node): void => {
|
|
56
|
-
if (
|
|
56
|
+
if (
|
|
57
|
+
ts.isCallExpression(node) &&
|
|
58
|
+
ts.isPropertyAccessExpression(node.expression)
|
|
59
|
+
) {
|
|
57
60
|
const sink = describeSink(checker, node.expression)
|
|
58
61
|
if (sink) {
|
|
59
62
|
for (const arg of node.arguments) {
|
|
@@ -10,7 +10,7 @@ import {
|
|
|
10
10
|
extractContractsFromMeta,
|
|
11
11
|
} from './contract-hashes.js'
|
|
12
12
|
import type { ContractEntry, VersionManifest } from './contract-hashes.js'
|
|
13
|
-
import type { FunctionsMeta } from '@pikku/core'
|
|
13
|
+
import type { FunctionsMeta } from '@pikku/core/services'
|
|
14
14
|
import { ErrorCode } from '../error-codes.js'
|
|
15
15
|
|
|
16
16
|
function makeContracts(
|
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import type { FunctionsMeta
|
|
2
|
-
import {
|
|
1
|
+
import type { FunctionsMeta } from '@pikku/core/services'
|
|
2
|
+
import type { JSONValue } from '@pikku/core/utils'
|
|
3
|
+
import { parseVersionedId } from '@pikku/core/utils'
|
|
3
4
|
import type { TypesMap } from '../types-map.js'
|
|
4
5
|
import { ErrorCode } from '../error-codes.js'
|
|
5
6
|
import { canonicalJSON, hashString } from './hash.js'
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import * as ts from 'typescript'
|
|
2
2
|
import { createHash } from 'crypto'
|
|
3
3
|
import { relative } from 'path'
|
|
4
|
-
import { formatVersionedId } from '@pikku/core'
|
|
4
|
+
import { formatVersionedId } from '@pikku/core/utils'
|
|
5
5
|
|
|
6
6
|
/**
|
|
7
7
|
* Deterministic placeholder id for an anonymous/unnamed pikku function or
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import * as ts from 'typescript'
|
|
2
|
-
import type {
|
|
3
|
-
|
|
2
|
+
import type {
|
|
3
|
+
FunctionServicesMeta,
|
|
4
|
+
FunctionWiresMeta,
|
|
5
|
+
} from '@pikku/core/function'
|
|
4
6
|
/**
|
|
5
7
|
* Extract services from a function's first parameter destructuring pattern
|
|
6
8
|
*/
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
import assert from 'node:assert'
|
|
2
|
+
import { describe, test } from 'node:test'
|
|
3
|
+
import ts from 'typescript'
|
|
4
|
+
|
|
5
|
+
import {
|
|
6
|
+
accumulateSurfaceUsage,
|
|
7
|
+
surfaceUsageArea,
|
|
8
|
+
type SurfaceUsageCounts,
|
|
9
|
+
} from './extract-surface-usage.js'
|
|
10
|
+
|
|
11
|
+
const countsFor = (
|
|
12
|
+
files: Array<{ area: string; source: string }>
|
|
13
|
+
): SurfaceUsageCounts => {
|
|
14
|
+
const counts: SurfaceUsageCounts = {}
|
|
15
|
+
files.forEach(({ area, source }, index) => {
|
|
16
|
+
const file = `src/file-${index}.ts`
|
|
17
|
+
accumulateSurfaceUsage(
|
|
18
|
+
ts.createSourceFile(
|
|
19
|
+
`/project/${file}`,
|
|
20
|
+
source,
|
|
21
|
+
ts.ScriptTarget.Latest,
|
|
22
|
+
true
|
|
23
|
+
),
|
|
24
|
+
{ area, file },
|
|
25
|
+
counts
|
|
26
|
+
)
|
|
27
|
+
})
|
|
28
|
+
return counts
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
describe('accumulateSurfaceUsage', () => {
|
|
32
|
+
test('counts an import site per named import', () => {
|
|
33
|
+
const counts = countsFor([
|
|
34
|
+
{
|
|
35
|
+
area: 'src',
|
|
36
|
+
source:
|
|
37
|
+
"import { pikkuFunc, pikkuSessionlessFunc } from '#pikku/function'\n",
|
|
38
|
+
},
|
|
39
|
+
{ area: 'src', source: "import { pikkuFunc } from '#pikku/function'\n" },
|
|
40
|
+
])
|
|
41
|
+
|
|
42
|
+
assert.deepStrictEqual(counts['#pikku/function'], {
|
|
43
|
+
pikkuFunc: {
|
|
44
|
+
imports: 2,
|
|
45
|
+
seenIn: ['src'],
|
|
46
|
+
files: ['src/file-0.ts', 'src/file-1.ts'],
|
|
47
|
+
},
|
|
48
|
+
pikkuSessionlessFunc: {
|
|
49
|
+
imports: 1,
|
|
50
|
+
seenIn: ['src'],
|
|
51
|
+
files: ['src/file-0.ts'],
|
|
52
|
+
},
|
|
53
|
+
})
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
test('counts a deep specifier against the leaf it belongs to', () => {
|
|
57
|
+
const counts = countsFor([
|
|
58
|
+
{
|
|
59
|
+
area: 'src',
|
|
60
|
+
source:
|
|
61
|
+
"import { pikkuWorkflowFunc } from '#pikku/workflow/pikku-workflow-types.gen.js'\n",
|
|
62
|
+
},
|
|
63
|
+
{
|
|
64
|
+
area: 'workers',
|
|
65
|
+
source: "import { pikkuWorkflowFunc } from '#pikku/workflow'\n",
|
|
66
|
+
},
|
|
67
|
+
])
|
|
68
|
+
|
|
69
|
+
assert.deepStrictEqual(counts['#pikku/workflow'], {
|
|
70
|
+
pikkuWorkflowFunc: {
|
|
71
|
+
imports: 2,
|
|
72
|
+
seenIn: ['src', 'workers'],
|
|
73
|
+
files: ['src/file-0.ts', 'src/file-1.ts'],
|
|
74
|
+
},
|
|
75
|
+
})
|
|
76
|
+
})
|
|
77
|
+
|
|
78
|
+
test('records the aliased name, not the local one', () => {
|
|
79
|
+
const counts = countsFor([
|
|
80
|
+
{
|
|
81
|
+
area: 'src',
|
|
82
|
+
source: "import { cors as allowAll } from '#pikku/http'\n",
|
|
83
|
+
},
|
|
84
|
+
])
|
|
85
|
+
|
|
86
|
+
assert.deepStrictEqual(counts['#pikku/http'], {
|
|
87
|
+
cors: { imports: 1, seenIn: ['src'], files: ['src/file-0.ts'] },
|
|
88
|
+
})
|
|
89
|
+
})
|
|
90
|
+
|
|
91
|
+
test('counts a re-export as a use of the leaf', () => {
|
|
92
|
+
const counts = countsFor([
|
|
93
|
+
{ area: 'src', source: "export { wireHTTP } from '#pikku/http'\n" },
|
|
94
|
+
])
|
|
95
|
+
|
|
96
|
+
assert.deepStrictEqual(counts['#pikku/http']!.wireHTTP!.imports, 1)
|
|
97
|
+
})
|
|
98
|
+
|
|
99
|
+
test('ignores imports from anywhere else', () => {
|
|
100
|
+
const counts = countsFor([
|
|
101
|
+
{
|
|
102
|
+
area: 'src',
|
|
103
|
+
source: [
|
|
104
|
+
"import { z } from 'zod'",
|
|
105
|
+
"import { thing } from './local.js'",
|
|
106
|
+
"import * as everything from '#pikku/function'",
|
|
107
|
+
"import bootstrap from '#pikku/pikku-bootstrap.gen.js'",
|
|
108
|
+
].join('\n'),
|
|
109
|
+
},
|
|
110
|
+
])
|
|
111
|
+
|
|
112
|
+
assert.deepStrictEqual(counts, {})
|
|
113
|
+
})
|
|
114
|
+
})
|
|
115
|
+
|
|
116
|
+
describe('surfaceUsageArea', () => {
|
|
117
|
+
test('falls back to the top-level source directory in a single package', () => {
|
|
118
|
+
assert.strictEqual(
|
|
119
|
+
surfaceUsageArea('/project/src/wirings/todo.ts', '/project', new Map()),
|
|
120
|
+
'src'
|
|
121
|
+
)
|
|
122
|
+
})
|
|
123
|
+
|
|
124
|
+
test('does not label everything with the root package name', () => {
|
|
125
|
+
const cache = new Map<string, string | null>([
|
|
126
|
+
['/project/src/wirings', null],
|
|
127
|
+
['/project/src', null],
|
|
128
|
+
])
|
|
129
|
+
|
|
130
|
+
assert.strictEqual(
|
|
131
|
+
surfaceUsageArea('/project/src/wirings/todo.ts', '/project', cache),
|
|
132
|
+
'src'
|
|
133
|
+
)
|
|
134
|
+
})
|
|
135
|
+
|
|
136
|
+
test('prefers the workspace package a file belongs to', () => {
|
|
137
|
+
const cache = new Map<string, string | null>([
|
|
138
|
+
['/project/packages/api/src', null],
|
|
139
|
+
['/project/packages/api', '@app/api'],
|
|
140
|
+
])
|
|
141
|
+
|
|
142
|
+
assert.strictEqual(
|
|
143
|
+
surfaceUsageArea('/project/packages/api/src/todo.ts', '/project', cache),
|
|
144
|
+
'@app/api'
|
|
145
|
+
)
|
|
146
|
+
})
|
|
147
|
+
})
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from 'fs'
|
|
2
|
+
import { basename, dirname, relative, sep } from 'path'
|
|
3
|
+
import ts from 'typescript'
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* How often each `#pikku/*` export is imported, the source areas it was
|
|
7
|
+
* imported from, and the files those imports are written in. Counters only:
|
|
8
|
+
* this is accumulated during the sweep that already visits every source file,
|
|
9
|
+
* so nothing here may retain a node.
|
|
10
|
+
*/
|
|
11
|
+
export type SurfaceUsageCounts = Record<
|
|
12
|
+
string,
|
|
13
|
+
Record<string, { imports: number; seenIn: string[]; files: string[] }>
|
|
14
|
+
>
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* The leaf a specifier belongs to. `#pikku/workflow` and the deep
|
|
18
|
+
* `#pikku/workflow/pikku-workflow-types.gen.js` are the same door, so both
|
|
19
|
+
* count against `#pikku/workflow`.
|
|
20
|
+
*/
|
|
21
|
+
const leafSpecifierOf = (specifier: string): string | null => {
|
|
22
|
+
if (!specifier.startsWith('#pikku/')) return null
|
|
23
|
+
const leaf = specifier.slice('#pikku/'.length).split('/')[0]
|
|
24
|
+
// A root-level generated file, e.g. `#pikku/pikku-bootstrap.gen.js`, is not a
|
|
25
|
+
// leaf and has no editorial entry to count against.
|
|
26
|
+
if (!leaf || leaf.endsWith('.js')) return null
|
|
27
|
+
return `#pikku/${leaf}`
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const packageNameAt = (
|
|
31
|
+
directory: string,
|
|
32
|
+
cache: Map<string, string | null>
|
|
33
|
+
): string | null => {
|
|
34
|
+
const cached = cache.get(directory)
|
|
35
|
+
if (cached !== undefined) return cached
|
|
36
|
+
let name: string | null = null
|
|
37
|
+
const path = `${directory}${sep}package.json`
|
|
38
|
+
if (existsSync(path)) {
|
|
39
|
+
try {
|
|
40
|
+
name =
|
|
41
|
+
(JSON.parse(readFileSync(path, 'utf8')) as { name?: string }).name ??
|
|
42
|
+
null
|
|
43
|
+
} catch {
|
|
44
|
+
name = null
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
cache.set(directory, name)
|
|
48
|
+
return name
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* A label a person recognises: the workspace package a file belongs to, or the
|
|
53
|
+
* top-level source directory it sits under when the project is one package.
|
|
54
|
+
* The root package is skipped deliberately — naming it would label every file
|
|
55
|
+
* in a single-package project identically.
|
|
56
|
+
*/
|
|
57
|
+
export const surfaceUsageArea = (
|
|
58
|
+
fileName: string,
|
|
59
|
+
rootDir: string,
|
|
60
|
+
cache: Map<string, string | null>
|
|
61
|
+
): string => {
|
|
62
|
+
let directory = dirname(fileName)
|
|
63
|
+
while (directory.startsWith(rootDir) && directory !== rootDir) {
|
|
64
|
+
const name = packageNameAt(directory, cache)
|
|
65
|
+
if (name) return name
|
|
66
|
+
directory = dirname(directory)
|
|
67
|
+
}
|
|
68
|
+
const [segment] = relative(rootDir, fileName).split(sep)
|
|
69
|
+
return segment && segment !== basename(fileName) ? segment : basename(rootDir)
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const namesOf = (
|
|
73
|
+
clause: ts.NamedImportBindings | ts.NamedExportBindings | undefined
|
|
74
|
+
): string[] => {
|
|
75
|
+
if (!clause) return []
|
|
76
|
+
if (ts.isNamedImports(clause) || ts.isNamedExports(clause)) {
|
|
77
|
+
return clause.elements.map(
|
|
78
|
+
(element) => (element.propertyName ?? element.name).text
|
|
79
|
+
)
|
|
80
|
+
}
|
|
81
|
+
return []
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Counts the `#pikku/*` imports of one file into `into`. Only the file's own
|
|
86
|
+
* top-level statements are looked at — an import declaration can appear nowhere
|
|
87
|
+
* else — so this costs a single pass over the statement list and retains
|
|
88
|
+
* nothing.
|
|
89
|
+
*/
|
|
90
|
+
export const accumulateSurfaceUsage = (
|
|
91
|
+
sourceFile: ts.SourceFile,
|
|
92
|
+
{ area, file }: { area: string; file: string },
|
|
93
|
+
into: SurfaceUsageCounts
|
|
94
|
+
): void => {
|
|
95
|
+
for (const statement of sourceFile.statements) {
|
|
96
|
+
let specifier: ts.Expression | undefined
|
|
97
|
+
let names: string[] = []
|
|
98
|
+
|
|
99
|
+
if (ts.isImportDeclaration(statement)) {
|
|
100
|
+
specifier = statement.moduleSpecifier
|
|
101
|
+
names = namesOf(statement.importClause?.namedBindings)
|
|
102
|
+
} else if (ts.isExportDeclaration(statement) && statement.moduleSpecifier) {
|
|
103
|
+
specifier = statement.moduleSpecifier
|
|
104
|
+
names = namesOf(statement.exportClause)
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
if (!specifier || names.length === 0 || !ts.isStringLiteral(specifier)) {
|
|
108
|
+
continue
|
|
109
|
+
}
|
|
110
|
+
const leaf = leafSpecifierOf(specifier.text)
|
|
111
|
+
if (!leaf) continue
|
|
112
|
+
|
|
113
|
+
const symbols = (into[leaf] ??= {})
|
|
114
|
+
for (const name of names) {
|
|
115
|
+
const usage = (symbols[name] ??= { imports: 0, seenIn: [], files: [] })
|
|
116
|
+
usage.imports += 1
|
|
117
|
+
if (!usage.seenIn.includes(area)) usage.seenIn.push(area)
|
|
118
|
+
if (!usage.files.includes(file)) usage.files.push(file)
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
}
|
|
@@ -1614,6 +1614,54 @@ describe('addonServerlessIncompatible scoping', () => {
|
|
|
1614
1614
|
})
|
|
1615
1615
|
})
|
|
1616
1616
|
|
|
1617
|
+
describe("an addon's own deploy conflict is the consumer's only when used", () => {
|
|
1618
|
+
// An addon publishes metadata for everything it exports. A function it marks
|
|
1619
|
+
// `deploy: 'serverless'` while also naming one of its services
|
|
1620
|
+
// serverless-incompatible is the addon author's contradiction — but the
|
|
1621
|
+
// deploy scan walks every published function, so without care it fails the
|
|
1622
|
+
// build of a consumer that never wires that function.
|
|
1623
|
+
const withConflictingAddonFunction = (route?: string) => {
|
|
1624
|
+
const state = createMockInspectorState()
|
|
1625
|
+
;(state as any).addonFunctions = {
|
|
1626
|
+
ffmpeg: {
|
|
1627
|
+
transcode: {
|
|
1628
|
+
deploy: 'serverless',
|
|
1629
|
+
services: { services: ['FfmpegService'] },
|
|
1630
|
+
},
|
|
1631
|
+
},
|
|
1632
|
+
}
|
|
1633
|
+
state.addonServerlessIncompatible = new Map([['ffmpeg', ['FfmpegService']]])
|
|
1634
|
+
if (route) {
|
|
1635
|
+
;(state.http.meta.get as any)[route] = {
|
|
1636
|
+
pikkuFuncId: 'ffmpeg:transcode',
|
|
1637
|
+
route,
|
|
1638
|
+
method: 'GET',
|
|
1639
|
+
tags: [],
|
|
1640
|
+
middleware: [],
|
|
1641
|
+
permissions: [],
|
|
1642
|
+
}
|
|
1643
|
+
}
|
|
1644
|
+
return state
|
|
1645
|
+
}
|
|
1646
|
+
|
|
1647
|
+
test('a conflicting addon function nothing wires does not fail the build', () => {
|
|
1648
|
+
const state = withConflictingAddonFunction()
|
|
1649
|
+
|
|
1650
|
+
assert.doesNotThrow(() =>
|
|
1651
|
+
filterInspectorState(state, { target: ['serverless'] }, mockLogger)
|
|
1652
|
+
)
|
|
1653
|
+
})
|
|
1654
|
+
|
|
1655
|
+
test('wiring that same function reports the conflict', () => {
|
|
1656
|
+
const state = withConflictingAddonFunction('/transcode')
|
|
1657
|
+
|
|
1658
|
+
assert.throws(
|
|
1659
|
+
() => filterInspectorState(state, { target: ['serverless'] }, mockLogger),
|
|
1660
|
+
/FfmpegService/
|
|
1661
|
+
)
|
|
1662
|
+
})
|
|
1663
|
+
})
|
|
1664
|
+
|
|
1617
1665
|
describe('addonServerlessIncompatible serialization roundtrip', () => {
|
|
1618
1666
|
// Uses getInitialInspectorState to guarantee all Map fields are initialized —
|
|
1619
1667
|
// the lightweight mock in createMockInspectorState() omits fields like
|
|
@@ -1705,21 +1753,18 @@ describe('addon bootstrap tree-shake', () => {
|
|
|
1705
1753
|
test('keeps an addon when a kept route ref()-targets one of its functions', () => {
|
|
1706
1754
|
const state = withAddon((s) => {
|
|
1707
1755
|
s.http.meta.get['/workflow-run/stream'] = {
|
|
1708
|
-
pikkuFuncId: '
|
|
1709
|
-
|
|
1756
|
+
pikkuFuncId: 'console:streamWorkflowRun',
|
|
1757
|
+
packageName: '@pikku/console',
|
|
1710
1758
|
route: '/workflow-run/stream',
|
|
1711
1759
|
method: 'GET',
|
|
1712
|
-
tags: [],
|
|
1760
|
+
tags: ['stream'],
|
|
1713
1761
|
middleware: [],
|
|
1714
1762
|
permissions: [],
|
|
1715
1763
|
} as any
|
|
1716
|
-
s.functions.meta['http:get:/workflow-run/stream'] = {
|
|
1717
|
-
services: { optimized: false, services: [] },
|
|
1718
|
-
} as any
|
|
1719
1764
|
})
|
|
1720
1765
|
const filtered = filterInspectorState(
|
|
1721
1766
|
state,
|
|
1722
|
-
{
|
|
1767
|
+
{ tags: ['stream'] },
|
|
1723
1768
|
mockLogger
|
|
1724
1769
|
)
|
|
1725
1770
|
assert.strictEqual(filtered.rpc.wireAddonDeclarations.size, 1)
|
|
@@ -1729,19 +1774,41 @@ describe('addon bootstrap tree-shake', () => {
|
|
|
1729
1774
|
)
|
|
1730
1775
|
})
|
|
1731
1776
|
|
|
1732
|
-
test('
|
|
1777
|
+
test('keeps a ref()-wired route selected by its wiring id', () => {
|
|
1733
1778
|
const state = withAddon((s) => {
|
|
1734
1779
|
s.http.meta.get['/workflow-run/stream'] = {
|
|
1735
|
-
pikkuFuncId: '
|
|
1736
|
-
|
|
1780
|
+
pikkuFuncId: 'console:streamWorkflowRun',
|
|
1781
|
+
packageName: '@pikku/console',
|
|
1737
1782
|
route: '/workflow-run/stream',
|
|
1738
1783
|
method: 'GET',
|
|
1739
1784
|
tags: [],
|
|
1740
1785
|
middleware: [],
|
|
1741
1786
|
permissions: [],
|
|
1742
1787
|
} as any
|
|
1743
|
-
|
|
1744
|
-
|
|
1788
|
+
})
|
|
1789
|
+
const filtered = filterInspectorState(
|
|
1790
|
+
state,
|
|
1791
|
+
{ names: ['http:get:/workflow-run/stream'] },
|
|
1792
|
+
mockLogger
|
|
1793
|
+
)
|
|
1794
|
+
assert.ok(filtered.http.meta.get['/workflow-run/stream'])
|
|
1795
|
+
assert.ok(
|
|
1796
|
+
filtered.serviceAggregation.usedFunctions.has('console:streamWorkflowRun')
|
|
1797
|
+
)
|
|
1798
|
+
assert.strictEqual(filtered.rpc.wireAddonDeclarations.size, 1)
|
|
1799
|
+
assert.ok(filtered.rpc.wireAddonDeclarations.has('console'))
|
|
1800
|
+
})
|
|
1801
|
+
|
|
1802
|
+
test('drops the addon when the ref()-wired route is filtered out', () => {
|
|
1803
|
+
const state = withAddon((s) => {
|
|
1804
|
+
s.http.meta.get['/workflow-run/stream'] = {
|
|
1805
|
+
pikkuFuncId: 'console:streamWorkflowRun',
|
|
1806
|
+
packageName: '@pikku/console',
|
|
1807
|
+
route: '/workflow-run/stream',
|
|
1808
|
+
method: 'GET',
|
|
1809
|
+
tags: [],
|
|
1810
|
+
middleware: [],
|
|
1811
|
+
permissions: [],
|
|
1745
1812
|
} as any
|
|
1746
1813
|
})
|
|
1747
1814
|
const filtered = filterInspectorState(
|
|
@@ -1843,6 +1910,65 @@ describe('addon bootstrap tree-shake', () => {
|
|
|
1843
1910
|
const filtered = filterInspectorState(state, {}, mockLogger)
|
|
1844
1911
|
assert.strictEqual(filtered.rpc.wireAddonDeclarations.size, 1)
|
|
1845
1912
|
})
|
|
1913
|
+
|
|
1914
|
+
const withRefWiredAddonRoute = (
|
|
1915
|
+
addonFuncMeta: Record<string, unknown> = { services: [], optimized: false }
|
|
1916
|
+
) =>
|
|
1917
|
+
withAddon((s) => {
|
|
1918
|
+
s.http.meta.get['/workflow-run/stream'] = {
|
|
1919
|
+
pikkuFuncId: 'console:streamWorkflowRun',
|
|
1920
|
+
packageName: '@pikku/addon-console',
|
|
1921
|
+
route: '/workflow-run/stream',
|
|
1922
|
+
method: 'GET',
|
|
1923
|
+
tags: [],
|
|
1924
|
+
middleware: [],
|
|
1925
|
+
permissions: [],
|
|
1926
|
+
} as any
|
|
1927
|
+
s.addonFunctions = {
|
|
1928
|
+
console: { streamWorkflowRun: addonFuncMeta as any },
|
|
1929
|
+
}
|
|
1930
|
+
})
|
|
1931
|
+
|
|
1932
|
+
test('the deploy filter keeps a ref()-wired route onto a serverless addon function', () => {
|
|
1933
|
+
const filtered = filterInspectorState(
|
|
1934
|
+
withRefWiredAddonRoute(),
|
|
1935
|
+
{ target: ['serverless'] },
|
|
1936
|
+
mockLogger
|
|
1937
|
+
)
|
|
1938
|
+
assert.ok(filtered.http.meta.get['/workflow-run/stream'])
|
|
1939
|
+
assert.strictEqual(filtered.rpc.wireAddonDeclarations.size, 1)
|
|
1940
|
+
assert.ok(filtered.rpc.wireAddonDeclarations.has('console'))
|
|
1941
|
+
})
|
|
1942
|
+
|
|
1943
|
+
test('the deploy filter drops a ref()-wired route onto a server-only addon function', () => {
|
|
1944
|
+
const filtered = filterInspectorState(
|
|
1945
|
+
withRefWiredAddonRoute({
|
|
1946
|
+
services: [],
|
|
1947
|
+
optimized: false,
|
|
1948
|
+
deploy: 'server',
|
|
1949
|
+
}),
|
|
1950
|
+
{ target: ['serverless'] },
|
|
1951
|
+
mockLogger
|
|
1952
|
+
)
|
|
1953
|
+
assert.ok(!filtered.http.meta.get['/workflow-run/stream'])
|
|
1954
|
+
assert.strictEqual(filtered.rpc.wireAddonDeclarations.size, 0)
|
|
1955
|
+
})
|
|
1956
|
+
|
|
1957
|
+
test("the deploy filter honours the addon's own serverlessIncompatible services", () => {
|
|
1958
|
+
const state = withRefWiredAddonRoute({
|
|
1959
|
+
services: { services: ['FfmpegService'] },
|
|
1960
|
+
optimized: false,
|
|
1961
|
+
})
|
|
1962
|
+
state.addonServerlessIncompatible = new Map([
|
|
1963
|
+
['console', ['FfmpegService']],
|
|
1964
|
+
])
|
|
1965
|
+
const filtered = filterInspectorState(
|
|
1966
|
+
state,
|
|
1967
|
+
{ target: ['serverless'] },
|
|
1968
|
+
mockLogger
|
|
1969
|
+
)
|
|
1970
|
+
assert.ok(!filtered.http.meta.get['/workflow-run/stream'])
|
|
1971
|
+
})
|
|
1846
1972
|
})
|
|
1847
1973
|
|
|
1848
1974
|
describe('invokedFunctionsByFile serialization', () => {
|