From b8bd915569cda3b3004dee33721e230c0dab7ff7 Mon Sep 17 00:00:00 2001 From: Yuta Saito Date: Sat, 29 Nov 2025 05:58:49 +0000 Subject: [PATCH 1/5] PackageToJS: Make some options optional in option setup functions --- Plugins/PackageToJS/Templates/platforms/browser.d.ts | 4 ++-- Plugins/PackageToJS/Templates/platforms/browser.js | 12 ++++++------ Plugins/PackageToJS/Templates/platforms/node.d.ts | 4 ++-- Plugins/PackageToJS/Templates/platforms/node.js | 4 ++-- 4 files changed, 12 insertions(+), 12 deletions(-) diff --git a/Plugins/PackageToJS/Templates/platforms/browser.d.ts b/Plugins/PackageToJS/Templates/platforms/browser.d.ts index babe3f48..5eade844 100644 --- a/Plugins/PackageToJS/Templates/platforms/browser.d.ts +++ b/Plugins/PackageToJS/Templates/platforms/browser.d.ts @@ -1,6 +1,6 @@ import type { InstantiateOptions, ModuleSource/* #if HAS_IMPORTS */, Imports/* #endif */ } from "../instantiate.js" -export function defaultBrowserSetup(options: { +export function defaultBrowserSetup(options?: { module: ModuleSource, /* #if IS_WASI */ args?: string[], @@ -11,7 +11,7 @@ export function defaultBrowserSetup(options: { getImports: () => Imports, /* #endif */ /* #if USE_SHARED_MEMORY */ - spawnWorker: (module: WebAssembly.Module, memory: WebAssembly.Memory, startArg: any) => Worker, + spawnWorker?: (module: WebAssembly.Module, memory: WebAssembly.Memory, startArg: any) => Worker, /* #endif */ }): Promise diff --git a/Plugins/PackageToJS/Templates/platforms/browser.js b/Plugins/PackageToJS/Templates/platforms/browser.js index 3fce7c55..5629b7f8 100644 --- a/Plugins/PackageToJS/Templates/platforms/browser.js +++ b/Plugins/PackageToJS/Templates/platforms/browser.js @@ -102,9 +102,9 @@ class DefaultBrowserThreadRegistry { /** @type {import('./browser.d.ts').defaultBrowserSetup} */ export async function defaultBrowserSetup(options) { /* #if IS_WASI */ - const args = options.args ?? [] - const onStdoutLine = options.onStdoutLine ?? ((line) => console.log(line)) - const onStderrLine = options.onStderrLine ?? ((line) => console.error(line)) + const args = options?.args ?? [] + const onStdoutLine = options?.onStdoutLine ?? ((line) => console.log(line)) + const onStderrLine = options?.onStderrLine ?? ((line) => console.error(line)) const wasi = new WASI(/* args */[MODULE_PATH, ...args], /* env */[], /* fd */[ new OpenFile(new File([])), // stdin ConsoleStdout.lineBuffered((stdout) => { @@ -118,13 +118,13 @@ export async function defaultBrowserSetup(options) { /* #endif */ /* #if USE_SHARED_MEMORY */ const memory = new WebAssembly.Memory(MEMORY_TYPE); - const threadChannel = new DefaultBrowserThreadRegistry(options.spawnWorker) + const threadChannel = new DefaultBrowserThreadRegistry(options?.spawnWorker || createDefaultWorkerFactory()) /* #endif */ return { - module: options.module, + module: options?.module, /* #if HAS_IMPORTS */ - getImports() { return options.getImports() }, + getImports() { return options?.getImports() }, /* #endif */ /* #if IS_WASI */ wasi: Object.assign(wasi, { diff --git a/Plugins/PackageToJS/Templates/platforms/node.d.ts b/Plugins/PackageToJS/Templates/platforms/node.d.ts index ca0e826c..4b8d9584 100644 --- a/Plugins/PackageToJS/Templates/platforms/node.d.ts +++ b/Plugins/PackageToJS/Templates/platforms/node.d.ts @@ -7,10 +7,10 @@ export type DefaultNodeSetupOptions = { /* #endif */ onExit?: (code: number) => void, /* #if USE_SHARED_MEMORY */ - spawnWorker: (module: WebAssembly.Module, memory: WebAssembly.Memory, startArg: any) => Worker, + spawnWorker?: (module: WebAssembly.Module, memory: WebAssembly.Memory, startArg: any) => Worker, /* #endif */ } -export function defaultNodeSetup(options: DefaultNodeSetupOptions): Promise +export function defaultNodeSetup(options?: DefaultNodeSetupOptions): Promise export function createDefaultWorkerFactory(preludeScript?: string): (module: WebAssembly.Module, memory: WebAssembly.Memory, startArg: any) => Worker diff --git a/Plugins/PackageToJS/Templates/platforms/node.js b/Plugins/PackageToJS/Templates/platforms/node.js index 2fc0e8d1..3e125396 100644 --- a/Plugins/PackageToJS/Templates/platforms/node.js +++ b/Plugins/PackageToJS/Templates/platforms/node.js @@ -113,7 +113,7 @@ class DefaultNodeThreadRegistry { /* #endif */ /** @type {import('./node.d.ts').defaultNodeSetup} */ -export async function defaultNodeSetup(options) { +export async function defaultNodeSetup(options = {}) { const path = await import("node:path"); const { fileURLToPath } = await import("node:url"); const { readFile } = await import("node:fs/promises") @@ -134,7 +134,7 @@ export async function defaultNodeSetup(options) { const module = await WebAssembly.compile(new Uint8Array(await readFile(path.join(pkgDir, MODULE_PATH)))) /* #if USE_SHARED_MEMORY */ const memory = new WebAssembly.Memory(MEMORY_TYPE); - const threadChannel = new DefaultNodeThreadRegistry(options.spawnWorker) + const threadChannel = new DefaultNodeThreadRegistry(options.spawnWorker || createDefaultWorkerFactory()) /* #endif */ return { From 15b2a0d8890dcb0fe3c27b39df99c4c1f8715981 Mon Sep 17 00:00:00 2001 From: Yuta Saito Date: Sat, 29 Nov 2025 06:10:54 +0000 Subject: [PATCH 2/5] Examples: Add NodeJS example --- Examples/NodeJS/.gitignore | 8 ++++++++ Examples/NodeJS/Package.swift | 15 +++++++++++++++ Examples/NodeJS/README.md | 9 +++++++++ Examples/NodeJS/Sources/NodeJS/NodeJS.swift | 12 ++++++++++++ Examples/NodeJS/main.mjs | 18 ++++++++++++++++++ 5 files changed, 62 insertions(+) create mode 100644 Examples/NodeJS/.gitignore create mode 100644 Examples/NodeJS/Package.swift create mode 100644 Examples/NodeJS/README.md create mode 100644 Examples/NodeJS/Sources/NodeJS/NodeJS.swift create mode 100644 Examples/NodeJS/main.mjs diff --git a/Examples/NodeJS/.gitignore b/Examples/NodeJS/.gitignore new file mode 100644 index 00000000..0023a534 --- /dev/null +++ b/Examples/NodeJS/.gitignore @@ -0,0 +1,8 @@ +.DS_Store +/.build +/Packages +xcuserdata/ +DerivedData/ +.swiftpm/configuration/registries.json +.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata +.netrc diff --git a/Examples/NodeJS/Package.swift b/Examples/NodeJS/Package.swift new file mode 100644 index 00000000..83a84fc8 --- /dev/null +++ b/Examples/NodeJS/Package.swift @@ -0,0 +1,15 @@ +// swift-tools-version: 6.2 + +import PackageDescription + +let package = Package( + name: "NodeJS", + dependencies: [.package(name: "JavaScriptKit", path: "../../")], + targets: [ + .executableTarget( + name: "NodeJS", + dependencies: ["JavaScriptKit"] + ) + ], + swiftLanguageModes: [.v6] +) diff --git a/Examples/NodeJS/README.md b/Examples/NodeJS/README.md new file mode 100644 index 00000000..b4b96c32 --- /dev/null +++ b/Examples/NodeJS/README.md @@ -0,0 +1,9 @@ +# NodeJS example + +This example demonstrates how to use JavaScriptKit with Node.js. It shows how to export Swift functions to JavaScript and run them in a Node.js environment. + +```sh +$ swift package --swift-sdk $SWIFT_SDK_ID js +$ node main.mjs +``` + diff --git a/Examples/NodeJS/Sources/NodeJS/NodeJS.swift b/Examples/NodeJS/Sources/NodeJS/NodeJS.swift new file mode 100644 index 00000000..deebc1bd --- /dev/null +++ b/Examples/NodeJS/Sources/NodeJS/NodeJS.swift @@ -0,0 +1,12 @@ +import JavaScriptKit + +@main +struct NodeJS { + static func main() { + JSObject.global["greet"] = + JSClosure { args in + let nameString = args[0].string! + return .string("Hello, \(nameString) from NodeJS!") + }.jsValue + } +} diff --git a/Examples/NodeJS/main.mjs b/Examples/NodeJS/main.mjs new file mode 100644 index 00000000..5aca7d0a --- /dev/null +++ b/Examples/NodeJS/main.mjs @@ -0,0 +1,18 @@ +// @ts-check + +import { instantiate } from "./.build/plugins/PackageToJS/outputs/Package/instantiate.js" +import { defaultNodeSetup } from "./.build/plugins/PackageToJS/outputs/Package/platforms/node.js" + +async function main() { + // Create a default Node.js option object + const options = await defaultNodeSetup(); + // Instantiate the Swift code, executing + // NodeJS.main() in NodeJS.swift + await instantiate(options); + + // Call the greet function set by NodeJS.swift + const greet = globalThis.greet; + console.log(greet("World")); +} + +main() From cf25af79a8395f8627eb22db24a63b5f98cf583e Mon Sep 17 00:00:00 2001 From: Yuta Saito Date: Sat, 29 Nov 2025 06:37:59 +0000 Subject: [PATCH 3/5] Documentation: Add Package output structure article --- .../Articles/Package-Output-Structure.md | 155 ++++++++++++++++++ .../Documentation.docc/Documentation.md | 1 + 2 files changed, 156 insertions(+) create mode 100644 Sources/JavaScriptKit/Documentation.docc/Articles/Package-Output-Structure.md diff --git a/Sources/JavaScriptKit/Documentation.docc/Articles/Package-Output-Structure.md b/Sources/JavaScriptKit/Documentation.docc/Articles/Package-Output-Structure.md new file mode 100644 index 00000000..3b6026e5 --- /dev/null +++ b/Sources/JavaScriptKit/Documentation.docc/Articles/Package-Output-Structure.md @@ -0,0 +1,155 @@ +# Package Output Structure + +Understand the structure and contents of the JavaScript package generated by the `swift package js` command. + +## Overview + +When you run `swift package --swift-sdk $SWIFT_SDK_ID js`, the PackageToJS plugin compiles your Swift code to WebAssembly and generates a JavaScript package in `.build/plugins/PackageToJS/outputs/Package/`. This package contains all the necessary files to run your Swift application in JavaScript environments (browser or Node.js). + +## Package Structure + +The output package has the following 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 +``` + +## Using the Package + +### In Browser + +```html + + + + + + +``` + +### In Node.js + +```javascript +import { instantiate } from './.build/plugins/PackageToJS/outputs/Package/instantiate.js'; +import { defaultNodeSetup } from './.build/plugins/PackageToJS/outputs/Package/platforms/node.js'; + +async function main() { + const options = await defaultNodeSetup(); + await instantiate(options); +} + +main(); +``` + +> Tip: For a complete Node.js setup example, see the [NodeJS example](https://github.com/swiftwasm/JavaScriptKit/tree/main/Examples/NodeJS). + +### With Bundlers (Vite, Webpack, etc.) + +The generated package can be consumed by JavaScript bundlers: + +```bash +npm install .build/plugins/PackageToJS/outputs/Package +``` + +Then import it in your JavaScript code: + +```javascript +import { init } from 'package-name'; +await init(); +``` + +## Core Files + +### WebAssembly Module (`ProductName.wasm`) + +The compiled WebAssembly binary containing your Swift code. The filename matches your SwiftPM product name (e.g., `Basic.wasm` for a product named "Basic"). + +### Entry Point (`index.js`) + +The main entry point for browser environments. It provides a convenient `init()` function that handles module instantiation with default settings. + +```javascript +import { init } from './.build/plugins/PackageToJS/outputs/Package/index.js'; + +// Initialize with default browser setup +await init(); +``` + +For packages with BridgeJS imports, you can provide custom imports: + +```javascript +import { init } from './.build/plugins/PackageToJS/outputs/Package/index.js'; + +await init({ + getImports: () => ({ + // Your custom imports + }) +}); +``` + +### Instantiation API (`instantiate.js`) + +A lower-level API for more control over module instantiation. Use this when you need to customize the WebAssembly instantiation process or WASI setup. + +```javascript +import { instantiate } from './.build/plugins/PackageToJS/outputs/Package/instantiate.js'; +import { defaultBrowserSetup } from './.build/plugins/PackageToJS/outputs/Package/platforms/browser.js'; + +const options = await defaultBrowserSetup({ + module: fetch('./ProductName.wasm'), + // ... other options +}); + +const { instance, swift, exports } = await instantiate(options); +``` + +### Platform-Specific Setup + +The `platforms/` directory contains platform-specific setup functions: +- `platforms/browser.js` - Provides `defaultBrowserSetup()` for browser environments +- `platforms/node.js` - Provides `defaultNodeSetup()` for Node.js environments + +## Package Metadata (`package.json`) + +The generated `package.json` includes: + +```json +{ + "name": "package-name", + "version": "0.0.0", + "type": "module", + "private": true, + "exports": { + ".": "./index.js", + "./wasm": "./ProductName.wasm" + }, + "dependencies": { + "@bjorn3/browser_wasi_shim": "0.3.0" + } +} +``` + +The `exports` field allows importing the package as an npm dependency: + +```javascript +import { init } from '.build/plugins/PackageToJS/outputs/Package'; +``` + +## TypeScript Support + +All JavaScript files have corresponding `.d.ts` TypeScript definition files, providing full type safety when using the package in TypeScript projects. + diff --git a/Sources/JavaScriptKit/Documentation.docc/Documentation.md b/Sources/JavaScriptKit/Documentation.docc/Documentation.md index d506c386..60808a89 100644 --- a/Sources/JavaScriptKit/Documentation.docc/Documentation.md +++ b/Sources/JavaScriptKit/Documentation.docc/Documentation.md @@ -51,6 +51,7 @@ Check out the [examples](https://github.com/swiftwasm/JavaScriptKit/tree/main/Ex ### Articles +- - - - From 71b4bde99394688633edf3561f12103208c50520 Mon Sep 17 00:00:00 2001 From: Yuta Saito Date: Sat, 29 Nov 2025 15:52:46 +0900 Subject: [PATCH 4/5] Apply suggestions from code review Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- Examples/NodeJS/README.md | 2 +- .../Documentation.docc/Articles/Package-Output-Structure.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Examples/NodeJS/README.md b/Examples/NodeJS/README.md index b4b96c32..2b4e0321 100644 --- a/Examples/NodeJS/README.md +++ b/Examples/NodeJS/README.md @@ -1,4 +1,4 @@ -# NodeJS example +# Node.js example This example demonstrates how to use JavaScriptKit with Node.js. It shows how to export Swift functions to JavaScript and run them in a Node.js environment. diff --git a/Sources/JavaScriptKit/Documentation.docc/Articles/Package-Output-Structure.md b/Sources/JavaScriptKit/Documentation.docc/Articles/Package-Output-Structure.md index 3b6026e5..af9b0d2b 100644 --- a/Sources/JavaScriptKit/Documentation.docc/Articles/Package-Output-Structure.md +++ b/Sources/JavaScriptKit/Documentation.docc/Articles/Package-Output-Structure.md @@ -55,7 +55,7 @@ async function main() { main(); ``` -> Tip: For a complete Node.js setup example, see the [NodeJS example](https://github.com/swiftwasm/JavaScriptKit/tree/main/Examples/NodeJS). +> Tip: For a complete Node.js setup example, see the [Node.js example](https://github.com/swiftwasm/JavaScriptKit/tree/main/Examples/NodeJS). ### With Bundlers (Vite, Webpack, etc.) From b5e27bc8c64d368ea10520982d3bc381185a1dd7 Mon Sep 17 00:00:00 2001 From: Yuta Saito Date: Sat, 29 Nov 2025 06:52:25 +0000 Subject: [PATCH 5/5] Revert browser.d.ts API change --- Plugins/PackageToJS/Templates/platforms/browser.d.ts | 2 +- Plugins/PackageToJS/Templates/platforms/browser.js | 12 ++++++------ 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/Plugins/PackageToJS/Templates/platforms/browser.d.ts b/Plugins/PackageToJS/Templates/platforms/browser.d.ts index 5eade844..dcfb5f61 100644 --- a/Plugins/PackageToJS/Templates/platforms/browser.d.ts +++ b/Plugins/PackageToJS/Templates/platforms/browser.d.ts @@ -1,6 +1,6 @@ import type { InstantiateOptions, ModuleSource/* #if HAS_IMPORTS */, Imports/* #endif */ } from "../instantiate.js" -export function defaultBrowserSetup(options?: { +export function defaultBrowserSetup(options: { module: ModuleSource, /* #if IS_WASI */ args?: string[], diff --git a/Plugins/PackageToJS/Templates/platforms/browser.js b/Plugins/PackageToJS/Templates/platforms/browser.js index 5629b7f8..d471e841 100644 --- a/Plugins/PackageToJS/Templates/platforms/browser.js +++ b/Plugins/PackageToJS/Templates/platforms/browser.js @@ -102,9 +102,9 @@ class DefaultBrowserThreadRegistry { /** @type {import('./browser.d.ts').defaultBrowserSetup} */ export async function defaultBrowserSetup(options) { /* #if IS_WASI */ - const args = options?.args ?? [] - const onStdoutLine = options?.onStdoutLine ?? ((line) => console.log(line)) - const onStderrLine = options?.onStderrLine ?? ((line) => console.error(line)) + const args = options.args ?? [] + const onStdoutLine = options.onStdoutLine ?? ((line) => console.log(line)) + const onStderrLine = options.onStderrLine ?? ((line) => console.error(line)) const wasi = new WASI(/* args */[MODULE_PATH, ...args], /* env */[], /* fd */[ new OpenFile(new File([])), // stdin ConsoleStdout.lineBuffered((stdout) => { @@ -118,13 +118,13 @@ export async function defaultBrowserSetup(options) { /* #endif */ /* #if USE_SHARED_MEMORY */ const memory = new WebAssembly.Memory(MEMORY_TYPE); - const threadChannel = new DefaultBrowserThreadRegistry(options?.spawnWorker || createDefaultWorkerFactory()) + const threadChannel = new DefaultBrowserThreadRegistry(options.spawnWorker || createDefaultWorkerFactory()) /* #endif */ return { - module: options?.module, + module: options.module, /* #if HAS_IMPORTS */ - getImports() { return options?.getImports() }, + getImports() { return options.getImports() }, /* #endif */ /* #if IS_WASI */ wasi: Object.assign(wasi, {