@pikku/inspector 0.6.2

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 (48) hide show
  1. package/CHANGELOG.md +17 -0
  2. package/README.md +3 -0
  3. package/dist/add-channel.d.ts +3 -0
  4. package/dist/add-channel.js +122 -0
  5. package/dist/add-file-extends-core-type.d.ts +3 -0
  6. package/dist/add-file-extends-core-type.js +38 -0
  7. package/dist/add-file-with-config.d.ts +3 -0
  8. package/dist/add-file-with-config.js +31 -0
  9. package/dist/add-file-with-factory.d.ts +3 -0
  10. package/dist/add-file-with-factory.js +48 -0
  11. package/dist/add-route.d.ts +4 -0
  12. package/dist/add-route.js +89 -0
  13. package/dist/add-schedule.d.ts +3 -0
  14. package/dist/add-schedule.js +32 -0
  15. package/dist/does-type-extend-core-type.d.ts +2 -0
  16. package/dist/does-type-extend-core-type.js +41 -0
  17. package/dist/get-property-value.d.ts +3 -0
  18. package/dist/get-property-value.js +60 -0
  19. package/dist/index.d.ts +4 -0
  20. package/dist/index.js +1 -0
  21. package/dist/inspector.d.ts +3 -0
  22. package/dist/inspector.js +43 -0
  23. package/dist/types-map.d.ts +18 -0
  24. package/dist/types-map.js +103 -0
  25. package/dist/types.d.ts +49 -0
  26. package/dist/types.js +1 -0
  27. package/dist/utils.d.ts +30 -0
  28. package/dist/utils.js +245 -0
  29. package/dist/visit.d.ts +3 -0
  30. package/dist/visit.js +17 -0
  31. package/package.json +30 -0
  32. package/run-tests.sh +53 -0
  33. package/src/add-channel.ts +168 -0
  34. package/src/add-file-extends-core-type.ts +50 -0
  35. package/src/add-file-with-config.ts +45 -0
  36. package/src/add-file-with-factory.ts +65 -0
  37. package/src/add-route.ts +131 -0
  38. package/src/add-schedule.ts +47 -0
  39. package/src/does-type-extend-core-type.ts +53 -0
  40. package/src/get-property-value.ts +81 -0
  41. package/src/index.ts +4 -0
  42. package/src/inspector.ts +53 -0
  43. package/src/types-map.ts +130 -0
  44. package/src/types.ts +58 -0
  45. package/src/utils.ts +349 -0
  46. package/src/visit.ts +49 -0
  47. package/tsconfig.json +19 -0
  48. package/tsconfig.tsbuildinfo +1 -0
@@ -0,0 +1,103 @@
1
+ export class TypesMap {
2
+ map = new Map();
3
+ customTypes = new Map();
4
+ addCustomType(name, type, references) {
5
+ this.customTypes.set(name, { type, references });
6
+ }
7
+ addType(originalName, path) {
8
+ this.map.set(originalName, { originalName, path });
9
+ }
10
+ addUniqueType(originalName, path) {
11
+ const uniqueName = `${originalName}_${Math.random().toString(36).substring(7)}`;
12
+ this.map.set(uniqueName, { originalName, path });
13
+ return uniqueName;
14
+ }
15
+ getUniqueName(name) {
16
+ const meta = this.getTypeMeta(name);
17
+ return meta.uniqueName;
18
+ }
19
+ getTypeMeta(name) {
20
+ if (['string', 'number', 'boolean', 'null'].includes(name)) {
21
+ return {
22
+ originalName: name,
23
+ uniqueName: name,
24
+ path: null,
25
+ };
26
+ }
27
+ if (this.customTypes.has(name)) {
28
+ return {
29
+ originalName: name,
30
+ uniqueName: name,
31
+ path: null,
32
+ };
33
+ }
34
+ let meta = this.map.get(name);
35
+ if (!meta) {
36
+ meta = Array.from(this.map.entries()).find(([_, { originalName }]) => originalName === name)?.[1];
37
+ }
38
+ if (!meta) {
39
+ throw new Error(`Type ${name} not found in typesMap`);
40
+ }
41
+ const getName = this.squash();
42
+ return {
43
+ uniqueName: getName(name),
44
+ originalName: meta.originalName,
45
+ path: meta?.path,
46
+ };
47
+ }
48
+ exists(originalName, path) {
49
+ const found = Array.from(this.map.entries()).find(([_, type]) => {
50
+ return type.path === path && type.originalName === originalName;
51
+ });
52
+ return found ? found[0] : undefined;
53
+ }
54
+ squash() {
55
+ const duplicateNames = new Set();
56
+ const pathToNamesMap = new Map();
57
+ const nameOccurrences = new Map();
58
+ // First pass: Track occurrences of each original name across paths
59
+ this.map.forEach(({ path, originalName }) => {
60
+ if (path) {
61
+ if (!nameOccurrences.has(originalName)) {
62
+ nameOccurrences.set(originalName, new Set());
63
+ }
64
+ nameOccurrences.get(originalName).add(path);
65
+ }
66
+ });
67
+ // Second pass: Populate pathToNamesMap
68
+ this.map.forEach(({ path, originalName }, uniqueName) => {
69
+ if (!path)
70
+ return;
71
+ if (!pathToNamesMap.has(path)) {
72
+ pathToNamesMap.set(path, new Map());
73
+ }
74
+ const isDuplicate = nameOccurrences.get(originalName).size > 1;
75
+ if (isDuplicate) {
76
+ duplicateNames.add(uniqueName);
77
+ }
78
+ // Use uniqueName only if the originalName is duplicated across files
79
+ const nameToUse = isDuplicate ? uniqueName : originalName;
80
+ pathToNamesMap.get(path).set(nameToUse, originalName);
81
+ });
82
+ const getName = (uniqueName) => {
83
+ if (duplicateNames.has(uniqueName)) {
84
+ return uniqueName;
85
+ }
86
+ if (uniqueName === 'string' ||
87
+ uniqueName === 'number' ||
88
+ uniqueName === 'boolean' ||
89
+ uniqueName === 'null') {
90
+ return uniqueName;
91
+ }
92
+ if (!this.map.has(uniqueName)) {
93
+ const found = Array.from(this.map.entries()).find(([_, { originalName }]) => originalName === uniqueName)?.[1];
94
+ if (!found) {
95
+ throw new Error(`Type ${uniqueName} not found in typesMap`);
96
+ }
97
+ return found.originalName;
98
+ }
99
+ return this.map.get(uniqueName).originalName;
100
+ };
101
+ return getName;
102
+ }
103
+ }
@@ -0,0 +1,49 @@
1
+ import { ChannelsMeta } from '@pikku/core/channel';
2
+ import { HTTPRoutesMeta } from '@pikku/core/http';
3
+ import { ScheduledTasksMeta } from '@pikku/core/scheduler';
4
+ import { TypesMap } from './types-map.js';
5
+ export type PathToNameAndType = Map<string, {
6
+ variable: string;
7
+ type: string | null;
8
+ typePath: string | null;
9
+ }[]>;
10
+ export type MetaInputTypes = Map<string, {
11
+ query: string[] | undefined;
12
+ params: string[] | undefined;
13
+ body: string[] | undefined;
14
+ }>;
15
+ export type APIFunctionMeta = Array<{
16
+ name: string;
17
+ input: string;
18
+ output: string;
19
+ file: string;
20
+ }>;
21
+ export type InspectorAPIFunction = {
22
+ typesMap: TypesMap;
23
+ meta: APIFunctionMeta;
24
+ };
25
+ export interface InspectorHTTPState {
26
+ typesMap: TypesMap;
27
+ metaInputTypes: MetaInputTypes;
28
+ meta: HTTPRoutesMeta;
29
+ files: Set<string>;
30
+ }
31
+ export interface InspectorChannelState {
32
+ typesMap: TypesMap;
33
+ metaInputTypes: MetaInputTypes;
34
+ meta: ChannelsMeta;
35
+ files: Set<string>;
36
+ }
37
+ export interface InspectorState {
38
+ sessionServicesTypeImportMap: PathToNameAndType;
39
+ userSessionTypeImportMap: PathToNameAndType;
40
+ singletonServicesFactories: PathToNameAndType;
41
+ sessionServicesFactories: PathToNameAndType;
42
+ configFactories: PathToNameAndType;
43
+ http: InspectorHTTPState;
44
+ channels: InspectorChannelState;
45
+ scheduledTasks: {
46
+ meta: ScheduledTasksMeta;
47
+ files: Set<string>;
48
+ };
49
+ }
package/dist/types.js ADDED
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,30 @@
1
+ import * as ts from 'typescript';
2
+ import { TypesMap } from './types-map.js';
3
+ type FunctionTypes = {
4
+ inputTypes: ts.Type[];
5
+ inputs: null | string[];
6
+ outputTypes: ts.Type[];
7
+ outputs: null | string[];
8
+ };
9
+ export declare const extractTypeKeys: (type: ts.Type) => string[];
10
+ export declare const nullifyTypes: (type: string | null) => string | null;
11
+ export declare const getNamesAndTypes: (checker: ts.TypeChecker, typesMap: TypesMap, direction: "Input" | "Output", funcName: string, type: ts.Type) => {
12
+ names: string[];
13
+ types: ts.Type[];
14
+ };
15
+ export declare const isPrimitiveType: (type: ts.Type) => boolean;
16
+ export declare const resolveUnionTypes: (checker: ts.TypeChecker, type: ts.Type) => {
17
+ types: ts.Type[];
18
+ names: string[];
19
+ };
20
+ export declare const resolveTypeImports: (type: ts.Type, resolvedTypes: TypesMap, isCustom: boolean) => string[];
21
+ export declare const getPropertyAssignment: (obj: ts.ObjectLiteralExpression, name: string) => ts.ObjectLiteralElementLike | null;
22
+ export declare const getTypeArgumentsOfType: (checker: ts.TypeChecker, type: ts.Type) => readonly ts.Type[] | null;
23
+ export declare const getFunctionTypes: (checker: ts.TypeChecker, obj: ts.ObjectLiteralExpression, { typesMap, funcName, subFunctionName, inputIndex, outputIndex, }: {
24
+ typesMap: TypesMap;
25
+ subFunctionName?: string;
26
+ funcName: string;
27
+ inputIndex: number;
28
+ outputIndex: number;
29
+ }) => FunctionTypes;
30
+ export {};
package/dist/utils.js ADDED
@@ -0,0 +1,245 @@
1
+ import * as ts from 'typescript';
2
+ export const extractTypeKeys = (type) => {
3
+ return type.getProperties().map((symbol) => symbol.getName());
4
+ };
5
+ export const nullifyTypes = (type) => {
6
+ if (type === 'void' ||
7
+ type === 'undefined' ||
8
+ type === 'unknown' ||
9
+ type === 'any') {
10
+ return null;
11
+ }
12
+ return type;
13
+ };
14
+ const isValidVariableName = (name) => {
15
+ const regex = /^[a-zA-Z_$][a-zA-Z0-9_$]*$/;
16
+ return regex.test(name);
17
+ };
18
+ export const getNamesAndTypes = (checker, typesMap, direction, funcName, type) => {
19
+ const result = {
20
+ names: new Set(),
21
+ types: [],
22
+ };
23
+ const { names, types } = resolveUnionTypes(checker, type);
24
+ const firstName = names[0];
25
+ if (names.length > 1 || (firstName && !isValidVariableName(firstName))) {
26
+ const aliasType = names.join(' | ');
27
+ const aliasName = `${funcName.charAt(0).toUpperCase()}${funcName.slice(1)}${direction}`;
28
+ result.names = new Set([aliasName]);
29
+ result.types = types;
30
+ const references = types
31
+ .map((t) => resolveTypeImports(t, typesMap, true))
32
+ .flat();
33
+ typesMap.addCustomType(aliasName, aliasType, references);
34
+ }
35
+ else {
36
+ const uniqueNames = names
37
+ .map((name, i) => {
38
+ const type = types[i];
39
+ if (!type) {
40
+ throw new Error('TODO: Expected a type here to match name');
41
+ }
42
+ if (isPrimitiveType(type)) {
43
+ return name;
44
+ }
45
+ return resolveTypeImports(type, typesMap, false);
46
+ })
47
+ .flat();
48
+ result.names = new Set(uniqueNames);
49
+ result.types = types;
50
+ }
51
+ return {
52
+ names: Array.from(result.names),
53
+ types: result.types,
54
+ };
55
+ };
56
+ export const isPrimitiveType = (type) => {
57
+ const primitiveFlags = ts.TypeFlags.Number |
58
+ ts.TypeFlags.String |
59
+ ts.TypeFlags.Boolean |
60
+ ts.TypeFlags.BigInt |
61
+ ts.TypeFlags.ESSymbol |
62
+ ts.TypeFlags.Void |
63
+ ts.TypeFlags.Undefined |
64
+ ts.TypeFlags.Null |
65
+ ts.TypeFlags.Any |
66
+ ts.TypeFlags.Unknown;
67
+ return (type.flags & primitiveFlags) !== 0;
68
+ };
69
+ export const resolveUnionTypes = (checker, type) => {
70
+ const types = [];
71
+ const names = [];
72
+ // Check if it's a union type AND not part of an intersection
73
+ if (type.isUnion() && !(type.flags & ts.TypeFlags.Intersection)) {
74
+ for (const t of type.types) {
75
+ const name = nullifyTypes(checker.typeToString(t));
76
+ if (name) {
77
+ types.push(t);
78
+ names.push(name);
79
+ }
80
+ }
81
+ }
82
+ else {
83
+ const name = nullifyTypes(checker.typeToString(type));
84
+ if (name) {
85
+ types.push(type);
86
+ names.push(name);
87
+ }
88
+ }
89
+ return { types, names };
90
+ };
91
+ export const resolveTypeImports = (type, resolvedTypes, isCustom) => {
92
+ const types = [];
93
+ const visitType = (currentType) => {
94
+ const symbol = currentType.aliasSymbol || currentType.getSymbol();
95
+ if (symbol) {
96
+ const declarations = symbol.getDeclarations();
97
+ const declaration = declarations?.[0];
98
+ if (declaration) {
99
+ const sourceFile = declaration.getSourceFile();
100
+ const path = sourceFile.fileName;
101
+ // Skip built-in utility types or TypeScript lib types
102
+ if (!path.includes('node_modules/typescript') &&
103
+ symbol.getName() !== '__type' &&
104
+ !isPrimitiveType(currentType)) {
105
+ const originalName = symbol.getName();
106
+ // Check if the type is already in the map
107
+ let uniqueName = resolvedTypes.exists(originalName, path);
108
+ if (!uniqueName) {
109
+ if (isCustom) {
110
+ uniqueName = resolvedTypes.addUniqueType(originalName, path);
111
+ }
112
+ else {
113
+ resolvedTypes.addType(originalName, path);
114
+ uniqueName = originalName;
115
+ }
116
+ }
117
+ types.push(uniqueName);
118
+ }
119
+ }
120
+ }
121
+ if (isCustom) {
122
+ // Handle nested utility types like Partial, Pick, etc.
123
+ if (currentType.aliasTypeArguments) {
124
+ currentType.aliasTypeArguments.forEach(visitType);
125
+ }
126
+ // Handle intersections and unions
127
+ if (currentType.isUnionOrIntersection()) {
128
+ currentType.types.forEach(visitType);
129
+ }
130
+ // Handle object types with type arguments
131
+ if (currentType.flags & ts.TypeFlags.Object &&
132
+ currentType.objectFlags & ts.ObjectFlags.Reference) {
133
+ const typeRef = currentType;
134
+ typeRef.typeArguments?.forEach(visitType);
135
+ }
136
+ }
137
+ };
138
+ visitType(type);
139
+ return types;
140
+ };
141
+ export const getPropertyAssignment = (obj, name) => {
142
+ const property = obj.properties.find((p) => (ts.isPropertyAssignment(p) || ts.isShorthandPropertyAssignment(p)) &&
143
+ ts.isIdentifier(p.name) &&
144
+ p.name.text === name);
145
+ if (!property) {
146
+ console.error(`Missing property '${name}' in object`);
147
+ return null;
148
+ }
149
+ return property;
150
+ };
151
+ export const getTypeArgumentsOfType = (checker, type) => {
152
+ if (type.isUnionOrIntersection()) {
153
+ const types = [];
154
+ for (const subType of type.types) {
155
+ const subTypeArgs = getTypeArgumentsOfType(checker, subType);
156
+ if (subTypeArgs) {
157
+ types.push(...subTypeArgs);
158
+ }
159
+ }
160
+ return types.length > 0 ? types : null;
161
+ }
162
+ // If the type is a TypeReference with typeArguments, return them
163
+ if (type.flags & ts.TypeFlags.Object &&
164
+ type.objectFlags & ts.ObjectFlags.Reference) {
165
+ const typeRef = type;
166
+ if (typeRef.typeArguments && typeRef.typeArguments.length > 0) {
167
+ return typeRef.typeArguments;
168
+ }
169
+ }
170
+ // If the type is an alias with aliasTypeArguments, return them
171
+ if (type.aliasTypeArguments && type.aliasTypeArguments.length > 0) {
172
+ return type.aliasTypeArguments;
173
+ }
174
+ return null;
175
+ };
176
+ export const getFunctionTypes = (checker, obj, { typesMap, funcName, subFunctionName = funcName, inputIndex, outputIndex, }) => {
177
+ const result = {
178
+ inputTypes: [],
179
+ inputs: null,
180
+ outputTypes: [],
181
+ outputs: null,
182
+ };
183
+ const property = getPropertyAssignment(obj, subFunctionName);
184
+ if (!property) {
185
+ return result;
186
+ }
187
+ let type;
188
+ // Handle shorthand property assignment
189
+ if (ts.isShorthandPropertyAssignment(property)) {
190
+ const symbol = checker.getShorthandAssignmentValueSymbol(property);
191
+ if (symbol) {
192
+ type = checker.getTypeOfSymbolAtLocation(symbol, property);
193
+ if (funcName === 'func') {
194
+ funcName = symbol.name;
195
+ }
196
+ }
197
+ }
198
+ // Handle regular property assignment
199
+ else if (ts.isPropertyAssignment(property)) {
200
+ if (ts.isObjectLiteralExpression(property.initializer)) {
201
+ return getFunctionTypes(checker, property.initializer, {
202
+ typesMap,
203
+ funcName,
204
+ subFunctionName: 'func',
205
+ inputIndex,
206
+ outputIndex,
207
+ });
208
+ }
209
+ if (property.initializer) {
210
+ type = checker.getTypeAtLocation(property.initializer);
211
+ if (funcName === 'func') {
212
+ funcName = property.initializer.getText();
213
+ }
214
+ }
215
+ }
216
+ if (!type) {
217
+ console.error(`Unable to resolve type for property '${funcName}'`);
218
+ return result;
219
+ }
220
+ // Access type arguments from TypeReference
221
+ const typeArguments = getTypeArgumentsOfType(checker, type);
222
+ if (!typeArguments || typeArguments.length === 0) {
223
+ // This is the case for inline functions. In this case we would want to
224
+ // get the types from the second argument of the function...
225
+ console.error(`\x1b[31m• No generic type arguments found for ${funcName}. Support for inline functions is not yet implemented.\x1b[0m`);
226
+ return result;
227
+ }
228
+ if (inputIndex !== undefined && inputIndex < typeArguments.length) {
229
+ const { names, types } = getNamesAndTypes(checker, typesMap, 'Input', funcName, typeArguments[inputIndex]);
230
+ result.inputs = names;
231
+ result.inputTypes = types;
232
+ }
233
+ else {
234
+ console.log(`No input defined for ${funcName}`);
235
+ }
236
+ if (outputIndex !== undefined && outputIndex < typeArguments.length) {
237
+ const { names, types } = getNamesAndTypes(checker, typesMap, 'Output', funcName, typeArguments[outputIndex]);
238
+ result.outputs = names;
239
+ result.outputTypes = types;
240
+ }
241
+ else {
242
+ console.info(`No output defined for ${funcName}`);
243
+ }
244
+ return result;
245
+ };
@@ -0,0 +1,3 @@
1
+ import * as ts from 'typescript';
2
+ import { InspectorState } from './types.js';
3
+ export declare const visit: (checker: ts.TypeChecker, node: ts.Node, state: InspectorState) => void;
package/dist/visit.js ADDED
@@ -0,0 +1,17 @@
1
+ import * as ts from 'typescript';
2
+ import { addFileWithFactory } from './add-file-with-factory.js';
3
+ import { addFileExtendsCoreType } from './add-file-extends-core-type.js';
4
+ import { addRoute } from './add-route.js';
5
+ import { addSchedule } from './add-schedule.js';
6
+ import { addChannel } from './add-channel.js';
7
+ export const visit = (checker, node, state) => {
8
+ addFileExtendsCoreType(node, checker, state.sessionServicesTypeImportMap, 'CoreServices');
9
+ addFileExtendsCoreType(node, checker, state.userSessionTypeImportMap, 'CoreUserSession');
10
+ addFileWithFactory(node, checker, state.singletonServicesFactories, 'CreateSingletonServices');
11
+ addFileWithFactory(node, checker, state.sessionServicesFactories, 'CreateSessionServices');
12
+ addFileWithFactory(node, checker, state.configFactories, 'CreateConfig');
13
+ addRoute(node, checker, state);
14
+ addSchedule(node, checker, state);
15
+ addChannel(node, checker, state);
16
+ ts.forEachChild(node, (child) => visit(checker, child, state));
17
+ };
package/package.json ADDED
@@ -0,0 +1,30 @@
1
+ {
2
+ "name": "@pikku/inspector",
3
+ "version": "0.6.2",
4
+ "author": "yasser.fadl@gmail.com",
5
+ "license": "MIT",
6
+ "type": "module",
7
+ "module": "dist/index.js",
8
+ "main": "dist/index.js",
9
+ "scripts": {
10
+ "tsc": "tsc",
11
+ "build:esm": "tsc -b",
12
+ "build": "yarn build:esm",
13
+ "ncu": "ncu -x '/.*glob.*/'",
14
+ "release": "yarn build && npm test",
15
+ "test": "bash run-tests.sh",
16
+ "test:watch": "bash run-tests.sh --watch",
17
+ "test:coverage": "bash run-tests.sh --coverage"
18
+ },
19
+ "dependencies": {
20
+ "@pikku/core": "^0.6.5",
21
+ "path-to-regexp": "^8.2.0",
22
+ "typescript": "^5.6"
23
+ },
24
+ "devDependencies": {
25
+ "@types/node": "^22.7.8"
26
+ },
27
+ "engines": {
28
+ "node": ">=18"
29
+ }
30
+ }
package/run-tests.sh ADDED
@@ -0,0 +1,53 @@
1
+ #!/bin/bash
2
+
3
+ # Enable nullglob to handle cases where no files match the pattern
4
+ shopt -s nullglob
5
+
6
+ # Initialize variables for options
7
+ watch_mode=false
8
+ coverage_mode=false
9
+
10
+ # Parse command-line options
11
+ while [[ $# -gt 0 ]]; do
12
+ case $1 in
13
+ --watch)
14
+ watch_mode=true
15
+ shift
16
+ ;;
17
+ --coverage)
18
+ coverage_mode=true
19
+ shift
20
+ ;;
21
+ *)
22
+ echo "Unknown option: $1"
23
+ exit 1
24
+ ;;
25
+ esac
26
+ done
27
+
28
+ # Define the pattern to match your test files
29
+ pattern="src/*.test.ts"
30
+
31
+ # Expand the pattern into an array of files
32
+ files=($(find src -type f -name "*.test.ts"))
33
+
34
+ # Check if any files matched the pattern
35
+ if [ ${#files[@]} -eq 0 ]; then
36
+ echo "No test files found matching pattern: $pattern"
37
+ exit 0
38
+ fi
39
+
40
+ # Construct the node command
41
+ node_cmd="node --import tsx --test"
42
+
43
+ # Append options based on flags
44
+ if [ "$watch_mode" = true ]; then
45
+ node_cmd="$node_cmd --watch"
46
+ fi
47
+
48
+ if [ "$coverage_mode" = true ]; then
49
+ node_cmd="$node_cmd --experimental-test-coverage --test-reporter=lcov --test-reporter-destination=lcov.info"
50
+ fi
51
+
52
+ # Execute the node command with the expanded list of files
53
+ $node_cmd "${files[@]}"