Skip to content

Commit a6915eb

Browse files
committed
feat: expose namespaces to JS and add JS / TS code generation
1 parent 889fb2b commit a6915eb

File tree

3 files changed

+228
-5
lines changed

3 files changed

+228
-5
lines changed

Plugins/BridgeJS/Sources/BridgeJSLink/BridgeJSLink.swift

Lines changed: 132 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,7 @@ struct BridgeJSLink {
6262
var classLines: [String] = []
6363
var dtsExportLines: [String] = []
6464
var dtsClassLines: [String] = []
65+
var namespacedFunctions: [ExportedFunction] = []
6566

6667
if exportedSkeletons.contains(where: { $0.classes.count > 0 }) {
6768
classLines.append(
@@ -87,9 +88,15 @@ struct BridgeJSLink {
8788

8889
for function in skeleton.functions {
8990
var (js, dts) = renderExportedFunction(function: function)
91+
92+
if function.namespace != nil {
93+
namespacedFunctions.append(function)
94+
}
95+
9096
js[0] = "\(function.name): " + js[0]
9197
js[js.count - 1] += ","
9298
exportsLines.append(contentsOf: js)
99+
93100
dtsExportLines.append(contentsOf: dts)
94101
}
95102
}
@@ -108,6 +115,33 @@ struct BridgeJSLink {
108115
importObjectBuilders.append(importObjectBuilder)
109116
}
110117

118+
let hasNamespacedFunctions = !namespacedFunctions.isEmpty
119+
120+
let exportsSection: String
121+
if hasNamespacedFunctions {
122+
let setupLines = renderGlobalNamespace(namespacedFunctions: namespacedFunctions)
123+
let namespaceSetupCode = setupLines.map { $0.indent(count: 12) }.joined(separator: "\n")
124+
exportsSection = """
125+
\(classLines.map { $0.indent(count: 12) }.joined(separator: "\n"))
126+
const exports = {
127+
\(exportsLines.map { $0.indent(count: 16) }.joined(separator: "\n"))
128+
};
129+
130+
\(namespaceSetupCode)
131+
132+
return exports;
133+
},
134+
"""
135+
} else {
136+
exportsSection = """
137+
\(classLines.map { $0.indent(count: 12) }.joined(separator: "\n"))
138+
return {
139+
\(exportsLines.map { $0.indent(count: 16) }.joined(separator: "\n"))
140+
};
141+
},
142+
"""
143+
}
144+
111145
let outputJs = """
112146
// NOTICE: This is auto-generated code by BridgeJS from JavaScriptKit,
113147
// DO NOT EDIT.
@@ -169,15 +203,67 @@ struct BridgeJSLink {
169203
/** @param {WebAssembly.Instance} instance */
170204
createExports: (instance) => {
171205
const js = swift.memory.heap;
172-
\(classLines.map { $0.indent(count: 12) }.joined(separator: "\n"))
173-
return {
174-
\(exportsLines.map { $0.indent(count: 16) }.joined(separator: "\n"))
175-
};
176-
},
206+
\(exportsSection)
177207
}
178208
}
179209
"""
210+
211+
// Collect namespace declarations for TypeScript
212+
var namespaceDeclarations: [String: [(name: String, parameters: [Parameter], returnType: BridgeType)]] = [:]
213+
214+
for skeleton in exportedSkeletons {
215+
for function in skeleton.functions {
216+
if let namespace = function.namespace {
217+
let namespaceKey = namespace.joined(separator: ".")
218+
if namespaceDeclarations[namespaceKey] == nil {
219+
namespaceDeclarations[namespaceKey] = []
220+
}
221+
namespaceDeclarations[namespaceKey]?.append((function.name, function.parameters, function.returnType))
222+
}
223+
}
224+
}
225+
226+
// Generate namespace declarations in TypeScript
180227
var dtsLines: [String] = []
228+
229+
// Only add export {} and declare global block if we have namespace declarations
230+
let hasNamespaceDeclarations = !namespaceDeclarations.isEmpty
231+
232+
if hasNamespaceDeclarations {
233+
dtsLines.append("export {};")
234+
dtsLines.append("")
235+
dtsLines.append("declare global {")
236+
}
237+
238+
// Generate namespace structure using nested declarations
239+
for (namespacePath, functions) in namespaceDeclarations.sorted(by: { $0.key < $1.key }) {
240+
let parts = namespacePath.split(separator: ".").map(String.init)
241+
242+
// Open namespaces with proper indentation
243+
for i in 0..<parts.count {
244+
dtsLines.append("namespace \(parts[i]) {".indent(count: 4*(hasNamespaceDeclarations ? i + 1: 1)) )
245+
}
246+
247+
// Add function signatures with proper indentation
248+
let functionIndentationLevel = hasNamespaceDeclarations ? parts.count + 1 : parts.count
249+
for (name, parameters, returnType) in functions {
250+
let signature = "function \(name)\(renderTSSignature(parameters: parameters, returnType: returnType));"
251+
dtsLines.append("\(signature)".indent(count: 4*functionIndentationLevel))
252+
}
253+
254+
// Close namespaces with proper indentation (in reverse order)
255+
for i in (0..<parts.count).reversed() {
256+
let indentationLevel = hasNamespaceDeclarations ? i + 1 : i
257+
dtsLines.append("}".indent(count: 4*indentationLevel))
258+
}
259+
}
260+
261+
if hasNamespaceDeclarations {
262+
dtsLines.append("}")
263+
dtsLines.append("")
264+
}
265+
266+
// Add remaining class lines
181267
dtsLines.append(contentsOf: dtsClassLines)
182268
dtsLines.append("export type Exports = {")
183269
dtsLines.append(contentsOf: dtsExportLines.map { $0.indent(count: 4) })
@@ -395,6 +481,47 @@ struct BridgeJSLink {
395481

396482
return (jsLines, dtsTypeLines, dtsExportEntryLines)
397483
}
484+
485+
// __Swift.Foundation.UUID
486+
487+
// [__Swift, Foundation, UUID]
488+
// [[__Swift, Foundation, UUID], [__Swift, Foundation]]
489+
490+
// __Swift
491+
// __Swift.Foundation
492+
// __Swift.Foundation.UUID
493+
494+
func renderGlobalNamespace(namespacedFunctions: [ExportedFunction]) -> [String] {
495+
var lines: [String] = []
496+
var uniqueNamespaces: [String] = []
497+
498+
var namespacePaths: Set<[String]> = Set(namespacedFunctions
499+
.compactMap { $0.namespace })
500+
501+
namespacePaths.forEach { namespacePath in
502+
namespacePath.makeIterator().enumerated().forEach { (index, element) in
503+
let path = namespacePath[0...index].joined(separator: ".")
504+
if !uniqueNamespaces.contains(path) {
505+
uniqueNamespaces.append(path)
506+
}
507+
}
508+
}
509+
510+
uniqueNamespaces.map { namespace in
511+
lines.append("if (typeof globalThis.\(namespace) === 'undefined') {")
512+
lines.append(" globalThis.\(namespace) = {};")
513+
lines.append("}")
514+
}
515+
516+
for function in namespacedFunctions {
517+
if let namespace = function.namespace {
518+
let namespacePath = namespace.joined(separator: ".")
519+
lines.append("globalThis.\(namespacePath).\(function.name) = exports.\(function.name);")
520+
}
521+
}
522+
523+
return lines
524+
}
398525

399526
class ImportedThunkBuilder {
400527
var bodyLines: [String] = []

Plugins/BridgeJS/Sources/BridgeJSSkeleton/BridgeJSSkeleton.swift

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,12 +29,14 @@ struct ExportedFunction: Codable {
2929
var parameters: [Parameter]
3030
var returnType: BridgeType
3131
var effects: Effects
32+
var namespace: [String]?
3233
}
3334

3435
struct ExportedClass: Codable {
3536
var name: String
3637
var constructor: ExportedConstructor?
3738
var methods: [ExportedFunction]
39+
var namespace: [String]?
3840
}
3941

4042
struct ExportedConstructor: Codable {
Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
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 async function createInstantiator(options, swift) {
8+
let instance;
9+
let memory;
10+
let setException;
11+
const textDecoder = new TextDecoder("utf-8");
12+
const textEncoder = new TextEncoder("utf-8");
13+
14+
let tmpRetString;
15+
let tmpRetBytes;
16+
let tmpRetException;
17+
return {
18+
/** @param {WebAssembly.Imports} importObject */
19+
addImports: (importObject) => {
20+
const bjs = {};
21+
importObject["bjs"] = bjs;
22+
bjs["swift_js_return_string"] = function(ptr, len) {
23+
const bytes = new Uint8Array(memory.buffer, ptr, len);
24+
tmpRetString = textDecoder.decode(bytes);
25+
}
26+
bjs["swift_js_init_memory"] = function(sourceId, bytesPtr) {
27+
const source = swift.memory.getObject(sourceId);
28+
const bytes = new Uint8Array(memory.buffer, bytesPtr);
29+
bytes.set(source);
30+
}
31+
bjs["swift_js_make_js_string"] = function(ptr, len) {
32+
const bytes = new Uint8Array(memory.buffer, ptr, len);
33+
return swift.memory.retain(textDecoder.decode(bytes));
34+
}
35+
bjs["swift_js_init_memory_with_result"] = function(ptr, len) {
36+
const target = new Uint8Array(memory.buffer, ptr, len);
37+
target.set(tmpRetBytes);
38+
tmpRetBytes = undefined;
39+
}
40+
bjs["swift_js_throw"] = function(id) {
41+
tmpRetException = swift.memory.retainByRef(id);
42+
}
43+
bjs["swift_js_retain"] = function(id) {
44+
return swift.memory.retainByRef(id);
45+
}
46+
bjs["swift_js_release"] = function(id) {
47+
swift.memory.release(id);
48+
}
49+
const TestModule = importObject["TestModule"] = {};
50+
TestModule["bjs_returnAnimatable"] = function bjs_returnAnimatable() {
51+
try {
52+
let ret = options.imports.returnAnimatable();
53+
return swift.memory.retain(ret);
54+
} catch (error) {
55+
setException(error);
56+
return 0
57+
}
58+
}
59+
TestModule["bjs_Animatable_animate"] = function bjs_Animatable_animate(self, keyframes, options) {
60+
try {
61+
let ret = swift.memory.getObject(self).animate(swift.memory.getObject(keyframes), swift.memory.getObject(options));
62+
return swift.memory.retain(ret);
63+
} catch (error) {
64+
setException(error);
65+
return 0
66+
}
67+
}
68+
TestModule["bjs_Animatable_getAnimations"] = function bjs_Animatable_getAnimations(self, options) {
69+
try {
70+
let ret = swift.memory.getObject(self).getAnimations(swift.memory.getObject(options));
71+
return swift.memory.retain(ret);
72+
} catch (error) {
73+
setException(error);
74+
return 0
75+
}
76+
}
77+
},
78+
setInstance: (i) => {
79+
instance = i;
80+
memory = instance.exports.memory;
81+
setException = (error) => {
82+
instance.exports._swift_js_exception.value = swift.memory.retain(error)
83+
}
84+
},
85+
/** @param {WebAssembly.Instance} instance */
86+
createExports: (instance) => {
87+
const js = swift.memory.heap;
88+
89+
return {
90+
91+
};
92+
},
93+
}
94+
}

0 commit comments

Comments
 (0)