@pikku/inspector 0.12.60 → 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 +301 -0
- package/OPTIMIZATION-PLAN.md +19 -19
- package/dist/add/add-auth.js +1 -1
- package/dist/add/add-channel.d.ts +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-personas.js +4 -1
- 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/add/add-workflow.d.ts +1 -1
- 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 +12 -2
- package/dist/types.d.ts +25 -21
- 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 -2
- 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 +1 -1
- package/dist/utils/permissions.d.ts +1 -1
- package/dist/utils/post-process.d.ts +27 -7
- package/dist/utils/post-process.js +79 -14
- 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 +2 -1
- package/dist/utils/serialize-inspector-state.js +2 -0
- package/dist/utils/serialize-openapi-json.d.ts +2 -2
- package/dist/utils/validate-auth-sessionless.js +2 -1
- package/dist/utils/workflow/derive-workflow-plan.d.ts +1 -1
- package/dist/utils/workflow/dsl/extract-dsl-workflow.d.ts +1 -1
- package/dist/utils/workflow/graph/convert-dsl-to-graph.d.ts +1 -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 +11 -2
- package/package.json +10 -10
- package/run-tests.sh +0 -0
- package/src/add/add-agent.ts +2 -4
- package/src/add/add-auth.ts +2 -2
- package/src/add/add-channel.ts +1 -4
- package/src/add/add-cli.ts +7 -1
- package/src/add/add-credential.ts +3 -6
- package/src/add/add-file-with-factory.ts +2 -6
- package/src/add/add-functions.ts +24 -31
- package/src/add/add-gateway.ts +1 -1
- package/src/add/add-http-route-ref.test.ts +149 -0
- package/src/add/add-http-route.ts +26 -29
- package/src/add/add-keyed-wiring.ts +8 -7
- package/src/add/add-permission.ts +1 -1
- package/src/add/add-personas.test.ts +26 -2
- package/src/add/add-personas.ts +4 -3
- package/src/add/add-scenario-step.test.ts +37 -2
- package/src/add/add-scope.test.ts +15 -15
- package/src/add/add-scope.ts +5 -9
- package/src/add/add-secret.ts +1 -0
- package/src/add/add-system-role.test.ts +4 -4
- 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-input.test.ts +1 -1
- package/src/add/add-workflow-graph.ts +78 -6
- package/src/add/add-workflow-sleep-expression.test.ts +3 -4
- package/src/add/add-workflow.ts +1 -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 +19 -2
- package/src/types.ts +25 -28
- 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 -3
- package/src/utils/extract-function-name.ts +1 -1
- package/src/utils/extract-services.ts +1 -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.ts +1 -1
- package/src/utils/permissions.ts +1 -1
- package/src/utils/post-process.test.ts +59 -2
- package/src/utils/post-process.ts +88 -25
- package/src/utils/resolve-deploy-target.ts +1 -1
- package/src/utils/resolve-function-meta.ts +1 -4
- package/src/utils/resolve-function-types.ts +1 -1
- package/src/utils/resolve-versions.ts +2 -5
- package/src/utils/schema-generator.ts +3 -3
- package/src/utils/serialize-inspector-state.ts +4 -1
- package/src/utils/serialize-mcp-json.ts +1 -1
- package/src/utils/serialize-openapi-json.ts +2 -2
- 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/derive-workflow-plan.test.ts +1 -1
- package/src/utils/workflow/derive-workflow-plan.ts +1 -1
- package/src/utils/workflow/dsl/extract-approval-policy.test.ts +1 -1
- package/src/utils/workflow/dsl/extract-dsl-workflow.ts +2 -2
- package/src/utils/workflow/graph/convert-dsl-to-graph-skip.test.ts +1 -1
- package/src/utils/workflow/graph/convert-dsl-to-graph.ts +1 -4
- package/src/utils/workflow/graph/finalize-workflow-wires.ts +1 -1
- package/src/utils/workflow/graph/finalize-workflows.ts +2 -2
- package/src/utils/workflow/graph/serialize-workflow-graph.ts +34 -3
- package/src/utils/workflow/graph/workflow-graph.types.ts +12 -2
- package/tsconfig.tsbuildinfo +1 -1
- package/dist/add/add-ai-agent.d.ts +0 -4
- package/dist/add/add-ai-agent.js +0 -442
- package/dist/add/add-ai-scorer.d.ts +0 -2
- package/dist/add/add-ai-scorer.js +0 -61
- package/dist/utils/secret-brokers.d.ts +0 -2
- package/dist/utils/secret-brokers.js +0 -9
|
@@ -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', () => {
|
|
@@ -3,10 +3,23 @@ import type {
|
|
|
3
3
|
InspectorFilters,
|
|
4
4
|
InspectorLogger,
|
|
5
5
|
} from '../types.js'
|
|
6
|
-
import type { PikkuWiringTypes } from '@pikku/core/
|
|
7
|
-
import { parseVersionedId } from '@pikku/core/
|
|
6
|
+
import type { PikkuWiringTypes } from '@pikku/core/types'
|
|
7
|
+
import { parseVersionedId } from '@pikku/core/utils'
|
|
8
|
+
import { makeContextBasedId } from './extract-function-name.js'
|
|
8
9
|
import { aggregateRequiredServices } from './post-process.js'
|
|
9
|
-
import {
|
|
10
|
+
import {
|
|
11
|
+
IncompatibleDeployTargetError,
|
|
12
|
+
resolveDeployTarget,
|
|
13
|
+
} from './resolve-deploy-target.js'
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* The deploy filter, asked about one function at a time rather than handed over
|
|
17
|
+
* as a set, so that a function's own deploy conflict is raised only if this app
|
|
18
|
+
* turns out to wire it.
|
|
19
|
+
*/
|
|
20
|
+
type DeployFilter = {
|
|
21
|
+
has(pikkuFuncId: string): boolean
|
|
22
|
+
}
|
|
10
23
|
|
|
11
24
|
// Module-level Set to track warned groups across multiple filter calls
|
|
12
25
|
const globalWarnedGroups = new Set<string>()
|
|
@@ -97,6 +110,13 @@ function matchesFilters(
|
|
|
97
110
|
meta: {
|
|
98
111
|
type: PikkuWiringTypes
|
|
99
112
|
name: string
|
|
113
|
+
/**
|
|
114
|
+
* Further ids the `--names` filter may address this wiring by. An HTTP
|
|
115
|
+
* route wired with `ref('ns:fn')` carries the addon's own function id as
|
|
116
|
+
* its name, so its `http:<method>:<route>` wiring id is the only id that
|
|
117
|
+
* names this route rather than every wiring onto that addon function.
|
|
118
|
+
*/
|
|
119
|
+
altNames?: string[]
|
|
100
120
|
tags?: string[]
|
|
101
121
|
filePath?: string
|
|
102
122
|
httpRoute?: string
|
|
@@ -105,9 +125,10 @@ function matchesFilters(
|
|
|
105
125
|
},
|
|
106
126
|
logger: InspectorLogger,
|
|
107
127
|
warnedGroups?: Set<string>,
|
|
108
|
-
//
|
|
109
|
-
//
|
|
110
|
-
|
|
128
|
+
// Kept pikkuFuncIds under the deploy filter; null when the filter is
|
|
129
|
+
// inactive. Asked about one id at a time, which is what lets a conflict in an
|
|
130
|
+
// addon's own metadata surface only for a function this app actually wires.
|
|
131
|
+
keptByDeploy?: DeployFilter | null
|
|
111
132
|
): boolean {
|
|
112
133
|
// If no filters, allow everything
|
|
113
134
|
if (Object.keys(filters).length === 0) return true
|
|
@@ -188,10 +209,15 @@ function matchesFilters(
|
|
|
188
209
|
}
|
|
189
210
|
}
|
|
190
211
|
|
|
191
|
-
const
|
|
212
|
+
const candidateNames = [meta.name, ...(meta.altNames ?? [])]
|
|
192
213
|
const matchesNamePattern = (pattern: string) =>
|
|
193
|
-
|
|
194
|
-
|
|
214
|
+
candidateNames.some((candidate) => {
|
|
215
|
+
const { baseName } = parseVersionedId(candidate)
|
|
216
|
+
return (
|
|
217
|
+
matchesWildcard(candidate, pattern) ||
|
|
218
|
+
(baseName !== candidate && matchesWildcard(baseName, pattern))
|
|
219
|
+
)
|
|
220
|
+
})
|
|
195
221
|
|
|
196
222
|
// Check name include filter (match against both full ID and base name for versioned functions)
|
|
197
223
|
if (filters.names && filters.names.length > 0) {
|
|
@@ -330,7 +356,7 @@ export function filterInspectorState(
|
|
|
330
356
|
// Precompute kept-function set for the deploy filter (if active).
|
|
331
357
|
// resolveDeployTarget throws IncompatibleDeployTargetError when an
|
|
332
358
|
// explicit deploy: 'serverless' clashes with serverlessIncompatible.
|
|
333
|
-
let keptByDeploy:
|
|
359
|
+
let keptByDeploy: DeployFilter | null = null
|
|
334
360
|
if (
|
|
335
361
|
(filters.target && filters.target.length > 0) ||
|
|
336
362
|
(filters.excludeTarget && filters.excludeTarget.length > 0)
|
|
@@ -341,17 +367,56 @@ export function filterInspectorState(
|
|
|
341
367
|
: null
|
|
342
368
|
const incompatible = new Set(filters.serverlessIncompatible ?? [])
|
|
343
369
|
const defaultTarget = filters.defaultTarget ?? 'serverless'
|
|
344
|
-
|
|
345
|
-
for
|
|
370
|
+
const kept = new Set<string>()
|
|
371
|
+
// An addon publishes metadata for everything it exports, so a conflict in a
|
|
372
|
+
// function this app never wires is not this app's build error. Holding it
|
|
373
|
+
// here and raising it from `has` reports it at the one place the app is
|
|
374
|
+
// known to depend on that function.
|
|
375
|
+
const conflicts = new Map<string, IncompatibleDeployTargetError>()
|
|
376
|
+
keptByDeploy = {
|
|
377
|
+
has: (funcId: string) => {
|
|
378
|
+
const conflict = conflicts.get(funcId)
|
|
379
|
+
if (conflict) throw conflict
|
|
380
|
+
return kept.has(funcId)
|
|
381
|
+
},
|
|
382
|
+
}
|
|
383
|
+
const keepByTarget = (
|
|
384
|
+
funcId: string,
|
|
385
|
+
funcMeta: unknown,
|
|
386
|
+
incompatibleServices: Set<string>
|
|
387
|
+
) => {
|
|
346
388
|
const target = resolveDeployTarget(
|
|
347
389
|
funcMeta as any,
|
|
348
|
-
|
|
390
|
+
incompatibleServices,
|
|
349
391
|
funcId,
|
|
350
392
|
defaultTarget
|
|
351
393
|
)
|
|
352
|
-
if (allowed && !allowed.has(target))
|
|
353
|
-
if (excluded && excluded.has(target))
|
|
354
|
-
|
|
394
|
+
if (allowed && !allowed.has(target)) return
|
|
395
|
+
if (excluded && excluded.has(target)) return
|
|
396
|
+
kept.add(funcId)
|
|
397
|
+
}
|
|
398
|
+
for (const [funcId, funcMeta] of Object.entries(state.functions.meta)) {
|
|
399
|
+
keepByTarget(funcId, funcMeta, incompatible)
|
|
400
|
+
}
|
|
401
|
+
// A wiring onto an addon function records the addon's own id, so the
|
|
402
|
+
// addon's published metadata is the only place its deploy target can be
|
|
403
|
+
// read from. Its serverlessIncompatible services are namespace-scoped —
|
|
404
|
+
// a service name means whatever the addon that declared it means.
|
|
405
|
+
for (const [namespace, addonMeta] of Object.entries(
|
|
406
|
+
state.addonFunctions ?? {}
|
|
407
|
+
)) {
|
|
408
|
+
const addonIncompatible = new Set(
|
|
409
|
+
state.addonServerlessIncompatible?.get(namespace) ?? []
|
|
410
|
+
)
|
|
411
|
+
for (const [funcName, funcMeta] of Object.entries(addonMeta)) {
|
|
412
|
+
const funcId = `${namespace}:${funcName}`
|
|
413
|
+
try {
|
|
414
|
+
keepByTarget(funcId, funcMeta, addonIncompatible)
|
|
415
|
+
} catch (e) {
|
|
416
|
+
if (!(e instanceof IncompatibleDeployTargetError)) throw e
|
|
417
|
+
conflicts.set(funcId, e)
|
|
418
|
+
}
|
|
419
|
+
}
|
|
355
420
|
}
|
|
356
421
|
}
|
|
357
422
|
|
|
@@ -459,6 +524,7 @@ export function filterInspectorState(
|
|
|
459
524
|
{
|
|
460
525
|
type: 'http' as PikkuWiringTypes,
|
|
461
526
|
name: routeMeta.pikkuFuncId, // Use function name, not route
|
|
527
|
+
altNames: [makeContextBasedId('http', method, routeMeta.route)],
|
|
462
528
|
tags: routeMeta.tags,
|
|
463
529
|
filePath,
|
|
464
530
|
httpRoute: routeMeta.route,
|
|
@@ -478,11 +544,6 @@ export function filterInspectorState(
|
|
|
478
544
|
filteredState.serviceAggregation.usedFunctions.add(
|
|
479
545
|
routeMeta.pikkuFuncId
|
|
480
546
|
)
|
|
481
|
-
if (routeMeta.refTarget) {
|
|
482
|
-
filteredState.serviceAggregation.usedFunctions.add(
|
|
483
|
-
routeMeta.refTarget
|
|
484
|
-
)
|
|
485
|
-
}
|
|
486
547
|
// For workflow/agent routes, also add the base name
|
|
487
548
|
// so the workflow/agent definition survives pruning
|
|
488
549
|
const colonIdx = routeMeta.pikkuFuncId.indexOf(':')
|