11// @ts-check
22import * as fs from 'fs';
3- import { TypeProcessor } from './processor.js';
3+ import os from 'os';
4+ import path from 'path';
45import { parseArgs } from 'util';
56import ts from 'typescript';
6- import path from 'path ';
7+ import { TypeProcessor } from './processor.js ';
78
89class DiagnosticEngine {
910 /**
10- * @param {string } level
11+ * @param {keyof typeof DiagnosticEngine.LEVELS } level
1112 */
1213 constructor(level) {
1314 const levelInfo = DiagnosticEngine.LEVELS[level];
@@ -73,20 +74,35 @@ class DiagnosticEngine {
7374}
7475
7576function printUsage() {
76- console.error('Usage: ts2swift <d.ts file path> -p <tsconfig.json path> [--global <d.ts>]... [-o output.swift]');
77+ console.error(`Usage: ts2swift <input> [options]
78+
79+ <input> Path to a .d.ts file, or "-" to read from stdin
80+
81+ Options:
82+ -o, --output <path> Write Swift to <path>. Use "-" for stdout (default).
83+ -p, --project <path> Path to tsconfig.json (default: tsconfig.json).
84+ --global <path> Add a .d.ts as a global declaration file (repeatable).
85+ --log-level <level> One of: verbose, info, warning, error (default: info).
86+ -h, --help Show this help.
87+
88+ Examples:
89+ ts2swift lib.d.ts
90+ ts2swift lib.d.ts -o Generated.swift
91+ ts2swift lib.d.ts -p ./tsconfig.build.json -o Sources/Bridge/API.swift
92+ cat lib.d.ts | ts2swift - -o Generated.swift
93+ ts2swift lib.d.ts --global dom.d.ts --global lib.d.ts
94+ `);
7795}
7896
7997/**
8098 * Run ts2swift for a single input file (programmatic API, no process I/O).
81- * @param {string} filePath - Path to the .d.ts file
82- * @param {{ tsconfigPath: string, logLevel?: string , globalFiles?: string[] }} options
99+ * @param {string[]} filePaths - Paths to the .d.ts files
100+ * @param {{ tsconfigPath: string, logLevel?: keyof typeof DiagnosticEngine.LEVELS , globalFiles?: string[] }} options
83101 * @returns {string} Generated Swift source
84102 * @throws {Error} on parse/type-check errors (diagnostics are included in the message)
85103 */
86- export function run(filePath, options) {
87- const { tsconfigPath, logLevel = 'info', globalFiles: globalFilesOpt = [] } = options;
88- const globalFiles = Array.isArray(globalFilesOpt) ? globalFilesOpt : (globalFilesOpt ? [globalFilesOpt] : []);
89-
104+ export function run(filePaths, options) {
105+ const { tsconfigPath, logLevel = 'info', globalFiles = [] } = options;
90106 const diagnosticEngine = new DiagnosticEngine(logLevel);
91107
92108 const configFile = ts.readConfigFile(tsconfigPath, ts.sys.readFile);
@@ -105,7 +121,7 @@ export function run(filePath, options) {
105121 throw new Error(`TypeScript config/parse errors:\n${message}`);
106122 }
107123
108- const program = TypeProcessor.createProgram([filePath , ...globalFiles], configParseResult.options);
124+ const program = TypeProcessor.createProgram([...filePaths , ...globalFiles], configParseResult.options);
109125 const diagnostics = program.getSemanticDiagnostics();
110126 if (diagnostics.length > 0) {
111127 const message = ts.formatDiagnosticsWithColorAndContext(diagnostics, {
@@ -131,7 +147,7 @@ export function run(filePath, options) {
131147 /** @type {string[]} */
132148 const bodies = [];
133149 const globalFileSet = new Set(globalFiles);
134- for (const inputPath of [filePath , ...globalFiles]) {
150+ for (const inputPath of [...filePaths , ...globalFiles]) {
135151 const processor = new TypeProcessor(program.getTypeChecker(), diagnosticEngine, {
136152 defaultImportFromGlobal: globalFileSet.has(inputPath),
137153 });
@@ -169,39 +185,67 @@ export function main(args) {
169185 type: 'string',
170186 default: 'info',
171187 },
188+ help: {
189+ type: 'boolean',
190+ short: 'h',
191+ },
172192 },
173193 allowPositionals: true
174194 })
175195
176- if (options.positionals.length !== 1 ) {
196+ if (options.values.help ) {
177197 printUsage();
178- process.exit(1 );
198+ process.exit(0 );
179199 }
180200
181- const tsconfigPath = options.values.project;
182- if (!tsconfigPath) {
201+ if (options.positionals.length !== 1) {
183202 printUsage();
184203 process.exit(1);
185204 }
186205
187- const filePath = options.positionals[0];
188- const logLevel = options.values["log-level"] || "info";
189206 /** @type {string[]} */
190- const globalFiles = Array.isArray(options.values.global)
191- ? options.values.global
192- : (options.values.global ? [options.values.global] : []);
207+ let filePaths = options.positionals;
208+ /** @type {(() => void)[]} cleanup functions to run after completion */
209+ const cleanups = [];
210+
211+ if (filePaths[0] === '-') {
212+ const content = fs.readFileSync(0, 'utf-8');
213+ const stdinTempPath = path.join(os.tmpdir(), `ts2swift-stdin-${process.pid}-${Date.now()}.d.ts`);
214+ fs.writeFileSync(stdinTempPath, content);
215+ cleanups.push(() => fs.unlinkSync(stdinTempPath));
216+ filePaths = [stdinTempPath];
217+ }
218+ const logLevel = /** @type {keyof typeof DiagnosticEngine.LEVELS} */ ((() => {
219+ const logLevel = options.values["log-level"] || "info";
220+ if (!Object.keys(DiagnosticEngine.LEVELS).includes(logLevel)) {
221+ console.error(`Invalid log level: ${logLevel}. Valid levels are: ${Object.keys(DiagnosticEngine.LEVELS).join(", ")}`);
222+ process.exit(1);
223+ }
224+ return logLevel;
225+ })());
226+ const globalFiles = options.values.global || [];
227+ const tsconfigPath = options.values.project || "tsconfig.json";
193228
194229 const diagnosticEngine = new DiagnosticEngine(logLevel);
195- diagnosticEngine.print("verbose", `Processing ${filePath}... `);
230+ diagnosticEngine.print("verbose", `Processing ${filePaths.join(", ")} `);
196231
197232 let swiftOutput;
198233 try {
199- swiftOutput = run(filePath, { tsconfigPath, logLevel, globalFiles });
200- } catch (err) {
201- console.error(err.message);
234+ swiftOutput = run(filePaths, { tsconfigPath, logLevel, globalFiles });
235+ } catch (/** @type {unknown} */ err) {
236+ if (err instanceof Error) {
237+ diagnosticEngine.print("error", err.message);
238+ } else {
239+ diagnosticEngine.print("error", String(err));
240+ }
202241 process.exit(1);
242+ } finally {
243+ for (const cleanup of cleanups) {
244+ cleanup();
245+ }
203246 }
204- if (options.values.output) {
247+ // Write to file or stdout
248+ if (options.values.output && options.values.output !== "-") {
205249 if (swiftOutput.length > 0) {
206250 fs.mkdirSync(path.dirname(options.values.output), { recursive: true });
207251 fs.writeFileSync(options.values.output, swiftOutput);
0 commit comments