Skip to content

Commit 5a553c4

Browse files
Merge pull request #405 from PassiveLogic/feat/macro-namespace
BridgeJS: Macro extension to define namespace
2 parents d62db09 + e701702 commit 5a553c4

File tree

11 files changed

+931
-15
lines changed

11 files changed

+931
-15
lines changed

Plugins/BridgeJS/Sources/BridgeJSCore/ExportSwift.swift

Lines changed: 54 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -123,10 +123,21 @@ class ExportSwift {
123123
}
124124

125125
private func visitFunction(node: FunctionDeclSyntax) -> ExportedFunction? {
126-
guard node.attributes.hasJSAttribute() else {
126+
guard let jsAttribute = node.attributes.firstJSAttribute else {
127127
return nil
128128
}
129+
129130
let name = node.name.text
131+
let namespace = extractNamespace(from: jsAttribute)
132+
133+
if namespace != nil, case .classBody = state {
134+
diagnose(
135+
node: jsAttribute,
136+
message: "Namespace is only needed in top-level declaration",
137+
hint: "Remove the namespace from @JS attribute or move this function to top-level"
138+
)
139+
}
140+
130141
var parameters: [Parameter] = []
131142
for param in node.signature.parameterClause.parameters {
132143
guard let type = self.parent.lookupType(for: param.type) else {
@@ -165,7 +176,8 @@ class ExportSwift {
165176
abiName: abiName,
166177
parameters: parameters,
167178
returnType: returnType,
168-
effects: effects
179+
effects: effects,
180+
namespace: namespace
169181
)
170182
}
171183

@@ -193,12 +205,40 @@ class ExportSwift {
193205
return Effects(isAsync: isAsync, isThrows: isThrows)
194206
}
195207

208+
private func extractNamespace(
209+
from jsAttribute: AttributeSyntax
210+
) -> [String]? {
211+
guard let arguments = jsAttribute.arguments?.as(LabeledExprListSyntax.self) else {
212+
return nil
213+
}
214+
215+
guard let namespaceArg = arguments.first(where: { $0.label?.text == "namespace" }),
216+
let stringLiteral = namespaceArg.expression.as(StringLiteralExprSyntax.self),
217+
let namespaceString = stringLiteral.segments.first?.as(StringSegmentSyntax.self)?.content.text
218+
else {
219+
return nil
220+
}
221+
222+
return namespaceString.split(separator: ".").map(String.init)
223+
}
224+
196225
override func visit(_ node: InitializerDeclSyntax) -> SyntaxVisitorContinueKind {
197226
guard node.attributes.hasJSAttribute() else { return .skipChildren }
198227
guard case .classBody(let name) = state else {
199228
diagnose(node: node, message: "@JS init must be inside a @JS class")
200229
return .skipChildren
201230
}
231+
232+
if let jsAttribute = node.attributes.firstJSAttribute,
233+
extractNamespace(from: jsAttribute) != nil
234+
{
235+
diagnose(
236+
node: jsAttribute,
237+
message: "Namespace is not supported for initializer declarations",
238+
hint: "Remove the namespace from @JS attribute"
239+
)
240+
}
241+
202242
var parameters: [Parameter] = []
203243
for param in node.signature.parameterClause.parameters {
204244
guard let type = self.parent.lookupType(for: param.type) else {
@@ -225,13 +265,17 @@ class ExportSwift {
225265

226266
override func visit(_ node: ClassDeclSyntax) -> SyntaxVisitorContinueKind {
227267
let name = node.name.text
268+
228269
stateStack.push(state: .classBody(name: name))
229270

230-
guard node.attributes.hasJSAttribute() else { return .skipChildren }
271+
guard let jsAttribute = node.attributes.firstJSAttribute else { return .skipChildren }
272+
273+
let namespace = extractNamespace(from: jsAttribute)
231274
exportedClassByName[name] = ExportedClass(
232275
name: name,
233276
constructor: nil,
234-
methods: []
277+
methods: [],
278+
namespace: namespace
235279
)
236280
exportedClassNames.append(name)
237281
return .visitChildren
@@ -635,9 +679,13 @@ class ExportSwift {
635679

636680
extension AttributeListSyntax {
637681
fileprivate func hasJSAttribute() -> Bool {
638-
return first(where: {
682+
firstJSAttribute != nil
683+
}
684+
685+
fileprivate var firstJSAttribute: AttributeSyntax? {
686+
first(where: {
639687
$0.as(AttributeSyntax.self)?.attributeName.trimmedDescription == "JS"
640-
}) != nil
688+
})?.as(AttributeSyntax.self)
641689
}
642690
}
643691

Plugins/BridgeJS/Sources/BridgeJSLink/BridgeJSLink.swift

Lines changed: 187 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,8 @@ struct BridgeJSLink {
6262
var classLines: [String] = []
6363
var dtsExportLines: [String] = []
6464
var dtsClassLines: [String] = []
65+
var namespacedFunctions: [ExportedFunction] = []
66+
var namespacedClasses: [ExportedClass] = []
6567

6668
if exportedSkeletons.contains(where: { $0.classes.count > 0 }) {
6769
classLines.append(
@@ -83,10 +85,19 @@ struct BridgeJSLink {
8385
exportsLines.append("\(klass.name),")
8486
dtsExportLines.append(contentsOf: dtsExportEntry)
8587
dtsClassLines.append(contentsOf: dtsType)
88+
89+
if klass.namespace != nil {
90+
namespacedClasses.append(klass)
91+
}
8692
}
8793

8894
for function in skeleton.functions {
8995
var (js, dts) = renderExportedFunction(function: function)
96+
97+
if function.namespace != nil {
98+
namespacedFunctions.append(function)
99+
}
100+
90101
js[0] = "\(function.name): " + js[0]
91102
js[js.count - 1] += ","
92103
exportsLines.append(contentsOf: js)
@@ -108,6 +119,36 @@ struct BridgeJSLink {
108119
importObjectBuilders.append(importObjectBuilder)
109120
}
110121

122+
let hasNamespacedItems = !namespacedFunctions.isEmpty || !namespacedClasses.isEmpty
123+
124+
let exportsSection: String
125+
if hasNamespacedItems {
126+
let namespaceSetupCode = renderGlobalNamespace(
127+
namespacedFunctions: namespacedFunctions,
128+
namespacedClasses: namespacedClasses
129+
)
130+
.map { $0.indent(count: 12) }.joined(separator: "\n")
131+
exportsSection = """
132+
\(classLines.map { $0.indent(count: 12) }.joined(separator: "\n"))
133+
const exports = {
134+
\(exportsLines.map { $0.indent(count: 16) }.joined(separator: "\n"))
135+
};
136+
137+
\(namespaceSetupCode)
138+
139+
return exports;
140+
},
141+
"""
142+
} else {
143+
exportsSection = """
144+
\(classLines.map { $0.indent(count: 12) }.joined(separator: "\n"))
145+
return {
146+
\(exportsLines.map { $0.indent(count: 16) }.joined(separator: "\n"))
147+
};
148+
},
149+
"""
150+
}
151+
111152
let outputJs = """
112153
// NOTICE: This is auto-generated code by BridgeJS from JavaScriptKit,
113154
// DO NOT EDIT.
@@ -169,15 +210,13 @@ struct BridgeJSLink {
169210
/** @param {WebAssembly.Instance} instance */
170211
createExports: (instance) => {
171212
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-
},
213+
\(exportsSection)
177214
}
178215
}
179216
"""
217+
180218
var dtsLines: [String] = []
219+
dtsLines.append(contentsOf: namespaceDeclarations())
181220
dtsLines.append(contentsOf: dtsClassLines)
182221
dtsLines.append("export type Exports = {")
183222
dtsLines.append(contentsOf: dtsExportLines.map { $0.indent(count: 4) })
@@ -204,6 +243,102 @@ struct BridgeJSLink {
204243
return (outputJs, outputDts)
205244
}
206245

246+
private func namespaceDeclarations() -> [String] {
247+
var dtsLines: [String] = []
248+
var namespaceFunctions: [String: [ExportedFunction]] = [:]
249+
var namespaceClasses: [String: [ExportedClass]] = [:]
250+
251+
for skeleton in exportedSkeletons {
252+
for function in skeleton.functions {
253+
if let namespace = function.namespace {
254+
let namespaceKey = namespace.joined(separator: ".")
255+
if namespaceFunctions[namespaceKey] == nil {
256+
namespaceFunctions[namespaceKey] = []
257+
}
258+
namespaceFunctions[namespaceKey]?.append(function)
259+
}
260+
}
261+
262+
for klass in skeleton.classes {
263+
if let classNamespace = klass.namespace {
264+
let namespaceKey = classNamespace.joined(separator: ".")
265+
if namespaceClasses[namespaceKey] == nil {
266+
namespaceClasses[namespaceKey] = []
267+
}
268+
namespaceClasses[namespaceKey]?.append(klass)
269+
}
270+
}
271+
}
272+
273+
guard !namespaceFunctions.isEmpty || !namespaceClasses.isEmpty else { return dtsLines }
274+
275+
dtsLines.append("export {};")
276+
dtsLines.append("")
277+
dtsLines.append("declare global {")
278+
279+
let identBaseSize = 4
280+
281+
for (namespacePath, classes) in namespaceClasses.sorted(by: { $0.key < $1.key }) {
282+
let parts = namespacePath.split(separator: ".").map(String.init)
283+
284+
for i in 0..<parts.count {
285+
dtsLines.append("namespace \(parts[i]) {".indent(count: identBaseSize * (i + 1)))
286+
}
287+
288+
for klass in classes {
289+
dtsLines.append("class \(klass.name) {".indent(count: identBaseSize * (parts.count + 1)))
290+
291+
if let constructor = klass.constructor {
292+
let constructorSignature =
293+
"constructor(\(constructor.parameters.map { "\($0.name): \($0.type.tsType)" }.joined(separator: ", ")));"
294+
dtsLines.append("\(constructorSignature)".indent(count: identBaseSize * (parts.count + 2)))
295+
}
296+
297+
for method in klass.methods {
298+
let methodSignature =
299+
"\(method.name)\(renderTSSignature(parameters: method.parameters, returnType: method.returnType));"
300+
dtsLines.append("\(methodSignature)".indent(count: identBaseSize * (parts.count + 2)))
301+
}
302+
303+
dtsLines.append("}".indent(count: identBaseSize * (parts.count + 1)))
304+
}
305+
306+
for i in (0..<parts.count).reversed() {
307+
dtsLines.append("}".indent(count: identBaseSize * (i + 1)))
308+
}
309+
}
310+
311+
for (namespacePath, functions) in namespaceFunctions.sorted(by: { $0.key < $1.key }) {
312+
let parts = namespacePath.split(separator: ".").map(String.init)
313+
314+
var namespaceExists = false
315+
if namespaceClasses[namespacePath] != nil {
316+
namespaceExists = true
317+
} else {
318+
for i in 0..<parts.count {
319+
dtsLines.append("namespace \(parts[i]) {".indent(count: identBaseSize * (i + 1)))
320+
}
321+
}
322+
323+
for function in functions {
324+
let signature =
325+
"function \(function.name)\(renderTSSignature(parameters: function.parameters, returnType: function.returnType));"
326+
dtsLines.append("\(signature)".indent(count: identBaseSize * (parts.count + 1)))
327+
}
328+
329+
if !namespaceExists {
330+
for i in (0..<parts.count).reversed() {
331+
dtsLines.append("}".indent(count: identBaseSize * (i + 1)))
332+
}
333+
}
334+
}
335+
336+
dtsLines.append("}")
337+
dtsLines.append("")
338+
339+
return dtsLines
340+
}
341+
207342
class ExportedThunkBuilder {
208343
var bodyLines: [String] = []
209344
var cleanupLines: [String] = []
@@ -396,6 +531,53 @@ struct BridgeJSLink {
396531
return (jsLines, dtsTypeLines, dtsExportEntryLines)
397532
}
398533

534+
func renderGlobalNamespace(namespacedFunctions: [ExportedFunction], namespacedClasses: [ExportedClass]) -> [String]
535+
{
536+
var lines: [String] = []
537+
var uniqueNamespaces: [String] = []
538+
var seen = Set<String>()
539+
540+
let functionNamespacePaths: Set<[String]> = Set(
541+
namespacedFunctions
542+
.compactMap { $0.namespace }
543+
)
544+
let classNamespacePaths: Set<[String]> = Set(
545+
namespacedClasses
546+
.compactMap { $0.namespace }
547+
)
548+
549+
let allNamespacePaths =
550+
functionNamespacePaths
551+
.union(classNamespacePaths)
552+
553+
allNamespacePaths.forEach { namespacePath in
554+
namespacePath.makeIterator().enumerated().forEach { (index, _) in
555+
let path = namespacePath[0...index].joined(separator: ".")
556+
if seen.insert(path).inserted {
557+
uniqueNamespaces.append(path)
558+
}
559+
}
560+
}
561+
562+
uniqueNamespaces.sorted().forEach { namespace in
563+
lines.append("if (typeof globalThis.\(namespace) === 'undefined') {")
564+
lines.append(" globalThis.\(namespace) = {};")
565+
lines.append("}")
566+
}
567+
568+
namespacedClasses.forEach { klass in
569+
let namespacePath: String = klass.namespace?.joined(separator: ".") ?? ""
570+
lines.append("globalThis.\(namespacePath).\(klass.name) = exports.\(klass.name);")
571+
}
572+
573+
namespacedFunctions.forEach { function in
574+
let namespacePath: String = function.namespace?.joined(separator: ".") ?? ""
575+
lines.append("globalThis.\(namespacePath).\(function.name) = exports.\(function.name);")
576+
}
577+
578+
return lines
579+
}
580+
399581
class ImportedThunkBuilder {
400582
var bodyLines: [String] = []
401583
var parameterNames: [String] = []

Plugins/BridgeJS/Sources/BridgeJSSkeleton/BridgeJSSkeleton.swift

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,18 +29,21 @@ 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 {
4143
var abiName: String
4244
var parameters: [Parameter]
4345
var effects: Effects
46+
var namespace: [String]?
4447
}
4548

4649
struct ExportedSkeleton: Codable {

0 commit comments

Comments
 (0)