tsrx-typegen 0.2.2 → 0.3.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/README.md +88 -71
- package/bin/tsrx-typegen.mjs +11 -0
- package/dist/cli.js +546 -0
- package/dist/pack-check.js +394 -0
- package/package.json +12 -4
- package/src/{cli.mjs → cli.ts} +37 -20
- package/src/{pack-check.mjs → pack-check.ts} +88 -30
|
@@ -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 (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
|
+
"version": "0.3.1",
|
|
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": "./
|
|
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
|
-
"
|
|
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
|
}
|
package/src/{cli.mjs → cli.ts}
RENAMED
|
@@ -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.
|
|
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('.')) {
|
|
@@ -529,7 +529,7 @@ function rewriteDeclaration(ts, path, outputDir, extensions, generatedFiles, run
|
|
|
529
529
|
|
|
530
530
|
const declaration = relative(outputDir, target)
|
|
531
531
|
const runtimeExtension = runtimeByDeclaration.get(declaration) ?? '.js'
|
|
532
|
-
const isIndex =
|
|
532
|
+
const isIndex = dirname(target) === targetStem
|
|
533
533
|
const baseSpecifier = specifier.endsWith('/') ? specifier.slice(0, -1) : specifier
|
|
534
534
|
replacement = isIndex
|
|
535
535
|
? `${baseSpecifier}/index${runtimeExtension}`
|
|
@@ -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,12 +742,12 @@ 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)) {
|
|
749
|
-
const runtimeTargets = stringsIn(mapping).filter(
|
|
750
|
-
/\.(?:[cm]?[jt]sx?|tsrx)$/.test(entry),
|
|
749
|
+
const runtimeTargets = stringsIn(mapping).filter(
|
|
750
|
+
(entry) => !extname(entry) || /\.(?:[cm]?[jt]sx?|tsrx)$/.test(entry),
|
|
751
751
|
)
|
|
752
752
|
|
|
753
753
|
if (runtimeTargets.length && !configuredSubpaths.has(subpath)) {
|
|
@@ -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(
|
|
@@ -794,8 +794,8 @@ function verifyPublicTypes(projectRoot, generated, outDir, target, sourceOutputs
|
|
|
794
794
|
|
|
795
795
|
if (exportsMap && typeof exportsMap === 'object') {
|
|
796
796
|
for (const [subpath, mapping] of Object.entries(exportsMap)) {
|
|
797
|
-
const runtimeTargets = stringsIn(mapping).filter(
|
|
798
|
-
/\.(?:[cm]?[jt]sx?|tsrx)$/.test(target),
|
|
797
|
+
const runtimeTargets = stringsIn(mapping).filter(
|
|
798
|
+
(target) => !extname(target) || /\.(?:[cm]?[jt]sx?|tsrx)$/.test(target),
|
|
799
799
|
)
|
|
800
800
|
|
|
801
801
|
if (!runtimeTargets.length) {
|
|
@@ -925,6 +925,23 @@ function runTarget(projectRoot, args) {
|
|
|
925
925
|
return false
|
|
926
926
|
}
|
|
927
927
|
|
|
928
|
+
// TypeScript does not emit declaration inputs. Preserve included, local
|
|
929
|
+
// declarations beside emitted modules so their references remain closed.
|
|
930
|
+
for (const source of fileNames) {
|
|
931
|
+
if (!/\.d\.[cm]?ts$/.test(source) || !pathInside(rootDir, source)) {
|
|
932
|
+
continue
|
|
933
|
+
}
|
|
934
|
+
|
|
935
|
+
const declaration = relative(rootDir, source)
|
|
936
|
+
if (sourceOutputs.has(declaration)) {
|
|
937
|
+
continue
|
|
938
|
+
}
|
|
939
|
+
|
|
940
|
+
const output = join(outputDir, declaration)
|
|
941
|
+
mkdirSync(dirname(output), { recursive: true })
|
|
942
|
+
writeFileSync(output, readFileSync(source))
|
|
943
|
+
}
|
|
944
|
+
|
|
928
945
|
const generated = normalizeOutput(ts, outputDir, sourceExtensions, runtimeByDeclaration)
|
|
929
946
|
const managedOverrides = applyOverrides(ts, projectRoot, outDir, generated, overrides)
|
|
930
947
|
verifySourceDeclarations(generated, sourceOutputs)
|