diff --git a/Benchmarks/README.md b/Benchmarks/README.md index f7264b6b7..65d867eba 100644 --- a/Benchmarks/README.md +++ b/Benchmarks/README.md @@ -7,7 +7,7 @@ This directory contains performance benchmarks for JavaScriptKit. Before running the benchmarks, you need to build the test suite: ```bash -JAVASCRIPTKIT_EXPERIMENTAL_BRIDGEJS=1 swift package --swift-sdk $SWIFT_SDK_ID js -c release +swift package --swift-sdk $SWIFT_SDK_ID js -c release ``` ## Running Benchmarks diff --git a/Benchmarks/Sources/Generated/BridgeJS.Macros.swift b/Benchmarks/Sources/Generated/BridgeJS.Macros.swift index 39a46d22c..40fc29e91 100644 --- a/Benchmarks/Sources/Generated/BridgeJS.Macros.swift +++ b/Benchmarks/Sources/Generated/BridgeJS.Macros.swift @@ -4,7 +4,7 @@ // To update this file, just rebuild your project or run // `swift package bridge-js`. -@_spi(Experimental) @_spi(BridgeJS) import JavaScriptKit +@_spi(BridgeJS) import JavaScriptKit @JSFunction func benchmarkHelperNoop() throws(JSException) -> Void diff --git a/Examples/ExportSwift/README.md b/Examples/ExportSwift/README.md index 57d419295..39c66f0a6 100644 --- a/Examples/ExportSwift/README.md +++ b/Examples/ExportSwift/README.md @@ -5,7 +5,7 @@ This example demonstrates how to export Swift functions to JavaScript. 1. Build the project: ```sh - env JAVASCRIPTKIT_EXPERIMENTAL_BRIDGEJS=1 swift package --swift-sdk $SWIFT_SDK_ID js --use-cdn + swift package --swift-sdk $SWIFT_SDK_ID js --use-cdn ``` 2. Serve the files: diff --git a/Examples/PlayBridgeJS/Sources/PlayBridgeJS/Generated/BridgeJS.Macros.swift b/Examples/PlayBridgeJS/Sources/PlayBridgeJS/Generated/BridgeJS.Macros.swift index c619a8883..4a87a6d38 100644 --- a/Examples/PlayBridgeJS/Sources/PlayBridgeJS/Generated/BridgeJS.Macros.swift +++ b/Examples/PlayBridgeJS/Sources/PlayBridgeJS/Generated/BridgeJS.Macros.swift @@ -4,7 +4,7 @@ // To update this file, just rebuild your project or run // `swift package bridge-js`. -@_spi(Experimental) @_spi(BridgeJS) import JavaScriptKit +@_spi(BridgeJS) import JavaScriptKit @JSFunction func createTS2Swift() throws(JSException) -> TS2Swift diff --git a/Examples/PlayBridgeJS/build.sh b/Examples/PlayBridgeJS/build.sh index d2589f798..31c07896c 100755 --- a/Examples/PlayBridgeJS/build.sh +++ b/Examples/PlayBridgeJS/build.sh @@ -1,5 +1,5 @@ #!/bin/bash set -euxo pipefail -env JAVASCRIPTKIT_EXPERIMENTAL_BRIDGEJS=1 swift package --swift-sdk "${SWIFT_SDK_ID_wasm32_unknown_wasip1:-${SWIFT_SDK_ID:-wasm32-unknown-wasip1}}" \ +swift package --swift-sdk "${SWIFT_SDK_ID_wasm32_unknown_wasip1:-${SWIFT_SDK_ID:-wasm32-unknown-wasip1}}" \ plugin --allow-writing-to-package-directory \ js --use-cdn --output ./Bundle -c "${1:-debug}" diff --git a/Makefile b/Makefile index 135465a73..270eb9b36 100644 --- a/Makefile +++ b/Makefile @@ -16,7 +16,7 @@ unittest: echo "SWIFT_SDK_ID is not set. Run 'swift sdk list' and pass a matching SDK, e.g. 'make unittest SWIFT_SDK_ID='."; \ exit 2; \ } - env JAVASCRIPTKIT_EXPERIMENTAL_BRIDGEJS=1 swift package --swift-sdk "$(SWIFT_SDK_ID)" \ + swift package --swift-sdk "$(SWIFT_SDK_ID)" \ $(TRACING_ARGS) \ --disable-sandbox \ js test --prelude ./Tests/prelude.mjs -Xnode --expose-gc diff --git a/Plugins/BridgeJS/README.md b/Plugins/BridgeJS/README.md index 0559dc3fb..0b6ad121e 100644 --- a/Plugins/BridgeJS/README.md +++ b/Plugins/BridgeJS/README.md @@ -1,17 +1,17 @@ # BridgeJS > [!IMPORTANT] -> This feature is still experimental, and the API may change frequently. Use at your own risk with `JAVASCRIPTKIT_EXPERIMENTAL_BRIDGEJS=1` environment variable. +> This feature is still experimental, and the API may change frequently. Use at your own risk. > [!NOTE] -> This documentation is intended for JavaScriptKit developers, not JavaScriptKit users. For user documentation, see [Exporting Swift to JavaScript](https://swiftpackageindex.com/swiftwasm/JavaScriptKit/documentation/javascriptkit/exporting-swift-to-javascript) and [Importing TypeScript into Swift](https://swiftpackageindex.com/swiftwasm/JavaScriptKit/documentation/javascriptkit/importing-typescript-into-swift). +> This documentation is intended for JavaScriptKit developers, not JavaScriptKit users. For user documentation, see [Introducing BridgeJS](https://swiftpackageindex.com/swiftwasm/JavaScriptKit/documentation/javascriptkit/introducing-bridgejs), [Setting up BridgeJS](https://swiftpackageindex.com/swiftwasm/JavaScriptKit/documentation/javascriptkit/setting-up-bridgejs), [Exporting Swift to JavaScript](https://swiftpackageindex.com/swiftwasm/JavaScriptKit/documentation/javascriptkit/exporting-swift-to-javascript), [Importing JavaScript into Swift](https://swiftpackageindex.com/swiftwasm/JavaScriptKit/documentation/javascriptkit/importing-javascript-into-swift), and [Generating bindings from TypeScript](https://swiftpackageindex.com/swiftwasm/JavaScriptKit/documentation/javascriptkit/generating-from-typescript). ## Overview BridgeJS provides easy interoperability between Swift and JavaScript/TypeScript. It enables: -1. **Importing TypeScript APIs into Swift**: Use TypeScript/JavaScript APIs directly from Swift code -2. **Exporting Swift APIs to JavaScript**: Make your Swift APIs available to JavaScript code +1. **Export Swift to JavaScript** – Expose Swift functions, classes, and types to JavaScript; the plugin generates TypeScript definitions (`.d.ts`) for the exported API. +2. **Import JavaScript into Swift** – Make JavaScript/TypeScript APIs callable from Swift with macro-annotated bindings or bindings generated from a TypeScript declaration file (`bridge-js.d.ts`). The workflow is: @@ -161,7 +161,7 @@ Return values use direct Wasm returns for primitives, and imported intrinsic fun - **Structs/Arrays/Enums**: Copy semantics - data serialized across boundary. No cleanup needed. - **Closures**: Boxed on source side, released when GC'd on either side. -For detailed semantics, see the [How It Works sections](https://swiftpackageindex.com/swiftwasm/JavaScriptKit/documentation/javascriptkit/exporting-swift-class#How-It-Works) in the user documentation. +For detailed semantics, see the [How It Works sections](https://swiftpackageindex.com/swiftwasm/JavaScriptKit/documentation/javascriptkit/exporting-swift-class#how-it-works) in the user documentation. ## Testing diff --git a/Plugins/BridgeJS/Sources/TS2Swift/JavaScript/src/cli.js b/Plugins/BridgeJS/Sources/TS2Swift/JavaScript/src/cli.js index 766cd0432..98453ebf1 100644 --- a/Plugins/BridgeJS/Sources/TS2Swift/JavaScript/src/cli.js +++ b/Plugins/BridgeJS/Sources/TS2Swift/JavaScript/src/cli.js @@ -123,7 +123,7 @@ export function run(filePath, options) { "// To update this file, just rebuild your project or run", "// `swift package bridge-js`.", "", - "@_spi(Experimental) @_spi(BridgeJS) import JavaScriptKit", + "@_spi(BridgeJS) import JavaScriptKit", "", "", ].join("\n"); diff --git a/Plugins/BridgeJS/Sources/TS2Swift/JavaScript/test/__snapshots__/ts2swift.test.js.snap b/Plugins/BridgeJS/Sources/TS2Swift/JavaScript/test/__snapshots__/ts2swift.test.js.snap index 49814a996..85d1da0de 100644 --- a/Plugins/BridgeJS/Sources/TS2Swift/JavaScript/test/__snapshots__/ts2swift.test.js.snap +++ b/Plugins/BridgeJS/Sources/TS2Swift/JavaScript/test/__snapshots__/ts2swift.test.js.snap @@ -7,7 +7,7 @@ exports[`ts2swift > snapshots Swift output for ArrayParameter.d.ts > ArrayParame // To update this file, just rebuild your project or run // \`swift package bridge-js\`. -@_spi(Experimental) @_spi(BridgeJS) import JavaScriptKit +@_spi(BridgeJS) import JavaScriptKit @JSFunction func processNumbers(_ values: [Double]) throws(JSException) -> Void @@ -30,7 +30,7 @@ exports[`ts2swift > snapshots Swift output for Async.d.ts > Async 1`] = ` // To update this file, just rebuild your project or run // \`swift package bridge-js\`. -@_spi(Experimental) @_spi(BridgeJS) import JavaScriptKit +@_spi(BridgeJS) import JavaScriptKit @JSFunction func asyncReturnVoid() throws(JSException) -> JSPromise @@ -55,7 +55,7 @@ exports[`ts2swift > snapshots Swift output for Documentation.d.ts > Documentatio // To update this file, just rebuild your project or run // \`swift package bridge-js\`. -@_spi(Experimental) @_spi(BridgeJS) import JavaScriptKit +@_spi(BridgeJS) import JavaScriptKit /// Return a greeting for a user. /// @@ -112,7 +112,7 @@ exports[`ts2swift > snapshots Swift output for Interface.d.ts > Interface 1`] = // To update this file, just rebuild your project or run // \`swift package bridge-js\`. -@_spi(Experimental) @_spi(BridgeJS) import JavaScriptKit +@_spi(BridgeJS) import JavaScriptKit @JSFunction func returnAnimatable() throws(JSException) -> Animatable @@ -130,7 +130,7 @@ exports[`ts2swift > snapshots Swift output for InvalidPropertyNames.d.ts > Inval // To update this file, just rebuild your project or run // \`swift package bridge-js\`. -@_spi(Experimental) @_spi(BridgeJS) import JavaScriptKit +@_spi(BridgeJS) import JavaScriptKit @JSFunction func createArrayBuffer() throws(JSException) -> ArrayBufferLike @@ -178,7 +178,7 @@ exports[`ts2swift > snapshots Swift output for MultipleImportedTypes.d.ts > Mult // To update this file, just rebuild your project or run // \`swift package bridge-js\`. -@_spi(Experimental) @_spi(BridgeJS) import JavaScriptKit +@_spi(BridgeJS) import JavaScriptKit @JSFunction func createDatabaseConnection(_ config: JSValue) throws(JSException) -> DatabaseConnection @@ -215,7 +215,7 @@ exports[`ts2swift > snapshots Swift output for ObjectLikeTypes.d.ts > ObjectLike // To update this file, just rebuild your project or run // \`swift package bridge-js\`. -@_spi(Experimental) @_spi(BridgeJS) import JavaScriptKit +@_spi(BridgeJS) import JavaScriptKit @JSFunction func acceptObject(_ v: JSObject) throws(JSException) -> Void " @@ -228,7 +228,7 @@ exports[`ts2swift > snapshots Swift output for OptionalNullUndefined.d.ts > Opti // To update this file, just rebuild your project or run // \`swift package bridge-js\`. -@_spi(Experimental) @_spi(BridgeJS) import JavaScriptKit +@_spi(BridgeJS) import JavaScriptKit @JSFunction func roundTripNumberNull(_ value: Optional) throws(JSException) -> Optional @@ -269,7 +269,7 @@ exports[`ts2swift > snapshots Swift output for PrimitiveParameters.d.ts > Primit // To update this file, just rebuild your project or run // \`swift package bridge-js\`. -@_spi(Experimental) @_spi(BridgeJS) import JavaScriptKit +@_spi(BridgeJS) import JavaScriptKit @JSFunction func check(_ a: Double, _ b: Bool) throws(JSException) -> Void " @@ -282,7 +282,7 @@ exports[`ts2swift > snapshots Swift output for PrimitiveReturn.d.ts > PrimitiveR // To update this file, just rebuild your project or run // \`swift package bridge-js\`. -@_spi(Experimental) @_spi(BridgeJS) import JavaScriptKit +@_spi(BridgeJS) import JavaScriptKit @JSFunction func checkNumber() throws(JSException) -> Double @@ -297,7 +297,7 @@ exports[`ts2swift > snapshots Swift output for ReExportFrom.d.ts > ReExportFrom // To update this file, just rebuild your project or run // \`swift package bridge-js\`. -@_spi(Experimental) @_spi(BridgeJS) import JavaScriptKit +@_spi(BridgeJS) import JavaScriptKit @JSFunction func jsRoundTripNumber(_ v: Double) throws(JSException) -> Double @@ -315,7 +315,7 @@ exports[`ts2swift > snapshots Swift output for RecordDictionary.d.ts > RecordDic // To update this file, just rebuild your project or run // \`swift package bridge-js\`. -@_spi(Experimental) @_spi(BridgeJS) import JavaScriptKit +@_spi(BridgeJS) import JavaScriptKit @JSFunction func takeRecord(_ value: [String: Double]) throws(JSException) -> Void @@ -345,7 +345,7 @@ exports[`ts2swift > snapshots Swift output for StringEnum.d.ts > StringEnum 1`] // To update this file, just rebuild your project or run // \`swift package bridge-js\`. -@_spi(Experimental) @_spi(BridgeJS) import JavaScriptKit +@_spi(BridgeJS) import JavaScriptKit enum FeatureFlag: String { case foo = "foo" @@ -366,7 +366,7 @@ exports[`ts2swift > snapshots Swift output for StringParameter.d.ts > StringPara // To update this file, just rebuild your project or run // \`swift package bridge-js\`. -@_spi(Experimental) @_spi(BridgeJS) import JavaScriptKit +@_spi(BridgeJS) import JavaScriptKit @JSFunction func checkString(_ a: String) throws(JSException) -> Void @@ -381,7 +381,7 @@ exports[`ts2swift > snapshots Swift output for StringReturn.d.ts > StringReturn // To update this file, just rebuild your project or run // \`swift package bridge-js\`. -@_spi(Experimental) @_spi(BridgeJS) import JavaScriptKit +@_spi(BridgeJS) import JavaScriptKit @JSFunction func checkString() throws(JSException) -> String " @@ -394,7 +394,7 @@ exports[`ts2swift > snapshots Swift output for TS2SkeletonLike.d.ts > TS2Skeleto // To update this file, just rebuild your project or run // \`swift package bridge-js\`. -@_spi(Experimental) @_spi(BridgeJS) import JavaScriptKit +@_spi(BridgeJS) import JavaScriptKit @JSFunction func createTS2Skeleton() throws(JSException) -> TypeScriptProcessor @@ -420,7 +420,7 @@ exports[`ts2swift > snapshots Swift output for TypeAlias.d.ts > TypeAlias 1`] = // To update this file, just rebuild your project or run // \`swift package bridge-js\`. -@_spi(Experimental) @_spi(BridgeJS) import JavaScriptKit +@_spi(BridgeJS) import JavaScriptKit @JSFunction func checkSimple(_ a: Double) throws(JSException) -> Void " @@ -433,7 +433,7 @@ exports[`ts2swift > snapshots Swift output for TypeAliasObject.d.ts > TypeAliasO // To update this file, just rebuild your project or run // \`swift package bridge-js\`. -@_spi(Experimental) @_spi(BridgeJS) import JavaScriptKit +@_spi(BridgeJS) import JavaScriptKit @JSFunction func console() throws(JSException) -> Console @@ -452,7 +452,7 @@ exports[`ts2swift > snapshots Swift output for TypeScriptClass.d.ts > TypeScript // To update this file, just rebuild your project or run // \`swift package bridge-js\`. -@_spi(Experimental) @_spi(BridgeJS) import JavaScriptKit +@_spi(BridgeJS) import JavaScriptKit @JSClass struct Greeter { @JSGetter var name: String @@ -473,7 +473,7 @@ exports[`ts2swift > snapshots Swift output for VoidParameterVoidReturn.d.ts > Vo // To update this file, just rebuild your project or run // \`swift package bridge-js\`. -@_spi(Experimental) @_spi(BridgeJS) import JavaScriptKit +@_spi(BridgeJS) import JavaScriptKit @JSFunction func check() throws(JSException) -> Void " @@ -486,7 +486,7 @@ exports[`ts2swift > snapshots Swift output for WebIDLDOMDocs.d.ts > WebIDLDOMDoc // To update this file, just rebuild your project or run // \`swift package bridge-js\`. -@_spi(Experimental) @_spi(BridgeJS) import JavaScriptKit +@_spi(BridgeJS) import JavaScriptKit /// [MDN Reference](https://developer.mozilla.org/docs/Web/API/Document) @JSGetter var document: Document diff --git a/Plugins/PackageToJS/Sources/PackageToJS.swift b/Plugins/PackageToJS/Sources/PackageToJS.swift index 268f3f200..2e47dcf71 100644 --- a/Plugins/PackageToJS/Sources/PackageToJS.swift +++ b/Plugins/PackageToJS/Sources/PackageToJS.swift @@ -589,11 +589,6 @@ struct PackagingPlanner { packageInputs.append(packageJsonTask) if skeletons.count > 0 { - if ProcessInfo.processInfo.environment["JAVASCRIPTKIT_EXPERIMENTAL_BRIDGEJS"] == nil { - fatalError( - "BridgeJS is still an experimental feature. Set the environment variable JAVASCRIPTKIT_EXPERIMENTAL_BRIDGEJS=1 to enable." - ) - } let bridgeJs = outputDir.appending(path: "bridge-js.js") let bridgeDts = outputDir.appending(path: "bridge-js.d.ts") packageInputs.append( diff --git a/README.md b/README.md index 6a0acaa27..b5cb04801 100644 --- a/README.md +++ b/README.md @@ -48,14 +48,14 @@ _ = document.body.appendChild(button) BridgeJS provides easy interoperability between Swift and JavaScript/TypeScript. It enables: -- **Exporting Swift APIs to JavaScript**: Make your Swift code callable from JavaScript -- **Importing TypeScript APIs into Swift**: Use JavaScript APIs with type safety in Swift +- **Export Swift to JavaScript** – Expose Swift functions, classes, and types to JavaScript; the plugin also generates TypeScript definitions (`.d.ts`) for the exported API. +- **Import JavaScript into Swift** – Make JavaScript/TypeScript APIs (functions, classes, globals like `document` or `console`) callable from Swift with type-safe bindings. You can declare bindings with macros in Swift or generate them from a TypeScript declaration file (`bridge-js.d.ts`). -For architecture details, see the [BridgeJS Plugin README](Plugins/BridgeJS/README.md). +For architecture details, see the [BridgeJS Plugin README](Plugins/BridgeJS/README.md). For package and build setup, see [Setting up BridgeJS](https://swiftpackageindex.com/swiftwasm/JavaScriptKit/documentation/javascriptkit/setting-up-bridgejs) in the documentation. ### Exporting Swift to JavaScript -Mark Swift code with `@JS` to make it callable from JavaScript: +Mark Swift code with `@JS` (the ``JS(namespace:enumStyle:)`` attribute) to make it callable from JavaScript: ```swift import JavaScriptKit @@ -81,32 +81,36 @@ console.log(greeter.greet()); // "Hello, World!" **Learn more:** [Exporting Swift to JavaScript](https://swiftpackageindex.com/swiftwasm/JavaScriptKit/documentation/javascriptkit/exporting-swift-to-javascript) -### Importing TypeScript into Swift +### Importing JavaScript into Swift -Define TypeScript interfaces and BridgeJS generates type-safe Swift bindings: +Declare bindings in Swift with macros such as `@JSFunction`, `@JSClass`, `@JSGetter`, and `@JSSetter`: -```typescript -// bridge-js.d.ts -interface Document { - title: string; - getElementById(id: string): HTMLElement; - createElement(tagName: string): HTMLElement; +```swift +import JavaScriptKit + +@JSClass struct Document { + @JSFunction func getElementById(_ id: String) throws(JSException) -> HTMLElement + @JSFunction func createElement(_ tagName: String) throws(JSException) -> HTMLElement } +@JSGetter(from: .global) var document: Document -export function getDocument(): Document; -``` +@JSClass struct HTMLElement { + @JSGetter var innerText: String + @JSSetter func setInnerText(_ newValue: String) throws(JSException) + @JSFunction func appendChild(_ child: HTMLElement) throws(JSException) +} -**Swift usage:** -```swift @JS func run() throws(JSException) { - let document = try getDocument() - try document.setTitle("My Swift App") - let button = try document.createElement("button") + let button = document.createElement("button")! try button.setInnerText("Click Me") + let container = document.getElementById("app")! + container.appendChild(button) } ``` -**Learn more:** [Importing TypeScript into Swift](https://swiftpackageindex.com/swiftwasm/JavaScriptKit/documentation/javascriptkit/importing-typescript-into-swift) +You can also generate the same macro-annotated Swift from a TypeScript file (`bridge-js.d.ts`). See [Generating bindings from TypeScript](https://swiftpackageindex.com/swiftwasm/JavaScriptKit/documentation/javascriptkit/generating-from-typescript) in the documentation. + +**Learn more:** [Importing JavaScript into Swift](https://swiftpackageindex.com/swiftwasm/JavaScriptKit/documentation/javascriptkit/importing-javascript-into-swift) ### Try It Online diff --git a/Sources/JavaScriptKit/Documentation.docc/Articles/BridgeJS/Ahead-of-Time-Code-Generation.md b/Sources/JavaScriptKit/Documentation.docc/Articles/BridgeJS/Ahead-of-Time-Code-Generation.md index ee6ac3047..776f6f577 100644 --- a/Sources/JavaScriptKit/Documentation.docc/Articles/BridgeJS/Ahead-of-Time-Code-Generation.md +++ b/Sources/JavaScriptKit/Documentation.docc/Articles/BridgeJS/Ahead-of-Time-Code-Generation.md @@ -6,7 +6,7 @@ Learn how to improve build times by generating BridgeJS code ahead of time. > Important: This feature is still experimental. No API stability is guaranteed, and the API may change in future releases. -The BridgeJS build plugin automatically processes `@JS` annotations and TypeScript definitions during each build. While convenient, this can significantly increase build times for larger projects. To address this, JavaScriptKit provides a command plugin that lets you generate the bridge code ahead of time. +The BridgeJS build plugin automatically processes macro annotations and TypeScript definitions during each build. While convenient, this can significantly increase build times for larger projects. To address this, JavaScriptKit provides a command plugin that lets you generate the bridge code ahead of time. ## Using the Command Plugin @@ -54,7 +54,7 @@ $ echo "{}" > Sources/MyApp/bridge-js.config.json ### Step 3: Create Your Swift Code with @JS Annotations -Write your Swift code with `@JS` annotations as usual: +Write your Swift code with macro annotations as usual: ```swift import JavaScriptKit @@ -65,13 +65,13 @@ import JavaScriptKit @JS class Counter { private var count = 0 - + @JS init() {} - + @JS func increment() { count += 1 } - + @JS func getValue() -> Int { return count } @@ -104,14 +104,15 @@ swift package plugin bridge-js This command will: -1. Process all Swift files with `@JS` annotations +1. Process all Swift files with macro annotations 2. Process any TypeScript definition files 3. Generate Swift binding code in a `Generated` directory within your source folder For example, with a target named "MyApp", it will create: ``` -Sources/MyApp/Generated/BridgeJS.swift # Generated code for both exports and imports +Sources/MyApp/Generated/BridgeJS.swift # Glue code for both exports and imports +Sources/MyApp/Generated/BridgeJS.Macros.swift # Bridging interface generated from bridge-js.d.ts Sources/MyApp/Generated/JavaScript/BridgeJS.json # Unified skeleton JSON ``` @@ -173,4 +174,4 @@ git commit -m "Update generated BridgeJS code" 2. **Version Control**: Always commit the generated files if using the command plugin 3. **API Boundaries**: Try to stabilize your API boundaries to minimize regeneration 4. **Documentation**: Document your approach in your project README -5. **CI/CD**: If using the command plugin, consider verifying that generated code is up-to-date in CI +5. **CI/CD**: If using the command plugin, consider verifying that generated code is up-to-date in CI diff --git a/Sources/JavaScriptKit/Documentation.docc/Articles/BridgeJS/BridgeJS-Configuration.md b/Sources/JavaScriptKit/Documentation.docc/Articles/BridgeJS/BridgeJS-Configuration.md index 78b5d2d37..604017aad 100644 --- a/Sources/JavaScriptKit/Documentation.docc/Articles/BridgeJS/BridgeJS-Configuration.md +++ b/Sources/JavaScriptKit/Documentation.docc/Articles/BridgeJS/BridgeJS-Configuration.md @@ -42,7 +42,7 @@ Later files override settings from earlier files. This allows teams to commit a Controls whether exported Swift APIs are exposed to the JavaScript global namespace (`globalThis`). -When `true`, exported functions, classes, and namespaces are available via `globalThis` in JavaScript. When `false`, they are only available through the exports object returned by `createExports()`. +When `true`, exported functions, classes, and namespaces are available via `globalThis` in JavaScript. When `false`, they are only available through the exports object returned by `createExports()`. Using Exports provides better module isolation and support multiple WebAssembly instances in the same JavaScript context. Example: diff --git a/Sources/JavaScriptKit/Documentation.docc/Articles/BridgeJS/BridgeJS-Internals/BridgeJS-Internals.md b/Sources/JavaScriptKit/Documentation.docc/Articles/BridgeJS/BridgeJS-Internals/BridgeJS-Internals.md new file mode 100644 index 000000000..b8589b49e --- /dev/null +++ b/Sources/JavaScriptKit/Documentation.docc/Articles/BridgeJS/BridgeJS-Internals/BridgeJS-Internals.md @@ -0,0 +1,11 @@ +# BridgeJS Internals + +Internal design, performance rationale, and low-level details for BridgeJS. + +## Overview + +This section is for maintainers and contributors who want to understand how BridgeJS works under the hood, why it is designed the way it is, and how it interacts with the JavaScript engine and ABI. + +## Topics + +- diff --git a/Sources/JavaScriptKit/Documentation.docc/Articles/BridgeJS/BridgeJS-Internals/Design-Rationale.md b/Sources/JavaScriptKit/Documentation.docc/Articles/BridgeJS/BridgeJS-Internals/Design-Rationale.md new file mode 100644 index 000000000..8bfc5d302 --- /dev/null +++ b/Sources/JavaScriptKit/Documentation.docc/Articles/BridgeJS/BridgeJS-Internals/Design-Rationale.md @@ -0,0 +1,38 @@ +# BridgeJS Design Rationale + +Why BridgeJS is faster than dynamic `JSObject`/`JSValue` APIs and how engine optimizations influence the design. + +## Overview + +BridgeJS generates **specialized** bridge code per exported or imported interface. That specialization, combined with stable call and property access patterns, allows JavaScript engines to optimize the boundary crossing much better than with generic dynamic code. This page explains the main performance rationale. + +## Why generated code is faster + +1. **Specialized code per interface** - Each bridged function or property gets its own glue path with known types. The engine does not need to handle arbitrary shapes or types at the call site. + +2. **Use of static type information** - The generator knows parameter and return types at compile time. It can avoid dynamic type checks and boxing where the dynamic API would require them. + +3. **IC-friendly access patterns** - Property and method accesses use stable, predictable shapes instead of a single generic subscript path. That keeps engine **inline caches (ICs)** effective instead of turning them **megamorphic**. + +## Inline caches (ICs) and megamorphic penalty + +JavaScript engines (and many other dynamic-language VMs) use **inline caches** at property and method access sites: they remember the object shape (e.g. “this property is at offset X”) so the next access with the same shape can take a fast path. + +- **Monomorphic** - One shape seen at a site → very fast, offset cached. +- **Polymorphic** - A few shapes → still fast, small dispatch in the IC. +- **Megamorphic** - Too many different shapes at the same site → the IC gives up and falls back to a generic property lookup, which is much slower. + +Engines typically allow only a small number of shapes per IC (e.g. on the order of a few) before marking the site megamorphic. + +## Why `JSObject` subscript is problematic + +`JSObject.subscript` (and similar dynamic property access) shares **one** code path for all property names and all object shapes. Every access goes through the same call site with varying keys and receiver shapes. That site therefore sees many different shapes and quickly becomes **megamorphic**, so the engine cannot cache property offsets and must do a generic lookup every time. + +So even if you cache the property name (e.g. with `CachedJSStrings`), you are still using the same generic subscript path; the call site stays megamorphic and pays the slow-path cost. + +BridgeJS avoids this by generating **separate** access paths per property or method. Each generated getter/setter or function call has a stable shape at the engine level, so the IC can stay monomorphic or polymorphic and the fast path is used. + +## What to read next + +- ABI and binary interface details will be documented in this section as they stabilize. +- For using BridgeJS in your app, see , , and . diff --git a/Sources/JavaScriptKit/Documentation.docc/Articles/BridgeJS/Bringing-Swift-Closures-to-JavaScript.md b/Sources/JavaScriptKit/Documentation.docc/Articles/BridgeJS/Bringing-Swift-Closures-to-JavaScript.md new file mode 100644 index 000000000..ce1d9d555 --- /dev/null +++ b/Sources/JavaScriptKit/Documentation.docc/Articles/BridgeJS/Bringing-Swift-Closures-to-JavaScript.md @@ -0,0 +1,80 @@ +# Bringing Swift Closures to JavaScript + +Use ``JSTypedClosure`` to pass or return Swift closures to the JavaScript world with BridgeJS-with type safety and explicit lifetime management. + +## Overview + +``JSTypedClosure`` wraps a **Swift closure** so you can **pass it or return it to JavaScript** through BridgeJS. The closure lives in Swift; JavaScript receives a function that calls back into that closure when invoked. Use it when: + +- You **pass** a Swift closure as an argument to a JavaScript API (e.g. an ``JSFunction(jsName:from:)`` that takes a callback parameter). +- You **return** a Swift closure from Swift exported by ``JS(namespace:enumStyle:)`` so JavaScript can call it later. + +Unlike ``JSClosure``, which uses untyped ``JSValue`` arguments and return values, ``JSTypedClosure`` has a concrete **signature** (e.g. `(Int) -> Int` or `(String) -> Void`). BridgeJS generates the glue code for that signature, so you get compile-time type safety when crossing into the JS world. + +You **must call** ``JSTypedClosure/release()`` when the closure is no longer needed by JavaScript. After release, any attempt to invoke the closure from JavaScript throws an explicit JS exception. + +## Creating a JSTypedClosure + +BridgeJS generates an initializer for each closure signature used in your module. Wrap your Swift closure and pass or return it to JavaScript: + +```swift +import JavaScriptKit + +// Pass a Swift closure to a JS function that expects a callback (Int) -> Int +@JSFunction static func applyTransform(_ value: Int, _ transform: JSTypedClosure<(Int) -> Int>) throws(JSException) -> Int + +let double = JSTypedClosure<(Int) -> Int> { $0 * 2 } +defer { double.release() } +let result = try applyTransform(10, double) // 20 - JS calls back into Swift +``` + +You can pass or return typed closures with other signatures the same way: + +```swift +let sum = JSTypedClosure<(Int, Int) -> Int> { $0 + $1 } +defer { sum.release() } + +let log = JSTypedClosure<(String) -> Void> { print($0) } +defer { log.release() } +``` + +## Lifetime and release() + +A ``JSTypedClosure`` keeps the Swift closure alive and exposes a JavaScript function that calls into it. To avoid leaks and use-after-free: + +1. **Call `release()` exactly once** when the closure is no longer needed by JavaScript (e.g. when the callback is unregistered or the object that held it is released). +2. Prefer **`defer { closure.release() }`** right after creating the closure so cleanup runs when the current scope exits. +3. After `release()`, calling the closure from JavaScript throws an exception with a message that includes the file and line where the ``JSTypedClosure`` was created. + +A **FinalizationRegistry** on the JavaScript side may eventually release the Swift storage if you never call `release()`, but that is non-deterministic. Do not rely on it for timely cleanup. + +## Getting the underlying JavaScript function + +When you need to store or pass the function on the JavaScript side (e.g. to compare identity or attach to a DOM node), use the ``JSTypedClosure/jsObject`` property to get the ``JSObject`` that represents the JavaScript function. + +## JSTypedClosure vs JSClosure + +Both let you pass or return a Swift closure to the JavaScript world. The difference is how they are typed and which API you use: + +| | JSTypedClosure | JSClosure | +|:--|:--|:--| +| **API** | BridgeJS (macros, generated code) | Dynamic ``JSObject`` / ``JSValue`` | +| **Types** | Typed signature, e.g. `(Int) -> Int` | Untyped `([JSValue]) -> JSValue` | +| **Lifetime** | Explicit `release()` required | Explicit `release()` required | +| **Use case** | Passing/returning closures at the BridgeJS boundary with a fixed signature | Passing Swift functions to JS via dynamic APIs (e.g. DOM events) | + +Use ``JSTypedClosure`` when you pass or return closures through BridgeJS-declared APIs. Use ``JSClosure`` when you pass a Swift function to JavaScript using the dynamic APIs (e.g. ``JSObject``, ``JSValue``) without generated glue. + +## JSTypedClosure vs auto-managed closures + +BridgeJS can also expose **plain** Swift closure types (e.g. `(String) -> String`) as parameters and return values; lifetime is then managed automatically via ``FinalizationRegistry`` and no `release()` is required. See . + +**When returning** a closure from Swift to JavaScript, we **recommend** using ``JSTypedClosure`` and managing lifetime explicitly with `release()`, rather than returning a plain closure type. Explicit release makes cleanup predictable and avoids relying solely on JavaScript GC. + +## See also + +- ``JSTypedClosure`` +- ``JSClosure`` +- +- +- 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 75f0fcca5..b14d2eebe 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,60 +6,11 @@ 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 - -To use the BridgeJS feature, you need to enable the experimental `Extern` feature and add the BridgeJS plugin to your package. Here's an example of a `Package.swift` file: - -```swift -// swift-tools-version:6.0 - -import PackageDescription - -let package = Package( - name: "MyApp", - dependencies: [ - .package(url: "https://github.com/swiftwasm/JavaScriptKit.git", branch: "main") - ], - targets: [ - .executableTarget( - name: "MyApp", - dependencies: ["JavaScriptKit"], - swiftSettings: [ - // This is required because the generated code depends on @_extern(wasm) - .enableExperimentalFeature("Extern") - ], - plugins: [ - // Add build plugin for processing @JS and generate Swift glue code - .plugin(name: "BridgeJS", package: "JavaScriptKit") - ] - ) - ] -) -``` - -The `BridgeJS` plugin will process your Swift code to find declarations marked with `@JS` and generate the necessary bridge code to make them accessible from JavaScript. - -### Building your package for JavaScript - -After configuring your `Package.swift`, you can build your package for JavaScript using the following command: - -```bash -swift package --swift-sdk $SWIFT_SDK_ID js -``` - -This command will: - -1. Process all Swift files with `@JS` annotations -2. Generate JavaScript bindings and TypeScript type definitions (`.d.ts`) for your exported Swift code -3. Output everything to the `.build/plugins/PackageToJS/outputs/` directory - -> Note: For larger projects, you may want to generate the BridgeJS code ahead of time to improve build performance. See for more information. +> Tip: You can quickly preview what interfaces will be exposed on the Swift/JavaScript/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(namespace:enumStyle:)`` attribute. This enables JavaScript code to call into Swift code running in WebAssembly. +Configure your package and build for JavaScript as described in . Then use the topics below to expose Swift types and functions to JavaScript. ## Topics @@ -75,3 +26,7 @@ This command will: - - - + +## See Also + +- ``JS(namespace:enumStyle:)`` diff --git a/Sources/JavaScriptKit/Documentation.docc/Articles/BridgeJS/Exporting-Swift/Exporting-Swift-Array.md b/Sources/JavaScriptKit/Documentation.docc/Articles/BridgeJS/Exporting-Swift/Exporting-Swift-Array.md index 53d008068..e97bceefa 100644 --- a/Sources/JavaScriptKit/Documentation.docc/Articles/BridgeJS/Exporting-Swift/Exporting-Swift-Array.md +++ b/Sources/JavaScriptKit/Documentation.docc/Articles/BridgeJS/Exporting-Swift/Exporting-Swift-Array.md @@ -4,7 +4,7 @@ Learn how to pass Swift arrays to and from 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/). +> Tip: You can quickly preview what interfaces will be exposed on the Swift/JavaScript/TypeScript sides using the [BridgeJS Playground](https://swiftwasm.org/JavaScriptKit/PlayBridgeJS/). BridgeJS allows you to pass Swift arrays as function parameters and return values. 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 index 3e44581fa..9cd4a2224 100644 --- 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 @@ -4,7 +4,7 @@ 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/). +> Tip: You can quickly preview what interfaces will be exposed on the Swift/JavaScript/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: @@ -51,6 +51,7 @@ 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)}`); +cart.release(); // Call release() when done; don't rely on FinalizationRegistry as much as possible ``` The generated TypeScript declarations for this class would look like: @@ -83,7 +84,7 @@ Classes use **reference semantics** when crossing the Swift/JavaScript boundary: 1. **Object Creation**: When you create a class instance (via `new` in JS), the object lives on the Swift heap 2. **Reference Passing**: JavaScript receives a reference (handle) to the Swift object, not a copy 3. **Shared State**: Changes made through either Swift or JavaScript affect the same object -4. **Memory Management**: `FinalizationRegistry` automatically releases Swift objects when they're garbage collected in JavaScript. You can optionally call `release()` for deterministic cleanup. +4. **Memory Management**: `FinalizationRegistry` can release Swift objects when they're garbage collected in JavaScript, but you should **not rely on it** for cleanup. Call `release()` when an instance is no longer needed so that Swift memory is reclaimed deterministically. This is especially important for **short-lived instances**: GC may run late or not at all for objects that become unreachable quickly, so relying on `FinalizationRegistry` can delay or leak Swift-side resources. This differs from structs, which use copy semantics and transfer data by value. diff --git a/Sources/JavaScriptKit/Documentation.docc/Articles/BridgeJS/Exporting-Swift/Exporting-Swift-Closure.md b/Sources/JavaScriptKit/Documentation.docc/Articles/BridgeJS/Exporting-Swift/Exporting-Swift-Closure.md index fe934ddba..adb9ab33b 100644 --- a/Sources/JavaScriptKit/Documentation.docc/Articles/BridgeJS/Exporting-Swift/Exporting-Swift-Closure.md +++ b/Sources/JavaScriptKit/Documentation.docc/Articles/BridgeJS/Exporting-Swift/Exporting-Swift-Closure.md @@ -4,9 +4,11 @@ Learn how to use closure/function types as parameters and return values in Bridg ## 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/). +> Tip: You can quickly preview what interfaces will be exposed on the Swift/JavaScript/TypeScript sides using the [BridgeJS Playground](https://swiftwasm.org/JavaScriptKit/PlayBridgeJS/). -BridgeJS supports typed closure parameters and return values, allowing you to pass functions between Swift and JavaScript with full type safety. This enables functional programming patterns like callbacks, higher-order functions, and function composition across the language boundary. +BridgeJS supports typed closure parameters and return values, allowing you to pass or return Swift closures to JavaScript with full type safety. **Lifetime is automatic**: you use plain Swift closure types (e.g. `(String) -> String`); the runtime releases them when no longer needed-no manual `release()` required. This enables callbacks, higher-order functions, and function composition across the boundary. + +**Recommendation:** When **returning** a closure from Swift to JavaScript, prefer returning a ``JSTypedClosure`` and managing its lifetime explicitly (see ). Explicit `release()` makes cleanup predictable and avoids relying solely on JavaScript garbage collection. Use plain closure types (this article) when you want fully automatic lifetime or when passing closures only as parameters into your exported API. ## Example @@ -99,6 +101,8 @@ Closures use **reference semantics** when crossing the Swift/JavaScript boundary This differs from structs and arrays, which use copy semantics and transfer data by value. +When you **return** a closure to JavaScript, we recommend using ``JSTypedClosure`` and calling `release()` when the closure is no longer needed, instead of returning a plain closure type. See . + ## Supported Features | Swift Feature | Status | @@ -113,4 +117,5 @@ This differs from structs and arrays, which use copy semantics and transfer data ## See Also +- - passing or returning closures with ``JSTypedClosure`` and explicit `release()` - diff --git a/Sources/JavaScriptKit/Documentation.docc/Articles/BridgeJS/Exporting-Swift/Exporting-Swift-Default-Parameters.md b/Sources/JavaScriptKit/Documentation.docc/Articles/BridgeJS/Exporting-Swift/Exporting-Swift-Default-Parameters.md index f6115f063..e6f4b8e9a 100644 --- a/Sources/JavaScriptKit/Documentation.docc/Articles/BridgeJS/Exporting-Swift/Exporting-Swift-Default-Parameters.md +++ b/Sources/JavaScriptKit/Documentation.docc/Articles/BridgeJS/Exporting-Swift/Exporting-Swift-Default-Parameters.md @@ -4,7 +4,7 @@ Learn how to use default parameter values in Swift functions and constructors ex ## 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/). +> Tip: You can quickly preview what interfaces will be exposed on the Swift/JavaScript/TypeScript sides using the [BridgeJS Playground](https://swiftwasm.org/JavaScriptKit/PlayBridgeJS/). BridgeJS supports default parameter values for Swift functions and class constructors exported to JavaScript. When you specify default values in your Swift code, they are automatically applied in the generated JavaScript bindings. @@ -64,7 +64,7 @@ Constructor parameters also support default values, making it easy to create ins @JS var name: String @JS var timeout: Int @JS var retries: Int - + @JS init(name: String = "default", timeout: Int = 30, retries: Int = 3) { self.name = name self.timeout = timeout 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 index 6fb3afeda..2220d457c 100644 --- 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 @@ -4,7 +4,7 @@ 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/). +> Tip: You can quickly preview what interfaces will be exposed on the Swift/JavaScript/TypeScript sides using the [BridgeJS Playground](https://swiftwasm.org/JavaScriptKit/PlayBridgeJS/). BridgeJS supports two output styles for enums, controlled by the `enumStyle` parameter: 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 index ebc8037c4..c26841041 100644 --- 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 @@ -4,7 +4,7 @@ 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/). +> Tip: You can quickly preview what interfaces will be exposed on the Swift/JavaScript/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`: diff --git a/Sources/JavaScriptKit/Documentation.docc/Articles/BridgeJS/Exporting-Swift/Exporting-Swift-Optional.md b/Sources/JavaScriptKit/Documentation.docc/Articles/BridgeJS/Exporting-Swift/Exporting-Swift-Optional.md index 91fdc3472..be15b762f 100644 --- a/Sources/JavaScriptKit/Documentation.docc/Articles/BridgeJS/Exporting-Swift/Exporting-Swift-Optional.md +++ b/Sources/JavaScriptKit/Documentation.docc/Articles/BridgeJS/Exporting-Swift/Exporting-Swift-Optional.md @@ -4,14 +4,12 @@ Learn how to use Swift optionals in functions, classes, and enums exported to Ja ## 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/). +> Tip: You can quickly preview what interfaces will be exposed on the Swift/JavaScript/TypeScript sides using the [BridgeJS Playground](https://swiftwasm.org/JavaScriptKit/PlayBridgeJS/). BridgeJS provides comprehensive support for Swift optionals across all bridged types. When you use `Optional` or `T?` in Swift, it automatically maps to `T | null` in TypeScript/JavaScript. Swift optionals are translated to union types with `null` (not `undefined`) because `null` represents an explicit "no value" state that aligns semantically with Swift's `nil`. This design choice ensures consistent null handling across the Swift-JavaScript bridge and avoids the ambiguity that comes with JavaScript's `undefined`. -> Important: BridgeJS optional support is **Swift → JavaScript only**. TypeScript → Swift optional imports are not currently supported. - ## Supported Optional Syntax BridgeJS recognizes all Swift optional syntax variants: @@ -35,7 +33,7 @@ All parameter types can be made optional, including primitives, objects, and enu ```swift @JS public func processOptionalData( text: String?, // Optional string - count: Int?, // Optional integer + count: Int?, // Optional integer flag: Bool?, // Optional boolean rate: Double?, // Optional double user: User? // Optional Swift class @@ -85,7 +83,7 @@ Constructors can accept optional parameters. @JS public var email: String? @JS public var age: Int? @JS public var avatar: User? - + @JS public init(name: String?) @JS public func updateProfile(email: String?, age: Int?) } @@ -100,7 +98,7 @@ export interface UserProfile extends SwiftHeapObject { email: string | null; age: number | null; avatar: User | null; - + updateProfile(email: string | null, age: number | null): void; } export type UserProfile = { diff --git a/Sources/JavaScriptKit/Documentation.docc/Articles/BridgeJS/Exporting-Swift/Exporting-Swift-Protocols.md b/Sources/JavaScriptKit/Documentation.docc/Articles/BridgeJS/Exporting-Swift/Exporting-Swift-Protocols.md index 111b1f052..3b2ec1526 100644 --- a/Sources/JavaScriptKit/Documentation.docc/Articles/BridgeJS/Exporting-Swift/Exporting-Swift-Protocols.md +++ b/Sources/JavaScriptKit/Documentation.docc/Articles/BridgeJS/Exporting-Swift/Exporting-Swift-Protocols.md @@ -4,7 +4,7 @@ Learn how to expose Swift protocols to JavaScript as TypeScript interfaces. ## 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/). +> Tip: You can quickly preview what interfaces will be exposed on the Swift/JavaScript/TypeScript sides using the [BridgeJS Playground](https://swiftwasm.org/JavaScriptKit/PlayBridgeJS/). BridgeJS allows you to export Swift protocols as TypeScript interfaces. JavaScript objects implementing these interfaces can be passed to Swift code, enabling protocol-oriented design across the Swift-JavaScript boundary. @@ -31,28 +31,28 @@ import JavaScriptKit @JS class CounterManager { var delegate: Counter - + @JS init(delegate: Counter) { self.delegate = delegate } - + @JS func incrementTwice() { delegate.increment(by: 1) delegate.increment(by: 1) } - + @JS func getCurrentValue() -> Int { return delegate.getValue() } - + @JS func getCounterName() -> String { return delegate.name } - + @JS func setCountValue(_ value: Int) { delegate.count = value } - + @JS func updateLabel(_ newLabel: String?) { delegate.label = newLabel } @@ -130,15 +130,15 @@ You can also implement protocols in Swift and use them from JavaScript: final class SwiftCounter: Counter { var count = 0 let name = "SwiftCounter" - + func increment(by amount: Int) { count += amount } - + func reset() { count = 0 } - + func getValue() -> Int { return count } diff --git a/Sources/JavaScriptKit/Documentation.docc/Articles/BridgeJS/Exporting-Swift/Exporting-Swift-Static-Functions.md b/Sources/JavaScriptKit/Documentation.docc/Articles/BridgeJS/Exporting-Swift/Exporting-Swift-Static-Functions.md index b6f680d1f..1c4b22abe 100644 --- a/Sources/JavaScriptKit/Documentation.docc/Articles/BridgeJS/Exporting-Swift/Exporting-Swift-Static-Functions.md +++ b/Sources/JavaScriptKit/Documentation.docc/Articles/BridgeJS/Exporting-Swift/Exporting-Swift-Static-Functions.md @@ -4,7 +4,7 @@ Learn how to export Swift static and class functions as JavaScript static method ## 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/). +> Tip: You can quickly preview what interfaces will be exposed on the Swift/JavaScript/TypeScript sides using the [BridgeJS Playground](https://swiftwasm.org/JavaScriptKit/PlayBridgeJS/). BridgeJS supports exporting Swift `static func` and `class func` to JavaScript static methods. Both generate identical JavaScript output but differ in Swift inheritance behavior. @@ -17,15 +17,15 @@ Classes can export both `static` and `class` functions: ```swift @JS class MathUtils { @JS init() {} - + @JS static func add(a: Int, b: Int) -> Int { return a + b } - + @JS class func subtract(a: Int, b: Int) -> Int { return a - b } - + @JS func multiply(x: Int, y: Int) -> Int { return x * y } @@ -79,11 +79,11 @@ Enums can contain static functions that are exported as properties: @JS enum Calculator { case scientific case basic - + @JS static func square(value: Int) -> Int { return value * value } - + @JS static func cube(value: Int) -> Int { return value * value * value } diff --git a/Sources/JavaScriptKit/Documentation.docc/Articles/BridgeJS/Exporting-Swift/Exporting-Swift-Static-Properties.md b/Sources/JavaScriptKit/Documentation.docc/Articles/BridgeJS/Exporting-Swift/Exporting-Swift-Static-Properties.md index 4110e6166..4898535c8 100644 --- a/Sources/JavaScriptKit/Documentation.docc/Articles/BridgeJS/Exporting-Swift/Exporting-Swift-Static-Properties.md +++ b/Sources/JavaScriptKit/Documentation.docc/Articles/BridgeJS/Exporting-Swift/Exporting-Swift-Static-Properties.md @@ -4,7 +4,7 @@ Learn how to export Swift static and class properties as JavaScript static prope ## 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/). +> Tip: You can quickly preview what interfaces will be exposed on the Swift/JavaScript/TypeScript sides using the [BridgeJS Playground](https://swiftwasm.org/JavaScriptKit/PlayBridgeJS/). BridgeJS supports exporting Swift `static var`, `static let`, and `class var` properties to JavaScript static properties. Both stored and computed properties are supported. @@ -15,16 +15,16 @@ Classes can export both stored and computed static properties: ```swift @JS class Configuration { @JS init() {} - + @JS static let version = "1.0.0" @JS static var debugMode = false @JS class var defaultTimeout = 30 - + @JS static var timestamp: Double { get { return Date().timeIntervalSince1970 } set { /* custom setter logic */ } } - + @JS static var buildNumber: Int { return 12345 } @@ -81,7 +81,7 @@ Enums can contain static properties that are exported alongside enum cases: @JS enum PropertyEnum { case value1 case value2 - + @JS static var enumProperty = "mutable" @JS static let enumConstant = 42 @JS static var computedEnum: String { diff --git a/Sources/JavaScriptKit/Documentation.docc/Articles/BridgeJS/Exporting-Swift/Exporting-Swift-Struct.md b/Sources/JavaScriptKit/Documentation.docc/Articles/BridgeJS/Exporting-Swift/Exporting-Swift-Struct.md index 10babd9b6..32bb79ed3 100644 --- a/Sources/JavaScriptKit/Documentation.docc/Articles/BridgeJS/Exporting-Swift/Exporting-Swift-Struct.md +++ b/Sources/JavaScriptKit/Documentation.docc/Articles/BridgeJS/Exporting-Swift/Exporting-Swift-Struct.md @@ -4,7 +4,7 @@ Learn how to export Swift structs 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/). +> Tip: You can quickly preview what interfaces will be exposed on the Swift/JavaScript/TypeScript sides using the [BridgeJS Playground](https://swiftwasm.org/JavaScriptKit/PlayBridgeJS/). To export a Swift struct, mark it with `@JS`: @@ -91,7 +91,7 @@ export type Exports = { ```swift @JS struct Config { var name: String - + @JS nonisolated(unsafe) static var defaultTimeout: Double = 30.0 @JS static let maxRetries: Int = 3 } 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 index 057b5f030..1aff82f65 100644 --- a/Sources/JavaScriptKit/Documentation.docc/Articles/BridgeJS/Exporting-Swift/Using-Namespace.md +++ b/Sources/JavaScriptKit/Documentation.docc/Articles/BridgeJS/Exporting-Swift/Using-Namespace.md @@ -4,7 +4,7 @@ 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/). +> Tip: You can quickly preview what interfaces will be exposed on the Swift/JavaScript/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. @@ -121,7 +121,3 @@ export interface Greeter extends SwiftHeapObject { ``` 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. - -## See Also - -- - Namespace enums section diff --git a/Sources/JavaScriptKit/Documentation.docc/Articles/BridgeJS/Generating-from-TypeScript.md b/Sources/JavaScriptKit/Documentation.docc/Articles/BridgeJS/Generating-from-TypeScript.md new file mode 100644 index 000000000..9c0a80dc1 --- /dev/null +++ b/Sources/JavaScriptKit/Documentation.docc/Articles/BridgeJS/Generating-from-TypeScript.md @@ -0,0 +1,419 @@ +# Generating bindings from TypeScript + +How to generate macro-annotated Swift bindings from a TypeScript declaration file (`bridge-js.d.ts`) so you don't have to write the Swift by hand. + +## Overview + +The BridgeJS plugin can read a `bridge-js.d.ts` file in your target and generate Swift code that uses the same macros as hand-written bindings (see ). + +The output is macro-annotated Swift; you can inspect the generated file at the following path to see exactly what was produced: + +``` +./build/plugins/outputs///destination/BridgeJS/BridgeJS.Macros.swift +``` + +Use this workflow when you have existing TypeScript definitions or many APIs to bind. + +> 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/JavaScript/TypeScript sides using the [BridgeJS Playground](https://swiftwasm.org/JavaScriptKit/PlayBridgeJS/). + +## How to generate bindings from TypeScript + +### Step 1: Configure your package + +Add the BridgeJS plugin and enable the Extern feature as described in . + +### Step 2: Create TypeScript definitions + +Create a file named `bridge-js.d.ts` in your target source directory (e.g. `Sources//bridge-js.d.ts`). Declare the JavaScript APIs you want to use in Swift: + +```typescript +export function consoleLog(message: string): void; +``` + +### Step 3: Build your package + +Run: + +```bash +swift package --swift-sdk $SWIFT_SDK_ID js +``` + +The plugin processes `bridge-js.d.ts`, generates Swift bindings (using the same macros as in ), compiles to WebAssembly, and produces JavaScript glue in `.build/plugins/PackageToJS/outputs/`. + +> Note: For larger projects, see . + +## Declaration mappings + +### Functions + +Each exported function becomes a top-level Swift function with `@JSFunction` and `throws(JSException)`. + +@Row { + @Column { + ```typescript + // TypeScript (bridge-js.d.ts) + + /** Returns the sum of two numbers. */ + export function sum(a: number, b: number): number; + /** + * Sets the document title. + * @param title - The new title string + */ + export function setDocumentTitle(title: string): void; + ``` + } + @Column { + ```swift + // Generated Swift + + /// Returns the sum of two numbers. + @JSFunction func sum(a: Double, b: Double) throws(JSException) -> Double + + + /// Sets the document title. + /// - Parameter title: The new title string + @JSFunction func setDocumentTitle(title: String) throws(JSException) + ``` + } +} + +### Global getters + +Module-level `declare const` or top-level readonly bindings that reference a bridged type become a Swift global property with `@JSGetter`. + +@Row { + @Column { + ```typescript + // TypeScript (bridge-js.d.ts) + /** [MDN Reference](https://developer.mozilla.org/docs/Web/API/Document) */ + export const document: Document; + ``` + } + @Column { + ```swift + // Generated Swift + /// [MDN Reference](https://developer.mozilla.org/docs/Web/API/Document) + @JSGetter var document: Document + ``` + } +} + +### Classes + +A class becomes a Swift struct with `@JSClass`. The constructor becomes `init(...)` with `@JSFunction`. Properties become `@JSGetter`; writable properties also get `@JSSetter` as `set(_:)`. Methods become `@JSFunction`. Static methods become `static func` on the struct. All thunks throw `JSException` if the underlying JavaScript throws. + +@Row { + @Column { + ```typescript + // TypeScript (bridge-js.d.ts) + export class Greeter { + /** A readonly field is translated with `@JSGetter` */ + readonly id: number; + + /** A read-writable field is translated with `@JSGetter` and `@JSSetter` */ + message: string; + + /** A constructor */ + constructor(id: string, name: string); + /** A method */ + greet(): string; + /** A static method */ + static createDefault(greetingId: number, locale: string): string; + } + ``` + } + @Column { + ```swift + // Generated Swift + @JSClass struct Greeter { + /// A readonly field is translated with `@JSGetter` + @JSGetter var id: Double + + /// A read-writable field is translated with `@JSGetter` and `@JSSetter` + @JSGetter var message: String + @JSSetter func setMessage(_ newValue: String) throws(JSException) + + /// A constructor + @JSFunction init(id: String, name: String) throws(JSException) + /// A method + @JSFunction func greet() throws(JSException) -> String + @JSFunction static func createDefault(_ greetingId: Double, _ locale: String) throws(JSException) -> String + } + ``` + } +} + +### Interfaces + +An interface becomes a Swift struct with `@JSClass`. No constructor. Properties and methods are bridged the same way as for classes (`@JSGetter`, `@JSSetter`, `@JSFunction`). Instances are obtained from other calls (e.g. a function that returns the interface type). + +@Row { + @Column { + ```typescript + // TypeScript (bridge-js.d.ts) + export interface HTMLElement { + readonly innerText: string; + className: string; + + appendChild(child: HTMLElement): void; + } + ``` + } + @Column { + ```swift + // Generated Swift + @JSClass struct HTMLElement { + @JSGetter var innerText: String + @JSGetter var className: String + @JSSetter func setClassName(_ newValue: String) throws(JSException) + @JSFunction func appendChild(_ child: HTMLElement) throws(JSException) + } + ``` + } +} + +### Type aliases + +Type aliases are resolved when generating Swift; the resolved shape is emitted, not a separate alias type. + +- **Primitive alias** (e.g. `type UserId = string`): Replaced by the underlying Swift type (e.g. `String`) everywhere it is used. +- **Object-shaped alias** (e.g. `type User = { id: string; name: string }`): The generator emits a named Swift struct with that name and the corresponding `@JSClass` / getters / setters. No constructor; use is the same as for interfaces. + +@Row { + @Column { + ```typescript + // TypeScript (bridge-js.d.ts) + export type UserId = string; + export type User = { + readonly id: UserId; + name: string; + }; + export function getUser(): User; + ``` + } + @Column { + ```swift + // Generated Swift (UserId inlined as String) + @JSClass struct User { + @JSGetter var id: String + @JSGetter var name: String + @JSSetter func setName(_ newValue: String) throws(JSException) + } + @JSFunction func getUser() throws(JSException) -> User + ``` + } +} + +### String enums + +TypeScript enums with string literal values become Swift enums with `String` raw value and the appropriate BridgeJS protocol conformances. Usable as parameter and return types. + +@Row { + @Column { + ```typescript + // TypeScript (bridge-js.d.ts) + export enum Theme { + light = "light", + dark = "dark", + } + + + export function setTheme(theme: Theme): void; + export function getTheme(): Theme; + ``` + } + @Column { + ```swift + // Generated Swift + enum Theme: String { + case light = "light" + case dark = "dark" + } + extension Theme: _BridgedSwiftEnumNoPayload, _BridgedSwiftRawValueEnum {} + + @JSFunction func setTheme(_ theme: Theme) throws(JSException) -> Void + @JSFunction func getTheme() throws(JSException) -> Theme + ``` + } +} + +## Type mappings + +The following mappings apply to function parameters, return types, and class/interface properties. + +### Primitives + +| TypeScript | Swift | +|------------|-------| +| `number` | `Double` | +| `string` | `String` | +| `boolean` | `Bool` | + +These appear in the function and class examples above. + +### Arrays + +`T[]` and `Array` → `[T]`. + +@Row { + @Column { + ```typescript + // TypeScript (bridge-js.d.ts) + export function sumAll(values: number[]): number; + export function getScores(): number[]; + export function getLabels(): string[]; + export function normalize(values: Array): Array; + ``` + } + @Column { + ```swift + // Generated Swift + @JSFunction func sumAll(_ values: [Double]) throws(JSException) -> Double + @JSFunction func getScores() throws(JSException) -> [Double] + @JSFunction func getLabels() throws(JSException) -> [String] + @JSFunction func normalize(_ values: [Double]) throws(JSException) -> [Double] + ``` + } +} + +### Optional and undefined + +- **`T | null`** → Swift `Optional` (e.g. `Optional`, `Optional`, `Optional`). +- **`T | undefined`** or optional parameters → Swift `JSUndefinedOr`. + +@Row { + @Column { + ```typescript + // TypeScript (bridge-js.d.ts) + export function parseCount(value: number | null): number | null; + export function parseLimit(value: number | undefined): number | undefined; + export function scale(factor: number, offset: number | null): number; + + export interface Payload {} + export class RequestOptions { + body: Payload | null; + + headers: Payload | undefined; + + } + ``` + } + @Column { + ```swift + // Generated Swift + @JSFunction func parseCount(_ value: Optional) throws(JSException) -> Optional + @JSFunction func parseLimit(_ value: JSUndefinedOr) throws(JSException) -> JSUndefinedOr + @JSFunction func scale(_ factor: Double, _ offset: Optional) throws(JSException) -> Double + + @JSClass struct Payload {} + @JSClass struct RequestOptions { + @JSGetter var body: Optional + @JSSetter func setBody(_ value: Optional) throws(JSException) + @JSGetter var headers: JSUndefinedOr + @JSSetter func setHeaders(_ value: JSUndefinedOr) throws(JSException) + } + ``` + } +} + +### Records (dictionaries) + +`Record` → Swift `[String: V]`. Supported value types: primitives, arrays, nested records, optional object types. Keys other than `string` (e.g. `Record`) are unsupported; such parameters become `JSObject`. + +@Row { + @Column { + ```typescript + // TypeScript (bridge-js.d.ts) + export function applyScores(scores: Record): void; + export function getMetadata(): Record; + export function getMatrix(value: Record>): Record>; + export function getSeries(values: Record): Record; + export function lookupByIndex(values: Record): void; + ``` + } + @Column { + ```swift + // Generated Swift + @JSFunction func applyScores(_ scores: [String: Double]) throws(JSException) -> Void + @JSFunction func getMetadata() throws(JSException) -> [String: String] + @JSFunction func getMatrix(_ value: [String: [String: Double]]) throws(JSException) -> [String: [String: Double]] + @JSFunction func getSeries(_ values: [String: [Double]]) throws(JSException) -> [String: [Double]] + @JSFunction func lookupByIndex(_ values: JSObject) throws(JSException) -> Void + ``` + } +} + +### Unbridged types (JSValue, JSObject) + +Types that cannot be expressed in the bridge (e.g. `any`, unsupported generics, or `Record`) are emitted as `JSValue` or `JSObject`. + +@Row { + @Column { + ```typescript + // TypeScript (bridge-js.d.ts) + interface Animatable { + animate(keyframes: any, options: any): any; + } + export function getAnimatable(): Animatable; + ``` + } + @Column { + ```swift + // Generated Swift + @JSClass struct Animatable { + @JSFunction func animate(_ keyframes: JSValue, _ options: JSValue) throws(JSException) -> JSValue + } + @JSFunction func getAnimatable() throws(JSException) -> Animatable + ``` + } +} + +## Name mapping + +### Invalid or special property and type names + +When a TypeScript name is not a valid Swift identifier (e.g. contains dashes, spaces, or starts with a number), or is a Swift keyword, the generator emits a Swift-safe name and uses `jsName` so the JavaScript binding remains correct. Classes whose names start with `$` are prefixed with `_` in Swift and get `@JSClass(jsName: "...")`. + +@Row { + @Column { + ```typescript + // TypeScript (bridge-js.d.ts) + interface DOMTokenList { + "data-attrib": number; + + "0": boolean; + + for: string; + + as(): void; + } + export class $jQuery { + "call-plugin"(): void; + } + ``` + } + @Column { + ```swift + // Generated Swift + @JSClass struct DOMTokenList { + @JSGetter(jsName: "data-attrib") var data_attrib: Double + @JSSetter(jsName: "data-attrib") func setData_attrib(_ value: Double) throws(JSException) + @JSGetter(jsName: "0") var _0: Bool + @JSSetter(jsName: "0") func set_0(_ value: Bool) throws(JSException) + @JSGetter var `for`: String + @JSSetter func setFor(_ value: String) throws(JSException) + @JSFunction func `as`() throws(JSException) -> Void + } + @JSClass(jsName: "$jQuery") struct _jQuery { + @JSFunction(jsName: "call-plugin") func call_plugin() throws(JSException) -> Void + } + ``` + } +} + +## Limitations + +- No first-class support for async/Promise-returning functions;. +- No generic type parameter can appear on a bridged function signature. \ No newline at end of file diff --git a/Sources/JavaScriptKit/Documentation.docc/Articles/BridgeJS/Importing-JavaScript-into-Swift.md b/Sources/JavaScriptKit/Documentation.docc/Articles/BridgeJS/Importing-JavaScript-into-Swift.md new file mode 100644 index 000000000..14fccbfc1 --- /dev/null +++ b/Sources/JavaScriptKit/Documentation.docc/Articles/BridgeJS/Importing-JavaScript-into-Swift.md @@ -0,0 +1,98 @@ +# Importing JavaScript into Swift + +Learn how to make JavaScript APIs callable from your Swift code using macro-annotated bindings. + +## Overview + +> 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/JavaScript/TypeScript sides using the [BridgeJS Playground](https://swiftwasm.org/JavaScriptKit/PlayBridgeJS/). + +You can import JavaScript APIs into Swift in two ways: + +1. **Annotate Swift with macros** - Use `@JSFunction`, `@JSClass`, `@JSGetter`, and `@JSSetter` to declare bindings directly in Swift. No TypeScript required. Prefer this to get started. +2. **Generate bindings from TypeScript** - Use a `bridge-js.d.ts` file; the BridgeJS plugin generates the same macro-annotated Swift. See when you have existing `.d.ts` definitions or many APIs to bind. + +This guide covers the macro-based path. + +## How to import JavaScript APIs (macro path) + +### Step 1: Configure your package + +Add the BridgeJS plugin and enable the Extern feature as described in . + +### Step 2: Declare bindings in Swift with macros + +Create Swift declarations that mirror the JavaScript API you want to call. + +You can bring JavaScript into Swift in two ways: + +- **Inject at initialization**: Declare in Swift and supply the implementation in `getImports()` (e.g. a `today()` function). +- **Import from `globalThis`**: For APIs on the JavaScript global object (e.g. `console`, `document`), use `@JSGetter(from: .global)` so they are read from `globalThis` and you don't pass them in `getImports()`. + +```swift +import JavaScriptKit + +@JSFunction func today() -> String + +@JSClass struct JSConsole { + @JSFunction func log(_ message: String) throws(JSException) +} +@JSGetter(from: .global) var console: JSConsole + +@JSClass struct Document { + @JSFunction func getElementById(_ id: String) throws(JSException) -> HTMLElement + @JSFunction func createElement(_ tagName: String) throws(JSException) -> HTMLElement +} +@JSGetter(from: .global) var document: Document + +@JSClass struct HTMLElement { + @JSGetter var innerText: String + @JSSetter func setInnerText(_ newValue: String) throws(JSException) + @JSFunction func appendChild(_ child: HTMLElement) throws(JSException) +} +``` + +For full details, see , , and . + +### Step 3: Use the bindings in Swift + +Call the bound APIs from Swift: + +```swift +import JavaScriptKit + +@JS func run() { + try console.log("Hello from Swift! (\(today()))") + + let button = document.createElement("button")! + try button.setInnerText("Click Me") + let container = document.getElementById("app")! + container.appendChild(button) +} +``` + +### Step 5: Inject JavaScript implementations (if needed) + +Bindings imported from globalThis (e.g. `console`, `document`) are read at runtime and do not go in `getImports()`. Only APIs you inject at initialization must be supplied there: + +```javascript +// index.js +import { init } from "./.build/plugins/PackageToJS/outputs/Package/index.js"; + +const { exports } = await init({ + getImports() { + return { + today: () => new Date().toString() + }; + } +}); + +exports.run(); +``` + +## Topics + +- +- +- \ No newline at end of file diff --git a/Sources/JavaScriptKit/Documentation.docc/Articles/BridgeJS/Importing-JavaScript/Importing-JS-Class.md b/Sources/JavaScriptKit/Documentation.docc/Articles/BridgeJS/Importing-JavaScript/Importing-JS-Class.md new file mode 100644 index 000000000..4302e0e49 --- /dev/null +++ b/Sources/JavaScriptKit/Documentation.docc/Articles/BridgeJS/Importing-JavaScript/Importing-JS-Class.md @@ -0,0 +1,54 @@ +# Importing a JavaScript class or object into Swift + +This guide shows how to bind a JavaScript class (or object shape) so you can construct and use it from Swift with `@JSClass`. + +## Steps + +### 1. Declare a Swift struct with `@JSClass` + +Add a struct that mirrors the JavaScript class. Use `@JSFunction` for the initializer and for methods, and `@JSGetter` / `@JSSetter` for properties. Property setters are exposed as functions (e.g. `setMessage(_:)`) because Swift property setters cannot `throw`. + +```swift +import JavaScriptKit + +@JSClass struct Greeter { + @JSFunction init(id: String, name: String) throws(JSException) + + @JSGetter var id: String + @JSGetter var message: String + @JSSetter func setMessage(_ newValue: String) throws(JSException) + + @JSFunction func greet() throws(JSException) -> String +} +``` + +If the class is on `globalThis`, add `from: .global` to `@JSClass` and omit the type from `getImports()` in the next step. + +### 2. Wire the JavaScript side + +**If you chose injection:** Implement the class in JavaScript and pass it in `getImports()`. + +```javascript +// index.js +import { init } from "./.build/plugins/PackageToJS/outputs/Package/index.js"; + +class Greeter { + constructor(id, name) { /* ... */ } + get id() { /* ... */ } + get message() { /* ... */ } + set message(value) { /* ... */ } + greet() { /* ... */ } +} + +const { exports } = await init({ + getImports() { + return { Greeter }; + } +}); +``` + +**If you chose global:** Do not pass the class in `getImports()`; the runtime will resolve it from `globalThis`. + +## Macro options + +For optional parameters (`jsName`, `from`, etc.), see the API reference: ``JSClass(jsName:from:)``, ``JSFunction(jsName:from:)``, ``JSGetter(jsName:from:)``, and ``JSSetter(jsName:from:)``. diff --git a/Sources/JavaScriptKit/Documentation.docc/Articles/BridgeJS/Importing-JavaScript/Importing-JS-Function.md b/Sources/JavaScriptKit/Documentation.docc/Articles/BridgeJS/Importing-JavaScript/Importing-JS-Function.md new file mode 100644 index 000000000..64475acfa --- /dev/null +++ b/Sources/JavaScriptKit/Documentation.docc/Articles/BridgeJS/Importing-JavaScript/Importing-JS-Function.md @@ -0,0 +1,51 @@ +# Importing a JavaScript function into Swift + +This guide shows how to bind a JavaScript function so it is callable from Swift using `@JSFunction`. + +## Steps + +### 1. Declare the function in Swift with `@JSFunction` + +Match the JavaScript name and signature. Use Swift types that bridge to the JS types you need (see ). + +```swift +import JavaScriptKit + +@JSFunction func add(_ a: Double, _ b: Double) throws(JSException) -> Double +@JSFunction func setTitle(_ title: String) throws(JSException) +``` + +To bind a function that lives on the JavaScript global object (e.g. `parseInt`, `setTimeout`), add `from: .global`. Use `jsName` when the Swift name differs from the JavaScript name - see the ``JSFunction(jsName:from:)`` API reference for options. + +### 2. Provide the implementation at initialization + +Return the corresponding function(s) in the object passed to `getImports()` when initializing the WebAssembly module. + +```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 }, + }; + } +}); +``` + +If you used `from: .global`, do not pass the function in `getImports()`; the runtime resolves it from `globalThis`. + +### 3. Handle errors + +Bound functions are `throws(JSException)`. Call them with `try` or `try?`; they throw when the JavaScript implementation throws. + +## Supported features + +| Feature | Status | +|:--|:--| +| Primitive parameter/result types (e.g. `Double`, `Bool`) | ✅ | +| `String` parameter/result type | ✅ | +| Async function | ❌ | +| Generics | ❌ | diff --git a/Sources/JavaScriptKit/Documentation.docc/Articles/BridgeJS/Importing-JavaScript/Importing-JS-Variable.md b/Sources/JavaScriptKit/Documentation.docc/Articles/BridgeJS/Importing-JavaScript/Importing-JS-Variable.md new file mode 100644 index 000000000..044ebbe52 --- /dev/null +++ b/Sources/JavaScriptKit/Documentation.docc/Articles/BridgeJS/Importing-JavaScript/Importing-JS-Variable.md @@ -0,0 +1,59 @@ +# Importing a global JavaScript variable into Swift + +This guide shows how to bind a JavaScript global variable (or any property you provide at initialization) so you can read and optionally write it from Swift using `@JSGetter` and `@JSSetter`. + +## Steps + +### 1. Declare the variable in Swift with `@JSGetter` + +Use a type that bridges to the JavaScript value (see ). For object-shaped values, use a struct conforming to the bridged type (e.g. a `@JSClass` struct). + +```swift +import JavaScriptKit + +@JSGetter(from: .global) var document: Document +@JSGetter(from: .global) var myConfig: String +``` + +To bind a variable that is not on `globalThis`, omit `from: .global` and supply the value in `getImports()` in the next step. Use `jsName` when the Swift name differs from the JavaScript property name - see the ``JSGetter(jsName:from:)`` API reference. + +### 2. Add a setter for writable variables (optional) + +If the JavaScript property is writable and you need to set it from Swift, add a corresponding `@JSSetter` function. Property setters are exposed as functions (e.g. `setMyConfig(_:)`) because Swift property setters cannot `throw`. + +```swift +@JSGetter(from: .global) var myConfig: String +@JSSetter(from: .global) func setMyConfig(_ newValue: String) throws(JSException) +``` + +### 3. Provide the value at initialization (injected only) + +If you did **not** use `from: .global`, pass the value in the object returned by `getImports()` when initializing the WebAssembly module. + +```javascript +// index.js +import { init } from "./.build/plugins/PackageToJS/outputs/Package/index.js"; + +const { exports } = await init({ + getImports() { + return { + myConfig: "production", + }; + } +}); +``` + +If you used `from: .global`, do not pass the variable in `getImports()`; the runtime reads it from `globalThis`. + +## Supported features + +| Feature | Status | +|:--|:--| +| Read-only global (e.g. `document`, `console`) | ✅ | +| Writable global | ✅ (`@JSSetter`) | +| Injected variable (via `getImports()`) | ✅ | + +## See also + +- ``JSGetter(jsName:from:)`` +- ``JSSetter(jsName:from:)`` 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 deleted file mode 100644 index b091f714b..000000000 --- a/Sources/JavaScriptKit/Documentation.docc/Articles/BridgeJS/Importing-TypeScript-into-Swift.md +++ /dev/null @@ -1,185 +0,0 @@ -# Importing TypeScript into Swift - -Learn how to leverage TypeScript definitions to create type-safe bindings for JavaScript APIs in your Swift code. - -## Overview - -> 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: - -- **Type Safety**: Catch errors at compile-time rather than runtime -- **IDE Support**: Get autocompletion and documentation in your Swift editor -- **Performance**: Eliminating dynamism allows us to optimize the glue code - -If you prefer keeping your project simple, you can continue using `@dynamicMemberLookup`-based APIs. - -## Getting Started - -### Step 1: Configure Your Package - -First, add the BridgeJS plugin to your Swift package by modifying your `Package.swift` file: - -```swift -// swift-tools-version:6.0 - -import PackageDescription - -let package = Package( - name: "MyApp", - dependencies: [ - .package(url: "https://github.com/swiftwasm/JavaScriptKit.git", branch: "main") - ], - targets: [ - .executableTarget( - name: "MyApp", - dependencies: ["JavaScriptKit"], - swiftSettings: [ - // This is required because the generated code depends on @_extern(wasm) - .enableExperimentalFeature("Extern") - ], - plugins: [ - // Add build plugin for processing @JS and generate Swift glue code - .plugin(name: "BridgeJS", package: "JavaScriptKit") - ] - ) - ] -) -``` - -### Step 2: Create TypeScript Definitions - -Create a file named `bridge-js.d.ts` in your target source directory (e.g. `Sources//bridge-js.d.ts`). This file defines the JavaScript APIs you want to use in Swift: - -```typescript -// Simple function -export function consoleLog(message: string): void; - -// Define a subset of DOM API you want to use -interface Document { - // Properties - title: string; - readonly body: HTMLElement; - - // Methods - getElementById(id: string): HTMLElement; - createElement(tagName: string): HTMLElement; -} - -// You can use type-level operations like `Pick` to reuse -// type definitions provided by `lib.dom.d.ts`. -interface HTMLElement extends Pick { - appendChild(child: HTMLElement): void; - // TODO: Function types on function signatures are not supported yet. - // addEventListener(event: string, handler: (event: any) => void): void; -} - -// Provide access to `document` -export function getDocument(): Document; -``` - -BridgeJS will generate Swift code that matches these TypeScript declarations. For example: - -```swift -func consoleLog(message: String) - -struct Document { - var title: String { get set } - var body: HTMLElement { get } - - func getElementById(_ id: String) -> HTMLElement - func createElement(_ tagName: String) -> HTMLElement -} - -struct HTMLElement { - var innerText: String { get set } - var className: String { get set } - - func appendChild(_ child: HTMLElement) -} - -func getDocument() -> Document -``` - -### Step 3: Build Your Package - -Build your package with the following command: - -```bash -swift package --swift-sdk $SWIFT_SDK_ID js -``` - -This command: -1. Processes your TypeScript definition files -2. Generates corresponding Swift bindings -3. Compiles your Swift code to WebAssembly -4. Produces JavaScript glue code in `.build/plugins/PackageToJS/outputs/` - -> Note: For larger projects, you may want to generate the BridgeJS code ahead of time to improve build performance. See for more information. - -### Step 4: Use the Generated Swift Bindings - -The BridgeJS plugin automatically generates Swift bindings that match your TypeScript definitions. You can now use these APIs directly in your Swift code: - -```swift -import JavaScriptKit - -@JS func run() { - // Simple function call - consoleLog("Hello from Swift!") - - // Get `document` - let document = getDocument() - - // Property access - document.title = "My Swift App" - - // Method calls - let button = document.createElement("button") - button.innerText = "Click Me" - - // TODO: Function types on function signatures are not supported yet. - // buttion.addEventListener("click") { _ in - // print("On click!") - // } - - // DOM manipulation - let container = document.getElementById("app") - container.appendChild(button) -} -``` - -### Step 5: Inject JavaScript Implementations - -The final step is to provide the actual JavaScript implementations for the TypeScript declarations you defined. You need to create a JavaScript file that initializes your WebAssembly module with the appropriate implementations: - -```javascript -// index.js -import { init } from "./.build/plugins/PackageToJS/outputs/Package/index.js"; - -// Initialize the WebAssembly module with JavaScript implementations -const { exports } = await init({ - getImports() { - return { - consoleLog: (message) => { - console.log(message); - }, - getDocument: () => document, - } - } -}); - -// 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 deleted file mode 100644 index e04bb9e75..000000000 --- a/Sources/JavaScriptKit/Documentation.docc/Articles/BridgeJS/Importing-TypeScript/Importing-TS-Class.md +++ /dev/null @@ -1,64 +0,0 @@ -# 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 deleted file mode 100644 index 060ac390f..000000000 --- a/Sources/JavaScriptKit/Documentation.docc/Articles/BridgeJS/Importing-TypeScript/Importing-TS-Function.md +++ /dev/null @@ -1,60 +0,0 @@ -# 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 deleted file mode 100644 index 0e0aed0de..000000000 --- a/Sources/JavaScriptKit/Documentation.docc/Articles/BridgeJS/Importing-TypeScript/Importing-TS-Interface.md +++ /dev/null @@ -1,34 +0,0 @@ -# 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 deleted file mode 100644 index b5242ee33..000000000 --- a/Sources/JavaScriptKit/Documentation.docc/Articles/BridgeJS/Importing-TypeScript/Importing-TS-TypeAlias.md +++ /dev/null @@ -1,47 +0,0 @@ -# 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/Introducing-BridgeJS.md b/Sources/JavaScriptKit/Documentation.docc/Articles/BridgeJS/Introducing-BridgeJS.md new file mode 100644 index 000000000..da4ae0a06 --- /dev/null +++ b/Sources/JavaScriptKit/Documentation.docc/Articles/BridgeJS/Introducing-BridgeJS.md @@ -0,0 +1,32 @@ +# Introducing BridgeJS + +Faster, easier Swift–JavaScript bridging for WebAssembly. + +## Overview + +**BridgeJS** is a layer underneath JavaScriptKit that makes Swift–JavaScript interop **faster and easier**: you declare the shape of the API in Swift (or in TypeScript, which generates Swift), and the tool generates glue code. + +Benefits over the dynamic `JSObject` / `JSValue` APIs include: + +- **Performance** - Generated code is specialized per interface, so crossing the bridge typically costs less than using generic dynamic APIs. +- **Type safety** - Mistakes are caught at compile time instead of at runtime. +- **Easier integration** - Declarative annotations and optional TypeScript input replace manual boilerplate (closures, serializers, cached strings). + +You can still use the dynamic APIs when you need them; BridgeJS is an additional option for boundaries where you want strong typing and better performance. + +## Two directions + +BridgeJS supports both directions of the bridge: + +1. **Export Swift to JavaScript** - Expose Swift functions, classes, and types to JavaScript using the ``JS(namespace:enumStyle:)`` macro. JavaScript can then call into your Swift code. See . +2. **Import JavaScript into Swift** - Make JavaScript APIs (functions, classes, globals like `document` or `console`) callable from Swift with macros such as ``JSClass(jsName:from:)``. Start with . You can also generate the same bindings from a TypeScript file; see . + +Many apps use both: import DOM or host APIs into Swift, and export an entry point or callbacks to JavaScript. + +## Getting started + +- To **call Swift from JavaScript**: +- To **call JavaScript from Swift**: +- To **generate bindings from TypeScript**: + +All require the same package and build setup; see for configuration. diff --git a/Sources/JavaScriptKit/Documentation.docc/Articles/BridgeJS/Setting-up-BridgeJS.md b/Sources/JavaScriptKit/Documentation.docc/Articles/BridgeJS/Setting-up-BridgeJS.md new file mode 100644 index 000000000..99a3d5b1c --- /dev/null +++ b/Sources/JavaScriptKit/Documentation.docc/Articles/BridgeJS/Setting-up-BridgeJS.md @@ -0,0 +1,60 @@ +# Setting up BridgeJS + +Package and build setup required for all BridgeJS workflows + +## Overview + +BridgeJS requires the JavaScriptKit package dependency, the experimental `Extern` Swift feature, and the BridgeJS build plugin. The same configuration applies whether you are exporting Swift to JavaScript, importing JavaScript into Swift with macros, and generating bindings from a TypeScript file. + +## Package.swift + +Add the JavaScriptKit dependency, enable the Extern feature on the target that uses BridgeJS, and register the BridgeJS plugin: + +```swift +// swift-tools-version:6.0 + +import PackageDescription + +let package = Package( + name: "MyApp", + dependencies: [ + .package(url: "https://github.com/swiftwasm/JavaScriptKit.git", branch: "main") + ], + targets: [ + .executableTarget( + name: "MyApp", + dependencies: ["JavaScriptKit"], + swiftSettings: [ + .enableExperimentalFeature("Extern") + ], + plugins: [ + .plugin(name: "BridgeJS", package: "JavaScriptKit") + ] + ) + ] +) +``` + +- **Dependency**: The target must depend on `"JavaScriptKit"`. +- **Extern feature**: Required because the generated bridge code uses `@_extern(wasm)`. +- **BridgeJS plugin**: Processes macro annotations to generate glue code and, [when present, `bridge-js.d.ts`; generates JS binding Swift code](). + +## Build command + +Build the package for the JavaScript/WebAssembly SDK: + +```bash +swift package --swift-sdk $SWIFT_SDK_ID js +``` + +This command: + +1. Runs the BridgeJS plugin +2. Compiles Swift to WebAssembly and generates JavaScript glue and TypeScript type definitions. +3. Writes output to `.build/plugins/PackageToJS/outputs/Package/`. + +For package layout and how to consume the output from JavaScript, see . + +## Larger projects + +The build plugin runs on every build. For larger projects, generating bridge code ahead of time can improve build performance. See . diff --git a/Sources/JavaScriptKit/Documentation.docc/Articles/BridgeJS/Supported-Types.md b/Sources/JavaScriptKit/Documentation.docc/Articles/BridgeJS/Supported-Types.md index c872c0dda..81a135af3 100644 --- a/Sources/JavaScriptKit/Documentation.docc/Articles/BridgeJS/Supported-Types.md +++ b/Sources/JavaScriptKit/Documentation.docc/Articles/BridgeJS/Supported-Types.md @@ -1,18 +1,22 @@ -# TypeScript and Swift Type Mapping +# Supported Types -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. +Swift types and their JavaScript/TypeScript equivalents at the BridgeJS boundary. -## Type mapping +## Swift and JavaScript type mapping -| TypeScript type | Swift type | -|:--|:--| -| `number` | `Double` | -| `string` | `String` | -| `boolean` | `Bool` | -| `T[]` | `[T]` | -| TODO | `Dictionary` | -| `T \| undefined` | TODO | -| `T \| null` | `Optional` | -| `Promise` | `JSPromise` | -| `any` / `unknown` / `object` | `JSObject` | -| Other types | `JSObject` | +| Swift type | JavaScript | TypeScript | +|:--|:--|:--| +| `Int`, `UInt`, `Double`, `Float` | number | `number` | +| `String` | string | `string` | +| `Bool` | boolean | `boolean` | +| `Void` | - | `void` | +| `[T]` | array | `T[]` | +| `[String: T]` | object | `Record` | +| `Optional` | `null` or `T` | `T \| null` | +| ``JSUndefinedOr`` `` | `undefined` or `T` | `T \| undefined` | +| ``JSObject`` | object | `object` | +| ``JSValue`` | any | `any` | + +## See Also + +- diff --git a/Sources/JavaScriptKit/Documentation.docc/Articles/FAQ.md b/Sources/JavaScriptKit/Documentation.docc/Articles/FAQ.md new file mode 100644 index 000000000..aabd2f008 --- /dev/null +++ b/Sources/JavaScriptKit/Documentation.docc/Articles/FAQ.md @@ -0,0 +1,28 @@ +# FAQ + +Common questions about JavaScriptKit and BridgeJS. + +## Why does the initialization need to be async? Why must I await it? + +Initialization is asynchronous because the runtime must **fetch and instantiate the WebAssembly module** before your Swift code can run. That means: + +1. **Fetching** the `.wasm` binary (e.g. over the network or from disk). +2. **Instantiating** it with `WebAssembly.instantiate()` (or `instantiateStreaming()`), which compiles the module and allocates the linear memory and table. + +Until that completes, there is no WebAssembly instance to call into, so the entry point that sets up the Swift–JavaScript bridge has to be `async` and must be `await`ed from the JavaScript host (e.g. in your `index.js` or HTML script). This matches the standard WebAssembly JavaScript API, which is promise-based. + +## Why does every imported JavaScript interface via BridgeJS declare `throws(JSException)`? + +This is a conservative, safety-oriented design. Calling into JavaScript can throw at any time. If the Swift call site does not expect errors (i.e. the call is not in a `throws` context and the compiler does not force handling), then when JavaScript throws: + +- Control leaves the WebAssembly call frames **without running function epilogues**. +- Even a `do { } catch { }` in Swift does not run its `catch` block in the way you might expect, because unwinding crosses the WASM/JS boundary. +- **`defer` blocks are not executed** in those frames. + +That can lead to **inconsistent memory state** and **resource leaks**. To avoid that, every call that might invoke JavaScript is modeled as throwing: all imported JS functions, property accessors, and related operations are marked with `throws(JSException)`. That way: + +- The compiler requires you to handle or propagate errors. +- You explicitly decide how to react to JavaScript exceptions (e.g. `try`/`catch`, or propagating `throws`). +- Epilogues and cleanup run in a well-defined way when you handle the error in Swift. + +So the “everything throws” rule is there to keep behavior predictable and safe when crossing the Swift–JavaScript boundary. diff --git a/Sources/JavaScriptKit/Documentation.docc/Documentation.md b/Sources/JavaScriptKit/Documentation.docc/Documentation.md index 51ee62539..75fd24d44 100644 --- a/Sources/JavaScriptKit/Documentation.docc/Documentation.md +++ b/Sources/JavaScriptKit/Documentation.docc/Documentation.md @@ -59,14 +59,25 @@ Check out the [examples](https://github.com/swiftwasm/JavaScriptKit/tree/main/Ex - - - +- ### BridgeJS -- +- +- - +- +- +- - - - +- +- ``JS(namespace:enumStyle:)`` +- ``JSFunction(jsName:from:)`` +- ``JSClass(jsName:from:)`` +- ``JSGetter(jsName:from:)`` +- ``JSSetter(jsName:from:)`` ### Core APIs diff --git a/Sources/JavaScriptKit/Macros.swift b/Sources/JavaScriptKit/Macros.swift index 8decf1a76..67b3488bf 100644 --- a/Sources/JavaScriptKit/Macros.swift +++ b/Sources/JavaScriptKit/Macros.swift @@ -125,7 +125,7 @@ public macro JS(namespace: String? = nil, enumStyle: JSEnumStyle = .const) = Bui /// Example: /// /// ```swift -/// @_spi(Experimental) import JavaScriptKit +/// import JavaScriptKit /// /// @JSGetter var count: Int /// @@ -137,7 +137,6 @@ public macro JS(namespace: String? = nil, enumStyle: JSEnumStyle = .const) = Bui /// - Parameter from: Selects where the property is read from. /// Use `.global` to read from `globalThis` (e.g. `console`, `document`). @attached(accessor) -@_spi(Experimental) public macro JSGetter(jsName: String? = nil, from: JSImportFrom? = nil) = #externalMacro(module: "BridgeJSMacros", type: "JSGetterMacro") @@ -151,12 +150,11 @@ public macro JSGetter(jsName: String? = nil, from: JSImportFrom? = nil) = /// Example: /// /// ```swift -/// @_spi(Experimental) import JavaScriptKit +/// import JavaScriptKit /// /// @JSSetter func setName(_ value: String) throws (JSException) /// ``` @attached(body) -@_spi(Experimental) public macro JSSetter(jsName: String? = nil, from: JSImportFrom? = nil) = #externalMacro(module: "BridgeJSMacros", type: "JSSetterMacro") @@ -167,7 +165,7 @@ public macro JSSetter(jsName: String? = nil, from: JSImportFrom? = nil) = /// Example: /// /// ```swift -/// @_spi(Experimental) import JavaScriptKit +/// import JavaScriptKit /// /// @JSFunction func greet() throws (JSException) -> String /// @JSFunction init(_ name: String) throws (JSException) @@ -178,7 +176,6 @@ public macro JSSetter(jsName: String? = nil, from: JSImportFrom? = nil) = /// - Parameter from: Selects where the function is looked up from. /// Use `.global` to call a function on `globalThis` (e.g. `setTimeout`). @attached(body) -@_spi(Experimental) public macro JSFunction(jsName: String? = nil, from: JSImportFrom? = nil) = #externalMacro(module: "BridgeJSMacros", type: "JSFunctionMacro") @@ -189,7 +186,7 @@ public macro JSFunction(jsName: String? = nil, from: JSImportFrom? = nil) = /// Example: /// /// ```swift -/// @_spi(Experimental) import JavaScriptKit +/// import JavaScriptKit /// /// @JSClass /// struct JsGreeter { @@ -204,6 +201,5 @@ public macro JSFunction(jsName: String? = nil, from: JSImportFrom? = nil) = /// Use `.global` to construct globals like `WebSocket` via `globalThis`. @attached(member, names: named(jsObject), named(init(unsafelyWrapping:))) @attached(extension, conformances: _JSBridgedClass) -@_spi(Experimental) public macro JSClass(jsName: String? = nil, from: JSImportFrom? = nil) = #externalMacro(module: "BridgeJSMacros", type: "JSClassMacro") diff --git a/Tests/BridgeJSRuntimeTests/ClosureSupportTests.swift b/Tests/BridgeJSRuntimeTests/ClosureSupportTests.swift index 320fec526..8078ed777 100644 --- a/Tests/BridgeJSRuntimeTests/ClosureSupportTests.swift +++ b/Tests/BridgeJSRuntimeTests/ClosureSupportTests.swift @@ -1,5 +1,5 @@ import XCTest -@_spi(Experimental) import JavaScriptKit +import JavaScriptKit @JSClass struct ClosureSupportImports { diff --git a/Tests/BridgeJSRuntimeTests/DictionaryTests.swift b/Tests/BridgeJSRuntimeTests/DictionaryTests.swift index ba702205d..95f2706e6 100644 --- a/Tests/BridgeJSRuntimeTests/DictionaryTests.swift +++ b/Tests/BridgeJSRuntimeTests/DictionaryTests.swift @@ -1,4 +1,4 @@ -@_spi(Experimental) @_spi(BridgeJS) import JavaScriptKit +@_spi(BridgeJS) import JavaScriptKit import XCTest final class DictionaryTests: XCTestCase { diff --git a/Tests/BridgeJSRuntimeTests/ExportAPITests.swift b/Tests/BridgeJSRuntimeTests/ExportAPITests.swift index 7f9a20586..97c2d5fbf 100644 --- a/Tests/BridgeJSRuntimeTests/ExportAPITests.swift +++ b/Tests/BridgeJSRuntimeTests/ExportAPITests.swift @@ -1,5 +1,5 @@ import XCTest -@_spi(Experimental) import JavaScriptKit +import JavaScriptKit import JavaScriptEventLoop @_extern(wasm, module: "BridgeJSRuntimeTests", name: "runJsWorks") diff --git a/Tests/BridgeJSRuntimeTests/Generated/BridgeJS.Macros.swift b/Tests/BridgeJSRuntimeTests/Generated/BridgeJS.Macros.swift index 9f500f4a4..c63a421e0 100644 --- a/Tests/BridgeJSRuntimeTests/Generated/BridgeJS.Macros.swift +++ b/Tests/BridgeJSRuntimeTests/Generated/BridgeJS.Macros.swift @@ -4,7 +4,7 @@ // To update this file, just rebuild your project or run // `swift package bridge-js`. -@_spi(Experimental) @_spi(BridgeJS) import JavaScriptKit +@_spi(BridgeJS) import JavaScriptKit @JSFunction func jsRoundTripVoid() throws(JSException) -> Void diff --git a/Tests/BridgeJSRuntimeTests/ImportArrayAPIs.swift b/Tests/BridgeJSRuntimeTests/ImportArrayAPIs.swift index 3f33de86a..104304d5b 100644 --- a/Tests/BridgeJSRuntimeTests/ImportArrayAPIs.swift +++ b/Tests/BridgeJSRuntimeTests/ImportArrayAPIs.swift @@ -1,4 +1,4 @@ -@_spi(Experimental) @_spi(BridgeJS) import JavaScriptKit +@_spi(BridgeJS) import JavaScriptKit @JSFunction func jsRoundTripIntArray(_ items: [Int]) throws(JSException) -> [Int] @JSFunction func jsArrayLength(_ items: [Int]) throws(JSException) -> Int diff --git a/Tests/BridgeJSRuntimeTests/ImportStructAPIs.swift b/Tests/BridgeJSRuntimeTests/ImportStructAPIs.swift index 82908701c..41929772e 100644 --- a/Tests/BridgeJSRuntimeTests/ImportStructAPIs.swift +++ b/Tests/BridgeJSRuntimeTests/ImportStructAPIs.swift @@ -1,4 +1,4 @@ -@_spi(Experimental) import JavaScriptKit +import JavaScriptKit @JS struct Point { diff --git a/Tests/BridgeJSRuntimeTests/OptionalSupportTests.swift b/Tests/BridgeJSRuntimeTests/OptionalSupportTests.swift index 454f5011d..998f8b90f 100644 --- a/Tests/BridgeJSRuntimeTests/OptionalSupportTests.swift +++ b/Tests/BridgeJSRuntimeTests/OptionalSupportTests.swift @@ -1,5 +1,5 @@ import XCTest -@_spi(Experimental) import JavaScriptKit +import JavaScriptKit import JavaScriptEventLoop @JSFunction func runJsOptionalSupportTests() throws diff --git a/Tests/BridgeJSRuntimeTests/StructAPIs.swift b/Tests/BridgeJSRuntimeTests/StructAPIs.swift index 38e7b12e6..6c0079dd1 100644 --- a/Tests/BridgeJSRuntimeTests/StructAPIs.swift +++ b/Tests/BridgeJSRuntimeTests/StructAPIs.swift @@ -1,4 +1,4 @@ -@_spi(Experimental) import JavaScriptKit +import JavaScriptKit @JS struct PointerFields { var raw: UnsafeRawPointer diff --git a/Tests/BridgeJSRuntimeTests/SwiftClassSupportTests.swift b/Tests/BridgeJSRuntimeTests/SwiftClassSupportTests.swift index 1a3dd0a4f..7eefbad56 100644 --- a/Tests/BridgeJSRuntimeTests/SwiftClassSupportTests.swift +++ b/Tests/BridgeJSRuntimeTests/SwiftClassSupportTests.swift @@ -1,5 +1,5 @@ import XCTest -@_spi(Experimental) import JavaScriptKit +import JavaScriptKit @JSClass struct SwiftClassSupportImports { @JSFunction static func jsRoundTripGreeter(_ greeter: Greeter) throws(JSException) -> Greeter