Skip to content

Commit 6892354

Browse files
Add Node.js example (#473)
* PackageToJS: Make some options optional in option setup functions * Examples: Add NodeJS example * Documentation: Add Package output structure article * Apply suggestions from code review Co-authored-by: Copilot <[email protected]> * Revert browser.d.ts API change --------- Co-authored-by: Copilot <[email protected]>
1 parent 2d7709c commit 6892354

File tree

11 files changed

+224
-6
lines changed

11 files changed

+224
-6
lines changed

Examples/NodeJS/.gitignore

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
.DS_Store
2+
/.build
3+
/Packages
4+
xcuserdata/
5+
DerivedData/
6+
.swiftpm/configuration/registries.json
7+
.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata
8+
.netrc

Examples/NodeJS/Package.swift

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
// swift-tools-version: 6.2
2+
3+
import PackageDescription
4+
5+
let package = Package(
6+
name: "NodeJS",
7+
dependencies: [.package(name: "JavaScriptKit", path: "../../")],
8+
targets: [
9+
.executableTarget(
10+
name: "NodeJS",
11+
dependencies: ["JavaScriptKit"]
12+
)
13+
],
14+
swiftLanguageModes: [.v6]
15+
)

Examples/NodeJS/README.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
# Node.js example
2+
3+
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.
4+
5+
```sh
6+
$ swift package --swift-sdk $SWIFT_SDK_ID js
7+
$ node main.mjs
8+
```
9+
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
import JavaScriptKit
2+
3+
@main
4+
struct NodeJS {
5+
static func main() {
6+
JSObject.global["greet"] =
7+
JSClosure { args in
8+
let nameString = args[0].string!
9+
return .string("Hello, \(nameString) from NodeJS!")
10+
}.jsValue
11+
}
12+
}

Examples/NodeJS/main.mjs

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
// @ts-check
2+
3+
import { instantiate } from "./.build/plugins/PackageToJS/outputs/Package/instantiate.js"
4+
import { defaultNodeSetup } from "./.build/plugins/PackageToJS/outputs/Package/platforms/node.js"
5+
6+
async function main() {
7+
// Create a default Node.js option object
8+
const options = await defaultNodeSetup();
9+
// Instantiate the Swift code, executing
10+
// NodeJS.main() in NodeJS.swift
11+
await instantiate(options);
12+
13+
// Call the greet function set by NodeJS.swift
14+
const greet = globalThis.greet;
15+
console.log(greet("World"));
16+
}
17+
18+
main()

Plugins/PackageToJS/Templates/platforms/browser.d.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ export function defaultBrowserSetup(options: {
1111
getImports: () => Imports,
1212
/* #endif */
1313
/* #if USE_SHARED_MEMORY */
14-
spawnWorker: (module: WebAssembly.Module, memory: WebAssembly.Memory, startArg: any) => Worker,
14+
spawnWorker?: (module: WebAssembly.Module, memory: WebAssembly.Memory, startArg: any) => Worker,
1515
/* #endif */
1616
}): Promise<InstantiateOptions>
1717

Plugins/PackageToJS/Templates/platforms/browser.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -118,7 +118,7 @@ export async function defaultBrowserSetup(options) {
118118
/* #endif */
119119
/* #if USE_SHARED_MEMORY */
120120
const memory = new WebAssembly.Memory(MEMORY_TYPE);
121-
const threadChannel = new DefaultBrowserThreadRegistry(options.spawnWorker)
121+
const threadChannel = new DefaultBrowserThreadRegistry(options.spawnWorker || createDefaultWorkerFactory())
122122
/* #endif */
123123

124124
return {

Plugins/PackageToJS/Templates/platforms/node.d.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,10 @@ export type DefaultNodeSetupOptions = {
77
/* #endif */
88
onExit?: (code: number) => void,
99
/* #if USE_SHARED_MEMORY */
10-
spawnWorker: (module: WebAssembly.Module, memory: WebAssembly.Memory, startArg: any) => Worker,
10+
spawnWorker?: (module: WebAssembly.Module, memory: WebAssembly.Memory, startArg: any) => Worker,
1111
/* #endif */
1212
}
1313

14-
export function defaultNodeSetup(options: DefaultNodeSetupOptions): Promise<InstantiateOptions>
14+
export function defaultNodeSetup(options?: DefaultNodeSetupOptions): Promise<InstantiateOptions>
1515

1616
export function createDefaultWorkerFactory(preludeScript?: string): (module: WebAssembly.Module, memory: WebAssembly.Memory, startArg: any) => Worker

Plugins/PackageToJS/Templates/platforms/node.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -113,7 +113,7 @@ class DefaultNodeThreadRegistry {
113113
/* #endif */
114114

115115
/** @type {import('./node.d.ts').defaultNodeSetup} */
116-
export async function defaultNodeSetup(options) {
116+
export async function defaultNodeSetup(options = {}) {
117117
const path = await import("node:path");
118118
const { fileURLToPath } = await import("node:url");
119119
const { readFile } = await import("node:fs/promises")
@@ -134,7 +134,7 @@ export async function defaultNodeSetup(options) {
134134
const module = await WebAssembly.compile(new Uint8Array(await readFile(path.join(pkgDir, MODULE_PATH))))
135135
/* #if USE_SHARED_MEMORY */
136136
const memory = new WebAssembly.Memory(MEMORY_TYPE);
137-
const threadChannel = new DefaultNodeThreadRegistry(options.spawnWorker)
137+
const threadChannel = new DefaultNodeThreadRegistry(options.spawnWorker || createDefaultWorkerFactory())
138138
/* #endif */
139139

140140
return {
Lines changed: 155 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,155 @@
1+
# Package Output Structure
2+
3+
Understand the structure and contents of the JavaScript package generated by the `swift package js` command.
4+
5+
## Overview
6+
7+
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).
8+
9+
## Package Structure
10+
11+
The output package has the following structure:
12+
13+
```
14+
.build/plugins/PackageToJS/outputs/Package/
15+
├── ProductName.wasm # Compiled WebAssembly module
16+
├── index.js # Main entry point for browser environments
17+
├── index.d.ts # TypeScript type definitions for index.js
18+
├── instantiate.js # Low-level instantiation API
19+
├── instantiate.d.ts # TypeScript type definitions for instantiate.js
20+
├── package.json # npm package metadata
21+
└── platforms/
22+
├── browser.js # Browser-specific platform setup
23+
├── browser.d.ts # TypeScript definitions for browser.js
24+
├── node.js # Node.js-specific platform setup
25+
└── node.d.ts # TypeScript definitions for node.js
26+
```
27+
28+
## Using the Package
29+
30+
### In Browser
31+
32+
```html
33+
<!DOCTYPE html>
34+
<html>
35+
<body>
36+
<script type="module">
37+
import { init } from './.build/plugins/PackageToJS/outputs/Package/index.js';
38+
await init();
39+
</script>
40+
</body>
41+
</html>
42+
```
43+
44+
### In Node.js
45+
46+
```javascript
47+
import { instantiate } from './.build/plugins/PackageToJS/outputs/Package/instantiate.js';
48+
import { defaultNodeSetup } from './.build/plugins/PackageToJS/outputs/Package/platforms/node.js';
49+
50+
async function main() {
51+
const options = await defaultNodeSetup();
52+
await instantiate(options);
53+
}
54+
55+
main();
56+
```
57+
58+
> Tip: For a complete Node.js setup example, see the [Node.js example](https://github.com/swiftwasm/JavaScriptKit/tree/main/Examples/NodeJS).
59+
60+
### With Bundlers (Vite, Webpack, etc.)
61+
62+
The generated package can be consumed by JavaScript bundlers:
63+
64+
```bash
65+
npm install .build/plugins/PackageToJS/outputs/Package
66+
```
67+
68+
Then import it in your JavaScript code:
69+
70+
```javascript
71+
import { init } from 'package-name';
72+
await init();
73+
```
74+
75+
## Core Files
76+
77+
### WebAssembly Module (`ProductName.wasm`)
78+
79+
The compiled WebAssembly binary containing your Swift code. The filename matches your SwiftPM product name (e.g., `Basic.wasm` for a product named "Basic").
80+
81+
### Entry Point (`index.js`)
82+
83+
The main entry point for browser environments. It provides a convenient `init()` function that handles module instantiation with default settings.
84+
85+
```javascript
86+
import { init } from './.build/plugins/PackageToJS/outputs/Package/index.js';
87+
88+
// Initialize with default browser setup
89+
await init();
90+
```
91+
92+
For packages with BridgeJS imports, you can provide custom imports:
93+
94+
```javascript
95+
import { init } from './.build/plugins/PackageToJS/outputs/Package/index.js';
96+
97+
await init({
98+
getImports: () => ({
99+
// Your custom imports
100+
})
101+
});
102+
```
103+
104+
### Instantiation API (`instantiate.js`)
105+
106+
A lower-level API for more control over module instantiation. Use this when you need to customize the WebAssembly instantiation process or WASI setup.
107+
108+
```javascript
109+
import { instantiate } from './.build/plugins/PackageToJS/outputs/Package/instantiate.js';
110+
import { defaultBrowserSetup } from './.build/plugins/PackageToJS/outputs/Package/platforms/browser.js';
111+
112+
const options = await defaultBrowserSetup({
113+
module: fetch('./ProductName.wasm'),
114+
// ... other options
115+
});
116+
117+
const { instance, swift, exports } = await instantiate(options);
118+
```
119+
120+
### Platform-Specific Setup
121+
122+
The `platforms/` directory contains platform-specific setup functions:
123+
- `platforms/browser.js` - Provides `defaultBrowserSetup()` for browser environments
124+
- `platforms/node.js` - Provides `defaultNodeSetup()` for Node.js environments
125+
126+
## Package Metadata (`package.json`)
127+
128+
The generated `package.json` includes:
129+
130+
```json
131+
{
132+
"name": "package-name",
133+
"version": "0.0.0",
134+
"type": "module",
135+
"private": true,
136+
"exports": {
137+
".": "./index.js",
138+
"./wasm": "./ProductName.wasm"
139+
},
140+
"dependencies": {
141+
"@bjorn3/browser_wasi_shim": "0.3.0"
142+
}
143+
}
144+
```
145+
146+
The `exports` field allows importing the package as an npm dependency:
147+
148+
```javascript
149+
import { init } from '.build/plugins/PackageToJS/outputs/Package';
150+
```
151+
152+
## TypeScript Support
153+
154+
All JavaScript files have corresponding `.d.ts` TypeScript definition files, providing full type safety when using the package in TypeScript projects.
155+

0 commit comments

Comments
 (0)