tsrx-typegen 0.3.0 → 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/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} +15 -15
- package/src/{pack-check.mjs → pack-check.ts} +18 -12
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Prefer the emitted dist/ build (published tarballs); fall back to src/ so
|
|
3
|
+
// in-repo typegen needs no build step (Node ≥22.18 strips types).
|
|
4
|
+
import { existsSync } from 'node:fs'
|
|
5
|
+
|
|
6
|
+
const dist = new URL('../dist/cli.js', import.meta.url)
|
|
7
|
+
if (existsSync(dist)) {
|
|
8
|
+
await import(dist.href)
|
|
9
|
+
} else {
|
|
10
|
+
await import(new URL('../src/cli.ts', import.meta.url).href)
|
|
11
|
+
}
|
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 (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.
|
|
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('.')) {
|
|
@@ -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(
|
|
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
|
|
@@ -355,7 +361,7 @@ function compilerExports(ts, program, file, cache = new Map(), pending = new Set
|
|
|
355
361
|
// target's symbols with their original flags — an ambient `declare const`
|
|
356
362
|
// still reads as a value even though no value binding crosses the edge.
|
|
357
363
|
// Subtract names whose only export edges are type-only.
|
|
358
|
-
const typeOnly = new Set()
|
|
364
|
+
const typeOnly = new Set<any>()
|
|
359
365
|
for (const statement of source.statements) {
|
|
360
366
|
if (!ts.isExportDeclaration(statement)) {
|
|
361
367
|
continue
|
|
@@ -517,7 +523,7 @@ function verifyExportMap(ts, packageRoot, files, manifest, exportsMap, failures,
|
|
|
517
523
|
return
|
|
518
524
|
}
|
|
519
525
|
|
|
520
|
-
const pairs = []
|
|
526
|
+
const pairs: any[] = []
|
|
521
527
|
for (const branch of branches) {
|
|
522
528
|
if (!branch.types.length) {
|
|
523
529
|
failures.push(`${branch.subpath} runtime ${branch.runtime} has no types condition`)
|
|
@@ -598,7 +604,7 @@ function verifyExportMap(ts, packageRoot, files, manifest, exportsMap, failures,
|
|
|
598
604
|
}
|
|
599
605
|
|
|
600
606
|
export function verifyPackedPackage(ts, packageRoot) {
|
|
601
|
-
const failures = []
|
|
607
|
+
const failures: any[] = []
|
|
602
608
|
const manifestPath = join(packageRoot, 'package.json')
|
|
603
609
|
let manifest
|
|
604
610
|
try {
|