Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
40 changes: 40 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,27 @@ 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)
Comment thread
MaxDesiatov marked this conversation as resolved.
Outdated
}

private func lowerReturnValue(
returnType: BridgeType,
returnExpr: String?,
Expand Down Expand Up @@ -2881,6 +2905,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)
Comment thread
MaxDesiatov marked this conversation as resolved.
Outdated
}

@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
Loading