Skip to content

Commit 724e62e

Browse files
[BridgeJS] Support @js var declarations for global scope imports (#505)
1 parent eca0298 commit 724e62e

File tree

9 files changed

+410
-11
lines changed

9 files changed

+410
-11
lines changed

Plugins/BridgeJS/Package.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@ let package = Package(
5555
"BridgeJSLink",
5656
"TS2Swift",
5757
],
58-
exclude: ["__Snapshots__", "Inputs", "MultifileInputs"]
58+
exclude: ["__Snapshots__", "Inputs", "MultifileInputs", "ImportMacroInputs"]
5959
),
6060
.macro(
6161
name: "BridgeJSMacros",

Plugins/BridgeJS/Sources/BridgeJSCore/ImportSwiftMacros.swift

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,8 @@ public final class ImportSwiftMacros {
4444
importedFiles.append(
4545
ImportedFileSkeleton(
4646
functions: collector.importedFunctions,
47-
types: collector.importedTypes
47+
types: collector.importedTypes,
48+
globalGetters: collector.importedGlobalGetters
4849
)
4950
)
5051
}
@@ -94,6 +95,7 @@ public final class ImportSwiftMacros {
9495
fileprivate final class APICollector: SyntaxAnyVisitor {
9596
var importedFunctions: [ImportedFunctionSkeleton] = []
9697
var importedTypes: [ImportedTypeSkeleton] = []
98+
var importedGlobalGetters: [ImportedGetterSkeleton] = []
9799
var errors: [DiagnosticError] = []
98100

99101
private let inputFilePath: String
@@ -432,12 +434,9 @@ public final class ImportSwiftMacros {
432434

433435
switch state {
434436
case .topLevel:
435-
errors.append(
436-
DiagnosticError(
437-
node: node,
438-
message: "@JSGetter is not supported at top-level. Use it only in @JSClass types."
439-
)
440-
)
437+
if let getter = parseGetterSkeleton(node, enclosingTypeName: nil) {
438+
importedGlobalGetters.append(getter)
439+
}
441440
return .skipChildren
442441

443442
case .jsClassBody(let typeName):

Plugins/BridgeJS/Sources/BridgeJSCore/ImportTS.swift

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,10 @@ public struct ImportTS {
3636
public func finalize() throws -> String? {
3737
var decls: [DeclSyntax] = []
3838
for skeleton in self.skeleton.children {
39+
for getter in skeleton.globalGetters {
40+
let getterDecls = try renderSwiftGlobalGetter(getter, topLevelDecls: &decls)
41+
decls.append(contentsOf: getterDecls)
42+
}
3943
for function in skeleton.functions {
4044
let thunkDecls = try renderSwiftThunk(function, topLevelDecls: &decls)
4145
decls.append(contentsOf: thunkDecls)
@@ -54,6 +58,24 @@ public struct ImportTS {
5458
return decls.map { $0.formatted(using: format).description }.joined(separator: "\n\n")
5559
}
5660

61+
func renderSwiftGlobalGetter(
62+
_ getter: ImportedGetterSkeleton,
63+
topLevelDecls: inout [DeclSyntax]
64+
) throws -> [DeclSyntax] {
65+
let builder = CallJSEmission(moduleName: moduleName, abiName: getter.abiName(context: nil))
66+
try builder.call(returnType: getter.type)
67+
try builder.liftReturnValue(returnType: getter.type)
68+
topLevelDecls.append(builder.renderImportDecl())
69+
return [
70+
builder.renderThunkDecl(
71+
name: "_$\(getter.name)_get",
72+
parameters: [],
73+
returnType: getter.type
74+
)
75+
.with(\.leadingTrivia, Self.renderDocumentation(documentation: getter.documentation))
76+
]
77+
}
78+
5779
class CallJSEmission {
5880
let abiName: String
5981
let moduleName: String

Plugins/BridgeJS/Sources/BridgeJSLink/BridgeJSLink.swift

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -172,6 +172,9 @@ struct BridgeJSLink {
172172
guard let imported = unified.imported else { continue }
173173
let importObjectBuilder = ImportObjectBuilder(moduleName: unified.moduleName)
174174
for fileSkeleton in imported.children {
175+
for getter in fileSkeleton.globalGetters {
176+
try renderImportedGlobalGetter(importObjectBuilder: importObjectBuilder, getter: getter)
177+
}
175178
for function in fileSkeleton.functions {
176179
try renderImportedFunction(importObjectBuilder: importObjectBuilder, function: function)
177180
}
@@ -2142,6 +2145,31 @@ extension BridgeJSLink {
21422145
body.write("\(call);")
21432146
}
21442147

2148+
func getImportProperty(name: String, returnType: BridgeType) throws -> String? {
2149+
if returnType == .void {
2150+
throw BridgeJSLinkError(message: "Void is not supported for imported JS properties")
2151+
}
2152+
2153+
let loweringFragment = try IntrinsicJSFragment.lowerReturn(type: returnType, context: context)
2154+
let expr = "imports[\"\(name)\"]"
2155+
2156+
let returnExpr: String?
2157+
if loweringFragment.parameters.count == 0 {
2158+
body.write("\(expr);")
2159+
returnExpr = nil
2160+
} else {
2161+
let resultVariable = scope.variable("ret")
2162+
body.write("let \(resultVariable) = \(expr);")
2163+
returnExpr = resultVariable
2164+
}
2165+
2166+
return try lowerReturnValue(
2167+
returnType: returnType,
2168+
returnExpr: returnExpr,
2169+
loweringFragment: loweringFragment
2170+
)
2171+
}
2172+
21452173
private func lowerReturnValue(
21462174
returnType: BridgeType,
21472175
returnExpr: String?,
@@ -2881,6 +2909,22 @@ extension BridgeJSLink {
28812909
importObjectBuilder.assignToImportObject(name: function.abiName(context: nil), function: funcLines)
28822910
}
28832911

2912+
func renderImportedGlobalGetter(
2913+
importObjectBuilder: ImportObjectBuilder,
2914+
getter: ImportedGetterSkeleton
2915+
) throws {
2916+
let thunkBuilder = ImportedThunkBuilder()
2917+
let returnExpr = try thunkBuilder.getImportProperty(name: getter.name, returnType: getter.type)
2918+
let abiName = getter.abiName(context: nil)
2919+
let funcLines = thunkBuilder.renderFunction(
2920+
name: abiName,
2921+
returnExpr: returnExpr,
2922+
returnType: getter.type
2923+
)
2924+
importObjectBuilder.appendDts(["readonly \(getter.name): \(getter.type.tsType);"])
2925+
importObjectBuilder.assignToImportObject(name: abiName, function: funcLines)
2926+
}
2927+
28842928
func renderImportedType(
28852929
importObjectBuilder: ImportObjectBuilder,
28862930
type: ImportedTypeSkeleton

Plugins/BridgeJS/Sources/BridgeJSSkeleton/BridgeJSSkeleton.swift

Lines changed: 41 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -634,7 +634,7 @@ public struct ImportedGetterSkeleton: Codable {
634634
self.functionName = functionName
635635
}
636636

637-
public func abiName(context: ImportedTypeSkeleton) -> String {
637+
public func abiName(context: ImportedTypeSkeleton?) -> String {
638638
if let functionName = functionName {
639639
return ABINameGenerator.generateImportedABIName(
640640
baseName: functionName,
@@ -669,7 +669,7 @@ public struct ImportedSetterSkeleton: Codable {
669669
self.functionName = functionName
670670
}
671671

672-
public func abiName(context: ImportedTypeSkeleton) -> String {
672+
public func abiName(context: ImportedTypeSkeleton?) -> String {
673673
if let functionName = functionName {
674674
return ABINameGenerator.generateImportedABIName(
675675
baseName: functionName,
@@ -713,10 +713,48 @@ public struct ImportedTypeSkeleton: Codable {
713713
public struct ImportedFileSkeleton: Codable {
714714
public let functions: [ImportedFunctionSkeleton]
715715
public let types: [ImportedTypeSkeleton]
716+
/// Global-scope imported properties (e.g. `@JSGetter var console: JSConsole`)
717+
public let globalGetters: [ImportedGetterSkeleton]
718+
/// Global-scope imported properties (future use; not currently emitted by macros)
719+
public let globalSetters: [ImportedSetterSkeleton]
716720

717-
public init(functions: [ImportedFunctionSkeleton], types: [ImportedTypeSkeleton]) {
721+
public init(
722+
functions: [ImportedFunctionSkeleton],
723+
types: [ImportedTypeSkeleton],
724+
globalGetters: [ImportedGetterSkeleton] = [],
725+
globalSetters: [ImportedSetterSkeleton] = []
726+
) {
718727
self.functions = functions
719728
self.types = types
729+
self.globalGetters = globalGetters
730+
self.globalSetters = globalSetters
731+
}
732+
733+
private enum CodingKeys: String, CodingKey {
734+
case functions
735+
case types
736+
case globalGetters
737+
case globalSetters
738+
}
739+
740+
public init(from decoder: any Decoder) throws {
741+
let container = try decoder.container(keyedBy: CodingKeys.self)
742+
self.functions = try container.decode([ImportedFunctionSkeleton].self, forKey: .functions)
743+
self.types = try container.decode([ImportedTypeSkeleton].self, forKey: .types)
744+
self.globalGetters = try container.decodeIfPresent([ImportedGetterSkeleton].self, forKey: .globalGetters) ?? []
745+
self.globalSetters = try container.decodeIfPresent([ImportedSetterSkeleton].self, forKey: .globalSetters) ?? []
746+
}
747+
748+
public func encode(to encoder: any Encoder) throws {
749+
var container = encoder.container(keyedBy: CodingKeys.self)
750+
try container.encode(functions, forKey: .functions)
751+
try container.encode(types, forKey: .types)
752+
if !globalGetters.isEmpty {
753+
try container.encode(globalGetters, forKey: .globalGetters)
754+
}
755+
if !globalSetters.isEmpty {
756+
try container.encode(globalSetters, forKey: .globalSetters)
757+
}
720758
}
721759
}
722760

Plugins/BridgeJS/Tests/BridgeJSToolTests/BridgeJSLinkTests.swift

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,12 +38,21 @@ import Testing
3838
"Inputs"
3939
)
4040

41+
static let importMacroInputsDirectory = URL(fileURLWithPath: #filePath).deletingLastPathComponent()
42+
.appendingPathComponent("ImportMacroInputs")
43+
4144
static func collectInputs(extension: String) -> [String] {
4245
let fileManager = FileManager.default
4346
let inputs = try! fileManager.contentsOfDirectory(atPath: Self.inputsDirectory.path)
4447
return inputs.filter { $0.hasSuffix(`extension`) }
4548
}
4649

50+
static func collectImportMacroInputs() -> [String] {
51+
let fileManager = FileManager.default
52+
let inputs = try! fileManager.contentsOfDirectory(atPath: Self.importMacroInputsDirectory.path)
53+
return inputs.filter { $0.hasSuffix(".swift") }
54+
}
55+
4756
@Test(arguments: collectInputs(extension: ".swift"))
4857
func snapshotExport(input: String) throws {
4958
let url = Self.inputsDirectory.appendingPathComponent(input)
@@ -101,6 +110,35 @@ import Testing
101110
try snapshot(bridgeJSLink: bridgeJSLink, name: name + ".Import")
102111
}
103112

113+
@Test(arguments: collectImportMacroInputs())
114+
func snapshotImportMacroInput(input: String) throws {
115+
let url = Self.importMacroInputsDirectory.appendingPathComponent(input)
116+
let name = url.deletingPathExtension().lastPathComponent
117+
118+
let sourceFile = Parser.parse(source: try String(contentsOf: url, encoding: .utf8))
119+
let importSwift = ImportSwiftMacros(progress: .silent, moduleName: "TestModule")
120+
importSwift.addSourceFile(sourceFile, "\(name).swift")
121+
let importResult = try importSwift.finalize()
122+
123+
var importTS = ImportTS(progress: .silent, moduleName: "TestModule")
124+
for child in importResult.outputSkeleton.children {
125+
importTS.addSkeleton(child)
126+
}
127+
let importSkeleton = importTS.skeleton
128+
129+
var bridgeJSLink = BridgeJSLink(sharedMemory: false)
130+
let unifiedSkeleton = BridgeJSSkeleton(
131+
moduleName: "TestModule",
132+
exported: nil,
133+
imported: importSkeleton
134+
)
135+
let encoder = JSONEncoder()
136+
encoder.outputFormatting = [.prettyPrinted, .sortedKeys]
137+
let unifiedData = try encoder.encode(unifiedSkeleton)
138+
try bridgeJSLink.addSkeletonFile(data: unifiedData)
139+
try snapshot(bridgeJSLink: bridgeJSLink, name: name + ".ImportMacros")
140+
}
141+
104142
@Test(arguments: [
105143
"Namespaces.swift",
106144
"StaticFunctions.swift",
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
@JSClass
2+
struct JSConsole {
3+
@JSFunction func log(_ message: String) throws(JSException)
4+
}
5+
6+
@JSGetter var console: JSConsole
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
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 interface JSConsole {
8+
log(message: string): void;
9+
}
10+
export type Exports = {
11+
}
12+
export type Imports = {
13+
readonly console: JSConsole;
14+
}
15+
export function createInstantiator(options: {
16+
imports: Imports;
17+
}, swift: any): Promise<{
18+
addImports: (importObject: WebAssembly.Imports) => void;
19+
setInstance: (instance: WebAssembly.Instance) => void;
20+
createExports: (instance: WebAssembly.Instance) => Exports;
21+
}>;

0 commit comments

Comments
 (0)