Skip to content

Commit ea68a71

Browse files
Added a BridgeJS runtime import test for TS string enums.
- Updated `Tests/BridgeJSRuntimeTests/bridge-js.d.ts` with `FeatureFlag` + `jsRoundTripFeatureFlag`. - Added JS implementation in `Tests/prelude.mjs`. - Added XCTest in `Tests/BridgeJSRuntimeTests/ImportAPITests.swift` (`testRoundTripFeatureFlag`). - Regenerated runtime fixtures under `Tests/BridgeJSRuntimeTests/Generated/` (via `BridgeJSTool generate`). - Verified runtime: `make unittest SWIFT_SDK_ID=DEVELOPMENT-SNAPSHOT+MAIN-wasm32-unknown-wasip1-threads` (passes).
1 parent 5ac25c3 commit ea68a71

File tree

7 files changed

+94
-31
lines changed

7 files changed

+94
-31
lines changed

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

Lines changed: 31 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -251,35 +251,6 @@ export class TypeProcessor {
251251
return;
252252
}
253253

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-
283254
/** @type {{ name: string, raw: string }[]} */
284255
const stringMembers = [];
285256
/** @type {{ name: string, raw: number }[]} */
@@ -291,7 +262,7 @@ export class TypeProcessor {
291262
for (const member of members) {
292263
const rawMemberName = member.name.getText();
293264
const unquotedName = rawMemberName.replace(/^["']|["']$/g, "");
294-
const swiftCaseNameBase = toSwiftCaseName(unquotedName);
265+
const swiftCaseNameBase = makeValidSwiftIdentifier(unquotedName, { emptyFallback: "_case" });
295266

296267
if (member.initializer && ts.isStringLiteral(member.initializer)) {
297268
stringMembers.push({ name: swiftCaseNameBase, raw: member.initializer.text });
@@ -811,3 +782,33 @@ export function isValidSwiftDeclName(name) {
811782
const swiftIdentifierRegex = /^[_\p{ID_Start}][\p{ID_Continue}\u{200C}\u{200D}]*$/u;
812783
return swiftIdentifierRegex.test(name);
813784
}
785+
786+
/**
787+
* Convert an arbitrary string into a valid Swift identifier.
788+
* @param {string} name
789+
* @param {{ emptyFallback?: string }} options
790+
* @returns {string}
791+
*/
792+
function makeValidSwiftIdentifier(name, options = {}) {
793+
const emptyFallback = options.emptyFallback ?? "_";
794+
let result = "";
795+
for (const ch of name) {
796+
const isIdentifierChar = /^[_\p{ID_Continue}\u{200C}\u{200D}]$/u.test(ch);
797+
result += isIdentifierChar ? ch : "_";
798+
}
799+
if (!result) result = emptyFallback;
800+
if (!/^[_\p{ID_Start}]$/u.test(result[0])) {
801+
result = "_" + result;
802+
}
803+
if (!isValidSwiftDeclName(result)) {
804+
result = result.replace(/[^_\p{ID_Continue}\u{200C}\u{200D}]/gu, "_");
805+
if (!result) result = emptyFallback;
806+
if (!/^[_\p{ID_Start}]$/u.test(result[0])) {
807+
result = "_" + result;
808+
}
809+
}
810+
if (isSwiftKeyword(result)) {
811+
result = result + "_";
812+
}
813+
return result;
814+
}

Tests/BridgeJSRuntimeTests/Generated/BridgeJS.Macros.swift

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,14 @@
2222

2323
@JSFunction func jsThrowOrString(_ shouldThrow: Bool) throws (JSException) -> String
2424

25+
enum FeatureFlag: String {
26+
case foo = "foo"
27+
case bar = "bar"
28+
}
29+
extension FeatureFlag: _BridgedSwiftEnumNoPayload {}
30+
31+
@JSFunction func jsRoundTripFeatureFlag(_ flag: FeatureFlag) throws (JSException) -> FeatureFlag
32+
2533
@JSClass struct JsGreeter {
2634
@JSGetter var name: String
2735
@JSSetter func setName(_ value: String) throws (JSException)

Tests/BridgeJSRuntimeTests/Generated/BridgeJS.swift

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6307,6 +6307,24 @@ func _$jsThrowOrString(_ shouldThrow: Bool) throws(JSException) -> String {
63076307
return String.bridgeJSLiftReturn(ret)
63086308
}
63096309

6310+
#if arch(wasm32)
6311+
@_extern(wasm, module: "BridgeJSRuntimeTests", name: "bjs_jsRoundTripFeatureFlag")
6312+
fileprivate func bjs_jsRoundTripFeatureFlag(_ flag: Int32) -> Int32
6313+
#else
6314+
fileprivate func bjs_jsRoundTripFeatureFlag(_ flag: Int32) -> Int32 {
6315+
fatalError("Only available on WebAssembly")
6316+
}
6317+
#endif
6318+
6319+
func _$jsRoundTripFeatureFlag(_ flag: FeatureFlag) throws(JSException) -> FeatureFlag {
6320+
let flagValue = flag.bridgeJSLowerParameter()
6321+
let ret = bjs_jsRoundTripFeatureFlag(flagValue)
6322+
if let error = _swift_js_take_exception() {
6323+
throw error
6324+
}
6325+
return FeatureFlag.bridgeJSLiftReturn(ret)
6326+
}
6327+
63106328
#if arch(wasm32)
63116329
@_extern(wasm, module: "BridgeJSRuntimeTests", name: "bjs_runAsyncWorks")
63126330
fileprivate func bjs_runAsyncWorks() -> Int32

Tests/BridgeJSRuntimeTests/Generated/JavaScript/BridgeJS.json

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9070,6 +9070,26 @@
90709070
}
90719071
}
90729072
},
9073+
{
9074+
"name" : "jsRoundTripFeatureFlag",
9075+
"parameters" : [
9076+
{
9077+
"name" : "flag",
9078+
"type" : {
9079+
"rawValueEnum" : {
9080+
"_0" : "FeatureFlag",
9081+
"_1" : "String"
9082+
}
9083+
}
9084+
}
9085+
],
9086+
"returnType" : {
9087+
"rawValueEnum" : {
9088+
"_0" : "FeatureFlag",
9089+
"_1" : "String"
9090+
}
9091+
}
9092+
},
90739093
{
90749094
"name" : "runAsyncWorks",
90759095
"parameters" : [

Tests/BridgeJSRuntimeTests/ImportAPITests.swift

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,12 @@ class ImportAPITests: XCTestCase {
3535
}
3636
}
3737

38+
func testRoundTripFeatureFlag() throws {
39+
for v in [FeatureFlag.foo, .bar] {
40+
try XCTAssertEqual(jsRoundTripFeatureFlag(v), v)
41+
}
42+
}
43+
3844
func ensureThrows<T>(_ f: (Bool) throws(JSException) -> T) throws {
3945
do {
4046
_ = try f(true)

Tests/BridgeJSRuntimeTests/bridge-js.d.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,13 @@ export function jsThrowOrNumber(shouldThrow: boolean): number
77
export function jsThrowOrBool(shouldThrow: boolean): boolean
88
export function jsThrowOrString(shouldThrow: boolean): string
99

10+
export enum FeatureFlag {
11+
foo = "foo",
12+
bar = "bar",
13+
}
14+
15+
export function jsRoundTripFeatureFlag(flag: FeatureFlag): FeatureFlag
16+
1017
export class JsGreeter {
1118
name: string;
1219
readonly prefix: string;
@@ -15,4 +22,4 @@ export class JsGreeter {
1522
changeName(name: string): void;
1623
}
1724

18-
export function runAsyncWorks(): Promise<void>;
25+
export function runAsyncWorks(): Promise<void>;

Tests/prelude.mjs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,9 @@ export async function setupOptions(options, context) {
4747
}
4848
return "Hello, world!";
4949
},
50+
"jsRoundTripFeatureFlag": (flag) => {
51+
return flag;
52+
},
5053
JsGreeter: class {
5154
/**
5255
* @param {string} name

0 commit comments

Comments
 (0)