tsrx-typegen 0.3.0 → 0.4.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/README.md CHANGED
@@ -150,6 +150,14 @@ the package and bare package imports are declared dependencies or peers. The com
150
150
  TypeScript consumer test for the module-resolution modes and public prop
151
151
  contracts your package supports.
152
152
 
153
+ The checker requires paired runtime and type exports. Pure type-only export
154
+ maps and mixed Node tooling exports without type entries need separate
155
+ package-specific validation.
156
+
157
+ ```sh
158
+ pnpm exec tsrx-typegen --pack-check
159
+ ```
160
+
153
161
  A target with `"emit": false` exists only to drive packed-package
154
162
  verification. Packages whose declarations are handwritten or produced by
155
163
  another tool declare one so `--pack-check` skips generation and freshness
@@ -0,0 +1,7 @@
1
+ #!/usr/bin/env node
2
+ // Published installs live under node_modules, where Node refuses type
3
+ // stripping — run the emitted dist/. In-repo runs always execute src/ (Node
4
+ // ≥22.18 strips types): a stale, gitignored dist/ would otherwise run old code
5
+ // under turbo cache keys hashed from current source.
6
+ const entry = import.meta.url.includes('/node_modules/') ? '../dist/cli.js' : '../src/cli.ts'
7
+ await import(new URL(entry, import.meta.url).href)
package/dist/cli.js ADDED
@@ -0,0 +1,546 @@
1
+ #!/usr/bin/env node
2
+ import { checkPackedPackage } from "./pack-check.js";
3
+ import { createRequire } from "node:module";
4
+ import { spawnSync } from "node:child_process";
5
+ import { mkdirSync, mkdtempSync, readFileSync, readdirSync, rmSync, statSync, writeFileSync } from "node:fs";
6
+ import { tmpdir } from "node:os";
7
+ import { dirname, extname, isAbsolute, join, normalize, relative, resolve, sep } from "node:path";
8
+ //#region src/cli.ts
9
+ var manifestName = ".tsrx-typegen-manifest.json";
10
+ var defaultExtensions = {
11
+ ".ts": ".js",
12
+ ".tsx": ".js",
13
+ ".tsrx": ".js",
14
+ ".mts": ".mjs",
15
+ ".cts": ".cjs"
16
+ };
17
+ function usage() {
18
+ console.log(`Usage: tsrx-typegen --project <tsconfig> [--target <name>] [--check]
19
+ tsrx-typegen --target <name> [--check]
20
+ tsrx-typegen --pack-check [--target <name>]
21
+
22
+ Generate declarations with the consuming project's TypeScript and tsrx compiler.
23
+
24
+ Options:
25
+ --project <path> TypeScript config for declaration generation
26
+ --target <name> Target in tsrx-typegen.json (selects its project and output)
27
+ --check Compare generated declarations without writing files
28
+ --pack-check Check declarations, pack the package, and validate the tarball
29
+ (targets with emit: false skip generation and only validate)
30
+ -h, --help Show this help`);
31
+ }
32
+ function parseArgs(args) {
33
+ let project;
34
+ let target;
35
+ let check = false;
36
+ let packCheck = false;
37
+ for (let index = 0; index < args.length; index += 1) {
38
+ const arg = args[index];
39
+ if (arg === "--help" || arg === "-h") {
40
+ usage();
41
+ process.exit(0);
42
+ } else if (arg === "--check") check = true;
43
+ else if (arg === "--pack-check") packCheck = true;
44
+ else if (arg === "--project") {
45
+ if (!args[index + 1] || args[index + 1].startsWith("--")) throw new Error("Pass a path after --project.");
46
+ project = args[++index];
47
+ } else if (arg === "--target") {
48
+ if (!args[index + 1] || args[index + 1].startsWith("--")) throw new Error("Pass a name after --target.");
49
+ target = args[++index];
50
+ } else throw new Error(`Unknown argument: ${arg}`);
51
+ }
52
+ if (!project && !target && !packCheck) throw new Error("Pass --project <tsconfig>, --target <name>, or --pack-check.");
53
+ if (packCheck && project) throw new Error("--pack-check uses targets from tsrx-typegen.json; do not pass --project.");
54
+ return {
55
+ project: project ? resolve(project) : void 0,
56
+ target,
57
+ check,
58
+ packCheck
59
+ };
60
+ }
61
+ function configuredTargetNames(projectRoot, selectedTarget) {
62
+ const path = join(projectRoot, "tsrx-typegen.json");
63
+ let config;
64
+ try {
65
+ config = JSON.parse(readFileSync(path, "utf8"));
66
+ } catch (error) {
67
+ throw new Error(`${path}: ${error.message}`);
68
+ }
69
+ const targets = config?.targets;
70
+ if (!targets || typeof targets !== "object" || Array.isArray(targets) || !Object.keys(targets).length) throw new Error(`${path}: --pack-check requires at least one configured target`);
71
+ if (selectedTarget && !Object.hasOwn(targets, selectedTarget)) throw new Error(`${path}: unknown target ${selectedTarget}`);
72
+ return {
73
+ names: selectedTarget ? [selectedTarget] : Object.keys(targets),
74
+ targets
75
+ };
76
+ }
77
+ function loadProjectModule(projectRoot, specifier) {
78
+ return createRequire(join(projectRoot, "package.json")).resolve(specifier);
79
+ }
80
+ function readProjectConfig(ts, configPath, projectRoot, target) {
81
+ const configFile = ts.readConfigFile(configPath, ts.sys.readFile);
82
+ if (configFile.error) throw new Error(ts.flattenDiagnosticMessageText(configFile.error.messageText, "\n"));
83
+ const parsed = ts.parseJsonConfigFileContent(configFile.config, { ...ts.sys }, dirname(configPath), {}, configPath, void 0, [{
84
+ extension: ".tsrx",
85
+ isMixedContent: true,
86
+ scriptKind: ts.ScriptKind.Deferred
87
+ }]);
88
+ if (!parsed) throw new Error(`Could not read ${configPath}`);
89
+ const errors = parsed.errors.map((diagnostic) => ts.flattenDiagnosticMessageText(diagnostic.messageText, "\n"));
90
+ if (errors.length) throw new Error(errors.join("\n"));
91
+ const options = parsed.options;
92
+ const jsxRenderer = options.jsxImportSource ?? (options.jsx === ts.JsxEmit.ReactJSX || options.jsx === ts.JsxEmit.ReactJSXDev ? "react" : void 0);
93
+ if (target?.renderer && jsxRenderer && target.renderer !== jsxRenderer) throw new Error(`target ${target.name} renderer ${target.renderer} does not match compilerOptions.jsxImportSource ${jsxRenderer}`);
94
+ if (!options.declaration || !options.emitDeclarationOnly || options.noEmit) throw new Error(`${configPath} must set declaration: true, emitDeclarationOnly: true, and noEmit: false.`);
95
+ const outputDirectory = target?.outDir ?? options.declarationDir ?? options.outDir;
96
+ if (!outputDirectory) throw new Error(`${configPath} must set target outDir, compilerOptions.outDir, or declarationDir.`);
97
+ return {
98
+ outDir: target?.outDir ? resolve(projectRoot, outputDirectory) : resolve(dirname(configPath), outputDirectory),
99
+ outputFlag: options.declarationDir ? "--declarationDir" : "--outDir",
100
+ rootDir: options.rootDir ? resolve(options.rootDir) : commonDirectory(parsed.fileNames),
101
+ fileNames: parsed.fileNames
102
+ };
103
+ }
104
+ function commonDirectory(paths) {
105
+ if (!paths.length) throw new Error("TypeScript project has no root files.");
106
+ const segments = paths.map((path) => resolve(dirname(path)).split(sep));
107
+ const common = [];
108
+ for (let index = 0; segments.every((parts) => parts[index] === segments[0][index]); index += 1) common.push(segments[0][index]);
109
+ return common.join(sep) || sep;
110
+ }
111
+ function sourceDeclarationMap(rootDir, fileNames, extensions, overrides) {
112
+ const sourceOutputs = /* @__PURE__ */ new Map();
113
+ const runtimeByDeclaration = /* @__PURE__ */ new Map();
114
+ for (const source of fileNames) {
115
+ const sourceExtension = extname(source).toLowerCase();
116
+ if (!Object.hasOwn(extensions, sourceExtension) || /\.d\.[cm]?ts$/i.test(source)) continue;
117
+ const sourcePath = relative(rootDir, source);
118
+ if (!sourcePath || sourcePath.startsWith(`..${sep}`) || isAbsolute(sourcePath)) throw new Error(`Source file is outside compilerOptions.rootDir: ${source}`);
119
+ const declarationExtension = sourceExtension === ".mts" ? ".d.mts" : sourceExtension === ".cts" ? ".d.cts" : ".d.ts";
120
+ const declaration = normalize(sourcePath.slice(0, -sourceExtension.length) + declarationExtension);
121
+ const prior = sourceOutputs.get(declaration);
122
+ if (prior && !overrides.has(declaration)) throw new Error(`source output collision: ${prior} and ${source} both emit ${declaration}; rename one source or declare an override for ${declaration}`);
123
+ if (!prior) {
124
+ sourceOutputs.set(declaration, source);
125
+ runtimeByDeclaration.set(declaration, extensions[sourceExtension]);
126
+ }
127
+ }
128
+ return {
129
+ sourceOutputs,
130
+ runtimeByDeclaration
131
+ };
132
+ }
133
+ function configOptions(projectRoot, projectArgument, targetArgument) {
134
+ const path = join(projectRoot, "tsrx-typegen.json");
135
+ let config;
136
+ try {
137
+ config = JSON.parse(readFileSync(path, "utf8"));
138
+ } catch (error) {
139
+ if (error.code === "ENOENT") {
140
+ if (targetArgument) throw new Error(`No tsrx-typegen.json defines target ${targetArgument}.`);
141
+ return {
142
+ project: projectArgument,
143
+ target: null,
144
+ sourceExtensions: defaultExtensions,
145
+ overrides: {}
146
+ };
147
+ }
148
+ throw new Error(`${path}: ${error.message}`);
149
+ }
150
+ if (!config || typeof config !== "object" || Array.isArray(config)) throw new Error(`${path}: expected a JSON object`);
151
+ const targets = config.targets ?? {};
152
+ if (!targets || typeof targets !== "object" || Array.isArray(targets)) throw new Error(`${path}: targets must be an object`);
153
+ const resolveProject = (value) => resolve(projectRoot, value);
154
+ let selectedName = targetArgument;
155
+ let selected = selectedName ? targets[selectedName] : void 0;
156
+ if (selectedName && !selected) throw new Error(`${path}: unknown target ${selectedName}`);
157
+ if (!selected && projectArgument) {
158
+ const matches = Object.entries(targets).filter(([, target]) => target?.project && resolveProject(target.project) === projectArgument);
159
+ if (matches.length > 1) throw new Error(`${path}: project matches multiple targets; pass --target <name>`);
160
+ if (matches.length === 1) [selectedName, selected] = matches[0];
161
+ else if (Object.keys(targets).length) throw new Error(`${path}: ${projectArgument} is not declared by a target`);
162
+ }
163
+ if (selected) {
164
+ if (selected.emit === false) throw new Error(`${path}: target ${selectedName} has emit: false — it configures --pack-check only`);
165
+ if (typeof selected.project !== "string" || !selected.project) throw new Error(`${path}: target ${selectedName} must name a project`);
166
+ if (projectArgument && resolveProject(selected.project) !== projectArgument) throw new Error(`${path}: --project does not match target ${selectedName}`);
167
+ if (typeof selected.renderer !== "string" || !selected.renderer) throw new Error(`${path}: target ${selectedName} must name its renderer`);
168
+ if (!selected.entrypoints || typeof selected.entrypoints !== "object" || Array.isArray(selected.entrypoints) || !Object.keys(selected.entrypoints).length) throw new Error(`${path}: target ${selectedName} must declare entrypoints`);
169
+ for (const [subpath, entry] of Object.entries(selected.entrypoints)) {
170
+ if (!entry || typeof entry !== "object" || typeof entry.source !== "string" || !stringsIn(entry.runtime).length || !stringsIn(entry.types).length) throw new Error(`${path}: target ${selectedName} entrypoint ${subpath} needs source, runtime, and types`);
171
+ for (const publishedPath of [...stringsIn(entry.runtime), ...stringsIn(entry.types)]) if (!publishedPath.startsWith("./") || !pathInside(projectRoot, resolve(projectRoot, publishedPath))) throw new Error(`${path}: ${subpath} mapping escapes the package: ${publishedPath}`);
172
+ const source = resolve(projectRoot, entry.source);
173
+ let isFile = false;
174
+ try {
175
+ isFile = statSync(source).isFile();
176
+ } catch {}
177
+ if (!pathInside(projectRoot, source) || !isFile) throw new Error(`${path}: target ${selectedName} entrypoint source is missing: ${entry.source}`);
178
+ }
179
+ if (selected.outDir) {
180
+ if (typeof selected.outDir !== "string") throw new Error(`${path}: target ${selectedName} outDir must be a package-relative path`);
181
+ if (!pathInside(projectRoot, resolve(projectRoot, selected.outDir))) throw new Error(`${path}: target ${selectedName} outDir escapes the package`);
182
+ }
183
+ }
184
+ if (!selected && Object.keys(targets).length) throw new Error(`${path}: pass --target <name> or a matching --project`);
185
+ const project = projectArgument ?? resolveProject(selected.project);
186
+ const sourceExtensions = {
187
+ ...defaultExtensions,
188
+ ...config.sourceExtensions,
189
+ ...selected?.sourceExtensions
190
+ };
191
+ for (const [source, runtime] of Object.entries(sourceExtensions)) if (!source.startsWith(".") || typeof runtime !== "string" || !runtime.startsWith(".")) throw new Error(`${path}: sourceExtensions must map dot-prefixed extensions to dot-prefixed runtime extensions`);
192
+ const globalOverrides = config.overrides ?? {};
193
+ const targetOverrides = selected?.overrides ?? {};
194
+ for (const overrides of [globalOverrides, targetOverrides]) if (!overrides || typeof overrides !== "object" || Array.isArray(overrides)) throw new Error(`${path}: overrides must map declaration output paths to source files`);
195
+ return {
196
+ project,
197
+ target: selected ? {
198
+ name: selectedName,
199
+ ...selected
200
+ } : null,
201
+ sourceExtensions,
202
+ overrides: {
203
+ ...globalOverrides,
204
+ ...targetOverrides
205
+ }
206
+ };
207
+ }
208
+ function applyOverrides(ts, projectRoot, outputDir, generated, overrides) {
209
+ const normalized = /* @__PURE__ */ new Map();
210
+ for (const [outputPath, sourcePath] of Object.entries(overrides)) {
211
+ if (typeof sourcePath !== "string" || !sourcePath) throw new Error(`override for ${outputPath} must name a source declaration file`);
212
+ const outputFile = normalize(outputPath);
213
+ if (!pathInside(outputDir, resolve(outputDir, outputFile)) || !/\.d\.[cm]?ts$/.test(outputFile)) throw new Error(`invalid override output path: ${outputPath}`);
214
+ const source = resolve(projectRoot, sourcePath);
215
+ if (!pathInside(projectRoot, source)) throw new Error(`override source escapes the package: ${sourcePath}`);
216
+ let content;
217
+ try {
218
+ content = readFileSync(source, "utf8");
219
+ } catch (error) {
220
+ throw new Error(`override source ${sourcePath}: ${error.message}`);
221
+ }
222
+ assertNoTsrxModuleReferences(ts, source, content);
223
+ normalized.set(outputFile, content);
224
+ }
225
+ for (const [outputPath, content] of normalized) generated.set(outputPath, content);
226
+ return new Set(normalized.keys());
227
+ }
228
+ function collectFiles(root, output = []) {
229
+ if (!statSync(root).isDirectory()) {
230
+ output.push(root);
231
+ return output;
232
+ }
233
+ for (const entry of readdirSync(root, { withFileTypes: true })) {
234
+ const path = join(root, entry.name);
235
+ if (entry.isDirectory()) collectFiles(path, output);
236
+ else if (entry.isFile()) output.push(path);
237
+ }
238
+ return output;
239
+ }
240
+ function moduleSpecifierNodes(ts, sourceFile) {
241
+ const nodes = [];
242
+ const visit = (node) => {
243
+ if ((ts.isImportDeclaration(node) || ts.isExportDeclaration(node)) && node.moduleSpecifier && ts.isStringLiteralLike(node.moduleSpecifier)) nodes.push(node.moduleSpecifier);
244
+ else if (ts.isImportTypeNode(node)) {
245
+ const argument = node.argument;
246
+ if (ts.isLiteralTypeNode(argument) && ts.isStringLiteralLike(argument.literal)) nodes.push(argument.literal);
247
+ } else if (ts.isExternalModuleReference(node) && node.expression && ts.isStringLiteral(node.expression)) nodes.push(node.expression);
248
+ ts.forEachChild(node, visit);
249
+ };
250
+ visit(sourceFile);
251
+ return nodes;
252
+ }
253
+ function assertNoTsrxModuleReferences(ts, path, text) {
254
+ const unresolved = moduleSpecifierNodes(ts, ts.createSourceFile(path, text, ts.ScriptTarget.Latest, true, ts.ScriptKind.TS)).find((node) => /\.tsrx(?:$|[?#])/.test(node.text));
255
+ if (unresolved) throw new Error(`${path}: unresolved .tsrx module specifier ${unresolved.text}`);
256
+ }
257
+ function rewriteDeclaration(ts, path, outputDir, extensions, generatedFiles, runtimeByDeclaration) {
258
+ const text = readFileSync(path, "utf8");
259
+ const source = ts.createSourceFile(path, text, ts.ScriptTarget.Latest, true, ts.ScriptKind.TS);
260
+ const edits = [];
261
+ for (const node of moduleSpecifierNodes(ts, source)) {
262
+ const specifier = node.text;
263
+ if (!specifier.startsWith(".")) continue;
264
+ if (/\.d\.[cm]?ts$/i.test(specifier)) continue;
265
+ const extension = Object.keys(extensions).find((candidate) => !candidate.startsWith(".d.") && specifier.endsWith(candidate));
266
+ let replacement;
267
+ if (extension) replacement = specifier.slice(0, -extension.length) + extensions[extension];
268
+ else if (!/\.[cm]?[jt]sx?$/.test(specifier)) {
269
+ const targetStem = resolve(dirname(path), specifier);
270
+ const target = [
271
+ `${targetStem}.d.ts`,
272
+ `${targetStem}.d.mts`,
273
+ `${targetStem}.d.cts`,
274
+ join(targetStem, "index.d.ts"),
275
+ join(targetStem, "index.d.mts"),
276
+ join(targetStem, "index.d.cts")
277
+ ].find((candidate) => generatedFiles.has(relative(outputDir, candidate)));
278
+ if (!target) continue;
279
+ const declaration = relative(outputDir, target);
280
+ const runtimeExtension = runtimeByDeclaration.get(declaration) ?? ".js";
281
+ const isIndex = dirname(target) === targetStem;
282
+ const baseSpecifier = specifier.endsWith("/") ? specifier.slice(0, -1) : specifier;
283
+ replacement = isIndex ? `${baseSpecifier}/index${runtimeExtension}` : `${specifier}${runtimeExtension}`;
284
+ } else continue;
285
+ edits.push({
286
+ start: node.getStart(source),
287
+ end: node.end,
288
+ replacement: `${text[node.getStart(source)]}${replacement}${text[node.end - 1]}`
289
+ });
290
+ }
291
+ let result = text;
292
+ for (const edit of edits.sort((a, b) => b.start - a.start)) result = result.slice(0, edit.start) + edit.replacement + result.slice(edit.end);
293
+ assertNoTsrxModuleReferences(ts, path, result);
294
+ return result;
295
+ }
296
+ function normalizeOutput(ts, outputDir, extensions, runtimeByDeclaration) {
297
+ const files = collectFiles(outputDir).filter((path) => /\.d\.[cm]?ts$/.test(path)).sort();
298
+ if (!files.length) throw new Error("TypeScript emitted no declaration files.");
299
+ const generatedFiles = new Set(files.map((path) => relative(outputDir, path)));
300
+ return new Map(files.map((path) => [relative(outputDir, path), rewriteDeclaration(ts, path, outputDir, extensions, generatedFiles, runtimeByDeclaration)]));
301
+ }
302
+ function verifySourceDeclarations(generated, sourceOutputs) {
303
+ const missing = [...sourceOutputs.keys()].filter((declaration) => !generated.has(declaration));
304
+ if (missing.length) throw new Error(`TypeScript emitted no declarations for source file(s): ${missing.join(", ")}`);
305
+ }
306
+ function previousManifest(outputDir) {
307
+ const path = join(outputDir, manifestName);
308
+ try {
309
+ const manifest = JSON.parse(readFileSync(path, "utf8"));
310
+ if (manifest.version !== 1 || !Array.isArray(manifest.files)) throw new Error("unsupported manifest format");
311
+ return new Set(manifest.files);
312
+ } catch (error) {
313
+ if (error.code === "ENOENT") return null;
314
+ throw new Error(`${path}: ${error.message}`);
315
+ }
316
+ }
317
+ function pathInside(root, file) {
318
+ const relativePath = relative(root, file);
319
+ return relativePath && !relativePath.startsWith(`..${sep}`) && relativePath !== ".." && !isAbsolute(relativePath);
320
+ }
321
+ function existingOutputPath(outputDir, file) {
322
+ let current = outputDir;
323
+ const actualParts = [];
324
+ for (const part of normalize(file).split(sep)) {
325
+ let entries;
326
+ try {
327
+ entries = readdirSync(current);
328
+ } catch (error) {
329
+ if (error.code === "ENOENT") return null;
330
+ throw error;
331
+ }
332
+ const actual = entries.includes(part) ? part : entries.find((entry) => entry.toLowerCase() === part.toLowerCase());
333
+ if (!actual) return null;
334
+ actualParts.push(actual);
335
+ current = join(current, actual);
336
+ }
337
+ return actualParts.join(sep);
338
+ }
339
+ function writeOutput(outputDir, generated, oldFiles, overrideFiles) {
340
+ for (const file of generated.keys()) {
341
+ const existing = existingOutputPath(outputDir, file);
342
+ if (existing && !oldFiles?.has(existing) && !overrideFiles.has(existing)) throw new Error(`refusing to overwrite unmanaged declaration: ${file}`);
343
+ }
344
+ for (const file of oldFiles ?? []) {
345
+ const path = resolve(outputDir, file);
346
+ if (!pathInside(outputDir, path)) throw new Error(`Invalid generated path in manifest: ${file}`);
347
+ if (!generated.has(file)) rmSync(path, { force: true });
348
+ }
349
+ for (const [file, content] of generated) {
350
+ const path = join(outputDir, file);
351
+ mkdirSync(dirname(path), { recursive: true });
352
+ writeFileSync(path, content);
353
+ }
354
+ mkdirSync(outputDir, { recursive: true });
355
+ writeFileSync(join(outputDir, manifestName), JSON.stringify({
356
+ version: 1,
357
+ files: [...generated.keys()]
358
+ }, null, 2) + "\n");
359
+ }
360
+ function compareOutput(outputDir, generated, oldFiles, overrideFiles) {
361
+ const errors = [];
362
+ if (!oldFiles) errors.push(`missing ${manifestName}; run tsrx-typegen --project <tsconfig> first`);
363
+ for (const file of generated.keys()) {
364
+ if (!oldFiles?.has(file) && !overrideFiles.has(file)) try {
365
+ statSync(join(outputDir, file));
366
+ errors.push(`${file} conflicts with unmanaged output`);
367
+ } catch (error) {
368
+ if (error.code !== "ENOENT") throw error;
369
+ }
370
+ try {
371
+ if (readFileSync(join(outputDir, file), "utf8") !== generated.get(file)) errors.push(`${file} is stale`);
372
+ } catch {
373
+ errors.push(`${file} is missing`);
374
+ }
375
+ }
376
+ for (const file of oldFiles ?? []) if (!generated.has(file)) errors.push(`${file} is stale`);
377
+ return errors;
378
+ }
379
+ function stringsIn(value) {
380
+ if (typeof value === "string") return [value];
381
+ if (!value || typeof value !== "object") return [];
382
+ return Object.values(value).flatMap(stringsIn);
383
+ }
384
+ function typeTargets(value) {
385
+ if (!value || typeof value !== "object" || Array.isArray(value)) return [];
386
+ return [...Object.hasOwn(value, "types") ? stringsIn(value.types) : [], ...Object.entries(value).filter(([key]) => key !== "types" && key !== "default").flatMap(([, child]) => typeTargets(child))];
387
+ }
388
+ function verifyPublicTypes(projectRoot, generated, outDir, target, sourceOutputs) {
389
+ const manifestPath = join(projectRoot, "package.json");
390
+ let manifest;
391
+ try {
392
+ manifest = JSON.parse(readFileSync(manifestPath, "utf8"));
393
+ } catch (error) {
394
+ throw new Error(`${manifestPath}: ${error.message}`);
395
+ }
396
+ const exportsMap = manifest.publishConfig?.exports ?? manifest.exports;
397
+ const failures = [];
398
+ if (target && exportsMap && typeof exportsMap === "object") {
399
+ const configuredSubpaths = new Set(Object.keys(target.entrypoints));
400
+ for (const [subpath, mapping] of Object.entries(exportsMap)) if (stringsIn(mapping).filter((entry) => !extname(entry) || /\.(?:[cm]?[jt]sx?|tsrx)$/.test(entry)).length && !configuredSubpaths.has(subpath)) failures.push(`${subpath} has code exports but no ${target.name} entrypoint config`);
401
+ for (const [subpath, entry] of Object.entries(target.entrypoints)) {
402
+ const mapping = exportsMap[subpath];
403
+ if (!mapping) {
404
+ failures.push(`${subpath} is configured for ${target.name} but missing from effective package exports`);
405
+ continue;
406
+ }
407
+ const source = resolve(projectRoot, entry.source);
408
+ if (![...sourceOutputs.values()].some((included) => resolve(included) === source)) failures.push(`${subpath} source is not included by its TypeScript project: ${entry.source}`);
409
+ const declaredRuntime = stringsIn(entry.runtime);
410
+ const publishedRuntime = stringsIn(mapping);
411
+ for (const runtime of declaredRuntime) if (!publishedRuntime.includes(runtime)) failures.push(`${subpath} runtime mapping is not present in package exports: ${runtime}`);
412
+ const declaredTypes = stringsIn(entry.types);
413
+ const publishedTypes = typeTargets(mapping);
414
+ for (const typePath of declaredTypes) if (!publishedTypes.includes(typePath)) failures.push(`${subpath} types mapping is not present in package exports: ${typePath}`);
415
+ }
416
+ }
417
+ if (exportsMap && typeof exportsMap === "object") for (const [subpath, mapping] of Object.entries(exportsMap)) {
418
+ if (!stringsIn(mapping).filter((target) => !extname(target) || /\.(?:[cm]?[jt]sx?|tsrx)$/.test(target)).length) continue;
419
+ const targets = typeTargets(mapping);
420
+ if (!targets.length) {
421
+ failures.push(`${subpath} has code exports but no types condition`);
422
+ continue;
423
+ }
424
+ for (const target of targets) {
425
+ if (!target.startsWith("./") || target.includes("*")) continue;
426
+ const absolute = resolve(projectRoot, target);
427
+ if (!pathInside(projectRoot, absolute)) {
428
+ failures.push(`${subpath} types target escapes the package: ${target}`);
429
+ continue;
430
+ }
431
+ const generatedPath = relative(outDir, absolute);
432
+ if (pathInside(outDir, absolute) && generated.has(generatedPath)) continue;
433
+ try {
434
+ if (!statSync(absolute).isFile()) failures.push(`${subpath} types target is not a file: ${target}`);
435
+ } catch {
436
+ failures.push(`${subpath} types target is missing: ${target}`);
437
+ }
438
+ }
439
+ }
440
+ else if (manifest.types ?? manifest.typings) {
441
+ const targetPath = manifest.types ?? manifest.typings;
442
+ const target = resolve(projectRoot, targetPath);
443
+ if (!pathInside(projectRoot, target)) failures.push(`package types target escapes the package: ${targetPath}`);
444
+ else if (!(pathInside(outDir, target) && generated.has(relative(outDir, target)))) try {
445
+ if (!statSync(target).isFile()) failures.push(`package types target is not a file: ${targetPath}`);
446
+ } catch {
447
+ failures.push(`package types target is missing: ${targetPath}`);
448
+ }
449
+ }
450
+ if (failures.length) throw new Error(failures.join("\n"));
451
+ }
452
+ function runTarget(projectRoot, args) {
453
+ const { project, target, sourceExtensions, overrides } = configOptions(projectRoot, args.project, args.target);
454
+ const tsPath = loadProjectModule(projectRoot, "typescript");
455
+ const ts = createRequire(import.meta.url)(tsPath);
456
+ const { outDir, outputFlag, rootDir, fileNames } = readProjectConfig(ts, project, projectRoot, target);
457
+ const { sourceOutputs, runtimeByDeclaration } = sourceDeclarationMap(rootDir, fileNames, sourceExtensions, new Set(Object.keys(overrides).map(normalize)));
458
+ const compilerPath = loadProjectModule(projectRoot, "@tsrx/typescript-plugin/dist/tsc.js");
459
+ const temporaryRoot = mkdtempSync(join(tmpdir(), "tsrx-typegen-"));
460
+ const temporaryOut = join(temporaryRoot, "types");
461
+ const outputDir = temporaryOut;
462
+ try {
463
+ const compilerArgs = [
464
+ compilerPath,
465
+ "--pretty",
466
+ "false",
467
+ "--project",
468
+ project,
469
+ outputFlag,
470
+ temporaryOut
471
+ ];
472
+ const result = spawnSync(process.execPath, compilerArgs, {
473
+ cwd: projectRoot,
474
+ encoding: "utf8",
475
+ maxBuffer: 33554432
476
+ });
477
+ if (result.stdout) process.stdout.write(result.stdout);
478
+ if (result.stderr) process.stderr.write(result.stderr);
479
+ if (result.error) throw result.error;
480
+ if (/\[tsrx-tsc\]/.test(result.stderr ?? "")) throw new Error("tsrx compiler reported transform diagnostics; declarations were not written. Fix the source or configure an explicit declaration override.");
481
+ if (result.status !== 0) {
482
+ console.error("tsrx-typegen: declaration emit failed; generated files were not changed. Fix compiler diagnostics or configure an explicit declaration override for an unsupported public signature.");
483
+ return false;
484
+ }
485
+ for (const source of fileNames) {
486
+ if (!/\.d\.[cm]?ts$/.test(source) || !pathInside(rootDir, source)) continue;
487
+ const declaration = relative(rootDir, source);
488
+ if (sourceOutputs.has(declaration)) continue;
489
+ const output = join(outputDir, declaration);
490
+ mkdirSync(dirname(output), { recursive: true });
491
+ writeFileSync(output, readFileSync(source));
492
+ }
493
+ const generated = normalizeOutput(ts, outputDir, sourceExtensions, runtimeByDeclaration);
494
+ const managedOverrides = applyOverrides(ts, projectRoot, outDir, generated, overrides);
495
+ verifySourceDeclarations(generated, sourceOutputs);
496
+ const oldFiles = previousManifest(outDir);
497
+ verifyPublicTypes(projectRoot, generated, outDir, target, sourceOutputs);
498
+ if (args.check) {
499
+ const errors = compareOutput(outDir, generated, oldFiles, managedOverrides);
500
+ if (errors.length) {
501
+ for (const error of errors) console.error(`tsrx-typegen: ${error}`);
502
+ return false;
503
+ }
504
+ console.log(`tsrx-typegen: ${generated.size} declaration file(s) are current`);
505
+ } else {
506
+ writeOutput(outDir, generated, oldFiles, managedOverrides);
507
+ console.log(`tsrx-typegen: wrote ${generated.size} declaration file(s) to ${relative(projectRoot, outDir) || outDir}`);
508
+ }
509
+ return true;
510
+ } finally {
511
+ rmSync(temporaryRoot, {
512
+ recursive: true,
513
+ force: true
514
+ });
515
+ }
516
+ }
517
+ function run() {
518
+ const args = parseArgs(process.argv.slice(2));
519
+ const projectRoot = process.cwd();
520
+ if (!args.packCheck) {
521
+ if (!runTarget(projectRoot, args)) process.exitCode = 1;
522
+ return;
523
+ }
524
+ const { names, targets } = configuredTargetNames(projectRoot, args.target);
525
+ for (const target of names) {
526
+ if (targets[target]?.emit === false) continue;
527
+ if (!runTarget(projectRoot, {
528
+ ...args,
529
+ target,
530
+ check: true
531
+ })) {
532
+ process.exitCode = 1;
533
+ return;
534
+ }
535
+ }
536
+ const tsPath = loadProjectModule(projectRoot, "typescript");
537
+ const ts = createRequire(import.meta.url)(tsPath);
538
+ checkPackedPackage(projectRoot, ts);
539
+ }
540
+ try {
541
+ run();
542
+ } catch (error) {
543
+ console.error(`tsrx-typegen: ${error.message}`);
544
+ process.exitCode = 1;
545
+ }
546
+ //#endregion
@@ -0,0 +1,394 @@
1
+ import { spawnSync } from "node:child_process";
2
+ import { mkdirSync, mkdtempSync, readFileSync, readdirSync, rmSync, statSync } from "node:fs";
3
+ import { tmpdir } from "node:os";
4
+ import { dirname, extname, isAbsolute, join, relative, resolve, sep } from "node:path";
5
+ //#region src/pack-check.ts
6
+ var moduleSpecifierNodes = (ts, sourceFile) => {
7
+ const nodes = [];
8
+ const visit = (node) => {
9
+ if ((ts.isImportDeclaration(node) || ts.isExportDeclaration(node)) && node.moduleSpecifier && ts.isStringLiteralLike(node.moduleSpecifier)) nodes.push(node.moduleSpecifier);
10
+ else if (ts.isImportTypeNode(node)) {
11
+ const argument = node.argument;
12
+ if (ts.isLiteralTypeNode(argument) && ts.isStringLiteralLike(argument.literal)) nodes.push(argument.literal);
13
+ } else if (ts.isExternalModuleReference(node) && node.expression && ts.isStringLiteral(node.expression)) nodes.push(node.expression);
14
+ ts.forEachChild(node, visit);
15
+ };
16
+ visit(sourceFile);
17
+ return nodes;
18
+ };
19
+ var pathInside = (root, file) => {
20
+ const rel = relative(root, file);
21
+ return rel === "" || rel !== ".." && !rel.startsWith(`..${sep}`) && !isAbsolute(rel);
22
+ };
23
+ var stringsIn = (value) => {
24
+ if (typeof value === "string") return [value];
25
+ if (!value || typeof value !== "object") return [];
26
+ return Object.values(value).flatMap(stringsIn);
27
+ };
28
+ var codeTarget = (target) => (!extname(target) || /\.(?:[cm]?[jt]sx?|tsrx)$/.test(target)) && !/\.d\.[cm]?ts$/.test(target);
29
+ function exportBranches(exportsMap, fallbackTypes = []) {
30
+ const branches = [];
31
+ for (const [subpath, mapping] of Object.entries(exportsMap ?? {})) {
32
+ const visit = (value, inheritedTypes) => {
33
+ if (typeof value === "string") {
34
+ if (codeTarget(value)) branches.push({
35
+ subpath,
36
+ runtime: value,
37
+ types: inheritedTypes.length ? inheritedTypes : fallbackTypes
38
+ });
39
+ return;
40
+ }
41
+ if (!value || typeof value !== "object" || Array.isArray(value)) return;
42
+ let types = inheritedTypes;
43
+ for (const [condition, child] of Object.entries(value)) if (condition === "types") {
44
+ if (!types.length) types = stringsIn(child);
45
+ } else visit(child, types);
46
+ };
47
+ visit(mapping, []);
48
+ }
49
+ return branches;
50
+ }
51
+ function packageFiles(root) {
52
+ const files = [];
53
+ const visit = (dir) => {
54
+ for (const entry of readdirSync(dir, { withFileTypes: true })) {
55
+ if (entry.name === "node_modules") continue;
56
+ const file = join(dir, entry.name);
57
+ if (entry.isDirectory()) visit(file);
58
+ else files.push(file);
59
+ }
60
+ };
61
+ visit(root);
62
+ return files;
63
+ }
64
+ function expandTarget(target, files, packageRoot) {
65
+ const extensionless = !extname(target);
66
+ if (!target.includes("*") && !extensionless) return [{
67
+ path: target,
68
+ capture: void 0
69
+ }];
70
+ if ((target.match(/\*/g) ?? []).length > 1) throw new Error(`packed export target uses more than one wildcard: ${target}`);
71
+ if (!target.startsWith("./")) throw new Error(`packed export target must be package-relative: ${target}`);
72
+ const pattern = target.slice(2);
73
+ const marker = pattern.indexOf("*");
74
+ const prefix = marker === -1 ? pattern : pattern.slice(0, marker);
75
+ const suffix = marker === -1 ? "" : pattern.slice(marker + 1);
76
+ const matches = files.flatMap((file) => {
77
+ const path = relative(packageRoot, file).replaceAll("\\", "/");
78
+ if (!codeTarget(path) || extensionless && !extname(path)) return [];
79
+ const matchedPath = extensionless ? path.slice(0, -extname(path).length) : path;
80
+ if (!matchedPath.startsWith(prefix) || !matchedPath.endsWith(suffix) || matchedPath.length < prefix.length + suffix.length || marker === -1 && matchedPath !== pattern) return [];
81
+ const capture = marker === -1 ? void 0 : matchedPath.slice(prefix.length, matchedPath.length - suffix.length);
82
+ return [{
83
+ path: `./${path}`,
84
+ capture
85
+ }];
86
+ });
87
+ if (!matches.length) throw new Error(`packed export target matches no files: ${target}`);
88
+ return matches;
89
+ }
90
+ function packedPath(packageRoot, target) {
91
+ if (typeof target !== "string" || !target.startsWith("./") || target.includes("*")) throw new Error(`packed export target is not a concrete package-relative path: ${target}`);
92
+ const file = resolve(packageRoot, target);
93
+ if (!pathInside(packageRoot, file)) throw new Error(`packed export target escapes the package: ${target}`);
94
+ try {
95
+ if (statSync(file).isFile()) return file;
96
+ } catch {}
97
+ throw new Error(`packed export target is missing from the tarball: ${target}`);
98
+ }
99
+ function sourceFile(ts, file) {
100
+ const extension = extname(file).toLowerCase();
101
+ const scriptKind = extension === ".tsx" || extension === ".tsrx" ? ts.ScriptKind.TSX : extension === ".jsx" ? ts.ScriptKind.JSX : ts.ScriptKind.TS;
102
+ return ts.createSourceFile(file, readFileSync(file, "utf8"), ts.ScriptTarget.Latest, true, scriptKind);
103
+ }
104
+ function localSourceModule(file, specifier) {
105
+ if (!specifier.startsWith(".")) return null;
106
+ const target = resolve(dirname(file), specifier);
107
+ const extension = extname(target);
108
+ const candidates = [target];
109
+ if (extension === ".js" || extension === ".mjs" || extension === ".cjs") {
110
+ const stem = target.slice(0, -extension.length);
111
+ candidates.push(`${stem}.ts`, `${stem}.tsx`, `${stem}.tsrx`, `${stem}.d.ts`);
112
+ } else if (![
113
+ ".ts",
114
+ ".tsx",
115
+ ".tsrx"
116
+ ].includes(extension)) {
117
+ candidates.push(`${target}.ts`, `${target}.tsx`, `${target}.tsrx`, `${target}.d.ts`);
118
+ candidates.push(join(target, "index.ts"), join(target, "index.tsx"), join(target, "index.tsrx"), join(target, "index.d.ts"));
119
+ }
120
+ for (const candidate of candidates) try {
121
+ if (statSync(candidate).isFile()) return candidate;
122
+ } catch {}
123
+ return null;
124
+ }
125
+ function staticRuntimeExports(ts, file, seen = /* @__PURE__ */ new Set()) {
126
+ const absolute = resolve(file);
127
+ if (seen.has(absolute)) return /* @__PURE__ */ new Set();
128
+ seen.add(absolute);
129
+ const source = sourceFile(ts, absolute);
130
+ const names = /* @__PURE__ */ new Set();
131
+ const addBindingName = (name) => {
132
+ if (ts.isIdentifier(name)) names.add(name.text);
133
+ else if (ts.isObjectBindingPattern(name) || ts.isArrayBindingPattern(name)) {
134
+ for (const element of name.elements) if (element && ts.isBindingElement(element)) addBindingName(element.name);
135
+ }
136
+ };
137
+ for (const statement of source.statements) {
138
+ const modifiers = ts.canHaveModifiers(statement) ? ts.getModifiers(statement) ?? [] : [];
139
+ const exported = modifiers.some((modifier) => modifier.kind === ts.SyntaxKind.ExportKeyword);
140
+ if (modifiers.some((modifier) => modifier.kind === ts.SyntaxKind.DefaultKeyword)) names.add("default");
141
+ if (exported) {
142
+ if (ts.isFunctionDeclaration(statement) || ts.isClassDeclaration(statement) || ts.isEnumDeclaration(statement) || ts.isModuleDeclaration(statement)) {
143
+ if (statement.name) names.add(statement.name.text);
144
+ }
145
+ if (ts.isVariableStatement(statement)) for (const declaration of statement.declarationList.declarations) addBindingName(declaration.name);
146
+ }
147
+ if (ts.isExportDeclaration(statement) && !statement.isTypeOnly) {
148
+ if (!statement.exportClause && statement.moduleSpecifier && ts.isStringLiteralLike(statement.moduleSpecifier)) {
149
+ const target = localSourceModule(absolute, statement.moduleSpecifier.text);
150
+ if (target) {
151
+ for (const name of staticRuntimeExports(ts, target, seen)) if (name !== "default") names.add(name);
152
+ }
153
+ } else if (statement.exportClause && ts.isNamedExports(statement.exportClause)) {
154
+ for (const element of statement.exportClause.elements) if (!element.isTypeOnly) names.add(element.name.text);
155
+ } else if (statement.exportClause && ts.isNamespaceExport(statement.exportClause)) names.add(statement.exportClause.name.text);
156
+ }
157
+ if (ts.isExportAssignment(statement)) names.add("default");
158
+ }
159
+ return names;
160
+ }
161
+ function compilerExports(ts, program, file, cache = /* @__PURE__ */ new Map(), pending = /* @__PURE__ */ new Set()) {
162
+ const absolute = resolve(file);
163
+ if (cache.has(absolute)) return cache.get(absolute);
164
+ if (pending.has(absolute)) return {
165
+ all: /* @__PURE__ */ new Set(),
166
+ values: /* @__PURE__ */ new Set()
167
+ };
168
+ const source = program.getSourceFile(file);
169
+ if (!source) return;
170
+ const checker = program.getTypeChecker();
171
+ const module = checker.getSymbolAtLocation(source);
172
+ if (!module) return;
173
+ const all = /* @__PURE__ */ new Set();
174
+ const values = /* @__PURE__ */ new Set();
175
+ for (const symbol of checker.getExportsOfModule(module)) {
176
+ all.add(symbol.name);
177
+ let resolved = symbol;
178
+ if (symbol.flags & ts.SymbolFlags.Alias) try {
179
+ resolved = checker.getAliasedSymbol(symbol);
180
+ } catch {}
181
+ if (!symbol.declarations?.some((declaration) => ts.isExportSpecifier(declaration) && (declaration.isTypeOnly || declaration.parent.parent.isTypeOnly)) && resolved.flags & ts.SymbolFlags.Value) values.add(symbol.name);
182
+ }
183
+ const typeOnly = /* @__PURE__ */ new Set();
184
+ for (const statement of source.statements) {
185
+ if (!ts.isExportDeclaration(statement)) continue;
186
+ if (statement.exportClause && ts.isNamedExports(statement.exportClause)) {
187
+ for (const element of statement.exportClause.elements) if (statement.isTypeOnly || element.isTypeOnly) typeOnly.add(element.name.text);
188
+ continue;
189
+ }
190
+ if (!statement.isTypeOnly) continue;
191
+ if (!statement.exportClause && statement.moduleSpecifier && ts.isStringLiteralLike(statement.moduleSpecifier)) {
192
+ pending.add(absolute);
193
+ const target = ts.resolveModuleName(statement.moduleSpecifier.text, file, program.getCompilerOptions(), ts.sys).resolvedModule?.resolvedFileName;
194
+ const targetExports = target && compilerExports(ts, program, target, cache, pending);
195
+ pending.delete(absolute);
196
+ for (const name of targetExports?.all ?? []) typeOnly.add(name);
197
+ } else if (statement.exportClause && ts.isNamedExports(statement.exportClause)) for (const element of statement.exportClause.elements) typeOnly.add(element.name.text);
198
+ else if (statement.exportClause && ts.isNamespaceExport(statement.exportClause)) typeOnly.add(statement.exportClause.name.text);
199
+ }
200
+ if (typeOnly.size) {
201
+ const valueEdges = staticRuntimeExports(ts, file);
202
+ for (const name of values) if (typeOnly.has(name) && !valueEdges.has(name)) values.delete(name);
203
+ }
204
+ const result = {
205
+ all,
206
+ values
207
+ };
208
+ cache.set(absolute, result);
209
+ return result;
210
+ }
211
+ function declarationFiles(packageRoot, files) {
212
+ return files.filter((file) => /\.d\.[cm]?ts$/.test(relative(packageRoot, file)));
213
+ }
214
+ function moduleResolutionOptions(ts) {
215
+ return {
216
+ allowImportingTsExtensions: true,
217
+ module: ts.ModuleKind.NodeNext,
218
+ moduleResolution: ts.ModuleResolutionKind.NodeNext,
219
+ moduleSuffixes: [
220
+ "",
221
+ ".web",
222
+ ".ios",
223
+ ".android",
224
+ ".mobile",
225
+ ".native",
226
+ ".macos",
227
+ ".linux",
228
+ ".windows"
229
+ ],
230
+ noEmit: true,
231
+ skipLibCheck: true,
232
+ target: ts.ScriptTarget.Latest
233
+ };
234
+ }
235
+ function verifyDeclarationClosure(ts, packageRoot, files, manifest, failures) {
236
+ const options = moduleResolutionOptions(ts);
237
+ const host = ts.sys;
238
+ const declared = /* @__PURE__ */ new Set([
239
+ manifest.name,
240
+ ...Object.keys(manifest.dependencies ?? {}),
241
+ ...Object.keys(manifest.optionalDependencies ?? {}),
242
+ ...Object.keys(manifest.peerDependencies ?? {}),
243
+ ...manifest.bundledDependencies ?? []
244
+ ]);
245
+ for (const file of declarationFiles(packageRoot, files)) {
246
+ const source = sourceFile(ts, file);
247
+ for (const node of moduleSpecifierNodes(ts, source)) {
248
+ const specifier = node.text;
249
+ if (specifier.startsWith(".") || specifier.startsWith("#")) {
250
+ const resolved = ts.resolveModuleName(specifier, file, options, host).resolvedModule;
251
+ if (!resolved) failures.push(`${relative(packageRoot, file)} has unresolved declaration reference ${specifier}`);
252
+ else if (!pathInside(packageRoot, resolve(resolved.resolvedFileName))) failures.push(`${relative(packageRoot, file)} resolves ${specifier} outside the package`);
253
+ else if (!/\.d\.[cm]?ts$/.test(resolved.resolvedFileName)) failures.push(`${relative(packageRoot, file)} resolves ${specifier} to implementation source; publish its declaration`);
254
+ continue;
255
+ }
256
+ if (specifier.startsWith("node:")) {
257
+ if (!declared.has("@types/node")) failures.push(`${relative(packageRoot, file)} imports ${specifier}; declare @types/node as a dependency or peer`);
258
+ continue;
259
+ }
260
+ if (specifier.startsWith("/")) {
261
+ failures.push(`${relative(packageRoot, file)} has absolute declaration reference ${specifier}`);
262
+ continue;
263
+ }
264
+ const name = specifier.startsWith("@") ? specifier.split("/").slice(0, 2).join("/") : specifier.split("/")[0];
265
+ if (!declared.has(name)) failures.push(`${relative(packageRoot, file)} imports ${specifier}; declare ${name} in dependencies or peerDependencies`);
266
+ }
267
+ }
268
+ }
269
+ function verifyExportMap(ts, packageRoot, files, manifest, exportsMap, failures, options) {
270
+ const branches = exportBranches(exportsMap, [manifest.types ?? manifest.typings].filter(Boolean));
271
+ if (!branches.length) {
272
+ failures.push("package exports contain no runtime entrypoints to verify");
273
+ return;
274
+ }
275
+ const pairs = [];
276
+ for (const branch of branches) {
277
+ if (!branch.types.length) {
278
+ failures.push(`${branch.subpath} runtime ${branch.runtime} has no types condition`);
279
+ continue;
280
+ }
281
+ let runtimeMatches;
282
+ try {
283
+ runtimeMatches = expandTarget(branch.runtime, files, packageRoot);
284
+ } catch (error) {
285
+ failures.push(`${branch.subpath}: ${error.message}`);
286
+ continue;
287
+ }
288
+ for (const runtimeMatch of runtimeMatches) {
289
+ let runtimeFile;
290
+ try {
291
+ runtimeFile = packedPath(packageRoot, runtimeMatch.path);
292
+ } catch (error) {
293
+ failures.push(`${branch.subpath}: ${error.message}`);
294
+ continue;
295
+ }
296
+ for (const typeTarget of branch.types) {
297
+ const concreteType = typeTarget.includes("*") && runtimeMatch.capture !== void 0 ? typeTarget.replace("*", runtimeMatch.capture) : typeTarget;
298
+ let typeFile;
299
+ try {
300
+ typeFile = packedPath(packageRoot, concreteType);
301
+ } catch (error) {
302
+ failures.push(`${branch.subpath}: ${error.message}`);
303
+ continue;
304
+ }
305
+ pairs.push({
306
+ branch,
307
+ runtimeFile,
308
+ runtimePath: runtimeMatch.path,
309
+ typeFile,
310
+ concreteType
311
+ });
312
+ }
313
+ }
314
+ }
315
+ const programRoots = [...new Set(pairs.flatMap(({ runtimeFile, typeFile }) => [runtimeFile, typeFile]))].filter((file) => !file.endsWith(".tsrx"));
316
+ const program = ts.createProgram(programRoots, options);
317
+ for (const pair of pairs) {
318
+ const declarationExports = compilerExports(ts, program, pair.typeFile);
319
+ const runtimeExports = compilerExports(ts, program, pair.runtimeFile);
320
+ const declaredValues = declarationExports?.values ?? /* @__PURE__ */ new Set();
321
+ let runtimeValues;
322
+ if (pair.runtimeFile.endsWith(".ts") || pair.runtimeFile.endsWith(".tsx") || pair.runtimeFile.endsWith(".tsrx")) {
323
+ runtimeValues = staticRuntimeExports(ts, pair.runtimeFile);
324
+ for (const name of declarationExports?.all ?? []) if (!declaredValues.has(name)) runtimeValues.delete(name);
325
+ } else runtimeValues = runtimeExports?.values ?? staticRuntimeExports(ts, pair.runtimeFile);
326
+ const missing = [...runtimeValues].filter((name) => !declaredValues.has(name)).sort();
327
+ const extra = [...declaredValues].filter((name) => !runtimeValues.has(name)).sort();
328
+ if (missing.length || extra.length) failures.push(`${pair.branch.subpath} runtime/type value exports differ for ${pair.runtimePath} → ${pair.concreteType}${missing.length ? `; missing declarations: ${missing.join(", ")}` : ""}${extra.length ? `; declarations without runtime values: ${extra.join(", ")}` : ""}`);
329
+ }
330
+ }
331
+ function verifyPackedPackage(ts, packageRoot) {
332
+ const failures = [];
333
+ const manifestPath = join(packageRoot, "package.json");
334
+ let manifest;
335
+ try {
336
+ manifest = JSON.parse(readFileSync(manifestPath, "utf8"));
337
+ } catch (error) {
338
+ throw new Error(`packed package.json: ${error.message}`);
339
+ }
340
+ const files = packageFiles(packageRoot);
341
+ const maps = [manifest.exports, manifest.publishConfig?.exports].filter((value, index, all) => value && all.indexOf(value) === index);
342
+ if (maps.length) for (const exportsMap of maps) verifyExportMap(ts, packageRoot, files, manifest, exportsMap, failures, moduleResolutionOptions(ts));
343
+ else if (manifest.main || manifest.module) verifyExportMap(ts, packageRoot, files, manifest, { ".": manifest.module ?? manifest.main }, failures, moduleResolutionOptions(ts));
344
+ else failures.push("package has no exports, main, or module entrypoint");
345
+ verifyDeclarationClosure(ts, packageRoot, files, manifest, failures);
346
+ if (failures.length) throw new Error(failures.join("\n"));
347
+ }
348
+ function checkPackedPackage(projectRoot, ts) {
349
+ const temporary = mkdtempSync(join(tmpdir(), "tsrx-typegen-pack-"));
350
+ const packOutput = join(temporary, "pack");
351
+ const extractedRoot = join(temporary, "extracted");
352
+ mkdirSync(packOutput);
353
+ mkdirSync(extractedRoot);
354
+ try {
355
+ const packed = spawnSync("pnpm", [
356
+ "--config.ignore-scripts=true",
357
+ "pack",
358
+ "--pack-destination",
359
+ packOutput
360
+ ], {
361
+ cwd: projectRoot,
362
+ encoding: "utf8",
363
+ maxBuffer: 16777216
364
+ });
365
+ if (packed.stdout) process.stdout.write(packed.stdout);
366
+ if (packed.stderr) process.stderr.write(packed.stderr);
367
+ if (packed.error) throw packed.error;
368
+ if (packed.status !== 0) throw new Error(`pnpm pack exited with ${packed.status}`);
369
+ const tarballs = readdirSync(packOutput).filter((name) => name.endsWith(".tgz"));
370
+ if (tarballs.length !== 1) throw new Error(`expected one package tarball; found ${tarballs.length}`);
371
+ const extracted = spawnSync("tar", [
372
+ "-xzf",
373
+ join(packOutput, tarballs[0]),
374
+ "-C",
375
+ extractedRoot
376
+ ], {
377
+ encoding: "utf8",
378
+ maxBuffer: 16777216
379
+ });
380
+ if (extracted.stdout) process.stdout.write(extracted.stdout);
381
+ if (extracted.stderr) process.stderr.write(extracted.stderr);
382
+ if (extracted.error) throw extracted.error;
383
+ if (extracted.status !== 0) throw new Error(`tar extraction exited with ${extracted.status}`);
384
+ verifyPackedPackage(ts, join(extractedRoot, "package"));
385
+ console.log("tsrx-typegen: packed exports and declaration graph are valid");
386
+ } finally {
387
+ rmSync(temporary, {
388
+ recursive: true,
389
+ force: true
390
+ });
391
+ }
392
+ }
393
+ //#endregion
394
+ export { checkPackedPackage, verifyPackedPackage };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tsrx-typegen",
3
- "version": "0.3.0",
3
+ "version": "0.4.0",
4
4
  "description": "Generate consumer-ready TypeScript declarations for TSRX libraries",
5
5
  "keywords": [
6
6
  "build-tool",
@@ -19,17 +19,25 @@
19
19
  "directory": "packages/tsrx-typegen"
20
20
  },
21
21
  "bin": {
22
- "tsrx-typegen": "./src/cli.mjs"
22
+ "tsrx-typegen": "./bin/tsrx-typegen.mjs"
23
23
  },
24
24
  "files": [
25
- "src"
25
+ "src",
26
+ "dist",
27
+ "bin"
26
28
  ],
27
29
  "type": "module",
30
+ "devDependencies": {
31
+ "@types/node": "24.10.1",
32
+ "vite": "8.3.0"
33
+ },
28
34
  "peerDependencies": {
29
35
  "@tsrx/typescript-plugin": ">=0.4.11 <1",
30
36
  "typescript": ">=5.9.3 <6"
31
37
  },
32
38
  "scripts": {
33
- "test": "node tests/pack-check.mjs && node tests/packed-consumer.mjs"
39
+ "build": "vite build",
40
+ "test": "pnpm build && node tests/pack-check.mjs && node tests/packed-consumer.mjs",
41
+ "typecheck": "tsc -p tsconfig.json"
34
42
  }
35
43
  }
@@ -13,7 +13,7 @@ import {
13
13
 
14
14
  import { tmpdir } from 'node:os'
15
15
  import { dirname, extname, isAbsolute, join, normalize, relative, resolve, sep } from 'node:path'
16
- import { checkPackedPackage } from './pack-check.mjs'
16
+ import { checkPackedPackage } from './pack-check.ts'
17
17
 
18
18
  const manifestName = '.tsrx-typegen-manifest.json'
19
19
  const defaultExtensions = {
@@ -91,7 +91,7 @@ function configuredTargetNames(projectRoot, selectedTarget) {
91
91
  throw new Error(`${path}: ${error.message}`)
92
92
  }
93
93
 
94
- const targets = config?.targets
94
+ const targets: Record<string, any> | undefined = config?.targets
95
95
  if (
96
96
  !targets ||
97
97
  typeof targets !== 'object' ||
@@ -183,7 +183,7 @@ function commonDirectory(paths) {
183
183
  }
184
184
 
185
185
  const segments = paths.map((path) => resolve(dirname(path)).split(sep))
186
- const common = []
186
+ const common: any[] = []
187
187
  for (let index = 0; segments.every((parts) => parts[index] === segments[0][index]); index += 1) {
188
188
  common.push(segments[0][index])
189
189
  }
@@ -192,8 +192,8 @@ function commonDirectory(paths) {
192
192
  }
193
193
 
194
194
  function sourceDeclarationMap(rootDir, fileNames, extensions, overrides) {
195
- const sourceOutputs = new Map()
196
- const runtimeByDeclaration = new Map()
195
+ const sourceOutputs = new Map<any, any>()
196
+ const runtimeByDeclaration = new Map<any, any>()
197
197
  for (const source of fileNames) {
198
198
  const sourceExtension = extname(source).toLowerCase()
199
199
  if (!Object.hasOwn(extensions, sourceExtension) || /\.d\.[cm]?ts$/i.test(source)) {
@@ -254,7 +254,7 @@ function configOptions(projectRoot, projectArgument, targetArgument) {
254
254
  throw new Error(`${path}: expected a JSON object`)
255
255
  }
256
256
 
257
- const targets = config.targets ?? {}
257
+ const targets: Record<string, any> = config.targets ?? {}
258
258
  if (!targets || typeof targets !== 'object' || Array.isArray(targets)) {
259
259
  throw new Error(`${path}: targets must be an object`)
260
260
  }
@@ -310,7 +310,7 @@ function configOptions(projectRoot, projectArgument, targetArgument) {
310
310
  throw new Error(`${path}: target ${selectedName} must declare entrypoints`)
311
311
  }
312
312
 
313
- for (const [subpath, entry] of Object.entries(selected.entrypoints)) {
313
+ for (const [subpath, entry] of Object.entries<any>(selected.entrypoints)) {
314
314
  if (
315
315
  !entry ||
316
316
  typeof entry !== 'object' ||
@@ -393,7 +393,7 @@ function configOptions(projectRoot, projectArgument, targetArgument) {
393
393
  }
394
394
 
395
395
  function applyOverrides(ts, projectRoot, outputDir, generated, overrides) {
396
- const normalized = new Map()
396
+ const normalized = new Map<any, any>()
397
397
  for (const [outputPath, sourcePath] of Object.entries(overrides)) {
398
398
  if (typeof sourcePath !== 'string' || !sourcePath) {
399
399
  throw new Error(`override for ${outputPath} must name a source declaration file`)
@@ -429,7 +429,7 @@ function applyOverrides(ts, projectRoot, outputDir, generated, overrides) {
429
429
  return new Set(normalized.keys())
430
430
  }
431
431
 
432
- function collectFiles(root, output = []) {
432
+ function collectFiles(root, output: any[] = []) {
433
433
  if (!statSync(root).isDirectory()) {
434
434
  output.push(root)
435
435
  return output
@@ -448,7 +448,7 @@ function collectFiles(root, output = []) {
448
448
  }
449
449
 
450
450
  function moduleSpecifierNodes(ts, sourceFile) {
451
- const nodes = []
451
+ const nodes: any[] = []
452
452
  const visit = (node) => {
453
453
  if (
454
454
  (ts.isImportDeclaration(node) || ts.isExportDeclaration(node)) &&
@@ -490,7 +490,7 @@ function assertNoTsrxModuleReferences(ts, path, text) {
490
490
  function rewriteDeclaration(ts, path, outputDir, extensions, generatedFiles, runtimeByDeclaration) {
491
491
  const text = readFileSync(path, 'utf8')
492
492
  const source = ts.createSourceFile(path, text, ts.ScriptTarget.Latest, true, ts.ScriptKind.TS)
493
- const edits = []
493
+ const edits: any[] = []
494
494
  for (const node of moduleSpecifierNodes(ts, source)) {
495
495
  const specifier = node.text
496
496
  if (!specifier.startsWith('.')) {
@@ -610,7 +610,7 @@ function pathInside(root, file) {
610
610
 
611
611
  function existingOutputPath(outputDir, file) {
612
612
  let current = outputDir
613
- const actualParts = []
613
+ const actualParts: any[] = []
614
614
 
615
615
  for (const part of normalize(file).split(sep)) {
616
616
  let entries
@@ -672,7 +672,7 @@ function writeOutput(outputDir, generated, oldFiles, overrideFiles) {
672
672
  }
673
673
 
674
674
  function compareOutput(outputDir, generated, oldFiles, overrideFiles) {
675
- const errors = []
675
+ const errors: any[] = []
676
676
  if (!oldFiles) {
677
677
  errors.push(`missing ${manifestName}; run tsrx-typegen --project <tsconfig> first`)
678
678
  }
@@ -742,7 +742,7 @@ function verifyPublicTypes(projectRoot, generated, outDir, target, sourceOutputs
742
742
  }
743
743
 
744
744
  const exportsMap = manifest.publishConfig?.exports ?? manifest.exports
745
- const failures = []
745
+ const failures: any[] = []
746
746
  if (target && exportsMap && typeof exportsMap === 'object') {
747
747
  const configuredSubpaths = new Set(Object.keys(target.entrypoints))
748
748
  for (const [subpath, mapping] of Object.entries(exportsMap)) {
@@ -755,7 +755,7 @@ function verifyPublicTypes(projectRoot, generated, outDir, target, sourceOutputs
755
755
  }
756
756
  }
757
757
 
758
- for (const [subpath, entry] of Object.entries(target.entrypoints)) {
758
+ for (const [subpath, entry] of Object.entries<any>(target.entrypoints)) {
759
759
  const mapping = exportsMap[subpath]
760
760
  if (!mapping) {
761
761
  failures.push(
@@ -4,7 +4,7 @@ import { tmpdir } from 'node:os'
4
4
  import { dirname, extname, isAbsolute, join, relative, resolve, sep } from 'node:path'
5
5
 
6
6
  const moduleSpecifierNodes = (ts, sourceFile) => {
7
- const nodes = []
7
+ const nodes: any[] = []
8
8
  const visit = (node) => {
9
9
  if (
10
10
  (ts.isImportDeclaration(node) || ts.isExportDeclaration(node)) &&
@@ -52,8 +52,8 @@ const stringsIn = (value) => {
52
52
  const codeTarget = (target) =>
53
53
  (!extname(target) || /\.(?:[cm]?[jt]sx?|tsrx)$/.test(target)) && !/\.d\.[cm]?ts$/.test(target)
54
54
 
55
- function exportBranches(exportsMap, fallbackTypes = []) {
56
- const branches = []
55
+ function exportBranches(exportsMap, fallbackTypes: any[] = []) {
56
+ const branches: any[] = []
57
57
  for (const [subpath, mapping] of Object.entries(exportsMap ?? {})) {
58
58
  const visit = (value, inheritedTypes) => {
59
59
  if (typeof value === 'string') {
@@ -92,7 +92,7 @@ function exportBranches(exportsMap, fallbackTypes = []) {
92
92
  }
93
93
 
94
94
  function packageFiles(root) {
95
- const files = []
95
+ const files: any[] = []
96
96
  const visit = (dir) => {
97
97
  for (const entry of readdirSync(dir, { withFileTypes: true })) {
98
98
  if (entry.name === 'node_modules') {
@@ -232,7 +232,7 @@ function localSourceModule(file, specifier) {
232
232
  return null
233
233
  }
234
234
 
235
- function staticRuntimeExports(ts, file, seen = new Set()) {
235
+ function staticRuntimeExports(ts, file, seen: Set<any> = new Set()) {
236
236
  const absolute = resolve(file)
237
237
  if (seen.has(absolute)) {
238
238
  return new Set()
@@ -241,7 +241,7 @@ function staticRuntimeExports(ts, file, seen = new Set()) {
241
241
  seen.add(absolute)
242
242
 
243
243
  const source = sourceFile(ts, absolute)
244
- const names = new Set()
244
+ const names = new Set<any>()
245
245
  const addBindingName = (name) => {
246
246
  if (ts.isIdentifier(name)) {
247
247
  names.add(name.text)
@@ -314,7 +314,13 @@ function staticRuntimeExports(ts, file, seen = new Set()) {
314
314
  return names
315
315
  }
316
316
 
317
- function compilerExports(ts, program, file, cache = new Map(), pending = new Set()) {
317
+ function compilerExports(
318
+ ts,
319
+ program,
320
+ file,
321
+ cache: Map<any, any> = new Map(),
322
+ pending: Set<any> = new Set(),
323
+ ) {
318
324
  const absolute = resolve(file)
319
325
  if (cache.has(absolute)) {
320
326
  return cache.get(absolute)
@@ -335,8 +341,8 @@ function compilerExports(ts, program, file, cache = new Map(), pending = new Set
335
341
  return undefined
336
342
  }
337
343
 
338
- const all = new Set()
339
- const values = new Set()
344
+ const all = new Set<any>()
345
+ const values = new Set<any>()
340
346
  for (const symbol of checker.getExportsOfModule(module)) {
341
347
  all.add(symbol.name)
342
348
  let resolved = symbol
@@ -346,7 +352,15 @@ function compilerExports(ts, program, file, cache = new Map(), pending = new Set
346
352
  } catch {}
347
353
  }
348
354
 
349
- if (resolved.flags & ts.SymbolFlags.Value) {
355
+ // Star barrels retain the originating alias declaration. An unresolved
356
+ // external type resolves to TypeScript's unknown symbol (which has value
357
+ // flags), so preserve an explicit type-only edge before using those flags.
358
+ const typeOnlyAlias = symbol.declarations?.some(
359
+ (declaration) =>
360
+ ts.isExportSpecifier(declaration) &&
361
+ (declaration.isTypeOnly || declaration.parent.parent.isTypeOnly),
362
+ )
363
+ if (!typeOnlyAlias && resolved.flags & ts.SymbolFlags.Value) {
350
364
  values.add(symbol.name)
351
365
  }
352
366
  }
@@ -355,7 +369,7 @@ function compilerExports(ts, program, file, cache = new Map(), pending = new Set
355
369
  // target's symbols with their original flags — an ambient `declare const`
356
370
  // still reads as a value even though no value binding crosses the edge.
357
371
  // Subtract names whose only export edges are type-only.
358
- const typeOnly = new Set()
372
+ const typeOnly = new Set<any>()
359
373
  for (const statement of source.statements) {
360
374
  if (!ts.isExportDeclaration(statement)) {
361
375
  continue
@@ -517,7 +531,7 @@ function verifyExportMap(ts, packageRoot, files, manifest, exportsMap, failures,
517
531
  return
518
532
  }
519
533
 
520
- const pairs = []
534
+ const pairs: any[] = []
521
535
  for (const branch of branches) {
522
536
  if (!branch.types.length) {
523
537
  failures.push(`${branch.subpath} runtime ${branch.runtime} has no types condition`)
@@ -598,7 +612,7 @@ function verifyExportMap(ts, packageRoot, files, manifest, exportsMap, failures,
598
612
  }
599
613
 
600
614
  export function verifyPackedPackage(ts, packageRoot) {
601
- const failures = []
615
+ const failures: any[] = []
602
616
  const manifestPath = join(packageRoot, 'package.json')
603
617
  let manifest
604
618
  try {