tsrx-typegen 0.1.0 → 0.2.1
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 +27 -8
- package/package.json +13 -2
- package/src/cli.mjs +204 -64
- package/src/pack-check.mjs +73 -6
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
The MIT License (MIT)
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Alec Larson
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in
|
|
13
|
+
all copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
|
21
|
+
THE SOFTWARE.
|
package/README.md
CHANGED
|
@@ -28,6 +28,16 @@ and removes stale files only when the manifest says it owns them. Same-name
|
|
|
28
28
|
sources such as `Button.ts` and `Button.tsrx` need separate target configs or an
|
|
29
29
|
explicit declaration override.
|
|
30
30
|
|
|
31
|
+
Treat the output directory, including its ownership manifest, as generated
|
|
32
|
+
build artifacts: ignore it in Git, formatting, and linting. Keep handwritten
|
|
33
|
+
declarations and override sources outside that directory. Include the output
|
|
34
|
+
directory in the package's `files` list so ignored declarations still ship in
|
|
35
|
+
the tarball. Generate before local consumer typechecks, and regenerate in
|
|
36
|
+
`prepack` before validating the package. Workspaces that resolve generated
|
|
37
|
+
types directly also need generation during setup and refreshes during
|
|
38
|
+
development; this repository's workflow is documented in
|
|
39
|
+
[Develop against generated declarations](../../docs/toolchain-notes.md#develop-against-generated-declarations).
|
|
40
|
+
|
|
31
41
|
`--project` selects the target whose project path matches. Use `--target` to
|
|
32
42
|
select a named target directly. A target can look like this:
|
|
33
43
|
|
|
@@ -46,7 +56,7 @@ select a named target directly. A target can look like this:
|
|
|
46
56
|
}
|
|
47
57
|
}
|
|
48
58
|
}
|
|
49
|
-
|
|
59
|
+
}
|
|
50
60
|
}
|
|
51
61
|
```
|
|
52
62
|
|
|
@@ -55,9 +65,9 @@ generated directory, then map their generated output path explicitly:
|
|
|
55
65
|
|
|
56
66
|
```json
|
|
57
67
|
{
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
68
|
+
"overrides": {
|
|
69
|
+
"Button.d.ts": "types/overrides/Button.d.ts"
|
|
70
|
+
}
|
|
61
71
|
}
|
|
62
72
|
```
|
|
63
73
|
|
|
@@ -91,14 +101,23 @@ bare package imports are declared dependencies or peers. The command needs
|
|
|
91
101
|
TypeScript consumer test for the module-resolution modes and public prop
|
|
92
102
|
contracts your package supports.
|
|
93
103
|
|
|
104
|
+
A target with `"emit": false` exists only to drive packed-package
|
|
105
|
+
verification. Packages whose declarations are handwritten or produced by
|
|
106
|
+
another tool declare one so `--pack-check` skips generation and freshness
|
|
107
|
+
checks for it and only validates the packed package. Such targets cannot be
|
|
108
|
+
selected for generation; a plain `--target` run against them fails. Every code
|
|
109
|
+
export path still needs a `types` condition — pointing at a `.d.ts` file or,
|
|
110
|
+
for source-published packages, the `.ts` source itself.
|
|
111
|
+
|
|
94
112
|
Run the same gate from `prepack` so `pnpm pack` and publication share it:
|
|
95
113
|
|
|
96
114
|
```json
|
|
97
115
|
{
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
116
|
+
"scripts": {
|
|
117
|
+
"typegen": "tsrx-typegen --target octane",
|
|
118
|
+
"build": "vite build && pnpm typegen",
|
|
119
|
+
"prepack": "pnpm build && tsrx-typegen --pack-check"
|
|
120
|
+
}
|
|
102
121
|
}
|
|
103
122
|
```
|
|
104
123
|
|
package/package.json
CHANGED
|
@@ -1,10 +1,21 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "tsrx-typegen",
|
|
3
|
-
"version": "0.1
|
|
3
|
+
"version": "0.2.1",
|
|
4
4
|
"description": "Generate consumer-ready TypeScript declarations for TSRX libraries",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"build-tool",
|
|
7
|
+
"codegen",
|
|
8
|
+
"declarations",
|
|
9
|
+
"dts",
|
|
10
|
+
"tsrx",
|
|
11
|
+
"typegen",
|
|
12
|
+
"typescript"
|
|
13
|
+
],
|
|
14
|
+
"homepage": "https://github.com/octane-xplat/octane-xplat/tree/main/packages/tsrx-typegen",
|
|
15
|
+
"license": "MIT",
|
|
5
16
|
"repository": {
|
|
6
17
|
"type": "git",
|
|
7
|
-
"url": "
|
|
18
|
+
"url": "https://github.com/octane-xplat/octane-xplat",
|
|
8
19
|
"directory": "packages/tsrx-typegen"
|
|
9
20
|
},
|
|
10
21
|
"bin": {
|
package/src/cli.mjs
CHANGED
|
@@ -36,6 +36,7 @@ Options:
|
|
|
36
36
|
--target <name> Target in tsrx-typegen.json (selects its project and output)
|
|
37
37
|
--check Compare generated declarations without writing files
|
|
38
38
|
--pack-check Check declarations, pack the package, and validate the tarball
|
|
39
|
+
(targets with emit: false skip generation and only validate)
|
|
39
40
|
-h, --help Show this help`)
|
|
40
41
|
}
|
|
41
42
|
|
|
@@ -91,7 +92,12 @@ function configuredTargetNames(projectRoot, selectedTarget) {
|
|
|
91
92
|
}
|
|
92
93
|
|
|
93
94
|
const targets = config?.targets
|
|
94
|
-
if (
|
|
95
|
+
if (
|
|
96
|
+
!targets ||
|
|
97
|
+
typeof targets !== 'object' ||
|
|
98
|
+
Array.isArray(targets) ||
|
|
99
|
+
!Object.keys(targets).length
|
|
100
|
+
) {
|
|
95
101
|
throw new Error(`${path}: --pack-check requires at least one configured target`)
|
|
96
102
|
}
|
|
97
103
|
|
|
@@ -99,7 +105,7 @@ function configuredTargetNames(projectRoot, selectedTarget) {
|
|
|
99
105
|
throw new Error(`${path}: unknown target ${selectedTarget}`)
|
|
100
106
|
}
|
|
101
107
|
|
|
102
|
-
return selectedTarget ? [selectedTarget] : Object.keys(targets)
|
|
108
|
+
return { names: selectedTarget ? [selectedTarget] : Object.keys(targets), targets }
|
|
103
109
|
}
|
|
104
110
|
|
|
105
111
|
function loadProjectModule(projectRoot, specifier) {
|
|
@@ -123,16 +129,24 @@ function readProjectConfig(ts, configPath, projectRoot, target) {
|
|
|
123
129
|
[{ extension: '.tsrx', isMixedContent: true, scriptKind: ts.ScriptKind.Deferred }],
|
|
124
130
|
)
|
|
125
131
|
|
|
126
|
-
if (!parsed) {
|
|
132
|
+
if (!parsed) {
|
|
133
|
+
throw new Error(`Could not read ${configPath}`)
|
|
134
|
+
}
|
|
135
|
+
|
|
127
136
|
const errors = parsed.errors.map((diagnostic) =>
|
|
128
137
|
ts.flattenDiagnosticMessageText(diagnostic.messageText, '\n'),
|
|
129
138
|
)
|
|
130
139
|
|
|
131
|
-
if (errors.length) {
|
|
140
|
+
if (errors.length) {
|
|
141
|
+
throw new Error(errors.join('\n'))
|
|
142
|
+
}
|
|
143
|
+
|
|
132
144
|
const options = parsed.options
|
|
133
145
|
const jsxRenderer =
|
|
134
146
|
options.jsxImportSource ??
|
|
135
|
-
(options.jsx === ts.JsxEmit.ReactJSX || options.jsx === ts.JsxEmit.ReactJSXDev
|
|
147
|
+
(options.jsx === ts.JsxEmit.ReactJSX || options.jsx === ts.JsxEmit.ReactJSXDev
|
|
148
|
+
? 'react'
|
|
149
|
+
: undefined)
|
|
136
150
|
|
|
137
151
|
if (target?.renderer && jsxRenderer && target.renderer !== jsxRenderer) {
|
|
138
152
|
throw new Error(
|
|
@@ -148,7 +162,9 @@ function readProjectConfig(ts, configPath, projectRoot, target) {
|
|
|
148
162
|
|
|
149
163
|
const outputDirectory = target?.outDir ?? options.declarationDir ?? options.outDir
|
|
150
164
|
if (!outputDirectory) {
|
|
151
|
-
throw new Error(
|
|
165
|
+
throw new Error(
|
|
166
|
+
`${configPath} must set target outDir, compilerOptions.outDir, or declarationDir.`,
|
|
167
|
+
)
|
|
152
168
|
}
|
|
153
169
|
|
|
154
170
|
return {
|
|
@@ -162,7 +178,10 @@ function readProjectConfig(ts, configPath, projectRoot, target) {
|
|
|
162
178
|
}
|
|
163
179
|
|
|
164
180
|
function commonDirectory(paths) {
|
|
165
|
-
if (!paths.length) {
|
|
181
|
+
if (!paths.length) {
|
|
182
|
+
throw new Error('TypeScript project has no root files.')
|
|
183
|
+
}
|
|
184
|
+
|
|
166
185
|
const segments = paths.map((path) => resolve(dirname(path)).split(sep))
|
|
167
186
|
const common = []
|
|
168
187
|
for (let index = 0; segments.every((parts) => parts[index] === segments[0][index]); index += 1) {
|
|
@@ -177,7 +196,10 @@ function sourceDeclarationMap(rootDir, fileNames, extensions, overrides) {
|
|
|
177
196
|
const runtimeByDeclaration = new Map()
|
|
178
197
|
for (const source of fileNames) {
|
|
179
198
|
const sourceExtension = extname(source).toLowerCase()
|
|
180
|
-
if (!Object.hasOwn(extensions, sourceExtension) || /\.d\.[cm]?ts$/i.test(source)) {
|
|
199
|
+
if (!Object.hasOwn(extensions, sourceExtension) || /\.d\.[cm]?ts$/i.test(source)) {
|
|
200
|
+
continue
|
|
201
|
+
}
|
|
202
|
+
|
|
181
203
|
const sourcePath = relative(rootDir, source)
|
|
182
204
|
if (!sourcePath || sourcePath.startsWith(`..${sep}`) || isAbsolute(sourcePath)) {
|
|
183
205
|
throw new Error(`Source file is outside compilerOptions.rootDir: ${source}`)
|
|
@@ -186,7 +208,10 @@ function sourceDeclarationMap(rootDir, fileNames, extensions, overrides) {
|
|
|
186
208
|
const declarationExtension =
|
|
187
209
|
sourceExtension === '.mts' ? '.d.mts' : sourceExtension === '.cts' ? '.d.cts' : '.d.ts'
|
|
188
210
|
|
|
189
|
-
const declaration = normalize(
|
|
211
|
+
const declaration = normalize(
|
|
212
|
+
sourcePath.slice(0, -sourceExtension.length) + declarationExtension,
|
|
213
|
+
)
|
|
214
|
+
|
|
190
215
|
const prior = sourceOutputs.get(declaration)
|
|
191
216
|
if (prior && !overrides.has(declaration)) {
|
|
192
217
|
throw new Error(
|
|
@@ -210,7 +235,10 @@ function configOptions(projectRoot, projectArgument, targetArgument) {
|
|
|
210
235
|
config = JSON.parse(readFileSync(path, 'utf8'))
|
|
211
236
|
} catch (error) {
|
|
212
237
|
if (error.code === 'ENOENT') {
|
|
213
|
-
if (targetArgument) {
|
|
238
|
+
if (targetArgument) {
|
|
239
|
+
throw new Error(`No tsrx-typegen.json defines target ${targetArgument}.`)
|
|
240
|
+
}
|
|
241
|
+
|
|
214
242
|
return {
|
|
215
243
|
project: projectArgument,
|
|
216
244
|
target: null,
|
|
@@ -234,7 +262,10 @@ function configOptions(projectRoot, projectArgument, targetArgument) {
|
|
|
234
262
|
const resolveProject = (value) => resolve(projectRoot, value)
|
|
235
263
|
let selectedName = targetArgument
|
|
236
264
|
let selected = selectedName ? targets[selectedName] : undefined
|
|
237
|
-
if (selectedName && !selected) {
|
|
265
|
+
if (selectedName && !selected) {
|
|
266
|
+
throw new Error(`${path}: unknown target ${selectedName}`)
|
|
267
|
+
}
|
|
268
|
+
|
|
238
269
|
if (!selected && projectArgument) {
|
|
239
270
|
const matches = Object.entries(targets).filter(
|
|
240
271
|
([, target]) => target?.project && resolveProject(target.project) === projectArgument,
|
|
@@ -244,13 +275,20 @@ function configOptions(projectRoot, projectArgument, targetArgument) {
|
|
|
244
275
|
throw new Error(`${path}: project matches multiple targets; pass --target <name>`)
|
|
245
276
|
}
|
|
246
277
|
|
|
247
|
-
if (matches.length === 1) {
|
|
248
|
-
|
|
278
|
+
if (matches.length === 1) {
|
|
279
|
+
;[selectedName, selected] = matches[0]
|
|
280
|
+
} else if (Object.keys(targets).length) {
|
|
249
281
|
throw new Error(`${path}: ${projectArgument} is not declared by a target`)
|
|
250
282
|
}
|
|
251
283
|
}
|
|
252
284
|
|
|
253
285
|
if (selected) {
|
|
286
|
+
if (selected.emit === false) {
|
|
287
|
+
throw new Error(
|
|
288
|
+
`${path}: target ${selectedName} has emit: false — it configures --pack-check only`,
|
|
289
|
+
)
|
|
290
|
+
}
|
|
291
|
+
|
|
254
292
|
if (typeof selected.project !== 'string' || !selected.project) {
|
|
255
293
|
throw new Error(`${path}: target ${selectedName} must name a project`)
|
|
256
294
|
}
|
|
@@ -277,10 +315,12 @@ function configOptions(projectRoot, projectArgument, targetArgument) {
|
|
|
277
315
|
!entry ||
|
|
278
316
|
typeof entry !== 'object' ||
|
|
279
317
|
typeof entry.source !== 'string' ||
|
|
280
|
-
|
|
318
|
+
!stringsIn(entry.runtime).length ||
|
|
281
319
|
!stringsIn(entry.types).length
|
|
282
320
|
) {
|
|
283
|
-
throw new Error(
|
|
321
|
+
throw new Error(
|
|
322
|
+
`${path}: target ${selectedName} entrypoint ${subpath} needs source, runtime, and types`,
|
|
323
|
+
)
|
|
284
324
|
}
|
|
285
325
|
|
|
286
326
|
for (const publishedPath of [...stringsIn(entry.runtime), ...stringsIn(entry.types)]) {
|
|
@@ -299,7 +339,9 @@ function configOptions(projectRoot, projectArgument, targetArgument) {
|
|
|
299
339
|
} catch {}
|
|
300
340
|
|
|
301
341
|
if (!pathInside(projectRoot, source) || !isFile) {
|
|
302
|
-
throw new Error(
|
|
342
|
+
throw new Error(
|
|
343
|
+
`${path}: target ${selectedName} entrypoint source is missing: ${entry.source}`,
|
|
344
|
+
)
|
|
303
345
|
}
|
|
304
346
|
}
|
|
305
347
|
|
|
@@ -320,10 +362,17 @@ function configOptions(projectRoot, projectArgument, targetArgument) {
|
|
|
320
362
|
}
|
|
321
363
|
|
|
322
364
|
const project = projectArgument ?? resolveProject(selected.project)
|
|
323
|
-
const sourceExtensions = {
|
|
365
|
+
const sourceExtensions = {
|
|
366
|
+
...defaultExtensions,
|
|
367
|
+
...config.sourceExtensions,
|
|
368
|
+
...selected?.sourceExtensions,
|
|
369
|
+
}
|
|
370
|
+
|
|
324
371
|
for (const [source, runtime] of Object.entries(sourceExtensions)) {
|
|
325
372
|
if (!source.startsWith('.') || typeof runtime !== 'string' || !runtime.startsWith('.')) {
|
|
326
|
-
throw new Error(
|
|
373
|
+
throw new Error(
|
|
374
|
+
`${path}: sourceExtensions must map dot-prefixed extensions to dot-prefixed runtime extensions`,
|
|
375
|
+
)
|
|
327
376
|
}
|
|
328
377
|
}
|
|
329
378
|
|
|
@@ -373,7 +422,10 @@ function applyOverrides(ts, projectRoot, outputDir, generated, overrides) {
|
|
|
373
422
|
normalized.set(outputFile, content)
|
|
374
423
|
}
|
|
375
424
|
|
|
376
|
-
for (const [outputPath, content] of normalized) {
|
|
425
|
+
for (const [outputPath, content] of normalized) {
|
|
426
|
+
generated.set(outputPath, content)
|
|
427
|
+
}
|
|
428
|
+
|
|
377
429
|
return new Set(normalized.keys())
|
|
378
430
|
}
|
|
379
431
|
|
|
@@ -385,8 +437,11 @@ function collectFiles(root, output = []) {
|
|
|
385
437
|
|
|
386
438
|
for (const entry of readdirSync(root, { withFileTypes: true })) {
|
|
387
439
|
const path = join(root, entry.name)
|
|
388
|
-
if (entry.isDirectory()) {
|
|
389
|
-
|
|
440
|
+
if (entry.isDirectory()) {
|
|
441
|
+
collectFiles(path, output)
|
|
442
|
+
} else if (entry.isFile()) {
|
|
443
|
+
output.push(path)
|
|
444
|
+
}
|
|
390
445
|
}
|
|
391
446
|
|
|
392
447
|
return output
|
|
@@ -423,7 +478,10 @@ function moduleSpecifierNodes(ts, sourceFile) {
|
|
|
423
478
|
|
|
424
479
|
function assertNoTsrxModuleReferences(ts, path, text) {
|
|
425
480
|
const source = ts.createSourceFile(path, text, ts.ScriptTarget.Latest, true, ts.ScriptKind.TS)
|
|
426
|
-
const unresolved = moduleSpecifierNodes(ts, source).find((node) =>
|
|
481
|
+
const unresolved = moduleSpecifierNodes(ts, source).find((node) =>
|
|
482
|
+
/\.tsrx(?:$|[?#])/.test(node.text),
|
|
483
|
+
)
|
|
484
|
+
|
|
427
485
|
if (unresolved) {
|
|
428
486
|
throw new Error(`${path}: unresolved .tsrx module specifier ${unresolved.text}`)
|
|
429
487
|
}
|
|
@@ -435,8 +493,14 @@ function rewriteDeclaration(ts, path, outputDir, extensions, generatedFiles, run
|
|
|
435
493
|
const edits = []
|
|
436
494
|
for (const node of moduleSpecifierNodes(ts, source)) {
|
|
437
495
|
const specifier = node.text
|
|
438
|
-
if (!specifier.startsWith('.')) {
|
|
439
|
-
|
|
496
|
+
if (!specifier.startsWith('.')) {
|
|
497
|
+
continue
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
if (/\.d\.[cm]?ts$/i.test(specifier)) {
|
|
501
|
+
continue
|
|
502
|
+
}
|
|
503
|
+
|
|
440
504
|
const extension = Object.keys(extensions).find(
|
|
441
505
|
(candidate) => !candidate.startsWith('.d.') && specifier.endsWith(candidate),
|
|
442
506
|
)
|
|
@@ -459,7 +523,10 @@ function rewriteDeclaration(ts, path, outputDir, extensions, generatedFiles, run
|
|
|
459
523
|
generatedFiles.has(relative(outputDir, candidate)),
|
|
460
524
|
)
|
|
461
525
|
|
|
462
|
-
if (!target) {
|
|
526
|
+
if (!target) {
|
|
527
|
+
continue
|
|
528
|
+
}
|
|
529
|
+
|
|
463
530
|
const declaration = relative(outputDir, target)
|
|
464
531
|
const runtimeExtension = runtimeByDeclaration.get(declaration) ?? '.js'
|
|
465
532
|
const isIndex = /[\\/]index\.d\.[cm]?ts$/.test(target)
|
|
@@ -493,19 +560,15 @@ function normalizeOutput(ts, outputDir, extensions, runtimeByDeclaration) {
|
|
|
493
560
|
.filter((path) => /\.d\.[cm]?ts$/.test(path))
|
|
494
561
|
.sort()
|
|
495
562
|
|
|
496
|
-
if (!files.length) {
|
|
563
|
+
if (!files.length) {
|
|
564
|
+
throw new Error('TypeScript emitted no declaration files.')
|
|
565
|
+
}
|
|
566
|
+
|
|
497
567
|
const generatedFiles = new Set(files.map((path) => relative(outputDir, path)))
|
|
498
568
|
return new Map(
|
|
499
569
|
files.map((path) => [
|
|
500
570
|
relative(outputDir, path),
|
|
501
|
-
rewriteDeclaration(
|
|
502
|
-
ts,
|
|
503
|
-
path,
|
|
504
|
-
outputDir,
|
|
505
|
-
extensions,
|
|
506
|
-
generatedFiles,
|
|
507
|
-
runtimeByDeclaration,
|
|
508
|
-
),
|
|
571
|
+
rewriteDeclaration(ts, path, outputDir, extensions, generatedFiles, runtimeByDeclaration),
|
|
509
572
|
]),
|
|
510
573
|
)
|
|
511
574
|
}
|
|
@@ -527,7 +590,10 @@ function previousManifest(outputDir) {
|
|
|
527
590
|
|
|
528
591
|
return new Set(manifest.files)
|
|
529
592
|
} catch (error) {
|
|
530
|
-
if (error.code === 'ENOENT') {
|
|
593
|
+
if (error.code === 'ENOENT') {
|
|
594
|
+
return null
|
|
595
|
+
}
|
|
596
|
+
|
|
531
597
|
throw new Error(`${path}: ${error.message}`)
|
|
532
598
|
}
|
|
533
599
|
}
|
|
@@ -544,19 +610,29 @@ function pathInside(root, file) {
|
|
|
544
610
|
|
|
545
611
|
function writeOutput(outputDir, generated, oldFiles, overrideFiles) {
|
|
546
612
|
for (const file of generated.keys()) {
|
|
547
|
-
if (oldFiles?.has(file) || overrideFiles.has(file)) {
|
|
613
|
+
if (oldFiles?.has(file) || overrideFiles.has(file)) {
|
|
614
|
+
continue
|
|
615
|
+
}
|
|
616
|
+
|
|
548
617
|
try {
|
|
549
618
|
statSync(join(outputDir, file))
|
|
550
619
|
throw new Error(`refusing to overwrite unmanaged declaration: ${file}`)
|
|
551
620
|
} catch (error) {
|
|
552
|
-
if (error.code !== 'ENOENT') {
|
|
621
|
+
if (error.code !== 'ENOENT') {
|
|
622
|
+
throw error
|
|
623
|
+
}
|
|
553
624
|
}
|
|
554
625
|
}
|
|
555
626
|
|
|
556
627
|
for (const file of oldFiles ?? []) {
|
|
557
628
|
const path = resolve(outputDir, file)
|
|
558
|
-
if (!pathInside(outputDir, path)) {
|
|
559
|
-
|
|
629
|
+
if (!pathInside(outputDir, path)) {
|
|
630
|
+
throw new Error(`Invalid generated path in manifest: ${file}`)
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
if (!generated.has(file)) {
|
|
634
|
+
rmSync(path, { force: true })
|
|
635
|
+
}
|
|
560
636
|
}
|
|
561
637
|
|
|
562
638
|
for (const [file, content] of generated) {
|
|
@@ -574,40 +650,57 @@ function writeOutput(outputDir, generated, oldFiles, overrideFiles) {
|
|
|
574
650
|
|
|
575
651
|
function compareOutput(outputDir, generated, oldFiles, overrideFiles) {
|
|
576
652
|
const errors = []
|
|
577
|
-
if (!oldFiles) {
|
|
653
|
+
if (!oldFiles) {
|
|
654
|
+
errors.push(`missing ${manifestName}; run tsrx-typegen --project <tsconfig> first`)
|
|
655
|
+
}
|
|
656
|
+
|
|
578
657
|
for (const file of generated.keys()) {
|
|
579
658
|
if (!oldFiles?.has(file) && !overrideFiles.has(file)) {
|
|
580
659
|
try {
|
|
581
660
|
statSync(join(outputDir, file))
|
|
582
661
|
errors.push(`${file} conflicts with unmanaged output`)
|
|
583
662
|
} catch (error) {
|
|
584
|
-
if (error.code !== 'ENOENT') {
|
|
663
|
+
if (error.code !== 'ENOENT') {
|
|
664
|
+
throw error
|
|
665
|
+
}
|
|
585
666
|
}
|
|
586
667
|
}
|
|
587
668
|
|
|
588
669
|
try {
|
|
589
|
-
if (readFileSync(join(outputDir, file), 'utf8') !== generated.get(file))
|
|
590
|
-
|
|
670
|
+
if (readFileSync(join(outputDir, file), 'utf8') !== generated.get(file)) {
|
|
671
|
+
errors.push(`${file} is stale`)
|
|
672
|
+
}
|
|
591
673
|
} catch {
|
|
592
674
|
errors.push(`${file} is missing`)
|
|
593
675
|
}
|
|
594
676
|
}
|
|
595
677
|
|
|
596
678
|
for (const file of oldFiles ?? []) {
|
|
597
|
-
if (!generated.has(file)) {
|
|
679
|
+
if (!generated.has(file)) {
|
|
680
|
+
errors.push(`${file} is stale`)
|
|
681
|
+
}
|
|
598
682
|
}
|
|
599
683
|
|
|
600
684
|
return errors
|
|
601
685
|
}
|
|
602
686
|
|
|
603
687
|
function stringsIn(value) {
|
|
604
|
-
if (typeof value === 'string') {
|
|
605
|
-
|
|
688
|
+
if (typeof value === 'string') {
|
|
689
|
+
return [value]
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
if (!value || typeof value !== 'object') {
|
|
693
|
+
return []
|
|
694
|
+
}
|
|
695
|
+
|
|
606
696
|
return Object.values(value).flatMap(stringsIn)
|
|
607
697
|
}
|
|
608
698
|
|
|
609
699
|
function typeTargets(value) {
|
|
610
|
-
if (!value || typeof value !== 'object' || Array.isArray(value)) {
|
|
700
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) {
|
|
701
|
+
return []
|
|
702
|
+
}
|
|
703
|
+
|
|
611
704
|
return [
|
|
612
705
|
...(Object.hasOwn(value, 'types') ? stringsIn(value.types) : []),
|
|
613
706
|
...Object.entries(value)
|
|
@@ -642,13 +735,18 @@ function verifyPublicTypes(projectRoot, generated, outDir, target, sourceOutputs
|
|
|
642
735
|
for (const [subpath, entry] of Object.entries(target.entrypoints)) {
|
|
643
736
|
const mapping = exportsMap[subpath]
|
|
644
737
|
if (!mapping) {
|
|
645
|
-
failures.push(
|
|
738
|
+
failures.push(
|
|
739
|
+
`${subpath} is configured for ${target.name} but missing from effective package exports`,
|
|
740
|
+
)
|
|
741
|
+
|
|
646
742
|
continue
|
|
647
743
|
}
|
|
648
744
|
|
|
649
745
|
const source = resolve(projectRoot, entry.source)
|
|
650
746
|
if (![...sourceOutputs.values()].includes(source)) {
|
|
651
|
-
failures.push(
|
|
747
|
+
failures.push(
|
|
748
|
+
`${subpath} source is not included by its TypeScript project: ${entry.source}`,
|
|
749
|
+
)
|
|
652
750
|
}
|
|
653
751
|
|
|
654
752
|
const declaredRuntime = stringsIn(entry.runtime)
|
|
@@ -675,7 +773,10 @@ function verifyPublicTypes(projectRoot, generated, outDir, target, sourceOutputs
|
|
|
675
773
|
/\.(?:[cm]?[jt]sx?|tsrx)$/.test(target),
|
|
676
774
|
)
|
|
677
775
|
|
|
678
|
-
if (!runtimeTargets.length) {
|
|
776
|
+
if (!runtimeTargets.length) {
|
|
777
|
+
continue
|
|
778
|
+
}
|
|
779
|
+
|
|
679
780
|
const targets = typeTargets(mapping)
|
|
680
781
|
if (!targets.length) {
|
|
681
782
|
failures.push(`${subpath} has code exports but no types condition`)
|
|
@@ -683,7 +784,10 @@ function verifyPublicTypes(projectRoot, generated, outDir, target, sourceOutputs
|
|
|
683
784
|
}
|
|
684
785
|
|
|
685
786
|
for (const target of targets) {
|
|
686
|
-
if (!target.startsWith('./') || target.includes('*')) {
|
|
787
|
+
if (!target.startsWith('./') || target.includes('*')) {
|
|
788
|
+
continue
|
|
789
|
+
}
|
|
790
|
+
|
|
687
791
|
const absolute = resolve(projectRoot, target)
|
|
688
792
|
if (!pathInside(projectRoot, absolute)) {
|
|
689
793
|
failures.push(`${subpath} types target escapes the package: ${target}`)
|
|
@@ -691,10 +795,14 @@ function verifyPublicTypes(projectRoot, generated, outDir, target, sourceOutputs
|
|
|
691
795
|
}
|
|
692
796
|
|
|
693
797
|
const generatedPath = relative(outDir, absolute)
|
|
694
|
-
if (pathInside(outDir, absolute) && generated.has(generatedPath)) {
|
|
798
|
+
if (pathInside(outDir, absolute) && generated.has(generatedPath)) {
|
|
799
|
+
continue
|
|
800
|
+
}
|
|
801
|
+
|
|
695
802
|
try {
|
|
696
|
-
if (!statSync(absolute).isFile())
|
|
697
|
-
|
|
803
|
+
if (!statSync(absolute).isFile()) {
|
|
804
|
+
failures.push(`${subpath} types target is not a file: ${target}`)
|
|
805
|
+
}
|
|
698
806
|
} catch {
|
|
699
807
|
failures.push(`${subpath} types target is missing: ${target}`)
|
|
700
808
|
}
|
|
@@ -707,14 +815,18 @@ function verifyPublicTypes(projectRoot, generated, outDir, target, sourceOutputs
|
|
|
707
815
|
failures.push(`package types target escapes the package: ${targetPath}`)
|
|
708
816
|
} else if (!(pathInside(outDir, target) && generated.has(relative(outDir, target)))) {
|
|
709
817
|
try {
|
|
710
|
-
if (!statSync(target).isFile()) {
|
|
818
|
+
if (!statSync(target).isFile()) {
|
|
819
|
+
failures.push(`package types target is not a file: ${targetPath}`)
|
|
820
|
+
}
|
|
711
821
|
} catch {
|
|
712
822
|
failures.push(`package types target is missing: ${targetPath}`)
|
|
713
823
|
}
|
|
714
824
|
}
|
|
715
825
|
}
|
|
716
826
|
|
|
717
|
-
if (failures.length) {
|
|
827
|
+
if (failures.length) {
|
|
828
|
+
throw new Error(failures.join('\n'))
|
|
829
|
+
}
|
|
718
830
|
}
|
|
719
831
|
|
|
720
832
|
function runTarget(projectRoot, args) {
|
|
@@ -746,16 +858,34 @@ function runTarget(projectRoot, args) {
|
|
|
746
858
|
const temporaryOut = join(temporaryRoot, 'types')
|
|
747
859
|
const outputDir = temporaryOut
|
|
748
860
|
try {
|
|
749
|
-
const compilerArgs = [
|
|
861
|
+
const compilerArgs = [
|
|
862
|
+
compilerPath,
|
|
863
|
+
'--pretty',
|
|
864
|
+
'false',
|
|
865
|
+
'--project',
|
|
866
|
+
project,
|
|
867
|
+
outputFlag,
|
|
868
|
+
temporaryOut,
|
|
869
|
+
]
|
|
870
|
+
|
|
750
871
|
const result = spawnSync(process.execPath, compilerArgs, {
|
|
751
872
|
cwd: projectRoot,
|
|
752
873
|
encoding: 'utf8',
|
|
753
874
|
maxBuffer: 32 * 1024 * 1024,
|
|
754
875
|
})
|
|
755
876
|
|
|
756
|
-
if (result.stdout) {
|
|
757
|
-
|
|
758
|
-
|
|
877
|
+
if (result.stdout) {
|
|
878
|
+
process.stdout.write(result.stdout)
|
|
879
|
+
}
|
|
880
|
+
|
|
881
|
+
if (result.stderr) {
|
|
882
|
+
process.stderr.write(result.stderr)
|
|
883
|
+
}
|
|
884
|
+
|
|
885
|
+
if (result.error) {
|
|
886
|
+
throw result.error
|
|
887
|
+
}
|
|
888
|
+
|
|
759
889
|
if (/\[tsrx-tsc\]/.test(result.stderr ?? '')) {
|
|
760
890
|
throw new Error(
|
|
761
891
|
'tsrx compiler reported transform diagnostics; declarations were not written. Fix the source or configure an explicit declaration override.',
|
|
@@ -778,7 +908,10 @@ function runTarget(projectRoot, args) {
|
|
|
778
908
|
if (args.check) {
|
|
779
909
|
const errors = compareOutput(outDir, generated, oldFiles, managedOverrides)
|
|
780
910
|
if (errors.length) {
|
|
781
|
-
for (const error of errors) {
|
|
911
|
+
for (const error of errors) {
|
|
912
|
+
console.error(`tsrx-typegen: ${error}`)
|
|
913
|
+
}
|
|
914
|
+
|
|
782
915
|
return false
|
|
783
916
|
}
|
|
784
917
|
|
|
@@ -800,12 +933,19 @@ function run() {
|
|
|
800
933
|
const args = parseArgs(process.argv.slice(2))
|
|
801
934
|
const projectRoot = process.cwd()
|
|
802
935
|
if (!args.packCheck) {
|
|
803
|
-
if (!runTarget(projectRoot, args)) {
|
|
936
|
+
if (!runTarget(projectRoot, args)) {
|
|
937
|
+
process.exitCode = 1
|
|
938
|
+
}
|
|
939
|
+
|
|
804
940
|
return
|
|
805
941
|
}
|
|
806
942
|
|
|
807
|
-
const targets = configuredTargetNames(projectRoot, args.target)
|
|
808
|
-
for (const target of
|
|
943
|
+
const { names, targets } = configuredTargetNames(projectRoot, args.target)
|
|
944
|
+
for (const target of names) {
|
|
945
|
+
if (targets[target]?.emit === false) {
|
|
946
|
+
continue
|
|
947
|
+
}
|
|
948
|
+
|
|
809
949
|
if (!runTarget(projectRoot, { ...args, target, check: true })) {
|
|
810
950
|
process.exitCode = 1
|
|
811
951
|
return
|
package/src/pack-check.mjs
CHANGED
|
@@ -190,10 +190,15 @@ function localSourceModule(file, specifier) {
|
|
|
190
190
|
const candidates = [target]
|
|
191
191
|
if (extension === '.js' || extension === '.mjs' || extension === '.cjs') {
|
|
192
192
|
const stem = target.slice(0, -extension.length)
|
|
193
|
-
candidates.push(`${stem}.ts`, `${stem}.tsx`, `${stem}.tsrx`)
|
|
193
|
+
candidates.push(`${stem}.ts`, `${stem}.tsx`, `${stem}.tsrx`, `${stem}.d.ts`)
|
|
194
194
|
} else if (!['.ts', '.tsx', '.tsrx'].includes(extension)) {
|
|
195
|
-
candidates.push(`${target}.ts`, `${target}.tsx`, `${target}.tsrx`)
|
|
196
|
-
candidates.push(
|
|
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
|
+
)
|
|
197
202
|
}
|
|
198
203
|
|
|
199
204
|
for (const candidate of candidates) {
|
|
@@ -257,7 +262,11 @@ function staticRuntimeExports(ts, file, seen = new Set()) {
|
|
|
257
262
|
}
|
|
258
263
|
|
|
259
264
|
if (ts.isExportDeclaration(statement) && !statement.isTypeOnly) {
|
|
260
|
-
if (
|
|
265
|
+
if (
|
|
266
|
+
!statement.exportClause &&
|
|
267
|
+
statement.moduleSpecifier &&
|
|
268
|
+
ts.isStringLiteralLike(statement.moduleSpecifier)
|
|
269
|
+
) {
|
|
261
270
|
const target = localSourceModule(absolute, statement.moduleSpecifier.text)
|
|
262
271
|
if (target) {
|
|
263
272
|
for (const name of staticRuntimeExports(ts, target, seen)) {
|
|
@@ -285,7 +294,16 @@ function staticRuntimeExports(ts, file, seen = new Set()) {
|
|
|
285
294
|
return names
|
|
286
295
|
}
|
|
287
296
|
|
|
288
|
-
function compilerExports(ts, program, file) {
|
|
297
|
+
function compilerExports(ts, program, file, cache = new Map(), pending = new Set()) {
|
|
298
|
+
const absolute = resolve(file)
|
|
299
|
+
if (cache.has(absolute)) {
|
|
300
|
+
return cache.get(absolute)
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
if (pending.has(absolute)) {
|
|
304
|
+
return { all: new Set(), values: new Set() }
|
|
305
|
+
}
|
|
306
|
+
|
|
289
307
|
const source = program.getSourceFile(file)
|
|
290
308
|
if (!source) {
|
|
291
309
|
return undefined
|
|
@@ -313,7 +331,56 @@ function compilerExports(ts, program, file) {
|
|
|
313
331
|
}
|
|
314
332
|
}
|
|
315
333
|
|
|
316
|
-
|
|
334
|
+
// Type-only edges (`export type *`, `export type { X }`) re-list the
|
|
335
|
+
// target's symbols with their original flags — an ambient `declare const`
|
|
336
|
+
// still reads as a value even though no value binding crosses the edge.
|
|
337
|
+
// Subtract names whose only export edges are type-only.
|
|
338
|
+
const typeOnly = new Set()
|
|
339
|
+
for (const statement of source.statements) {
|
|
340
|
+
if (!ts.isExportDeclaration(statement) || !statement.isTypeOnly) {
|
|
341
|
+
continue
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
if (
|
|
345
|
+
!statement.exportClause &&
|
|
346
|
+
statement.moduleSpecifier &&
|
|
347
|
+
ts.isStringLiteralLike(statement.moduleSpecifier)
|
|
348
|
+
) {
|
|
349
|
+
pending.add(absolute)
|
|
350
|
+
const target = ts.resolveModuleName(
|
|
351
|
+
statement.moduleSpecifier.text,
|
|
352
|
+
file,
|
|
353
|
+
program.getCompilerOptions(),
|
|
354
|
+
ts.sys,
|
|
355
|
+
).resolvedModule?.resolvedFileName
|
|
356
|
+
|
|
357
|
+
const targetExports = target && compilerExports(ts, program, target, cache, pending)
|
|
358
|
+
|
|
359
|
+
pending.delete(absolute)
|
|
360
|
+
for (const name of targetExports?.all ?? []) {
|
|
361
|
+
typeOnly.add(name)
|
|
362
|
+
}
|
|
363
|
+
} else if (statement.exportClause && ts.isNamedExports(statement.exportClause)) {
|
|
364
|
+
for (const element of statement.exportClause.elements) {
|
|
365
|
+
typeOnly.add(element.name.text)
|
|
366
|
+
}
|
|
367
|
+
} else if (statement.exportClause && ts.isNamespaceExport(statement.exportClause)) {
|
|
368
|
+
typeOnly.add(statement.exportClause.name.text)
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
if (typeOnly.size) {
|
|
373
|
+
const valueEdges = staticRuntimeExports(ts, file)
|
|
374
|
+
for (const name of values) {
|
|
375
|
+
if (typeOnly.has(name) && !valueEdges.has(name)) {
|
|
376
|
+
values.delete(name)
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
const result = { all, values }
|
|
382
|
+
cache.set(absolute, result)
|
|
383
|
+
return result
|
|
317
384
|
}
|
|
318
385
|
|
|
319
386
|
function declarationFiles(packageRoot, files) {
|