From 2c0915fc7f9e0fe150447449ee7efca2d31363e2 Mon Sep 17 00:00:00 2001 From: Yuta Saito Date: Thu, 22 Jan 2026 09:40:53 +0900 Subject: [PATCH] Using `bridgejs-development` (this is in the BridgeJS/ImportTS + TS2Swift pipeline). - Added TS2Swift handling for TypeScript re-export declarations so `export { Thing } from "./pkg"` in `bridge-js.d.ts` is followed and the referenced declarations are emitted (`Plugins/BridgeJS/Sources/TS2Swift/JavaScript/src/processor.js:118`). - Added a regression fixture and snapshots covering `export { jsRoundTripNumber } from "./Support/ReExportTarget"` and `export { JsGreeter } from "./Support/ReExportTarget"` (`Plugins/BridgeJS/Tests/BridgeJSToolTests/Inputs/ReExportFrom.d.ts:1`). - Verified with `swift test --package-path ./Plugins/BridgeJS` (note: TS2Swift needs `typescript` available via `npm ci` at repo root). --- .../TS2Swift/JavaScript/src/processor.js | 66 +++++ .../Inputs/ReExportFrom.d.ts | 2 + .../Inputs/Support/ReExportTarget.d.ts | 7 + .../ReExportFrom.Import.d.ts | 24 ++ .../BridgeJSLinkTests/ReExportFrom.Import.js | 241 ++++++++++++++++++ .../ImportTSTests/ReExportFrom.Macros.swift | 14 + .../ImportTSTests/ReExportFrom.swift | 53 ++++ 7 files changed, 407 insertions(+) create mode 100644 Plugins/BridgeJS/Tests/BridgeJSToolTests/Inputs/ReExportFrom.d.ts create mode 100644 Plugins/BridgeJS/Tests/BridgeJSToolTests/Inputs/Support/ReExportTarget.d.ts create mode 100644 Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/ReExportFrom.Import.d.ts create mode 100644 Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/ReExportFrom.Import.js create mode 100644 Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/ImportTSTests/ReExportFrom.Macros.swift create mode 100644 Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/ImportTSTests/ReExportFrom.swift diff --git a/Plugins/BridgeJS/Sources/TS2Swift/JavaScript/src/processor.js b/Plugins/BridgeJS/Sources/TS2Swift/JavaScript/src/processor.js index 47a4a1ac..47b92176 100644 --- a/Plugins/BridgeJS/Sources/TS2Swift/JavaScript/src/processor.js +++ b/Plugins/BridgeJS/Sources/TS2Swift/JavaScript/src/processor.js @@ -54,6 +54,9 @@ export class TypeProcessor { this.seenTypes = new Map(); /** @type {string[]} Collected Swift code lines */ this.swiftLines = []; + + /** @type {Set} */ + this.visitedDeclarationKeys = new Set(); } /** @@ -117,6 +120,69 @@ export class TypeProcessor { this.visitFunctionDeclaration(node); } else if (ts.isClassDeclaration(node)) { this.visitClassDecl(node); + } else if (ts.isExportDeclaration(node)) { + this.visitExportDeclaration(node); + } + } + + /** + * Visit an export declaration and process re-exports like: + * - export { Thing } from "./module"; + * - export { Thing as Alias } from "./module"; + * - export * from "./module"; + * @param {ts.ExportDeclaration} node + */ + visitExportDeclaration(node) { + if (!node.moduleSpecifier) return; + + const moduleSymbol = this.checker.getSymbolAtLocation(node.moduleSpecifier); + if (!moduleSymbol) { + this.diagnosticEngine.print("warning", "Failed to resolve module for export declaration", node); + return; + } + + /** @type {ts.Symbol[]} */ + let targetSymbols = []; + + if (!node.exportClause) { + // export * from "..." + targetSymbols = this.checker.getExportsOfModule(moduleSymbol); + } else if (ts.isNamedExports(node.exportClause)) { + const moduleExports = this.checker.getExportsOfModule(moduleSymbol); + for (const element of node.exportClause.elements) { + const originalName = element.propertyName?.text ?? element.name.text; + + const match = moduleExports.find(s => s.name === originalName); + if (match) { + targetSymbols.push(match); + continue; + } + + // Fallback for unusual bindings/resolution failures. + const fallback = this.checker.getSymbolAtLocation(element.propertyName ?? element.name); + if (fallback) { + targetSymbols.push(fallback); + continue; + } + + this.diagnosticEngine.print("warning", `Failed to resolve re-exported symbol '${originalName}'`, node); + } + } else { + // export * as ns from "..." is not currently supported by BridgeJS imports. + return; + } + + for (const symbol of targetSymbols) { + const declarations = symbol.getDeclarations() ?? []; + for (const declaration of declarations) { + // Avoid duplicate emission when the same declaration is reached via multiple re-exports. + const sourceFile = declaration.getSourceFile(); + const key = `${sourceFile.fileName}:${declaration.pos}:${declaration.end}`; + if (this.visitedDeclarationKeys.has(key)) continue; + this.visitedDeclarationKeys.add(key); + + this.visitNode(declaration); + } } } diff --git a/Plugins/BridgeJS/Tests/BridgeJSToolTests/Inputs/ReExportFrom.d.ts b/Plugins/BridgeJS/Tests/BridgeJSToolTests/Inputs/ReExportFrom.d.ts new file mode 100644 index 00000000..0614f14c --- /dev/null +++ b/Plugins/BridgeJS/Tests/BridgeJSToolTests/Inputs/ReExportFrom.d.ts @@ -0,0 +1,2 @@ +export { jsRoundTripNumber } from "./Support/ReExportTarget" +export { JsGreeter } from "./Support/ReExportTarget" diff --git a/Plugins/BridgeJS/Tests/BridgeJSToolTests/Inputs/Support/ReExportTarget.d.ts b/Plugins/BridgeJS/Tests/BridgeJSToolTests/Inputs/Support/ReExportTarget.d.ts new file mode 100644 index 00000000..1867b197 --- /dev/null +++ b/Plugins/BridgeJS/Tests/BridgeJSToolTests/Inputs/Support/ReExportTarget.d.ts @@ -0,0 +1,7 @@ +export function jsRoundTripNumber(v: number): number + +export class JsGreeter { + constructor(name: string); + greet(): string; +} + diff --git a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/ReExportFrom.Import.d.ts b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/ReExportFrom.Import.d.ts new file mode 100644 index 00000000..db6d2be0 --- /dev/null +++ b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/ReExportFrom.Import.d.ts @@ -0,0 +1,24 @@ +// NOTICE: This is auto-generated code by BridgeJS from JavaScriptKit, +// DO NOT EDIT. +// +// To update this file, just rebuild your project or run +// `swift package bridge-js`. + +export interface JsGreeter { + greet(): string; +} +export type Exports = { +} +export type Imports = { + jsRoundTripNumber(v: number): number; + JsGreeter: { + new(name: string): JsGreeter; + } +} +export function createInstantiator(options: { + imports: Imports; +}, swift: any): Promise<{ + addImports: (importObject: WebAssembly.Imports) => void; + setInstance: (instance: WebAssembly.Instance) => void; + createExports: (instance: WebAssembly.Instance) => Exports; +}>; \ No newline at end of file diff --git a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/ReExportFrom.Import.js b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/ReExportFrom.Import.js new file mode 100644 index 00000000..4a13f2d7 --- /dev/null +++ b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/ReExportFrom.Import.js @@ -0,0 +1,241 @@ +// NOTICE: This is auto-generated code by BridgeJS from JavaScriptKit, +// DO NOT EDIT. +// +// To update this file, just rebuild your project or run +// `swift package bridge-js`. + +export async function createInstantiator(options, swift) { + let instance; + let memory; + let setException; + const textDecoder = new TextDecoder("utf-8"); + const textEncoder = new TextEncoder("utf-8"); + let tmpRetString; + let tmpRetBytes; + let tmpRetException; + let tmpRetOptionalBool; + let tmpRetOptionalInt; + let tmpRetOptionalFloat; + let tmpRetOptionalDouble; + let tmpRetOptionalHeapObject; + let tmpRetTag; + let tmpRetStrings = []; + let tmpRetInts = []; + let tmpRetF32s = []; + let tmpRetF64s = []; + let tmpParamInts = []; + let tmpParamF32s = []; + let tmpParamF64s = []; + let tmpRetPointers = []; + let tmpParamPointers = []; + const enumHelpers = {}; + const structHelpers = {}; + + let _exports = null; + let bjs = null; + + return { + /** + * @param {WebAssembly.Imports} importObject + */ + addImports: (importObject, importsContext) => { + bjs = {}; + importObject["bjs"] = bjs; + const imports = options.getImports(importsContext); + bjs["swift_js_return_string"] = function(ptr, len) { + const bytes = new Uint8Array(memory.buffer, ptr, len); + tmpRetString = textDecoder.decode(bytes); + } + bjs["swift_js_init_memory"] = function(sourceId, bytesPtr) { + const source = swift.memory.getObject(sourceId); + const bytes = new Uint8Array(memory.buffer, bytesPtr); + bytes.set(source); + } + bjs["swift_js_make_js_string"] = function(ptr, len) { + const bytes = new Uint8Array(memory.buffer, ptr, len); + return swift.memory.retain(textDecoder.decode(bytes)); + } + bjs["swift_js_init_memory_with_result"] = function(ptr, len) { + const target = new Uint8Array(memory.buffer, ptr, len); + target.set(tmpRetBytes); + tmpRetBytes = undefined; + } + bjs["swift_js_throw"] = function(id) { + tmpRetException = swift.memory.retainByRef(id); + } + bjs["swift_js_retain"] = function(id) { + return swift.memory.retainByRef(id); + } + bjs["swift_js_release"] = function(id) { + swift.memory.release(id); + } + bjs["swift_js_push_tag"] = function(tag) { + tmpRetTag = tag; + } + bjs["swift_js_push_int"] = function(v) { + tmpRetInts.push(v | 0); + } + bjs["swift_js_push_f32"] = function(v) { + tmpRetF32s.push(Math.fround(v)); + } + bjs["swift_js_push_f64"] = function(v) { + tmpRetF64s.push(v); + } + bjs["swift_js_push_string"] = function(ptr, len) { + const bytes = new Uint8Array(memory.buffer, ptr, len); + const value = textDecoder.decode(bytes); + tmpRetStrings.push(value); + } + bjs["swift_js_pop_param_int32"] = function() { + return tmpParamInts.pop(); + } + bjs["swift_js_pop_param_f32"] = function() { + return tmpParamF32s.pop(); + } + bjs["swift_js_pop_param_f64"] = function() { + return tmpParamF64s.pop(); + } + bjs["swift_js_push_pointer"] = function(pointer) { + tmpRetPointers.push(pointer); + } + bjs["swift_js_pop_param_pointer"] = function() { + return tmpParamPointers.pop(); + } + bjs["swift_js_return_optional_bool"] = function(isSome, value) { + if (isSome === 0) { + tmpRetOptionalBool = null; + } else { + tmpRetOptionalBool = value !== 0; + } + } + bjs["swift_js_return_optional_int"] = function(isSome, value) { + if (isSome === 0) { + tmpRetOptionalInt = null; + } else { + tmpRetOptionalInt = value | 0; + } + } + bjs["swift_js_return_optional_float"] = function(isSome, value) { + if (isSome === 0) { + tmpRetOptionalFloat = null; + } else { + tmpRetOptionalFloat = Math.fround(value); + } + } + bjs["swift_js_return_optional_double"] = function(isSome, value) { + if (isSome === 0) { + tmpRetOptionalDouble = null; + } else { + tmpRetOptionalDouble = value; + } + } + bjs["swift_js_return_optional_string"] = function(isSome, ptr, len) { + if (isSome === 0) { + tmpRetString = null; + } else { + const bytes = new Uint8Array(memory.buffer, ptr, len); + tmpRetString = textDecoder.decode(bytes); + } + } + bjs["swift_js_return_optional_object"] = function(isSome, objectId) { + if (isSome === 0) { + tmpRetString = null; + } else { + tmpRetString = swift.memory.getObject(objectId); + } + } + bjs["swift_js_return_optional_heap_object"] = function(isSome, pointer) { + if (isSome === 0) { + tmpRetOptionalHeapObject = null; + } else { + tmpRetOptionalHeapObject = pointer; + } + } + bjs["swift_js_get_optional_int_presence"] = function() { + return tmpRetOptionalInt != null ? 1 : 0; + } + bjs["swift_js_get_optional_int_value"] = function() { + const value = tmpRetOptionalInt; + tmpRetOptionalInt = undefined; + return value; + } + bjs["swift_js_get_optional_string"] = function() { + const str = tmpRetString; + tmpRetString = undefined; + if (str == null) { + return -1; + } else { + const bytes = textEncoder.encode(str); + tmpRetBytes = bytes; + return bytes.length; + } + } + bjs["swift_js_get_optional_float_presence"] = function() { + return tmpRetOptionalFloat != null ? 1 : 0; + } + bjs["swift_js_get_optional_float_value"] = function() { + const value = tmpRetOptionalFloat; + tmpRetOptionalFloat = undefined; + return value; + } + bjs["swift_js_get_optional_double_presence"] = function() { + return tmpRetOptionalDouble != null ? 1 : 0; + } + bjs["swift_js_get_optional_double_value"] = function() { + const value = tmpRetOptionalDouble; + tmpRetOptionalDouble = undefined; + return value; + } + bjs["swift_js_get_optional_heap_object_pointer"] = function() { + const pointer = tmpRetOptionalHeapObject; + tmpRetOptionalHeapObject = undefined; + return pointer || 0; + } + const TestModule = importObject["TestModule"] = importObject["TestModule"] || {}; + TestModule["bjs_jsRoundTripNumber"] = function bjs_jsRoundTripNumber(v) { + try { + let ret = imports.jsRoundTripNumber(v); + return ret; + } catch (error) { + setException(error); + return 0 + } + } + TestModule["bjs_JsGreeter_init"] = function bjs_JsGreeter_init(name) { + try { + const nameObject = swift.memory.getObject(name); + swift.memory.release(name); + return swift.memory.retain(new imports.JsGreeter(nameObject)); + } catch (error) { + setException(error); + return 0 + } + } + TestModule["bjs_JsGreeter_greet"] = function bjs_JsGreeter_greet(self) { + try { + let ret = swift.memory.getObject(self).greet(); + tmpRetBytes = textEncoder.encode(ret); + return tmpRetBytes.length; + } catch (error) { + setException(error); + } + } + }, + setInstance: (i) => { + instance = i; + memory = instance.exports.memory; + + setException = (error) => { + instance.exports._swift_js_exception.value = swift.memory.retain(error) + } + }, + /** @param {WebAssembly.Instance} instance */ + createExports: (instance) => { + const js = swift.memory.heap; + const exports = { + }; + _exports = exports; + return exports; + }, + } +} \ No newline at end of file diff --git a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/ImportTSTests/ReExportFrom.Macros.swift b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/ImportTSTests/ReExportFrom.Macros.swift new file mode 100644 index 00000000..0c0dfb36 --- /dev/null +++ b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/ImportTSTests/ReExportFrom.Macros.swift @@ -0,0 +1,14 @@ +// NOTICE: This is auto-generated code by BridgeJS from JavaScriptKit, +// DO NOT EDIT. +// +// To update this file, just rebuild your project or run +// `swift package bridge-js`. + +@_spi(Experimental) import JavaScriptKit + +@JSFunction func jsRoundTripNumber(_ v: Double) throws (JSException) -> Double + +@JSClass struct JsGreeter { + @JSFunction init(_ name: String) throws (JSException) + @JSFunction func greet() throws (JSException) -> String +} diff --git a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/ImportTSTests/ReExportFrom.swift b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/ImportTSTests/ReExportFrom.swift new file mode 100644 index 00000000..01c2b1e6 --- /dev/null +++ b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/ImportTSTests/ReExportFrom.swift @@ -0,0 +1,53 @@ +#if arch(wasm32) +@_extern(wasm, module: "Check", name: "bjs_jsRoundTripNumber") +fileprivate func bjs_jsRoundTripNumber(_ v: Float64) -> Float64 +#else +fileprivate func bjs_jsRoundTripNumber(_ v: Float64) -> Float64 { + fatalError("Only available on WebAssembly") +} +#endif + +func _$jsRoundTripNumber(_ v: Double) throws(JSException) -> Double { + let vValue = v.bridgeJSLowerParameter() + let ret = bjs_jsRoundTripNumber(vValue) + if let error = _swift_js_take_exception() { + throw error + } + return Double.bridgeJSLiftReturn(ret) +} + +#if arch(wasm32) +@_extern(wasm, module: "Check", name: "bjs_JsGreeter_init") +fileprivate func bjs_JsGreeter_init(_ name: Int32) -> Int32 +#else +fileprivate func bjs_JsGreeter_init(_ name: Int32) -> Int32 { + fatalError("Only available on WebAssembly") +} +#endif + +#if arch(wasm32) +@_extern(wasm, module: "Check", name: "bjs_JsGreeter_greet") +fileprivate func bjs_JsGreeter_greet(_ self: Int32) -> Int32 +#else +fileprivate func bjs_JsGreeter_greet(_ self: Int32) -> Int32 { + fatalError("Only available on WebAssembly") +} +#endif + +func _$JsGreeter_init(_ name: String) throws(JSException) -> JSObject { + let nameValue = name.bridgeJSLowerParameter() + let ret = bjs_JsGreeter_init(nameValue) + if let error = _swift_js_take_exception() { + throw error + } + return JSObject.bridgeJSLiftReturn(ret) +} + +func _$JsGreeter_greet(_ self: JSObject) throws(JSException) -> String { + let selfValue = self.bridgeJSLowerParameter() + let ret = bjs_JsGreeter_greet(selfValue) + if let error = _swift_js_take_exception() { + throw error + } + return String.bridgeJSLiftReturn(ret) +} \ No newline at end of file