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 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
- "overrides": {
59
- "Button.d.ts": "types/overrides/Button.d.ts"
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
- "scripts": {
99
- "build": "vite build && tsrx-typegen --target octane",
100
- "prepack": "pnpm build && tsrx-typegen --pack-check"
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.0",
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": "git+https://github.com/octane-xplat/octane-xplat.git",
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 (!targets || typeof targets !== 'object' || Array.isArray(targets) || !Object.keys(targets).length) {
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) {throw new Error(`Could not read ${configPath}`)}
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) {throw new Error(errors.join('\n'))}
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 ? 'react' : undefined)
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(`${configPath} must set target outDir, compilerOptions.outDir, or declarationDir.`)
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) {throw new Error('TypeScript project has no root files.')}
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)) {continue}
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(sourcePath.slice(0, -sourceExtension.length) + declarationExtension)
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) {throw new Error(`No tsrx-typegen.json defines target ${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) {throw new Error(`${path}: unknown target ${selectedName}`)}
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) {[selectedName, selected] = matches[0]}
248
- else if (Object.keys(targets).length) {
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
- !stringsIn(entry.runtime).length ||
318
+ !stringsIn(entry.runtime).length ||
281
319
  !stringsIn(entry.types).length
282
320
  ) {
283
- throw new Error(`${path}: target ${selectedName} entrypoint ${subpath} needs source, runtime, and types`)
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(`${path}: target ${selectedName} entrypoint source is missing: ${entry.source}`)
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 = { ...defaultExtensions, ...config.sourceExtensions, ...selected?.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(`${path}: sourceExtensions must map dot-prefixed extensions to dot-prefixed runtime extensions`)
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) {generated.set(outputPath, content)}
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()) {collectFiles(path, output)}
389
- else if (entry.isFile()) {output.push(path)}
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) => /\.tsrx(?:$|[?#])/.test(node.text))
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('.')) {continue}
439
- if (/\.d\.[cm]?ts$/i.test(specifier)) {continue}
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) {continue}
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) {throw new Error('TypeScript emitted no declaration files.')}
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') {return null}
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)) {continue}
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') {throw error}
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)) {throw new Error(`Invalid generated path in manifest: ${file}`)}
559
- if (!generated.has(file)) {rmSync(path, { force: true })}
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) {errors.push(`missing ${manifestName}; run tsrx-typegen --project <tsconfig> first`)}
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') {throw error}
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
- {errors.push(`${file} is stale`)}
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)) {errors.push(`${file} is stale`)}
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') {return [value]}
605
- if (!value || typeof value !== 'object') {return []}
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)) {return []}
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(`${subpath} is configured for ${target.name} but missing from effective package exports`)
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(`${subpath} source is not included by its TypeScript project: ${entry.source}`)
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) {continue}
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('*')) {continue}
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)) {continue}
798
+ if (pathInside(outDir, absolute) && generated.has(generatedPath)) {
799
+ continue
800
+ }
801
+
695
802
  try {
696
- if (!statSync(absolute).isFile())
697
- {failures.push(`${subpath} types target is not a file: ${target}`)}
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()) {failures.push(`package types target is not a file: ${targetPath}`)}
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) {throw new Error(failures.join('\n'))}
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 = [compilerPath, '--pretty', 'false', '--project', project, outputFlag, temporaryOut]
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) {process.stdout.write(result.stdout)}
757
- if (result.stderr) {process.stderr.write(result.stderr)}
758
- if (result.error) {throw result.error}
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) {console.error(`tsrx-typegen: ${error}`)}
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)) {process.exitCode = 1}
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 targets) {
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
@@ -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(join(target, 'index.ts'), join(target, 'index.tsx'), join(target, 'index.tsrx'))
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 (!statement.exportClause && statement.moduleSpecifier && ts.isStringLiteralLike(statement.moduleSpecifier)) {
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
- return { all, values }
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) {