Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Plugins/BridgeJS/Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ let package = Package(
"BridgeJSLink",
"TS2Swift",
],
exclude: ["__Snapshots__", "Inputs", "MultifileInputs"]
exclude: ["__Snapshots__", "Inputs", "MultifileInputs", "ImportMacroInputs"]
),
.macro(
name: "BridgeJSMacros",
Expand Down
13 changes: 6 additions & 7 deletions Plugins/BridgeJS/Sources/BridgeJSCore/ImportSwiftMacros.swift
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,8 @@ public final class ImportSwiftMacros {
importedFiles.append(
ImportedFileSkeleton(
functions: collector.importedFunctions,
types: collector.importedTypes
types: collector.importedTypes,
globalGetters: collector.importedGlobalGetters
)
)
}
Expand Down Expand Up @@ -94,6 +95,7 @@ public final class ImportSwiftMacros {
fileprivate final class APICollector: SyntaxAnyVisitor {
var importedFunctions: [ImportedFunctionSkeleton] = []
var importedTypes: [ImportedTypeSkeleton] = []
var importedGlobalGetters: [ImportedGetterSkeleton] = []
var errors: [DiagnosticError] = []

private let inputFilePath: String
Expand Down Expand Up @@ -432,12 +434,9 @@ public final class ImportSwiftMacros {

switch state {
case .topLevel:
errors.append(
DiagnosticError(
node: node,
message: "@JSGetter is not supported at top-level. Use it only in @JSClass types."
)
)
if let getter = parseGetterSkeleton(node, enclosingTypeName: nil) {
importedGlobalGetters.append(getter)
}
return .skipChildren

case .jsClassBody(let typeName):
Expand Down
22 changes: 22 additions & 0 deletions Plugins/BridgeJS/Sources/BridgeJSCore/ImportTS.swift
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,10 @@ public struct ImportTS {
public func finalize() throws -> String? {
var decls: [DeclSyntax] = []
for skeleton in self.skeleton.children {
for getter in skeleton.globalGetters {
let getterDecls = try renderSwiftGlobalGetter(getter, topLevelDecls: &decls)
decls.append(contentsOf: getterDecls)
}
for function in skeleton.functions {
let thunkDecls = try renderSwiftThunk(function, topLevelDecls: &decls)
decls.append(contentsOf: thunkDecls)
Expand All @@ -54,6 +58,24 @@ public struct ImportTS {
return decls.map { $0.formatted(using: format).description }.joined(separator: "\n\n")
}

func renderSwiftGlobalGetter(
_ getter: ImportedGetterSkeleton,
topLevelDecls: inout [DeclSyntax]
) throws -> [DeclSyntax] {
let builder = CallJSEmission(moduleName: moduleName, abiName: getter.abiName(context: nil))
try builder.call(returnType: getter.type)
try builder.liftReturnValue(returnType: getter.type)
topLevelDecls.append(builder.renderImportDecl())
return [
builder.renderThunkDecl(
name: "_$\(getter.name)_get",
parameters: [],
returnType: getter.type
)
.with(\.leadingTrivia, Self.renderDocumentation(documentation: getter.documentation))
]
}

class CallJSEmission {
let abiName: String
let moduleName: String
Expand Down
44 changes: 44 additions & 0 deletions Plugins/BridgeJS/Sources/BridgeJSLink/BridgeJSLink.swift
Original file line number Diff line number Diff line change
Expand Up @@ -172,6 +172,9 @@ struct BridgeJSLink {
guard let imported = unified.imported else { continue }
let importObjectBuilder = ImportObjectBuilder(moduleName: unified.moduleName)
for fileSkeleton in imported.children {
for getter in fileSkeleton.globalGetters {
try renderImportedGlobalGetter(importObjectBuilder: importObjectBuilder, getter: getter)
}
for function in fileSkeleton.functions {
try renderImportedFunction(importObjectBuilder: importObjectBuilder, function: function)
}
Expand Down Expand Up @@ -2142,6 +2145,31 @@ extension BridgeJSLink {
body.write("\(call);")
}

func getImportProperty(name: String, returnType: BridgeType) throws -> String? {
if returnType == .void {
throw BridgeJSLinkError(message: "Void is not supported for imported JS properties")
}

let loweringFragment = try IntrinsicJSFragment.lowerReturn(type: returnType, context: context)
let expr = "imports[\"\(name)\"]"

let returnExpr: String?
if loweringFragment.parameters.count == 0 {
body.write("\(expr);")
returnExpr = nil
} else {
let resultVariable = scope.variable("ret")
body.write("let \(resultVariable) = \(expr);")
returnExpr = resultVariable
}

return try lowerReturnValue(
returnType: returnType,
returnExpr: returnExpr,
loweringFragment: loweringFragment
)
}

private func lowerReturnValue(
returnType: BridgeType,
returnExpr: String?,
Expand Down Expand Up @@ -2881,6 +2909,22 @@ extension BridgeJSLink {
importObjectBuilder.assignToImportObject(name: function.abiName(context: nil), function: funcLines)
}

func renderImportedGlobalGetter(
importObjectBuilder: ImportObjectBuilder,
getter: ImportedGetterSkeleton
) throws {
let thunkBuilder = ImportedThunkBuilder()
let returnExpr = try thunkBuilder.getImportProperty(name: getter.name, returnType: getter.type)
let abiName = getter.abiName(context: nil)
let funcLines = thunkBuilder.renderFunction(
name: abiName,
returnExpr: returnExpr,
returnType: getter.type
)
importObjectBuilder.appendDts(["readonly \(getter.name): \(getter.type.tsType);"])
importObjectBuilder.assignToImportObject(name: abiName, function: funcLines)
}

func renderImportedType(
importObjectBuilder: ImportObjectBuilder,
type: ImportedTypeSkeleton
Expand Down
44 changes: 41 additions & 3 deletions Plugins/BridgeJS/Sources/BridgeJSSkeleton/BridgeJSSkeleton.swift
Original file line number Diff line number Diff line change
Expand Up @@ -634,7 +634,7 @@ public struct ImportedGetterSkeleton: Codable {
self.functionName = functionName
}

public func abiName(context: ImportedTypeSkeleton) -> String {
public func abiName(context: ImportedTypeSkeleton?) -> String {
if let functionName = functionName {
return ABINameGenerator.generateImportedABIName(
baseName: functionName,
Expand Down Expand Up @@ -669,7 +669,7 @@ public struct ImportedSetterSkeleton: Codable {
self.functionName = functionName
}

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

public init(functions: [ImportedFunctionSkeleton], types: [ImportedTypeSkeleton]) {
public init(
functions: [ImportedFunctionSkeleton],
types: [ImportedTypeSkeleton],
globalGetters: [ImportedGetterSkeleton] = [],
globalSetters: [ImportedSetterSkeleton] = []
) {
self.functions = functions
self.types = types
self.globalGetters = globalGetters
self.globalSetters = globalSetters
}

private enum CodingKeys: String, CodingKey {
case functions
case types
case globalGetters
case globalSetters
}

public init(from decoder: any Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
self.functions = try container.decode([ImportedFunctionSkeleton].self, forKey: .functions)
self.types = try container.decode([ImportedTypeSkeleton].self, forKey: .types)
self.globalGetters = try container.decodeIfPresent([ImportedGetterSkeleton].self, forKey: .globalGetters) ?? []
self.globalSetters = try container.decodeIfPresent([ImportedSetterSkeleton].self, forKey: .globalSetters) ?? []
}

public func encode(to encoder: any Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(functions, forKey: .functions)
try container.encode(types, forKey: .types)
if !globalGetters.isEmpty {
try container.encode(globalGetters, forKey: .globalGetters)
}
if !globalSetters.isEmpty {
try container.encode(globalSetters, forKey: .globalSetters)
}
}
}

Expand Down
38 changes: 38 additions & 0 deletions Plugins/BridgeJS/Tests/BridgeJSToolTests/BridgeJSLinkTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -38,12 +38,21 @@ import Testing
"Inputs"
)

static let importMacroInputsDirectory = URL(fileURLWithPath: #filePath).deletingLastPathComponent()
.appendingPathComponent("ImportMacroInputs")

static func collectInputs(extension: String) -> [String] {
let fileManager = FileManager.default
let inputs = try! fileManager.contentsOfDirectory(atPath: Self.inputsDirectory.path)
return inputs.filter { $0.hasSuffix(`extension`) }
}

static func collectImportMacroInputs() -> [String] {
let fileManager = FileManager.default
let inputs = try! fileManager.contentsOfDirectory(atPath: Self.importMacroInputsDirectory.path)
return inputs.filter { $0.hasSuffix(".swift") }
}

@Test(arguments: collectInputs(extension: ".swift"))
func snapshotExport(input: String) throws {
let url = Self.inputsDirectory.appendingPathComponent(input)
Expand Down Expand Up @@ -101,6 +110,35 @@ import Testing
try snapshot(bridgeJSLink: bridgeJSLink, name: name + ".Import")
}

@Test(arguments: collectImportMacroInputs())
func snapshotImportMacroInput(input: String) throws {
let url = Self.importMacroInputsDirectory.appendingPathComponent(input)
let name = url.deletingPathExtension().lastPathComponent

let sourceFile = Parser.parse(source: try String(contentsOf: url, encoding: .utf8))
let importSwift = ImportSwiftMacros(progress: .silent, moduleName: "TestModule")
importSwift.addSourceFile(sourceFile, "\(name).swift")
let importResult = try importSwift.finalize()

var importTS = ImportTS(progress: .silent, moduleName: "TestModule")
for child in importResult.outputSkeleton.children {
importTS.addSkeleton(child)
}
let importSkeleton = importTS.skeleton

var bridgeJSLink = BridgeJSLink(sharedMemory: false)
let unifiedSkeleton = BridgeJSSkeleton(
moduleName: "TestModule",
exported: nil,
imported: importSkeleton
)
let encoder = JSONEncoder()
encoder.outputFormatting = [.prettyPrinted, .sortedKeys]
let unifiedData = try encoder.encode(unifiedSkeleton)
try bridgeJSLink.addSkeletonFile(data: unifiedData)
try snapshot(bridgeJSLink: bridgeJSLink, name: name + ".ImportMacros")
}

@Test(arguments: [
"Namespaces.swift",
"StaticFunctions.swift",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
@JSClass
struct JSConsole {
@JSFunction func log(_ message: String) throws(JSException)
}

@JSGetter var console: JSConsole
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
// 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 JSConsole {
log(message: string): void;
}
export type Exports = {
}
export type Imports = {
readonly console: JSConsole;
}
export function createInstantiator(options: {
imports: Imports;
}, swift: any): Promise<{
addImports: (importObject: WebAssembly.Imports) => void;
setInstance: (instance: WebAssembly.Instance) => void;
createExports: (instance: WebAssembly.Instance) => Exports;
}>;
Loading