Skip to content

Commit 5ac25c3

Browse files
Implemented TypeScript enum → Swift enum import for BridgeJS (string-valued enums, plus int-valued as a bonus).
- `Plugins/BridgeJS/Sources/TS2Swift/JavaScript/src/processor.js`: emits Swift `enum Name: String { ... }` (or `: Int`) for TS enums, adds `extension Name: _BridgedSwiftEnumNoPayload {}`, and ensures enum-typed parameters/returns stay typed as the enum (not downgraded to `String`). - `Plugins/BridgeJS/Sources/BridgeJSCore/ImportSwiftMacros.swift`: resolves referenced Swift enums/typealiases via `TypeDeclResolver` so `FeatureFlag` becomes `.rawValueEnum("FeatureFlag", .string)` in the imported skeleton. - `Plugins/BridgeJS/Sources/BridgeJSCore/ImportTS.swift`: enables lowering/lifting for `.rawValueEnum` in the `.importTS` context (parameters + returns). - Added coverage: `Plugins/BridgeJS/Tests/BridgeJSToolTests/Inputs/StringEnum.d.ts` with new snapshots `Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/ImportTSTests/StringEnum.Macros.swift` and `Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/ImportTSTests/StringEnum.swift`. - Verified with `swift test --package-path ./Plugins/BridgeJS --filter ImportTSTests`.
1 parent cd3edbe commit 5ac25c3

File tree

7 files changed

+498
-2
lines changed

7 files changed

+498
-2
lines changed

Plugins/BridgeJS/Sources/BridgeJSCore/ImportTS.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -869,7 +869,7 @@ extension BridgeType {
869869
case .rawValueEnum(_, let rawType):
870870
switch context {
871871
case .importTS:
872-
throw BridgeJSCoreError("Enum types are not yet supported in TypeScript imports")
872+
return LoweringParameterInfo(loweredParameters: [("value", rawType.wasmCoreType ?? .i32)])
873873
case .exportSwift:
874874
// For protocol export we return .i32 for String raw value type instead of nil
875875
return LoweringParameterInfo(loweredParameters: [("value", rawType.wasmCoreType ?? .i32)])
@@ -952,7 +952,7 @@ extension BridgeType {
952952
case .rawValueEnum(_, let rawType):
953953
switch context {
954954
case .importTS:
955-
throw BridgeJSCoreError("Enum types are not yet supported in TypeScript imports")
955+
return LiftingReturnInfo(valueToLift: rawType.wasmCoreType ?? .i32)
956956
case .exportSwift:
957957
// For protocol export we return .i32 for String raw value type instead of nil
958958
return LiftingReturnInfo(valueToLift: rawType.wasmCoreType ?? .i32)

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

Lines changed: 188 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,10 @@ export class TypeProcessor {
5353
this.seenTypes = new Map();
5454
/** @type {string[]} Collected Swift code lines */
5555
this.swiftLines = [];
56+
/** @type {Set<string>} */
57+
this.emittedEnumNames = new Set();
58+
/** @type {Set<string>} */
59+
this.emittedStructuredTypeNames = new Set();
5660

5761
/** @type {Set<string>} */
5862
this.visitedDeclarationKeys = new Set();
@@ -92,6 +96,10 @@ export class TypeProcessor {
9296

9397
for (const [type, node] of this.seenTypes) {
9498
this.seenTypes.delete(type);
99+
if (this.isEnumType(type)) {
100+
this.visitEnumType(type, node);
101+
continue;
102+
}
95103
const typeString = this.checker.typeToString(type);
96104
const members = type.getProperties();
97105
if (members) {
@@ -119,6 +127,8 @@ export class TypeProcessor {
119127
this.visitFunctionDeclaration(node);
120128
} else if (ts.isClassDeclaration(node)) {
121129
this.visitClassDecl(node);
130+
} else if (ts.isEnumDeclaration(node)) {
131+
this.visitEnumDeclaration(node);
122132
} else if (ts.isExportDeclaration(node)) {
123133
this.visitExportDeclaration(node);
124134
}
@@ -185,6 +195,174 @@ export class TypeProcessor {
185195
}
186196
}
187197

198+
/**
199+
* @param {ts.Type} type
200+
* @returns {boolean}
201+
* @private
202+
*/
203+
isEnumType(type) {
204+
const symbol = type.getSymbol() ?? type.aliasSymbol;
205+
if (!symbol) return false;
206+
return (symbol.flags & ts.SymbolFlags.Enum) !== 0;
207+
}
208+
209+
/**
210+
* @param {ts.EnumDeclaration} node
211+
* @private
212+
*/
213+
visitEnumDeclaration(node) {
214+
const name = node.name?.text;
215+
if (!name) return;
216+
this.emitEnumFromDeclaration(name, node, node);
217+
}
218+
219+
/**
220+
* @param {ts.Type} type
221+
* @param {ts.Node} node
222+
* @private
223+
*/
224+
visitEnumType(type, node) {
225+
const symbol = type.getSymbol() ?? type.aliasSymbol;
226+
const name = symbol?.name;
227+
if (!name) return;
228+
const decl = symbol?.getDeclarations()?.find(d => ts.isEnumDeclaration(d));
229+
if (!decl || !ts.isEnumDeclaration(decl)) {
230+
this.diagnosticEngine.print("warning", `Enum declaration not found for type: ${name}`, node);
231+
return;
232+
}
233+
this.emitEnumFromDeclaration(name, decl, node);
234+
}
235+
236+
/**
237+
* @param {string} enumName
238+
* @param {ts.EnumDeclaration} decl
239+
* @param {ts.Node} diagnosticNode
240+
* @private
241+
*/
242+
emitEnumFromDeclaration(enumName, decl, diagnosticNode) {
243+
if (this.emittedEnumNames.has(enumName)) return;
244+
this.emittedEnumNames.add(enumName);
245+
246+
const members = decl.members ?? [];
247+
if (members.length === 0) {
248+
this.diagnosticEngine.print("warning", `Empty enum is not supported: ${enumName}`, diagnosticNode);
249+
this.swiftLines.push(`typealias ${this.renderIdentifier(enumName)} = String`);
250+
this.swiftLines.push("");
251+
return;
252+
}
253+
254+
/**
255+
* Convert a TypeScript enum member name into a valid Swift identifier.
256+
* @param {string} name
257+
* @returns {string}
258+
*/
259+
const toSwiftCaseName = (name) => {
260+
const swiftIdentifierRegex = /^[_\p{ID_Start}][\p{ID_Continue}\u{200C}\u{200D}]*$/u;
261+
let result = "";
262+
for (const ch of name) {
263+
const isIdentifierChar = /^[_\p{ID_Continue}\u{200C}\u{200D}]$/u.test(ch);
264+
result += isIdentifierChar ? ch : "_";
265+
}
266+
if (!result) result = "_case";
267+
if (!/^[_\p{ID_Start}]$/u.test(result[0])) {
268+
result = "_" + result;
269+
}
270+
if (!swiftIdentifierRegex.test(result)) {
271+
result = result.replace(/[^_\p{ID_Continue}\u{200C}\u{200D}]/gu, "_");
272+
if (!result) result = "_case";
273+
if (!/^[_\p{ID_Start}]$/u.test(result[0])) {
274+
result = "_" + result;
275+
}
276+
}
277+
if (isSwiftKeyword(result)) {
278+
result = result + "_";
279+
}
280+
return result;
281+
};
282+
283+
/** @type {{ name: string, raw: string }[]} */
284+
const stringMembers = [];
285+
/** @type {{ name: string, raw: number }[]} */
286+
const intMembers = [];
287+
let canBeStringEnum = true;
288+
let canBeIntEnum = true;
289+
let nextAutoValue = 0;
290+
291+
for (const member of members) {
292+
const rawMemberName = member.name.getText();
293+
const unquotedName = rawMemberName.replace(/^["']|["']$/g, "");
294+
const swiftCaseNameBase = toSwiftCaseName(unquotedName);
295+
296+
if (member.initializer && ts.isStringLiteral(member.initializer)) {
297+
stringMembers.push({ name: swiftCaseNameBase, raw: member.initializer.text });
298+
canBeIntEnum = false;
299+
continue;
300+
}
301+
302+
if (member.initializer && ts.isNumericLiteral(member.initializer)) {
303+
const rawValue = Number(member.initializer.text);
304+
if (!Number.isInteger(rawValue)) {
305+
canBeIntEnum = false;
306+
} else {
307+
intMembers.push({ name: swiftCaseNameBase, raw: rawValue });
308+
nextAutoValue = rawValue + 1;
309+
canBeStringEnum = false;
310+
continue;
311+
}
312+
}
313+
314+
if (!member.initializer) {
315+
intMembers.push({ name: swiftCaseNameBase, raw: nextAutoValue });
316+
nextAutoValue += 1;
317+
canBeStringEnum = false;
318+
continue;
319+
}
320+
321+
canBeStringEnum = false;
322+
canBeIntEnum = false;
323+
}
324+
const swiftEnumName = this.renderIdentifier(enumName);
325+
const dedupeNames = (items) => {
326+
const seen = new Map();
327+
return items.map(item => {
328+
const count = seen.get(item.name) ?? 0;
329+
seen.set(item.name, count + 1);
330+
if (count === 0) return item;
331+
return { ...item, name: `${item.name}_${count + 1}` };
332+
});
333+
};
334+
335+
if (canBeStringEnum && stringMembers.length > 0) {
336+
this.swiftLines.push(`enum ${swiftEnumName}: String {`);
337+
for (const { name, raw } of dedupeNames(stringMembers)) {
338+
this.swiftLines.push(` case ${this.renderIdentifier(name)} = "${raw.replaceAll("\"", "\\\\\"")}"`);
339+
}
340+
this.swiftLines.push("}");
341+
this.swiftLines.push(`extension ${swiftEnumName}: _BridgedSwiftEnumNoPayload {}`);
342+
this.swiftLines.push("");
343+
return;
344+
}
345+
346+
if (canBeIntEnum && intMembers.length > 0) {
347+
this.swiftLines.push(`enum ${swiftEnumName}: Int {`);
348+
for (const { name, raw } of dedupeNames(intMembers)) {
349+
this.swiftLines.push(` case ${this.renderIdentifier(name)} = ${raw}`);
350+
}
351+
this.swiftLines.push("}");
352+
this.swiftLines.push(`extension ${swiftEnumName}: _BridgedSwiftEnumNoPayload {}`);
353+
this.swiftLines.push("");
354+
return;
355+
}
356+
357+
this.diagnosticEngine.print(
358+
"warning",
359+
`Unsupported enum (only string or int enums are supported): ${enumName}`,
360+
diagnosticNode
361+
);
362+
this.swiftLines.push(`typealias ${swiftEnumName} = String`);
363+
this.swiftLines.push("");
364+
}
365+
188366
/**
189367
* Visit a function declaration and render Swift code
190368
* @param {ts.FunctionDeclaration} node - The function node
@@ -332,6 +510,9 @@ export class TypeProcessor {
332510
* @private
333511
*/
334512
visitStructuredType(name, members) {
513+
if (this.emittedStructuredTypeNames.has(name)) return;
514+
this.emittedStructuredTypeNames.add(name);
515+
335516
const typeName = this.renderIdentifier(name);
336517
this.swiftLines.push(`@JSClass struct ${typeName} {`);
337518

@@ -415,6 +596,13 @@ export class TypeProcessor {
415596
return typeMap[typeString];
416597
}
417598

599+
const symbol = type.getSymbol() ?? type.aliasSymbol;
600+
if (symbol && (symbol.flags & ts.SymbolFlags.Enum) !== 0) {
601+
const typeName = symbol.name;
602+
this.seenTypes.set(type, node);
603+
return this.renderIdentifier(typeName);
604+
}
605+
418606
if (this.checker.isArrayType(type) || this.checker.isTupleType(type) || type.getCallSignatures().length > 0) {
419607
return "JSObject";
420608
}
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
export enum FeatureFlag {
2+
foo = "foo",
3+
bar = "bar",
4+
}
5+
6+
export function takesFeatureFlag(flag: FeatureFlag): void
7+
8+
export function returnsFeatureFlag(): FeatureFlag
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
// NOTICE: This is auto-generated code by BridgeJS from JavaScriptKit,
2+
// DO NOT EDIT.
3+
//
4+
// To update this file, just rebuild your project or run
5+
// `swift package bridge-js`.
6+
7+
export type Exports = {
8+
}
9+
export type Imports = {
10+
takesFeatureFlag(flag: FeatureFlagTag): void;
11+
returnsFeatureFlag(): FeatureFlagTag;
12+
}
13+
export function createInstantiator(options: {
14+
imports: Imports;
15+
}, swift: any): Promise<{
16+
addImports: (importObject: WebAssembly.Imports) => void;
17+
setInstance: (instance: WebAssembly.Instance) => void;
18+
createExports: (instance: WebAssembly.Instance) => Exports;
19+
}>;

0 commit comments

Comments
 (0)