@knapsack/spec-utils 4.78.13--canary.5646.9581069.0 → 4.78.13

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 (57) hide show
  1. package/.turbo/turbo-build.log +4 -0
  2. package/.turbo/turbo-lint.log +4 -0
  3. package/.turbo/turbo-test.log +50 -0
  4. package/CHANGELOG.md +12 -0
  5. package/dist/align/align.vtest.d.ts +2 -0
  6. package/dist/align/align.vtest.d.ts.map +1 -0
  7. package/dist/align/align.vtest.js +46 -0
  8. package/dist/align/align.vtest.js.map +1 -0
  9. package/dist/analyze-exports.sandbox-components.vtest.d.ts +2 -0
  10. package/dist/analyze-exports.sandbox-components.vtest.d.ts.map +1 -0
  11. package/dist/analyze-exports.sandbox-components.vtest.js +51 -0
  12. package/dist/analyze-exports.sandbox-components.vtest.js.map +1 -0
  13. package/dist/analyze-exports.vtest.d.ts +2 -0
  14. package/dist/analyze-exports.vtest.d.ts.map +1 -0
  15. package/dist/analyze-exports.vtest.js +160 -0
  16. package/dist/analyze-exports.vtest.js.map +1 -0
  17. package/dist/convert-to-spec.vtest.d.ts +2 -0
  18. package/dist/convert-to-spec.vtest.d.ts.map +1 -0
  19. package/dist/convert-to-spec.vtest.js +131 -0
  20. package/dist/convert-to-spec.vtest.js.map +1 -0
  21. package/dist/get-ts-config.vtest.d.ts +2 -0
  22. package/dist/get-ts-config.vtest.d.ts.map +1 -0
  23. package/dist/get-ts-config.vtest.js +9 -0
  24. package/dist/get-ts-config.vtest.js.map +1 -0
  25. package/dist/resolve.vtest.d.ts +2 -0
  26. package/dist/resolve.vtest.d.ts.map +1 -0
  27. package/dist/resolve.vtest.js +57 -0
  28. package/dist/resolve.vtest.js.map +1 -0
  29. package/dist/utils.vtest.d.ts +2 -0
  30. package/dist/utils.vtest.d.ts.map +1 -0
  31. package/dist/utils.vtest.js +37 -0
  32. package/dist/utils.vtest.js.map +1 -0
  33. package/package.json +10 -10
  34. package/src/align/align.vtest.ts +56 -0
  35. package/src/align/get-exports.bench.ts +28 -0
  36. package/src/align/resolve.bench.ts +20 -0
  37. package/src/align/utils.ts +14 -0
  38. package/src/analyze-exports.sandbox-components.vtest.ts +53 -0
  39. package/src/analyze-exports.ts +54 -0
  40. package/src/analyze-exports.vtest.ts +178 -0
  41. package/src/analyze-symbol.ts +213 -0
  42. package/src/analyze-type.ts +316 -0
  43. package/src/boot.ts +31 -0
  44. package/src/convert-to-spec.ts +196 -0
  45. package/src/convert-to-spec.vtest.ts +136 -0
  46. package/src/get-exports.ts +70 -0
  47. package/src/get-ts-config.ts +96 -0
  48. package/src/get-ts-config.vtest.ts +9 -0
  49. package/src/index.ts +5 -0
  50. package/src/resolve.ts +54 -0
  51. package/src/resolve.vtest.ts +69 -0
  52. package/src/test-fixtures/basics.ts +17 -0
  53. package/src/test-fixtures/functions.ts +50 -0
  54. package/src/test-fixtures/index.ts +2 -0
  55. package/src/types.ts +66 -0
  56. package/src/utils.ts +61 -0
  57. package/src/utils.vtest.ts +39 -0
@@ -0,0 +1,213 @@
1
+ import ts from 'typescript';
2
+ import { analyzeType } from './analyze-type.js';
3
+ import { SymbolInfo, TypeInfo } from './types.js';
4
+ import { getSetFlags } from './utils.js';
5
+
6
+ export function analyzeSymbol({
7
+ symbol,
8
+ checker,
9
+ symbolsVisited = new Set(),
10
+ }: {
11
+ symbol: ts.Symbol;
12
+ checker: ts.TypeChecker;
13
+ /** use to prevent infinite recursion */
14
+ symbolsVisited?: Set<ts.Symbol>;
15
+ }): SymbolInfo {
16
+ symbolsVisited.add(symbol);
17
+ const type = symbol.valueDeclaration
18
+ ? checker.getTypeOfSymbolAtLocation(symbol, symbol.valueDeclaration)
19
+ : checker.getTypeOfSymbol(symbol);
20
+ const description = ts.displayPartsToString(
21
+ symbol.getDocumentationComment(checker),
22
+ );
23
+ const jsDocTags = symbol.getJsDocTags().map(({ name, text }) => ({
24
+ name,
25
+ text: text ? ts.displayPartsToString(text) : undefined,
26
+ }));
27
+ const {
28
+ // Here in case a `Node` is needed
29
+ valueDeclaration: node,
30
+ } = symbol;
31
+
32
+ const symbolFlags = getSetFlags({
33
+ flags: symbol.flags,
34
+ enumObject: ts.SymbolFlags,
35
+ });
36
+ const typeFlags = getSetFlags({
37
+ flags: type.flags,
38
+ enumObject: ts.TypeFlags,
39
+ });
40
+ const isOptional = symbolFlags.includes('Optional');
41
+ const isArray = checker.isArrayType(type) || checker.isArrayLikeType(type);
42
+
43
+ // set in `if` blocks below
44
+ let typeInfo: TypeInfo;
45
+ const baseSymbolInfo: Omit<SymbolInfo, 'typeInfo'> = {
46
+ tsMetadata: {
47
+ symbol,
48
+ type,
49
+ symbolFlags,
50
+ },
51
+ jsDoc:
52
+ description || jsDocTags.length
53
+ ? {
54
+ description,
55
+ jsDocTags,
56
+ }
57
+ : undefined,
58
+ name: symbol.getName(),
59
+ };
60
+
61
+ // Data I have seen in debugger, but am not sure how to safely access:
62
+ // type.members - reliably indicates an "object" by providing a Map of Symbols of its properties. The check for Object below is not as reliable since everything in JS is an object.
63
+ // type.objectFlags - seems to indicate the type of the object, but not sure how to safely access it.
64
+
65
+ /**
66
+ * A function can have multiple call signatures - this is done when using "overloads", like this:
67
+ * ```ts
68
+ * export function sayHello(msg: string[]): string;
69
+ * export function sayHello(msg: string): string;
70
+ * export function sayHello(msg: unknown): string {
71
+ * return `Hello ${Array.isArray(msg) ? msg.join(' ') : msg}`;
72
+ * }
73
+ * ```
74
+ *
75
+ * Handling that seems like too much (and doubtfully useful) so we only handle the first call signature.
76
+ */
77
+ const [callSignature] = type.getCallSignatures();
78
+ // consider using over `callSignature`
79
+ const _isFunction = symbolFlags.includes('Function');
80
+ // if it's callable, it's a function
81
+ if (callSignature) {
82
+ const returnType = callSignature.getReturnType();
83
+ const typeParameters = callSignature.getTypeParameters();
84
+ typeInfo = {
85
+ type: 'function',
86
+ parameters: callSignature.parameters
87
+ .filter((paramSymbol) => !symbolsVisited.has(paramSymbol))
88
+ .map((paramSymbol) => {
89
+ return analyzeSymbol({
90
+ symbol: paramSymbol,
91
+ checker,
92
+ symbolsVisited,
93
+ });
94
+ }),
95
+ typeParameters: typeParameters?.map((typeParameter) => {
96
+ return analyzeType({ type: typeParameter, checker });
97
+ }),
98
+ // returnType: returnType
99
+ // ? analyzeType({ type: returnType, checker })
100
+ // : undefined,
101
+ returnType: analyzeType({ type: returnType, checker }),
102
+ tsRawType: checker.typeToString(
103
+ type,
104
+ node, // remove if it looks weird
105
+ ts.TypeFormatFlags.NoTruncation,
106
+ ),
107
+ isOptional,
108
+ tsMetadata: {
109
+ typeFlags: getSetFlags({
110
+ flags: type.flags,
111
+ enumObject: ts.TypeFlags,
112
+ }),
113
+ type,
114
+ },
115
+ };
116
+ } else if (symbolFlags.includes('Class')) {
117
+ const [constructSignature] = type.getConstructSignatures();
118
+ const prototype = type.getProperty('prototype');
119
+ if (!prototype) {
120
+ throw new Error(`${symbol.getName()} has no prototype`);
121
+ }
122
+ typeInfo = {
123
+ type: 'class',
124
+ tsRawType: checker.typeToString(
125
+ type,
126
+ undefined,
127
+ ts.TypeFormatFlags.NoTruncation,
128
+ ),
129
+ isOptional,
130
+ tsMetadata: {
131
+ typeFlags: getSetFlags({
132
+ flags: type.flags,
133
+ enumObject: ts.TypeFlags,
134
+ }),
135
+ type,
136
+ },
137
+ prototype: analyzeSymbol({
138
+ symbol: prototype,
139
+ checker,
140
+ symbolsVisited,
141
+ }),
142
+ properties: type.getProperties().reduce((acc, propSymbol) => {
143
+ if (symbolsVisited.has(propSymbol)) return acc;
144
+ const symbolInfo = analyzeSymbol({
145
+ symbol: propSymbol,
146
+ checker,
147
+ symbolsVisited,
148
+ });
149
+ acc[symbolInfo.name] = symbolInfo;
150
+ return acc;
151
+ }, {} as Extract<TypeInfo, { type: 'object' }>['properties']),
152
+ // constructorReturnType: constructSignature
153
+ // ? analyzeType({ type: constructSignature.getReturnType(), checker })
154
+ // : undefined,
155
+ constructorReturnType: analyzeType({
156
+ type: constructSignature.getReturnType(),
157
+ checker,
158
+ }),
159
+
160
+ constructorParameters: constructSignature
161
+ ?.getParameters()
162
+ .map((constructParam) => {
163
+ return analyzeSymbol({
164
+ symbol: constructParam,
165
+ checker,
166
+ symbolsVisited,
167
+ });
168
+ }),
169
+ };
170
+ } else if (
171
+ // this check could be better: lots of things are `ts.TypeFlags.Object`
172
+ typeFlags.includes('Object') &&
173
+ // here we exclude things that are technically `ts.TypeFlags.Object` but are not "objects" in JS
174
+ !isArray
175
+ ) {
176
+ typeInfo = {
177
+ type: 'object',
178
+ tsRawType: checker.typeToString(
179
+ type,
180
+ undefined,
181
+ ts.TypeFormatFlags.NoTruncation,
182
+ ),
183
+ properties: type.getProperties().reduce((acc, propSymbol) => {
184
+ if (symbolsVisited.has(propSymbol)) return acc;
185
+ const symbolInfo = analyzeSymbol({
186
+ symbol: propSymbol,
187
+ checker,
188
+ symbolsVisited,
189
+ });
190
+ acc[symbolInfo.name] = symbolInfo;
191
+ return acc;
192
+ }, {} as Extract<TypeInfo, { type: 'object' }>['properties']),
193
+ isOptional,
194
+ tsMetadata: {
195
+ typeFlags: getSetFlags({
196
+ flags: type.flags,
197
+ enumObject: ts.TypeFlags,
198
+ }),
199
+ type,
200
+ },
201
+ };
202
+ } else {
203
+ typeInfo = analyzeType({
204
+ type,
205
+ checker,
206
+ isOptional,
207
+ });
208
+ }
209
+ return {
210
+ ...baseSymbolInfo,
211
+ typeInfo,
212
+ };
213
+ }
@@ -0,0 +1,316 @@
1
+ import ts from 'typescript';
2
+ import { TypeInfo, TypeInfoCommon } from './types.js';
3
+ import { getSetFlags } from './utils.js';
4
+
5
+ // Cache for type IDs to improve performance
6
+ const typeIdMap = new WeakMap<ts.Type, string>();
7
+
8
+ /**
9
+ * Generates a consistent identifier for a TypeScript type
10
+ * Uses the type's string representation to ensure identical types get the same ID
11
+ */
12
+ function getTypeId(type: ts.Type, checker: ts.TypeChecker): string {
13
+ // Check if we already have an ID for this type
14
+ const existingId = typeIdMap.get(type);
15
+ if (existingId) {
16
+ return existingId;
17
+ }
18
+
19
+ // Generate a new ID based on the type's string representation
20
+ // This ensures identical types get the same ID
21
+ const typeString = checker.typeToString(
22
+ type,
23
+ undefined,
24
+ // eslint-disable-next-line no-bitwise
25
+ ts.TypeFormatFlags.NoTruncation,
26
+ );
27
+
28
+ // Add some additional information to help distinguish types
29
+ // that might have the same string representation
30
+ const idComponents = [typeString];
31
+
32
+ // Add symbol name if available
33
+ const symbol = type.getSymbol();
34
+ if (symbol) {
35
+ idComponents.push(symbol.getName());
36
+ }
37
+
38
+ // Add type flags to help distinguish types
39
+ idComponents.push(String(type.flags));
40
+
41
+ const newId = idComponents.join('_');
42
+ typeIdMap.set(type, newId);
43
+ return newId;
44
+ }
45
+
46
+ /**
47
+ * Analyzes a TypeScript type and returns a structured TypeInfo representation
48
+ */
49
+ export function analyzeType({
50
+ type,
51
+ checker,
52
+ isOptional,
53
+ visitedTypes = new Map<string, TypeInfo>(),
54
+ depth = 0,
55
+ /**
56
+ * The maximum depth of type analysis.
57
+ * This is used to avoid infinite recursion.
58
+ */
59
+ maxDepth = 3,
60
+ }: {
61
+ type: ts.Type;
62
+ checker: ts.TypeChecker;
63
+ isOptional?: boolean;
64
+ visitedTypes?: Map<string, TypeInfo>;
65
+ depth?: number;
66
+ maxDepth?: number;
67
+ }): TypeInfo {
68
+ // Generate a unique key for the type
69
+ const typeId = getTypeId(type, checker);
70
+
71
+ // Check if we've already analyzed this type
72
+ if (visitedTypes.has(typeId)) {
73
+ return visitedTypes.get(typeId)!;
74
+ }
75
+
76
+ const tsRawType = checker.typeToString(
77
+ type,
78
+ undefined,
79
+ // eslint-disable-next-line no-bitwise
80
+ ts.TypeFormatFlags.MultilineObjectLiterals |
81
+ ts.TypeFormatFlags.NoTruncation,
82
+ );
83
+ const typeFlags = getSetFlags({
84
+ flags: type.flags,
85
+ enumObject: ts.TypeFlags,
86
+ });
87
+ const typeInfoCommon: TypeInfoCommon = {
88
+ tsRawType,
89
+ tsMetadata: {
90
+ typeFlags,
91
+ type,
92
+ },
93
+ isOptional,
94
+ };
95
+
96
+ // If we've reached max depth, return a simplified type
97
+ if (depth >= maxDepth) {
98
+ const result: TypeInfo = {
99
+ type: 'misc',
100
+ ...typeInfoCommon,
101
+ };
102
+ visitedTypes.set(typeId, result);
103
+ return result;
104
+ }
105
+
106
+ let result: TypeInfo;
107
+
108
+ // Handle primitive types first
109
+ if (
110
+ tsRawType === 'number' ||
111
+ tsRawType === 'boolean' ||
112
+ tsRawType === 'string'
113
+ ) {
114
+ result = {
115
+ type: tsRawType,
116
+ ...typeInfoCommon,
117
+ };
118
+ visitedTypes.set(typeId, result);
119
+ return result;
120
+ }
121
+
122
+ // Handle literal types
123
+ if (type.isStringLiteral()) {
124
+ result = {
125
+ type: 'stringLiteral',
126
+ value: type.value,
127
+ ...typeInfoCommon,
128
+ };
129
+ visitedTypes.set(typeId, result);
130
+ return result;
131
+ }
132
+
133
+ if (type.isNumberLiteral()) {
134
+ result = {
135
+ type: 'numberLiteral',
136
+ value: type.value,
137
+ ...typeInfoCommon,
138
+ };
139
+ visitedTypes.set(typeId, result);
140
+ return result;
141
+ }
142
+
143
+ // Create a placeholder to avoid infinite recursion
144
+ const placeholder: TypeInfo = {
145
+ type: 'misc',
146
+ ...typeInfoCommon,
147
+ };
148
+ visitedTypes.set(typeId, placeholder);
149
+
150
+ // Handle unions
151
+ if (type.isUnion()) {
152
+ result = {
153
+ type: 'union',
154
+ items: type.types.map((t) =>
155
+ analyzeType({
156
+ type: t,
157
+ checker,
158
+ isOptional,
159
+ visitedTypes,
160
+ depth: depth + 1,
161
+ maxDepth,
162
+ }),
163
+ ),
164
+ ...typeInfoCommon,
165
+ };
166
+ }
167
+ // Handle arrays
168
+ else if (checker.isArrayType(type) || checker.isArrayLikeType(type)) {
169
+ const indexInfos = checker.getIndexInfosOfType(type);
170
+
171
+ result = {
172
+ type: 'array',
173
+ items:
174
+ indexInfos.length === 1
175
+ ? analyzeType({
176
+ type: indexInfos[0]?.type,
177
+ checker,
178
+ isOptional,
179
+ visitedTypes,
180
+ depth: depth + 1,
181
+ maxDepth,
182
+ })
183
+ : { type: 'misc', ...typeInfoCommon },
184
+ ...typeInfoCommon,
185
+ };
186
+ }
187
+ // Handle intersections
188
+ // This is where ForwardRef is being inferred
189
+ // @TODO: We need to find a way to handle depth here. Currently we bring in all of the properties
190
+ // from each type in the intersection, but we may want to limit the depth of the intersection
191
+ // to avoid bringing in too much.
192
+ // https://linear.app/knapsack/issue/KSP-5885/update-handling-of-intersection-types-in-spec-utils
193
+ else if (type.isIntersection()) {
194
+ const properties = type.getProperties();
195
+ if (properties.length > 0) {
196
+ const analyzedProperties = properties.reduce((acc, prop) => {
197
+ try {
198
+ // Get the type directly from the symbol - this is the most reliable method
199
+ const propType = checker.getTypeOfSymbol(prop);
200
+
201
+ if (propType) {
202
+ const propAnalysis = analyzeType({
203
+ type: propType,
204
+ checker,
205
+ // eslint-disable-next-line no-bitwise
206
+ isOptional: !!(prop.flags & ts.SymbolFlags.Optional),
207
+ visitedTypes,
208
+ depth: depth + 1,
209
+ maxDepth,
210
+ });
211
+
212
+ acc[prop.getName()] = {
213
+ name: prop.getName(),
214
+ typeInfo: propAnalysis,
215
+ tsMetadata: {
216
+ symbol: prop,
217
+ type: propType,
218
+ symbolFlags: getSetFlags({
219
+ flags: prop.flags,
220
+ enumObject: ts.SymbolFlags,
221
+ }),
222
+ },
223
+ };
224
+ }
225
+ } catch (error) {
226
+ console.debug(`Error analyzing property ${prop.getName()}: ${error}`);
227
+ }
228
+ return acc;
229
+ }, {} as Record<string, any>);
230
+
231
+ if (Object.keys(analyzedProperties).length > 0) {
232
+ result = {
233
+ type: 'object',
234
+ properties: analyzedProperties,
235
+ ...typeInfoCommon,
236
+ };
237
+ } else {
238
+ result = placeholder;
239
+ }
240
+ } else {
241
+ result = placeholder;
242
+ }
243
+ }
244
+ // Handle function types
245
+ else if (
246
+ typeFlags.includes('Object') &&
247
+ type.getCallSignatures().length > 0
248
+ ) {
249
+ result = {
250
+ type: 'function',
251
+ parameters: [],
252
+ returnType: { type: 'misc', ...typeInfoCommon },
253
+ ...typeInfoCommon,
254
+ };
255
+ }
256
+
257
+ // Handle object types
258
+ // @TODO: update handling of object type inference
259
+ // https://linear.app/knapsack/issue/KSP-5880/handle-arrays-of-objects-in-react-renderer-infer-spec
260
+ // else if (typeFlags.includes('Object')) {
261
+ // const properties = type.getProperties();
262
+ // if (properties.length > 0) {
263
+ // const analyzedProperties = properties.reduce((acc, prop) => {
264
+ // try {
265
+ // const propType = checker.getTypeOfSymbol(prop);
266
+ // if (propType) {
267
+ // const propAnalysis = analyzeType({
268
+ // type: propType,
269
+ // checker,
270
+ // // eslint-disable-next-line no-bitwise
271
+ // isOptional: !!(prop.flags & ts.SymbolFlags.Optional),
272
+ // visitedTypes,
273
+ // depth: depth + 1,
274
+ // maxDepth,
275
+ // });
276
+ // acc[prop.getName()] = {
277
+ // name: prop.getName(),
278
+ // typeInfo: propAnalysis,
279
+ // tsMetadata: {
280
+ // symbol: prop,
281
+ // type: propType,
282
+ // symbolFlags: getSetFlags({
283
+ // flags: prop.flags,
284
+ // enumObject: ts.SymbolFlags,
285
+ // }),
286
+ // },
287
+ // };
288
+ // }
289
+ // } catch (error) {
290
+ // console.debug(`Error analyzing property ${prop.getName()}: ${error}`);
291
+ // }
292
+ // return acc;
293
+ // }, {} as Record<string, any>);
294
+
295
+ // if (Object.keys(analyzedProperties).length > 0) {
296
+ // result = {
297
+ // type: 'object',
298
+ // properties: analyzedProperties,
299
+ // ...typeInfoCommon,
300
+ // };
301
+ // } else {
302
+ // result = placeholder;
303
+ // }
304
+ // } else {
305
+ // result = placeholder;
306
+ // }
307
+ // }
308
+ // Default to misc type for anything we can't properly analyze
309
+ else {
310
+ result = placeholder;
311
+ }
312
+
313
+ // Update the cache with the actual result
314
+ visitedTypes.set(typeId, result);
315
+ return result;
316
+ }
package/src/boot.ts ADDED
@@ -0,0 +1,31 @@
1
+ import { createCompilerHost, createProgram, Program } from 'typescript';
2
+ import { getTsConfigCompilerOptions } from './get-ts-config.js';
3
+
4
+ export function prepTypeScriptBoot({
5
+ configSrc,
6
+ }: {
7
+ configSrc: Parameters<typeof getTsConfigCompilerOptions>[0];
8
+ }) {
9
+ const compilerOptions = getTsConfigCompilerOptions(configSrc);
10
+ const compilerHost = createCompilerHost(compilerOptions);
11
+ return { compilerOptions, compilerHost };
12
+ }
13
+
14
+ export function bootTypescript({
15
+ configSrc,
16
+ rootFiles,
17
+ oldProgram,
18
+ }: {
19
+ configSrc: Parameters<typeof getTsConfigCompilerOptions>[0];
20
+ rootFiles: string[];
21
+ oldProgram?: Program;
22
+ }) {
23
+ const { compilerOptions, compilerHost } = prepTypeScriptBoot({ configSrc });
24
+ const program = createProgram({
25
+ options: compilerOptions,
26
+ rootNames: rootFiles,
27
+ oldProgram,
28
+ });
29
+ const checker = program.getTypeChecker();
30
+ return { compilerHost, compilerOptions, program, checker };
31
+ }