@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.
- package/.turbo/turbo-build.log +4 -0
- package/.turbo/turbo-lint.log +4 -0
- package/.turbo/turbo-test.log +50 -0
- package/CHANGELOG.md +12 -0
- package/dist/align/align.vtest.d.ts +2 -0
- package/dist/align/align.vtest.d.ts.map +1 -0
- package/dist/align/align.vtest.js +46 -0
- package/dist/align/align.vtest.js.map +1 -0
- package/dist/analyze-exports.sandbox-components.vtest.d.ts +2 -0
- package/dist/analyze-exports.sandbox-components.vtest.d.ts.map +1 -0
- package/dist/analyze-exports.sandbox-components.vtest.js +51 -0
- package/dist/analyze-exports.sandbox-components.vtest.js.map +1 -0
- package/dist/analyze-exports.vtest.d.ts +2 -0
- package/dist/analyze-exports.vtest.d.ts.map +1 -0
- package/dist/analyze-exports.vtest.js +160 -0
- package/dist/analyze-exports.vtest.js.map +1 -0
- package/dist/convert-to-spec.vtest.d.ts +2 -0
- package/dist/convert-to-spec.vtest.d.ts.map +1 -0
- package/dist/convert-to-spec.vtest.js +131 -0
- package/dist/convert-to-spec.vtest.js.map +1 -0
- package/dist/get-ts-config.vtest.d.ts +2 -0
- package/dist/get-ts-config.vtest.d.ts.map +1 -0
- package/dist/get-ts-config.vtest.js +9 -0
- package/dist/get-ts-config.vtest.js.map +1 -0
- package/dist/resolve.vtest.d.ts +2 -0
- package/dist/resolve.vtest.d.ts.map +1 -0
- package/dist/resolve.vtest.js +57 -0
- package/dist/resolve.vtest.js.map +1 -0
- package/dist/utils.vtest.d.ts +2 -0
- package/dist/utils.vtest.d.ts.map +1 -0
- package/dist/utils.vtest.js +37 -0
- package/dist/utils.vtest.js.map +1 -0
- package/package.json +10 -10
- package/src/align/align.vtest.ts +56 -0
- package/src/align/get-exports.bench.ts +28 -0
- package/src/align/resolve.bench.ts +20 -0
- package/src/align/utils.ts +14 -0
- package/src/analyze-exports.sandbox-components.vtest.ts +53 -0
- package/src/analyze-exports.ts +54 -0
- package/src/analyze-exports.vtest.ts +178 -0
- package/src/analyze-symbol.ts +213 -0
- package/src/analyze-type.ts +316 -0
- package/src/boot.ts +31 -0
- package/src/convert-to-spec.ts +196 -0
- package/src/convert-to-spec.vtest.ts +136 -0
- package/src/get-exports.ts +70 -0
- package/src/get-ts-config.ts +96 -0
- package/src/get-ts-config.vtest.ts +9 -0
- package/src/index.ts +5 -0
- package/src/resolve.ts +54 -0
- package/src/resolve.vtest.ts +69 -0
- package/src/test-fixtures/basics.ts +17 -0
- package/src/test-fixtures/functions.ts +50 -0
- package/src/test-fixtures/index.ts +2 -0
- package/src/types.ts +66 -0
- package/src/utils.ts +61 -0
- package/src/utils.vtest.ts +39 -0
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
PropertyTypes,
|
|
3
|
+
StringProp,
|
|
4
|
+
BooleanProp,
|
|
5
|
+
NumberProp,
|
|
6
|
+
ObjectProp,
|
|
7
|
+
FunctionProp,
|
|
8
|
+
} from '@knapsack/types';
|
|
9
|
+
import { TypeInfo } from './types.js';
|
|
10
|
+
|
|
11
|
+
export function convertToSpec({
|
|
12
|
+
typeInfo,
|
|
13
|
+
trustRequiredProps,
|
|
14
|
+
}: {
|
|
15
|
+
typeInfo: TypeInfo;
|
|
16
|
+
trustRequiredProps: boolean;
|
|
17
|
+
}): {
|
|
18
|
+
prop: PropertyTypes | null;
|
|
19
|
+
isOptional: boolean;
|
|
20
|
+
} {
|
|
21
|
+
const { isOptional = false } = typeInfo;
|
|
22
|
+
switch (typeInfo.type) {
|
|
23
|
+
case 'object': {
|
|
24
|
+
const { properties } = typeInfo;
|
|
25
|
+
const required: string[] = [];
|
|
26
|
+
return {
|
|
27
|
+
isOptional,
|
|
28
|
+
prop: {
|
|
29
|
+
type: 'object',
|
|
30
|
+
required,
|
|
31
|
+
properties: Object.entries(properties).reduce(
|
|
32
|
+
(acc, [propName, { typeInfo: propTypeInfo, jsDoc }]) => {
|
|
33
|
+
const details = convertToSpec({
|
|
34
|
+
typeInfo: propTypeInfo,
|
|
35
|
+
trustRequiredProps,
|
|
36
|
+
});
|
|
37
|
+
if (!details.prop) return acc;
|
|
38
|
+
|
|
39
|
+
acc[propName] = details.prop;
|
|
40
|
+
if (jsDoc?.description) {
|
|
41
|
+
acc[propName].description = jsDoc.description;
|
|
42
|
+
}
|
|
43
|
+
if (!details.isOptional && trustRequiredProps) {
|
|
44
|
+
required.push(propName);
|
|
45
|
+
}
|
|
46
|
+
return acc;
|
|
47
|
+
},
|
|
48
|
+
{} as ObjectProp['properties'],
|
|
49
|
+
),
|
|
50
|
+
} satisfies ObjectProp,
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
case 'array': {
|
|
54
|
+
const { items } = typeInfo;
|
|
55
|
+
const { prop } = convertToSpec({
|
|
56
|
+
typeInfo: items,
|
|
57
|
+
trustRequiredProps,
|
|
58
|
+
});
|
|
59
|
+
return {
|
|
60
|
+
isOptional,
|
|
61
|
+
prop: {
|
|
62
|
+
type: 'array',
|
|
63
|
+
// Use the converted items type if available, fallback to object type for complex types
|
|
64
|
+
items:
|
|
65
|
+
prop || ({ type: 'object', properties: {} } satisfies ObjectProp),
|
|
66
|
+
...(typeInfo.tsRawType && { tsType: typeInfo.tsRawType }),
|
|
67
|
+
},
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
case 'string': {
|
|
71
|
+
return {
|
|
72
|
+
isOptional,
|
|
73
|
+
prop: {
|
|
74
|
+
type: 'string',
|
|
75
|
+
} satisfies StringProp,
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
case 'number': {
|
|
79
|
+
return {
|
|
80
|
+
isOptional,
|
|
81
|
+
prop: {
|
|
82
|
+
type: 'number',
|
|
83
|
+
} satisfies NumberProp,
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
case 'boolean': {
|
|
87
|
+
return {
|
|
88
|
+
isOptional,
|
|
89
|
+
prop: {
|
|
90
|
+
type: 'boolean',
|
|
91
|
+
} satisfies BooleanProp,
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
case 'union': {
|
|
95
|
+
const { items } = typeInfo;
|
|
96
|
+
|
|
97
|
+
// Handle string literal unions
|
|
98
|
+
const isUnionOfStrings = items.every(
|
|
99
|
+
(item): item is Extract<TypeInfo, { type: 'stringLiteral' }> =>
|
|
100
|
+
item.type === 'stringLiteral' || item.type === 'string',
|
|
101
|
+
);
|
|
102
|
+
|
|
103
|
+
// Handle number literal unions
|
|
104
|
+
const isUnionOfNumbers = items.every(
|
|
105
|
+
(item): item is Extract<TypeInfo, { type: 'numberLiteral' }> =>
|
|
106
|
+
item.type === 'numberLiteral' || item.type === 'number',
|
|
107
|
+
);
|
|
108
|
+
|
|
109
|
+
if (isUnionOfStrings) {
|
|
110
|
+
return {
|
|
111
|
+
isOptional,
|
|
112
|
+
prop: {
|
|
113
|
+
type: 'string',
|
|
114
|
+
enum: items
|
|
115
|
+
.filter(
|
|
116
|
+
(item): item is Extract<TypeInfo, { type: 'stringLiteral' }> =>
|
|
117
|
+
item.type === 'stringLiteral',
|
|
118
|
+
)
|
|
119
|
+
.map((item) => item.value),
|
|
120
|
+
},
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
if (isUnionOfNumbers) {
|
|
125
|
+
return {
|
|
126
|
+
isOptional,
|
|
127
|
+
prop: {
|
|
128
|
+
type: 'number',
|
|
129
|
+
enum: items
|
|
130
|
+
.filter(
|
|
131
|
+
(item): item is Extract<TypeInfo, { type: 'numberLiteral' }> =>
|
|
132
|
+
item.type === 'numberLiteral',
|
|
133
|
+
)
|
|
134
|
+
.map((item) => item.value),
|
|
135
|
+
},
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
return {
|
|
140
|
+
isOptional,
|
|
141
|
+
prop: null,
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
case 'function': {
|
|
145
|
+
return {
|
|
146
|
+
isOptional,
|
|
147
|
+
prop: {
|
|
148
|
+
typeof: 'function',
|
|
149
|
+
// ...(typeInfo.tsRawType && { tsType: typeInfo.tsRawType }),
|
|
150
|
+
tsType: typeInfo.tsRawType,
|
|
151
|
+
} satisfies FunctionProp,
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
case 'misc': {
|
|
155
|
+
// if there is type info in the tsRawType we try to convert it
|
|
156
|
+
if (typeInfo.tsRawType) {
|
|
157
|
+
// Create a new TypeInfo based on the tsRawType
|
|
158
|
+
const derivedTypeInfo = {
|
|
159
|
+
type: typeInfo.tsRawType,
|
|
160
|
+
isOptional,
|
|
161
|
+
tsRawType: typeInfo.tsRawType,
|
|
162
|
+
tsMetadata: typeInfo.tsMetadata,
|
|
163
|
+
} as TypeInfo;
|
|
164
|
+
|
|
165
|
+
return convertToSpec({
|
|
166
|
+
typeInfo: derivedTypeInfo,
|
|
167
|
+
trustRequiredProps,
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
return {
|
|
172
|
+
isOptional,
|
|
173
|
+
prop: null,
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
case 'numberLiteral':
|
|
177
|
+
case 'stringLiteral':
|
|
178
|
+
case 'class':
|
|
179
|
+
case 'unknown':
|
|
180
|
+
case 'any': {
|
|
181
|
+
return {
|
|
182
|
+
isOptional,
|
|
183
|
+
prop: null,
|
|
184
|
+
};
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
default: {
|
|
188
|
+
const _exhaustiveCheck: never = typeInfo;
|
|
189
|
+
|
|
190
|
+
return {
|
|
191
|
+
isOptional,
|
|
192
|
+
prop: null,
|
|
193
|
+
};
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
}
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { convertToSpec } from './convert-to-spec.js';
|
|
3
|
+
import { TypeInfo, SymbolInfo } from './types.js';
|
|
4
|
+
|
|
5
|
+
// these two are up here to make TS happy
|
|
6
|
+
const typeInfoStubBase = {
|
|
7
|
+
tsRawType: '',
|
|
8
|
+
tsMetadata: null as unknown as TypeInfo['tsMetadata'],
|
|
9
|
+
};
|
|
10
|
+
const tsMetadata = null as unknown as SymbolInfo['tsMetadata'];
|
|
11
|
+
|
|
12
|
+
describe('convert-to-spec', () => {
|
|
13
|
+
it('handles string', () => {
|
|
14
|
+
const { prop } = convertToSpec({
|
|
15
|
+
typeInfo: {
|
|
16
|
+
type: 'string',
|
|
17
|
+
...typeInfoStubBase,
|
|
18
|
+
},
|
|
19
|
+
trustRequiredProps: false,
|
|
20
|
+
});
|
|
21
|
+
expect(prop).toEqual({
|
|
22
|
+
type: 'string',
|
|
23
|
+
} satisfies typeof prop);
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it('handles boolean', () => {
|
|
27
|
+
const { prop } = convertToSpec({
|
|
28
|
+
typeInfo: {
|
|
29
|
+
type: 'boolean',
|
|
30
|
+
...typeInfoStubBase,
|
|
31
|
+
},
|
|
32
|
+
trustRequiredProps: false,
|
|
33
|
+
});
|
|
34
|
+
expect(prop).toEqual({
|
|
35
|
+
type: 'boolean',
|
|
36
|
+
} satisfies typeof prop);
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it('handles object', () => {
|
|
40
|
+
const { prop } = convertToSpec({
|
|
41
|
+
typeInfo: {
|
|
42
|
+
type: 'object',
|
|
43
|
+
...typeInfoStubBase,
|
|
44
|
+
properties: {
|
|
45
|
+
name: {
|
|
46
|
+
name: 'name',
|
|
47
|
+
typeInfo: {
|
|
48
|
+
type: 'string',
|
|
49
|
+
tsRawType: '',
|
|
50
|
+
tsMetadata: null as unknown as TypeInfo['tsMetadata'],
|
|
51
|
+
},
|
|
52
|
+
tsMetadata,
|
|
53
|
+
},
|
|
54
|
+
age: {
|
|
55
|
+
name: 'age',
|
|
56
|
+
typeInfo: {
|
|
57
|
+
isOptional: true,
|
|
58
|
+
type: 'number',
|
|
59
|
+
...typeInfoStubBase,
|
|
60
|
+
},
|
|
61
|
+
tsMetadata,
|
|
62
|
+
},
|
|
63
|
+
shoePrefs: {
|
|
64
|
+
name: 'shoePrefs',
|
|
65
|
+
typeInfo: {
|
|
66
|
+
type: 'union',
|
|
67
|
+
...typeInfoStubBase,
|
|
68
|
+
items: [
|
|
69
|
+
{
|
|
70
|
+
type: 'stringLiteral',
|
|
71
|
+
value: 'sandals',
|
|
72
|
+
...typeInfoStubBase,
|
|
73
|
+
},
|
|
74
|
+
{ type: 'stringLiteral', value: 'boots', ...typeInfoStubBase },
|
|
75
|
+
],
|
|
76
|
+
},
|
|
77
|
+
tsMetadata,
|
|
78
|
+
},
|
|
79
|
+
},
|
|
80
|
+
},
|
|
81
|
+
trustRequiredProps: true,
|
|
82
|
+
});
|
|
83
|
+
expect(prop).toEqual({
|
|
84
|
+
type: 'object',
|
|
85
|
+
required: ['name', 'shoePrefs'],
|
|
86
|
+
properties: {
|
|
87
|
+
name: {
|
|
88
|
+
type: 'string',
|
|
89
|
+
},
|
|
90
|
+
age: {
|
|
91
|
+
type: 'number',
|
|
92
|
+
},
|
|
93
|
+
shoePrefs: {
|
|
94
|
+
type: 'string',
|
|
95
|
+
enum: ['sandals', 'boots'],
|
|
96
|
+
},
|
|
97
|
+
},
|
|
98
|
+
} satisfies typeof prop);
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
it('handles array of objects', () => {
|
|
102
|
+
const { prop } = convertToSpec({
|
|
103
|
+
typeInfo: {
|
|
104
|
+
type: 'array',
|
|
105
|
+
items: {
|
|
106
|
+
type: 'object',
|
|
107
|
+
properties: {
|
|
108
|
+
name: {
|
|
109
|
+
name: 'name',
|
|
110
|
+
typeInfo: {
|
|
111
|
+
type: 'string',
|
|
112
|
+
...typeInfoStubBase,
|
|
113
|
+
},
|
|
114
|
+
tsMetadata,
|
|
115
|
+
},
|
|
116
|
+
},
|
|
117
|
+
...typeInfoStubBase,
|
|
118
|
+
},
|
|
119
|
+
...typeInfoStubBase,
|
|
120
|
+
},
|
|
121
|
+
trustRequiredProps: false,
|
|
122
|
+
});
|
|
123
|
+
expect(prop).toEqual({
|
|
124
|
+
type: 'array',
|
|
125
|
+
items: {
|
|
126
|
+
type: 'object',
|
|
127
|
+
required: [],
|
|
128
|
+
properties: {
|
|
129
|
+
name: {
|
|
130
|
+
type: 'string',
|
|
131
|
+
},
|
|
132
|
+
},
|
|
133
|
+
},
|
|
134
|
+
} satisfies typeof prop);
|
|
135
|
+
});
|
|
136
|
+
});
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { join } from 'node:path';
|
|
2
|
+
import { resolveTsModule } from './resolve.js';
|
|
3
|
+
import { bootTypescript } from './boot.js';
|
|
4
|
+
|
|
5
|
+
function getTsExportsUncached({
|
|
6
|
+
fileName,
|
|
7
|
+
pkgPathAliases,
|
|
8
|
+
resolveFromDir = process.cwd(),
|
|
9
|
+
}: {
|
|
10
|
+
fileName: string;
|
|
11
|
+
pkgPathAliases?: Record<string, string>;
|
|
12
|
+
resolveFromDir?: string;
|
|
13
|
+
}) {
|
|
14
|
+
const { resolvedFileName } = resolveTsModule({
|
|
15
|
+
containingFile: join(resolveFromDir, 'containing-file.ts'),
|
|
16
|
+
moduleName: fileName,
|
|
17
|
+
pkgPathAliases,
|
|
18
|
+
});
|
|
19
|
+
const { program, checker } = bootTypescript({
|
|
20
|
+
rootFiles: [resolvedFileName],
|
|
21
|
+
configSrc: {
|
|
22
|
+
type: 'preset',
|
|
23
|
+
preset: 'bundler',
|
|
24
|
+
},
|
|
25
|
+
});
|
|
26
|
+
const files = program.getSourceFiles();
|
|
27
|
+
const file = files.find((f) => f.fileName === resolvedFileName);
|
|
28
|
+
if (!file) {
|
|
29
|
+
throw new Error(`File ${fileName} not found`);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const moduleSymbol = checker.getSymbolAtLocation(file);
|
|
33
|
+
if (!moduleSymbol) {
|
|
34
|
+
throw new Error(`Module symbol for ${fileName} not found`);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
return {
|
|
38
|
+
exports: checker.getExportsOfModule(moduleSymbol),
|
|
39
|
+
checker,
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const cache = new Map<string, ReturnType<typeof getTsExportsUncached>>();
|
|
44
|
+
const timeouts = new Map<string, NodeJS.Timeout>();
|
|
45
|
+
// clear the timeout when the process is terminated so process.exit() doesn't hang
|
|
46
|
+
process.on('SIGTERM', () => {
|
|
47
|
+
for (const timeout of timeouts.values()) {
|
|
48
|
+
clearTimeout(timeout);
|
|
49
|
+
}
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
export function getTsExports(opt: Parameters<typeof getTsExportsUncached>[0]) {
|
|
53
|
+
if (process.env.NODE_ENV === 'test') {
|
|
54
|
+
return getTsExportsUncached(opt);
|
|
55
|
+
}
|
|
56
|
+
const key = JSON.stringify(opt);
|
|
57
|
+
|
|
58
|
+
const exports = cache.get(key);
|
|
59
|
+
if (exports) {
|
|
60
|
+
return exports;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const result = getTsExportsUncached(opt);
|
|
64
|
+
cache.set(key, result);
|
|
65
|
+
timeouts.set(
|
|
66
|
+
key,
|
|
67
|
+
setTimeout(() => cache.delete(key), 5_000),
|
|
68
|
+
);
|
|
69
|
+
return result;
|
|
70
|
+
}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import ts, {
|
|
2
|
+
sys as tsSys,
|
|
3
|
+
findConfigFile,
|
|
4
|
+
readConfigFile,
|
|
5
|
+
parseJsonConfigFileContent,
|
|
6
|
+
getDefaultCompilerOptions,
|
|
7
|
+
CompilerOptions,
|
|
8
|
+
} from 'typescript';
|
|
9
|
+
import { dirname } from 'node:path';
|
|
10
|
+
|
|
11
|
+
export function getTsConfigJson({
|
|
12
|
+
basePath = process.cwd(),
|
|
13
|
+
}: {
|
|
14
|
+
basePath?: string;
|
|
15
|
+
} = {}) {
|
|
16
|
+
// Find tsconfig.json file
|
|
17
|
+
const tsConfigPath = findConfigFile(
|
|
18
|
+
basePath,
|
|
19
|
+
tsSys.fileExists,
|
|
20
|
+
'tsconfig.json',
|
|
21
|
+
);
|
|
22
|
+
|
|
23
|
+
if (!tsConfigPath) {
|
|
24
|
+
throw new Error(
|
|
25
|
+
`No tsconfig.json file found searching up from ${basePath} `,
|
|
26
|
+
);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// Read tsconfig.json file
|
|
30
|
+
const tsconfigFile = readConfigFile(tsConfigPath, tsSys.readFile);
|
|
31
|
+
if (tsconfigFile.error) {
|
|
32
|
+
throw new Error(
|
|
33
|
+
`Failed to read tsconfig.json file: ${tsconfigFile.error.messageText}`,
|
|
34
|
+
);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// Resolve extends
|
|
38
|
+
const tsConfig = parseJsonConfigFileContent(
|
|
39
|
+
tsconfigFile.config,
|
|
40
|
+
tsSys,
|
|
41
|
+
dirname(tsConfigPath),
|
|
42
|
+
);
|
|
43
|
+
|
|
44
|
+
return { tsConfig, tsConfigPath };
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function getTsConfigCompilerOptions(
|
|
48
|
+
configSrc:
|
|
49
|
+
| {
|
|
50
|
+
type: 'resolveTsConfigJson';
|
|
51
|
+
basePath?: string;
|
|
52
|
+
}
|
|
53
|
+
| {
|
|
54
|
+
type: 'inline';
|
|
55
|
+
config: CompilerOptions;
|
|
56
|
+
}
|
|
57
|
+
| {
|
|
58
|
+
type: 'preset';
|
|
59
|
+
preset: 'default' | 'modern' | 'bundler';
|
|
60
|
+
},
|
|
61
|
+
): CompilerOptions {
|
|
62
|
+
switch (configSrc.type) {
|
|
63
|
+
case 'resolveTsConfigJson':
|
|
64
|
+
return getTsConfigJson({ basePath: configSrc.basePath }).tsConfig.options;
|
|
65
|
+
case 'inline': {
|
|
66
|
+
return configSrc.config;
|
|
67
|
+
}
|
|
68
|
+
case 'preset': {
|
|
69
|
+
switch (configSrc.preset) {
|
|
70
|
+
case 'default': {
|
|
71
|
+
return getDefaultCompilerOptions();
|
|
72
|
+
}
|
|
73
|
+
case 'bundler': {
|
|
74
|
+
return {
|
|
75
|
+
module: ts.ModuleKind.ESNext,
|
|
76
|
+
moduleResolution: ts.ModuleResolutionKind.Bundler,
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
case 'modern': {
|
|
80
|
+
return {
|
|
81
|
+
module: ts.ModuleKind.NodeNext,
|
|
82
|
+
moduleResolution: ts.ModuleResolutionKind.NodeNext,
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
default: {
|
|
86
|
+
const _exhaustiveCheck: never = configSrc.preset;
|
|
87
|
+
throw new Error(`Unknown preset: ${configSrc.preset}`);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
default: {
|
|
92
|
+
const _exhaustiveCheck: never = configSrc;
|
|
93
|
+
throw new Error(`Unknown config source: ${JSON.stringify(configSrc)}`);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { getTsConfigJson } from './get-ts-config.js';
|
|
3
|
+
|
|
4
|
+
describe('get-ts-config', () => {
|
|
5
|
+
it('reads this pkg tsconfig.json', () => {
|
|
6
|
+
const { tsConfig } = getTsConfigJson({ basePath: import.meta.dirname });
|
|
7
|
+
expect(Object.keys(tsConfig.options).length).toBeGreaterThan(0);
|
|
8
|
+
});
|
|
9
|
+
});
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export { getTsConfigCompilerOptions } from './get-ts-config.js';
|
|
2
|
+
export { resolveTsModule } from './resolve.js';
|
|
3
|
+
export { analyzeExport, analyzeExports } from './analyze-exports.js';
|
|
4
|
+
export type { TypeInfo, SymbolInfo } from './types.js';
|
|
5
|
+
export { convertToSpec } from './convert-to-spec.js';
|
package/src/resolve.ts
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import ts, { resolveModuleName } from 'typescript';
|
|
2
|
+
import { prepTypeScriptBoot } from './boot.js';
|
|
3
|
+
|
|
4
|
+
function pkgPathAliasesToPaths(pkgPathAliases: Record<string, string>) {
|
|
5
|
+
if (!pkgPathAliases) return undefined;
|
|
6
|
+
return Object.entries(pkgPathAliases).reduce((acc, [alias, path]) => {
|
|
7
|
+
acc[alias] = [path];
|
|
8
|
+
acc[`${alias}/*`] = [`${path}/*`];
|
|
9
|
+
return acc;
|
|
10
|
+
}, {} as Record<string, string[]>);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function resolveTsModule({
|
|
14
|
+
containingFile,
|
|
15
|
+
moduleName,
|
|
16
|
+
pkgPathAliases = {},
|
|
17
|
+
}: {
|
|
18
|
+
moduleName: string;
|
|
19
|
+
containingFile: string;
|
|
20
|
+
pkgPathAliases?: Record<string, string>;
|
|
21
|
+
}) {
|
|
22
|
+
const paths = pkgPathAliasesToPaths(
|
|
23
|
+
Object.fromEntries(
|
|
24
|
+
Object.entries(pkgPathAliases).map(([alias, pkgPath]) => {
|
|
25
|
+
const { resolvedFileName: pkgPathResolved } = resolveTsModule({
|
|
26
|
+
moduleName: pkgPath,
|
|
27
|
+
containingFile,
|
|
28
|
+
});
|
|
29
|
+
return [alias, pkgPathResolved];
|
|
30
|
+
}),
|
|
31
|
+
),
|
|
32
|
+
);
|
|
33
|
+
|
|
34
|
+
const { compilerOptions, compilerHost } = prepTypeScriptBoot({
|
|
35
|
+
configSrc: {
|
|
36
|
+
type: 'inline',
|
|
37
|
+
config: {
|
|
38
|
+
moduleResolution: ts.ModuleResolutionKind.Bundler,
|
|
39
|
+
module: ts.ModuleKind.NodeNext,
|
|
40
|
+
paths,
|
|
41
|
+
},
|
|
42
|
+
},
|
|
43
|
+
});
|
|
44
|
+
const { resolvedModule } = resolveModuleName(
|
|
45
|
+
moduleName,
|
|
46
|
+
containingFile,
|
|
47
|
+
compilerOptions,
|
|
48
|
+
compilerHost,
|
|
49
|
+
);
|
|
50
|
+
if (!resolvedModule) {
|
|
51
|
+
throw new Error(`Failed to resolve module: ${moduleName}`);
|
|
52
|
+
}
|
|
53
|
+
return resolvedModule;
|
|
54
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
import { resolveTsModule } from './resolve.js';
|
|
4
|
+
|
|
5
|
+
describe('resolve module tests', () => {
|
|
6
|
+
it('resolves @knapsack/utils', () => {
|
|
7
|
+
const { resolvedFileName, isExternalLibraryImport } = resolveTsModule({
|
|
8
|
+
containingFile: import.meta.filename,
|
|
9
|
+
moduleName: '@knapsack/utils',
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
expect(resolvedFileName).toBeTruthy();
|
|
13
|
+
expect(resolvedFileName).toMatch(/\.d\.ts$/);
|
|
14
|
+
expect(resolvedFileName).toContain('utils/dist');
|
|
15
|
+
expect(isExternalLibraryImport).toBe(true);
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
it('resolves local ts file with .js extension', () => {
|
|
19
|
+
const { resolvedFileName } = resolveTsModule({
|
|
20
|
+
containingFile: import.meta.filename,
|
|
21
|
+
moduleName: './boot.js',
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
expect(resolvedFileName).toBe(join(import.meta.dirname, './boot.ts'));
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it('resolves local ts file with .ts extension', () => {
|
|
28
|
+
const { resolvedFileName } = resolveTsModule({
|
|
29
|
+
containingFile: import.meta.filename,
|
|
30
|
+
moduleName: './boot.ts',
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
expect(resolvedFileName).toBe(join(import.meta.dirname, './boot.ts'));
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it('resolves a local package.json', () => {
|
|
37
|
+
const { resolvedFileName } = resolveTsModule({
|
|
38
|
+
containingFile: import.meta.filename,
|
|
39
|
+
moduleName: '../package.json',
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
expect(resolvedFileName).toMatch(/package\.json$/);
|
|
43
|
+
expect(resolvedFileName).toBe(join(import.meta.dirname, '../package.json'));
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it('resolves a local file', () => {
|
|
47
|
+
const { resolvedFileName } = resolveTsModule({
|
|
48
|
+
containingFile: import.meta.filename,
|
|
49
|
+
moduleName: '../.eslintrc.cjs',
|
|
50
|
+
});
|
|
51
|
+
expect(resolvedFileName).toBe(
|
|
52
|
+
join(import.meta.dirname, '../.eslintrc.cjs'),
|
|
53
|
+
);
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it('resolves alias of @knapsack/utils', () => {
|
|
57
|
+
const { resolvedFileName } = resolveTsModule({
|
|
58
|
+
containingFile: import.meta.filename,
|
|
59
|
+
moduleName: 'foo',
|
|
60
|
+
pkgPathAliases: {
|
|
61
|
+
foo: '@knapsack/utils',
|
|
62
|
+
},
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
expect(resolvedFileName).toBeTruthy();
|
|
66
|
+
expect(resolvedFileName).toMatch(/\.d\.ts$/);
|
|
67
|
+
expect(resolvedFileName).toContain('utils/dist');
|
|
68
|
+
});
|
|
69
|
+
});
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/* eslint-disable @typescript-eslint/no-inferrable-types */
|
|
2
|
+
|
|
3
|
+
export const foo = 'bar';
|
|
4
|
+
export type AString = string;
|
|
5
|
+
export const stringViaTypeAlias: AString = 'foo';
|
|
6
|
+
export const aNumber: number = 1;
|
|
7
|
+
export const aString: string = 'foo';
|
|
8
|
+
export const aBoolean: boolean = true;
|
|
9
|
+
export const anArrayOfNumbers: number[] = [1, 2, 3];
|
|
10
|
+
export const anArrayOfStrings: string[] = ['foo', 'bar', 'baz'];
|
|
11
|
+
export const anArrayOfBooleans: boolean[] = [true, false, true];
|
|
12
|
+
export const stringUnion: 'small' | 'medium' | 'large' = 'small';
|
|
13
|
+
export const anObject = {
|
|
14
|
+
a: 1,
|
|
15
|
+
b: 'Beep',
|
|
16
|
+
c: ['foo', 'bar', 'baz'],
|
|
17
|
+
};
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
export const aFunction = (a: number) => a;
|
|
2
|
+
export const asyncFunction = async (a: number) => a;
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* The thing to do
|
|
6
|
+
* @param item - The item to do
|
|
7
|
+
* @link https://www.example.com
|
|
8
|
+
* @deprecated
|
|
9
|
+
*/
|
|
10
|
+
export function doIt({ item, isFast }: { isFast?: boolean; item: string }) {
|
|
11
|
+
return `${item} ${isFast ? 'fast' : 'slow'}`;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
type AString = string;
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Some basic types
|
|
18
|
+
* @link https://www.example.com
|
|
19
|
+
*/
|
|
20
|
+
export function complexFuncTypes(obj: {
|
|
21
|
+
/** ima string */
|
|
22
|
+
aString: string;
|
|
23
|
+
aStringViaType: AString;
|
|
24
|
+
aNumber: number;
|
|
25
|
+
/** an optional string */
|
|
26
|
+
anOptionalString?: string;
|
|
27
|
+
/** a boolean */
|
|
28
|
+
aBoolean: boolean;
|
|
29
|
+
/** some sizes */
|
|
30
|
+
someSizes: 'small' | 'medium' | 'large';
|
|
31
|
+
/** an array of strings */
|
|
32
|
+
anArray: string[];
|
|
33
|
+
anObject: { aString: string };
|
|
34
|
+
aFunc: (a: string) => string;
|
|
35
|
+
anotherFunc: (opt: { foo: string; onFoo: (a: string) => string }) => {
|
|
36
|
+
b: string;
|
|
37
|
+
};
|
|
38
|
+
}) {
|
|
39
|
+
return obj;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function sayHello(msg: string[]): string;
|
|
43
|
+
export function sayHello(msg: string): string;
|
|
44
|
+
export function sayHello(msg: unknown): string {
|
|
45
|
+
return `Hello ${Array.isArray(msg) ? msg.join(' ') : msg}`;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export const sayBye = <T>(msg: T): string => {
|
|
49
|
+
return `Bye ${msg}`;
|
|
50
|
+
};
|