@pikku/inspector 0.12.60 → 0.12.62

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.
Files changed (139) hide show
  1. package/CHANGELOG.md +322 -0
  2. package/OPTIMIZATION-PLAN.md +19 -19
  3. package/dist/add/add-auth.js +1 -1
  4. package/dist/add/add-channel.d.ts +1 -1
  5. package/dist/add/add-cli.js +5 -0
  6. package/dist/add/add-functions.js +10 -1
  7. package/dist/add/add-http-route.js +18 -18
  8. package/dist/add/add-keyed-wiring.d.ts +2 -0
  9. package/dist/add/add-keyed-wiring.js +3 -1
  10. package/dist/add/add-personas.js +4 -1
  11. package/dist/add/add-secret.js +1 -0
  12. package/dist/add/add-variable.js +1 -0
  13. package/dist/add/add-wire-remote-addon.js +2 -1
  14. package/dist/add/add-workflow-graph.js +63 -6
  15. package/dist/add/add-workflow.d.ts +1 -1
  16. package/dist/error-codes.d.ts +3 -1
  17. package/dist/error-codes.js +3 -1
  18. package/dist/index.d.ts +4 -2
  19. package/dist/index.js +1 -0
  20. package/dist/inspector.js +12 -2
  21. package/dist/types.d.ts +25 -21
  22. package/dist/utils/annotate-http-route-auth.js +2 -1
  23. package/dist/utils/check-secret-sinks.js +2 -1
  24. package/dist/utils/compute-required-schemas.d.ts +1 -1
  25. package/dist/utils/contract-hashes.d.ts +2 -2
  26. package/dist/utils/contract-hashes.js +1 -1
  27. package/dist/utils/extract-function-name.js +1 -1
  28. package/dist/utils/extract-services.d.ts +1 -1
  29. package/dist/utils/extract-surface-usage.d.ts +29 -0
  30. package/dist/utils/extract-surface-usage.js +97 -0
  31. package/dist/utils/filter-inspector-state.js +53 -16
  32. package/dist/utils/filter-utils.d.ts +1 -1
  33. package/dist/utils/get-property-value.js +42 -3
  34. package/dist/utils/load-addon-functions-meta.js +31 -0
  35. package/dist/utils/middleware.d.ts +1 -1
  36. package/dist/utils/permissions.d.ts +1 -1
  37. package/dist/utils/post-process.d.ts +27 -7
  38. package/dist/utils/post-process.js +79 -14
  39. package/dist/utils/resolve-deploy-target.d.ts +1 -1
  40. package/dist/utils/resolve-function-meta.d.ts +1 -1
  41. package/dist/utils/resolve-function-types.d.ts +1 -1
  42. package/dist/utils/resolve-versions.js +1 -1
  43. package/dist/utils/schema-generator.d.ts +1 -1
  44. package/dist/utils/serialize-inspector-state.d.ts +2 -1
  45. package/dist/utils/serialize-inspector-state.js +2 -0
  46. package/dist/utils/serialize-openapi-json.d.ts +2 -2
  47. package/dist/utils/validate-auth-sessionless.js +2 -1
  48. package/dist/utils/workflow/derive-workflow-plan.d.ts +1 -1
  49. package/dist/utils/workflow/dsl/extract-dsl-workflow.d.ts +1 -1
  50. package/dist/utils/workflow/graph/convert-dsl-to-graph.d.ts +1 -1
  51. package/dist/utils/workflow/graph/finalize-workflows.js +1 -1
  52. package/dist/utils/workflow/graph/index.d.ts +2 -1
  53. package/dist/utils/workflow/graph/index.js +1 -1
  54. package/dist/utils/workflow/graph/serialize-workflow-graph.d.ts +6 -1
  55. package/dist/utils/workflow/graph/serialize-workflow-graph.js +26 -2
  56. package/dist/utils/workflow/graph/workflow-graph.types.d.ts +11 -2
  57. package/package.json +10 -10
  58. package/run-tests.sh +0 -0
  59. package/src/add/add-agent.ts +2 -4
  60. package/src/add/add-auth.ts +2 -2
  61. package/src/add/add-channel.ts +1 -4
  62. package/src/add/add-cli.ts +7 -1
  63. package/src/add/add-credential.ts +3 -6
  64. package/src/add/add-file-with-factory.ts +2 -6
  65. package/src/add/add-functions.ts +24 -31
  66. package/src/add/add-gateway.ts +1 -1
  67. package/src/add/add-http-route-ref.test.ts +149 -0
  68. package/src/add/add-http-route.ts +26 -29
  69. package/src/add/add-keyed-wiring.ts +8 -7
  70. package/src/add/add-permission.ts +1 -1
  71. package/src/add/add-personas.test.ts +26 -2
  72. package/src/add/add-personas.ts +4 -3
  73. package/src/add/add-scenario-step.test.ts +37 -2
  74. package/src/add/add-scope.test.ts +15 -15
  75. package/src/add/add-scope.ts +5 -9
  76. package/src/add/add-secret.ts +1 -0
  77. package/src/add/add-system-role.test.ts +4 -4
  78. package/src/add/add-system-role.ts +2 -4
  79. package/src/add/add-variable.ts +1 -0
  80. package/src/add/add-wire-remote-addon.ts +4 -1
  81. package/src/add/add-workflow-graph-foreach.test.ts +174 -0
  82. package/src/add/add-workflow-graph-input.test.ts +1 -1
  83. package/src/add/add-workflow-graph.ts +78 -6
  84. package/src/add/add-workflow-sleep-expression.test.ts +3 -4
  85. package/src/add/add-workflow.ts +1 -4
  86. package/src/add/addon-contracts.test.ts +1 -1
  87. package/src/add/config-value-metadata.test.ts +94 -0
  88. package/src/add/secret-sink-check.test.ts +4 -1
  89. package/src/error-codes.ts +3 -1
  90. package/src/index.ts +19 -5
  91. package/src/inspector.ts +19 -2
  92. package/src/types.ts +25 -28
  93. package/src/utils/annotate-http-route-auth.test.ts +5 -7
  94. package/src/utils/annotate-http-route-auth.ts +2 -1
  95. package/src/utils/check-secret-sinks.ts +4 -1
  96. package/src/utils/compute-required-schemas.ts +1 -1
  97. package/src/utils/contract-hashes.test.ts +1 -1
  98. package/src/utils/contract-hashes.ts +3 -3
  99. package/src/utils/extract-function-name.ts +1 -1
  100. package/src/utils/extract-services.ts +1 -2
  101. package/src/utils/extract-surface-usage.test.ts +147 -0
  102. package/src/utils/extract-surface-usage.ts +121 -0
  103. package/src/utils/filter-inspector-state.test.ts +138 -12
  104. package/src/utils/filter-inspector-state.ts +82 -21
  105. package/src/utils/filter-utils.ts +1 -1
  106. package/src/utils/get-property-value.ts +43 -5
  107. package/src/utils/load-addon-functions-meta.ts +40 -0
  108. package/src/utils/middleware.ts +1 -1
  109. package/src/utils/permissions.ts +1 -1
  110. package/src/utils/post-process.test.ts +59 -2
  111. package/src/utils/post-process.ts +88 -25
  112. package/src/utils/resolve-deploy-target.ts +1 -1
  113. package/src/utils/resolve-function-meta.ts +1 -4
  114. package/src/utils/resolve-function-types.ts +1 -1
  115. package/src/utils/resolve-versions.ts +2 -5
  116. package/src/utils/schema-generator.ts +3 -3
  117. package/src/utils/serialize-inspector-state.ts +4 -1
  118. package/src/utils/serialize-mcp-json.ts +1 -1
  119. package/src/utils/serialize-openapi-json.ts +2 -2
  120. package/src/utils/validate-auth-sessionless.ts +2 -1
  121. package/src/utils/validate-tags-resolve-to-middleware.test.ts +4 -1
  122. package/src/utils/workflow/derive-workflow-plan.test.ts +1 -1
  123. package/src/utils/workflow/derive-workflow-plan.ts +1 -1
  124. package/src/utils/workflow/dsl/extract-approval-policy.test.ts +1 -1
  125. package/src/utils/workflow/dsl/extract-dsl-workflow.ts +2 -2
  126. package/src/utils/workflow/graph/convert-dsl-to-graph-skip.test.ts +1 -1
  127. package/src/utils/workflow/graph/convert-dsl-to-graph.ts +1 -4
  128. package/src/utils/workflow/graph/finalize-workflow-wires.ts +1 -1
  129. package/src/utils/workflow/graph/finalize-workflows.ts +2 -2
  130. package/src/utils/workflow/graph/index.ts +18 -1
  131. package/src/utils/workflow/graph/serialize-workflow-graph.ts +34 -3
  132. package/src/utils/workflow/graph/workflow-graph.types.ts +12 -2
  133. package/tsconfig.tsbuildinfo +1 -1
  134. package/dist/add/add-ai-agent.d.ts +0 -4
  135. package/dist/add/add-ai-agent.js +0 -442
  136. package/dist/add/add-ai-scorer.d.ts +0 -2
  137. package/dist/add/add-ai-scorer.js +0 -61
  138. package/dist/utils/secret-brokers.d.ts +0 -2
  139. 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: 'http:get:/workflow-run/stream',
1709
- refTarget: 'console:streamWorkflowRun',
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
- { names: ['http:get:/workflow-run/stream'] },
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('drops the addon when the ref()-wired route is filtered out', () => {
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: 'http:get:/workflow-run/stream',
1736
- refTarget: 'console:streamWorkflowRun',
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
- s.functions.meta['http:get:/workflow-run/stream'] = {
1744
- services: { optimized: false, services: [] },
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/ecosystem/types'
7
- import { parseVersionedId } from '@pikku/core/ecosystem/types'
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 { resolveDeployTarget } from './resolve-deploy-target.js'
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
- // Set of pikkuFuncIds to keep based on the deploy filter; null when
109
- // the deploy filter is inactive.
110
- keptByDeploy?: Set<string> | null
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 { baseName } = parseVersionedId(meta.name)
212
+ const candidateNames = [meta.name, ...(meta.altNames ?? [])]
192
213
  const matchesNamePattern = (pattern: string) =>
193
- matchesWildcard(meta.name, pattern) ||
194
- (baseName !== meta.name && matchesWildcard(baseName, pattern))
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: Set<string> | null = null
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
- keptByDeploy = new Set<string>()
345
- for (const [funcId, funcMeta] of Object.entries(state.functions.meta)) {
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
- incompatible,
390
+ incompatibleServices,
349
391
  funcId,
350
392
  defaultTarget
351
393
  )
352
- if (allowed && !allowed.has(target)) continue
353
- if (excluded && excluded.has(target)) continue
354
- keptByDeploy.add(funcId)
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(':')
@@ -1,5 +1,5 @@
1
1
  import type { InspectorFilters, InspectorLogger } from '../types.js'
2
- import type { PikkuWiringTypes } from '@pikku/core/ecosystem/types'
2
+ import type { PikkuWiringTypes } from '@pikku/core/types'
3
3
 
4
4
  /**
5
5
  * Match a value against a pattern with wildcard support