tsrx-typegen 0.0.0 → 0.2.0
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/LICENSE +21 -0
- package/README.md +139 -0
- package/package.json +34 -5
- package/src/cli.mjs +834 -0
- package/src/pack-check.mjs +658 -0
|
@@ -0,0 +1,658 @@
|
|
|
1
|
+
import { spawnSync } from 'node:child_process'
|
|
2
|
+
import { mkdirSync, mkdtempSync, readdirSync, readFileSync, rmSync, statSync } from 'node:fs'
|
|
3
|
+
import { tmpdir } from 'node:os'
|
|
4
|
+
import { dirname, extname, isAbsolute, join, relative, resolve, sep } from 'node:path'
|
|
5
|
+
|
|
6
|
+
const moduleSpecifierNodes = (ts, sourceFile) => {
|
|
7
|
+
const nodes = []
|
|
8
|
+
const visit = (node) => {
|
|
9
|
+
if (
|
|
10
|
+
(ts.isImportDeclaration(node) || ts.isExportDeclaration(node)) &&
|
|
11
|
+
node.moduleSpecifier &&
|
|
12
|
+
ts.isStringLiteralLike(node.moduleSpecifier)
|
|
13
|
+
) {
|
|
14
|
+
nodes.push(node.moduleSpecifier)
|
|
15
|
+
} else if (ts.isImportTypeNode(node)) {
|
|
16
|
+
const argument = node.argument
|
|
17
|
+
if (ts.isLiteralTypeNode(argument) && ts.isStringLiteralLike(argument.literal)) {
|
|
18
|
+
nodes.push(argument.literal)
|
|
19
|
+
}
|
|
20
|
+
} else if (
|
|
21
|
+
ts.isExternalModuleReference(node) &&
|
|
22
|
+
node.expression &&
|
|
23
|
+
ts.isStringLiteral(node.expression)
|
|
24
|
+
) {
|
|
25
|
+
nodes.push(node.expression)
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
ts.forEachChild(node, visit)
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
visit(sourceFile)
|
|
32
|
+
return nodes
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const pathInside = (root, file) => {
|
|
36
|
+
const rel = relative(root, file)
|
|
37
|
+
return rel === '' || (rel !== '..' && !rel.startsWith(`..${sep}`) && !isAbsolute(rel))
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const stringsIn = (value) => {
|
|
41
|
+
if (typeof value === 'string') {
|
|
42
|
+
return [value]
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
if (!value || typeof value !== 'object') {
|
|
46
|
+
return []
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
return Object.values(value).flatMap(stringsIn)
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const codeTarget = (target) =>
|
|
53
|
+
/\.(?:[cm]?[jt]sx?|tsrx)$/.test(target) && !/\.d\.[cm]?ts$/.test(target)
|
|
54
|
+
|
|
55
|
+
function exportBranches(exportsMap, fallbackTypes = []) {
|
|
56
|
+
const branches = []
|
|
57
|
+
for (const [subpath, mapping] of Object.entries(exportsMap ?? {})) {
|
|
58
|
+
const visit = (value, inheritedTypes) => {
|
|
59
|
+
if (typeof value === 'string') {
|
|
60
|
+
if (codeTarget(value)) {
|
|
61
|
+
branches.push({ subpath, runtime: value, types: inheritedTypes })
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
return
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) {
|
|
68
|
+
return
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const types = Object.hasOwn(value, 'types') ? stringsIn(value.types) : inheritedTypes
|
|
72
|
+
for (const [condition, child] of Object.entries(value)) {
|
|
73
|
+
if (condition !== 'types') {
|
|
74
|
+
visit(child, types)
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
visit(mapping, fallbackTypes)
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
return branches
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function packageFiles(root) {
|
|
86
|
+
const files = []
|
|
87
|
+
const visit = (dir) => {
|
|
88
|
+
for (const entry of readdirSync(dir, { withFileTypes: true })) {
|
|
89
|
+
if (entry.name === 'node_modules') {
|
|
90
|
+
continue
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const file = join(dir, entry.name)
|
|
94
|
+
if (entry.isDirectory()) {
|
|
95
|
+
visit(file)
|
|
96
|
+
} else {
|
|
97
|
+
files.push(file)
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
visit(root)
|
|
103
|
+
return files
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function expandTarget(target, files, packageRoot) {
|
|
107
|
+
if (!target.includes('*')) {
|
|
108
|
+
return [{ path: target, capture: undefined }]
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
if ((target.match(/\*/g) ?? []).length !== 1) {
|
|
112
|
+
throw new Error(`packed export target uses more than one wildcard: ${target}`)
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
if (!target.startsWith('./')) {
|
|
116
|
+
throw new Error(`packed export target must be package-relative: ${target}`)
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const pattern = target.slice(2)
|
|
120
|
+
const marker = pattern.indexOf('*')
|
|
121
|
+
const prefix = pattern.slice(0, marker)
|
|
122
|
+
const suffix = pattern.slice(marker + 1)
|
|
123
|
+
const matches = files.flatMap((file) => {
|
|
124
|
+
const path = relative(packageRoot, file).replaceAll('\\', '/')
|
|
125
|
+
if (
|
|
126
|
+
!path.startsWith(prefix) ||
|
|
127
|
+
!path.endsWith(suffix) ||
|
|
128
|
+
path.length < prefix.length + suffix.length
|
|
129
|
+
) {
|
|
130
|
+
return []
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
const capture = path.slice(prefix.length, path.length - suffix.length)
|
|
134
|
+
return [{ path: `./${path}`, capture }]
|
|
135
|
+
})
|
|
136
|
+
|
|
137
|
+
if (!matches.length) {
|
|
138
|
+
throw new Error(`packed export target matches no files: ${target}`)
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
return matches
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function packedPath(packageRoot, target) {
|
|
145
|
+
if (typeof target !== 'string' || !target.startsWith('./') || target.includes('*')) {
|
|
146
|
+
throw new Error(`packed export target is not a concrete package-relative path: ${target}`)
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
const file = resolve(packageRoot, target)
|
|
150
|
+
if (!pathInside(packageRoot, file)) {
|
|
151
|
+
throw new Error(`packed export target escapes the package: ${target}`)
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
try {
|
|
155
|
+
if (statSync(file).isFile()) {
|
|
156
|
+
return file
|
|
157
|
+
}
|
|
158
|
+
} catch {}
|
|
159
|
+
|
|
160
|
+
throw new Error(`packed export target is missing from the tarball: ${target}`)
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function sourceFile(ts, file) {
|
|
164
|
+
const extension = extname(file).toLowerCase()
|
|
165
|
+
const scriptKind =
|
|
166
|
+
extension === '.tsx'
|
|
167
|
+
? ts.ScriptKind.TSX
|
|
168
|
+
: extension === '.jsx'
|
|
169
|
+
? ts.ScriptKind.JSX
|
|
170
|
+
: extension === '.tsrx'
|
|
171
|
+
? ts.ScriptKind.Deferred
|
|
172
|
+
: ts.ScriptKind.TS
|
|
173
|
+
|
|
174
|
+
return ts.createSourceFile(
|
|
175
|
+
file,
|
|
176
|
+
readFileSync(file, 'utf8'),
|
|
177
|
+
ts.ScriptTarget.Latest,
|
|
178
|
+
true,
|
|
179
|
+
scriptKind,
|
|
180
|
+
)
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
function localSourceModule(file, specifier) {
|
|
184
|
+
if (!specifier.startsWith('.')) {
|
|
185
|
+
return null
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
const target = resolve(dirname(file), specifier)
|
|
189
|
+
const extension = extname(target)
|
|
190
|
+
const candidates = [target]
|
|
191
|
+
if (extension === '.js' || extension === '.mjs' || extension === '.cjs') {
|
|
192
|
+
const stem = target.slice(0, -extension.length)
|
|
193
|
+
candidates.push(`${stem}.ts`, `${stem}.tsx`, `${stem}.tsrx`, `${stem}.d.ts`)
|
|
194
|
+
} else if (!['.ts', '.tsx', '.tsrx'].includes(extension)) {
|
|
195
|
+
candidates.push(`${target}.ts`, `${target}.tsx`, `${target}.tsrx`, `${target}.d.ts`)
|
|
196
|
+
candidates.push(
|
|
197
|
+
join(target, 'index.ts'),
|
|
198
|
+
join(target, 'index.tsx'),
|
|
199
|
+
join(target, 'index.tsrx'),
|
|
200
|
+
join(target, 'index.d.ts'),
|
|
201
|
+
)
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
for (const candidate of candidates) {
|
|
205
|
+
try {
|
|
206
|
+
if (statSync(candidate).isFile()) {
|
|
207
|
+
return candidate
|
|
208
|
+
}
|
|
209
|
+
} catch {}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
return null
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
function staticRuntimeExports(ts, file, seen = new Set()) {
|
|
216
|
+
const absolute = resolve(file)
|
|
217
|
+
if (seen.has(absolute)) {
|
|
218
|
+
return new Set()
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
seen.add(absolute)
|
|
222
|
+
|
|
223
|
+
const source = sourceFile(ts, absolute)
|
|
224
|
+
const names = new Set()
|
|
225
|
+
const addBindingName = (name) => {
|
|
226
|
+
if (ts.isIdentifier(name)) {
|
|
227
|
+
names.add(name.text)
|
|
228
|
+
} else if (ts.isObjectBindingPattern(name) || ts.isArrayBindingPattern(name)) {
|
|
229
|
+
for (const element of name.elements) {
|
|
230
|
+
if (element && ts.isBindingElement(element)) {
|
|
231
|
+
addBindingName(element.name)
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
for (const statement of source.statements) {
|
|
238
|
+
const modifiers = ts.canHaveModifiers(statement) ? (ts.getModifiers(statement) ?? []) : []
|
|
239
|
+
const exported = modifiers.some((modifier) => modifier.kind === ts.SyntaxKind.ExportKeyword)
|
|
240
|
+
const isDefault = modifiers.some((modifier) => modifier.kind === ts.SyntaxKind.DefaultKeyword)
|
|
241
|
+
if (isDefault) {
|
|
242
|
+
names.add('default')
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
if (exported) {
|
|
246
|
+
if (
|
|
247
|
+
ts.isFunctionDeclaration(statement) ||
|
|
248
|
+
ts.isClassDeclaration(statement) ||
|
|
249
|
+
ts.isEnumDeclaration(statement) ||
|
|
250
|
+
ts.isModuleDeclaration(statement)
|
|
251
|
+
) {
|
|
252
|
+
if (statement.name) {
|
|
253
|
+
names.add(statement.name.text)
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
if (ts.isVariableStatement(statement)) {
|
|
258
|
+
for (const declaration of statement.declarationList.declarations) {
|
|
259
|
+
addBindingName(declaration.name)
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
if (ts.isExportDeclaration(statement) && !statement.isTypeOnly) {
|
|
265
|
+
if (!statement.exportClause && statement.moduleSpecifier && ts.isStringLiteralLike(statement.moduleSpecifier)) {
|
|
266
|
+
const target = localSourceModule(absolute, statement.moduleSpecifier.text)
|
|
267
|
+
if (target) {
|
|
268
|
+
for (const name of staticRuntimeExports(ts, target, seen)) {
|
|
269
|
+
if (name !== 'default') {
|
|
270
|
+
names.add(name)
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
} else if (statement.exportClause && ts.isNamedExports(statement.exportClause)) {
|
|
275
|
+
for (const element of statement.exportClause.elements) {
|
|
276
|
+
if (!element.isTypeOnly) {
|
|
277
|
+
names.add(element.name.text)
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
} else if (statement.exportClause && ts.isNamespaceExport(statement.exportClause)) {
|
|
281
|
+
names.add(statement.exportClause.name.text)
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
if (ts.isExportAssignment(statement)) {
|
|
286
|
+
names.add('default')
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
return names
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
function compilerExports(ts, program, file, cache = new Map(), pending = new Set()) {
|
|
294
|
+
const absolute = resolve(file)
|
|
295
|
+
if (cache.has(absolute)) {
|
|
296
|
+
return cache.get(absolute)
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
if (pending.has(absolute)) {
|
|
300
|
+
return { all: new Set(), values: new Set() }
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
const source = program.getSourceFile(file)
|
|
304
|
+
if (!source) {
|
|
305
|
+
return undefined
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
const checker = program.getTypeChecker()
|
|
309
|
+
const module = checker.getSymbolAtLocation(source)
|
|
310
|
+
if (!module) {
|
|
311
|
+
return undefined
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
const all = new Set()
|
|
315
|
+
const values = new Set()
|
|
316
|
+
for (const symbol of checker.getExportsOfModule(module)) {
|
|
317
|
+
all.add(symbol.name)
|
|
318
|
+
let resolved = symbol
|
|
319
|
+
if (symbol.flags & ts.SymbolFlags.Alias) {
|
|
320
|
+
try {
|
|
321
|
+
resolved = checker.getAliasedSymbol(symbol)
|
|
322
|
+
} catch {}
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
if (resolved.flags & ts.SymbolFlags.Value) {
|
|
326
|
+
values.add(symbol.name)
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
// Type-only edges (`export type *`, `export type { X }`) re-list the
|
|
331
|
+
// target's symbols with their original flags — an ambient `declare const`
|
|
332
|
+
// still reads as a value even though no value binding crosses the edge.
|
|
333
|
+
// Subtract names whose only export edges are type-only.
|
|
334
|
+
const typeOnly = new Set()
|
|
335
|
+
for (const statement of source.statements) {
|
|
336
|
+
if (!ts.isExportDeclaration(statement) || !statement.isTypeOnly) {
|
|
337
|
+
continue
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
if (
|
|
341
|
+
!statement.exportClause &&
|
|
342
|
+
statement.moduleSpecifier &&
|
|
343
|
+
ts.isStringLiteralLike(statement.moduleSpecifier)
|
|
344
|
+
) {
|
|
345
|
+
pending.add(absolute)
|
|
346
|
+
const target = ts.resolveModuleName(
|
|
347
|
+
statement.moduleSpecifier.text,
|
|
348
|
+
file,
|
|
349
|
+
program.getCompilerOptions(),
|
|
350
|
+
ts.sys,
|
|
351
|
+
).resolvedModule?.resolvedFileName
|
|
352
|
+
|
|
353
|
+
const targetExports =
|
|
354
|
+
target && compilerExports(ts, program, target, cache, pending)
|
|
355
|
+
|
|
356
|
+
pending.delete(absolute)
|
|
357
|
+
for (const name of targetExports?.all ?? []) {
|
|
358
|
+
typeOnly.add(name)
|
|
359
|
+
}
|
|
360
|
+
} else if (statement.exportClause && ts.isNamedExports(statement.exportClause)) {
|
|
361
|
+
for (const element of statement.exportClause.elements) {
|
|
362
|
+
typeOnly.add(element.name.text)
|
|
363
|
+
}
|
|
364
|
+
} else if (statement.exportClause && ts.isNamespaceExport(statement.exportClause)) {
|
|
365
|
+
typeOnly.add(statement.exportClause.name.text)
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
if (typeOnly.size) {
|
|
370
|
+
const valueEdges = staticRuntimeExports(ts, file)
|
|
371
|
+
for (const name of values) {
|
|
372
|
+
if (typeOnly.has(name) && !valueEdges.has(name)) {
|
|
373
|
+
values.delete(name)
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
const result = { all, values }
|
|
379
|
+
cache.set(absolute, result)
|
|
380
|
+
return result
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
function declarationFiles(packageRoot, files) {
|
|
384
|
+
return files.filter((file) => /\.d\.[cm]?ts$/.test(relative(packageRoot, file)))
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
function moduleResolutionOptions(ts) {
|
|
388
|
+
return {
|
|
389
|
+
allowImportingTsExtensions: true,
|
|
390
|
+
module: ts.ModuleKind.NodeNext,
|
|
391
|
+
moduleResolution: ts.ModuleResolutionKind.NodeNext,
|
|
392
|
+
noEmit: true,
|
|
393
|
+
skipLibCheck: true,
|
|
394
|
+
target: ts.ScriptTarget.Latest,
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
function verifyDeclarationClosure(ts, packageRoot, files, manifest, failures) {
|
|
399
|
+
const options = moduleResolutionOptions(ts)
|
|
400
|
+
const host = ts.sys
|
|
401
|
+
const declared = new Set([
|
|
402
|
+
manifest.name,
|
|
403
|
+
...Object.keys(manifest.dependencies ?? {}),
|
|
404
|
+
...Object.keys(manifest.optionalDependencies ?? {}),
|
|
405
|
+
...Object.keys(manifest.peerDependencies ?? {}),
|
|
406
|
+
...(manifest.bundledDependencies ?? []),
|
|
407
|
+
])
|
|
408
|
+
|
|
409
|
+
for (const file of declarationFiles(packageRoot, files)) {
|
|
410
|
+
const source = sourceFile(ts, file)
|
|
411
|
+
for (const node of moduleSpecifierNodes(ts, source)) {
|
|
412
|
+
const specifier = node.text
|
|
413
|
+
if (specifier.startsWith('.') || specifier.startsWith('#')) {
|
|
414
|
+
const resolved = ts.resolveModuleName(specifier, file, options, host).resolvedModule
|
|
415
|
+
if (!resolved) {
|
|
416
|
+
failures.push(
|
|
417
|
+
`${relative(packageRoot, file)} has unresolved declaration reference ${specifier}`,
|
|
418
|
+
)
|
|
419
|
+
} else if (!pathInside(packageRoot, resolve(resolved.resolvedFileName))) {
|
|
420
|
+
failures.push(`${relative(packageRoot, file)} resolves ${specifier} outside the package`)
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
continue
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
if (specifier.startsWith('node:')) {
|
|
427
|
+
if (!declared.has('@types/node')) {
|
|
428
|
+
failures.push(
|
|
429
|
+
`${relative(packageRoot, file)} imports ${specifier}; declare @types/node as a dependency or peer`,
|
|
430
|
+
)
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
continue
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
if (specifier.startsWith('/')) {
|
|
437
|
+
failures.push(
|
|
438
|
+
`${relative(packageRoot, file)} has absolute declaration reference ${specifier}`,
|
|
439
|
+
)
|
|
440
|
+
|
|
441
|
+
continue
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
const name = specifier.startsWith('@')
|
|
445
|
+
? specifier.split('/').slice(0, 2).join('/')
|
|
446
|
+
: specifier.split('/')[0]
|
|
447
|
+
|
|
448
|
+
if (!declared.has(name)) {
|
|
449
|
+
failures.push(
|
|
450
|
+
`${relative(packageRoot, file)} imports ${specifier}; declare ${name} in dependencies or peerDependencies`,
|
|
451
|
+
)
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
function verifyExportMap(ts, packageRoot, files, manifest, exportsMap, failures, options) {
|
|
458
|
+
const fallbackTypes = [manifest.types ?? manifest.typings].filter(Boolean)
|
|
459
|
+
const branches = exportBranches(exportsMap, fallbackTypes)
|
|
460
|
+
if (!branches.length) {
|
|
461
|
+
failures.push('package exports contain no runtime entrypoints to verify')
|
|
462
|
+
return
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
const pairs = []
|
|
466
|
+
for (const branch of branches) {
|
|
467
|
+
if (!branch.types.length) {
|
|
468
|
+
failures.push(`${branch.subpath} runtime ${branch.runtime} has no types condition`)
|
|
469
|
+
continue
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
let runtimeMatches
|
|
473
|
+
try {
|
|
474
|
+
runtimeMatches = expandTarget(branch.runtime, files, packageRoot)
|
|
475
|
+
} catch (error) {
|
|
476
|
+
failures.push(`${branch.subpath}: ${error.message}`)
|
|
477
|
+
continue
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
for (const runtimeMatch of runtimeMatches) {
|
|
481
|
+
let runtimeFile
|
|
482
|
+
try {
|
|
483
|
+
runtimeFile = packedPath(packageRoot, runtimeMatch.path)
|
|
484
|
+
} catch (error) {
|
|
485
|
+
failures.push(`${branch.subpath}: ${error.message}`)
|
|
486
|
+
continue
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
for (const typeTarget of branch.types) {
|
|
490
|
+
const concreteType =
|
|
491
|
+
typeTarget.includes('*') && runtimeMatch.capture !== undefined
|
|
492
|
+
? typeTarget.replace('*', runtimeMatch.capture)
|
|
493
|
+
: typeTarget
|
|
494
|
+
|
|
495
|
+
let typeFile
|
|
496
|
+
try {
|
|
497
|
+
typeFile = packedPath(packageRoot, concreteType)
|
|
498
|
+
} catch (error) {
|
|
499
|
+
failures.push(`${branch.subpath}: ${error.message}`)
|
|
500
|
+
continue
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
pairs.push({ branch, runtimeFile, runtimePath: runtimeMatch.path, typeFile, concreteType })
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
const programRoots = [
|
|
509
|
+
...new Set(pairs.flatMap(({ runtimeFile, typeFile }) => [runtimeFile, typeFile])),
|
|
510
|
+
].filter((file) => !file.endsWith('.tsrx'))
|
|
511
|
+
|
|
512
|
+
const program = ts.createProgram(programRoots, options)
|
|
513
|
+
for (const pair of pairs) {
|
|
514
|
+
const declarationExports = compilerExports(ts, program, pair.typeFile)
|
|
515
|
+
const runtimeExports = compilerExports(ts, program, pair.runtimeFile)
|
|
516
|
+
const declaredValues = declarationExports?.values ?? new Set()
|
|
517
|
+
let runtimeValues
|
|
518
|
+
if (
|
|
519
|
+
pair.runtimeFile.endsWith('.ts') ||
|
|
520
|
+
pair.runtimeFile.endsWith('.tsx') ||
|
|
521
|
+
pair.runtimeFile.endsWith('.tsrx')
|
|
522
|
+
) {
|
|
523
|
+
runtimeValues = staticRuntimeExports(ts, pair.runtimeFile)
|
|
524
|
+
for (const name of declarationExports?.all ?? []) {
|
|
525
|
+
if (!declaredValues.has(name)) {
|
|
526
|
+
runtimeValues.delete(name)
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
} else {
|
|
530
|
+
runtimeValues = runtimeExports?.values ?? staticRuntimeExports(ts, pair.runtimeFile)
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
const missing = [...runtimeValues].filter((name) => !declaredValues.has(name)).sort()
|
|
534
|
+
const extra = [...declaredValues].filter((name) => !runtimeValues.has(name)).sort()
|
|
535
|
+
if (missing.length || extra.length) {
|
|
536
|
+
failures.push(
|
|
537
|
+
`${pair.branch.subpath} runtime/type value exports differ for ${pair.runtimePath} → ${pair.concreteType}` +
|
|
538
|
+
`${missing.length ? `; missing declarations: ${missing.join(', ')}` : ''}` +
|
|
539
|
+
`${extra.length ? `; declarations without runtime values: ${extra.join(', ')}` : ''}`,
|
|
540
|
+
)
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
export function verifyPackedPackage(ts, packageRoot) {
|
|
546
|
+
const failures = []
|
|
547
|
+
const manifestPath = join(packageRoot, 'package.json')
|
|
548
|
+
let manifest
|
|
549
|
+
try {
|
|
550
|
+
manifest = JSON.parse(readFileSync(manifestPath, 'utf8'))
|
|
551
|
+
} catch (error) {
|
|
552
|
+
throw new Error(`packed package.json: ${error.message}`)
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
const files = packageFiles(packageRoot)
|
|
556
|
+
const maps = [manifest.exports, manifest.publishConfig?.exports].filter(
|
|
557
|
+
(value, index, all) => value && all.indexOf(value) === index,
|
|
558
|
+
)
|
|
559
|
+
|
|
560
|
+
if (maps.length) {
|
|
561
|
+
for (const exportsMap of maps) {
|
|
562
|
+
verifyExportMap(
|
|
563
|
+
ts,
|
|
564
|
+
packageRoot,
|
|
565
|
+
files,
|
|
566
|
+
manifest,
|
|
567
|
+
exportsMap,
|
|
568
|
+
failures,
|
|
569
|
+
moduleResolutionOptions(ts),
|
|
570
|
+
)
|
|
571
|
+
}
|
|
572
|
+
} else if (manifest.main || manifest.module) {
|
|
573
|
+
verifyExportMap(
|
|
574
|
+
ts,
|
|
575
|
+
packageRoot,
|
|
576
|
+
files,
|
|
577
|
+
manifest,
|
|
578
|
+
{ '.': manifest.module ?? manifest.main },
|
|
579
|
+
failures,
|
|
580
|
+
moduleResolutionOptions(ts),
|
|
581
|
+
)
|
|
582
|
+
} else {
|
|
583
|
+
failures.push('package has no exports, main, or module entrypoint')
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
verifyDeclarationClosure(ts, packageRoot, files, manifest, failures)
|
|
587
|
+
if (failures.length) {
|
|
588
|
+
throw new Error(failures.join('\n'))
|
|
589
|
+
}
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
export function checkPackedPackage(projectRoot, ts) {
|
|
593
|
+
const temporary = mkdtempSync(join(tmpdir(), 'tsrx-typegen-pack-'))
|
|
594
|
+
const packOutput = join(temporary, 'pack')
|
|
595
|
+
const extractedRoot = join(temporary, 'extracted')
|
|
596
|
+
mkdirSync(packOutput)
|
|
597
|
+
mkdirSync(extractedRoot)
|
|
598
|
+
try {
|
|
599
|
+
verifyPackedPackage(ts, projectRoot)
|
|
600
|
+
console.log('tsrx-typegen: workspace exports and declaration graph are valid')
|
|
601
|
+
const packed = spawnSync(
|
|
602
|
+
'pnpm',
|
|
603
|
+
['--config.ignore-scripts=true', 'pack', '--pack-destination', packOutput],
|
|
604
|
+
{ cwd: projectRoot, encoding: 'utf8', maxBuffer: 16 * 1024 * 1024 },
|
|
605
|
+
)
|
|
606
|
+
|
|
607
|
+
if (packed.stdout) {
|
|
608
|
+
process.stdout.write(packed.stdout)
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
if (packed.stderr) {
|
|
612
|
+
process.stderr.write(packed.stderr)
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
if (packed.error) {
|
|
616
|
+
throw packed.error
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
if (packed.status !== 0) {
|
|
620
|
+
throw new Error(`pnpm pack exited with ${packed.status}`)
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
const tarballs = readdirSync(packOutput).filter((name) => name.endsWith('.tgz'))
|
|
624
|
+
if (tarballs.length !== 1) {
|
|
625
|
+
throw new Error(`expected one package tarball; found ${tarballs.length}`)
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
const extracted = spawnSync(
|
|
629
|
+
'tar',
|
|
630
|
+
['-xzf', join(packOutput, tarballs[0]), '-C', extractedRoot],
|
|
631
|
+
{
|
|
632
|
+
encoding: 'utf8',
|
|
633
|
+
maxBuffer: 16 * 1024 * 1024,
|
|
634
|
+
},
|
|
635
|
+
)
|
|
636
|
+
|
|
637
|
+
if (extracted.stdout) {
|
|
638
|
+
process.stdout.write(extracted.stdout)
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
if (extracted.stderr) {
|
|
642
|
+
process.stderr.write(extracted.stderr)
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
if (extracted.error) {
|
|
646
|
+
throw extracted.error
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
if (extracted.status !== 0) {
|
|
650
|
+
throw new Error(`tar extraction exited with ${extracted.status}`)
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
verifyPackedPackage(ts, join(extractedRoot, 'package'))
|
|
654
|
+
console.log('tsrx-typegen: packed runtime exports and declaration graph are valid')
|
|
655
|
+
} finally {
|
|
656
|
+
rmSync(temporary, { recursive: true, force: true })
|
|
657
|
+
}
|
|
658
|
+
}
|