diff --git a/Sources/JavaScriptKit/Documentation.docc/Articles/BridgeJS/Exporting-Swift-to-JavaScript.md b/Sources/JavaScriptKit/Documentation.docc/Articles/BridgeJS/Exporting-Swift-to-JavaScript.md index ad7932f1..7c512982 100644 --- a/Sources/JavaScriptKit/Documentation.docc/Articles/BridgeJS/Exporting-Swift-to-JavaScript.md +++ b/Sources/JavaScriptKit/Documentation.docc/Articles/BridgeJS/Exporting-Swift-to-JavaScript.md @@ -6,6 +6,8 @@ Learn how to make your Swift code callable from JavaScript. > Important: This feature is still experimental. No API stability is guaranteed, and the API may change in future releases. +> Tip: You can quickly preview what interfaces will be exposed on the Swift/TypeScript sides using the [BridgeJS Playground](https://swiftwasm.org/JavaScriptKit/PlayBridgeJS/). + BridgeJS allows you to expose Swift functions, classes, and methods to JavaScript by using the `@JS` attribute. This enables JavaScript code to call into Swift code running in WebAssembly. ## Configuring the BridgeJS plugin @@ -57,603 +59,11 @@ This command will: > Note: For larger projects, you may want to generate the BridgeJS code ahead of time to improve build performance. See for more information. -## Marking Swift Code for Export - -### Functions - -To export a Swift function to JavaScript, mark it with the `@JS` attribute and make it `public`: - -```swift -import JavaScriptKit - -@JS public func calculateTotal(price: Double, quantity: Int) -> Double { - return price * Double(quantity) -} - -@JS public func formatCurrency(amount: Double) -> String { - return "$\(String(format: "%.2f", amount))" -} -``` - -These functions will be accessible from JavaScript: - -```javascript -const total = exports.calculateTotal(19.99, 3); -const formattedTotal = exports.formatCurrency(total); -console.log(formattedTotal); // "$59.97" -``` - -The generated TypeScript declarations for these functions would look like: - -```typescript -export type Exports = { - calculateTotal(price: number, quantity: number): number; - formatCurrency(amount: number): string; -} -``` - -### Classes - -To export a Swift class, mark both the class and any members you want to expose: - -```swift -import JavaScriptKit - -@JS class ShoppingCart { - private var items: [(name: String, price: Double, quantity: Int)] = [] - - @JS init() {} - - @JS public func addItem(name: String, price: Double, quantity: Int) { - items.append((name, price, quantity)) - } - - @JS public func removeItem(atIndex index: Int) { - guard index >= 0 && index < items.count else { return } - items.remove(at: index) - } - - @JS public func getTotal() -> Double { - return items.reduce(0) { $0 + $1.price * Double($1.quantity) } - } - - @JS public func getItemCount() -> Int { - return items.count - } - - // This method won't be accessible from JavaScript (no @JS) - var debugDescription: String { - return "Cart with \(items.count) items, total: \(getTotal())" - } -} -``` - -In JavaScript: - -```javascript -import { init } from "./.build/plugins/PackageToJS/outputs/Package/index.js"; -const { exports } = await init({}); - -const cart = new exports.ShoppingCart(); -cart.addItem("Laptop", 999.99, 1); -cart.addItem("Mouse", 24.99, 2); -console.log(`Items in cart: ${cart.getItemCount()}`); -console.log(`Total: $${cart.getTotal().toFixed(2)}`); -``` - -The generated TypeScript declarations for this class would look like: - -```typescript -// Base interface for Swift reference types -export interface SwiftHeapObject { - release(): void; -} - -// ShoppingCart interface with all exported methods -export interface ShoppingCart extends SwiftHeapObject { - addItem(name: string, price: number, quantity: number): void; - removeItem(atIndex: number): void; - getTotal(): number; - getItemCount(): number; -} - -export type Exports = { - ShoppingCart: { - new(): ShoppingCart; - } -} -``` - - -### Enum Support - -BridgeJS supports two output styles for enums, controlled by the `enumStyle` parameter: - -- **`.const` (default)**: Generates const objects with union types -- **`.tsEnum`**: Generates native TypeScript enum declarations - **only available for case enums and raw value enums with String or numeric raw types** - -Examples output of both styles can be found below. - -#### Case Enums - -**Swift Definition:** - -```swift -@JS enum Direction { - case north - case south - case east - case west -} - -@JS(enumStyle: .tsEnum) enum TSDirection { - case north - case south - case east - case west -} - -@JS enum Status { - case loading - case success - case error -} -``` - -**Generated TypeScript Declaration:** - -```typescript -// Const object style (default) -const Direction: { - readonly North: 0; - readonly South: 1; - readonly East: 2; - readonly West: 3; -}; -type Direction = typeof Direction[keyof typeof Direction]; - -// Native TypeScript enum style -enum TSDirection { - North = 0, - South = 1, - East = 2, - West = 3, -} - -const Status: { - readonly Loading: 0; - readonly Success: 1; - readonly Error: 2; -}; -type Status = typeof Status[keyof typeof Status]; -``` - -**Usage in TypeScript:** - -```typescript -const direction: Direction = exports.Direction.North; -const tsDirection: TSDirection = exports.TSDirection.North; -const status: Status = exports.Status.Loading; - -exports.setDirection(exports.Direction.South); -exports.setTSDirection(exports.TSDirection.East); -const currentDirection: Direction = exports.getDirection(); -const currentTSDirection: TSDirection = exports.getTSDirection(); - -const result: Status = exports.processDirection(exports.Direction.East); - -function handleDirection(direction: Direction) { - switch (direction) { - case exports.Direction.North: - console.log("Going north"); - break; - case exports.Direction.South: - console.log("Going south"); - break; - // TypeScript will warn about missing cases - } -} -``` - -BridgeJS also generates convenience initializers and computed properties for each case-style enum, allowing the rest of the Swift glue code to remain minimal and consistent. This avoids repetitive switch statements in every function that passes enum values between JavaScript and Swift. - -```swift -extension Direction { - init?(bridgeJSRawValue: Int32) { - switch bridgeJSRawValue { - case 0: - self = .north - case 1: - self = .south - case 2: - self = .east - case 3: - self = .west - default: - return nil - } - } - - var bridgeJSRawValue: Int32 { - switch self { - case .north: - return 0 - case .south: - return 1 - case .east: - return 2 - case .west: - return 3 - } - } -} -... -@_expose(wasm, "bjs_setDirection") -@_cdecl("bjs_setDirection") -public func _bjs_setDirection(direction: Int32) -> Void { - #if arch(wasm32) - setDirection(_: Direction(bridgeJSRawValue: direction)!) - #else - fatalError("Only available on WebAssembly") - #endif -} - -@_expose(wasm, "bjs_getDirection") -@_cdecl("bjs_getDirection") -public func _bjs_getDirection() -> Int32 { - #if arch(wasm32) - let ret = getDirection() - return ret.bridgeJSRawValue - #else - fatalError("Only available on WebAssembly") - #endif -} -``` - -#### Raw Value Enums - -##### String Raw Values - -**Swift Definition:** - -```swift -// Default const object style -@JS enum Theme: String { - case light = "light" - case dark = "dark" - case auto = "auto" -} - -// Native TypeScript enum style -@JS(enumStyle: .tsEnum) enum TSTheme: String { - case light = "light" - case dark = "dark" - case auto = "auto" -} -``` - -**Generated TypeScript Declaration:** - -```typescript -// Const object style (default) -const Theme: { - readonly Light: "light"; - readonly Dark: "dark"; - readonly Auto: "auto"; -}; -type Theme = typeof Theme[keyof typeof Theme]; - -// Native TypeScript enum style -enum TSTheme { - Light = "light", - Dark = "dark", - Auto = "auto", -} -``` - -**Usage in TypeScript:** - -```typescript -// Both styles work similarly in usage -const theme: Theme = exports.Theme.Dark; -const tsTheme: TSTheme = exports.TSTheme.Dark; -exports.setTheme(exports.Theme.Light); -const currentTheme: Theme = exports.getTheme(); -const status: HttpStatus = exports.processTheme(exports.Theme.Auto); -``` - -##### Integer Raw Values - -**Swift Definition:** - -```swift -// Default const object style -@JS enum HttpStatus: Int { - case ok = 200 - case notFound = 404 - case serverError = 500 -} - -// Native TypeScript enum style -@JS(enumStyle: .tsEnum) enum TSHttpStatus: Int { - case ok = 200 - case notFound = 404 - case serverError = 500 -} - -@JS enum Priority: Int32 { - case lowest = 1 - case low = 2 - case medium = 3 - case high = 4 - case highest = 5 -} -``` - -**Generated TypeScript Declaration:** - -```typescript -// Const object style (default) -const HttpStatus: { - readonly Ok: 200; - readonly NotFound: 404; - readonly ServerError: 500; -}; -type HttpStatus = typeof HttpStatus[keyof typeof HttpStatus]; - -// Native TypeScript enum style -enum TSHttpStatus { - Ok = 200, - NotFound = 404, - ServerError = 500, -} - -const Priority: { - readonly Lowest: 1; - readonly Low: 2; - readonly Medium: 3; - readonly High: 4; - readonly Highest: 5; -}; -type Priority = typeof Priority[keyof typeof Priority]; -``` - -**Usage in TypeScript:** - -```typescript -const status: HttpStatus = exports.HttpStatus.Ok; -const tsStatus: TSHttpStatus = exports.TSHttpStatus.Ok; -const priority: Priority = exports.Priority.High; - -exports.setHttpStatus(exports.HttpStatus.NotFound); -exports.setPriority(exports.Priority.Medium); - -const convertedPriority: Priority = exports.convertPriority(exports.HttpStatus.Ok); -``` - -### Namespace Enums - -Namespace enums are empty enums (containing no cases) used for organizing related types and functions into hierarchical namespaces. - -**Swift Definition:** - -```swift -@JS enum Utils { - @JS class Converter { - @JS init() {} - - @JS func toString(value: Int) -> String { - return String(value) - } - } -} - -// Nested namespace enums with no @JS(namespace:) macro used -@JS enum Networking { - @JS enum API { - @JS enum Method { - case get - case post - } - - @JS class HTTPServer { - @JS init() {} - @JS func call(_ method: Method) - } - } -} - -// Top level enum can still use explicit namespace via @JS(namespace:) -@JS(namespace: "Networking.APIV2") -enum Internal { - @JS enum SupportedMethod { - case get - case post - } - - @JS class TestServer { - @JS init() {} - @JS func call(_ method: SupportedMethod) - } -} -``` - -**Generated TypeScript Declaration:** - -```typescript -declare global { - namespace Utils { - class Converter { - constructor(); - toString(value: number): string; - } - } - namespace Networking { - namespace API { - class HTTPServer { - constructor(); - call(method: Networking.API.Method): void; - } - const Method: { - readonly Get: 0; - readonly Post: 1; - }; - type Method = typeof Method[keyof typeof Method]; - } - namespace APIV2 { - namespace Internal { - class TestServer { - constructor(); - call(method: Internal.SupportedMethod): void; - } - const SupportedMethod: { - readonly Get: 0; - readonly Post: 1; - }; - type SupportedMethod = typeof SupportedMethod[keyof typeof SupportedMethod]; - } - } - } -} -``` - -**Usage in TypeScript:** - -```typescript -// Access nested classes through namespaces -const converter = new globalThis.Utils.Converter(); -const result: string = converter.toString(42) - -const server = new globalThis.Networking.API.HTTPServer(); -const method: Networking.API.Method = globalThis.Networking.API.Method.Get; -server.call(method) - -const testServer = new globalThis.Networking.APIV2.Internal.TestServer(); -const supportedMethod: Internal.SupportedMethod = globalThis.Networking.APIV2.Internal.SupportedMethod.Post; -testServer.call(supportedMethod); -``` - -Things to remember when using enums for namespacing: - -1. Only enums with no cases will be used for namespaces -2. Top-level enums can use `@JS(namespace: "Custom.Path")` to place themselves in custom namespaces, which will be used as "base namespace" for all nested elements as well -3. Classes and enums nested within namespace enums **cannot** use `@JS(namespace:)` - this would create conflicting namespace declarations - -**Invalid Usage:** - -```swift -@JS enum Utils { - // Invalid - nested items cannot specify their own namespace - @JS(namespace: "Custom") class Helper { - @JS init() {} - } -} -``` - -**Valid Usage:** - -```swift -// Valid - top-level enum with explicit namespace -@JS(namespace: "Custom.Utils") -enum Helper { - @JS class Converter { - @JS init() {} - } -} -``` - -#### Associated Value Enums - -Associated value enums are not currently supported, but are planned for future releases. - -## Using Namespaces - -The `@JS` macro supports organizing your exported Swift code into namespaces using dot-separated strings. This allows you to create hierarchical structures in JavaScript that mirror your Swift code organization. - -### Functions with Namespaces - -You can export functions to specific namespaces by providing a namespace parameter: - -```swift -import JavaScriptKit - -// Export a function to a custom namespace -@JS(namespace: "MyModule.Utils") func namespacedFunction() -> String { - return "namespaced" -} -``` - -This function will be accessible in JavaScript through its namespace hierarchy: - -```javascript -// Access the function through its namespace -const result = globalThis.MyModule.Utils.namespacedFunction(); -console.log(result); // "namespaced" -``` - -The generated TypeScript declaration will reflect the namespace structure: - -```typescript -declare global { - namespace MyModule { - namespace Utils { - function namespacedFunction(): string; - } - } -} -``` - -### Classes with Namespaces - -For classes, you only need to specify the namespace on the top-level class declaration. All exported methods within the class will be part of that namespace: - -```swift -import JavaScriptKit - -@JS(namespace: "__Swift.Foundation") class Greeter { - var name: String - - @JS init(name: String) { - self.name = name - } - - @JS func greet() -> String { - return "Hello, " + self.name + "!" - } - - func changeName(name: String) { - self.name = name - } -} -``` - -In JavaScript, this class is accessible through its namespace: - -```javascript -// Create instances through namespaced constructors -const greeter = new globalThis.__Swift.Foundation.Greeter("World"); -console.log(greeter.greet()); // "Hello, World!" -``` - -The generated TypeScript declaration will organize the class within its namespace: - -```typescript -declare global { - namespace __Swift { - namespace Foundation { - class Greeter { - constructor(name: string); - greet(): string; - } - } - } -} - -export interface Greeter extends SwiftHeapObject { - greet(): string; -} -``` +## Topics -Using namespaces can be preferable for projects with many global functions, as they help prevent naming collisions. Namespaces also provide intuitive hierarchies for organizing your exported Swift code, and they do not affect the code generated by `@JS` declarations without namespaces. +- +- +- +- diff --git a/Sources/JavaScriptKit/Documentation.docc/Articles/BridgeJS/Exporting-Swift/Exporting-Swift-Class.md b/Sources/JavaScriptKit/Documentation.docc/Articles/BridgeJS/Exporting-Swift/Exporting-Swift-Class.md new file mode 100644 index 00000000..d91d0616 --- /dev/null +++ b/Sources/JavaScriptKit/Documentation.docc/Articles/BridgeJS/Exporting-Swift/Exporting-Swift-Class.md @@ -0,0 +1,94 @@ +# Exporting Swift Classes to JS + +Learn how to export Swift classes to JavaScript. + +## Overview + +> Tip: You can quickly preview what interfaces will be exposed on the Swift/TypeScript sides using the [BridgeJS Playground](https://swiftwasm.org/JavaScriptKit/PlayBridgeJS/). + +To export a Swift class, mark both the class and any members you want to expose: + +```swift +import JavaScriptKit + +@JS class ShoppingCart { + private var items: [(name: String, price: Double, quantity: Int)] = [] + + @JS init() {} + + @JS public func addItem(name: String, price: Double, quantity: Int) { + items.append((name, price, quantity)) + } + + @JS public func removeItem(atIndex index: Int) { + guard index >= 0 && index < items.count else { return } + items.remove(at: index) + } + + @JS public func getTotal() -> Double { + return items.reduce(0) { $0 + $1.price * Double($1.quantity) } + } + + @JS public func getItemCount() -> Int { + return items.count + } + + // This method won't be accessible from JavaScript (no @JS) + var debugDescription: String { + return "Cart with \(items.count) items, total: \(getTotal())" + } +} +``` + +In JavaScript: + +```javascript +import { init } from "./.build/plugins/PackageToJS/outputs/Package/index.js"; +const { exports } = await init({}); + +const cart = new exports.ShoppingCart(); +cart.addItem("Laptop", 999.99, 1); +cart.addItem("Mouse", 24.99, 2); +console.log(`Items in cart: ${cart.getItemCount()}`); +console.log(`Total: $${cart.getTotal().toFixed(2)}`); +``` + +The generated TypeScript declarations for this class would look like: + +```typescript +// Base interface for Swift reference types +export interface SwiftHeapObject { + release(): void; +} + +// ShoppingCart interface with all exported methods +export interface ShoppingCart extends SwiftHeapObject { + addItem(name: string, price: number, quantity: number): void; + removeItem(atIndex: number): void; + getTotal(): number; + getItemCount(): number; +} + +export type Exports = { + ShoppingCart: { + new(): ShoppingCart; + } +} +``` + +## Supported Features + +| Swift Feature | Status | +|:--------------|:-------| +| Initializers: `init()` | ✅ | +| Initializers that throw JSException: `init() throws(JSException)` | ✅ | +| Initializers that throw any exception: `init() throws` | ❌ | +| Async initializers: `init() async` | ❌ | +| Deinitializers: `deinit` | ✅ | +| Stored properties: `var`, `let` (with `willSet`, `didSet`) | ✅ | +| Computed properties: `var x: X { get set }` | ✅ | +| Computed properties with effects: `var x: X { get async throws }` | 🚧 | +| Methods: `func` | ✅ (See ) | +| Static/class methods: `static func`, `class func` | 🚧 | +| Subscripts: `subscript()` | ❌ | +| Generics | ❌ | diff --git a/Sources/JavaScriptKit/Documentation.docc/Articles/BridgeJS/Exporting-Swift/Exporting-Swift-Enum.md b/Sources/JavaScriptKit/Documentation.docc/Articles/BridgeJS/Exporting-Swift/Exporting-Swift-Enum.md new file mode 100644 index 00000000..e4bc1d7f --- /dev/null +++ b/Sources/JavaScriptKit/Documentation.docc/Articles/BridgeJS/Exporting-Swift/Exporting-Swift-Enum.md @@ -0,0 +1,408 @@ +# Exporting Swift Enums to JS + +Learn how to export Swift enums to JavaScript. + +## Overview + +> Tip: You can quickly preview what interfaces will be exposed on the Swift/TypeScript sides using the [BridgeJS Playground](https://swiftwasm.org/JavaScriptKit/PlayBridgeJS/). + +BridgeJS supports two output styles for enums, controlled by the `enumStyle` parameter: + +- **`.const` (default)**: Generates const objects with union types +- **`.tsEnum`**: Generates native TypeScript enum declarations - **only available for case enums and raw value enums with String or numeric raw types** + +Examples output of both styles can be found below. + +#### Case Enums + +**Swift Definition:** + +```swift +@JS enum Direction { + case north + case south + case east + case west +} + +@JS(enumStyle: .tsEnum) enum TSDirection { + case north + case south + case east + case west +} + +@JS enum Status { + case loading + case success + case error +} +``` + +**Generated TypeScript Declaration:** + +```typescript +// Const object style (default) +const Direction: { + readonly North: 0; + readonly South: 1; + readonly East: 2; + readonly West: 3; +}; +type Direction = typeof Direction[keyof typeof Direction]; + +// Native TypeScript enum style +enum TSDirection { + North = 0, + South = 1, + East = 2, + West = 3, +} + +const Status: { + readonly Loading: 0; + readonly Success: 1; + readonly Error: 2; +}; +type Status = typeof Status[keyof typeof Status]; +``` + +**Usage in TypeScript:** + +```typescript +const direction: Direction = exports.Direction.North; +const tsDirection: TSDirection = exports.TSDirection.North; +const status: Status = exports.Status.Loading; + +exports.setDirection(exports.Direction.South); +exports.setTSDirection(exports.TSDirection.East); +const currentDirection: Direction = exports.getDirection(); +const currentTSDirection: TSDirection = exports.getTSDirection(); + +const result: Status = exports.processDirection(exports.Direction.East); + +function handleDirection(direction: Direction) { + switch (direction) { + case exports.Direction.North: + console.log("Going north"); + break; + case exports.Direction.South: + console.log("Going south"); + break; + // TypeScript will warn about missing cases + } +} +``` + +BridgeJS also generates convenience initializers and computed properties for each case-style enum, allowing the rest of the Swift glue code to remain minimal and consistent. This avoids repetitive switch statements in every function that passes enum values between JavaScript and Swift. + +```swift +extension Direction { + init?(bridgeJSRawValue: Int32) { + switch bridgeJSRawValue { + case 0: + self = .north + case 1: + self = .south + case 2: + self = .east + case 3: + self = .west + default: + return nil + } + } + + var bridgeJSRawValue: Int32 { + switch self { + case .north: + return 0 + case .south: + return 1 + case .east: + return 2 + case .west: + return 3 + } + } +} +... +@_expose(wasm, "bjs_setDirection") +@_cdecl("bjs_setDirection") +public func _bjs_setDirection(direction: Int32) -> Void { + #if arch(wasm32) + setDirection(_: Direction(bridgeJSRawValue: direction)!) + #else + fatalError("Only available on WebAssembly") + #endif +} + +@_expose(wasm, "bjs_getDirection") +@_cdecl("bjs_getDirection") +public func _bjs_getDirection() -> Int32 { + #if arch(wasm32) + let ret = getDirection() + return ret.bridgeJSRawValue + #else + fatalError("Only available on WebAssembly") + #endif +} +``` + +#### Raw Value Enums + +##### String Raw Values + +**Swift Definition:** + +```swift +// Default const object style +@JS enum Theme: String { + case light = "light" + case dark = "dark" + case auto = "auto" +} + +// Native TypeScript enum style +@JS(enumStyle: .tsEnum) enum TSTheme: String { + case light = "light" + case dark = "dark" + case auto = "auto" +} +``` + +**Generated TypeScript Declaration:** + +```typescript +// Const object style (default) +const Theme: { + readonly Light: "light"; + readonly Dark: "dark"; + readonly Auto: "auto"; +}; +type Theme = typeof Theme[keyof typeof Theme]; + +// Native TypeScript enum style +enum TSTheme { + Light = "light", + Dark = "dark", + Auto = "auto", +} +``` + +**Usage in TypeScript:** + +```typescript +// Both styles work similarly in usage +const theme: Theme = exports.Theme.Dark; +const tsTheme: TSTheme = exports.TSTheme.Dark; + +exports.setTheme(exports.Theme.Light); +const currentTheme: Theme = exports.getTheme(); + +const status: HttpStatus = exports.processTheme(exports.Theme.Auto); +``` + +##### Integer Raw Values + +**Swift Definition:** + +```swift +// Default const object style +@JS enum HttpStatus: Int { + case ok = 200 + case notFound = 404 + case serverError = 500 +} + +// Native TypeScript enum style +@JS(enumStyle: .tsEnum) enum TSHttpStatus: Int { + case ok = 200 + case notFound = 404 + case serverError = 500 +} + +@JS enum Priority: Int32 { + case lowest = 1 + case low = 2 + case medium = 3 + case high = 4 + case highest = 5 +} +``` + +**Generated TypeScript Declaration:** + +```typescript +// Const object style (default) +const HttpStatus: { + readonly Ok: 200; + readonly NotFound: 404; + readonly ServerError: 500; +}; +type HttpStatus = typeof HttpStatus[keyof typeof HttpStatus]; + +// Native TypeScript enum style +enum TSHttpStatus { + Ok = 200, + NotFound = 404, + ServerError = 500, +} + +const Priority: { + readonly Lowest: 1; + readonly Low: 2; + readonly Medium: 3; + readonly High: 4; + readonly Highest: 5; +}; +type Priority = typeof Priority[keyof typeof Priority]; +``` + +**Usage in TypeScript:** + +```typescript +const status: HttpStatus = exports.HttpStatus.Ok; +const tsStatus: TSHttpStatus = exports.TSHttpStatus.Ok; +const priority: Priority = exports.Priority.High; + +exports.setHttpStatus(exports.HttpStatus.NotFound); +exports.setPriority(exports.Priority.Medium); + +const convertedPriority: Priority = exports.convertPriority(exports.HttpStatus.Ok); +``` + +### Namespace Enums + +Namespace enums are empty enums (containing no cases) used for organizing related types and functions into hierarchical namespaces. + +**Swift Definition:** + +```swift +@JS enum Utils { + @JS class Converter { + @JS init() {} + + @JS func toString(value: Int) -> String { + return String(value) + } + } +} + +// Nested namespace enums with no @JS(namespace:) macro used +@JS enum Networking { + @JS enum API { + @JS enum Method { + case get + case post + } + + @JS class HTTPServer { + @JS init() {} + @JS func call(_ method: Method) + } + } +} + +// Top level enum can still use explicit namespace via @JS(namespace:) +@JS(namespace: "Networking.APIV2") +enum Internal { + @JS enum SupportedMethod { + case get + case post + } + + @JS class TestServer { + @JS init() {} + @JS func call(_ method: SupportedMethod) + } +} +``` + +**Generated TypeScript Declaration:** + +```typescript +declare global { + namespace Utils { + class Converter { + constructor(); + toString(value: number): string; + } + } + namespace Networking { + namespace API { + class HTTPServer { + constructor(); + call(method: Networking.API.Method): void; + } + const Method: { + readonly Get: 0; + readonly Post: 1; + }; + type Method = typeof Method[keyof typeof Method]; + } + namespace APIV2 { + namespace Internal { + class TestServer { + constructor(); + call(method: Internal.SupportedMethod): void; + } + const SupportedMethod: { + readonly Get: 0; + readonly Post: 1; + }; + type SupportedMethod = typeof SupportedMethod[keyof typeof SupportedMethod]; + } + } + } +} +``` + +**Usage in TypeScript:** + +```typescript +// Access nested classes through namespaces +const converter = new globalThis.Utils.Converter(); +const result: string = converter.toString(42) + +const server = new globalThis.Networking.API.HTTPServer(); +const method: Networking.API.Method = globalThis.Networking.API.Method.Get; +server.call(method) + +const testServer = new globalThis.Networking.APIV2.Internal.TestServer(); +const supportedMethod: Internal.SupportedMethod = globalThis.Networking.APIV2.Internal.SupportedMethod.Post; +testServer.call(supportedMethod); +``` + +Things to remember when using enums for namespacing: + +1. Only enums with no cases will be used for namespaces +2. Top-level enums can use `@JS(namespace: "Custom.Path")` to place themselves in custom namespaces, which will be used as "base namespace" for all nested elements as well +3. Classes and enums nested within namespace enums **cannot** use `@JS(namespace:)` - this would create conflicting namespace declarations + +**Invalid Usage:** + +```swift +@JS enum Utils { + // Invalid - nested items cannot specify their own namespace + @JS(namespace: "Custom") class Helper { + @JS init() {} + } +} +``` + +**Valid Usage:** + +```swift +// Valid - top-level enum with explicit namespace +@JS(namespace: "Custom.Utils") +enum Helper { + @JS class Converter { + @JS init() {} + } +} +``` + +#### Associated Value Enums + +Associated value enums are not currently supported, but are planned for future releases. diff --git a/Sources/JavaScriptKit/Documentation.docc/Articles/BridgeJS/Exporting-Swift/Exporting-Swift-Function.md b/Sources/JavaScriptKit/Documentation.docc/Articles/BridgeJS/Exporting-Swift/Exporting-Swift-Function.md new file mode 100644 index 00000000..fff45f11 --- /dev/null +++ b/Sources/JavaScriptKit/Documentation.docc/Articles/BridgeJS/Exporting-Swift/Exporting-Swift-Function.md @@ -0,0 +1,153 @@ +# Exporting Swift Functions to JS + +Learn how to export Swift functions to JavaScript. + +## Overview + +> Tip: You can quickly preview what interfaces will be exposed on the Swift/TypeScript sides using the [BridgeJS Playground](https://swiftwasm.org/JavaScriptKit/PlayBridgeJS/). + +To export a Swift function to JavaScript, mark it with the `@JS` attribute and make it `public`: + +```swift +import JavaScriptKit + +@JS public func calculateTotal(price: Double, quantity: Int) -> Double { + return price * Double(quantity) +} + +@JS public func formatCurrency(amount: Double) -> String { + return "$\(String(format: "%.2f", amount))" +} +``` + +These functions will be accessible from JavaScript: + +```javascript +const total = exports.calculateTotal(19.99, 3); +const formattedTotal = exports.formatCurrency(total); +console.log(formattedTotal); // "$59.97" +``` + +The generated TypeScript declarations for these functions would look like: + +```typescript +export type Exports = { + calculateTotal(price: number, quantity: number): number; + formatCurrency(amount: number): string; +} +``` + +### Throwing functions + +Swift functions can throw JavaScript errors using `throws(JSException)`. + +```swift +import JavaScriptKit + +@JS public func findUser(id: Int) throws(JSException) -> String { + if id <= 0 { + throw JSException(JSError(message: "Invalid ID").jsValue) + } + return "User_\(id)" +} +``` + +From JavaScript, call with `try/catch`: + +```javascript +try { + const name = exports.findUser(42); + console.log(name); +} catch (e) { + console.error("findUser failed:", e); +} +``` + +Generated TypeScript type: + +```typescript +export type Exports = { + findUser(id: number): string; // throws at runtime +} +``` + +Notes: +- Only `throws(JSException)` is supported. Plain `throws` is not supported. +- Thrown values are surfaced to JS as normal JS exceptions. + +### Async functions + +Async Swift functions are exposed as Promise-returning JS functions. + +```swift +import JavaScriptKit + +@JS public func fetchCount(endpoint: String) async -> Int { + // Simulate async work + try? await Task.sleep(nanoseconds: 50_000_000) + return endpoint.count +} +``` + +Usage from JavaScript: + +```javascript +const count = await exports.fetchCount("/items"); +``` + +Generated TypeScript type: + +```typescript +export type Exports = { + fetchCount(endpoint: string): Promise; +} +``` + +### Async + throws + +Async throwing functions become Promise-returning JS functions that reject on error. + +```swift +import JavaScriptKit + +@JS public func loadProfile(userId: Int) async throws(JSException) -> String { + if userId <= 0 { throw JSException(JSError(message: "Bad userId").jsValue) } + try? await Task.sleep(nanoseconds: 50_000_000) + return "Profile_\(userId)" +} +``` + +JavaScript usage: + +```javascript +try { + const profile = await exports.loadProfile(1); + console.log(profile); +} catch (e) { + console.error("loadProfile failed:", e); +} +``` + +TypeScript: + +```typescript +export type Exports = { + loadProfile(userId: number): Promise; +} +``` + +## Supported Features + +| Swift Feature | Status | +|:--------------|:-------| +| Primitive parameter/result types: (e.g. `Bool`, `Int`, `Double`) | ✅ | +| `String` parameter/result type | ✅ | +| `@JS class` parameter/result type | ✅ | +| `@JS enum` parameter/result type | ✅ | +| `JSObject` parameter/result type | ✅ | +| Throwing JS exception: `func x() throws(JSException)` | ✅ | +| Throwing any exception: `func x() throws` | ❌ | +| Async methods: `func x() async` | ✅ | +| Generics | ❌ | +| Opaque types: `func x() -> some P`, `func y(_: some P)` | ❌ | +| Default parameter values: `func x(_ foo: String = "")` | ❌ | \ No newline at end of file diff --git a/Sources/JavaScriptKit/Documentation.docc/Articles/BridgeJS/Exporting-Swift/Using-Namespace.md b/Sources/JavaScriptKit/Documentation.docc/Articles/BridgeJS/Exporting-Swift/Using-Namespace.md new file mode 100644 index 00000000..2026c6a2 --- /dev/null +++ b/Sources/JavaScriptKit/Documentation.docc/Articles/BridgeJS/Exporting-Swift/Using-Namespace.md @@ -0,0 +1,95 @@ +# Using Namespaces + +Learn how to organize exported Swift code into JavaScript namespaces. + +## Overview + +> Tip: You can quickly preview what interfaces will be exposed on the Swift/TypeScript sides using the [BridgeJS Playground](https://swiftwasm.org/JavaScriptKit/PlayBridgeJS/). + +The `@JS` macro supports organizing your exported Swift code into namespaces using dot-separated strings. This allows you to create hierarchical structures in JavaScript that mirror your Swift code organization. + +### Functions with Namespaces + +You can export functions to specific namespaces by providing a namespace parameter: + +```swift +import JavaScriptKit + +// Export a function to a custom namespace +@JS(namespace: "MyModule.Utils") func namespacedFunction() -> String { + return "namespaced" +} +``` + +This function will be accessible in JavaScript through its namespace hierarchy: + +```javascript +// Access the function through its namespace +const result = globalThis.MyModule.Utils.namespacedFunction(); +console.log(result); // "namespaced" +``` + +The generated TypeScript declaration will reflect the namespace structure: + +```typescript +declare global { + namespace MyModule { + namespace Utils { + function namespacedFunction(): string; + } + } +} +``` + +### Classes with Namespaces + +For classes, you only need to specify the namespace on the top-level class declaration. All exported methods within the class will be part of that namespace: + +```swift +import JavaScriptKit + +@JS(namespace: "__Swift.Foundation") class Greeter { + var name: String + + @JS init(name: String) { + self.name = name + } + + @JS func greet() -> String { + return "Hello, " + self.name + "!" + } + + func changeName(name: String) { + self.name = name + } +} +``` + +In JavaScript, this class is accessible through its namespace: + +```javascript +// Create instances through namespaced constructors +const greeter = new globalThis.__Swift.Foundation.Greeter("World"); +console.log(greeter.greet()); // "Hello, World!" +``` + +The generated TypeScript declaration will organize the class within its namespace: + +```typescript +declare global { + namespace __Swift { + namespace Foundation { + class Greeter { + constructor(name: string); + greet(): string; + } + } + } +} + +export interface Greeter extends SwiftHeapObject { + greet(): string; +} +``` + +Using namespaces can be preferable for projects with many global functions, as they help prevent naming collisions. Namespaces also provide intuitive hierarchies for organizing your exported Swift code, and they do not affect the code generated by `@JS` declarations without namespaces. \ No newline at end of file diff --git a/Sources/JavaScriptKit/Documentation.docc/Articles/BridgeJS/Importing-TypeScript-into-Swift.md b/Sources/JavaScriptKit/Documentation.docc/Articles/BridgeJS/Importing-TypeScript-into-Swift.md index 853c8800..b091f714 100644 --- a/Sources/JavaScriptKit/Documentation.docc/Articles/BridgeJS/Importing-TypeScript-into-Swift.md +++ b/Sources/JavaScriptKit/Documentation.docc/Articles/BridgeJS/Importing-TypeScript-into-Swift.md @@ -6,6 +6,8 @@ Learn how to leverage TypeScript definitions to create type-safe bindings for Ja > Important: This feature is still experimental. No API stability is guaranteed, and the API may change in future releases. +> Tip: You can quickly preview what interfaces will be exposed on the Swift/TypeScript sides using the [BridgeJS Playground](https://swiftwasm.org/JavaScriptKit/PlayBridgeJS/). + BridgeJS enables seamless integration between Swift and JavaScript by automatically generating Swift bindings from TypeScript declaration files (`.d.ts`). This provides type-safe access to JavaScript APIs directly from your Swift code. The key benefits of this approach over `@dynamicMemberLookup`-based APIs include: @@ -174,3 +176,10 @@ const { exports } = await init({ // Call the entry point of your Swift application exports.run(); ``` + +## Topics + +- +- +- +- diff --git a/Sources/JavaScriptKit/Documentation.docc/Articles/BridgeJS/Importing-TypeScript/Importing-TS-Class.md b/Sources/JavaScriptKit/Documentation.docc/Articles/BridgeJS/Importing-TypeScript/Importing-TS-Class.md new file mode 100644 index 00000000..e04bb9e7 --- /dev/null +++ b/Sources/JavaScriptKit/Documentation.docc/Articles/BridgeJS/Importing-TypeScript/Importing-TS-Class.md @@ -0,0 +1,64 @@ +# Importing TypeScript Classes into Swift + +Learn how TypeScript classes map to Swift when importing APIs. + +## Overview + +> Tip: You can quickly preview what interfaces will be exposed on the Swift/TypeScript sides using the [BridgeJS Playground](https://swiftwasm.org/JavaScriptKit/PlayBridgeJS/). + +BridgeJS reads class declarations in your `bridge-js.d.ts` and generates Swift structs that represent JS objects. Constructors, methods, and properties are bridged via thunks that call into your JavaScript implementations at runtime. + +## Example + +TypeScript definition (`bridge-js.d.ts`): + +```typescript +export class Greeter { + readonly id: string; + message: string; + constructor(id: string, name: string); + greet(): string; +} +``` + +Generated Swift API: + +```swift +struct Greeter { + init(id: String, name: String) throws(JSException) + + // Properties + // Readonly property + var id: String { get throws(JSException) } + // Writable property + var message: String { get throws(JSException) } + func setMessage(_ newValue: String) throws(JSException) + + // Methods + func greet() throws(JSException) -> String +} +``` + +Notes: +- Property setters are emitted as `set(_:)` functions, not Swift `set` accessors since `set` accessors can't have `throws` +- All thunks throw `JSException` if the underlying JS throws. + +JavaScript implementation wiring (provided by your app): + +```javascript +// index.js +import { init } from "./.build/plugins/PackageToJS/outputs/Package/index.js"; + +class Greeter { + readonly id: string; + message: string; + constructor(id: string, name: string) { ... } + greet(): string { ... } +} + +const { exports } = await init({ + getImports() { + return { Greeter }; + } +}); +``` diff --git a/Sources/JavaScriptKit/Documentation.docc/Articles/BridgeJS/Importing-TypeScript/Importing-TS-Function.md b/Sources/JavaScriptKit/Documentation.docc/Articles/BridgeJS/Importing-TypeScript/Importing-TS-Function.md new file mode 100644 index 00000000..060ac390 --- /dev/null +++ b/Sources/JavaScriptKit/Documentation.docc/Articles/BridgeJS/Importing-TypeScript/Importing-TS-Function.md @@ -0,0 +1,60 @@ +# Importing TypeScript Functions into Swift + +Learn how functions declared in TypeScript become callable Swift functions. + +## Overview + +> Tip: You can quickly preview what interfaces will be exposed on the Swift/TypeScript sides using the [BridgeJS Playground](https://swiftwasm.org/JavaScriptKit/PlayBridgeJS/). + +BridgeJS reads your `bridge-js.d.ts` and generates Swift thunks that call into JavaScript implementations provided at runtime. Each imported function becomes a top-level Swift function with the same name and a throwing signature. + +### Example + +TypeScript definition (`bridge-js.d.ts`): + +```typescript +export function add(a: number, b: number): number; +export function setTitle(title: string): void; +export function fetchUser(id: string): Promise; +``` + +Generated Swift signatures: + +```swift +func add(a: Double, b: Double) throws(JSException) -> Double +func setTitle(title: String) throws(JSException) +func fetchUser(id: String) throws(JSException) -> JSPromise +``` + +JavaScript implementation wiring (provided by your app): + +```javascript +// index.js +import { init } from "./.build/plugins/PackageToJS/outputs/Package/index.js"; + +const { exports } = await init({ + getImports() { + return { + add: (a, b) => a + b, + setTitle: (title) => { document.title = title }, + fetchUser: (id) => fetch(`/api/users/${id}`).then(r => r.json()), + }; + } +}); +``` + +### Error handling + +- All imported Swift functions are generated as `throws(JSException)` and will throw if the underlying JS implementation throws. + +## Supported features + +| Feature | Status | +|:--|:--| +| Primitive parameter/result types: (e.g. `boolean`, `number`) | ✅ | +| `string` parameter/result type | ✅ | +| Enums in signatures | ❌ | +| Async function | ✅ | +| Generics | ❌ | + + diff --git a/Sources/JavaScriptKit/Documentation.docc/Articles/BridgeJS/Importing-TypeScript/Importing-TS-Interface.md b/Sources/JavaScriptKit/Documentation.docc/Articles/BridgeJS/Importing-TypeScript/Importing-TS-Interface.md new file mode 100644 index 00000000..0e0aed0d --- /dev/null +++ b/Sources/JavaScriptKit/Documentation.docc/Articles/BridgeJS/Importing-TypeScript/Importing-TS-Interface.md @@ -0,0 +1,34 @@ +# Importing TypeScript Interfaces into Swift + +Learn how TypeScript interfaces become Swift value types with methods and properties. + +## Overview + +> Tip: You can quickly preview what interfaces will be exposed on the Swift/TypeScript sides using the [BridgeJS Playground](https://swiftwasm.org/JavaScriptKit/PlayBridgeJS/). + +BridgeJS converts TS interfaces to Swift structs conforming to an internal bridging protocol and provides thunks for methods and properties that call into your JavaScript implementations. + +> Note: Interfaces are bridged very similarly to classes. Methods and properties map the same way. See for more details. + +### Example + +TypeScript definition (`bridge-js.d.ts`): + +```typescript +export interface HTMLElement { + readonly innerText: string; + className: string; + appendChild(child: HTMLElement): void; +} +``` + +Generated Swift API: + +```swift +struct HTMLElement { + var innerText: String { get throws(JSException) } + var className: String { get throws(JSException) } + func setClassName(_ newValue: String) throws(JSException) + func appendChild(_ child: HTMLElement) throws(JSException) +} +``` diff --git a/Sources/JavaScriptKit/Documentation.docc/Articles/BridgeJS/Importing-TypeScript/Importing-TS-TypeAlias.md b/Sources/JavaScriptKit/Documentation.docc/Articles/BridgeJS/Importing-TypeScript/Importing-TS-TypeAlias.md new file mode 100644 index 00000000..b5242ee3 --- /dev/null +++ b/Sources/JavaScriptKit/Documentation.docc/Articles/BridgeJS/Importing-TypeScript/Importing-TS-TypeAlias.md @@ -0,0 +1,47 @@ +# Importing TypeScript Type Aliases into Swift + +Understand how TypeScript type aliases are handled when generating Swift bindings. + +## Overview + +> Tip: You can quickly preview what interfaces will be exposed on the Swift/TypeScript sides using the [BridgeJS Playground](https://swiftwasm.org/JavaScriptKit/PlayBridgeJS/). + +BridgeJS resolves TypeScript aliases while importing. If an alias names an anonymous object type, Swift will generate a corresponding bridged struct using that name. + +> Note: When a type alias names an anonymous object type, its bridging behavior (constructors not applicable, but methods/properties if referenced) mirrors class/interface importing. See for more details. + +### Examples + +```typescript +// Primitive alias → maps to the underlying primitive +export type Price = number; + +// Object-shaped alias with a name → becomes a named bridged type when referenced +export type User = { + readonly id: string; + name: string; + age: Price; +} + +export function getUser(): User; +``` + +Generated Swift (simplified): + +```swift +// Price → Double + +struct User { + // Readonly property + var id: String { get throws(JSException) } + + // Writable properties + var name: String { get throws(JSException) } + func setName(_ newValue: String) throws(JSException) + + var age: Double { get throws(JSException) } + func setAge(_ newValue: Double) throws(JSException) +} + +func getUser() throws(JSException) -> User +``` diff --git a/Sources/JavaScriptKit/Documentation.docc/Articles/BridgeJS/Supported-Types.md b/Sources/JavaScriptKit/Documentation.docc/Articles/BridgeJS/Supported-Types.md new file mode 100644 index 00000000..71b92573 --- /dev/null +++ b/Sources/JavaScriptKit/Documentation.docc/Articles/BridgeJS/Supported-Types.md @@ -0,0 +1,18 @@ +# TypeScript and Swift Type Mapping + +Use this page as a quick reference for how common TypeScript types appear in Swift when importing, and how exported Swift types surface on the TypeScript side. + +## Type mapping + +| TypeScript type | Swift type | +|:--|:--| +| `number` | `Double` | +| `string` | `String` | +| `boolean` | `Bool` | +| TODO | `Array` | +| TODO | `Dictionary` | +| TODO | `Optional` | +| `T \| undefined` | TODO | +| `Promise` | `JSPromise` | +| `any` / `unknown` / `object` | `JSObject` | +| Other types | `JSObject` | diff --git a/Sources/JavaScriptKit/Documentation.docc/Documentation.md b/Sources/JavaScriptKit/Documentation.docc/Documentation.md index 0a410916..13a6a321 100644 --- a/Sources/JavaScriptKit/Documentation.docc/Documentation.md +++ b/Sources/JavaScriptKit/Documentation.docc/Documentation.md @@ -58,6 +58,7 @@ Check out the [examples](https://github.com/swiftwasm/JavaScriptKit/tree/main/Ex - - +- - - @@ -65,4 +66,4 @@ Check out the [examples](https://github.com/swiftwasm/JavaScriptKit/tree/main/Ex - ``JSValue`` - ``JSObject`` -- ``JS(namespace:)`` +- ``JS(namespace:enumStyle:)``