JavaScriptKit Documentation

repository·main·Indexed 21 days ago

https://github.com/swiftwasm/javascriptkit

A Swift framework for WebAssembly environments that provides a bridge to interact with JavaScript objects, functions, and the DOM directly from Swift code. The library includes support for @JS types, multi-module architectures, and integration with Node.js and WebWorkers.

Tokens
47.6K
Snippets
131
Records
187
Agent score
75%

What's inside JavaScriptKit

  1. Use PackageToJS to build and package Swift WebAssembly applications

    main

    PackageToJS is a Swift Package Manager (SPM) command plugin designed to simplify compiling Swift code to WebAssembly and generating the required JavaScript bindings. It is particularly useful for SwiftWasm projects that utilize JavaScriptKit to facilitate interaction between Swift and JavaScript environments.

    Key capabilities include:

    • Compiling Swift to WebAssembly files.
    • Generating JavaScript wrappers for the compiled Wasm.
    • Providing a test driver for Swift Testing and XCTest.
    • Producing generated JS files that are compatible with modern JavaScript bundler tools like Vite.
  2. Key Features of JavaScriptKit

    main

    JavaScriptKit provides the following capabilities for SwiftWasm developers:

    • Object Access: Access JavaScript objects and functions directly from Swift.
    • Closures: Create Swift closures that can be invoked from the JavaScript environment.
    • Type Conversion: Convert between Swift and JavaScript data types.
    • Async/Await: Use JavaScript promises seamlessly with Swift's async/await syntax.
    • Multi-threading: Support for working with multi-threading in the context of JavaScript interop.
  3. Understand the JavaScript package output structure

    main

    When you run swift package --swift-sdk $SWIFT_SDK_ID js, the PackageToJS plugin generates a JavaScript package in .build/plugins/PackageToJS/outputs/Package/. This package contains the compiled WebAssembly module, entry points for different environments, and TypeScript definitions.

    Package Structure

    .build/plugins/PackageToJS/outputs/Package/
    ├── ProductName.wasm          # Compiled WebAssembly module
    ├── index.js                  # Main entry point for browser environments
    ├── index.d.ts                # TypeScript type definitions for index.js
    ├── instantiate.js            # Low-level instantiation API
    ├── instantiate.d.ts          # TypeScript type definitions for instantiate.js
    ├── package.json              # npm package metadata
    └── platforms/
        ├── browser.js            # Browser-specific platform setup
        ├── browser.d.ts          # TypeScript definitions for browser.js
        ├── node.js               # Node.js-specific platform setup
        └── node.d.ts             # TypeScript definitions for node.js
  4. What is BridgeJS and when to use it

    main

    BridgeJS is a high-performance, type-safe bridging layer for JavaScriptKit designed for WebAssembly. While JavaScriptKit provides dynamic JSObject and JSValue APIs for interacting with JavaScript, BridgeJS is intended for boundaries where you want to avoid manual boilerplate (like closures and serializers) and improve performance.

    Key Benefits:

    • Performance: Generated code is specialized for specific interfaces, reducing the cost of crossing the Swift-JavaScript bridge compared to generic dynamic APIs.
    • Type Safety: Errors are caught at compile time rather than at runtime.
    • Easier Integration: Uses declarative annotations or TypeScript definitions to generate glue code automatically.
  5. Overview of BridgeJS workflow

    main

    BridgeJS enables interoperability between Swift and JavaScript/TypeScript by allowing you to export Swift APIs to JavaScript and import JavaScript APIs into Swift.

    The core workflow involves three main steps:

    1. Conversion: ts2swift converts TypeScript definition files (.d.ts) into macro-annotated Swift declarations (BridgeJS.Macros.swift).
    2. Generation: bridge-js generate processes Swift source files (for export) and the macro-annotated files (for import) to produce Swift glue code (BridgeJS.swift) and a unified skeleton (JavaScript/BridgeJS.json).
    3. Linking: bridge-js link combines module skeletons (JavaScript/BridgeJS.json) to produce the final JavaScript glue code (bridge-js.js and bridge-js.d.ts).
  6. Export Swift enum static functions to JavaScript

    main

    BridgeJS allows you to export Swift enums containing static functions. When you do this, BridgeJS uses a Values/Tag/Object pattern to ensure you can access both the enum constants and the static methods through a single interface in JavaScript.

    The Pattern Breakdown:

    • Values: A constant object containing the enum cases (e.g., CalculatorValues).
    • Tag: A TypeScript type alias representing the enum values (e.g., CalculatorTag).
    • Object: An intersection type that combines the Values and the static methods (e.g., CalculatorObject).
    • Exports: The final exported object uses the Object type, allowing unified access.

    This pattern enables you to call exports.EnumName.CaseName for constants and exports.EnumName.methodName() for functions.

    @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
        }
    }
  7. Compare JSTypedClosure, JSClosure, and auto-managed closures

    main

    Choosing the right way to pass closures depends on whether you are using BridgeJS-generated glue code or dynamic APIs.

    FeatureJSTypedClosureJSClosureAuto-managed Closures
    API TypeBridgeJS (Typed)Dynamic JSObjectBridgeJS (Plain Swift types)
    SignatureExplicit (e.g. (Int) -> Int)Untyped ([JSValue]) -> JSValueExplicit (e.g. (String) -> String)
    LifetimeManual release()Manual release()Automatic (via FinalizationRegistry)
    Best Use CaseFixed BridgeJS APIsDynamic DOM/JS APIsSimple BridgeJS parameters

    Recommendation: When returning a closure from Swift to JavaScript, use JSTypedClosure with explicit release() management rather than plain closure types to ensure predictable cleanup.

  8. Why `JSObject` subscript is slower than BridgeJS

    main

    The dynamic JSObject.subscript (and similar dynamic property access methods) uses a single code path for all property names and all object shapes. Because every access goes through the same call site with varying keys and receiver shapes, the JavaScript engine's Inline Cache (IC) quickly becomes megamorphic.

    When a site is megamorphic, the engine cannot cache property offsets and must fall back to a slow, generic property lookup. Even if you use CachedJSStrings to cache the property name, you are still using the same generic subscript path, meaning the call site remains megamorphic and continues to pay the slow-path cost.

    BridgeJS avoids this by generating separate access paths for every property or method. This ensures each generated getter, setter, or function call has a stable shape, allowing the engine to use the fast path (monomorphic or polymorphic ICs).

  9. Declaration mappings: TypeScript to Swift

    main

    The BridgeJS plugin maps TypeScript declarations to Swift using specific macros.

    Functions

    Exported functions become top-level Swift functions annotated with @JSFunction and throws(JSException).

    Global Getters

    Module-level declare const or top-level readonly bindings become Swift global properties annotated with @JSGetter.

    Classes

    Classes become Swift structs annotated with @JSClass:

    • Constructor: Becomes init(...) with @JSFunction.
    • Properties: Readonly properties use @JSGetter. Read-writable properties use @JSGetter and @JSSetter (as set<Name>(_:)).
    • Methods: Become @JSFunction.
    • Static Methods: Become static func on the struct.

    Interfaces

    Interfaces become Swift structs annotated with @JSClass. They do not have constructors; instances are typically obtained via other function calls returning that interface type. Properties and methods are bridged using @JSGetter, @JSSetter, and @JSFunction.

    Type Aliases

    • Primitive aliases (e.g., type UserId = string): Inlined as the underlying Swift type (e.g., String).
    • Object-shaped aliases (e.g., type User = { id: string }): Emitted as a named Swift struct with @JSClass.

    String Enums

    TypeScript string enums become Swift enums with String raw values, conforming to necessary BridgeJS protocols.

    // TypeScript (bridge-js.d.ts)
    export class Greeter {
        readonly id: number;
        message: string;
        constructor(id: string, name: string);
        greet(): string;
        static createDefault(greetingId: number, locale: string): string;
    }
    // Generated Swift
    @JSClass struct Greeter {
        @JSGetter var id: Double
        @JSGetter var message: String
        @JSSetter func setMessage(_ newValue: String) throws(JSException)
        @JSFunction init(id: String, name: String) throws(JSException)
        @JSFunction func greet() throws(JSException) -> String
        @JSFunction static func createDefault(_ greetingId: Double, _ locale: String) throws(JSException) -> String
    }
  10. Enable Identity Mode for Swift classes

    main

    By default, every time a Swift object crosses the boundary, a new JavaScript wrapper is created. This means identity checks (===) will fail even if the underlying Swift object is the same.

    To ensure that the same Swift object always returns the same JavaScript wrapper, you can enable Identity Mode.

    Options for enabling Identity Mode

    1. Per-class (Recommended): Use the @JS attribute on the class definition. This has zero overhead for classes that do not use it.

      @JS(identityMode: true)
      class Model { ... }
    2. Global Configuration: Enable it for all classes in a target via bridge-js.config.json:

      { "identityMode": "pointer" }

    Tradeoffs

    • Pros: Improves performance for workloads where the same objects are passed back and forth frequently.
    • Cons: Adds overhead for workloads that create many short-lived objects due to the internal cache (Map, WeakRef, FinalizationRegistry) used to maintain identity.
    @JS(identityMode: true)
    class Model {
        @JS var name: String
        @JS init(name: String) { self.name = name }
    }
  11. Use JSTypedClosure for type-safe Swift closures in JavaScript

    main

    Use JSTypedClosure to pass or return Swift closures to JavaScript via BridgeJS with compile-time type safety. Unlike the untyped JSClosure, JSTypedClosure requires a concrete signature (e.g., (Int) -> Int).

    Use cases:

    • Passing a Swift closure as an argument to a JavaScript API (e.g., a callback).
    • Returning a Swift closure from a Swift function exported via JS(namespace:enumStyle:) so JavaScript can invoke it later.

    Important: You must manually manage the lifetime of a JSTypedClosure by calling release() when it is no longer needed by JavaScript. Failure to do so can lead to memory leaks, and calling it too early will cause JavaScript to throw an exception when attempting to invoke the function.

    import JavaScriptKit
    
    // Example: Passing a typed closure to a JS function
    @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
  12. PackageToJS vs Carton

    main

    While both tools are used in the SwiftWasm ecosystem, they serve different purposes:

    FeaturePackageToJSCarton
    Primary FocusCompilation and JS wrapper generationDevelopment server and hot-reloading
    IntegrationIntegrated SPM command pluginStandalone tool

    Use PackageToJS when you need to build and package your application for production or bundler integration. Use Carton if you require a development server with hot-reloading capabilities.