@pikku/inspector 0.12.64 → 0.12.65
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 +24 -0
- package/dist/add/add-cli.js +3 -11
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/utils/post-process.js +10 -11
- package/dist/utils/resolve-core-type.d.ts +21 -0
- package/dist/utils/resolve-core-type.js +29 -0
- package/package.json +3 -3
- package/src/add/add-cli.ts +6 -12
- package/src/index.ts +1 -0
- package/src/utils/post-process.test.ts +122 -0
- package/src/utils/post-process.ts +21 -11
- package/src/utils/resolve-core-type.test.ts +142 -0
- package/src/utils/resolve-core-type.ts +35 -0
- package/tsconfig.tsbuildinfo +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,27 @@
|
|
|
1
|
+
## 0.12.65
|
|
2
|
+
|
|
3
|
+
### Patch Changes
|
|
4
|
+
|
|
5
|
+
- b5e79c1: Resolve `Config`, `SingletonServices` and `Services` by declaration rather than
|
|
6
|
+
by name.
|
|
7
|
+
|
|
8
|
+
Service extraction read `typesLookup` under the hardcoded names the scaffold
|
|
9
|
+
happens to use, but the lookup is keyed by whatever the project named its
|
|
10
|
+
interface. A project that renamed one satisfied every required-type check and
|
|
11
|
+
then resolved to no services at all, surfacing much later as PKU724 or as every
|
|
12
|
+
singleton service turning optional. These now go through the import maps, which
|
|
13
|
+
carry the real name.
|
|
14
|
+
|
|
15
|
+
`pikku workflow` carried the same lookup as its fallback when aggregation came
|
|
16
|
+
back empty, so a project with a renamed interface was told
|
|
17
|
+
`WORKFLOW_ORCHESTRATOR_NOT_CONFIGURED` while holding a perfectly good
|
|
18
|
+
`workflowService`.
|
|
19
|
+
|
|
20
|
+
- Updated dependencies [1cc50ef]
|
|
21
|
+
- Updated dependencies [a3deea4]
|
|
22
|
+
- Updated dependencies [2a02288]
|
|
23
|
+
- @pikku/core@0.12.95
|
|
24
|
+
|
|
1
25
|
## 0.12.64
|
|
2
26
|
|
|
3
27
|
### Patch Changes
|
package/dist/add/add-cli.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import ts from 'typescript';
|
|
2
2
|
import { extractFunctionName, makeContextBasedId, } from '../utils/extract-function-name.js';
|
|
3
|
+
import { resolveCoreType } from '../utils/resolve-core-type.js';
|
|
3
4
|
import { resolveMiddleware } from '../utils/middleware.js';
|
|
4
5
|
import { resolveFunctionMeta } from '../utils/resolve-function-meta.js';
|
|
5
6
|
import { extractWireNames } from '../utils/post-process.js';
|
|
@@ -538,8 +539,8 @@ function extractEnumFromPropertyType(type, propertyName, typeChecker) {
|
|
|
538
539
|
*/
|
|
539
540
|
function extractEnumFromConfigType(logger, propertyName, typeChecker, inspectorState, _inspectorOptions) {
|
|
540
541
|
// Look for Config type in typesLookup
|
|
541
|
-
const
|
|
542
|
-
if (!
|
|
542
|
+
const configType = resolveCoreType(inspectorState.typesLookup, inspectorState.configTypeImportMap);
|
|
543
|
+
if (!configType) {
|
|
543
544
|
// Only warn once per CLI file to avoid spamming logs
|
|
544
545
|
if (!configTypeWarningShown.has('missing-config-type')) {
|
|
545
546
|
configTypeWarningShown.add('missing-config-type');
|
|
@@ -548,15 +549,6 @@ function extractEnumFromConfigType(logger, propertyName, typeChecker, inspectorS
|
|
|
548
549
|
}
|
|
549
550
|
return null;
|
|
550
551
|
}
|
|
551
|
-
// Use the first Config type (there should only be one)
|
|
552
|
-
const configType = configTypes[0];
|
|
553
|
-
if (!configType) {
|
|
554
|
-
if (!configTypeWarningShown.has('undefined-config-type')) {
|
|
555
|
-
configTypeWarningShown.add('undefined-config-type');
|
|
556
|
-
logger.warn(`Config type is undefined in typesLookup.`);
|
|
557
|
-
}
|
|
558
|
-
return null;
|
|
559
|
-
}
|
|
560
552
|
// Extract enum from the property
|
|
561
553
|
return extractEnumFromPropertyType(configType, propertyName, typeChecker);
|
|
562
554
|
}
|
package/dist/index.d.ts
CHANGED
|
@@ -23,3 +23,4 @@ export { deserializeDslWorkflow, deserializeGraphWorkflow, deserializeAllDslWork
|
|
|
23
23
|
export { getFilesAndMethods } from './utils/get-files-and-methods.js';
|
|
24
24
|
export { resolveFunctionMeta } from './utils/resolve-function-meta.js';
|
|
25
25
|
export type { SerializedWorkflowGraph, SerializedWorkflowGraphs, } from './utils/workflow/graph/index.js';
|
|
26
|
+
export { resolveCoreType } from './utils/resolve-core-type.js';
|
package/dist/index.js
CHANGED
|
@@ -12,3 +12,4 @@ export { serializeMCPJson } from './utils/serialize-mcp-json.js';
|
|
|
12
12
|
export { deserializeDslWorkflow, deserializeGraphWorkflow, deserializeAllDslWorkflows, } from './utils/workflow/dsl/index.js';
|
|
13
13
|
export { getFilesAndMethods } from './utils/get-files-and-methods.js';
|
|
14
14
|
export { resolveFunctionMeta } from './utils/resolve-function-meta.js';
|
|
15
|
+
export { resolveCoreType } from './utils/resolve-core-type.js';
|
|
@@ -4,6 +4,7 @@ import { join } from 'node:path';
|
|
|
4
4
|
import { extractTypeKeys } from './type-utils.js';
|
|
5
5
|
import { ErrorCode } from '../error-codes.js';
|
|
6
6
|
import { findSecretAliasServices } from './secret-alias-services.js';
|
|
7
|
+
import { resolveCoreType } from './resolve-core-type.js';
|
|
7
8
|
import { relative } from 'node:path';
|
|
8
9
|
import { AUTH_HANDLER_FUNC_ID } from '../add/add-auth.js';
|
|
9
10
|
import { flattenScopeDefinitions } from '@pikku/core/scope';
|
|
@@ -79,18 +80,16 @@ function extractAllServices(state) {
|
|
|
79
80
|
return;
|
|
80
81
|
}
|
|
81
82
|
// Extract all singleton services from the SingletonServices type
|
|
82
|
-
const
|
|
83
|
-
if (
|
|
84
|
-
|
|
85
|
-
state.serviceAggregation.allSingletonServices = singletonServiceNames.sort();
|
|
83
|
+
const singletonServicesType = resolveCoreType(state.typesLookup, state.singletonServicesTypeImportMap);
|
|
84
|
+
if (singletonServicesType) {
|
|
85
|
+
state.serviceAggregation.allSingletonServices = extractTypeKeys(singletonServicesType).sort();
|
|
86
86
|
}
|
|
87
87
|
// Extract all services from the Services type
|
|
88
|
-
const
|
|
89
|
-
if (
|
|
90
|
-
const allServiceNames = extractTypeKeys(servicesTypes[0]);
|
|
88
|
+
const servicesType = resolveCoreType(state.typesLookup, state.wireServicesTypeImportMap);
|
|
89
|
+
if (servicesType) {
|
|
91
90
|
// Wire services are those in Services but not in SingletonServices
|
|
92
91
|
const singletonSet = new Set(state.serviceAggregation.allSingletonServices);
|
|
93
|
-
state.serviceAggregation.allWireServices =
|
|
92
|
+
state.serviceAggregation.allWireServices = extractTypeKeys(servicesType)
|
|
94
93
|
.filter((name) => !singletonSet.has(name))
|
|
95
94
|
.sort();
|
|
96
95
|
}
|
|
@@ -898,11 +897,11 @@ export function validateNoSecretAliasServices(logger, checker, state) {
|
|
|
898
897
|
if (!('typesLookup' in state)) {
|
|
899
898
|
return;
|
|
900
899
|
}
|
|
901
|
-
const
|
|
902
|
-
if (!
|
|
900
|
+
const singletonServicesType = resolveCoreType(state.typesLookup, state.singletonServicesTypeImportMap);
|
|
901
|
+
if (!singletonServicesType) {
|
|
903
902
|
return;
|
|
904
903
|
}
|
|
905
|
-
const aliases = findSecretAliasServices(
|
|
904
|
+
const aliases = findSecretAliasServices(singletonServicesType, checker);
|
|
906
905
|
if (aliases.length === 0) {
|
|
907
906
|
return;
|
|
908
907
|
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type ts from 'typescript';
|
|
2
|
+
import type { PathToNameAndType } from '../types.js';
|
|
3
|
+
/**
|
|
4
|
+
* Resolves the one interface extending a core type (`CoreConfig`,
|
|
5
|
+
* `CoreSingletonServices`, `CoreServices`) to the `ts.Type` the AST walk
|
|
6
|
+
* recorded for it.
|
|
7
|
+
*
|
|
8
|
+
* `typesLookup` is keyed by whatever the project named the interface, so
|
|
9
|
+
* reading it under a hardcoded `'SingletonServices'` only works for projects
|
|
10
|
+
* that follow the scaffold's naming. A project that renamed the interface still
|
|
11
|
+
* satisfies every required-type check and then resolves to no services at all,
|
|
12
|
+
* which surfaces far downstream as PKU724 or as every service turning optional.
|
|
13
|
+
* The import map carries the real name, so no name matching is needed.
|
|
14
|
+
*
|
|
15
|
+
* A project is only ever meant to declare one of each: `getMetaTypes` records
|
|
16
|
+
* `More than one ... found` when it sees a second, which `checkRequiredTypes`
|
|
17
|
+
* then reports. That check is the caller's to run, and extraction happens
|
|
18
|
+
* first, so where two declarations do exist this takes the first in traversal
|
|
19
|
+
* order rather than diagnosing the ambiguity itself.
|
|
20
|
+
*/
|
|
21
|
+
export declare const resolveCoreType: (typesLookup: Map<string, ts.Type[]>, importMap: PathToNameAndType) => ts.Type | undefined;
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Resolves the one interface extending a core type (`CoreConfig`,
|
|
3
|
+
* `CoreSingletonServices`, `CoreServices`) to the `ts.Type` the AST walk
|
|
4
|
+
* recorded for it.
|
|
5
|
+
*
|
|
6
|
+
* `typesLookup` is keyed by whatever the project named the interface, so
|
|
7
|
+
* reading it under a hardcoded `'SingletonServices'` only works for projects
|
|
8
|
+
* that follow the scaffold's naming. A project that renamed the interface still
|
|
9
|
+
* satisfies every required-type check and then resolves to no services at all,
|
|
10
|
+
* which surfaces far downstream as PKU724 or as every service turning optional.
|
|
11
|
+
* The import map carries the real name, so no name matching is needed.
|
|
12
|
+
*
|
|
13
|
+
* A project is only ever meant to declare one of each: `getMetaTypes` records
|
|
14
|
+
* `More than one ... found` when it sees a second, which `checkRequiredTypes`
|
|
15
|
+
* then reports. That check is the caller's to run, and extraction happens
|
|
16
|
+
* first, so where two declarations do exist this takes the first in traversal
|
|
17
|
+
* order rather than diagnosing the ambiguity itself.
|
|
18
|
+
*/
|
|
19
|
+
export const resolveCoreType = (typesLookup, importMap) => {
|
|
20
|
+
for (const entries of importMap.values()) {
|
|
21
|
+
for (const { type } of entries) {
|
|
22
|
+
const resolved = type ? typesLookup.get(type)?.[0] : undefined;
|
|
23
|
+
if (resolved) {
|
|
24
|
+
return resolved;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
return undefined;
|
|
29
|
+
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pikku/inspector",
|
|
3
|
-
"version": "0.12.
|
|
3
|
+
"version": "0.12.65",
|
|
4
4
|
"description": "TypeScript inspector for Pikku — discovers functions and wirings to drive code generation",
|
|
5
5
|
"author": "yasser.fadl@gmail.com",
|
|
6
6
|
"license": "BUSL-1.1",
|
|
@@ -45,10 +45,10 @@
|
|
|
45
45
|
"zod-to-ts": "^2.1.0"
|
|
46
46
|
},
|
|
47
47
|
"peerDependencies": {
|
|
48
|
-
"@pikku/core": "^0.12.
|
|
48
|
+
"@pikku/core": "^0.12.95"
|
|
49
49
|
},
|
|
50
50
|
"devDependencies": {
|
|
51
|
-
"@pikku/core": "^0.12.
|
|
51
|
+
"@pikku/core": "^0.12.95",
|
|
52
52
|
"@types/node": "^24.13.3"
|
|
53
53
|
},
|
|
54
54
|
"engines": {
|
package/src/add/add-cli.ts
CHANGED
|
@@ -11,6 +11,7 @@ import {
|
|
|
11
11
|
extractFunctionName,
|
|
12
12
|
makeContextBasedId,
|
|
13
13
|
} from '../utils/extract-function-name.js'
|
|
14
|
+
import { resolveCoreType } from '../utils/resolve-core-type.js'
|
|
14
15
|
import { resolveMiddleware } from '../utils/middleware.js'
|
|
15
16
|
import { resolveFunctionMeta } from '../utils/resolve-function-meta.js'
|
|
16
17
|
import { extractWireNames } from '../utils/post-process.js'
|
|
@@ -825,8 +826,11 @@ function extractEnumFromConfigType(
|
|
|
825
826
|
_inspectorOptions: InspectorOptions
|
|
826
827
|
): string[] | null {
|
|
827
828
|
// Look for Config type in typesLookup
|
|
828
|
-
const
|
|
829
|
-
|
|
829
|
+
const configType = resolveCoreType(
|
|
830
|
+
inspectorState.typesLookup,
|
|
831
|
+
inspectorState.configTypeImportMap
|
|
832
|
+
)
|
|
833
|
+
if (!configType) {
|
|
830
834
|
// Only warn once per CLI file to avoid spamming logs
|
|
831
835
|
if (!configTypeWarningShown.has('missing-config-type')) {
|
|
832
836
|
configTypeWarningShown.add('missing-config-type')
|
|
@@ -838,16 +842,6 @@ function extractEnumFromConfigType(
|
|
|
838
842
|
return null
|
|
839
843
|
}
|
|
840
844
|
|
|
841
|
-
// Use the first Config type (there should only be one)
|
|
842
|
-
const configType = configTypes[0]
|
|
843
|
-
if (!configType) {
|
|
844
|
-
if (!configTypeWarningShown.has('undefined-config-type')) {
|
|
845
|
-
configTypeWarningShown.add('undefined-config-type')
|
|
846
|
-
logger.warn(`Config type is undefined in typesLookup.`)
|
|
847
|
-
}
|
|
848
|
-
return null
|
|
849
|
-
}
|
|
850
|
-
|
|
851
845
|
// Extract enum from the property
|
|
852
846
|
return extractEnumFromPropertyType(configType, propertyName, typeChecker)
|
|
853
847
|
}
|
package/src/index.ts
CHANGED
|
@@ -12,9 +12,11 @@ import {
|
|
|
12
12
|
validateRemoteAddonAuth,
|
|
13
13
|
validateAgentToolReferences,
|
|
14
14
|
validateAgentModels,
|
|
15
|
+
validateSchemaReferences,
|
|
15
16
|
} from './post-process.js'
|
|
16
17
|
import { ErrorCode } from '../error-codes.js'
|
|
17
18
|
import type { InspectorState, InspectorLogger } from '../types.js'
|
|
19
|
+
import { TypesMap } from '../types-map.js'
|
|
18
20
|
|
|
19
21
|
const makeCriticalLogger = () => {
|
|
20
22
|
const criticals: { code: string; message: string }[] = []
|
|
@@ -928,3 +930,123 @@ describe('validateAgentModels (aliases resolve against pikku.config.json)', () =
|
|
|
928
930
|
})
|
|
929
931
|
}
|
|
930
932
|
})
|
|
933
|
+
|
|
934
|
+
const makeDiagnosticLogger = () => {
|
|
935
|
+
const diagnostics: { code: string; severity: string; message: string }[] = []
|
|
936
|
+
const logger = {
|
|
937
|
+
debug: () => {},
|
|
938
|
+
info: () => {},
|
|
939
|
+
warn: () => {},
|
|
940
|
+
error: () => {},
|
|
941
|
+
critical: () => {},
|
|
942
|
+
diagnostic: (d: any) =>
|
|
943
|
+
diagnostics.push({
|
|
944
|
+
code: String(d.code),
|
|
945
|
+
severity: d.severity,
|
|
946
|
+
message: d.message,
|
|
947
|
+
}),
|
|
948
|
+
} as unknown as InspectorLogger
|
|
949
|
+
return { logger, diagnostics }
|
|
950
|
+
}
|
|
951
|
+
|
|
952
|
+
const makeSchemaRefState = (
|
|
953
|
+
meta: Record<string, { inputs?: string[]; outputs?: string[] }>,
|
|
954
|
+
schemas: string[],
|
|
955
|
+
types: Record<string, string> = {}
|
|
956
|
+
): InspectorState => {
|
|
957
|
+
const typesMap = new TypesMap()
|
|
958
|
+
for (const [name, path] of Object.entries(types)) {
|
|
959
|
+
typesMap.addType(name, path)
|
|
960
|
+
}
|
|
961
|
+
return {
|
|
962
|
+
functions: { meta, typesMap },
|
|
963
|
+
schemas: Object.fromEntries(schemas.map((name) => [name, {}])),
|
|
964
|
+
} as unknown as InspectorState
|
|
965
|
+
}
|
|
966
|
+
|
|
967
|
+
describe('validateSchemaReferences', () => {
|
|
968
|
+
test('an output type that was never exported is reported, not silent', () => {
|
|
969
|
+
const { logger, diagnostics } = makeDiagnosticLogger()
|
|
970
|
+
validateSchemaReferences(
|
|
971
|
+
logger,
|
|
972
|
+
makeSchemaRefState(
|
|
973
|
+
{ getStructure: { outputs: ['StructureSummary'] } },
|
|
974
|
+
[]
|
|
975
|
+
)
|
|
976
|
+
)
|
|
977
|
+
assert.equal(diagnostics.length, 1)
|
|
978
|
+
assert.equal(diagnostics[0]!.code, ErrorCode.SCHEMA_REFERENCE_UNRESOLVED)
|
|
979
|
+
assert.equal(diagnostics[0]!.severity, 'error')
|
|
980
|
+
assert.match(
|
|
981
|
+
diagnostics[0]!.message,
|
|
982
|
+
/getStructure\.output → 'StructureSummary'/
|
|
983
|
+
)
|
|
984
|
+
assert.match(diagnostics[0]!.message, /declared but not exported/)
|
|
985
|
+
})
|
|
986
|
+
|
|
987
|
+
test('an unresolved input is reported the same way', () => {
|
|
988
|
+
const { logger, diagnostics } = makeDiagnosticLogger()
|
|
989
|
+
validateSchemaReferences(
|
|
990
|
+
logger,
|
|
991
|
+
makeSchemaRefState({ getStructure: { inputs: ['StructureQuery'] } }, [])
|
|
992
|
+
)
|
|
993
|
+
assert.equal(diagnostics.length, 1)
|
|
994
|
+
assert.match(
|
|
995
|
+
diagnostics[0]!.message,
|
|
996
|
+
/getStructure\.input → 'StructureQuery'/
|
|
997
|
+
)
|
|
998
|
+
})
|
|
999
|
+
|
|
1000
|
+
test('a generated schema resolves and says nothing', () => {
|
|
1001
|
+
const { logger, diagnostics } = makeDiagnosticLogger()
|
|
1002
|
+
validateSchemaReferences(
|
|
1003
|
+
logger,
|
|
1004
|
+
makeSchemaRefState({ getStructure: { outputs: ['StructureSummary'] } }, [
|
|
1005
|
+
'StructureSummary',
|
|
1006
|
+
])
|
|
1007
|
+
)
|
|
1008
|
+
assert.deepEqual(diagnostics, [])
|
|
1009
|
+
})
|
|
1010
|
+
|
|
1011
|
+
test('primitive contracts are never schema references', () => {
|
|
1012
|
+
const { logger, diagnostics } = makeDiagnosticLogger()
|
|
1013
|
+
validateSchemaReferences(
|
|
1014
|
+
logger,
|
|
1015
|
+
makeSchemaRefState(
|
|
1016
|
+
{ getStructure: { inputs: ['string'], outputs: ['void'] } },
|
|
1017
|
+
[]
|
|
1018
|
+
)
|
|
1019
|
+
)
|
|
1020
|
+
assert.deepEqual(diagnostics, [])
|
|
1021
|
+
})
|
|
1022
|
+
|
|
1023
|
+
test('a name shared by two files is reported under the disambiguated name', () => {
|
|
1024
|
+
const { logger, diagnostics } = makeDiagnosticLogger()
|
|
1025
|
+
const typesMap = new TypesMap()
|
|
1026
|
+
const mine = typesMap.addUniqueType('StructureSummary', '/structure.ts')
|
|
1027
|
+
typesMap.addUniqueType('StructureSummary', '/other-structure.ts')
|
|
1028
|
+
const state = {
|
|
1029
|
+
functions: { meta: { getStructure: { outputs: [mine] } }, typesMap },
|
|
1030
|
+
schemas: {},
|
|
1031
|
+
} as unknown as InspectorState
|
|
1032
|
+
validateSchemaReferences(logger, state)
|
|
1033
|
+
assert.equal(diagnostics.length, 1)
|
|
1034
|
+
assert.match(diagnostics[0]!.message, new RegExp(`→ '${mine}'`))
|
|
1035
|
+
})
|
|
1036
|
+
|
|
1037
|
+
test('every unresolved contract is named in one diagnostic, not one each', () => {
|
|
1038
|
+
const { logger, diagnostics } = makeDiagnosticLogger()
|
|
1039
|
+
validateSchemaReferences(
|
|
1040
|
+
logger,
|
|
1041
|
+
makeSchemaRefState(
|
|
1042
|
+
{
|
|
1043
|
+
getStructure: { outputs: ['StructureSummary'] },
|
|
1044
|
+
listStructures: { outputs: ['StructureSummary'] },
|
|
1045
|
+
},
|
|
1046
|
+
[]
|
|
1047
|
+
)
|
|
1048
|
+
)
|
|
1049
|
+
assert.equal(diagnostics.length, 1)
|
|
1050
|
+
assert.match(diagnostics[0]!.message, /2 function contracts name a schema/)
|
|
1051
|
+
})
|
|
1052
|
+
})
|
|
@@ -17,6 +17,7 @@ import { join } from 'node:path'
|
|
|
17
17
|
import { extractTypeKeys } from './type-utils.js'
|
|
18
18
|
import { ErrorCode } from '../error-codes.js'
|
|
19
19
|
import { findSecretAliasServices } from './secret-alias-services.js'
|
|
20
|
+
import { resolveCoreType } from './resolve-core-type.js'
|
|
20
21
|
import { relative } from 'node:path'
|
|
21
22
|
import { AUTH_HANDLER_FUNC_ID } from '../add/add-auth.js'
|
|
22
23
|
import { flattenScopeDefinitions } from '@pikku/core/scope'
|
|
@@ -106,19 +107,25 @@ function extractAllServices(
|
|
|
106
107
|
}
|
|
107
108
|
|
|
108
109
|
// Extract all singleton services from the SingletonServices type
|
|
109
|
-
const
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
110
|
+
const singletonServicesType = resolveCoreType(
|
|
111
|
+
state.typesLookup,
|
|
112
|
+
state.singletonServicesTypeImportMap
|
|
113
|
+
)
|
|
114
|
+
if (singletonServicesType) {
|
|
115
|
+
state.serviceAggregation.allSingletonServices = extractTypeKeys(
|
|
116
|
+
singletonServicesType
|
|
117
|
+
).sort()
|
|
113
118
|
}
|
|
114
119
|
|
|
115
120
|
// Extract all services from the Services type
|
|
116
|
-
const
|
|
117
|
-
|
|
118
|
-
|
|
121
|
+
const servicesType = resolveCoreType(
|
|
122
|
+
state.typesLookup,
|
|
123
|
+
state.wireServicesTypeImportMap
|
|
124
|
+
)
|
|
125
|
+
if (servicesType) {
|
|
119
126
|
// Wire services are those in Services but not in SingletonServices
|
|
120
127
|
const singletonSet = new Set(state.serviceAggregation.allSingletonServices)
|
|
121
|
-
state.serviceAggregation.allWireServices =
|
|
128
|
+
state.serviceAggregation.allWireServices = extractTypeKeys(servicesType)
|
|
122
129
|
.filter((name) => !singletonSet.has(name))
|
|
123
130
|
.sort()
|
|
124
131
|
}
|
|
@@ -1101,11 +1108,14 @@ export function validateNoSecretAliasServices(
|
|
|
1101
1108
|
if (!('typesLookup' in state)) {
|
|
1102
1109
|
return
|
|
1103
1110
|
}
|
|
1104
|
-
const
|
|
1105
|
-
|
|
1111
|
+
const singletonServicesType = resolveCoreType(
|
|
1112
|
+
state.typesLookup,
|
|
1113
|
+
state.singletonServicesTypeImportMap
|
|
1114
|
+
)
|
|
1115
|
+
if (!singletonServicesType) {
|
|
1106
1116
|
return
|
|
1107
1117
|
}
|
|
1108
|
-
const aliases = findSecretAliasServices(
|
|
1118
|
+
const aliases = findSecretAliasServices(singletonServicesType, checker)
|
|
1109
1119
|
if (aliases.length === 0) {
|
|
1110
1120
|
return
|
|
1111
1121
|
}
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
import { describe, test } from 'node:test'
|
|
2
|
+
import assert from 'node:assert/strict'
|
|
3
|
+
import ts from 'typescript'
|
|
4
|
+
import { aggregateRequiredServices } from './post-process.js'
|
|
5
|
+
import type { InspectorState, PathToNameAndType } from '../types.js'
|
|
6
|
+
|
|
7
|
+
const SOURCE_FILE = '/application-types.ts'
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Builds the slice of inspector state that service extraction reads: the
|
|
11
|
+
* `typesLookup` entry the AST walk records under whatever the project named its
|
|
12
|
+
* interface, and the import map that points at that name.
|
|
13
|
+
*/
|
|
14
|
+
const stateWithCoreTypes = (
|
|
15
|
+
source: string,
|
|
16
|
+
singletonName: string,
|
|
17
|
+
wireName: string
|
|
18
|
+
): InspectorState => {
|
|
19
|
+
const host: ts.CompilerHost = {
|
|
20
|
+
...ts.createCompilerHost({}),
|
|
21
|
+
getSourceFile: (name, languageVersion) =>
|
|
22
|
+
name === SOURCE_FILE
|
|
23
|
+
? ts.createSourceFile(name, source, languageVersion, true)
|
|
24
|
+
: undefined,
|
|
25
|
+
fileExists: (name) => name === SOURCE_FILE,
|
|
26
|
+
readFile: (name) => (name === SOURCE_FILE ? source : undefined),
|
|
27
|
+
}
|
|
28
|
+
const program = ts.createProgram([SOURCE_FILE], { noLib: true }, host)
|
|
29
|
+
const checker = program.getTypeChecker()
|
|
30
|
+
const sourceFile = program.getSourceFile(SOURCE_FILE)!
|
|
31
|
+
|
|
32
|
+
const typesLookup = new Map<string, ts.Type[]>()
|
|
33
|
+
const importMapFor = (name: string): PathToNameAndType => {
|
|
34
|
+
const declaration = sourceFile.statements.find(
|
|
35
|
+
(statement): statement is ts.InterfaceDeclaration =>
|
|
36
|
+
ts.isInterfaceDeclaration(statement) && statement.name.text === name
|
|
37
|
+
)!
|
|
38
|
+
typesLookup.set(name, [checker.getTypeAtLocation(declaration)])
|
|
39
|
+
return new Map([
|
|
40
|
+
[SOURCE_FILE, [{ variable: name, type: name, typePath: SOURCE_FILE }]],
|
|
41
|
+
])
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
return {
|
|
45
|
+
typesLookup,
|
|
46
|
+
singletonServicesTypeImportMap: importMapFor(singletonName),
|
|
47
|
+
wireServicesTypeImportMap: importMapFor(wireName),
|
|
48
|
+
serviceAggregation: {
|
|
49
|
+
requiredServices: new Set<string>(),
|
|
50
|
+
usedFunctions: new Set<string>(),
|
|
51
|
+
usedMiddleware: new Set<string>(),
|
|
52
|
+
usedPermissions: new Set<string>(),
|
|
53
|
+
allSingletonServices: [],
|
|
54
|
+
allWireServices: [],
|
|
55
|
+
},
|
|
56
|
+
functions: { meta: {} },
|
|
57
|
+
middleware: { definitions: {}, tagMiddleware: new Map() },
|
|
58
|
+
permissions: { definitions: {} },
|
|
59
|
+
http: {
|
|
60
|
+
meta: {
|
|
61
|
+
get: {},
|
|
62
|
+
post: {},
|
|
63
|
+
put: {},
|
|
64
|
+
patch: {},
|
|
65
|
+
delete: {},
|
|
66
|
+
head: {},
|
|
67
|
+
options: {},
|
|
68
|
+
},
|
|
69
|
+
routeMiddleware: new Map(),
|
|
70
|
+
},
|
|
71
|
+
channels: { meta: {} },
|
|
72
|
+
queueWorkers: { meta: {} },
|
|
73
|
+
scheduledTasks: { meta: {} },
|
|
74
|
+
mcpEndpoints: { toolsMeta: {}, promptsMeta: {}, resourcesMeta: {} },
|
|
75
|
+
agents: { agentsMeta: {} },
|
|
76
|
+
workflows: { meta: {}, graphMeta: {} },
|
|
77
|
+
wireServicesMeta: new Map(),
|
|
78
|
+
rpc: { internalMeta: {}, exposedMeta: {} },
|
|
79
|
+
scopes: { definitions: [] },
|
|
80
|
+
addonFunctions: {},
|
|
81
|
+
addonRequiredParentServices: [],
|
|
82
|
+
} as unknown as InspectorState
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const CONVENTIONAL = `
|
|
86
|
+
interface SingletonServices { logger: string; kysely: string }
|
|
87
|
+
interface Services { logger: string; kysely: string; http: string }
|
|
88
|
+
`
|
|
89
|
+
|
|
90
|
+
const RENAMED = `
|
|
91
|
+
interface AppSingletonServices { logger: string; kysely: string }
|
|
92
|
+
interface AppServices { logger: string; kysely: string; http: string }
|
|
93
|
+
`
|
|
94
|
+
|
|
95
|
+
describe('service extraction resolves core types by declaration, not by name', () => {
|
|
96
|
+
test('the conventional names still resolve', () => {
|
|
97
|
+
const state = stateWithCoreTypes(
|
|
98
|
+
CONVENTIONAL,
|
|
99
|
+
'SingletonServices',
|
|
100
|
+
'Services'
|
|
101
|
+
)
|
|
102
|
+
aggregateRequiredServices(state)
|
|
103
|
+
assert.deepEqual(state.serviceAggregation.allSingletonServices, [
|
|
104
|
+
'kysely',
|
|
105
|
+
'logger',
|
|
106
|
+
])
|
|
107
|
+
assert.deepEqual(state.serviceAggregation.allWireServices, ['http'])
|
|
108
|
+
})
|
|
109
|
+
|
|
110
|
+
test('a project that renamed its interfaces resolves the same way', () => {
|
|
111
|
+
const state = stateWithCoreTypes(
|
|
112
|
+
RENAMED,
|
|
113
|
+
'AppSingletonServices',
|
|
114
|
+
'AppServices'
|
|
115
|
+
)
|
|
116
|
+
aggregateRequiredServices(state)
|
|
117
|
+
assert.deepEqual(
|
|
118
|
+
state.serviceAggregation.allSingletonServices,
|
|
119
|
+
['kysely', 'logger'],
|
|
120
|
+
'a renamed SingletonServices must not silently resolve to no services'
|
|
121
|
+
)
|
|
122
|
+
assert.deepEqual(state.serviceAggregation.allWireServices, ['http'])
|
|
123
|
+
})
|
|
124
|
+
|
|
125
|
+
test('no declaration at all leaves the lists empty for PKU724 to catch', () => {
|
|
126
|
+
const state = stateWithCoreTypes(
|
|
127
|
+
CONVENTIONAL,
|
|
128
|
+
'SingletonServices',
|
|
129
|
+
'Services'
|
|
130
|
+
)
|
|
131
|
+
state.singletonServicesTypeImportMap = new Map()
|
|
132
|
+
state.wireServicesTypeImportMap = new Map()
|
|
133
|
+
// Left at their initial empty value on purpose: `extractAllServices` only
|
|
134
|
+
// assigns when a type resolves, so seeding them here would make the
|
|
135
|
+
// assertions below true no matter what it did. `typesLookup` still holds
|
|
136
|
+
// both interfaces under their conventional names — resolving one from
|
|
137
|
+
// there is exactly the bug this guards.
|
|
138
|
+
aggregateRequiredServices(state)
|
|
139
|
+
assert.deepEqual(state.serviceAggregation.allSingletonServices, [])
|
|
140
|
+
assert.deepEqual(state.serviceAggregation.allWireServices, [])
|
|
141
|
+
})
|
|
142
|
+
})
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import type ts from 'typescript'
|
|
2
|
+
import type { PathToNameAndType } from '../types.js'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Resolves the one interface extending a core type (`CoreConfig`,
|
|
6
|
+
* `CoreSingletonServices`, `CoreServices`) to the `ts.Type` the AST walk
|
|
7
|
+
* recorded for it.
|
|
8
|
+
*
|
|
9
|
+
* `typesLookup` is keyed by whatever the project named the interface, so
|
|
10
|
+
* reading it under a hardcoded `'SingletonServices'` only works for projects
|
|
11
|
+
* that follow the scaffold's naming. A project that renamed the interface still
|
|
12
|
+
* satisfies every required-type check and then resolves to no services at all,
|
|
13
|
+
* which surfaces far downstream as PKU724 or as every service turning optional.
|
|
14
|
+
* The import map carries the real name, so no name matching is needed.
|
|
15
|
+
*
|
|
16
|
+
* A project is only ever meant to declare one of each: `getMetaTypes` records
|
|
17
|
+
* `More than one ... found` when it sees a second, which `checkRequiredTypes`
|
|
18
|
+
* then reports. That check is the caller's to run, and extraction happens
|
|
19
|
+
* first, so where two declarations do exist this takes the first in traversal
|
|
20
|
+
* order rather than diagnosing the ambiguity itself.
|
|
21
|
+
*/
|
|
22
|
+
export const resolveCoreType = (
|
|
23
|
+
typesLookup: Map<string, ts.Type[]>,
|
|
24
|
+
importMap: PathToNameAndType
|
|
25
|
+
): ts.Type | undefined => {
|
|
26
|
+
for (const entries of importMap.values()) {
|
|
27
|
+
for (const { type } of entries) {
|
|
28
|
+
const resolved = type ? typesLookup.get(type)?.[0] : undefined
|
|
29
|
+
if (resolved) {
|
|
30
|
+
return resolved
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
return undefined
|
|
35
|
+
}
|