Skip to content

Commit 3fffd36

Browse files
TS2Swift: Make it easier to use ts2swift directly
1 parent 118db02 commit 3fffd36

File tree

5 files changed

+70
-44
lines changed

5 files changed

+70
-44
lines changed

Plugins/BridgeJS/Sources/TS2Swift/JavaScript/package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
"ts2swift": "./bin/ts2swift.js"
88
},
99
"scripts": {
10-
"test": "vitest run"
10+
"test": "vitest run",
11+
"tsc": "tsc --noEmit"
1112
}
1213
}

Plugins/BridgeJS/Sources/TS2Swift/JavaScript/src/cli.js

Lines changed: 36 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import path from 'path';
77

88
class DiagnosticEngine {
99
/**
10-
* @param {string} level
10+
* @param {keyof typeof DiagnosticEngine.LEVELS} level
1111
*/
1212
constructor(level) {
1313
const levelInfo = DiagnosticEngine.LEVELS[level];
@@ -73,20 +73,18 @@ class DiagnosticEngine {
7373
}
7474

7575
function printUsage() {
76-
console.error('Usage: ts2swift <d.ts file path> -p <tsconfig.json path> [--global <d.ts>]... [-o output.swift]');
76+
console.error('Usage: ts2swift <d.ts file path>... [-p <tsconfig.json path>] [--global <d.ts>]... [-o output.swift]');
7777
}
7878

7979
/**
8080
* 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
81+
* @param {string[]} filePaths - Paths to the .d.ts files
82+
* @param {{ tsconfigPath: string, logLevel?: keyof typeof DiagnosticEngine.LEVELS, globalFiles?: string[] }} options
8383
* @returns {string} Generated Swift source
8484
* @throws {Error} on parse/type-check errors (diagnostics are included in the message)
8585
*/
86-
export function run(filePath, options) {
87-
const { tsconfigPath, logLevel = 'info', globalFiles: globalFilesOpt = [] } = options;
88-
const globalFiles = Array.isArray(globalFilesOpt) ? globalFilesOpt : (globalFilesOpt ? [globalFilesOpt] : []);
89-
86+
export function run(filePaths, options) {
87+
const { tsconfigPath, logLevel = 'info', globalFiles = [] } = options;
9088
const diagnosticEngine = new DiagnosticEngine(logLevel);
9189

9290
const configFile = ts.readConfigFile(tsconfigPath, ts.sys.readFile);
@@ -105,7 +103,7 @@ export function run(filePath, options) {
105103
throw new Error(`TypeScript config/parse errors:\n${message}`);
106104
}
107105

108-
const program = TypeProcessor.createProgram([filePath, ...globalFiles], configParseResult.options);
106+
const program = TypeProcessor.createProgram([...filePaths, ...globalFiles], configParseResult.options);
109107
const diagnostics = program.getSemanticDiagnostics();
110108
if (diagnostics.length > 0) {
111109
const message = ts.formatDiagnosticsWithColorAndContext(diagnostics, {
@@ -131,7 +129,7 @@ export function run(filePath, options) {
131129
/** @type {string[]} */
132130
const bodies = [];
133131
const globalFileSet = new Set(globalFiles);
134-
for (const inputPath of [filePath, ...globalFiles]) {
132+
for (const inputPath of [...filePaths, ...globalFiles]) {
135133
const processor = new TypeProcessor(program.getTypeChecker(), diagnosticEngine, {
136134
defaultImportFromGlobal: globalFileSet.has(inputPath),
137135
});
@@ -169,39 +167,52 @@ export function main(args) {
169167
type: 'string',
170168
default: 'info',
171169
},
170+
help: {
171+
type: 'boolean',
172+
short: 'h',
173+
},
172174
},
173175
allowPositionals: true
174176
})
175177

176-
if (options.positionals.length !== 1) {
178+
if (options.values.help) {
177179
printUsage();
178-
process.exit(1);
180+
process.exit(0);
179181
}
180182

181-
const tsconfigPath = options.values.project;
182-
if (!tsconfigPath) {
183+
if (options.positionals.length !== 1) {
183184
printUsage();
184185
process.exit(1);
185186
}
186187

187-
const filePath = options.positionals[0];
188-
const logLevel = options.values["log-level"] || "info";
189-
/** @type {string[]} */
190-
const globalFiles = Array.isArray(options.values.global)
191-
? options.values.global
192-
: (options.values.global ? [options.values.global] : []);
188+
const filePaths = options.positionals;
189+
const logLevel = /** @type {keyof typeof DiagnosticEngine.LEVELS} */ ((() => {
190+
const logLevel = options.values["log-level"] || "info";
191+
if (!Object.keys(DiagnosticEngine.LEVELS).includes(logLevel)) {
192+
console.error(`Invalid log level: ${logLevel}. Valid levels are: ${Object.keys(DiagnosticEngine.LEVELS).join(", ")}`);
193+
process.exit(1);
194+
}
195+
return logLevel;
196+
})());
197+
const globalFiles = options.values.global || [];
198+
const tsconfigPath = options.values.project || "tsconfig.json";
193199

194200
const diagnosticEngine = new DiagnosticEngine(logLevel);
195-
diagnosticEngine.print("verbose", `Processing ${filePath}...`);
201+
diagnosticEngine.print("verbose", `Processing ${filePaths.join(", ")}`);
196202

197203
let swiftOutput;
198204
try {
199-
swiftOutput = run(filePath, { tsconfigPath, logLevel, globalFiles });
200-
} catch (err) {
201-
console.error(err.message);
205+
swiftOutput = run(filePaths, { tsconfigPath, logLevel, globalFiles });
206+
} catch (/** @type {unknown} */ err) {
207+
if (err instanceof Error) {
208+
diagnosticEngine.print("error", err.message);
209+
} else {
210+
diagnosticEngine.print("error", String(err));
211+
}
202212
process.exit(1);
203213
}
204-
if (options.values.output) {
214+
// Write to file or stdout
215+
if (options.values.output && options.values.output !== "-") {
205216
if (swiftOutput.length > 0) {
206217
fs.mkdirSync(path.dirname(options.values.output), { recursive: true });
207218
fs.writeFileSync(options.values.output, swiftOutput);

Plugins/BridgeJS/Sources/TS2Swift/JavaScript/src/processor.js

Lines changed: 17 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -20,14 +20,13 @@ import ts from 'typescript';
2020
export class TypeProcessor {
2121
/**
2222
* Create a TypeScript program from a d.ts file
23-
* @param {string} filePath - Path to the d.ts file
23+
* @param {string[]} filePaths - Paths to the d.ts file
2424
* @param {ts.CompilerOptions} options - Compiler options
2525
* @returns {ts.Program} TypeScript program object
2626
*/
2727
static createProgram(filePaths, options) {
2828
const host = ts.createCompilerHost(options);
29-
const roots = Array.isArray(filePaths) ? filePaths : [filePaths];
30-
return ts.createProgram(roots, {
29+
return ts.createProgram(filePaths, {
3130
...options,
3231
noCheck: true,
3332
skipLibCheck: true,
@@ -39,14 +38,7 @@ export class TypeProcessor {
3938
* @param {DiagnosticEngine} diagnosticEngine - Diagnostic engine
4039
*/
4140
constructor(checker, diagnosticEngine, options = {
42-
inheritIterable: true,
43-
inheritArraylike: true,
44-
inheritPromiselike: true,
45-
addAllParentMembersToClass: true,
46-
replaceAliasToFunction: true,
47-
replaceRankNFunction: true,
48-
replaceNewableFunction: true,
49-
noExtendsInTyprm: false,
41+
defaultImportFromGlobal: false,
5042
}) {
5143
this.checker = checker;
5244
this.diagnosticEngine = diagnosticEngine;
@@ -163,8 +155,12 @@ export class TypeProcessor {
163155
}
164156
}
165157
});
166-
} catch (error) {
167-
this.diagnosticEngine.print("error", `Error processing ${sourceFile.fileName}: ${error.message}`);
158+
} catch (/** @type {unknown} */ error) {
159+
if (error instanceof Error) {
160+
this.diagnosticEngine.print("error", `Error processing ${sourceFile.fileName}: ${error.message}`);
161+
} else {
162+
this.diagnosticEngine.print("error", `Error processing ${sourceFile.fileName}: ${String(error)}`);
163+
}
168164
}
169165
}
170166

@@ -388,7 +384,7 @@ export class TypeProcessor {
388384
canBeIntEnum = false;
389385
}
390386
const swiftEnumName = this.renderTypeIdentifier(enumName);
391-
const dedupeNames = (items) => {
387+
const dedupeNames = (/** @type {{ name: string, raw: string | number }[]} */ items) => {
392388
const seen = new Map();
393389
return items.map(item => {
394390
const count = seen.get(item.name) ?? 0;
@@ -401,6 +397,10 @@ export class TypeProcessor {
401397
if (canBeStringEnum && stringMembers.length > 0) {
402398
this.swiftLines.push(`enum ${swiftEnumName}: String {`);
403399
for (const { name, raw } of dedupeNames(stringMembers)) {
400+
if (typeof raw !== "string") {
401+
this.diagnosticEngine.print("warning", `Invalid string literal: ${raw}`, diagnosticNode);
402+
continue;
403+
}
404404
this.swiftLines.push(` case ${this.renderIdentifier(name)} = "${raw.replaceAll("\"", "\\\\\"")}"`);
405405
}
406406
this.swiftLines.push("}");
@@ -815,7 +815,7 @@ export class TypeProcessor {
815815
visitType(type, node) {
816816
const typeArguments = this.getTypeArguments(type);
817817
if (this.checker.isArrayType(type)) {
818-
const typeArgs = this.checker.getTypeArguments(type);
818+
const typeArgs = this.checker.getTypeArguments(/** @type {ts.TypeReference} */ (type));
819819
if (typeArgs && typeArgs.length > 0) {
820820
const elementType = this.visitType(typeArgs[0], node);
821821
return `[${elementType}]`;
@@ -920,7 +920,7 @@ export class TypeProcessor {
920920
* Convert a `Record<string, T>` TypeScript type into a Swift dictionary type.
921921
* Falls back to `JSObject` when keys are not string-compatible or type arguments are missing.
922922
* @param {ts.Type} type
923-
* @param {ts.Type[]} typeArguments
923+
* @param {readonly ts.Type[]} typeArguments
924924
* @param {ts.Node} node
925925
* @returns {string | null}
926926
* @private
@@ -952,7 +952,7 @@ export class TypeProcessor {
952952
/**
953953
* Retrieve type arguments for a given type, including type alias instantiations.
954954
* @param {ts.Type} type
955-
* @returns {ts.Type[]}
955+
* @returns {readonly ts.Type[]}
956956
* @private
957957
*/
958958
getTypeArguments(type) {

Plugins/BridgeJS/Sources/TS2Swift/JavaScript/test/ts2swift.test.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ const inputsDir = path.resolve(__dirname, 'fixtures');
1212
const tsconfigPath = path.join(inputsDir, 'tsconfig.json');
1313

1414
function runTs2Swift(dtsPath) {
15-
return run(dtsPath, { tsconfigPath, logLevel: 'error' });
15+
return run([dtsPath], { tsconfigPath, logLevel: 'error' });
1616
}
1717

1818
function collectDtsInputs() {
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
{
2+
"compilerOptions": {
3+
"strict": true,
4+
"allowJs": true,
5+
"skipLibCheck": true,
6+
"noEmit": true,
7+
"target": "ES2022",
8+
"module": "NodeNext",
9+
"moduleResolution": "nodenext"
10+
},
11+
"include": [
12+
"src/*.js"
13+
]
14+
}

0 commit comments

Comments
 (0)