@pikku/inspector 0.12.72 → 0.12.73
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 +7 -0
- package/dist/inspector.js +19 -3
- package/dist/source-file-cache.d.ts +11 -0
- package/dist/source-file-cache.js +54 -0
- package/dist/types.d.ts +26 -0
- package/dist/utils/filter-inspector-state.js +13 -0
- package/package.json +1 -1
- package/src/inspector.ts +19 -2
- package/src/source-file-cache.ts +73 -0
- package/src/types.ts +27 -0
- package/src/utils/filter-inspector-state.test.ts +63 -0
- package/src/utils/filter-inspector-state.ts +13 -0
- package/tsconfig.tsbuildinfo +1 -1
- package/dist/utils/check-secret-sinks.d.ts +0 -31
- package/dist/utils/check-secret-sinks.js +0 -118
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,10 @@
|
|
|
1
|
+
## 0.12.73
|
|
2
|
+
|
|
3
|
+
### Patch Changes
|
|
4
|
+
|
|
5
|
+
- 6519c66: Report what each inspector pass did, not only how long it took. `inspect()` now records per-pass work counters on the state (`stats`: files in the program, files reused from the previous program, type/instantiation/symbol counts, CPU and wall time, heap in use), and `pikku all` prints one `[INSPECT]` row per pass under `PIKKU_TIMING`. The codegen benchmark gates on those counts and on live heap per pass instead of on step timings, which swing with the runner, and its fixture now includes an agent so the post-agent re-inspection is actually exercised.
|
|
6
|
+
- 5447dba: Keep parsed source files across inspector passes. A re-inspection in `pikku all` used to re-parse and re-bind every file, because the previous `ts.Program` — the thing TypeScript reuses parses from — had to be dropped to free its type checker. The inspector now builds each program through a compiler host with a content-hashed `SourceFile` cache, so a re-inspection parses only what codegen just wrote while holding no checker: on the 500-function benchmark the re-inspection reuses all 908 files, its CPU time falls by a third, and peak heap drops.
|
|
7
|
+
|
|
1
8
|
## 0.12.72
|
|
2
9
|
|
|
3
10
|
### Patch Changes
|
package/dist/inspector.js
CHANGED
|
@@ -5,6 +5,7 @@ import { visitSetup, visitFunctions, visitRoutes } from './visit.js';
|
|
|
5
5
|
import { TypesMap } from './types-map.js';
|
|
6
6
|
import { getFilesAndMethods } from './utils/get-files-and-methods.js';
|
|
7
7
|
import { findCommonAncestor } from './utils/find-root-dir.js';
|
|
8
|
+
import { createSourceFileCacheHost } from './source-file-cache.js';
|
|
8
9
|
import { createNestedProjectFilter } from './utils/nested-project-filter.js';
|
|
9
10
|
import { aggregateRequiredServices, stampAuthHandlerServices, validateAgentModels, validateAgentToolReferences, validateAgentScorerReferences, validateSecretOverrides, validateVariableOverrides, validateCredentialOverrides, validateRemoteAddonDependencies, validateRemoteAddonAuth, validateScopeReferences, validateSystemRoleScopes, validatePersonaRoles, computeResolvedIOTypes, computeMiddlewareGroupsMeta, computePermissionsGroupsMeta, computeRequiredSchemas, validateNoSecretAliasServices, validateSecretUsage, computeDiagnostics, validateSchemaReferences, validateSchemaWiringSeparation, validateScenarioServices, validateScenarioSteps, validateWorkflowGraphAddons, } from './utils/post-process.js';
|
|
10
11
|
import { validateExposedFunctionsGated } from './utils/validate-exposed-functions-gated.js';
|
|
@@ -273,10 +274,13 @@ export const inspect = async (logger, routeFiles, options = {}) => {
|
|
|
273
274
|
allowJs: false,
|
|
274
275
|
checkJs: false,
|
|
275
276
|
};
|
|
277
|
+
const cpuStart = process.cpuUsage();
|
|
276
278
|
const startProgram = performance.now();
|
|
277
|
-
const
|
|
278
|
-
options.oldProgram);
|
|
279
|
-
|
|
279
|
+
const host = createSourceFileCacheHost(compilerOptions);
|
|
280
|
+
const program = ts.createProgram(normalizedRouteFiles, compilerOptions, host, options.oldProgram);
|
|
281
|
+
host.prune(program);
|
|
282
|
+
const filesReused = host.reused();
|
|
283
|
+
logger.debug(`Created program in ${(performance.now() - startProgram).toFixed(0)}ms (${normalizedRouteFiles.length} files, ${filesReused} source files reused)`);
|
|
280
284
|
const startChecker = performance.now();
|
|
281
285
|
const checker = program.getTypeChecker();
|
|
282
286
|
logger.debug(`Got type checker in ${(performance.now() - startChecker).toFixed(2)}ms`);
|
|
@@ -417,6 +421,18 @@ export const inspect = async (logger, routeFiles, options = {}) => {
|
|
|
417
421
|
validatePersonaRoles(logger, state);
|
|
418
422
|
logger.endValidationPass?.();
|
|
419
423
|
}
|
|
424
|
+
const cpu = process.cpuUsage(cpuStart);
|
|
425
|
+
state.stats = {
|
|
426
|
+
files: program.getSourceFiles().length,
|
|
427
|
+
projectFiles: sourceFiles.length,
|
|
428
|
+
filesReused,
|
|
429
|
+
types: program.getTypeCount(),
|
|
430
|
+
instantiations: program.getInstantiationCount(),
|
|
431
|
+
symbols: program.getSymbolCount(),
|
|
432
|
+
cpuMs: Math.round((cpu.user + cpu.system) / 1000),
|
|
433
|
+
wallMs: Math.round(performance.now() - startProgram),
|
|
434
|
+
heapUsedMb: Math.round(process.memoryUsage().heapUsed / 1048576),
|
|
435
|
+
};
|
|
420
436
|
state.program = program;
|
|
421
437
|
return state;
|
|
422
438
|
};
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import * as ts from 'typescript';
|
|
2
|
+
export interface SourceFileCacheHost extends ts.CompilerHost {
|
|
3
|
+
/** Files served from the cache by the program just built. */
|
|
4
|
+
readonly reused: () => number;
|
|
5
|
+
/**
|
|
6
|
+
* Drop entries the program did not use, so a file that left the project
|
|
7
|
+
* (or watch mode's churn) does not pin its AST forever.
|
|
8
|
+
*/
|
|
9
|
+
readonly prune: (program: ts.Program) => void;
|
|
10
|
+
}
|
|
11
|
+
export declare const createSourceFileCacheHost: (options: ts.CompilerOptions) => SourceFileCacheHost;
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import * as ts from 'typescript';
|
|
2
|
+
import { createHash } from 'crypto';
|
|
3
|
+
/**
|
|
4
|
+
* Parsed source files, kept across programs so a re-inspection only re-parses
|
|
5
|
+
* what changed. Keyed by path; an entry is valid while the file's content
|
|
6
|
+
* hash matches.
|
|
7
|
+
*
|
|
8
|
+
* This is the incremental reuse `oldProgram` used to give, without its cost.
|
|
9
|
+
* Passing the previous ts.Program keeps that program reachable, and a program
|
|
10
|
+
* memoises its type checker — so every re-inspection held a second, fully
|
|
11
|
+
* exercised checker for the sake of skipping a re-parse. A bare SourceFile
|
|
12
|
+
* carries its AST and binder symbols and nothing else; sharing it between
|
|
13
|
+
* programs is what the language service's document registry does.
|
|
14
|
+
*
|
|
15
|
+
* Module-level on purpose: `inspect()` is called several times per `pikku all`
|
|
16
|
+
* (and indefinitely in watch mode) from one process, and the cache is only
|
|
17
|
+
* worth having if it outlives the call.
|
|
18
|
+
*/
|
|
19
|
+
const cache = new Map();
|
|
20
|
+
export const createSourceFileCacheHost = (options) => {
|
|
21
|
+
const host = ts.createCompilerHost(options, true);
|
|
22
|
+
let reused = 0;
|
|
23
|
+
host.getSourceFile = (fileName, languageVersionOrOptions, onError) => {
|
|
24
|
+
let text;
|
|
25
|
+
try {
|
|
26
|
+
text = host.readFile(fileName);
|
|
27
|
+
}
|
|
28
|
+
catch (e) {
|
|
29
|
+
onError?.(e instanceof Error ? e.message : String(e));
|
|
30
|
+
return undefined;
|
|
31
|
+
}
|
|
32
|
+
if (text === undefined)
|
|
33
|
+
return undefined;
|
|
34
|
+
const hash = createHash('sha1').update(text).digest('hex');
|
|
35
|
+
const hit = cache.get(fileName);
|
|
36
|
+
if (hit && hit.hash === hash) {
|
|
37
|
+
reused++;
|
|
38
|
+
return hit.file;
|
|
39
|
+
}
|
|
40
|
+
const file = ts.createSourceFile(fileName, text, languageVersionOrOptions, true);
|
|
41
|
+
cache.set(fileName, { hash, file });
|
|
42
|
+
return file;
|
|
43
|
+
};
|
|
44
|
+
return Object.assign(host, {
|
|
45
|
+
reused: () => reused,
|
|
46
|
+
prune: (program) => {
|
|
47
|
+
const live = new Set(program.getSourceFiles().map((sf) => sf.fileName));
|
|
48
|
+
for (const fileName of cache.keys()) {
|
|
49
|
+
if (!live.has(fileName))
|
|
50
|
+
cache.delete(fileName);
|
|
51
|
+
}
|
|
52
|
+
},
|
|
53
|
+
});
|
|
54
|
+
};
|
package/dist/types.d.ts
CHANGED
|
@@ -223,6 +223,26 @@ export type AddonConfig = {
|
|
|
223
223
|
secretOverrides?: Record<string, string>;
|
|
224
224
|
forceInclude?: boolean;
|
|
225
225
|
};
|
|
226
|
+
export interface InspectorPassStats {
|
|
227
|
+
/** Source files in the ts.Program, node_modules and lib included. */
|
|
228
|
+
files: number;
|
|
229
|
+
/** Files under rootDir the inspector actually swept. */
|
|
230
|
+
projectFiles: number;
|
|
231
|
+
/**
|
|
232
|
+
* Files served from the source-file cache without re-parsing. Zero on a
|
|
233
|
+
* first pass; on a re-inspection it should be nearly all of `files` — that
|
|
234
|
+
* reuse is what keeps a re-inspection cheaper than a cold one.
|
|
235
|
+
*/
|
|
236
|
+
filesReused: number;
|
|
237
|
+
/** Checker work, as `tsc --extendedDiagnostics` counts it. */
|
|
238
|
+
types: number;
|
|
239
|
+
instantiations: number;
|
|
240
|
+
symbols: number;
|
|
241
|
+
cpuMs: number;
|
|
242
|
+
wallMs: number;
|
|
243
|
+
/** V8 heap in use when the pass finished, before any GC. */
|
|
244
|
+
heapUsedMb: number;
|
|
245
|
+
}
|
|
226
246
|
export type InspectorOptions = Partial<{
|
|
227
247
|
setupOnly: boolean;
|
|
228
248
|
rootDir: string;
|
|
@@ -456,6 +476,12 @@ export interface InspectorState {
|
|
|
456
476
|
filesAndMethods: InspectorFilesAndMethods;
|
|
457
477
|
filesAndMethodsErrors: Map<string, PathToNameAndType>;
|
|
458
478
|
typesLookup: Map<string, ts.Type[]>;
|
|
479
|
+
/**
|
|
480
|
+
* What this pass cost, in counts rather than time where it can be counted:
|
|
481
|
+
* counts don't move with the machine, so they are what a perf gate can
|
|
482
|
+
* compare across runners (see benchmarks/bench-codegen-perf.ts).
|
|
483
|
+
*/
|
|
484
|
+
stats?: InspectorPassStats;
|
|
459
485
|
schemaLookup: Map<string, SchemaRef>;
|
|
460
486
|
schemas: Record<string, JSONValue>;
|
|
461
487
|
http: InspectorHTTPState;
|
|
@@ -914,8 +914,21 @@ export function filterInspectorState(state, filters, logger) {
|
|
|
914
914
|
for (const tool of agentMeta.tools ?? [])
|
|
915
915
|
referencedIds.add(tool);
|
|
916
916
|
}
|
|
917
|
+
// A unit carrying the generic `/rpc/:rpcName` catch-all (the rpcCaller
|
|
918
|
+
// unit, and agent units) dispatches arbitrary RPCs at runtime via
|
|
919
|
+
// `rpc.exposed(name)`, including namespaced addon RPCs (e.g.
|
|
920
|
+
// `admin:createUser`). Those never appear in referencedIds because no
|
|
921
|
+
// app-local function statically references them, so keep every wired
|
|
922
|
+
// addon's declaration for such units — otherwise the per-unit bootstrap
|
|
923
|
+
// omits the addon package bootstrap and `rpc.exposed('admin:createUser')`
|
|
924
|
+
// throws RPCNotFoundError on the deployed stage.
|
|
925
|
+
const hasRpcCatchAll = (filters.names ?? []).includes('/rpc/:rpcName');
|
|
917
926
|
const keptNamespaces = new Set();
|
|
918
927
|
for (const namespace of filteredState.rpc.wireAddonDeclarations.keys()) {
|
|
928
|
+
if (hasRpcCatchAll) {
|
|
929
|
+
keptNamespaces.add(namespace);
|
|
930
|
+
continue;
|
|
931
|
+
}
|
|
919
932
|
const prefix = `${namespace}:`;
|
|
920
933
|
for (const id of referencedIds) {
|
|
921
934
|
if (id.startsWith(prefix)) {
|
package/package.json
CHANGED
package/src/inspector.ts
CHANGED
|
@@ -10,6 +10,7 @@ import type {
|
|
|
10
10
|
} from './types.js'
|
|
11
11
|
import { getFilesAndMethods } from './utils/get-files-and-methods.js'
|
|
12
12
|
import { findCommonAncestor } from './utils/find-root-dir.js'
|
|
13
|
+
import { createSourceFileCacheHost } from './source-file-cache.js'
|
|
13
14
|
import { createNestedProjectFilter } from './utils/nested-project-filter.js'
|
|
14
15
|
import {
|
|
15
16
|
aggregateRequiredServices,
|
|
@@ -335,15 +336,19 @@ export const inspect = async (
|
|
|
335
336
|
allowJs: false,
|
|
336
337
|
checkJs: false,
|
|
337
338
|
}
|
|
339
|
+
const cpuStart = process.cpuUsage()
|
|
338
340
|
const startProgram = performance.now()
|
|
341
|
+
const host = createSourceFileCacheHost(compilerOptions)
|
|
339
342
|
const program = ts.createProgram(
|
|
340
343
|
normalizedRouteFiles,
|
|
341
344
|
compilerOptions,
|
|
342
|
-
|
|
345
|
+
host,
|
|
343
346
|
options.oldProgram
|
|
344
347
|
)
|
|
348
|
+
host.prune(program)
|
|
349
|
+
const filesReused = host.reused()
|
|
345
350
|
logger.debug(
|
|
346
|
-
`Created program in ${(performance.now() - startProgram).toFixed(0)}ms (${normalizedRouteFiles.length} files${
|
|
351
|
+
`Created program in ${(performance.now() - startProgram).toFixed(0)}ms (${normalizedRouteFiles.length} files, ${filesReused} source files reused)`
|
|
347
352
|
)
|
|
348
353
|
|
|
349
354
|
const startChecker = performance.now()
|
|
@@ -552,6 +557,18 @@ export const inspect = async (
|
|
|
552
557
|
logger.endValidationPass?.()
|
|
553
558
|
}
|
|
554
559
|
|
|
560
|
+
const cpu = process.cpuUsage(cpuStart)
|
|
561
|
+
state.stats = {
|
|
562
|
+
files: program.getSourceFiles().length,
|
|
563
|
+
projectFiles: sourceFiles.length,
|
|
564
|
+
filesReused,
|
|
565
|
+
types: program.getTypeCount(),
|
|
566
|
+
instantiations: program.getInstantiationCount(),
|
|
567
|
+
symbols: program.getSymbolCount(),
|
|
568
|
+
cpuMs: Math.round((cpu.user + cpu.system) / 1000),
|
|
569
|
+
wallMs: Math.round(performance.now() - startProgram),
|
|
570
|
+
heapUsedMb: Math.round(process.memoryUsage().heapUsed / 1048576),
|
|
571
|
+
}
|
|
555
572
|
state.program = program
|
|
556
573
|
|
|
557
574
|
return state
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import * as ts from 'typescript'
|
|
2
|
+
import { createHash } from 'crypto'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Parsed source files, kept across programs so a re-inspection only re-parses
|
|
6
|
+
* what changed. Keyed by path; an entry is valid while the file's content
|
|
7
|
+
* hash matches.
|
|
8
|
+
*
|
|
9
|
+
* This is the incremental reuse `oldProgram` used to give, without its cost.
|
|
10
|
+
* Passing the previous ts.Program keeps that program reachable, and a program
|
|
11
|
+
* memoises its type checker — so every re-inspection held a second, fully
|
|
12
|
+
* exercised checker for the sake of skipping a re-parse. A bare SourceFile
|
|
13
|
+
* carries its AST and binder symbols and nothing else; sharing it between
|
|
14
|
+
* programs is what the language service's document registry does.
|
|
15
|
+
*
|
|
16
|
+
* Module-level on purpose: `inspect()` is called several times per `pikku all`
|
|
17
|
+
* (and indefinitely in watch mode) from one process, and the cache is only
|
|
18
|
+
* worth having if it outlives the call.
|
|
19
|
+
*/
|
|
20
|
+
const cache = new Map<string, { hash: string; file: ts.SourceFile }>()
|
|
21
|
+
|
|
22
|
+
export interface SourceFileCacheHost extends ts.CompilerHost {
|
|
23
|
+
/** Files served from the cache by the program just built. */
|
|
24
|
+
readonly reused: () => number
|
|
25
|
+
/**
|
|
26
|
+
* Drop entries the program did not use, so a file that left the project
|
|
27
|
+
* (or watch mode's churn) does not pin its AST forever.
|
|
28
|
+
*/
|
|
29
|
+
readonly prune: (program: ts.Program) => void
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export const createSourceFileCacheHost = (
|
|
33
|
+
options: ts.CompilerOptions
|
|
34
|
+
): SourceFileCacheHost => {
|
|
35
|
+
const host = ts.createCompilerHost(options, true)
|
|
36
|
+
let reused = 0
|
|
37
|
+
|
|
38
|
+
host.getSourceFile = (fileName, languageVersionOrOptions, onError) => {
|
|
39
|
+
let text: string | undefined
|
|
40
|
+
try {
|
|
41
|
+
text = host.readFile(fileName)
|
|
42
|
+
} catch (e) {
|
|
43
|
+
onError?.(e instanceof Error ? e.message : String(e))
|
|
44
|
+
return undefined
|
|
45
|
+
}
|
|
46
|
+
if (text === undefined) return undefined
|
|
47
|
+
|
|
48
|
+
const hash = createHash('sha1').update(text).digest('hex')
|
|
49
|
+
const hit = cache.get(fileName)
|
|
50
|
+
if (hit && hit.hash === hash) {
|
|
51
|
+
reused++
|
|
52
|
+
return hit.file
|
|
53
|
+
}
|
|
54
|
+
const file = ts.createSourceFile(
|
|
55
|
+
fileName,
|
|
56
|
+
text,
|
|
57
|
+
languageVersionOrOptions,
|
|
58
|
+
true
|
|
59
|
+
)
|
|
60
|
+
cache.set(fileName, { hash, file })
|
|
61
|
+
return file
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
return Object.assign(host, {
|
|
65
|
+
reused: () => reused,
|
|
66
|
+
prune: (program: ts.Program) => {
|
|
67
|
+
const live = new Set(program.getSourceFiles().map((sf) => sf.fileName))
|
|
68
|
+
for (const fileName of cache.keys()) {
|
|
69
|
+
if (!live.has(fileName)) cache.delete(fileName)
|
|
70
|
+
}
|
|
71
|
+
},
|
|
72
|
+
})
|
|
73
|
+
}
|
package/src/types.ts
CHANGED
|
@@ -290,6 +290,27 @@ export type AddonConfig = {
|
|
|
290
290
|
forceInclude?: boolean
|
|
291
291
|
}
|
|
292
292
|
|
|
293
|
+
export interface InspectorPassStats {
|
|
294
|
+
/** Source files in the ts.Program, node_modules and lib included. */
|
|
295
|
+
files: number
|
|
296
|
+
/** Files under rootDir the inspector actually swept. */
|
|
297
|
+
projectFiles: number
|
|
298
|
+
/**
|
|
299
|
+
* Files served from the source-file cache without re-parsing. Zero on a
|
|
300
|
+
* first pass; on a re-inspection it should be nearly all of `files` — that
|
|
301
|
+
* reuse is what keeps a re-inspection cheaper than a cold one.
|
|
302
|
+
*/
|
|
303
|
+
filesReused: number
|
|
304
|
+
/** Checker work, as `tsc --extendedDiagnostics` counts it. */
|
|
305
|
+
types: number
|
|
306
|
+
instantiations: number
|
|
307
|
+
symbols: number
|
|
308
|
+
cpuMs: number
|
|
309
|
+
wallMs: number
|
|
310
|
+
/** V8 heap in use when the pass finished, before any GC. */
|
|
311
|
+
heapUsedMb: number
|
|
312
|
+
}
|
|
313
|
+
|
|
293
314
|
export type InspectorOptions = Partial<{
|
|
294
315
|
setupOnly: boolean
|
|
295
316
|
rootDir: string
|
|
@@ -534,6 +555,12 @@ export interface InspectorState {
|
|
|
534
555
|
filesAndMethods: InspectorFilesAndMethods
|
|
535
556
|
filesAndMethodsErrors: Map<string, PathToNameAndType>
|
|
536
557
|
typesLookup: Map<string, ts.Type[]> // Lookup for types by name (e.g., function input types, Config type)
|
|
558
|
+
/**
|
|
559
|
+
* What this pass cost, in counts rather than time where it can be counted:
|
|
560
|
+
* counts don't move with the machine, so they are what a perf gate can
|
|
561
|
+
* compare across runners (see benchmarks/bench-codegen-perf.ts).
|
|
562
|
+
*/
|
|
563
|
+
stats?: InspectorPassStats
|
|
537
564
|
schemaLookup: Map<string, SchemaRef> // Lookup for schemas by name for deferred JSON Schema conversion (supports Standard Schema vendors)
|
|
538
565
|
schemas: Record<string, JSONValue>
|
|
539
566
|
http: InspectorHTTPState
|
|
@@ -2141,4 +2141,67 @@ describe('invokedAgentsByFile', () => {
|
|
|
2141
2141
|
['/test/project/src/api/users.ts']
|
|
2142
2142
|
)
|
|
2143
2143
|
})
|
|
2144
|
+
|
|
2145
|
+
describe('Wired addon retention', () => {
|
|
2146
|
+
// A unit that owns the generic `/rpc/:rpcName` catch-all (the rpcCaller
|
|
2147
|
+
// unit, and agent units) dispatches arbitrary RPCs at runtime via
|
|
2148
|
+
// `rpc.exposed(name)`, including namespaced addon RPCs like
|
|
2149
|
+
// `admin:createUser`. No app-local function statically references those
|
|
2150
|
+
// ids, so the catch-all must be enough to keep every wired addon's
|
|
2151
|
+
// declaration — otherwise the per-unit bootstrap omits the addon package
|
|
2152
|
+
// bootstrap and `rpc.exposed('admin:createUser')` throws RPCNotFoundError
|
|
2153
|
+
// on a deployed stage.
|
|
2154
|
+
const withAddons = () => {
|
|
2155
|
+
const state = createMockInspectorState()
|
|
2156
|
+
state.rpc.wireAddonDeclarations = new Map([
|
|
2157
|
+
['admin', { package: '@pikku/addon-admin' } as any],
|
|
2158
|
+
['console', { package: '@pikku/addon-console' } as any],
|
|
2159
|
+
])
|
|
2160
|
+
state.rpc.usedAddons = new Set(['admin', 'console'])
|
|
2161
|
+
return state
|
|
2162
|
+
}
|
|
2163
|
+
|
|
2164
|
+
test('keeps all wired addons for a unit with the /rpc/:rpcName catch-all', () => {
|
|
2165
|
+
const result = filterInspectorState(
|
|
2166
|
+
withAddons(),
|
|
2167
|
+
{ names: ['/rpc/:rpcName', 'http:post:/rpc/:rpcName', 'rpcCaller'] },
|
|
2168
|
+
mockLogger
|
|
2169
|
+
)
|
|
2170
|
+
assert.deepStrictEqual(
|
|
2171
|
+
[...result.rpc.wireAddonDeclarations.keys()].sort(),
|
|
2172
|
+
['admin', 'console']
|
|
2173
|
+
)
|
|
2174
|
+
assert.deepStrictEqual([...result.rpc.usedAddons].sort(), [
|
|
2175
|
+
'admin',
|
|
2176
|
+
'console',
|
|
2177
|
+
])
|
|
2178
|
+
})
|
|
2179
|
+
|
|
2180
|
+
test('drops wired addons for a unit without the catch-all that references no namespace', () => {
|
|
2181
|
+
const result = filterInspectorState(
|
|
2182
|
+
withAddons(),
|
|
2183
|
+
{ names: ['getUsers'] },
|
|
2184
|
+
mockLogger
|
|
2185
|
+
)
|
|
2186
|
+
assert.deepStrictEqual([...result.rpc.wireAddonDeclarations.keys()], [])
|
|
2187
|
+
assert.deepStrictEqual([...result.rpc.usedAddons], [])
|
|
2188
|
+
})
|
|
2189
|
+
|
|
2190
|
+
test('still keeps an addon a surviving function statically references, without the catch-all', () => {
|
|
2191
|
+
const state = withAddons()
|
|
2192
|
+
state.serviceAggregation.usedFunctions.add('getUsers')
|
|
2193
|
+
state.rpc.invokedFunctionsByFile = new Map([
|
|
2194
|
+
['/test/project/src/api/users.ts', new Set(['admin:createUser'])],
|
|
2195
|
+
])
|
|
2196
|
+
const result = filterInspectorState(
|
|
2197
|
+
state,
|
|
2198
|
+
{ names: ['getUsers'] },
|
|
2199
|
+
mockLogger
|
|
2200
|
+
)
|
|
2201
|
+
assert.deepStrictEqual(
|
|
2202
|
+
[...result.rpc.wireAddonDeclarations.keys()],
|
|
2203
|
+
['admin']
|
|
2204
|
+
)
|
|
2205
|
+
})
|
|
2206
|
+
})
|
|
2144
2207
|
})
|
|
@@ -1258,8 +1258,21 @@ export function filterInspectorState(
|
|
|
1258
1258
|
) as Array<{ tools?: string[] }>) {
|
|
1259
1259
|
for (const tool of agentMeta.tools ?? []) referencedIds.add(tool)
|
|
1260
1260
|
}
|
|
1261
|
+
// A unit carrying the generic `/rpc/:rpcName` catch-all (the rpcCaller
|
|
1262
|
+
// unit, and agent units) dispatches arbitrary RPCs at runtime via
|
|
1263
|
+
// `rpc.exposed(name)`, including namespaced addon RPCs (e.g.
|
|
1264
|
+
// `admin:createUser`). Those never appear in referencedIds because no
|
|
1265
|
+
// app-local function statically references them, so keep every wired
|
|
1266
|
+
// addon's declaration for such units — otherwise the per-unit bootstrap
|
|
1267
|
+
// omits the addon package bootstrap and `rpc.exposed('admin:createUser')`
|
|
1268
|
+
// throws RPCNotFoundError on the deployed stage.
|
|
1269
|
+
const hasRpcCatchAll = (filters.names ?? []).includes('/rpc/:rpcName')
|
|
1261
1270
|
const keptNamespaces = new Set<string>()
|
|
1262
1271
|
for (const namespace of filteredState.rpc.wireAddonDeclarations.keys()) {
|
|
1272
|
+
if (hasRpcCatchAll) {
|
|
1273
|
+
keptNamespaces.add(namespace)
|
|
1274
|
+
continue
|
|
1275
|
+
}
|
|
1263
1276
|
const prefix = `${namespace}:`
|
|
1264
1277
|
for (const id of referencedIds) {
|
|
1265
1278
|
if (id.startsWith(prefix)) {
|