diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 000000000..0e720b4df --- /dev/null +++ b/.editorconfig @@ -0,0 +1,7 @@ +root = true + +[*] +indent_style = space +indent_size = 4 +trim_trailing_whitespace = true +insert_final_newline = true diff --git a/.github/workflows/perf.yml b/.github/workflows/perf.yml deleted file mode 100644 index 501b16099..000000000 --- a/.github/workflows/perf.yml +++ /dev/null @@ -1,21 +0,0 @@ -name: Performance - -on: [pull_request] - -jobs: - perf: - runs-on: ubuntu-24.04 - steps: - - name: Checkout - uses: actions/checkout@v4 - - uses: ./.github/actions/install-swift - with: - download-url: https://download.swift.org/swift-6.0.3-release/ubuntu2404/swift-6.0.3-RELEASE/swift-6.0.3-RELEASE-ubuntu24.04.tar.gz - - uses: swiftwasm/setup-swiftwasm@v2 - - name: Run Benchmark - run: | - make bootstrap - make perf-tester - node ci/perf-tester - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 486f7b6bf..cf0224346 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -16,12 +16,17 @@ jobs: target: "wasm32-unknown-wasi" - os: ubuntu-22.04 toolchain: - download-url: https://download.swift.org/development/ubuntu2204/swift-DEVELOPMENT-SNAPSHOT-2025-02-26-a/swift-DEVELOPMENT-SNAPSHOT-2025-02-26-a-ubuntu22.04.tar.gz + download-url: https://download.swift.org/swift-6.1-release/ubuntu2204/swift-6.1-RELEASE/swift-6.1-RELEASE-ubuntu22.04.tar.gz wasi-backend: Node target: "wasm32-unknown-wasi" - os: ubuntu-22.04 toolchain: - download-url: https://download.swift.org/development/ubuntu2204/swift-DEVELOPMENT-SNAPSHOT-2025-02-26-a/swift-DEVELOPMENT-SNAPSHOT-2025-02-26-a-ubuntu22.04.tar.gz + download-url: https://download.swift.org/development/ubuntu2204/swift-DEVELOPMENT-SNAPSHOT-2025-04-12-a/swift-DEVELOPMENT-SNAPSHOT-2025-04-12-a-ubuntu22.04.tar.gz + wasi-backend: Node + target: "wasm32-unknown-wasi" + - os: ubuntu-22.04 + toolchain: + download-url: https://download.swift.org/development/ubuntu2204/swift-DEVELOPMENT-SNAPSHOT-2025-04-12-a/swift-DEVELOPMENT-SNAPSHOT-2025-04-12-a-ubuntu22.04.tar.gz wasi-backend: Node target: "wasm32-unknown-wasip1-threads" @@ -52,6 +57,7 @@ jobs: make regenerate_swiftpm_resources git diff --exit-code Sources/JavaScriptKit/Runtime - run: swift test --package-path ./Plugins/PackageToJS + - run: swift test --package-path ./Plugins/BridgeJS native-build: # Check native build to make it easy to develop applications by Xcode @@ -64,6 +70,21 @@ jobs: runs-on: ${{ matrix.os }} steps: - uses: actions/checkout@v4 - - run: swift build + - run: swift build --package-path ./Examples/Basic env: DEVELOPER_DIR: /Applications/${{ matrix.xcode }}.app/Contents/Developer/ + + format: + runs-on: ubuntu-latest + container: + image: swift:6.0.3 + steps: + - uses: actions/checkout@v4 + - run: ./Utilities/format.swift + - name: Check for formatting changes + run: | + git config --global --add safe.directory "$GITHUB_WORKSPACE" + git diff --exit-code || { + echo "::error::The formatting changed some files. Please run \`./Utilities/format.swift\` and commit the changes." + exit 1 + } diff --git a/.gitignore b/.gitignore index 232ea1145..5aac0048c 100644 --- a/.gitignore +++ b/.gitignore @@ -10,3 +10,4 @@ xcuserdata/ Examples/*/Bundle Examples/*/package-lock.json Package.resolved +Plugins/BridgeJS/Sources/JavaScript/package-lock.json diff --git a/.swift-format b/.swift-format new file mode 100644 index 000000000..22ce671e7 --- /dev/null +++ b/.swift-format @@ -0,0 +1,13 @@ +{ + "version": 1, + "lineLength": 120, + "indentation": { + "spaces": 4 + }, + "lineBreakBeforeEachArgument": true, + "indentConditionalCompilationBlocks": false, + "prioritizeKeepingFunctionOutputTogether": true, + "rules": { + "AlwaysUseLowerCamelCase": false + } +} diff --git a/Benchmarks/Package.swift b/Benchmarks/Package.swift new file mode 100644 index 000000000..4d59c772e --- /dev/null +++ b/Benchmarks/Package.swift @@ -0,0 +1,20 @@ +// swift-tools-version: 6.0 + +import PackageDescription + +let package = Package( + name: "Benchmarks", + dependencies: [ + .package(path: "../") + ], + targets: [ + .executableTarget( + name: "Benchmarks", + dependencies: ["JavaScriptKit"], + exclude: ["Generated/JavaScript", "bridge.d.ts"], + swiftSettings: [ + .enableExperimentalFeature("Extern") + ] + ) + ] +) diff --git a/Benchmarks/README.md b/Benchmarks/README.md new file mode 100644 index 000000000..eeafc395a --- /dev/null +++ b/Benchmarks/README.md @@ -0,0 +1,30 @@ +# JavaScriptKit Benchmarks + +This directory contains performance benchmarks for JavaScriptKit. + +## Building Benchmarks + +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 +``` + +## Running Benchmarks + +```bash +# Run with default settings +node run.js + +# Save results to a JSON file +node run.js --output=results.json + +# Specify number of iterations +node run.js --runs=20 + +# Run in adaptive mode until results stabilize +node run.js --adaptive --output=stable-results.json + +# Run benchmarks and compare with previous results +node run.js --baseline=previous-results.json +``` diff --git a/Benchmarks/Sources/Benchmarks.swift b/Benchmarks/Sources/Benchmarks.swift new file mode 100644 index 000000000..602aa843c --- /dev/null +++ b/Benchmarks/Sources/Benchmarks.swift @@ -0,0 +1,78 @@ +import JavaScriptKit + +class Benchmark { + init(_ title: String) { + self.title = title + } + + let title: String + + func testSuite(_ name: String, _ body: @escaping () -> Void) { + let jsBody = JSClosure { arguments -> JSValue in + body() + return .undefined + } + benchmarkRunner("\(title)/\(name)", jsBody) + } +} + +@JS func run() { + + let call = Benchmark("Call") + + call.testSuite("JavaScript function call through Wasm import") { + for _ in 0..<20_000_000 { + benchmarkHelperNoop() + } + } + + call.testSuite("JavaScript function call through Wasm import with int") { + for _ in 0..<10_000_000 { + benchmarkHelperNoopWithNumber(42) + } + } + + let propertyAccess = Benchmark("Property access") + + do { + let swiftInt: Double = 42 + let object = JSObject() + object.jsNumber = JSValue.number(swiftInt) + propertyAccess.testSuite("Write Number") { + for _ in 0..<1_000_000 { + object.jsNumber = JSValue.number(swiftInt) + } + } + } + + do { + let object = JSObject() + object.jsNumber = JSValue.number(42) + propertyAccess.testSuite("Read Number") { + for _ in 0..<1_000_000 { + _ = object.jsNumber.number + } + } + } + + do { + let swiftString = "Hello, world" + let object = JSObject() + object.jsString = swiftString.jsValue + propertyAccess.testSuite("Write String") { + for _ in 0..<1_000_000 { + object.jsString = swiftString.jsValue + } + } + } + + do { + let object = JSObject() + object.jsString = JSValue.string("Hello, world") + propertyAccess.testSuite("Read String") { + for _ in 0..<1_000_000 { + _ = object.jsString.string + } + } + } +} diff --git a/Benchmarks/Sources/Generated/ExportSwift.swift b/Benchmarks/Sources/Generated/ExportSwift.swift new file mode 100644 index 000000000..a8745b649 --- /dev/null +++ b/Benchmarks/Sources/Generated/ExportSwift.swift @@ -0,0 +1,15 @@ +// NOTICE: This is auto-generated code by BridgeJS from JavaScriptKit, +// DO NOT EDIT. +// +// To update this file, just rebuild your project or run +// `swift package bridge-js`. +@_extern(wasm, module: "bjs", name: "return_string") +private func _return_string(_ ptr: UnsafePointer?, _ len: Int32) +@_extern(wasm, module: "bjs", name: "init_memory") +private func _init_memory(_ sourceId: Int32, _ ptr: UnsafeMutablePointer?) + +@_expose(wasm, "bjs_main") +@_cdecl("bjs_main") +public func _bjs_main() -> Void { + main() +} \ No newline at end of file diff --git a/Benchmarks/Sources/Generated/ImportTS.swift b/Benchmarks/Sources/Generated/ImportTS.swift new file mode 100644 index 000000000..583b9ba58 --- /dev/null +++ b/Benchmarks/Sources/Generated/ImportTS.swift @@ -0,0 +1,38 @@ +// NOTICE: This is auto-generated code by BridgeJS from JavaScriptKit, +// DO NOT EDIT. +// +// To update this file, just rebuild your project or run +// `swift package bridge-js`. + +@_spi(JSObject_id) import JavaScriptKit + +@_extern(wasm, module: "bjs", name: "make_jsstring") +private func _make_jsstring(_ ptr: UnsafePointer?, _ len: Int32) -> Int32 + +@_extern(wasm, module: "bjs", name: "init_memory_with_result") +private func _init_memory_with_result(_ ptr: UnsafePointer?, _ len: Int32) + +@_extern(wasm, module: "bjs", name: "free_jsobject") +private func _free_jsobject(_ ptr: Int32) -> Void + +func benchmarkHelperNoop() -> Void { + @_extern(wasm, module: "Benchmarks", name: "bjs_benchmarkHelperNoop") + func bjs_benchmarkHelperNoop() -> Void + bjs_benchmarkHelperNoop() +} + +func benchmarkHelperNoopWithNumber(_ n: Double) -> Void { + @_extern(wasm, module: "Benchmarks", name: "bjs_benchmarkHelperNoopWithNumber") + func bjs_benchmarkHelperNoopWithNumber(_ n: Float64) -> Void + bjs_benchmarkHelperNoopWithNumber(n) +} + +func benchmarkRunner(_ name: String, _ body: JSObject) -> Void { + @_extern(wasm, module: "Benchmarks", name: "bjs_benchmarkRunner") + func bjs_benchmarkRunner(_ name: Int32, _ body: Int32) -> Void + var name = name + let nameId = name.withUTF8 { b in + _make_jsstring(b.baseAddress.unsafelyUnwrapped, Int32(b.count)) + } + bjs_benchmarkRunner(nameId, Int32(bitPattern: body.id)) +} \ No newline at end of file diff --git a/Benchmarks/Sources/Generated/JavaScript/ExportSwift.json b/Benchmarks/Sources/Generated/JavaScript/ExportSwift.json new file mode 100644 index 000000000..0b1b70b70 --- /dev/null +++ b/Benchmarks/Sources/Generated/JavaScript/ExportSwift.json @@ -0,0 +1,19 @@ +{ + "classes" : [ + + ], + "functions" : [ + { + "abiName" : "bjs_main", + "name" : "main", + "parameters" : [ + + ], + "returnType" : { + "void" : { + + } + } + } + ] +} \ No newline at end of file diff --git a/Benchmarks/Sources/Generated/JavaScript/ImportTS.json b/Benchmarks/Sources/Generated/JavaScript/ImportTS.json new file mode 100644 index 000000000..366342bbc --- /dev/null +++ b/Benchmarks/Sources/Generated/JavaScript/ImportTS.json @@ -0,0 +1,67 @@ +{ + "children" : [ + { + "functions" : [ + { + "name" : "benchmarkHelperNoop", + "parameters" : [ + + ], + "returnType" : { + "void" : { + + } + } + }, + { + "name" : "benchmarkHelperNoopWithNumber", + "parameters" : [ + { + "name" : "n", + "type" : { + "double" : { + + } + } + } + ], + "returnType" : { + "void" : { + + } + } + }, + { + "name" : "benchmarkRunner", + "parameters" : [ + { + "name" : "name", + "type" : { + "string" : { + + } + } + }, + { + "name" : "body", + "type" : { + "jsObject" : { + + } + } + } + ], + "returnType" : { + "void" : { + + } + } + } + ], + "types" : [ + + ] + } + ], + "moduleName" : "Benchmarks" +} \ No newline at end of file diff --git a/Benchmarks/Sources/bridge.d.ts b/Benchmarks/Sources/bridge.d.ts new file mode 100644 index 000000000..a9eb5d0bf --- /dev/null +++ b/Benchmarks/Sources/bridge.d.ts @@ -0,0 +1,3 @@ +declare function benchmarkHelperNoop(): void; +declare function benchmarkHelperNoopWithNumber(n: number): void; +declare function benchmarkRunner(name: string, body: (n: number) => void): void; diff --git a/Benchmarks/package.json b/Benchmarks/package.json new file mode 100644 index 000000000..5ffd9800b --- /dev/null +++ b/Benchmarks/package.json @@ -0,0 +1 @@ +{ "type": "module" } diff --git a/Benchmarks/run.js b/Benchmarks/run.js new file mode 100644 index 000000000..2305373a5 --- /dev/null +++ b/Benchmarks/run.js @@ -0,0 +1,449 @@ +import { instantiate } from "./.build/plugins/PackageToJS/outputs/Package/instantiate.js" +import { defaultNodeSetup } from "./.build/plugins/PackageToJS/outputs/Package/platforms/node.js" +import fs from 'fs'; +import path from 'path'; +import { parseArgs } from "util"; + +/** + * Update progress bar on the current line + * @param {number} current - Current progress + * @param {number} total - Total items + * @param {string} label - Label for the progress bar + * @param {number} width - Width of the progress bar + */ +function updateProgress(current, total, label = '', width) { + const percent = (current / total) * 100; + const completed = Math.round(width * (percent / 100)); + const remaining = width - completed; + const bar = '█'.repeat(completed) + '░'.repeat(remaining); + process.stdout.clearLine(); + process.stdout.cursorTo(0); + process.stdout.write(`${label} [${bar}] ${current}/${total}`); +} + +/** + * Calculate coefficient of variation (relative standard deviation) + * @param {Array} values - Array of measurement values + * @returns {number} Coefficient of variation as a percentage + */ +function calculateCV(values) { + if (values.length < 2) return 0; + + const sum = values.reduce((a, b) => a + b, 0); + const mean = sum / values.length; + + if (mean === 0) return 0; + + const variance = values.reduce((a, b) => a + Math.pow(b - mean, 2), 0) / values.length; + const stdDev = Math.sqrt(variance); + + return (stdDev / mean) * 100; // Return as percentage +} + +/** + * Calculate statistics from benchmark results + * @param {Object} results - Raw benchmark results + * @returns {Object} Formatted results with statistics + */ +function calculateStatistics(results) { + const formattedResults = {}; + const consoleTable = []; + + for (const [name, times] of Object.entries(results)) { + const sum = times.reduce((a, b) => a + b, 0); + const avg = sum / times.length; + const min = Math.min(...times); + const max = Math.max(...times); + const variance = times.reduce((a, b) => a + Math.pow(b - avg, 2), 0) / times.length; + const stdDev = Math.sqrt(variance); + const cv = (stdDev / avg) * 100; // Coefficient of variation as percentage + + formattedResults[name] = { + "avg_ms": parseFloat(avg.toFixed(2)), + "min_ms": parseFloat(min.toFixed(2)), + "max_ms": parseFloat(max.toFixed(2)), + "stdDev_ms": parseFloat(stdDev.toFixed(2)), + "cv_percent": parseFloat(cv.toFixed(2)), + "samples": times.length, + "rawTimes_ms": times.map(t => parseFloat(t.toFixed(2))) + }; + + consoleTable.push({ + Test: name, + 'Avg (ms)': avg.toFixed(2), + 'Min (ms)': min.toFixed(2), + 'Max (ms)': max.toFixed(2), + 'StdDev (ms)': stdDev.toFixed(2), + 'CV (%)': cv.toFixed(2), + 'Samples': times.length + }); + } + + return { formattedResults, consoleTable }; +} + +/** + * Load a JSON file + * @param {string} filePath - Path to the JSON file + * @returns {Object|null} Parsed JSON or null if file doesn't exist + */ +function loadJsonFile(filePath) { + try { + if (fs.existsSync(filePath)) { + const fileContent = fs.readFileSync(filePath, 'utf8'); + return JSON.parse(fileContent); + } + } catch (error) { + console.error(`Error loading JSON file ${filePath}:`, error.message); + } + return null; +} + +/** + * Compare current results with baseline + * @param {Object} current - Current benchmark results + * @param {Object} baseline - Baseline benchmark results + * @returns {Object} Comparison results with percent change + */ +function compareWithBaseline(current, baseline) { + const comparisonTable = []; + + // Get all unique test names from both current and baseline + const allTests = new Set([ + ...Object.keys(current), + ...Object.keys(baseline) + ]); + + for (const test of allTests) { + const currentTest = current[test]; + const baselineTest = baseline[test]; + + if (!currentTest) { + comparisonTable.push({ + Test: test, + 'Status': 'REMOVED', + 'Baseline (ms)': baselineTest.avg_ms.toFixed(2), + 'Current (ms)': 'N/A', + 'Change': 'N/A', + 'Change (%)': 'N/A' + }); + continue; + } + + if (!baselineTest) { + comparisonTable.push({ + Test: test, + 'Status': 'NEW', + 'Baseline (ms)': 'N/A', + 'Current (ms)': currentTest.avg_ms.toFixed(2), + 'Change': 'N/A', + 'Change (%)': 'N/A' + }); + continue; + } + + const change = currentTest.avg_ms - baselineTest.avg_ms; + const percentChange = (change / baselineTest.avg_ms) * 100; + + let status = 'NEUTRAL'; + if (percentChange < -5) status = 'FASTER'; + else if (percentChange > 5) status = 'SLOWER'; + + comparisonTable.push({ + Test: test, + 'Status': status, + 'Baseline (ms)': baselineTest.avg_ms.toFixed(2), + 'Current (ms)': currentTest.avg_ms.toFixed(2), + 'Change': (0 < change ? '+' : '') + change.toFixed(2) + ' ms', + 'Change (%)': (0 < percentChange ? '+' : '') + percentChange.toFixed(2) + '%' + }); + } + + return comparisonTable; +} + +/** + * Format and print comparison results + * @param {Array} comparisonTable - Comparison results + */ +function printComparisonResults(comparisonTable) { + console.log("\n=============================="); + console.log(" COMPARISON WITH BASELINE "); + console.log("==============================\n"); + + // Color code the output if terminal supports it + const colorize = (text, status) => { + if (process.stdout.isTTY) { + if (status === 'FASTER') return `\x1b[32m${text}\x1b[0m`; // Green + if (status === 'SLOWER') return `\x1b[31m${text}\x1b[0m`; // Red + if (status === 'NEW') return `\x1b[36m${text}\x1b[0m`; // Cyan + if (status === 'REMOVED') return `\x1b[33m${text}\x1b[0m`; // Yellow + } + return text; + }; + + // Manually format table for better control over colors + const columnWidths = { + Test: Math.max(4, ...comparisonTable.map(row => row.Test.length)), + Status: 8, + Baseline: 15, + Current: 15, + Change: 15, + PercentChange: 15 + }; + + // Print header + console.log( + 'Test'.padEnd(columnWidths.Test) + ' | ' + + 'Status'.padEnd(columnWidths.Status) + ' | ' + + 'Baseline (ms)'.padEnd(columnWidths.Baseline) + ' | ' + + 'Current (ms)'.padEnd(columnWidths.Current) + ' | ' + + 'Change'.padEnd(columnWidths.Change) + ' | ' + + 'Change (%)' + ); + + console.log('-'.repeat(columnWidths.Test + columnWidths.Status + columnWidths.Baseline + + columnWidths.Current + columnWidths.Change + columnWidths.PercentChange + 10)); + + // Print rows + for (const row of comparisonTable) { + console.log( + row.Test.padEnd(columnWidths.Test) + ' | ' + + colorize(row.Status.padEnd(columnWidths.Status), row.Status) + ' | ' + + row['Baseline (ms)'].toString().padEnd(columnWidths.Baseline) + ' | ' + + row['Current (ms)'].toString().padEnd(columnWidths.Current) + ' | ' + + colorize(row.Change.padEnd(columnWidths.Change), row.Status) + ' | ' + + colorize(row['Change (%)'].padEnd(columnWidths.PercentChange), row.Status) + ); + } +} + +/** + * Save results to JSON file + * @param {string} filePath - Output file path + * @param {Object} data - Data to save + */ +function saveJsonResults(filePath, data) { + const outputDir = path.dirname(filePath); + if (outputDir !== '.' && !fs.existsSync(outputDir)) { + fs.mkdirSync(outputDir, { recursive: true }); + } + + fs.writeFileSync(filePath, JSON.stringify(data, null, 2)); + console.log(`\nDetailed results saved to ${filePath}`); +} + +/** + * Run a single benchmark iteration + * @param {Object} results - Results object to store benchmark data + * @returns {Promise} + */ +async function singleRun(results) { + const options = await defaultNodeSetup({}) + const { exports } = await instantiate({ + ...options, + imports: { + benchmarkHelperNoop: () => { }, + benchmarkHelperNoopWithNumber: (n) => { }, + benchmarkRunner: (name, body) => { + const startTime = performance.now(); + body(); + const endTime = performance.now(); + const duration = endTime - startTime; + if (!results[name]) { + results[name] = [] + } + results[name].push(duration) + } + } + }); + exports.run(); +} + +/** + * Run until the coefficient of variation of measurements is below the threshold + * @param {Object} results - Benchmark results object + * @param {Object} options - Adaptive sampling options + * @returns {Promise} + */ +async function runUntilStable(results, options, width) { + const { + minRuns = 5, + maxRuns = 50, + targetCV = 5, + } = options; + + let runs = 0; + let allStable = false; + + console.log("\nAdaptive sampling enabled:"); + console.log(`- Minimum runs: ${minRuns}`); + console.log(`- Maximum runs: ${maxRuns}`); + console.log(`- Target CV: ${targetCV}%`); + + while (runs < maxRuns) { + // Update progress with estimated completion + updateProgress(runs, maxRuns, "Benchmark Progress:", width); + + await singleRun(results); + runs++; + + // Check if we've reached minimum runs + if (runs < minRuns) continue; + + // Check stability of all tests after each run + const cvs = []; + allStable = true; + + for (const [name, times] of Object.entries(results)) { + const cv = calculateCV(times); + cvs.push({ name, cv }); + + if (cv > targetCV) { + allStable = false; + } + } + + // Display current CV values periodically + if (runs % 3 === 0 || allStable) { + process.stdout.write("\n"); + console.log(`After ${runs} runs, coefficient of variation (%):`) + for (const { name, cv } of cvs) { + const stable = cv <= targetCV; + const status = stable ? '✓' : '…'; + const cvStr = cv.toFixed(2) + '%'; + console.log(` ${status} ${name}: ${stable ? '\x1b[32m' : ''}${cvStr}${stable ? '\x1b[0m' : ''}`); + } + } + + // Check if we should stop + if (allStable) { + console.log("\nAll benchmarks stable! Stopping adaptive sampling."); + break; + } + } + + updateProgress(maxRuns, maxRuns, "Benchmark Progress:", width); + console.log("\n"); + + if (!allStable) { + console.log("\nWarning: Not all benchmarks reached target stability!"); + for (const [name, times] of Object.entries(results)) { + const cv = calculateCV(times); + if (cv > targetCV) { + console.log(` ! ${name}: ${cv.toFixed(2)}% > ${targetCV}%`); + } + } + } +} + +function showHelp() { + console.log(` +Usage: node run.js [options] + +Options: + --runs=NUMBER Number of benchmark runs (default: 10) + --output=FILENAME Save JSON results to specified file + --baseline=FILENAME Compare results with baseline JSON file + --adaptive Enable adaptive sampling (run until stable) + --min-runs=NUMBER Minimum runs for adaptive sampling (default: 5) + --max-runs=NUMBER Maximum runs for adaptive sampling (default: 50) + --target-cv=NUMBER Target coefficient of variation % (default: 5) + --help Show this help message +`); +} + +async function main() { + const args = parseArgs({ + options: { + runs: { type: 'string', default: '10' }, + output: { type: 'string' }, + baseline: { type: 'string' }, + help: { type: 'boolean', default: false }, + adaptive: { type: 'boolean', default: false }, + 'min-runs': { type: 'string', default: '5' }, + 'max-runs': { type: 'string', default: '50' }, + 'target-cv': { type: 'string', default: '5' } + } + }); + + if (args.values.help) { + showHelp(); + return; + } + + const results = {}; + const width = 30; + + if (args.values.adaptive) { + // Adaptive sampling mode + const options = { + minRuns: parseInt(args.values['min-runs'], 10), + maxRuns: parseInt(args.values['max-runs'], 10), + targetCV: parseFloat(args.values['target-cv']) + }; + + console.log("Starting benchmark with adaptive sampling..."); + if (args.values.output) { + console.log(`Results will be saved to: ${args.values.output}`); + } + + await runUntilStable(results, options, width); + } else { + // Fixed number of runs mode + const runs = parseInt(args.values.runs, 10); + if (isNaN(runs)) { + console.error('Invalid number of runs:', args.values.runs); + process.exit(1); + } + + console.log(`Starting benchmark suite with ${runs} runs per test...`); + if (args.values.output) { + console.log(`Results will be saved to: ${args.values.output}`); + } + + if (args.values.baseline) { + console.log(`Will compare with baseline: ${args.values.baseline}`); + } + + // Show overall progress + console.log("\nOverall Progress:"); + for (let i = 0; i < runs; i++) { + updateProgress(i, runs, "Benchmark Runs:", width); + await singleRun(results); + } + updateProgress(runs, runs, "Benchmark Runs:", width); + console.log("\n"); + } + + // Calculate and display statistics + console.log("\n=============================="); + console.log(" BENCHMARK SUMMARY "); + console.log("==============================\n"); + + const { formattedResults, consoleTable } = calculateStatistics(results); + + // Print readable format to console + console.table(consoleTable); + + // Compare with baseline if provided + if (args.values.baseline) { + const baseline = loadJsonFile(args.values.baseline); + if (baseline) { + const comparisonResults = compareWithBaseline(formattedResults, baseline); + printComparisonResults(comparisonResults); + } else { + console.error(`Could not load baseline file: ${args.values.baseline}`); + } + } + + // Save JSON to file if specified + if (args.values.output) { + saveJsonResults(args.values.output, formattedResults); + } +} + +main().catch(err => { + console.error('Benchmark error:', err); + process.exit(1); +}); diff --git a/Examples/ActorOnWebWorker/Package.swift b/Examples/ActorOnWebWorker/Package.swift index 711bf6461..82e87dfdc 100644 --- a/Examples/ActorOnWebWorker/Package.swift +++ b/Examples/ActorOnWebWorker/Package.swift @@ -6,7 +6,7 @@ let package = Package( name: "Example", platforms: [.macOS("15"), .iOS("18"), .watchOS("11"), .tvOS("18"), .visionOS("2")], dependencies: [ - .package(path: "../../"), + .package(path: "../../") ], targets: [ .executableTarget( @@ -15,6 +15,6 @@ let package = Package( .product(name: "JavaScriptKit", package: "JavaScriptKit"), .product(name: "JavaScriptEventLoop", package: "JavaScriptKit"), ] - ), + ) ] ) diff --git a/Examples/ActorOnWebWorker/Sources/MyApp.swift b/Examples/ActorOnWebWorker/Sources/MyApp.swift index 7d362d13e..9b38fa30c 100644 --- a/Examples/ActorOnWebWorker/Sources/MyApp.swift +++ b/Examples/ActorOnWebWorker/Sources/MyApp.swift @@ -11,7 +11,7 @@ actor SearchService { } } - let serialExecutor: any SerialExecutor + let serialExecutor: OwnedExecutor // Simple in-memory index: word -> positions var index: [String: [Int]] = [:] @@ -21,10 +21,10 @@ actor SearchService { }() nonisolated var unownedExecutor: UnownedSerialExecutor { - return self.serialExecutor.asUnownedSerialExecutor() + return self.serialExecutor.unownedExecutor } - init(serialExecutor: any SerialExecutor) { + init(serialExecutor: OwnedExecutor) { self.serialExecutor = serialExecutor } @@ -120,13 +120,13 @@ final class App { private let alert = JSObject.global.alert.function! // UI elements - private var container: JSValue - private var urlInput: JSValue - private var indexButton: JSValue - private var searchInput: JSValue - private var searchButton: JSValue - private var statusElement: JSValue - private var resultsElement: JSValue + private let container: JSValue + private let urlInput: JSValue + private let indexButton: JSValue + private let searchInput: JSValue + private let searchButton: JSValue + private let statusElement: JSValue + private let resultsElement: JSValue // Search service private let service: SearchService @@ -144,17 +144,21 @@ final class App { } private func setupEventHandlers() { - indexButton.onclick = .object(JSClosure { [weak self] _ in - guard let self else { return .undefined } - self.performIndex() - return .undefined - }) - - searchButton.onclick = .object(JSClosure { [weak self] _ in - guard let self else { return .undefined } - self.performSearch() - return .undefined - }) + indexButton.onclick = .object( + JSClosure { [weak self] _ in + guard let self else { return .undefined } + self.performIndex() + return .undefined + } + ) + + searchButton.onclick = .object( + JSClosure { [weak self] _ in + guard let self else { return .undefined } + self.performSearch() + return .undefined + } + ) } private func performIndex() { @@ -210,53 +214,80 @@ final class App { resultsElement.innerHTML = .string("") if results.isEmpty { - var noResults = document.createElement("p") + let noResults = document.createElement("p") noResults.innerText = .string("No results found.") _ = resultsElement.appendChild(noResults) } else { // Display up to 10 results for (index, result) in results.prefix(10).enumerated() { - var resultItem = document.createElement("div") + let resultItem = document.createElement("div") resultItem.style = .string( "padding: 10px; margin: 5px 0; background: #f5f5f5; border-left: 3px solid blue;" ) resultItem.innerHTML = .string( - "Result \(index + 1): \(result.context)") + "Result \(index + 1): \(result.context)" + ) _ = resultsElement.appendChild(resultItem) } } } } +/// The fallback executor actor is used when the dedicated worker is not available. +actor FallbackExecutorActor {} + +enum OwnedExecutor { + case dedicated(WebWorkerDedicatedExecutor) + case fallback(FallbackExecutorActor) + + var unownedExecutor: UnownedSerialExecutor { + switch self { + case .dedicated(let executor): + return executor.asUnownedSerialExecutor() + case .fallback(let x): + return x.unownedExecutor + } + } +} + @main struct Main { @MainActor static var app: App? static func main() { JavaScriptEventLoop.installGlobalExecutor() - WebWorkerTaskExecutor.installGlobalExecutor() + let useDedicatedWorker = !(JSObject.global.disableDedicatedWorker.boolean ?? false) Task { - // Create dedicated worker and search service - let dedicatedWorker = try await WebWorkerDedicatedExecutor() - let service = SearchService(serialExecutor: dedicatedWorker) + let ownedExecutor: OwnedExecutor + if useDedicatedWorker { + // Create dedicated worker + let dedicatedWorker = try await WebWorkerDedicatedExecutor() + ownedExecutor = .dedicated(dedicatedWorker) + } else { + // Fallback to main thread executor + let fallbackExecutor = FallbackExecutorActor() + ownedExecutor = .fallback(fallbackExecutor) + } + // Create the service and app + let service = SearchService(serialExecutor: ownedExecutor) app = App(service: service) } } } #if canImport(wasi_pthread) - import wasi_pthread - import WASILibc - - /// Trick to avoid blocking the main thread. pthread_mutex_lock function is used by - /// the Swift concurrency runtime. - @_cdecl("pthread_mutex_lock") - func pthread_mutex_lock(_ mutex: UnsafeMutablePointer) -> Int32 { - // DO NOT BLOCK MAIN THREAD - var ret: Int32 - repeat { - ret = pthread_mutex_trylock(mutex) - } while ret == EBUSY - return ret - } +import wasi_pthread +import WASILibc + +/// Trick to avoid blocking the main thread. pthread_mutex_lock function is used by +/// the Swift concurrency runtime. +@_cdecl("pthread_mutex_lock") +func pthread_mutex_lock(_ mutex: UnsafeMutablePointer) -> Int32 { + // DO NOT BLOCK MAIN THREAD + var ret: Int32 + repeat { + ret = pthread_mutex_trylock(mutex) + } while ret == EBUSY + return ret +} #endif diff --git a/Examples/Basic/Package.swift b/Examples/Basic/Package.swift index f1a80aaaa..6c729741c 100644 --- a/Examples/Basic/Package.swift +++ b/Examples/Basic/Package.swift @@ -13,7 +13,7 @@ let package = Package( name: "Basic", dependencies: [ "JavaScriptKit", - .product(name: "JavaScriptEventLoop", package: "JavaScriptKit") + .product(name: "JavaScriptEventLoop", package: "JavaScriptKit"), ] ) ], diff --git a/Examples/Basic/Sources/main.swift b/Examples/Basic/Sources/main.swift index 98b8a6bb5..7ea9231e1 100644 --- a/Examples/Basic/Sources/main.swift +++ b/Examples/Basic/Sources/main.swift @@ -1,19 +1,21 @@ -import JavaScriptKit import JavaScriptEventLoop +import JavaScriptKit let alert = JSObject.global.alert.function! let document = JSObject.global.document -var divElement = document.createElement("div") +let divElement = document.createElement("div") divElement.innerText = "Hello, world" _ = document.body.appendChild(divElement) -var buttonElement = document.createElement("button") +let buttonElement = document.createElement("button") buttonElement.innerText = "Alert demo" -buttonElement.onclick = .object(JSClosure { _ in - alert("Swift is running on browser!") - return .undefined -}) +buttonElement.onclick = .object( + JSClosure { _ in + alert("Swift is running on browser!") + return .undefined + } +) _ = document.body.appendChild(buttonElement) @@ -28,21 +30,23 @@ struct Response: Decodable { let uuid: String } -var asyncButtonElement = document.createElement("button") +let asyncButtonElement = document.createElement("button") asyncButtonElement.innerText = "Fetch UUID demo" -asyncButtonElement.onclick = .object(JSClosure { _ in - Task { - do { - let response = try await fetch("https://httpbin.org/uuid").value - let json = try await JSPromise(response.json().object!)!.value - let parsedResponse = try JSValueDecoder().decode(Response.self, from: json) - alert(parsedResponse.uuid) - } catch { - print(error) +asyncButtonElement.onclick = .object( + JSClosure { _ in + Task { + do { + let response = try await fetch("https://httpbin.org/uuid").value + let json = try await JSPromise(response.json().object!)!.value + let parsedResponse = try JSValueDecoder().decode(Response.self, from: json) + alert(parsedResponse.uuid) + } catch { + print(error) + } } - } - return .undefined -}) + return .undefined + } +) _ = document.body.appendChild(asyncButtonElement) diff --git a/Examples/Embedded/Package.swift b/Examples/Embedded/Package.swift index f97638cc8..5ae19adc6 100644 --- a/Examples/Embedded/Package.swift +++ b/Examples/Embedded/Package.swift @@ -6,14 +6,14 @@ let package = Package( name: "Embedded", dependencies: [ .package(name: "JavaScriptKit", path: "../../"), - .package(url: "https://github.com/swiftwasm/swift-dlmalloc", branch: "0.1.0") + .package(url: "https://github.com/swiftwasm/swift-dlmalloc", branch: "0.1.0"), ], targets: [ .executableTarget( name: "EmbeddedApp", dependencies: [ "JavaScriptKit", - .product(name: "dlmalloc", package: "swift-dlmalloc") + .product(name: "dlmalloc", package: "swift-dlmalloc"), ], cSettings: [.unsafeFlags(["-fdeclspec"])], swiftSettings: [ @@ -28,7 +28,7 @@ let package = Package( .unsafeFlags([ "-Xclang-linker", "-nostdlib", "-Xlinker", "--no-entry", - "-Xlinker", "--export-if-defined=__main_argc_argv" + "-Xlinker", "--export-if-defined=__main_argc_argv", ]) ] ) diff --git a/Examples/Embedded/Sources/EmbeddedApp/main.swift b/Examples/Embedded/Sources/EmbeddedApp/main.swift index 2ba81dc63..610471321 100644 --- a/Examples/Embedded/Sources/EmbeddedApp/main.swift +++ b/Examples/Embedded/Sources/EmbeddedApp/main.swift @@ -7,20 +7,53 @@ print("Hello from WASM, document title: \(document.title.string ?? "")") var count = 0 -var divElement = document.createElement("div") +let divElement = document.createElement("div") divElement.innerText = .string("Count \(count)") _ = document.body.appendChild(divElement) -var buttonElement = document.createElement("button") -buttonElement.innerText = "Click me" -buttonElement.onclick = JSValue.object(JSClosure { _ in - count += 1 - divElement.innerText = .string("Count \(count)") - return .undefined -}) +let clickMeElement = document.createElement("button") +clickMeElement.innerText = "Click me" +clickMeElement.onclick = JSValue.object( + JSClosure { _ in + count += 1 + divElement.innerText = .string("Count \(count)") + return .undefined + } +) +_ = document.body.appendChild(clickMeElement) -_ = document.body.appendChild(buttonElement) +let encodeResultElement = document.createElement("pre") +let textInputElement = document.createElement("input") +textInputElement.type = "text" +textInputElement.placeholder = "Enter text to encode to UTF-8" +textInputElement.oninput = JSValue.object( + JSClosure { _ in + let textEncoder = JSObject.global.TextEncoder.function!.new() + let encode = textEncoder.encode.function! + let encodedData = JSTypedArray( + unsafelyWrapping: encode(this: textEncoder, textInputElement.value).object! + ) + encodeResultElement.innerText = .string( + encodedData.withUnsafeBytes { bytes in + bytes.map { hex($0) }.joined(separator: " ") + } + ) + return .undefined + } +) +let encoderContainer = document.createElement("div") +_ = encoderContainer.appendChild(textInputElement) +_ = encoderContainer.appendChild(encodeResultElement) +_ = document.body.appendChild(encoderContainer) func print(_ message: String) { _ = JSObject.global.console.log(message) } + +func hex(_ value: UInt8) -> String { + var result = "0x" + let hexChars: [Character] = ["0", "1", "2", "3", "4", "5", "6", "7", "8", "9", "A", "B", "C", "D", "E", "F"] + result.append(hexChars[Int(value / 16)]) + result.append(hexChars[Int(value % 16)]) + return result +} diff --git a/Examples/ExportSwift/Package.swift b/Examples/ExportSwift/Package.swift new file mode 100644 index 000000000..191278fda --- /dev/null +++ b/Examples/ExportSwift/Package.swift @@ -0,0 +1,25 @@ +// swift-tools-version:6.0 + +import PackageDescription + +let package = Package( + name: "MyApp", + platforms: [ + .macOS(.v14) + ], + dependencies: [.package(name: "JavaScriptKit", path: "../../")], + targets: [ + .executableTarget( + name: "MyApp", + dependencies: [ + "JavaScriptKit" + ], + swiftSettings: [ + .enableExperimentalFeature("Extern") + ], + plugins: [ + .plugin(name: "BridgeJS", package: "JavaScriptKit") + ] + ) + ] +) diff --git a/Examples/ExportSwift/Sources/main.swift b/Examples/ExportSwift/Sources/main.swift new file mode 100644 index 000000000..449155214 --- /dev/null +++ b/Examples/ExportSwift/Sources/main.swift @@ -0,0 +1,34 @@ +import JavaScriptKit + +// Mark functions you want to export to JavaScript with the @JS attribute +// This function will be available as `renderCircleSVG(size)` in JavaScript +@JS public func renderCircleSVG(size: Int) -> String { + let strokeWidth = 3 + let strokeColor = "black" + let fillColor = "red" + let cx = size / 2 + let cy = size / 2 + let r = (size / 2) - strokeWidth + var svg = "" + svg += + "" + svg += "" + return svg +} + +// Classes can also be exported using the @JS attribute +// This class will be available as a constructor in JavaScript: new Greeter("name") +@JS class Greeter { + var name: String + + // Use @JS for initializers you want to expose + @JS init(name: String) { + self.name = name + } + + // Methods need the @JS attribute to be accessible from JavaScript + // This method will be available as greeter.greet() in JavaScript + @JS public func greet() -> String { + "Hello, \(name)!" + } +} diff --git a/Examples/ExportSwift/index.html b/Examples/ExportSwift/index.html new file mode 100644 index 000000000..ef3d190ac --- /dev/null +++ b/Examples/ExportSwift/index.html @@ -0,0 +1,12 @@ + + + + + Getting Started + + + + + + + diff --git a/Examples/ExportSwift/index.js b/Examples/ExportSwift/index.js new file mode 100644 index 000000000..4c5576b25 --- /dev/null +++ b/Examples/ExportSwift/index.js @@ -0,0 +1,14 @@ +import { init } from "./.build/plugins/PackageToJS/outputs/Package/index.js"; +const { exports } = await init({}); + +const Greeter = exports.Greeter; +const greeter = new Greeter("World"); +const circle = exports.renderCircleSVG(100); + +// Display the results +const textOutput = document.createElement("div"); +textOutput.innerText = greeter.greet() +document.body.appendChild(textOutput); +const circleOutput = document.createElement("div"); +circleOutput.innerHTML = circle; +document.body.appendChild(circleOutput); diff --git a/Examples/ImportTS/Package.swift b/Examples/ImportTS/Package.swift new file mode 100644 index 000000000..4809ec006 --- /dev/null +++ b/Examples/ImportTS/Package.swift @@ -0,0 +1,29 @@ +// swift-tools-version:6.0 + +import PackageDescription + +let package = Package( + name: "MyApp", + platforms: [ + .macOS(.v10_15), + .iOS(.v13), + .tvOS(.v13), + .watchOS(.v6), + .macCatalyst(.v13), + ], + dependencies: [.package(name: "JavaScriptKit", path: "../../")], + targets: [ + .executableTarget( + name: "MyApp", + dependencies: [ + "JavaScriptKit" + ], + swiftSettings: [ + .enableExperimentalFeature("Extern") + ], + plugins: [ + .plugin(name: "BridgeJS", package: "JavaScriptKit") + ] + ) + ] +) diff --git a/Examples/ImportTS/Sources/bridge.d.ts b/Examples/ImportTS/Sources/bridge.d.ts new file mode 100644 index 000000000..856bba9c4 --- /dev/null +++ b/Examples/ImportTS/Sources/bridge.d.ts @@ -0,0 +1,24 @@ +// Function definition to expose console.log to Swift +// Will be imported as a Swift function: consoleLog(message: String) +export function consoleLog(message: string): void + +// TypeScript interface types are converted to Swift structs +// This defines a subset of the browser's HTMLElement interface +type HTMLElement = Pick & { + // Methods with object parameters are properly handled + appendChild(child: HTMLElement): void +} + +// TypeScript object type with read-only properties +// Properties will become Swift properties with appropriate access level +type Document = { + // Regular property - will be read/write in Swift + title: string + // Read-only property - will be read-only in Swift + readonly body: HTMLElement + // Method returning an object - will become a Swift method returning an HTMLElement + createElement(tagName: string): HTMLElement +} +// Function returning a complex object +// Will be imported as a Swift function: getDocument() -> Document +export function getDocument(): Document diff --git a/Examples/ImportTS/Sources/main.swift b/Examples/ImportTS/Sources/main.swift new file mode 100644 index 000000000..4328b0a3b --- /dev/null +++ b/Examples/ImportTS/Sources/main.swift @@ -0,0 +1,26 @@ +import JavaScriptKit + +// This function is automatically generated by the @JS plugin +// It demonstrates how to use TypeScript functions and types imported from bridge.d.ts +@JS public func run() { + // Call the imported consoleLog function defined in bridge.d.ts + consoleLog("Hello, World!") + + // Get the document object - this comes from the imported getDocument() function + let document = getDocument() + + // Access and modify properties - the title property is read/write + document.title = "Hello, World!" + + // Access read-only properties - body is defined as readonly in TypeScript + let body = document.body + + // Create a new element using the document.createElement method + let h1 = document.createElement("h1") + + // Set properties on the created element + h1.innerText = "Hello, World!" + + // Call methods on objects - appendChild is defined in the HTMLElement interface + body.appendChild(h1) +} diff --git a/Examples/ImportTS/index.html b/Examples/ImportTS/index.html new file mode 100644 index 000000000..31881c499 --- /dev/null +++ b/Examples/ImportTS/index.html @@ -0,0 +1,16 @@ + + + + + Getting Started + + + + + +
+
+

+
+
+
diff --git a/Examples/ImportTS/index.js b/Examples/ImportTS/index.js
new file mode 100644
index 000000000..9452b7ec7
--- /dev/null
+++ b/Examples/ImportTS/index.js
@@ -0,0 +1,13 @@
+import { init } from "./.build/plugins/PackageToJS/outputs/Package/index.js";
+const { exports } = await init({
+    imports: {
+        consoleLog: (message) => {
+            console.log(message);
+        },
+        getDocument: () => {
+            return document;
+        },
+    }
+});
+
+exports.run()
diff --git a/Examples/Multithreading/Package.resolved b/Examples/Multithreading/Package.resolved
index 1354cc039..f55b8400a 100644
--- a/Examples/Multithreading/Package.resolved
+++ b/Examples/Multithreading/Package.resolved
@@ -1,5 +1,5 @@
 {
-  "originHash" : "e66f4c272838a860049b7e3528f1db03ee6ae99c2b21c3b6ea58a293be4db41b",
+  "originHash" : "072d03a6e24e01bd372682a6090adb80cf29dea39421e065de6ff8853de704c9",
   "pins" : [
     {
       "identity" : "chibi-ray",
@@ -8,6 +8,15 @@
       "state" : {
         "revision" : "c8cab621a3338dd2f8e817d3785362409d3b8cf1"
       }
+    },
+    {
+      "identity" : "swift-syntax",
+      "kind" : "remoteSourceControl",
+      "location" : "https://github.com/swiftlang/swift-syntax",
+      "state" : {
+        "revision" : "0687f71944021d616d34d922343dcef086855920",
+        "version" : "600.0.1"
+      }
     }
   ],
   "version" : 3
diff --git a/Examples/Multithreading/Package.swift b/Examples/Multithreading/Package.swift
index 211f359a6..4d1ebde70 100644
--- a/Examples/Multithreading/Package.swift
+++ b/Examples/Multithreading/Package.swift
@@ -7,7 +7,10 @@ let package = Package(
     platforms: [.macOS("15"), .iOS("18"), .watchOS("11"), .tvOS("18"), .visionOS("2")],
     dependencies: [
         .package(path: "../../"),
-        .package(url: "https://github.com/kateinoigakukun/chibi-ray", revision: "c8cab621a3338dd2f8e817d3785362409d3b8cf1"),
+        .package(
+            url: "https://github.com/kateinoigakukun/chibi-ray",
+            revision: "c8cab621a3338dd2f8e817d3785362409d3b8cf1"
+        ),
     ],
     targets: [
         .executableTarget(
@@ -17,6 +20,6 @@ let package = Package(
                 .product(name: "JavaScriptEventLoop", package: "JavaScriptKit"),
                 .product(name: "ChibiRay", package: "chibi-ray"),
             ]
-        ),
+        )
     ]
 )
diff --git a/Examples/Multithreading/Sources/MyApp/Scene.swift b/Examples/Multithreading/Sources/MyApp/Scene.swift
index 5f6d467c5..bddde1715 100644
--- a/Examples/Multithreading/Sources/MyApp/Scene.swift
+++ b/Examples/Multithreading/Sources/MyApp/Scene.swift
@@ -60,7 +60,7 @@ func createDemoScene(size: Int) -> Scene {
                         surface: .diffuse
                     )
                 )
-            )
+            ),
         ],
         lights: [
             .spherical(
@@ -83,7 +83,7 @@ func createDemoScene(size: Int) -> Scene {
                     color: Color(red: 0.8, green: 0.8, blue: 0.8),
                     intensity: 0.2
                 )
-            )
+            ),
         ],
         shadowBias: 1e-13,
         maxRecursionDepth: 10
diff --git a/Examples/Multithreading/Sources/MyApp/main.swift b/Examples/Multithreading/Sources/MyApp/main.swift
index 29cb89f2f..f9839ffde 100644
--- a/Examples/Multithreading/Sources/MyApp/main.swift
+++ b/Examples/Multithreading/Sources/MyApp/main.swift
@@ -1,19 +1,18 @@
 import ChibiRay
-import JavaScriptKit
 import JavaScriptEventLoop
+import JavaScriptKit
 
 JavaScriptEventLoop.installGlobalExecutor()
-WebWorkerTaskExecutor.installGlobalExecutor()
 
 func renderInCanvas(ctx: JSObject, image: ImageView) {
     let imageData = ctx.createImageData!(image.width, image.height).object!
     let data = imageData.data.object!
-    
+
     for y in 0...allocate(capacity: scene.width * scene.height)
     // Initialize the buffer with black color
@@ -73,12 +78,15 @@ func render(scene: Scene, ctx: JSObject, renderTimeElement: JSObject, concurrenc
     }
 
     var checkTimer: JSValue?
-    checkTimer = JSObject.global.setInterval!(JSClosure { _ in
-        print("Checking thread work...")
-        renderInCanvas(ctx: ctx, image: imageView)
-        updateRenderTime()
-        return .undefined
-    }, 250)
+    checkTimer = JSObject.global.setInterval!(
+        JSClosure { _ in
+            print("Checking thread work...")
+            renderInCanvas(ctx: ctx, image: imageView)
+            updateRenderTime()
+            return .undefined
+        },
+        250
+    )
 
     await withTaskGroup(of: Void.self) { group in
         let yStride = scene.height / concurrency
@@ -117,10 +125,16 @@ func onClick() async throws {
 
     let scene = createDemoScene(size: size)
     let executor = background ? try await WebWorkerTaskExecutor(numberOfThreads: concurrency) : nil
-    canvasElement.width  = .number(Double(scene.width))
+    canvasElement.width = .number(Double(scene.width))
     canvasElement.height = .number(Double(scene.height))
 
-    await render(scene: scene, ctx: ctx, renderTimeElement: renderTimeElement, concurrency: concurrency, executor: executor)
+    await render(
+        scene: scene,
+        ctx: ctx,
+        renderTimeElement: renderTimeElement,
+        concurrency: concurrency,
+        executor: executor
+    )
     executor?.terminate()
     print("Render done")
 }
@@ -130,19 +144,21 @@ func main() async throws {
     let concurrencyElement = JSObject.global.document.getElementById("concurrency").object!
     concurrencyElement.value = JSObject.global.navigator.hardwareConcurrency
 
-    _ = renderButtonElement.addEventListener!("click", JSClosure { _ in
-        Task {
-            try await onClick()
+    _ = renderButtonElement.addEventListener!(
+        "click",
+        JSClosure { _ in
+            Task {
+                try await onClick()
+            }
+            return JSValue.undefined
         }
-        return JSValue.undefined
-    })
+    )
 }
 
 Task {
     try await main()
 }
 
-
 #if canImport(wasi_pthread)
 import wasi_pthread
 import WASILibc
diff --git a/Examples/OffscrenCanvas/Package.swift b/Examples/OffscrenCanvas/Package.swift
index 7fc45ad1b..ca6d7357f 100644
--- a/Examples/OffscrenCanvas/Package.swift
+++ b/Examples/OffscrenCanvas/Package.swift
@@ -6,7 +6,7 @@ let package = Package(
     name: "Example",
     platforms: [.macOS("15"), .iOS("18"), .watchOS("11"), .tvOS("18"), .visionOS("2")],
     dependencies: [
-        .package(path: "../../"),
+        .package(path: "../../")
     ],
     targets: [
         .executableTarget(
@@ -15,6 +15,6 @@ let package = Package(
                 .product(name: "JavaScriptKit", package: "JavaScriptKit"),
                 .product(name: "JavaScriptEventLoop", package: "JavaScriptKit"),
             ]
-        ),
+        )
     ]
 )
diff --git a/Examples/OffscrenCanvas/Sources/MyApp/main.swift b/Examples/OffscrenCanvas/Sources/MyApp/main.swift
index 67e087122..5709c664c 100644
--- a/Examples/OffscrenCanvas/Sources/MyApp/main.swift
+++ b/Examples/OffscrenCanvas/Sources/MyApp/main.swift
@@ -2,7 +2,6 @@ import JavaScriptEventLoop
 import JavaScriptKit
 
 JavaScriptEventLoop.installGlobalExecutor()
-WebWorkerTaskExecutor.installGlobalExecutor()
 
 protocol CanvasRenderer {
     func render(canvas: JSObject, size: Int) async throws
@@ -56,7 +55,8 @@ func startFPSMonitor() {
             JSClosure { _ in
                 countFrame()
                 return .undefined
-            })
+            }
+        )
     }
 
     // Start counting
@@ -107,14 +107,16 @@ func main() async throws {
                 try await onClick(renderer: renderer)
             }
             return JSValue.undefined
-        })
+        }
+    )
 
     _ = cancelButtonElement.addEventListener!(
         "click",
         JSClosure { _ in
             renderingTask?.cancel()
             return JSValue.undefined
-        })
+        }
+    )
 }
 
 Task {
@@ -122,18 +124,18 @@ Task {
 }
 
 #if canImport(wasi_pthread)
-    import wasi_pthread
-    import WASILibc
-
-    /// Trick to avoid blocking the main thread. pthread_mutex_lock function is used by
-    /// the Swift concurrency runtime.
-    @_cdecl("pthread_mutex_lock")
-    func pthread_mutex_lock(_ mutex: UnsafeMutablePointer) -> Int32 {
-        // DO NOT BLOCK MAIN THREAD
-        var ret: Int32
-        repeat {
-            ret = pthread_mutex_trylock(mutex)
-        } while ret == EBUSY
-        return ret
-    }
+import wasi_pthread
+import WASILibc
+
+/// Trick to avoid blocking the main thread. pthread_mutex_lock function is used by
+/// the Swift concurrency runtime.
+@_cdecl("pthread_mutex_lock")
+func pthread_mutex_lock(_ mutex: UnsafeMutablePointer) -> Int32 {
+    // DO NOT BLOCK MAIN THREAD
+    var ret: Int32
+    repeat {
+        ret = pthread_mutex_trylock(mutex)
+    } while ret == EBUSY
+    return ret
+}
 #endif
diff --git a/Examples/OffscrenCanvas/Sources/MyApp/render.swift b/Examples/OffscrenCanvas/Sources/MyApp/render.swift
index 714cac184..6a9d057a9 100644
--- a/Examples/OffscrenCanvas/Sources/MyApp/render.swift
+++ b/Examples/OffscrenCanvas/Sources/MyApp/render.swift
@@ -8,12 +8,17 @@ func sleepOnThread(milliseconds: Int, isolation: isolated (any Actor)? = #isolat
             JSOneshotClosure { _ in
                 continuation.resume()
                 return JSValue.undefined
-            }, milliseconds
+            },
+            milliseconds
         )
     }
 }
 
-func renderAnimation(canvas: JSObject, size: Int, isolation: isolated (any Actor)? = #isolation)
+func renderAnimation(
+    canvas: JSObject,
+    size: Int,
+    isolation: isolated (any Actor)? = #isolation
+)
     async throws
 {
     let ctx = canvas.getContext!("2d").object!
diff --git a/Examples/Testing/Package.swift b/Examples/Testing/Package.swift
index d9d1719f0..2a81bfa7a 100644
--- a/Examples/Testing/Package.swift
+++ b/Examples/Testing/Package.swift
@@ -7,7 +7,8 @@ let package = Package(
     products: [
         .library(
             name: "Counter",
-            targets: ["Counter"]),
+            targets: ["Counter"]
+        )
     ],
     dependencies: [.package(name: "JavaScriptKit", path: "../../")],
     targets: [
@@ -15,13 +16,14 @@ let package = Package(
             name: "Counter",
             dependencies: [
                 .product(name: "JavaScriptKit", package: "JavaScriptKit")
-            ]),
+            ]
+        ),
         .testTarget(
             name: "CounterTests",
             dependencies: [
                 "Counter",
                 // This is needed to run the tests in the JavaScript event loop
-                .product(name: "JavaScriptEventLoopTestSupport", package: "JavaScriptKit")
+                .product(name: "JavaScriptEventLoopTestSupport", package: "JavaScriptKit"),
             ]
         ),
     ]
diff --git a/Examples/Testing/Tests/CounterTests/CounterTests.swift b/Examples/Testing/Tests/CounterTests/CounterTests.swift
index 4421c1223..ec92cd14c 100644
--- a/Examples/Testing/Tests/CounterTests/CounterTests.swift
+++ b/Examples/Testing/Tests/CounterTests/CounterTests.swift
@@ -1,3 +1,5 @@
+import XCTest
+
 @testable import Counter
 
 #if canImport(Testing)
@@ -18,8 +20,6 @@ import Testing
 
 #endif
 
-import XCTest
-
 class CounterTests: XCTestCase {
     func testIncrement() async {
         var counter = Counter()
diff --git a/Examples/Testing/package.json b/Examples/Testing/package.json
new file mode 100644
index 000000000..2ce18c0a2
--- /dev/null
+++ b/Examples/Testing/package.json
@@ -0,0 +1,5 @@
+{
+    "devDependencies": {
+        "playwright": "^1.52.0"
+    }
+}
diff --git a/IntegrationTests/Makefile b/IntegrationTests/Makefile
deleted file mode 100644
index 54a656fd1..000000000
--- a/IntegrationTests/Makefile
+++ /dev/null
@@ -1,36 +0,0 @@
-CONFIGURATION ?= debug
-SWIFT_BUILD_FLAGS ?=
-NODEJS_FLAGS ?=
-
-NODEJS = node --experimental-wasi-unstable-preview1 $(NODEJS_FLAGS)
-
-FORCE:
-TestSuites/.build/$(CONFIGURATION)/%.wasm: FORCE
-	swift build --package-path TestSuites \
-	            --product $(basename $(notdir $@)) \
-	            --configuration $(CONFIGURATION) \
-	            -Xswiftc -Xclang-linker -Xswiftc -mexec-model=reactor \
-	            -Xlinker --export-if-defined=main -Xlinker --export-if-defined=__main_argc_argv \
-		    --static-swift-stdlib -Xswiftc -static-stdlib \
-		    $(SWIFT_BUILD_FLAGS)
-
-dist/%.wasm: TestSuites/.build/$(CONFIGURATION)/%.wasm
-	mkdir -p dist
-	cp $< $@
-
-node_modules: package-lock.json
-	npm ci
-
-.PHONY: build_rt
-build_rt: node_modules
-	cd .. && npm run build
-
-.PHONY: benchmark_setup
-benchmark_setup: build_rt dist/BenchmarkTests.wasm
-
-.PHONY: run_benchmark
-run_benchmark:
-	$(NODEJS) bin/benchmark-tests.js
-
-.PHONY: benchmark
-benchmark: benchmark_setup run_benchmark
diff --git a/IntegrationTests/TestSuites/.gitignore b/IntegrationTests/TestSuites/.gitignore
deleted file mode 100644
index 95c432091..000000000
--- a/IntegrationTests/TestSuites/.gitignore
+++ /dev/null
@@ -1,5 +0,0 @@
-.DS_Store
-/.build
-/Packages
-/*.xcodeproj
-xcuserdata/
diff --git a/IntegrationTests/TestSuites/Package.swift b/IntegrationTests/TestSuites/Package.swift
deleted file mode 100644
index 3d583d082..000000000
--- a/IntegrationTests/TestSuites/Package.swift
+++ /dev/null
@@ -1,23 +0,0 @@
-// swift-tools-version:5.7
-
-import PackageDescription
-
-let package = Package(
-    name: "TestSuites",
-    platforms: [
-        // This package doesn't work on macOS host, but should be able to be built for it
-        // for developing on Xcode. This minimum version requirement is to prevent availability
-        // errors for Concurrency API, whose runtime support is shipped from macOS 12.0
-        .macOS("12.0"),
-    ],
-    products: [
-        .executable(
-            name: "BenchmarkTests", targets: ["BenchmarkTests"]
-        ),
-    ],
-    dependencies: [.package(name: "JavaScriptKit", path: "../../")],
-    targets: [
-        .target(name: "CHelpers"),
-        .executableTarget(name: "BenchmarkTests", dependencies: ["JavaScriptKit", "CHelpers"]),
-    ]
-)
diff --git a/IntegrationTests/TestSuites/Sources/BenchmarkTests/Benchmark.swift b/IntegrationTests/TestSuites/Sources/BenchmarkTests/Benchmark.swift
deleted file mode 100644
index 4562898fb..000000000
--- a/IntegrationTests/TestSuites/Sources/BenchmarkTests/Benchmark.swift
+++ /dev/null
@@ -1,19 +0,0 @@
-import JavaScriptKit
-
-class Benchmark {
-    init(_ title: String) {
-        self.title = title
-    }
-
-    let title: String
-    let runner = JSObject.global.benchmarkRunner.function!
-
-    func testSuite(_ name: String, _ body: @escaping (Int) -> Void) {
-        let jsBody = JSClosure { arguments -> JSValue in
-            let iteration = Int(arguments[0].number!)
-            body(iteration)
-            return .undefined
-        }
-        runner("\(title)/\(name)", jsBody)
-    }
-}
diff --git a/IntegrationTests/TestSuites/Sources/BenchmarkTests/main.swift b/IntegrationTests/TestSuites/Sources/BenchmarkTests/main.swift
deleted file mode 100644
index 2803f0137..000000000
--- a/IntegrationTests/TestSuites/Sources/BenchmarkTests/main.swift
+++ /dev/null
@@ -1,85 +0,0 @@
-import JavaScriptKit
-import CHelpers
-
-let serialization = Benchmark("Serialization")
-
-let noopFunction = JSObject.global.noopFunction.function!
-
-serialization.testSuite("JavaScript function call through Wasm import") { n in
-    for _ in 0 ..< n {
-        benchmark_helper_noop()
-    }
-}
-
-serialization.testSuite("JavaScript function call through Wasm import with int") { n in
-    for _ in 0 ..< n {
-        benchmark_helper_noop_with_int(42)
-    }
-}
-
-serialization.testSuite("JavaScript function call from Swift") { n in
-    for _ in 0 ..< n {
-        _ = noopFunction()
-    }
-}
-
-let swiftInt: Double = 42
-serialization.testSuite("Swift Int to JavaScript with assignment") { n in
-    let jsNumber = JSValue.number(swiftInt)
-    let object = JSObject.global
-    let key = JSString("numberValue")
-    for _ in 0 ..< n {
-        object[key] = jsNumber
-    }
-}
-
-serialization.testSuite("Swift Int to JavaScript with call") { n in
-    let jsNumber = JSValue.number(swiftInt)
-    for _ in 0 ..< n {
-        _ = noopFunction(jsNumber)
-    }
-}
-
-serialization.testSuite("JavaScript Number to Swift Int") { n in
-    let object = JSObject.global
-    let key = JSString("jsNumber")
-    for _ in 0 ..< n {
-        _ = object[key].number
-    }
-}
-
-let swiftString = "Hello, world"
-serialization.testSuite("Swift String to JavaScript with assignment") { n in
-    let jsString = JSValue.string(swiftString)
-    let object = JSObject.global
-    let key = JSString("stringValue")
-    for _ in 0 ..< n {
-        object[key] = jsString
-    }
-}
-
-serialization.testSuite("Swift String to JavaScript with call") { n in
-    let jsString = JSValue.string(swiftString)
-    for _ in 0 ..< n {
-        _ = noopFunction(jsString)
-    }
-}
-
-serialization.testSuite("JavaScript String to Swift String") { n in
-    let object = JSObject.global
-    let key = JSString("jsString")
-    for _ in 0 ..< n {
-        _ = object[key].string
-    }
-}
-
-let objectHeap = Benchmark("Object heap")
-
-let global = JSObject.global
-let Object = global.Object.function!
-global.objectHeapDummy = .object(Object.new())
-objectHeap.testSuite("Increment and decrement RC") { n in
-    for _ in 0 ..< n {
-        _ = global.objectHeapDummy
-    }
-}
diff --git a/IntegrationTests/TestSuites/Sources/CHelpers/helpers.c b/IntegrationTests/TestSuites/Sources/CHelpers/helpers.c
deleted file mode 100644
index 8922cb735..000000000
--- a/IntegrationTests/TestSuites/Sources/CHelpers/helpers.c
+++ /dev/null
@@ -1,4 +0,0 @@
-int growMemory(int pages) {
-    return __builtin_wasm_memory_grow(0, pages);
-}
-
diff --git a/IntegrationTests/TestSuites/Sources/CHelpers/include/helpers.h b/IntegrationTests/TestSuites/Sources/CHelpers/include/helpers.h
deleted file mode 100644
index dea7a96d4..000000000
--- a/IntegrationTests/TestSuites/Sources/CHelpers/include/helpers.h
+++ /dev/null
@@ -1,10 +0,0 @@
-/// Ask host to grow WebAssembly module's allocated memory
-///
-/// @param pages Number of memory pages to increase memory by.
-int growMemory(int pages);
-
-__attribute__((__import_module__("benchmark_helper"), __import_name__("noop")))
-extern void benchmark_helper_noop(void);
-
-__attribute__((__import_module__("benchmark_helper"), __import_name__("noop_with_int")))
-extern void benchmark_helper_noop_with_int(int);
diff --git a/IntegrationTests/TestSuites/Sources/CHelpers/include/module.modulemap b/IntegrationTests/TestSuites/Sources/CHelpers/include/module.modulemap
deleted file mode 100644
index 3503a233f..000000000
--- a/IntegrationTests/TestSuites/Sources/CHelpers/include/module.modulemap
+++ /dev/null
@@ -1,4 +0,0 @@
-module CHelpers {
-    header "helpers.h"
-    export *
-}
diff --git a/IntegrationTests/bin/benchmark-tests.js b/IntegrationTests/bin/benchmark-tests.js
deleted file mode 100644
index 0d8b5410a..000000000
--- a/IntegrationTests/bin/benchmark-tests.js
+++ /dev/null
@@ -1,70 +0,0 @@
-import { startWasiTask } from "../lib.js";
-import { performance } from "perf_hooks";
-
-const SAMPLE_ITERATION = 1000000
-
-global.benchmarkRunner = function (name, body) {
-    console.log(`Running '${name}' ...`);
-    const startTime = performance.now();
-    body(SAMPLE_ITERATION);
-    const endTime = performance.now();
-    console.log("done " + (endTime - startTime) + " ms");
-};
-
-global.noopFunction = function () {}
-global.jsNumber = 42
-global.jsString = "myString"
-
-class JSBenchmark {
-    constructor(title) {
-        this.title = title;
-    }
-    testSuite(name, body) {
-        benchmarkRunner(`${this.title}/${name}`, (iteration) => {
-            body(iteration);
-        });
-    }
-}
-
-const serialization = new JSBenchmark("Serialization");
-serialization.testSuite("Call JavaScript function directly", (n) => {
-    for (let idx = 0; idx < n; idx++) {
-        global.noopFunction()
-    }
-});
-
-serialization.testSuite("Assign JavaScript number directly", (n) => {
-    const jsNumber = 42;
-    const object = global;
-    const key = "numberValue"
-    for (let idx = 0; idx < n; idx++) {
-        object[key] = jsNumber;
-    }
-});
-
-serialization.testSuite("Call with JavaScript number directly", (n) => {
-    const jsNumber = 42;
-    for (let idx = 0; idx < n; idx++) {
-        global.noopFunction(jsNumber)
-    }
-});
-
-serialization.testSuite("Write JavaScript string directly", (n) => {
-    const jsString = "Hello, world";
-    const object = global;
-    const key = "stringValue"
-    for (let idx = 0; idx < n; idx++) {
-        object[key] = jsString;
-    }
-});
-
-serialization.testSuite("Call with JavaScript string directly", (n) => {
-    const jsString = "Hello, world";
-    for (let idx = 0; idx < n; idx++) {
-        global.noopFunction(jsString)
-    }
-});
-
-startWasiTask("./dist/BenchmarkTests.wasm").catch((err) => {
-    console.log(err);
-});
diff --git a/IntegrationTests/lib.js b/IntegrationTests/lib.js
deleted file mode 100644
index d9c424f0e..000000000
--- a/IntegrationTests/lib.js
+++ /dev/null
@@ -1,86 +0,0 @@
-import { SwiftRuntime } from "javascript-kit-swift"
-import { WASI as NodeWASI } from "wasi"
-import { WASI as MicroWASI, useAll } from "uwasi"
-import * as fs from "fs/promises"
-import path from "path";
-
-const WASI = {
-    MicroWASI: ({ args }) => {
-        const wasi = new MicroWASI({
-            args: args,
-            env: {},
-            features: [useAll()],
-        })
-
-        return {
-            wasiImport: wasi.wasiImport,
-            setInstance(instance) {
-                wasi.instance = instance;
-            },
-            start(instance, swift) {
-                wasi.initialize(instance);
-                swift.main();
-            }
-        }
-    },
-    Node: ({ args }) => {
-        const wasi = new NodeWASI({
-            args: args,
-            env: {},
-            preopens: {
-              "/": "./",
-            },
-            returnOnExit: false,
-            version: "preview1",
-        })
-
-        return {
-            wasiImport: wasi.wasiImport,
-            start(instance, swift) {
-                wasi.initialize(instance);
-                swift.main();
-            }
-        }
-    },
-};
-
-const selectWASIBackend = () => {
-    const value = process.env["JAVASCRIPTKIT_WASI_BACKEND"]
-    if (value) {
-        return value;
-    }
-    return "Node"
-};
-
-function constructBaseImportObject(wasi, swift) {
-    return {
-        wasi_snapshot_preview1: wasi.wasiImport,
-        javascript_kit: swift.wasmImports,
-        benchmark_helper: {
-            noop: () => {},
-            noop_with_int: (_) => {},
-        },
-    }
-}
-
-export const startWasiTask = async (wasmPath, wasiConstructorKey = selectWASIBackend()) => {
-    // Fetch our Wasm File
-    const wasmBinary = await fs.readFile(wasmPath);
-    const programName = wasmPath;
-    const args = [path.basename(programName)];
-    args.push(...process.argv.slice(3));
-    const wasi = WASI[wasiConstructorKey]({ args });
-
-    const module = await WebAssembly.compile(wasmBinary);
-
-    const swift = new SwiftRuntime();
-
-    const importObject = constructBaseImportObject(wasi, swift);
-
-    // Instantiate the WebAssembly file
-    const instance = await WebAssembly.instantiate(module, importObject);
-
-    swift.setInstance(instance);
-    // Start the WebAssembly WASI instance!
-    wasi.start(instance, swift);
-};
diff --git a/IntegrationTests/package-lock.json b/IntegrationTests/package-lock.json
deleted file mode 100644
index 9ea81b961..000000000
--- a/IntegrationTests/package-lock.json
+++ /dev/null
@@ -1,86 +0,0 @@
-{
-  "name": "IntegrationTests",
-  "lockfileVersion": 2,
-  "requires": true,
-  "packages": {
-    "": {
-      "dependencies": {
-        "javascript-kit-swift": "file:..",
-        "uwasi": "^1.2.0"
-      }
-    },
-    "..": {
-      "name": "javascript-kit-swift",
-      "version": "0.0.0",
-      "license": "MIT",
-      "devDependencies": {
-        "@rollup/plugin-typescript": "^8.3.1",
-        "prettier": "2.6.1",
-        "rollup": "^2.70.0",
-        "tslib": "^2.3.1",
-        "typescript": "^4.6.3"
-      }
-    },
-    "../node_modules/prettier": {
-      "version": "2.1.2",
-      "integrity": "sha512-16c7K+x4qVlJg9rEbXl7HEGmQyZlG4R9AgP+oHKRMsMsuk8s+ATStlf1NpDqyBI1HpVyfjLOeMhH2LvuNvV5Vg==",
-      "dev": true,
-      "bin": {
-        "prettier": "bin-prettier.js"
-      },
-      "engines": {
-        "node": ">=10.13.0"
-      }
-    },
-    "../node_modules/typescript": {
-      "version": "4.4.2",
-      "integrity": "sha512-gzP+t5W4hdy4c+68bfcv0t400HVJMMd2+H9B7gae1nQlBzCqvrXX+6GL/b3GAgyTH966pzrZ70/fRjwAtZksSQ==",
-      "dev": true,
-      "bin": {
-        "tsc": "bin/tsc",
-        "tsserver": "bin/tsserver"
-      },
-      "engines": {
-        "node": ">=4.2.0"
-      }
-    },
-    "node_modules/javascript-kit-swift": {
-      "resolved": "..",
-      "link": true
-    },
-    "node_modules/uwasi": {
-      "version": "1.2.0",
-      "resolved": "https://registry.npmjs.org/uwasi/-/uwasi-1.2.0.tgz",
-      "integrity": "sha512-+U3ajjQgx/Xh1/ZNrgH0EzM5qI2czr94oz3DPDwTvUIlM4SFpDjTqJzDA3xcqlTmpp2YGpxApmjwZfablMUoOg=="
-    }
-  },
-  "dependencies": {
-    "javascript-kit-swift": {
-      "version": "file:..",
-      "requires": {
-        "@rollup/plugin-typescript": "^8.3.1",
-        "prettier": "2.6.1",
-        "rollup": "^2.70.0",
-        "tslib": "^2.3.1",
-        "typescript": "^4.6.3"
-      },
-      "dependencies": {
-        "prettier": {
-          "version": "2.1.2",
-          "integrity": "sha512-16c7K+x4qVlJg9rEbXl7HEGmQyZlG4R9AgP+oHKRMsMsuk8s+ATStlf1NpDqyBI1HpVyfjLOeMhH2LvuNvV5Vg==",
-          "dev": true
-        },
-        "typescript": {
-          "version": "4.4.2",
-          "integrity": "sha512-gzP+t5W4hdy4c+68bfcv0t400HVJMMd2+H9B7gae1nQlBzCqvrXX+6GL/b3GAgyTH966pzrZ70/fRjwAtZksSQ==",
-          "dev": true
-        }
-      }
-    },
-    "uwasi": {
-      "version": "1.2.0",
-      "resolved": "https://registry.npmjs.org/uwasi/-/uwasi-1.2.0.tgz",
-      "integrity": "sha512-+U3ajjQgx/Xh1/ZNrgH0EzM5qI2czr94oz3DPDwTvUIlM4SFpDjTqJzDA3xcqlTmpp2YGpxApmjwZfablMUoOg=="
-    }
-  }
-}
diff --git a/IntegrationTests/package.json b/IntegrationTests/package.json
deleted file mode 100644
index 8491e91fb..000000000
--- a/IntegrationTests/package.json
+++ /dev/null
@@ -1,8 +0,0 @@
-{
-  "private": true,
-  "type": "module",
-  "dependencies": {
-    "uwasi": "^1.2.0",
-    "javascript-kit-swift": "file:.."
-  }
-}
diff --git a/Makefile b/Makefile
index 3764ed06a..e3f41caeb 100644
--- a/Makefile
+++ b/Makefile
@@ -1,22 +1,13 @@
-MAKEFILE_DIR := $(dir $(lastword $(MAKEFILE_LIST)))
-
 SWIFT_SDK_ID ?= wasm32-unknown-wasi
-SWIFT_BUILD_FLAGS := --swift-sdk $(SWIFT_SDK_ID)
 
 .PHONY: bootstrap
 bootstrap:
 	npm ci
-	npx playwright install
-
-.PHONY: build
-build:
-	swift build --triple wasm32-unknown-wasi
-	npm run build
 
 .PHONY: unittest
 unittest:
 	@echo Running unit tests
-	swift package --swift-sdk "$(SWIFT_SDK_ID)" \
+	env JAVASCRIPTKIT_EXPERIMENTAL_BRIDGEJS=1 swift package --swift-sdk "$(SWIFT_SDK_ID)" \
 	    --disable-sandbox \
 	    -Xlinker --stack-first \
 	    -Xlinker --global-base=524288 \
@@ -24,19 +15,8 @@ unittest:
 	    -Xlinker stack-size=524288 \
 	    js test --prelude ./Tests/prelude.mjs
 
-.PHONY: benchmark_setup
-benchmark_setup:
-	SWIFT_BUILD_FLAGS="$(SWIFT_BUILD_FLAGS)" CONFIGURATION=release $(MAKE) -C IntegrationTests benchmark_setup
-
-.PHONY: run_benchmark
-run_benchmark:
-	SWIFT_BUILD_FLAGS="$(SWIFT_BUILD_FLAGS)" CONFIGURATION=release $(MAKE) -s -C IntegrationTests run_benchmark
-
-.PHONY: perf-tester
-perf-tester:
-	cd ci/perf-tester && npm ci
-
 .PHONY: regenerate_swiftpm_resources
 regenerate_swiftpm_resources:
 	npm run build
-	cp Runtime/lib/index.js Runtime/lib/index.mjs Sources/JavaScriptKit/Runtime
+	cp Runtime/lib/index.mjs Plugins/PackageToJS/Templates/runtime.mjs
+	cp Runtime/lib/index.d.ts Plugins/PackageToJS/Templates/runtime.d.ts
diff --git a/Package.swift b/Package.swift
index 86c533d1c..3657bfa99 100644
--- a/Package.swift
+++ b/Package.swift
@@ -1,34 +1,49 @@
 // swift-tools-version:6.0
 
+import CompilerPluginSupport
 import PackageDescription
 
 // NOTE: needed for embedded customizations, ideally this will not be necessary at all in the future, or can be replaced with traits
 let shouldBuildForEmbedded = Context.environment["JAVASCRIPTKIT_EXPERIMENTAL_EMBEDDED_WASM"].flatMap(Bool.init) ?? false
-let useLegacyResourceBundling = Context.environment["JAVASCRIPTKIT_USE_LEGACY_RESOURCE_BUNDLING"].flatMap(Bool.init) ?? false
+let useLegacyResourceBundling =
+    Context.environment["JAVASCRIPTKIT_USE_LEGACY_RESOURCE_BUNDLING"].flatMap(Bool.init) ?? false
 
 let package = Package(
     name: "JavaScriptKit",
+    platforms: [
+        .macOS(.v10_15),
+        .iOS(.v13),
+        .tvOS(.v13),
+        .watchOS(.v6),
+        .macCatalyst(.v13),
+    ],
     products: [
         .library(name: "JavaScriptKit", targets: ["JavaScriptKit"]),
         .library(name: "JavaScriptEventLoop", targets: ["JavaScriptEventLoop"]),
         .library(name: "JavaScriptBigIntSupport", targets: ["JavaScriptBigIntSupport"]),
         .library(name: "JavaScriptEventLoopTestSupport", targets: ["JavaScriptEventLoopTestSupport"]),
         .plugin(name: "PackageToJS", targets: ["PackageToJS"]),
+        .plugin(name: "BridgeJS", targets: ["BridgeJS"]),
+        .plugin(name: "BridgeJSCommandPlugin", targets: ["BridgeJSCommandPlugin"]),
+    ],
+    dependencies: [
+        .package(url: "https://github.com/swiftlang/swift-syntax", "600.0.0"..<"601.0.0")
     ],
     targets: [
         .target(
             name: "JavaScriptKit",
-            dependencies: ["_CJavaScriptKit"], 
+            dependencies: ["_CJavaScriptKit"],
             exclude: useLegacyResourceBundling ? [] : ["Runtime"],
             resources: useLegacyResourceBundling ? [.copy("Runtime")] : [],
-            cSettings: shouldBuildForEmbedded ? [
+            cSettings: shouldBuildForEmbedded
+                ? [
                     .unsafeFlags(["-fdeclspec"])
                 ] : nil,
-            swiftSettings: shouldBuildForEmbedded 
+            swiftSettings: shouldBuildForEmbedded
                 ? [
                     .enableExperimentalFeature("Embedded"),
                     .enableExperimentalFeature("Extern"),
-                    .unsafeFlags(["-Xfrontend", "-emit-empty-object-file"])
+                    .unsafeFlags(["-Xfrontend", "-emit-empty-object-file"]),
                 ] : nil
         ),
         .target(name: "_CJavaScriptKit"),
@@ -42,7 +57,12 @@ let package = Package(
 
         .target(
             name: "JavaScriptBigIntSupport",
-            dependencies: ["_CJavaScriptBigIntSupport", "JavaScriptKit"]
+            dependencies: ["_CJavaScriptBigIntSupport", "JavaScriptKit"],
+            swiftSettings: shouldBuildForEmbedded
+                ? [
+                    .enableExperimentalFeature("Embedded"),
+                    .unsafeFlags(["-Xfrontend", "-emit-empty-object-file"]),
+                ] : []
         ),
         .target(name: "_CJavaScriptBigIntSupport", dependencies: ["_CJavaScriptKit"]),
         .testTarget(
@@ -52,7 +72,12 @@ let package = Package(
 
         .target(
             name: "JavaScriptEventLoop",
-            dependencies: ["JavaScriptKit", "_CJavaScriptEventLoop"]
+            dependencies: ["JavaScriptKit", "_CJavaScriptEventLoop"],
+            swiftSettings: shouldBuildForEmbedded
+                ? [
+                    .enableExperimentalFeature("Embedded"),
+                    .unsafeFlags(["-Xfrontend", "-emit-empty-object-file"]),
+                ] : []
         ),
         .target(name: "_CJavaScriptEventLoop"),
         .testTarget(
@@ -75,18 +100,51 @@ let package = Package(
         ),
         .target(name: "_CJavaScriptEventLoopTestSupport"),
         .testTarget(
-          name: "JavaScriptEventLoopTestSupportTests",
-          dependencies: [
-            "JavaScriptKit",
-            "JavaScriptEventLoopTestSupport"
-          ]
+            name: "JavaScriptEventLoopTestSupportTests",
+            dependencies: [
+                "JavaScriptKit",
+                "JavaScriptEventLoopTestSupport",
+            ]
         ),
         .plugin(
             name: "PackageToJS",
             capability: .command(
                 intent: .custom(verb: "js", description: "Convert a Swift package to a JavaScript package")
             ),
-            sources: ["Sources"]
+            path: "Plugins/PackageToJS/Sources"
+        ),
+        .plugin(
+            name: "BridgeJS",
+            capability: .buildTool(),
+            dependencies: ["BridgeJSTool"],
+            path: "Plugins/BridgeJS/Sources/BridgeJSBuildPlugin"
+        ),
+        .plugin(
+            name: "BridgeJSCommandPlugin",
+            capability: .command(
+                intent: .custom(verb: "bridge-js", description: "Generate bridging code"),
+                permissions: [.writeToPackageDirectory(reason: "Generate bridging code")]
+            ),
+            dependencies: ["BridgeJSTool"],
+            path: "Plugins/BridgeJS/Sources/BridgeJSCommandPlugin"
+        ),
+        .executableTarget(
+            name: "BridgeJSTool",
+            dependencies: [
+                .product(name: "SwiftParser", package: "swift-syntax"),
+                .product(name: "SwiftSyntax", package: "swift-syntax"),
+                .product(name: "SwiftBasicFormat", package: "swift-syntax"),
+                .product(name: "SwiftSyntaxBuilder", package: "swift-syntax"),
+            ],
+            path: "Plugins/BridgeJS/Sources/BridgeJSTool"
+        ),
+        .testTarget(
+            name: "BridgeJSRuntimeTests",
+            dependencies: ["JavaScriptKit"],
+            exclude: ["Generated/JavaScript"],
+            swiftSettings: [
+                .enableExperimentalFeature("Extern")
+            ]
         ),
     ]
 )
diff --git a/Plugins/BridgeJS/Package.swift b/Plugins/BridgeJS/Package.swift
new file mode 100644
index 000000000..ab8b475cb
--- /dev/null
+++ b/Plugins/BridgeJS/Package.swift
@@ -0,0 +1,29 @@
+// swift-tools-version: 6.0
+
+import PackageDescription
+
+let package = Package(
+    name: "BridgeJS",
+    platforms: [.macOS(.v13)],
+    dependencies: [
+        .package(url: "https://github.com/swiftlang/swift-syntax", from: "600.0.1")
+    ],
+    targets: [
+        .target(name: "BridgeJSBuildPlugin"),
+        .target(name: "BridgeJSLink"),
+        .executableTarget(
+            name: "BridgeJSTool",
+            dependencies: [
+                .product(name: "SwiftParser", package: "swift-syntax"),
+                .product(name: "SwiftSyntax", package: "swift-syntax"),
+                .product(name: "SwiftBasicFormat", package: "swift-syntax"),
+                .product(name: "SwiftSyntaxBuilder", package: "swift-syntax"),
+            ]
+        ),
+        .testTarget(
+            name: "BridgeJSToolTests",
+            dependencies: ["BridgeJSTool", "BridgeJSLink"],
+            exclude: ["__Snapshots__", "Inputs"]
+        ),
+    ]
+)
diff --git a/Plugins/BridgeJS/README.md b/Plugins/BridgeJS/README.md
new file mode 100644
index 000000000..9cbd04011
--- /dev/null
+++ b/Plugins/BridgeJS/README.md
@@ -0,0 +1,137 @@
+# 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.
+
+> [!NOTE]
+> This documentation is intended for JavaScriptKit developers, not JavaScriptKit users.
+
+## 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
+
+## Architecture Diagram
+
+```mermaid
+graph LR
+    E1 --> G3[ExportSwift.json]
+    subgraph ModuleA
+        A.swift --> E1[[bridge-js export]]
+        B.swift --> E1
+        E1 --> G1[ExportSwift.swift]
+        B1[bridge.d.ts]-->I1[[bridge-js import]]
+        I1 --> G2[ImportTS.swift]
+    end
+    I1 --> G4[ImportTS.json]
+
+    E2 --> G7[ExportSwift.json]
+    subgraph ModuleB
+        C.swift --> E2[[bridge-js export]]
+        D.swift --> E2
+        E2 --> G5[ExportSwift.swift]
+        B2[bridge.d.ts]-->I2[[bridge-js import]]
+        I2 --> G6[ImportTS.swift]
+    end
+    I2 --> G8[ImportTS.json]
+
+    G3 --> L1[[bridge-js link]]
+    G4 --> L1
+    G7 --> L1
+    G8 --> L1
+
+    L1 --> F1[bridge.js]
+    L1 --> F2[bridge.d.ts]
+    ModuleA -----> App[App.wasm]
+    ModuleB -----> App
+
+    App --> PKG[[PackageToJS]]
+    F1 --> PKG
+    F2 --> PKG
+```
+
+## Type Mapping
+
+### Primitive Type Conversions
+
+TBD
+
+| Swift Type    | JS Type    | Wasm Core Type |
+|:--------------|:-----------|:---------------|
+| `Int`         | `number`   | `i32`          |
+| `UInt`        | `number`   | `i32`          |
+| `Int8`        | `number`   | `i32`          |
+| `UInt8`       | `number`   | `i32`          |
+| `Int16`       | `number`   | `i32`          |
+| `UInt16`      | `number`   | `i32`          |
+| `Int32`       | `number`   | `i32`          |
+| `UInt32`      | `number`   | `i32`          |
+| `Int64`       | `bigint`   | `i64`          |
+| `UInt64`      | `bigint`   | `i64`          |
+| `Float`       | `number`   | `f32`          |
+| `Double`      | `number`   | `f64`          |
+| `Bool`        | `boolean`  | `i32`          |
+| `Void`        | `void`     | -              |
+| `String`      | `string`   | `i32`          |
+
+## Type Modeling
+
+TypeScript uses [structural subtyping](https://www.typescriptlang.org/docs/handbook/type-compatibility.html), but Swift doesn't directly offer it. We can't map every TypeScript type to Swift, so we made several give-ups and heuristics.
+
+### `interface`
+
+We intentionally don't simulate TS's `interface` with Swift's `protocol` even though they are quite similar for the following reasons:
+
+* Adding a protocol conformance for each `interface` implementation adds binary size cost in debug build because it's not easy to DCE.
+* No straightforward way to represent the use of `interface` type on the return type position of TS function. Which concrete type it should it be?
+* For Embedded Swift, we should avoid use of existential type as much as possible.
+
+Instead of simulating the subtyping-rule with Swift's `protocol`, we represent each `interface` with Swift's struct.
+In this way, we lose implicit type coercion but it makes things simpler and clear.
+
+TBD: Consider providing type-conversion methods to simulate subtyping rule like `func asIface()`
+
+### Anonymous type literals
+
+Swift offers a few non-nominal types, tuple and function types, but they are not enough to provide access to the underlying storage lazily. So we gave up importing them in typed way.
+
+## ABI
+
+This section describes the ABI contract used between JavaScript and Swift.
+The ABI will not be stable, and not meant to be interposed by other tools.
+
+### Parameter Passing
+
+Parameter passing follows Wasm calling conventions, with custom handling for complex types like strings and objects.
+
+TBD
+
+### Return Values
+
+TBD
+
+## Future Work
+
+- [ ] Struct on parameter or return type
+- [ ] Throws functions
+- [ ] Async functions
+- [ ] Cast between TS interface
+- [ ] Closure support
+- [ ] Simplify constructor pattern
+    * https://github.com/ocsigen/ts2ocaml/blob/main/docs/js_of_ocaml.md#feature-immediate-constructor
+    ```typescript
+    interface Foo = {
+      someMethod(value: number): void;
+    }
+
+    interface FooConstructor {
+      new(name: string) : Foo;
+
+      anotherMethod(): number;
+    }
+
+    declare var Foo: FooConstructor;
+    ```
+- [ ] Use `externref` once it's widely available
diff --git a/Plugins/BridgeJS/Sources/BridgeJSBuildPlugin/BridgeJSBuildPlugin.swift b/Plugins/BridgeJS/Sources/BridgeJSBuildPlugin/BridgeJSBuildPlugin.swift
new file mode 100644
index 000000000..4ea725ed5
--- /dev/null
+++ b/Plugins/BridgeJS/Sources/BridgeJSBuildPlugin/BridgeJSBuildPlugin.swift
@@ -0,0 +1,71 @@
+#if canImport(PackagePlugin)
+import PackagePlugin
+import Foundation
+
+/// Build plugin for runtime code generation with BridgeJS.
+/// This plugin automatically generates bridge code between Swift and JavaScript
+/// during each build process.
+@main
+struct BridgeJSBuildPlugin: BuildToolPlugin {
+    func createBuildCommands(context: PluginContext, target: Target) throws -> [Command] {
+        guard let swiftSourceModuleTarget = target as? SwiftSourceModuleTarget else {
+            return []
+        }
+        return try [
+            createExportSwiftCommand(context: context, target: swiftSourceModuleTarget),
+            createImportTSCommand(context: context, target: swiftSourceModuleTarget),
+        ]
+    }
+
+    private func createExportSwiftCommand(context: PluginContext, target: SwiftSourceModuleTarget) throws -> Command {
+        let outputSwiftPath = context.pluginWorkDirectoryURL.appending(path: "ExportSwift.swift")
+        let outputSkeletonPath = context.pluginWorkDirectoryURL.appending(path: "ExportSwift.json")
+        let inputFiles = target.sourceFiles.filter { !$0.url.path.hasPrefix(context.pluginWorkDirectoryURL.path + "/") }
+            .map(\.url)
+        return .buildCommand(
+            displayName: "Export Swift API",
+            executable: try context.tool(named: "BridgeJSTool").url,
+            arguments: [
+                "export",
+                "--output-skeleton",
+                outputSkeletonPath.path,
+                "--output-swift",
+                outputSwiftPath.path,
+                "--always-write", "true",
+            ] + inputFiles.map(\.path),
+            inputFiles: inputFiles,
+            outputFiles: [
+                outputSwiftPath
+            ]
+        )
+    }
+
+    private func createImportTSCommand(context: PluginContext, target: SwiftSourceModuleTarget) throws -> Command {
+        let outputSwiftPath = context.pluginWorkDirectoryURL.appending(path: "ImportTS.swift")
+        let outputSkeletonPath = context.pluginWorkDirectoryURL.appending(path: "ImportTS.json")
+        let inputFiles = [
+            target.directoryURL.appending(path: "bridge.d.ts")
+        ]
+        return .buildCommand(
+            displayName: "Import TypeScript API",
+            executable: try context.tool(named: "BridgeJSTool").url,
+            arguments: [
+                "import",
+                "--output-skeleton",
+                outputSkeletonPath.path,
+                "--output-swift",
+                outputSwiftPath.path,
+                "--module-name",
+                target.name,
+                "--always-write", "true",
+                "--project",
+                context.package.directoryURL.appending(path: "tsconfig.json").path,
+            ] + inputFiles.map(\.path),
+            inputFiles: inputFiles,
+            outputFiles: [
+                outputSwiftPath
+            ]
+        )
+    }
+}
+#endif
diff --git a/Plugins/BridgeJS/Sources/BridgeJSCommandPlugin/BridgeJSCommandPlugin.swift b/Plugins/BridgeJS/Sources/BridgeJSCommandPlugin/BridgeJSCommandPlugin.swift
new file mode 100644
index 000000000..286b052d5
--- /dev/null
+++ b/Plugins/BridgeJS/Sources/BridgeJSCommandPlugin/BridgeJSCommandPlugin.swift
@@ -0,0 +1,182 @@
+#if canImport(PackagePlugin)
+import PackagePlugin
+@preconcurrency import Foundation
+
+/// Command plugin for ahead-of-time (AOT) code generation with BridgeJS.
+/// This plugin allows you to generate bridge code between Swift and JavaScript
+/// before the build process, improving build times for larger projects.
+/// See documentation: Ahead-of-Time-Code-Generation.md
+@main
+struct BridgeJSCommandPlugin: CommandPlugin {
+    static let JAVASCRIPTKIT_PACKAGE_NAME: String = "JavaScriptKit"
+
+    struct Options {
+        var targets: [String]
+
+        static func parse(extractor: inout ArgumentExtractor) -> Options {
+            let targets = extractor.extractOption(named: "target")
+            return Options(targets: targets)
+        }
+
+        static func help() -> String {
+            return """
+                OVERVIEW: Generate ahead-of-time (AOT) bridge code between Swift and JavaScript.
+
+                This command generates bridge code before the build process, which can significantly
+                improve build times for larger projects by avoiding runtime code generation.
+                Generated code will be placed in the target's 'Generated' directory.
+
+                OPTIONS:
+                    --target  Specify target(s) to generate bridge code for. If omitted,
+                                      generates for all targets with JavaScriptKit dependency.
+                """
+        }
+    }
+
+    func performCommand(context: PluginContext, arguments: [String]) throws {
+        // Check for help flags to display usage information
+        // This allows users to run `swift package plugin bridge-js --help` to understand the plugin's functionality
+        if arguments.contains(where: { ["-h", "--help"].contains($0) }) {
+            printStderr(Options.help())
+            return
+        }
+
+        var extractor = ArgumentExtractor(arguments)
+        let options = Options.parse(extractor: &extractor)
+        let remainingArguments = extractor.remainingArguments
+
+        if options.targets.isEmpty {
+            try runOnTargets(
+                context: context,
+                remainingArguments: remainingArguments,
+                where: { target in
+                    target.hasDependency(named: Self.JAVASCRIPTKIT_PACKAGE_NAME)
+                }
+            )
+        } else {
+            try runOnTargets(
+                context: context,
+                remainingArguments: remainingArguments,
+                where: { options.targets.contains($0.name) }
+            )
+        }
+    }
+
+    private func runOnTargets(
+        context: PluginContext,
+        remainingArguments: [String],
+        where predicate: (SwiftSourceModuleTarget) -> Bool
+    ) throws {
+        for target in context.package.targets {
+            guard let target = target as? SwiftSourceModuleTarget else {
+                continue
+            }
+            guard predicate(target) else {
+                continue
+            }
+            try runSingleTarget(context: context, target: target, remainingArguments: remainingArguments)
+        }
+    }
+
+    private func runSingleTarget(
+        context: PluginContext,
+        target: SwiftSourceModuleTarget,
+        remainingArguments: [String]
+    ) throws {
+        Diagnostics.progress("Exporting Swift API for \(target.name)...")
+
+        let generatedDirectory = target.directoryURL.appending(path: "Generated")
+        let generatedJavaScriptDirectory = generatedDirectory.appending(path: "JavaScript")
+
+        try runBridgeJSTool(
+            context: context,
+            arguments: [
+                "export",
+                "--output-skeleton",
+                generatedJavaScriptDirectory.appending(path: "ExportSwift.json").path,
+                "--output-swift",
+                generatedDirectory.appending(path: "ExportSwift.swift").path,
+            ]
+                + target.sourceFiles.filter {
+                    !$0.url.path.hasPrefix(generatedDirectory.path + "/")
+                }.map(\.url.path) + remainingArguments
+        )
+
+        try runBridgeJSTool(
+            context: context,
+            arguments: [
+                "import",
+                "--output-skeleton",
+                generatedJavaScriptDirectory.appending(path: "ImportTS.json").path,
+                "--output-swift",
+                generatedDirectory.appending(path: "ImportTS.swift").path,
+                "--module-name",
+                target.name,
+                "--project",
+                context.package.directoryURL.appending(path: "tsconfig.json").path,
+                target.directoryURL.appending(path: "bridge.d.ts").path,
+            ] + remainingArguments
+        )
+    }
+
+    private func runBridgeJSTool(context: PluginContext, arguments: [String]) throws {
+        let tool = try context.tool(named: "BridgeJSTool").url
+        printStderr("$ \(tool.path) \(arguments.joined(separator: " "))")
+        let process = Process()
+        process.executableURL = tool
+        process.arguments = arguments
+        try process.forwardTerminationSignals {
+            try process.run()
+            process.waitUntilExit()
+        }
+        if process.terminationStatus != 0 {
+            exit(process.terminationStatus)
+        }
+    }
+}
+
+private func printStderr(_ message: String) {
+    fputs(message + "\n", stderr)
+}
+
+extension SwiftSourceModuleTarget {
+    func hasDependency(named name: String) -> Bool {
+        return dependencies.contains(where: {
+            switch $0 {
+            case .product(let product):
+                return product.name == name
+            case .target(let target):
+                return target.name == name
+            @unknown default:
+                return false
+            }
+        })
+    }
+}
+
+extension Foundation.Process {
+    // Monitor termination/interrruption signals to forward them to child process
+    func setSignalForwarding(_ signalNo: Int32) -> DispatchSourceSignal {
+        let signalSource = DispatchSource.makeSignalSource(signal: signalNo)
+        signalSource.setEventHandler { [self] in
+            signalSource.cancel()
+            kill(processIdentifier, signalNo)
+        }
+        signalSource.resume()
+        return signalSource
+    }
+
+    func forwardTerminationSignals(_ body: () throws -> Void) rethrows {
+        let sources = [
+            setSignalForwarding(SIGINT),
+            setSignalForwarding(SIGTERM),
+        ]
+        defer {
+            for source in sources {
+                source.cancel()
+            }
+        }
+        try body()
+    }
+}
+#endif
diff --git a/Plugins/BridgeJS/Sources/BridgeJSLink/BridgeJSLink.swift b/Plugins/BridgeJS/Sources/BridgeJSLink/BridgeJSLink.swift
new file mode 100644
index 000000000..e62a9a639
--- /dev/null
+++ b/Plugins/BridgeJS/Sources/BridgeJSLink/BridgeJSLink.swift
@@ -0,0 +1,561 @@
+import Foundation
+
+struct BridgeJSLink {
+    /// The exported skeletons
+    var exportedSkeletons: [ExportedSkeleton] = []
+    var importedSkeletons: [ImportedModuleSkeleton] = []
+
+    mutating func addExportedSkeletonFile(data: Data) throws {
+        let skeleton = try JSONDecoder().decode(ExportedSkeleton.self, from: data)
+        exportedSkeletons.append(skeleton)
+    }
+
+    mutating func addImportedSkeletonFile(data: Data) throws {
+        let skeletons = try JSONDecoder().decode(ImportedModuleSkeleton.self, from: data)
+        importedSkeletons.append(skeletons)
+    }
+
+    let swiftHeapObjectClassDts = """
+        /// Represents a Swift heap object like a class instance or an actor instance.
+        export interface SwiftHeapObject {
+            /// Release the heap object.
+            ///
+            /// Note: Calling this method will release the heap object and it will no longer be accessible.
+            release(): void;
+        }
+        """
+
+    let swiftHeapObjectClassJs = """
+        /// Represents a Swift heap object like a class instance or an actor instance.
+        class SwiftHeapObject {
+            constructor(pointer, deinit) {
+                this.pointer = pointer;
+                this.hasReleased = false;
+                this.deinit = deinit;
+                this.registry = new FinalizationRegistry((pointer) => {
+                    deinit(pointer);
+                });
+                this.registry.register(this, this.pointer);
+            }
+
+            release() {
+                this.registry.unregister(this);
+                this.deinit(this.pointer);
+            }
+        }
+        """
+
+    func link() throws -> (outputJs: String, outputDts: String) {
+        var exportsLines: [String] = []
+        var importedLines: [String] = []
+        var classLines: [String] = []
+        var dtsExportLines: [String] = []
+        var dtsImportLines: [String] = []
+        var dtsClassLines: [String] = []
+
+        if exportedSkeletons.contains(where: { $0.classes.count > 0 }) {
+            classLines.append(
+                contentsOf: swiftHeapObjectClassJs.split(separator: "\n", omittingEmptySubsequences: false).map {
+                    String($0)
+                }
+            )
+            dtsClassLines.append(
+                contentsOf: swiftHeapObjectClassDts.split(separator: "\n", omittingEmptySubsequences: false).map {
+                    String($0)
+                }
+            )
+        }
+
+        for skeleton in exportedSkeletons {
+            for klass in skeleton.classes {
+                let (jsType, dtsType, dtsExportEntry) = renderExportedClass(klass)
+                classLines.append(contentsOf: jsType)
+                exportsLines.append("\(klass.name),")
+                dtsExportLines.append(contentsOf: dtsExportEntry)
+                dtsClassLines.append(contentsOf: dtsType)
+            }
+
+            for function in skeleton.functions {
+                var (js, dts) = renderExportedFunction(function: function)
+                js[0] = "\(function.name): " + js[0]
+                js[js.count - 1] += ","
+                exportsLines.append(contentsOf: js)
+                dtsExportLines.append(contentsOf: dts)
+            }
+        }
+
+        for skeletonSet in importedSkeletons {
+            importedLines.append("const \(skeletonSet.moduleName) = importObject[\"\(skeletonSet.moduleName)\"] = {};")
+            func assignToImportObject(name: String, function: [String]) {
+                var js = function
+                js[0] = "\(skeletonSet.moduleName)[\"\(name)\"] = " + js[0]
+                importedLines.append(contentsOf: js)
+            }
+            for fileSkeleton in skeletonSet.children {
+                for function in fileSkeleton.functions {
+                    let (js, dts) = try renderImportedFunction(function: function)
+                    assignToImportObject(name: function.abiName(context: nil), function: js)
+                    dtsImportLines.append(contentsOf: dts)
+                }
+                for type in fileSkeleton.types {
+                    for property in type.properties {
+                        let getterAbiName = property.getterAbiName(context: type)
+                        let (js, dts) = try renderImportedProperty(
+                            property: property,
+                            abiName: getterAbiName,
+                            emitCall: { thunkBuilder in
+                                thunkBuilder.callPropertyGetter(name: property.name, returnType: property.type)
+                                return try thunkBuilder.lowerReturnValue(returnType: property.type)
+                            }
+                        )
+                        assignToImportObject(name: getterAbiName, function: js)
+                        dtsImportLines.append(contentsOf: dts)
+
+                        if !property.isReadonly {
+                            let setterAbiName = property.setterAbiName(context: type)
+                            let (js, dts) = try renderImportedProperty(
+                                property: property,
+                                abiName: setterAbiName,
+                                emitCall: { thunkBuilder in
+                                    thunkBuilder.liftParameter(
+                                        param: Parameter(label: nil, name: "newValue", type: property.type)
+                                    )
+                                    thunkBuilder.callPropertySetter(name: property.name, returnType: property.type)
+                                    return nil
+                                }
+                            )
+                            assignToImportObject(name: setterAbiName, function: js)
+                            dtsImportLines.append(contentsOf: dts)
+                        }
+                    }
+                    for method in type.methods {
+                        let (js, dts) = try renderImportedMethod(context: type, method: method)
+                        assignToImportObject(name: method.abiName(context: type), function: js)
+                        dtsImportLines.append(contentsOf: dts)
+                    }
+                }
+            }
+        }
+
+        let outputJs = """
+            // NOTICE: This is auto-generated code by BridgeJS from JavaScriptKit,
+            // DO NOT EDIT.
+            //
+            // To update this file, just rebuild your project or run
+            // `swift package bridge-js`.
+
+            export async function createInstantiator(options, swift) {
+                let instance;
+                let memory;
+                const textDecoder = new TextDecoder("utf-8");
+                const textEncoder = new TextEncoder("utf-8");
+
+                let tmpRetString;
+                let tmpRetBytes;
+                return {
+                    /** @param {WebAssembly.Imports} importObject */
+                    addImports: (importObject) => {
+                        const bjs = {};
+                        importObject["bjs"] = bjs;
+                        bjs["return_string"] = function(ptr, len) {
+                            const bytes = new Uint8Array(memory.buffer, ptr, len);
+                            tmpRetString = textDecoder.decode(bytes);
+                        }
+                        bjs["init_memory"] = function(sourceId, bytesPtr) {
+                            const source = swift.memory.getObject(sourceId);
+                            const bytes = new Uint8Array(memory.buffer, bytesPtr);
+                            bytes.set(source);
+                        }
+                        bjs["make_jsstring"] = function(ptr, len) {
+                            const bytes = new Uint8Array(memory.buffer, ptr, len);
+                            return swift.memory.retain(textDecoder.decode(bytes));
+                        }
+                        bjs["init_memory_with_result"] = function(ptr, len) {
+                            const target = new Uint8Array(memory.buffer, ptr, len);
+                            target.set(tmpRetBytes);
+                            tmpRetBytes = undefined;
+                        }
+            \(importedLines.map { $0.indent(count: 12) }.joined(separator: "\n"))
+                    },
+                    setInstance: (i) => {
+                        instance = i;
+                        memory = instance.exports.memory;
+                    },
+                    /** @param {WebAssembly.Instance} instance */
+                    createExports: (instance) => {
+                        const js = swift.memory.heap;
+            \(classLines.map { $0.indent(count: 12) }.joined(separator: "\n"))
+                        return {
+            \(exportsLines.map { $0.indent(count: 16) }.joined(separator: "\n"))
+                        };
+                    },
+                }
+            }
+            """
+        var dtsLines: [String] = []
+        dtsLines.append(contentsOf: dtsClassLines)
+        dtsLines.append("export type Exports = {")
+        dtsLines.append(contentsOf: dtsExportLines.map { $0.indent(count: 4) })
+        dtsLines.append("}")
+        dtsLines.append("export type Imports = {")
+        dtsLines.append(contentsOf: dtsImportLines.map { $0.indent(count: 4) })
+        dtsLines.append("}")
+        let outputDts = """
+            // NOTICE: This is auto-generated code by BridgeJS from JavaScriptKit,
+            // DO NOT EDIT.
+            //
+            // To update this file, just rebuild your project or run
+            // `swift package bridge-js`.
+
+            \(dtsLines.joined(separator: "\n"))
+            export function createInstantiator(options: {
+                imports: Imports;
+            }, swift: any): Promise<{
+                addImports: (importObject: WebAssembly.Imports) => void;
+                setInstance: (instance: WebAssembly.Instance) => void;
+                createExports: (instance: WebAssembly.Instance) => Exports;
+            }>;
+            """
+        return (outputJs, outputDts)
+    }
+
+    class ExportedThunkBuilder {
+        var bodyLines: [String] = []
+        var cleanupLines: [String] = []
+        var parameterForwardings: [String] = []
+
+        func lowerParameter(param: Parameter) {
+            switch param.type {
+            case .void: return
+            case .int, .float, .double, .bool:
+                parameterForwardings.append(param.name)
+            case .string:
+                let bytesLabel = "\(param.name)Bytes"
+                let bytesIdLabel = "\(param.name)Id"
+                bodyLines.append("const \(bytesLabel) = textEncoder.encode(\(param.name));")
+                bodyLines.append("const \(bytesIdLabel) = swift.memory.retain(\(bytesLabel));")
+                cleanupLines.append("swift.memory.release(\(bytesIdLabel));")
+                parameterForwardings.append(bytesIdLabel)
+                parameterForwardings.append("\(bytesLabel).length")
+            case .jsObject:
+                parameterForwardings.append("swift.memory.retain(\(param.name))")
+            case .swiftHeapObject:
+                parameterForwardings.append("\(param.name).pointer")
+            }
+        }
+
+        func lowerSelf() {
+            parameterForwardings.append("this.pointer")
+        }
+
+        func call(abiName: String, returnType: BridgeType) -> String? {
+            let call = "instance.exports.\(abiName)(\(parameterForwardings.joined(separator: ", ")))"
+            var returnExpr: String?
+
+            switch returnType {
+            case .void:
+                bodyLines.append("\(call);")
+            case .string:
+                bodyLines.append("\(call);")
+                bodyLines.append("const ret = tmpRetString;")
+                bodyLines.append("tmpRetString = undefined;")
+                returnExpr = "ret"
+            case .int, .float, .double:
+                bodyLines.append("const ret = \(call);")
+                returnExpr = "ret"
+            case .bool:
+                bodyLines.append("const ret = \(call) !== 0;")
+                returnExpr = "ret"
+            case .jsObject:
+                bodyLines.append("const retId = \(call);")
+                // TODO: Implement "take" operation
+                bodyLines.append("const ret = swift.memory.getObject(retId);")
+                bodyLines.append("swift.memory.release(retId);")
+                returnExpr = "ret"
+            case .swiftHeapObject(let name):
+                bodyLines.append("const ret = new \(name)(\(call));")
+                returnExpr = "ret"
+            }
+            return returnExpr
+        }
+
+        func callConstructor(abiName: String) -> String {
+            return "instance.exports.\(abiName)(\(parameterForwardings.joined(separator: ", ")))"
+        }
+
+        func renderFunction(
+            name: String,
+            parameters: [Parameter],
+            returnType: BridgeType,
+            returnExpr: String?,
+            isMethod: Bool
+        ) -> [String] {
+            var funcLines: [String] = []
+            funcLines.append(
+                "\(isMethod ? "" : "function ")\(name)(\(parameters.map { $0.name }.joined(separator: ", "))) {"
+            )
+            funcLines.append(contentsOf: bodyLines.map { $0.indent(count: 4) })
+            funcLines.append(contentsOf: cleanupLines.map { $0.indent(count: 4) })
+            if let returnExpr = returnExpr {
+                funcLines.append("return \(returnExpr);".indent(count: 4))
+            }
+            funcLines.append("}")
+            return funcLines
+        }
+    }
+
+    private func renderTSSignature(parameters: [Parameter], returnType: BridgeType) -> String {
+        return "(\(parameters.map { "\($0.name): \($0.type.tsType)" }.joined(separator: ", "))): \(returnType.tsType)"
+    }
+
+    func renderExportedFunction(function: ExportedFunction) -> (js: [String], dts: [String]) {
+        let thunkBuilder = ExportedThunkBuilder()
+        for param in function.parameters {
+            thunkBuilder.lowerParameter(param: param)
+        }
+        let returnExpr = thunkBuilder.call(abiName: function.abiName, returnType: function.returnType)
+        let funcLines = thunkBuilder.renderFunction(
+            name: function.abiName,
+            parameters: function.parameters,
+            returnType: function.returnType,
+            returnExpr: returnExpr,
+            isMethod: false
+        )
+        var dtsLines: [String] = []
+        dtsLines.append(
+            "\(function.name)\(renderTSSignature(parameters: function.parameters, returnType: function.returnType));"
+        )
+
+        return (funcLines, dtsLines)
+    }
+
+    func renderExportedClass(_ klass: ExportedClass) -> (js: [String], dtsType: [String], dtsExportEntry: [String]) {
+        var jsLines: [String] = []
+        var dtsTypeLines: [String] = []
+        var dtsExportEntryLines: [String] = []
+
+        dtsTypeLines.append("export interface \(klass.name) extends SwiftHeapObject {")
+        dtsExportEntryLines.append("\(klass.name): {")
+        jsLines.append("class \(klass.name) extends SwiftHeapObject {")
+
+        if let constructor: ExportedConstructor = klass.constructor {
+            let thunkBuilder = ExportedThunkBuilder()
+            for param in constructor.parameters {
+                thunkBuilder.lowerParameter(param: param)
+            }
+            let returnExpr = thunkBuilder.callConstructor(abiName: constructor.abiName)
+            var funcLines: [String] = []
+            funcLines.append("constructor(\(constructor.parameters.map { $0.name }.joined(separator: ", "))) {")
+            funcLines.append(contentsOf: thunkBuilder.bodyLines.map { $0.indent(count: 4) })
+            funcLines.append("super(\(returnExpr), instance.exports.bjs_\(klass.name)_deinit);".indent(count: 4))
+            funcLines.append(contentsOf: thunkBuilder.cleanupLines.map { $0.indent(count: 4) })
+            funcLines.append("}")
+            jsLines.append(contentsOf: funcLines.map { $0.indent(count: 4) })
+
+            dtsExportEntryLines.append(
+                "new\(renderTSSignature(parameters: constructor.parameters, returnType: .swiftHeapObject(klass.name)));"
+                    .indent(count: 4)
+            )
+        }
+
+        for method in klass.methods {
+            let thunkBuilder = ExportedThunkBuilder()
+            thunkBuilder.lowerSelf()
+            for param in method.parameters {
+                thunkBuilder.lowerParameter(param: param)
+            }
+            let returnExpr = thunkBuilder.call(abiName: method.abiName, returnType: method.returnType)
+            jsLines.append(
+                contentsOf: thunkBuilder.renderFunction(
+                    name: method.name,
+                    parameters: method.parameters,
+                    returnType: method.returnType,
+                    returnExpr: returnExpr,
+                    isMethod: true
+                ).map { $0.indent(count: 4) }
+            )
+            dtsTypeLines.append(
+                "\(method.name)\(renderTSSignature(parameters: method.parameters, returnType: method.returnType));"
+                    .indent(count: 4)
+            )
+        }
+        jsLines.append("}")
+
+        dtsTypeLines.append("}")
+        dtsExportEntryLines.append("}")
+
+        return (jsLines, dtsTypeLines, dtsExportEntryLines)
+    }
+
+    class ImportedThunkBuilder {
+        var bodyLines: [String] = []
+        var parameterNames: [String] = []
+        var parameterForwardings: [String] = []
+
+        func liftSelf() {
+            parameterNames.append("self")
+        }
+
+        func liftParameter(param: Parameter) {
+            parameterNames.append(param.name)
+            switch param.type {
+            case .string:
+                let stringObjectName = "\(param.name)Object"
+                // TODO: Implement "take" operation
+                bodyLines.append("const \(stringObjectName) = swift.memory.getObject(\(param.name));")
+                bodyLines.append("swift.memory.release(\(param.name));")
+                parameterForwardings.append(stringObjectName)
+            case .jsObject:
+                parameterForwardings.append("swift.memory.getObject(\(param.name))")
+            default:
+                parameterForwardings.append(param.name)
+            }
+        }
+
+        func renderFunction(
+            name: String,
+            returnExpr: String?
+        ) -> [String] {
+            var funcLines: [String] = []
+            funcLines.append(
+                "function \(name)(\(parameterNames.joined(separator: ", "))) {"
+            )
+            funcLines.append(contentsOf: bodyLines.map { $0.indent(count: 4) })
+            if let returnExpr = returnExpr {
+                funcLines.append("return \(returnExpr);".indent(count: 4))
+            }
+            funcLines.append("}")
+            return funcLines
+        }
+
+        func call(name: String, returnType: BridgeType) {
+            let call = "options.imports.\(name)(\(parameterForwardings.joined(separator: ", ")))"
+            if returnType == .void {
+                bodyLines.append("\(call);")
+            } else {
+                bodyLines.append("let ret = \(call);")
+            }
+        }
+
+        func callMethod(name: String, returnType: BridgeType) {
+            let call = "swift.memory.getObject(self).\(name)(\(parameterForwardings.joined(separator: ", ")))"
+            if returnType == .void {
+                bodyLines.append("\(call);")
+            } else {
+                bodyLines.append("let ret = \(call);")
+            }
+        }
+
+        func callPropertyGetter(name: String, returnType: BridgeType) {
+            let call = "swift.memory.getObject(self).\(name)"
+            bodyLines.append("let ret = \(call);")
+        }
+
+        func callPropertySetter(name: String, returnType: BridgeType) {
+            let call = "swift.memory.getObject(self).\(name) = \(parameterForwardings.joined(separator: ", "))"
+            bodyLines.append("\(call);")
+        }
+
+        func lowerReturnValue(returnType: BridgeType) throws -> String? {
+            switch returnType {
+            case .void:
+                return nil
+            case .string:
+                bodyLines.append("tmpRetBytes = textEncoder.encode(ret);")
+                return "tmpRetBytes.length"
+            case .int, .float, .double:
+                return "ret"
+            case .bool:
+                return "ret !== 0"
+            case .jsObject:
+                return "swift.memory.retain(ret)"
+            case .swiftHeapObject:
+                throw BridgeJSLinkError(message: "Swift heap object is not supported in imported functions")
+            }
+        }
+    }
+
+    func renderImportedFunction(function: ImportedFunctionSkeleton) throws -> (js: [String], dts: [String]) {
+        let thunkBuilder = ImportedThunkBuilder()
+        for param in function.parameters {
+            thunkBuilder.liftParameter(param: param)
+        }
+        thunkBuilder.call(name: function.name, returnType: function.returnType)
+        let returnExpr = try thunkBuilder.lowerReturnValue(returnType: function.returnType)
+        let funcLines = thunkBuilder.renderFunction(
+            name: function.abiName(context: nil),
+            returnExpr: returnExpr
+        )
+        var dtsLines: [String] = []
+        dtsLines.append(
+            "\(function.name)\(renderTSSignature(parameters: function.parameters, returnType: function.returnType));"
+        )
+        return (funcLines, dtsLines)
+    }
+
+    func renderImportedProperty(
+        property: ImportedPropertySkeleton,
+        abiName: String,
+        emitCall: (ImportedThunkBuilder) throws -> String?
+    ) throws -> (js: [String], dts: [String]) {
+        let thunkBuilder = ImportedThunkBuilder()
+        thunkBuilder.liftSelf()
+        let returnExpr = try emitCall(thunkBuilder)
+        let funcLines = thunkBuilder.renderFunction(
+            name: abiName,
+            returnExpr: returnExpr
+        )
+        return (funcLines, [])
+    }
+
+    func renderImportedMethod(
+        context: ImportedTypeSkeleton,
+        method: ImportedFunctionSkeleton
+    ) throws -> (js: [String], dts: [String]) {
+        let thunkBuilder = ImportedThunkBuilder()
+        thunkBuilder.liftSelf()
+        for param in method.parameters {
+            thunkBuilder.liftParameter(param: param)
+        }
+        thunkBuilder.callMethod(name: method.name, returnType: method.returnType)
+        let returnExpr = try thunkBuilder.lowerReturnValue(returnType: method.returnType)
+        let funcLines = thunkBuilder.renderFunction(
+            name: method.abiName(context: context),
+            returnExpr: returnExpr
+        )
+        return (funcLines, [])
+    }
+}
+
+struct BridgeJSLinkError: Error {
+    let message: String
+}
+
+extension String {
+    func indent(count: Int) -> String {
+        return String(repeating: " ", count: count) + self
+    }
+}
+
+extension BridgeType {
+    var tsType: String {
+        switch self {
+        case .void:
+            return "void"
+        case .string:
+            return "string"
+        case .int:
+            return "number"
+        case .float:
+            return "number"
+        case .double:
+            return "number"
+        case .bool:
+            return "boolean"
+        case .jsObject:
+            return "any"
+        case .swiftHeapObject(let name):
+            return name
+        }
+    }
+}
diff --git a/Plugins/BridgeJS/Sources/BridgeJSLink/BridgeJSSkeleton b/Plugins/BridgeJS/Sources/BridgeJSLink/BridgeJSSkeleton
new file mode 120000
index 000000000..a2c26678f
--- /dev/null
+++ b/Plugins/BridgeJS/Sources/BridgeJSLink/BridgeJSSkeleton
@@ -0,0 +1 @@
+../BridgeJSSkeleton
\ No newline at end of file
diff --git a/Plugins/BridgeJS/Sources/BridgeJSSkeleton/BridgeJSSkeleton.swift b/Plugins/BridgeJS/Sources/BridgeJSSkeleton/BridgeJSSkeleton.swift
new file mode 100644
index 000000000..34492682f
--- /dev/null
+++ b/Plugins/BridgeJS/Sources/BridgeJSSkeleton/BridgeJSSkeleton.swift
@@ -0,0 +1,96 @@
+// This file is shared between BridgeTool and BridgeJSLink
+
+// MARK: - Types
+
+enum BridgeType: Codable, Equatable {
+    case int, float, double, string, bool, jsObject(String?), swiftHeapObject(String), void
+}
+
+enum WasmCoreType: String, Codable {
+    case i32, i64, f32, f64, pointer
+}
+
+struct Parameter: Codable {
+    let label: String?
+    let name: String
+    let type: BridgeType
+}
+
+// MARK: - Exported Skeleton
+
+struct ExportedFunction: Codable {
+    var name: String
+    var abiName: String
+    var parameters: [Parameter]
+    var returnType: BridgeType
+}
+
+struct ExportedClass: Codable {
+    var name: String
+    var constructor: ExportedConstructor?
+    var methods: [ExportedFunction]
+}
+
+struct ExportedConstructor: Codable {
+    var abiName: String
+    var parameters: [Parameter]
+}
+
+struct ExportedSkeleton: Codable {
+    let functions: [ExportedFunction]
+    let classes: [ExportedClass]
+}
+
+// MARK: - Imported Skeleton
+
+struct ImportedFunctionSkeleton: Codable {
+    let name: String
+    let parameters: [Parameter]
+    let returnType: BridgeType
+    let documentation: String?
+
+    func abiName(context: ImportedTypeSkeleton?) -> String {
+        return context.map { "bjs_\($0.name)_\(name)" } ?? "bjs_\(name)"
+    }
+}
+
+struct ImportedConstructorSkeleton: Codable {
+    let parameters: [Parameter]
+
+    func abiName(context: ImportedTypeSkeleton) -> String {
+        return "bjs_\(context.name)_init"
+    }
+}
+
+struct ImportedPropertySkeleton: Codable {
+    let name: String
+    let isReadonly: Bool
+    let type: BridgeType
+    let documentation: String?
+
+    func getterAbiName(context: ImportedTypeSkeleton) -> String {
+        return "bjs_\(context.name)_\(name)_get"
+    }
+
+    func setterAbiName(context: ImportedTypeSkeleton) -> String {
+        return "bjs_\(context.name)_\(name)_set"
+    }
+}
+
+struct ImportedTypeSkeleton: Codable {
+    let name: String
+    let constructor: ImportedConstructorSkeleton?
+    let methods: [ImportedFunctionSkeleton]
+    let properties: [ImportedPropertySkeleton]
+    let documentation: String?
+}
+
+struct ImportedFileSkeleton: Codable {
+    let functions: [ImportedFunctionSkeleton]
+    let types: [ImportedTypeSkeleton]
+}
+
+struct ImportedModuleSkeleton: Codable {
+    let moduleName: String
+    var children: [ImportedFileSkeleton]
+}
diff --git a/Plugins/BridgeJS/Sources/BridgeJSTool/BridgeJSSkeleton b/Plugins/BridgeJS/Sources/BridgeJSTool/BridgeJSSkeleton
new file mode 120000
index 000000000..a2c26678f
--- /dev/null
+++ b/Plugins/BridgeJS/Sources/BridgeJSTool/BridgeJSSkeleton
@@ -0,0 +1 @@
+../BridgeJSSkeleton
\ No newline at end of file
diff --git a/Plugins/BridgeJS/Sources/BridgeJSTool/BridgeJSTool.swift b/Plugins/BridgeJS/Sources/BridgeJSTool/BridgeJSTool.swift
new file mode 100644
index 000000000..a6bd5ff52
--- /dev/null
+++ b/Plugins/BridgeJS/Sources/BridgeJSTool/BridgeJSTool.swift
@@ -0,0 +1,340 @@
+@preconcurrency import func Foundation.exit
+@preconcurrency import func Foundation.fputs
+@preconcurrency import var Foundation.stderr
+@preconcurrency import struct Foundation.URL
+@preconcurrency import struct Foundation.Data
+@preconcurrency import class Foundation.JSONEncoder
+@preconcurrency import class Foundation.FileManager
+@preconcurrency import class Foundation.JSONDecoder
+@preconcurrency import class Foundation.ProcessInfo
+import SwiftParser
+
+/// BridgeJS Tool
+///
+/// A command-line tool to generate Swift-JavaScript bridge code for WebAssembly applications.
+/// This tool enables bidirectional interoperability between Swift and JavaScript:
+///
+/// 1. Import: Generate Swift bindings for TypeScript declarations
+/// 2. Export: Generate JavaScript bindings for Swift declarations
+///
+/// Usage:
+///   For importing TypeScript:
+///     $ bridge-js import --module-name  --output-swift  --output-skeleton  --project  
+///   For exporting Swift:
+///     $ bridge-js export --output-swift  --output-skeleton  
+///
+/// This tool is intended to be used through the Swift Package Manager plugin system
+/// and is not typically called directly by end users.
+@main struct BridgeJSTool {
+
+    static func help() -> String {
+        return """
+                Usage: \(CommandLine.arguments.first ?? "bridge-js-tool")  [options]
+
+                Subcommands:
+                    import   Generate binding code to import TypeScript APIs into Swift
+                    export   Generate binding code to export Swift APIs to JavaScript
+            """
+    }
+
+    static func main() throws {
+        do {
+            try run()
+        } catch {
+            printStderr("Error: \(error)")
+            exit(1)
+        }
+    }
+
+    static func run() throws {
+        let arguments = Array(CommandLine.arguments.dropFirst())
+        guard let subcommand = arguments.first else {
+            throw BridgeJSToolError(
+                """
+                Error: No subcommand provided
+
+                \(BridgeJSTool.help())
+                """
+            )
+        }
+        let progress = ProgressReporting()
+        switch subcommand {
+        case "import":
+            let parser = ArgumentParser(
+                singleDashOptions: [:],
+                doubleDashOptions: [
+                    "module-name": OptionRule(
+                        help: "The name of the module to import the TypeScript API into",
+                        required: true
+                    ),
+                    "always-write": OptionRule(
+                        help: "Always write the output files even if no APIs are imported",
+                        required: false
+                    ),
+                    "output-swift": OptionRule(help: "The output file path for the Swift source code", required: true),
+                    "output-skeleton": OptionRule(
+                        help: "The output file path for the skeleton of the imported TypeScript APIs",
+                        required: true
+                    ),
+                    "project": OptionRule(
+                        help: "The path to the TypeScript project configuration file",
+                        required: true
+                    ),
+                ]
+            )
+            let (positionalArguments, _, doubleDashOptions) = try parser.parse(
+                arguments: Array(arguments.dropFirst())
+            )
+            var importer = ImportTS(progress: progress, moduleName: doubleDashOptions["module-name"]!)
+            for inputFile in positionalArguments {
+                if inputFile.hasSuffix(".json") {
+                    let sourceURL = URL(fileURLWithPath: inputFile)
+                    let skeleton = try JSONDecoder().decode(
+                        ImportedFileSkeleton.self,
+                        from: Data(contentsOf: sourceURL)
+                    )
+                    importer.addSkeleton(skeleton)
+                } else if inputFile.hasSuffix(".d.ts") {
+                    let tsconfigPath = URL(fileURLWithPath: doubleDashOptions["project"]!)
+                    try importer.addSourceFile(inputFile, tsconfigPath: tsconfigPath.path)
+                }
+            }
+
+            let outputSwift = try importer.finalize()
+            let shouldWrite = doubleDashOptions["always-write"] == "true" || outputSwift != nil
+            guard shouldWrite else {
+                progress.print("No imported TypeScript APIs found")
+                return
+            }
+
+            let outputSwiftURL = URL(fileURLWithPath: doubleDashOptions["output-swift"]!)
+            try FileManager.default.createDirectory(
+                at: outputSwiftURL.deletingLastPathComponent(),
+                withIntermediateDirectories: true,
+                attributes: nil
+            )
+            try (outputSwift ?? "").write(to: outputSwiftURL, atomically: true, encoding: .utf8)
+
+            let outputSkeletonsURL = URL(fileURLWithPath: doubleDashOptions["output-skeleton"]!)
+            try FileManager.default.createDirectory(
+                at: outputSkeletonsURL.deletingLastPathComponent(),
+                withIntermediateDirectories: true,
+                attributes: nil
+            )
+            let encoder = JSONEncoder()
+            encoder.outputFormatting = [.prettyPrinted, .sortedKeys]
+            try encoder.encode(importer.skeleton).write(to: outputSkeletonsURL)
+
+            progress.print(
+                """
+                Imported TypeScript APIs:
+                  - \(outputSwiftURL.path)
+                  - \(outputSkeletonsURL.path)
+                """
+            )
+        case "export":
+            let parser = ArgumentParser(
+                singleDashOptions: [:],
+                doubleDashOptions: [
+                    "output-skeleton": OptionRule(
+                        help: "The output file path for the skeleton of the exported Swift APIs",
+                        required: true
+                    ),
+                    "output-swift": OptionRule(help: "The output file path for the Swift source code", required: true),
+                    "always-write": OptionRule(
+                        help: "Always write the output files even if no APIs are exported",
+                        required: false
+                    ),
+                ]
+            )
+            let (positionalArguments, _, doubleDashOptions) = try parser.parse(
+                arguments: Array(arguments.dropFirst())
+            )
+            let exporter = ExportSwift(progress: progress)
+            for inputFile in positionalArguments {
+                let sourceURL = URL(fileURLWithPath: inputFile)
+                guard sourceURL.pathExtension == "swift" else { continue }
+                let sourceContent = try String(contentsOf: sourceURL, encoding: .utf8)
+                let sourceFile = Parser.parse(source: sourceContent)
+                try exporter.addSourceFile(sourceFile, sourceURL.path)
+            }
+
+            // Finalize the export
+            let output = try exporter.finalize()
+            let outputSwiftURL = URL(fileURLWithPath: doubleDashOptions["output-swift"]!)
+            let outputSkeletonURL = URL(fileURLWithPath: doubleDashOptions["output-skeleton"]!)
+
+            let shouldWrite = doubleDashOptions["always-write"] == "true" || output != nil
+            guard shouldWrite else {
+                progress.print("No exported Swift APIs found")
+                return
+            }
+
+            // Create the output directory if it doesn't exist
+            try FileManager.default.createDirectory(
+                at: outputSwiftURL.deletingLastPathComponent(),
+                withIntermediateDirectories: true,
+                attributes: nil
+            )
+            try FileManager.default.createDirectory(
+                at: outputSkeletonURL.deletingLastPathComponent(),
+                withIntermediateDirectories: true,
+                attributes: nil
+            )
+
+            // Write the output Swift file
+            try (output?.outputSwift ?? "").write(to: outputSwiftURL, atomically: true, encoding: .utf8)
+
+            if let outputSkeleton = output?.outputSkeleton {
+                // Write the output skeleton file
+                let encoder = JSONEncoder()
+                encoder.outputFormatting = [.prettyPrinted, .sortedKeys]
+                let outputSkeletonData = try encoder.encode(outputSkeleton)
+                try outputSkeletonData.write(to: outputSkeletonURL)
+            }
+            progress.print(
+                """
+                Exported Swift APIs:
+                  - \(outputSwiftURL.path)
+                  - \(outputSkeletonURL.path)
+                """
+            )
+        default:
+            throw BridgeJSToolError(
+                """
+                Error: Invalid subcommand: \(subcommand)
+
+                \(BridgeJSTool.help())
+                """
+            )
+        }
+    }
+}
+
+internal func which(_ executable: String) throws -> URL {
+    do {
+        // Check overriding environment variable
+        let envVariable = executable.uppercased().replacingOccurrences(of: "-", with: "_") + "_PATH"
+        if let path = ProcessInfo.processInfo.environment[envVariable] {
+            let url = URL(fileURLWithPath: path).appendingPathComponent(executable)
+            if FileManager.default.isExecutableFile(atPath: url.path) {
+                return url
+            }
+        }
+    }
+    let pathSeparator: Character
+    #if os(Windows)
+    pathSeparator = ";"
+    #else
+    pathSeparator = ":"
+    #endif
+    let paths = ProcessInfo.processInfo.environment["PATH"]!.split(separator: pathSeparator)
+    for path in paths {
+        let url = URL(fileURLWithPath: String(path)).appendingPathComponent(executable)
+        if FileManager.default.isExecutableFile(atPath: url.path) {
+            return url
+        }
+    }
+    throw BridgeJSToolError("Executable \(executable) not found in PATH")
+}
+
+struct BridgeJSToolError: Swift.Error, CustomStringConvertible {
+    let description: String
+
+    init(_ message: String) {
+        self.description = message
+    }
+}
+
+private func printStderr(_ message: String) {
+    fputs(message + "\n", stderr)
+}
+
+struct ProgressReporting {
+    let print: (String) -> Void
+
+    init(print: @escaping (String) -> Void = { Swift.print($0) }) {
+        self.print = print
+    }
+
+    static var silent: ProgressReporting {
+        return ProgressReporting(print: { _ in })
+    }
+
+    func print(_ message: String) {
+        self.print(message)
+    }
+}
+
+// MARK: - Minimal Argument Parsing
+
+struct OptionRule {
+    var help: String
+    var required: Bool = false
+}
+
+struct ArgumentParser {
+
+    let singleDashOptions: [String: OptionRule]
+    let doubleDashOptions: [String: OptionRule]
+
+    init(singleDashOptions: [String: OptionRule], doubleDashOptions: [String: OptionRule]) {
+        self.singleDashOptions = singleDashOptions
+        self.doubleDashOptions = doubleDashOptions
+    }
+
+    typealias ParsedArguments = (
+        positionalArguments: [String],
+        singleDashOptions: [String: String],
+        doubleDashOptions: [String: String]
+    )
+
+    func help() -> String {
+        var help = "Usage: \(CommandLine.arguments.first ?? "bridge-js-tool") [options] \n\n"
+        help += "Options:\n"
+        // Align the options by the longest option
+        let maxOptionLength = max(
+            (singleDashOptions.keys.map(\.count).max() ?? 0) + 1,
+            (doubleDashOptions.keys.map(\.count).max() ?? 0) + 2
+        )
+        for (key, rule) in singleDashOptions {
+            help += "  -\(key)\(String(repeating: " ", count: maxOptionLength - key.count)): \(rule.help)\n"
+        }
+        for (key, rule) in doubleDashOptions {
+            help += "  --\(key)\(String(repeating: " ", count: maxOptionLength - key.count)): \(rule.help)\n"
+        }
+        return help
+    }
+
+    func parse(arguments: [String]) throws -> ParsedArguments {
+        var positionalArguments: [String] = []
+        var singleDashOptions: [String: String] = [:]
+        var doubleDashOptions: [String: String] = [:]
+
+        var arguments = arguments.makeIterator()
+
+        while let arg = arguments.next() {
+            if arg.starts(with: "-") {
+                if arg.starts(with: "--") {
+                    let key = String(arg.dropFirst(2))
+                    let value = arguments.next()
+                    doubleDashOptions[key] = value
+                } else {
+                    let key = String(arg.dropFirst(1))
+                    let value = arguments.next()
+                    singleDashOptions[key] = value
+                }
+            } else {
+                positionalArguments.append(arg)
+            }
+        }
+
+        for (key, rule) in self.doubleDashOptions {
+            if rule.required, doubleDashOptions[key] == nil {
+                throw BridgeJSToolError("Option --\(key) is required")
+            }
+        }
+
+        return (positionalArguments, singleDashOptions, doubleDashOptions)
+    }
+}
diff --git a/Plugins/BridgeJS/Sources/BridgeJSTool/DiagnosticError.swift b/Plugins/BridgeJS/Sources/BridgeJSTool/DiagnosticError.swift
new file mode 100644
index 000000000..2688f8da2
--- /dev/null
+++ b/Plugins/BridgeJS/Sources/BridgeJSTool/DiagnosticError.swift
@@ -0,0 +1,23 @@
+import SwiftSyntax
+
+struct DiagnosticError: Error {
+    let node: Syntax
+    let message: String
+    let hint: String?
+
+    init(node: some SyntaxProtocol, message: String, hint: String? = nil) {
+        self.node = Syntax(node)
+        self.message = message
+        self.hint = hint
+    }
+
+    func formattedDescription(fileName: String) -> String {
+        let locationConverter = SourceLocationConverter(fileName: fileName, tree: node.root)
+        let location = locationConverter.location(for: node.position)
+        var description = "\(fileName):\(location.line):\(location.column): error: \(message)"
+        if let hint {
+            description += "\nHint: \(hint)"
+        }
+        return description
+    }
+}
diff --git a/Plugins/BridgeJS/Sources/BridgeJSTool/ExportSwift.swift b/Plugins/BridgeJS/Sources/BridgeJSTool/ExportSwift.swift
new file mode 100644
index 000000000..9b4013473
--- /dev/null
+++ b/Plugins/BridgeJS/Sources/BridgeJSTool/ExportSwift.swift
@@ -0,0 +1,601 @@
+import SwiftBasicFormat
+import SwiftSyntax
+import SwiftSyntaxBuilder
+import class Foundation.FileManager
+
+/// Exports Swift functions and classes to JavaScript
+///
+/// This class processes Swift source files to find declarations marked with `@JS`
+/// and generates:
+/// 1. Swift glue code to call the Swift functions from JavaScript
+/// 2. Skeleton files that define the structure of the exported APIs
+///
+/// The generated skeletons will be used by ``BridgeJSLink`` to generate
+/// JavaScript glue code and TypeScript definitions.
+class ExportSwift {
+    let progress: ProgressReporting
+
+    private var exportedFunctions: [ExportedFunction] = []
+    private var exportedClasses: [ExportedClass] = []
+    private var typeDeclResolver: TypeDeclResolver = TypeDeclResolver()
+
+    init(progress: ProgressReporting = ProgressReporting()) {
+        self.progress = progress
+    }
+
+    /// Processes a Swift source file to find declarations marked with @JS
+    ///
+    /// - Parameters:
+    ///   - sourceFile: The parsed Swift source file to process
+    ///   - inputFilePath: The file path for error reporting
+    func addSourceFile(_ sourceFile: SourceFileSyntax, _ inputFilePath: String) throws {
+        progress.print("Processing \(inputFilePath)")
+        typeDeclResolver.addSourceFile(sourceFile)
+
+        let errors = try parseSingleFile(sourceFile)
+        if errors.count > 0 {
+            throw BridgeJSToolError(
+                errors.map { $0.formattedDescription(fileName: inputFilePath) }
+                    .joined(separator: "\n")
+            )
+        }
+    }
+
+    /// Finalizes the export process and generates the bridge code
+    ///
+    /// - Returns: A tuple containing the generated Swift code and a skeleton
+    /// describing the exported APIs
+    func finalize() throws -> (outputSwift: String, outputSkeleton: ExportedSkeleton)? {
+        guard let outputSwift = renderSwiftGlue() else {
+            return nil
+        }
+        return (
+            outputSwift: outputSwift,
+            outputSkeleton: ExportedSkeleton(functions: exportedFunctions, classes: exportedClasses)
+        )
+    }
+
+    fileprivate final class APICollector: SyntaxAnyVisitor {
+        var exportedFunctions: [ExportedFunction] = []
+        var exportedClasses: [String: ExportedClass] = [:]
+        var errors: [DiagnosticError] = []
+
+        enum State {
+            case topLevel
+            case classBody(name: String)
+        }
+
+        struct StateStack {
+            private var states: [State]
+            var current: State {
+                return states.last!
+            }
+
+            init(_ initialState: State) {
+                self.states = [initialState]
+            }
+            mutating func push(state: State) {
+                states.append(state)
+            }
+
+            mutating func pop() {
+                _ = states.removeLast()
+            }
+        }
+
+        var stateStack: StateStack = StateStack(.topLevel)
+        var state: State {
+            return stateStack.current
+        }
+        let parent: ExportSwift
+
+        init(parent: ExportSwift) {
+            self.parent = parent
+            super.init(viewMode: .sourceAccurate)
+        }
+
+        private func diagnose(node: some SyntaxProtocol, message: String, hint: String? = nil) {
+            errors.append(DiagnosticError(node: node, message: message, hint: hint))
+        }
+
+        private func diagnoseUnsupportedType(node: some SyntaxProtocol, type: String) {
+            diagnose(
+                node: node,
+                message: "Unsupported type: \(type)",
+                hint: "Only primitive types and types defined in the same module are allowed"
+            )
+        }
+
+        override func visit(_ node: FunctionDeclSyntax) -> SyntaxVisitorContinueKind {
+            switch state {
+            case .topLevel:
+                if let exportedFunction = visitFunction(node: node) {
+                    exportedFunctions.append(exportedFunction)
+                }
+                return .skipChildren
+            case .classBody(let name):
+                if let exportedFunction = visitFunction(node: node) {
+                    exportedClasses[name]?.methods.append(exportedFunction)
+                }
+                return .skipChildren
+            }
+        }
+
+        private func visitFunction(node: FunctionDeclSyntax) -> ExportedFunction? {
+            guard node.attributes.hasJSAttribute() else {
+                return nil
+            }
+            let name = node.name.text
+            var parameters: [Parameter] = []
+            for param in node.signature.parameterClause.parameters {
+                guard let type = self.parent.lookupType(for: param.type) else {
+                    diagnoseUnsupportedType(node: param.type, type: param.type.trimmedDescription)
+                    continue
+                }
+                let name = param.secondName?.text ?? param.firstName.text
+                let label = param.firstName.text
+                parameters.append(Parameter(label: label, name: name, type: type))
+            }
+            let returnType: BridgeType
+            if let returnClause = node.signature.returnClause {
+                guard let type = self.parent.lookupType(for: returnClause.type) else {
+                    diagnoseUnsupportedType(node: returnClause.type, type: returnClause.type.trimmedDescription)
+                    return nil
+                }
+                returnType = type
+            } else {
+                returnType = .void
+            }
+
+            let abiName: String
+            switch state {
+            case .topLevel:
+                abiName = "bjs_\(name)"
+            case .classBody(let className):
+                abiName = "bjs_\(className)_\(name)"
+            }
+
+            return ExportedFunction(
+                name: name,
+                abiName: abiName,
+                parameters: parameters,
+                returnType: returnType
+            )
+        }
+
+        override func visit(_ node: InitializerDeclSyntax) -> SyntaxVisitorContinueKind {
+            guard node.attributes.hasJSAttribute() else { return .skipChildren }
+            guard case .classBody(let name) = state else {
+                diagnose(node: node, message: "@JS init must be inside a @JS class")
+                return .skipChildren
+            }
+            var parameters: [Parameter] = []
+            for param in node.signature.parameterClause.parameters {
+                guard let type = self.parent.lookupType(for: param.type) else {
+                    diagnoseUnsupportedType(node: param.type, type: param.type.trimmedDescription)
+                    continue
+                }
+                let name = param.secondName?.text ?? param.firstName.text
+                let label = param.firstName.text
+                parameters.append(Parameter(label: label, name: name, type: type))
+            }
+
+            let constructor = ExportedConstructor(
+                abiName: "bjs_\(name)_init",
+                parameters: parameters
+            )
+            exportedClasses[name]?.constructor = constructor
+            return .skipChildren
+        }
+
+        override func visit(_ node: ClassDeclSyntax) -> SyntaxVisitorContinueKind {
+            let name = node.name.text
+            stateStack.push(state: .classBody(name: name))
+
+            guard node.attributes.hasJSAttribute() else { return .skipChildren }
+            exportedClasses[name] = ExportedClass(
+                name: name,
+                constructor: nil,
+                methods: []
+            )
+            return .visitChildren
+        }
+        override func visitPost(_ node: ClassDeclSyntax) {
+            stateStack.pop()
+        }
+    }
+
+    func parseSingleFile(_ sourceFile: SourceFileSyntax) throws -> [DiagnosticError] {
+        let collector = APICollector(parent: self)
+        collector.walk(sourceFile)
+        exportedFunctions.append(contentsOf: collector.exportedFunctions)
+        exportedClasses.append(contentsOf: collector.exportedClasses.values)
+        return collector.errors
+    }
+
+    func lookupType(for type: TypeSyntax) -> BridgeType? {
+        if let primitive = BridgeType(swiftType: type.trimmedDescription) {
+            return primitive
+        }
+        guard let identifier = type.as(IdentifierTypeSyntax.self) else {
+            return nil
+        }
+        guard let typeDecl = typeDeclResolver.lookupType(for: identifier) else {
+            print("Failed to lookup type \(type.trimmedDescription): not found in typeDeclResolver")
+            return nil
+        }
+        guard typeDecl.is(ClassDeclSyntax.self) || typeDecl.is(ActorDeclSyntax.self) else {
+            print("Failed to lookup type \(type.trimmedDescription): is not a class or actor")
+            return nil
+        }
+        return .swiftHeapObject(typeDecl.name.text)
+    }
+
+    static let prelude: DeclSyntax = """
+        // NOTICE: This is auto-generated code by BridgeJS from JavaScriptKit,
+        // DO NOT EDIT.
+        //
+        // To update this file, just rebuild your project or run
+        // `swift package bridge-js`.
+        @_extern(wasm, module: "bjs", name: "return_string")
+        private func _return_string(_ ptr: UnsafePointer?, _ len: Int32)
+        @_extern(wasm, module: "bjs", name: "init_memory")
+        private func _init_memory(_ sourceId: Int32, _ ptr: UnsafeMutablePointer?)
+        """
+
+    func renderSwiftGlue() -> String? {
+        var decls: [DeclSyntax] = []
+        guard exportedFunctions.count > 0 || exportedClasses.count > 0 else {
+            return nil
+        }
+        decls.append(Self.prelude)
+        for function in exportedFunctions {
+            decls.append(renderSingleExportedFunction(function: function))
+        }
+        for klass in exportedClasses {
+            decls.append(contentsOf: renderSingleExportedClass(klass: klass))
+        }
+        let format = BasicFormat()
+        return decls.map { $0.formatted(using: format).description }.joined(separator: "\n\n")
+    }
+
+    class ExportedThunkBuilder {
+        var body: [CodeBlockItemSyntax] = []
+        var abiParameterForwardings: [LabeledExprSyntax] = []
+        var abiParameterSignatures: [(name: String, type: WasmCoreType)] = []
+        var abiReturnType: WasmCoreType?
+
+        func liftParameter(param: Parameter) {
+            switch param.type {
+            case .bool:
+                abiParameterForwardings.append(
+                    LabeledExprSyntax(
+                        label: param.label,
+                        expression: ExprSyntax("\(raw: param.name) == 1")
+                    )
+                )
+                abiParameterSignatures.append((param.name, .i32))
+            case .int:
+                abiParameterForwardings.append(
+                    LabeledExprSyntax(
+                        label: param.label,
+                        expression: ExprSyntax("\(raw: param.type.swiftType)(\(raw: param.name))")
+                    )
+                )
+                abiParameterSignatures.append((param.name, .i32))
+            case .float:
+                abiParameterForwardings.append(
+                    LabeledExprSyntax(
+                        label: param.label,
+                        expression: ExprSyntax("\(raw: param.name)")
+                    )
+                )
+                abiParameterSignatures.append((param.name, .f32))
+            case .double:
+                abiParameterForwardings.append(
+                    LabeledExprSyntax(
+                        label: param.label,
+                        expression: ExprSyntax("\(raw: param.name)")
+                    )
+                )
+                abiParameterSignatures.append((param.name, .f64))
+            case .string:
+                let bytesLabel = "\(param.name)Bytes"
+                let lengthLabel = "\(param.name)Len"
+                let prepare: CodeBlockItemSyntax = """
+                    let \(raw: param.name) = String(unsafeUninitializedCapacity: Int(\(raw: lengthLabel))) { b in
+                        _init_memory(\(raw: bytesLabel), b.baseAddress.unsafelyUnwrapped)
+                        return Int(\(raw: lengthLabel))
+                    }
+                    """
+                body.append(prepare)
+                abiParameterForwardings.append(
+                    LabeledExprSyntax(
+                        label: param.label,
+                        expression: ExprSyntax("\(raw: param.name)")
+                    )
+                )
+                abiParameterSignatures.append((bytesLabel, .i32))
+                abiParameterSignatures.append((lengthLabel, .i32))
+            case .jsObject:
+                abiParameterForwardings.append(
+                    LabeledExprSyntax(
+                        label: param.label,
+                        expression: ExprSyntax("\(raw: param.name)")
+                    )
+                )
+                abiParameterSignatures.append((param.name, .i32))
+            case .swiftHeapObject:
+                // UnsafeMutableRawPointer is passed as an i32 pointer
+                let objectExpr: ExprSyntax =
+                    "Unmanaged<\(raw: param.type.swiftType)>.fromOpaque(\(raw: param.name)).takeUnretainedValue()"
+                abiParameterForwardings.append(
+                    LabeledExprSyntax(label: param.label, expression: objectExpr)
+                )
+                abiParameterSignatures.append((param.name, .pointer))
+            case .void:
+                break
+            }
+        }
+
+        func call(name: String, returnType: BridgeType) {
+            let retMutability = returnType == .string ? "var" : "let"
+            let callExpr: ExprSyntax =
+                "\(raw: name)(\(raw: abiParameterForwardings.map { $0.description }.joined(separator: ", ")))"
+            if returnType == .void {
+                body.append("\(raw: callExpr)")
+            } else {
+                body.append(
+                    """
+                    \(raw: retMutability) ret = \(raw: callExpr)
+                    """
+                )
+            }
+        }
+
+        func callMethod(klassName: String, methodName: String, returnType: BridgeType) {
+            let _selfParam = self.abiParameterForwardings.removeFirst()
+            let retMutability = returnType == .string ? "var" : "let"
+            let callExpr: ExprSyntax =
+                "\(raw: _selfParam).\(raw: methodName)(\(raw: abiParameterForwardings.map { $0.description }.joined(separator: ", ")))"
+            if returnType == .void {
+                body.append("\(raw: callExpr)")
+            } else {
+                body.append(
+                    """
+                    \(raw: retMutability) ret = \(raw: callExpr)
+                    """
+                )
+            }
+        }
+
+        func lowerReturnValue(returnType: BridgeType) {
+            switch returnType {
+            case .void:
+                abiReturnType = nil
+            case .bool:
+                abiReturnType = .i32
+            case .int:
+                abiReturnType = .i32
+            case .float:
+                abiReturnType = .f32
+            case .double:
+                abiReturnType = .f64
+            case .string:
+                abiReturnType = nil
+            case .jsObject:
+                abiReturnType = .i32
+            case .swiftHeapObject:
+                // UnsafeMutableRawPointer is returned as an i32 pointer
+                abiReturnType = .pointer
+            }
+
+            switch returnType {
+            case .void: break
+            case .int, .float, .double:
+                body.append("return \(raw: abiReturnType!.swiftType)(ret)")
+            case .bool:
+                body.append("return Int32(ret ? 1 : 0)")
+            case .string:
+                body.append(
+                    """
+                    return ret.withUTF8 { ptr in
+                        _return_string(ptr.baseAddress, Int32(ptr.count))
+                    }
+                    """
+                )
+            case .jsObject:
+                body.append(
+                    """
+                    return ret.id
+                    """
+                )
+            case .swiftHeapObject:
+                // Perform a manual retain on the object, which will be balanced by a
+                // release called via FinalizationRegistry
+                body.append(
+                    """
+                    return Unmanaged.passRetained(ret).toOpaque()
+                    """
+                )
+            }
+        }
+
+        func render(abiName: String) -> DeclSyntax {
+            return """
+                @_expose(wasm, "\(raw: abiName)")
+                @_cdecl("\(raw: abiName)")
+                public func _\(raw: abiName)(\(raw: parameterSignature())) -> \(raw: returnSignature()) {
+                \(CodeBlockItemListSyntax(body))
+                }
+                """
+        }
+
+        func parameterSignature() -> String {
+            abiParameterSignatures.map { "\($0.name): \($0.type.swiftType)" }.joined(
+                separator: ", "
+            )
+        }
+
+        func returnSignature() -> String {
+            return abiReturnType?.swiftType ?? "Void"
+        }
+    }
+
+    func renderSingleExportedFunction(function: ExportedFunction) -> DeclSyntax {
+        let builder = ExportedThunkBuilder()
+        for param in function.parameters {
+            builder.liftParameter(param: param)
+        }
+        builder.call(name: function.name, returnType: function.returnType)
+        builder.lowerReturnValue(returnType: function.returnType)
+        return builder.render(abiName: function.abiName)
+    }
+
+    /// # Example
+    ///
+    /// Given the following Swift code:
+    ///
+    /// ```swift
+    /// @JS class Greeter {
+    ///     var name: String
+    ///
+    ///     @JS init(name: String) {
+    ///         self.name = name
+    ///     }
+    ///
+    ///     @JS func greet() -> String {
+    ///         return "Hello, \(name)!"
+    ///     }
+    /// }
+    /// ```
+    ///
+    /// The following Swift glue code will be generated:
+    ///
+    /// ```swift
+    /// @_expose(wasm, "bjs_Greeter_init")
+    /// @_cdecl("bjs_Greeter_init")
+    /// public func _bjs_Greeter_init(nameBytes: Int32, nameLen: Int32) -> UnsafeMutableRawPointer {
+    ///     let name = String(unsafeUninitializedCapacity: Int(nameLen)) { b in
+    ///         _init_memory(nameBytes, b.baseAddress.unsafelyUnwrapped)
+    ///         return Int(nameLen)
+    ///     }
+    ///     let ret = Greeter(name: name)
+    ///     return Unmanaged.passRetained(ret).toOpaque()
+    /// }
+    ///
+    /// @_expose(wasm, "bjs_Greeter_greet")
+    /// @_cdecl("bjs_Greeter_greet")
+    /// public func _bjs_Greeter_greet(pointer: UnsafeMutableRawPointer) -> Void {
+    ///     let _self = Unmanaged.fromOpaque(pointer).takeUnretainedValue()
+    ///     var ret = _self.greet()
+    ///     return ret.withUTF8 { ptr in
+    ///         _return_string(ptr.baseAddress, Int32(ptr.count))
+    ///     }
+    /// }
+    /// @_expose(wasm, "bjs_Greeter_deinit")
+    /// @_cdecl("bjs_Greeter_deinit")
+    /// public func _bjs_Greeter_deinit(pointer: UnsafeMutableRawPointer) {
+    ///     Unmanaged.fromOpaque(pointer).release()
+    /// }
+    /// ```
+    func renderSingleExportedClass(klass: ExportedClass) -> [DeclSyntax] {
+        var decls: [DeclSyntax] = []
+        if let constructor = klass.constructor {
+            let builder = ExportedThunkBuilder()
+            for param in constructor.parameters {
+                builder.liftParameter(param: param)
+            }
+            builder.call(name: klass.name, returnType: .swiftHeapObject(klass.name))
+            builder.lowerReturnValue(returnType: .swiftHeapObject(klass.name))
+            decls.append(builder.render(abiName: constructor.abiName))
+        }
+        for method in klass.methods {
+            let builder = ExportedThunkBuilder()
+            builder.liftParameter(
+                param: Parameter(label: nil, name: "_self", type: .swiftHeapObject(klass.name))
+            )
+            for param in method.parameters {
+                builder.liftParameter(param: param)
+            }
+            builder.callMethod(
+                klassName: klass.name,
+                methodName: method.name,
+                returnType: method.returnType
+            )
+            builder.lowerReturnValue(returnType: method.returnType)
+            decls.append(builder.render(abiName: method.abiName))
+        }
+
+        do {
+            decls.append(
+                """
+                @_expose(wasm, "bjs_\(raw: klass.name)_deinit")
+                @_cdecl("bjs_\(raw: klass.name)_deinit")
+                public func _bjs_\(raw: klass.name)_deinit(pointer: UnsafeMutableRawPointer) {
+                    Unmanaged<\(raw: klass.name)>.fromOpaque(pointer).release()
+                }
+                """
+            )
+        }
+
+        return decls
+    }
+}
+
+extension AttributeListSyntax {
+    fileprivate func hasJSAttribute() -> Bool {
+        return first(where: {
+            $0.as(AttributeSyntax.self)?.attributeName.trimmedDescription == "JS"
+        }) != nil
+    }
+}
+
+extension BridgeType {
+    init?(swiftType: String) {
+        switch swiftType {
+        case "Int":
+            self = .int
+        case "Float":
+            self = .float
+        case "Double":
+            self = .double
+        case "String":
+            self = .string
+        case "Bool":
+            self = .bool
+        case "Void":
+            self = .void
+        default:
+            return nil
+        }
+    }
+}
+
+extension WasmCoreType {
+    var swiftType: String {
+        switch self {
+        case .i32: return "Int32"
+        case .i64: return "Int64"
+        case .f32: return "Float32"
+        case .f64: return "Float64"
+        case .pointer: return "UnsafeMutableRawPointer"
+        }
+    }
+}
+
+extension BridgeType {
+    var swiftType: String {
+        switch self {
+        case .bool: return "Bool"
+        case .int: return "Int"
+        case .float: return "Float"
+        case .double: return "Double"
+        case .string: return "String"
+        case .jsObject(nil): return "JSObject"
+        case .jsObject(let name?): return name
+        case .swiftHeapObject(let name): return name
+        case .void: return "Void"
+        }
+    }
+}
diff --git a/Plugins/BridgeJS/Sources/BridgeJSTool/ImportTS.swift b/Plugins/BridgeJS/Sources/BridgeJSTool/ImportTS.swift
new file mode 100644
index 000000000..a97550bd1
--- /dev/null
+++ b/Plugins/BridgeJS/Sources/BridgeJSTool/ImportTS.swift
@@ -0,0 +1,535 @@
+import SwiftBasicFormat
+import SwiftSyntax
+import SwiftSyntaxBuilder
+import Foundation
+
+/// Imports TypeScript declarations and generates Swift bridge code
+///
+/// This struct processes TypeScript definition files (.d.ts) and generates:
+/// 1. Swift code to call the JavaScript functions from Swift
+/// 2. Skeleton files that define the structure of the imported APIs
+///
+/// The generated skeletons will be used by ``BridgeJSLink`` to generate
+/// JavaScript glue code and TypeScript definitions.
+struct ImportTS {
+    let progress: ProgressReporting
+    private(set) var skeleton: ImportedModuleSkeleton
+    private var moduleName: String {
+        skeleton.moduleName
+    }
+
+    init(progress: ProgressReporting, moduleName: String) {
+        self.progress = progress
+        self.skeleton = ImportedModuleSkeleton(moduleName: moduleName, children: [])
+    }
+
+    /// Adds a skeleton to the importer's state
+    mutating func addSkeleton(_ skeleton: ImportedFileSkeleton) {
+        self.skeleton.children.append(skeleton)
+    }
+
+    /// Processes a TypeScript definition file and extracts its API information
+    mutating func addSourceFile(_ sourceFile: String, tsconfigPath: String) throws {
+        let nodePath = try which("node")
+        let ts2skeletonPath = URL(fileURLWithPath: #filePath)
+            .deletingLastPathComponent()
+            .deletingLastPathComponent()
+            .appendingPathComponent("JavaScript")
+            .appendingPathComponent("bin")
+            .appendingPathComponent("ts2skeleton.js")
+        let arguments = [ts2skeletonPath.path, sourceFile, "--project", tsconfigPath]
+
+        progress.print("Running ts2skeleton...")
+        progress.print("  \(([nodePath.path] + arguments).joined(separator: " "))")
+
+        let process = Process()
+        let stdoutPipe = Pipe()
+        nonisolated(unsafe) var stdoutData = Data()
+
+        process.executableURL = nodePath
+        process.arguments = arguments
+        process.standardOutput = stdoutPipe
+
+        stdoutPipe.fileHandleForReading.readabilityHandler = { handle in
+            let data = handle.availableData
+            if data.count > 0 {
+                stdoutData.append(data)
+            }
+        }
+        try process.forwardTerminationSignals {
+            try process.run()
+            process.waitUntilExit()
+        }
+
+        if process.terminationStatus != 0 {
+            throw BridgeJSToolError("ts2skeleton returned \(process.terminationStatus)")
+        }
+        let skeleton = try JSONDecoder().decode(ImportedFileSkeleton.self, from: stdoutData)
+        self.addSkeleton(skeleton)
+    }
+
+    /// Finalizes the import process and generates Swift code
+    func finalize() throws -> String? {
+        var decls: [DeclSyntax] = []
+        for skeleton in self.skeleton.children {
+            for function in skeleton.functions {
+                let thunkDecls = try renderSwiftThunk(function, topLevelDecls: &decls)
+                decls.append(contentsOf: thunkDecls)
+            }
+            for type in skeleton.types {
+                let typeDecls = try renderSwiftType(type, topLevelDecls: &decls)
+                decls.append(contentsOf: typeDecls)
+            }
+        }
+        if decls.isEmpty {
+            // No declarations to import
+            return nil
+        }
+
+        let format = BasicFormat()
+        let allDecls: [DeclSyntax] = [Self.prelude] + decls
+        return allDecls.map { $0.formatted(using: format).description }.joined(separator: "\n\n")
+    }
+
+    class ImportedThunkBuilder {
+        let abiName: String
+        let moduleName: String
+
+        var body: [CodeBlockItemSyntax] = []
+        var abiParameterForwardings: [LabeledExprSyntax] = []
+        var abiParameterSignatures: [(name: String, type: WasmCoreType)] = []
+        var abiReturnType: WasmCoreType?
+
+        init(moduleName: String, abiName: String) {
+            self.moduleName = moduleName
+            self.abiName = abiName
+        }
+
+        func lowerParameter(param: Parameter) throws {
+            switch param.type {
+            case .bool:
+                abiParameterForwardings.append(
+                    LabeledExprSyntax(
+                        label: param.label,
+                        expression: ExprSyntax("Int32(\(raw: param.name) ? 1 : 0)")
+                    )
+                )
+                abiParameterSignatures.append((param.name, .i32))
+            case .int:
+                abiParameterForwardings.append(
+                    LabeledExprSyntax(
+                        label: param.label,
+                        expression: ExprSyntax("\(raw: param.name)")
+                    )
+                )
+                abiParameterSignatures.append((param.name, .i32))
+            case .float:
+                abiParameterForwardings.append(
+                    LabeledExprSyntax(
+                        label: param.label,
+                        expression: ExprSyntax("\(raw: param.name)")
+                    )
+                )
+                abiParameterSignatures.append((param.name, .f32))
+            case .double:
+                abiParameterForwardings.append(
+                    LabeledExprSyntax(
+                        label: param.label,
+                        expression: ExprSyntax("\(raw: param.name)")
+                    )
+                )
+                abiParameterSignatures.append((param.name, .f64))
+            case .string:
+                let stringIdName = "\(param.name)Id"
+                body.append(
+                    """
+                    var \(raw: param.name) = \(raw: param.name)
+
+                    """
+                )
+                body.append(
+                    """
+                    let \(raw: stringIdName) = \(raw: param.name).withUTF8 { b in
+                        _make_jsstring(b.baseAddress.unsafelyUnwrapped, Int32(b.count))
+                    }
+                    """
+                )
+                abiParameterForwardings.append(
+                    LabeledExprSyntax(
+                        label: param.label,
+                        expression: ExprSyntax("\(raw: stringIdName)")
+                    )
+                )
+                abiParameterSignatures.append((param.name, .i32))
+            case .jsObject(_?):
+                abiParameterSignatures.append((param.name, .i32))
+                abiParameterForwardings.append(
+                    LabeledExprSyntax(
+                        label: param.label,
+                        expression: ExprSyntax("Int32(bitPattern: \(raw: param.name).this.id)")
+                    )
+                )
+            case .jsObject(nil):
+                abiParameterForwardings.append(
+                    LabeledExprSyntax(
+                        label: param.label,
+                        expression: ExprSyntax("Int32(bitPattern: \(raw: param.name).id)")
+                    )
+                )
+                abiParameterSignatures.append((param.name, .i32))
+            case .swiftHeapObject(_):
+                throw BridgeJSToolError("swiftHeapObject is not supported in imported signatures")
+            case .void:
+                break
+            }
+        }
+
+        func call(returnType: BridgeType) {
+            let call: ExprSyntax =
+                "\(raw: abiName)(\(raw: abiParameterForwardings.map { $0.description }.joined(separator: ", ")))"
+            if returnType == .void {
+                body.append("\(raw: call)")
+            } else {
+                body.append("let ret = \(raw: call)")
+            }
+        }
+
+        func liftReturnValue(returnType: BridgeType) throws {
+            switch returnType {
+            case .bool:
+                abiReturnType = .i32
+                body.append("return ret == 1")
+            case .int:
+                abiReturnType = .i32
+                body.append("return \(raw: returnType.swiftType)(ret)")
+            case .float:
+                abiReturnType = .f32
+                body.append("return \(raw: returnType.swiftType)(ret)")
+            case .double:
+                abiReturnType = .f64
+                body.append("return \(raw: returnType.swiftType)(ret)")
+            case .string:
+                abiReturnType = .i32
+                body.append(
+                    """
+                    return String(unsafeUninitializedCapacity: Int(ret)) { b in
+                        _init_memory_with_result(b.baseAddress.unsafelyUnwrapped, Int32(ret))
+                        return Int(ret)
+                    }
+                    """
+                )
+            case .jsObject(let name):
+                abiReturnType = .i32
+                if let name = name {
+                    body.append("return \(raw: name)(takingThis: ret)")
+                } else {
+                    body.append("return JSObject(id: UInt32(bitPattern: ret))")
+                }
+            case .swiftHeapObject(_):
+                throw BridgeJSToolError("swiftHeapObject is not supported in imported signatures")
+            case .void:
+                break
+            }
+        }
+
+        func assignThis(returnType: BridgeType) {
+            guard case .jsObject = returnType else {
+                preconditionFailure("assignThis can only be called with a jsObject return type")
+            }
+            abiReturnType = .i32
+            body.append("self.this = ret")
+        }
+
+        func renderImportDecl() -> DeclSyntax {
+            return DeclSyntax(
+                FunctionDeclSyntax(
+                    attributes: AttributeListSyntax(itemsBuilder: {
+                        "@_extern(wasm, module: \"\(raw: moduleName)\", name: \"\(raw: abiName)\")"
+                    }).with(\.trailingTrivia, .newline),
+                    name: .identifier(abiName),
+                    signature: FunctionSignatureSyntax(
+                        parameterClause: FunctionParameterClauseSyntax(parametersBuilder: {
+                            for param in abiParameterSignatures {
+                                FunctionParameterSyntax(
+                                    firstName: .wildcardToken(),
+                                    secondName: .identifier(param.name),
+                                    type: IdentifierTypeSyntax(name: .identifier(param.type.swiftType))
+                                )
+                            }
+                        }),
+                        returnClause: ReturnClauseSyntax(
+                            arrow: .arrowToken(),
+                            type: IdentifierTypeSyntax(name: .identifier(abiReturnType.map { $0.swiftType } ?? "Void"))
+                        )
+                    )
+                )
+            )
+        }
+
+        func renderThunkDecl(name: String, parameters: [Parameter], returnType: BridgeType) -> DeclSyntax {
+            return DeclSyntax(
+                FunctionDeclSyntax(
+                    name: .identifier(name),
+                    signature: FunctionSignatureSyntax(
+                        parameterClause: FunctionParameterClauseSyntax(parametersBuilder: {
+                            for param in parameters {
+                                FunctionParameterSyntax(
+                                    firstName: .wildcardToken(),
+                                    secondName: .identifier(param.name),
+                                    colon: .colonToken(),
+                                    type: IdentifierTypeSyntax(name: .identifier(param.type.swiftType))
+                                )
+                            }
+                        }),
+                        returnClause: ReturnClauseSyntax(
+                            arrow: .arrowToken(),
+                            type: IdentifierTypeSyntax(name: .identifier(returnType.swiftType))
+                        )
+                    ),
+                    body: CodeBlockSyntax {
+                        self.renderImportDecl()
+                        body
+                    }
+                )
+            )
+        }
+
+        func renderConstructorDecl(parameters: [Parameter]) -> DeclSyntax {
+            return DeclSyntax(
+                InitializerDeclSyntax(
+                    signature: FunctionSignatureSyntax(
+                        parameterClause: FunctionParameterClauseSyntax(
+                            parametersBuilder: {
+                                for param in parameters {
+                                    FunctionParameterSyntax(
+                                        firstName: .wildcardToken(),
+                                        secondName: .identifier(param.name),
+                                        type: IdentifierTypeSyntax(name: .identifier(param.type.swiftType))
+                                    )
+                                }
+                            }
+                        )
+                    ),
+                    bodyBuilder: {
+                        self.renderImportDecl()
+                        body
+                    }
+                )
+            )
+        }
+    }
+
+    static let prelude: DeclSyntax = """
+        // NOTICE: This is auto-generated code by BridgeJS from JavaScriptKit,
+        // DO NOT EDIT.
+        //
+        // To update this file, just rebuild your project or run
+        // `swift package bridge-js`.
+
+        @_spi(JSObject_id) import JavaScriptKit
+
+        @_extern(wasm, module: "bjs", name: "make_jsstring")
+        private func _make_jsstring(_ ptr: UnsafePointer?, _ len: Int32) -> Int32
+
+        @_extern(wasm, module: "bjs", name: "init_memory_with_result")
+        private func _init_memory_with_result(_ ptr: UnsafePointer?, _ len: Int32)
+
+        @_extern(wasm, module: "bjs", name: "free_jsobject")
+        private func _free_jsobject(_ ptr: Int32) -> Void
+        """
+
+    func renderSwiftThunk(
+        _ function: ImportedFunctionSkeleton,
+        topLevelDecls: inout [DeclSyntax]
+    ) throws -> [DeclSyntax] {
+        let builder = ImportedThunkBuilder(moduleName: moduleName, abiName: function.abiName(context: nil))
+        for param in function.parameters {
+            try builder.lowerParameter(param: param)
+        }
+        builder.call(returnType: function.returnType)
+        try builder.liftReturnValue(returnType: function.returnType)
+        return [
+            builder.renderThunkDecl(
+                name: function.name,
+                parameters: function.parameters,
+                returnType: function.returnType
+            )
+            .with(\.leadingTrivia, Self.renderDocumentation(documentation: function.documentation))
+        ]
+    }
+
+    func renderSwiftType(_ type: ImportedTypeSkeleton, topLevelDecls: inout [DeclSyntax]) throws -> [DeclSyntax] {
+        let name = type.name
+
+        func renderMethod(method: ImportedFunctionSkeleton) throws -> [DeclSyntax] {
+            let builder = ImportedThunkBuilder(moduleName: moduleName, abiName: method.abiName(context: type))
+            try builder.lowerParameter(param: Parameter(label: nil, name: "self", type: .jsObject(name)))
+            for param in method.parameters {
+                try builder.lowerParameter(param: param)
+            }
+            builder.call(returnType: method.returnType)
+            try builder.liftReturnValue(returnType: method.returnType)
+            return [
+                builder.renderThunkDecl(
+                    name: method.name,
+                    parameters: method.parameters,
+                    returnType: method.returnType
+                )
+                .with(\.leadingTrivia, Self.renderDocumentation(documentation: method.documentation))
+            ]
+        }
+
+        func renderConstructorDecl(constructor: ImportedConstructorSkeleton) throws -> [DeclSyntax] {
+            let builder = ImportedThunkBuilder(moduleName: moduleName, abiName: constructor.abiName(context: type))
+            for param in constructor.parameters {
+                try builder.lowerParameter(param: param)
+            }
+            builder.call(returnType: .jsObject(name))
+            builder.assignThis(returnType: .jsObject(name))
+            return [
+                builder.renderConstructorDecl(parameters: constructor.parameters)
+            ]
+        }
+
+        func renderGetterDecl(property: ImportedPropertySkeleton) throws -> AccessorDeclSyntax {
+            let builder = ImportedThunkBuilder(
+                moduleName: moduleName,
+                abiName: property.getterAbiName(context: type)
+            )
+            try builder.lowerParameter(param: Parameter(label: nil, name: "self", type: .jsObject(name)))
+            builder.call(returnType: property.type)
+            try builder.liftReturnValue(returnType: property.type)
+            return AccessorDeclSyntax(
+                accessorSpecifier: .keyword(.get),
+                body: CodeBlockSyntax {
+                    builder.renderImportDecl()
+                    builder.body
+                }
+            )
+        }
+
+        func renderSetterDecl(property: ImportedPropertySkeleton) throws -> AccessorDeclSyntax {
+            let builder = ImportedThunkBuilder(
+                moduleName: moduleName,
+                abiName: property.setterAbiName(context: type)
+            )
+            try builder.lowerParameter(param: Parameter(label: nil, name: "self", type: .jsObject(name)))
+            try builder.lowerParameter(param: Parameter(label: nil, name: "newValue", type: property.type))
+            builder.call(returnType: .void)
+            return AccessorDeclSyntax(
+                modifier: DeclModifierSyntax(name: .keyword(.nonmutating)),
+                accessorSpecifier: .keyword(.set),
+                body: CodeBlockSyntax {
+                    builder.renderImportDecl()
+                    builder.body
+                }
+            )
+        }
+
+        func renderPropertyDecl(property: ImportedPropertySkeleton) throws -> [DeclSyntax] {
+            var accessorDecls: [AccessorDeclSyntax] = []
+            accessorDecls.append(try renderGetterDecl(property: property))
+            if !property.isReadonly {
+                accessorDecls.append(try renderSetterDecl(property: property))
+            }
+            return [
+                DeclSyntax(
+                    VariableDeclSyntax(
+                        leadingTrivia: Self.renderDocumentation(documentation: property.documentation),
+                        bindingSpecifier: .keyword(.var),
+                        bindingsBuilder: {
+                            PatternBindingListSyntax {
+                                PatternBindingSyntax(
+                                    pattern: IdentifierPatternSyntax(identifier: .identifier(property.name)),
+                                    typeAnnotation: TypeAnnotationSyntax(
+                                        type: IdentifierTypeSyntax(name: .identifier(property.type.swiftType))
+                                    ),
+                                    accessorBlock: AccessorBlockSyntax(
+                                        accessors: .accessors(
+                                            AccessorDeclListSyntax(accessorDecls)
+                                        )
+                                    )
+                                )
+                            }
+                        }
+                    )
+                )
+            ]
+        }
+        let classDecl = try StructDeclSyntax(
+            leadingTrivia: Self.renderDocumentation(documentation: type.documentation),
+            name: .identifier(name),
+            memberBlockBuilder: {
+                DeclSyntax(
+                    """
+                    let this: JSObject
+                    """
+                ).with(\.trailingTrivia, .newlines(2))
+
+                DeclSyntax(
+                    """
+                    init(this: JSObject) {
+                        self.this = this
+                    }
+                    """
+                ).with(\.trailingTrivia, .newlines(2))
+
+                DeclSyntax(
+                    """
+                    init(takingThis this: Int32) {
+                        self.this = JSObject(id: UInt32(bitPattern: this))
+                    }
+                    """
+                ).with(\.trailingTrivia, .newlines(2))
+
+                if let constructor = type.constructor {
+                    try renderConstructorDecl(constructor: constructor).map { $0.with(\.trailingTrivia, .newlines(2)) }
+                }
+
+                for property in type.properties {
+                    try renderPropertyDecl(property: property).map { $0.with(\.trailingTrivia, .newlines(2)) }
+                }
+
+                for method in type.methods {
+                    try renderMethod(method: method).map { $0.with(\.trailingTrivia, .newlines(2)) }
+                }
+            }
+        )
+
+        return [DeclSyntax(classDecl)]
+    }
+
+    static func renderDocumentation(documentation: String?) -> Trivia {
+        guard let documentation = documentation else {
+            return Trivia()
+        }
+        let lines = documentation.split { $0.isNewline }
+        return Trivia(pieces: lines.flatMap { [TriviaPiece.docLineComment("/// \($0)"), .newlines(1)] })
+    }
+}
+
+extension Foundation.Process {
+    // Monitor termination/interrruption signals to forward them to child process
+    func setSignalForwarding(_ signalNo: Int32) -> DispatchSourceSignal {
+        let signalSource = DispatchSource.makeSignalSource(signal: signalNo)
+        signalSource.setEventHandler { [self] in
+            signalSource.cancel()
+            kill(processIdentifier, signalNo)
+        }
+        signalSource.resume()
+        return signalSource
+    }
+
+    func forwardTerminationSignals(_ body: () throws -> Void) rethrows {
+        let sources = [
+            setSignalForwarding(SIGINT),
+            setSignalForwarding(SIGTERM),
+        ]
+        defer {
+            for source in sources {
+                source.cancel()
+            }
+        }
+        try body()
+    }
+}
diff --git a/Plugins/BridgeJS/Sources/BridgeJSTool/TypeDeclResolver.swift b/Plugins/BridgeJS/Sources/BridgeJSTool/TypeDeclResolver.swift
new file mode 100644
index 000000000..a7b183af7
--- /dev/null
+++ b/Plugins/BridgeJS/Sources/BridgeJSTool/TypeDeclResolver.swift
@@ -0,0 +1,112 @@
+import SwiftSyntax
+
+/// Resolves type declarations from Swift syntax nodes
+class TypeDeclResolver {
+    typealias TypeDecl = NamedDeclSyntax & DeclGroupSyntax & DeclSyntaxProtocol
+    /// A representation of a qualified name of a type declaration
+    ///
+    /// `Outer.Inner` type declaration is represented as ["Outer", "Inner"]
+    typealias QualifiedName = [String]
+    private var typeDeclByQualifiedName: [QualifiedName: TypeDecl] = [:]
+
+    enum Error: Swift.Error {
+        case typeNotFound(QualifiedName)
+    }
+
+    private class TypeDeclCollector: SyntaxVisitor {
+        let resolver: TypeDeclResolver
+        var scope: [TypeDecl] = []
+        var rootTypeDecls: [TypeDecl] = []
+
+        init(resolver: TypeDeclResolver) {
+            self.resolver = resolver
+            super.init(viewMode: .all)
+        }
+
+        func visitNominalDecl(_ node: TypeDecl) -> SyntaxVisitorContinueKind {
+            let name = node.name.text
+            let qualifiedName = scope.map(\.name.text) + [name]
+            resolver.typeDeclByQualifiedName[qualifiedName] = node
+            scope.append(node)
+            return .visitChildren
+        }
+
+        func visitPostNominalDecl() {
+            let type = scope.removeLast()
+            if scope.isEmpty {
+                rootTypeDecls.append(type)
+            }
+        }
+
+        override func visit(_ node: StructDeclSyntax) -> SyntaxVisitorContinueKind {
+            return visitNominalDecl(node)
+        }
+        override func visit(_ node: ClassDeclSyntax) -> SyntaxVisitorContinueKind {
+            return visitNominalDecl(node)
+        }
+        override func visitPost(_ node: ClassDeclSyntax) {
+            visitPostNominalDecl()
+        }
+        override func visit(_ node: ActorDeclSyntax) -> SyntaxVisitorContinueKind {
+            return visitNominalDecl(node)
+        }
+        override func visitPost(_ node: ActorDeclSyntax) {
+            visitPostNominalDecl()
+        }
+        override func visitPost(_ node: StructDeclSyntax) {
+            visitPostNominalDecl()
+        }
+        override func visit(_ node: EnumDeclSyntax) -> SyntaxVisitorContinueKind {
+            return visitNominalDecl(node)
+        }
+        override func visitPost(_ node: EnumDeclSyntax) {
+            visitPostNominalDecl()
+        }
+    }
+
+    /// Collects type declarations from a parsed Swift source file
+    func addSourceFile(_ sourceFile: SourceFileSyntax) {
+        let collector = TypeDeclCollector(resolver: self)
+        collector.walk(sourceFile)
+    }
+
+    /// Builds the type name scope for a given type usage
+    private func buildScope(type: IdentifierTypeSyntax) -> QualifiedName {
+        var innerToOuter: [String] = []
+        var context: SyntaxProtocol = type
+        while let parent = context.parent {
+            if let parent = parent.asProtocol(NamedDeclSyntax.self), parent.isProtocol(DeclGroupSyntax.self) {
+                innerToOuter.append(parent.name.text)
+            }
+            context = parent
+        }
+        return innerToOuter.reversed()
+    }
+
+    /// Looks up a qualified name of a type declaration by its unqualified type usage
+    /// Returns the qualified name hierarchy of the type declaration
+    /// If the type declaration is not found, returns the unqualified name
+    private func tryQualify(type: IdentifierTypeSyntax) -> QualifiedName {
+        let name = type.name.text
+        let scope = buildScope(type: type)
+        /// Search for the type declaration from the innermost scope to the outermost scope
+        for i in (0...scope.count).reversed() {
+            let qualifiedName = Array(scope[0.. TypeDecl? {
+        let qualifiedName = tryQualify(type: type)
+        return typeDeclByQualifiedName[qualifiedName]
+    }
+
+    /// Looks up a type declaration by its fully qualified name
+    func lookupType(fullyQualified: QualifiedName) -> TypeDecl? {
+        return typeDeclByQualifiedName[fullyQualified]
+    }
+}
diff --git a/Plugins/BridgeJS/Sources/JavaScript/README.md b/Plugins/BridgeJS/Sources/JavaScript/README.md
new file mode 100644
index 000000000..de6806350
--- /dev/null
+++ b/Plugins/BridgeJS/Sources/JavaScript/README.md
@@ -0,0 +1,3 @@
+# ts2skeleton
+
+This script analyzes the TypeScript type definitions and produces a structured JSON output with skeleton information that can be used to generate Swift bindings.
diff --git a/Plugins/BridgeJS/Sources/JavaScript/bin/ts2skeleton.js b/Plugins/BridgeJS/Sources/JavaScript/bin/ts2skeleton.js
new file mode 100755
index 000000000..ba926a889
--- /dev/null
+++ b/Plugins/BridgeJS/Sources/JavaScript/bin/ts2skeleton.js
@@ -0,0 +1,14 @@
+#!/usr/bin/env node
+// @ts-check
+
+/**
+ * Main entry point for the ts2skeleton tool
+ *
+ * This script analyzes the TypeScript type definitions and produces a structured
+ * JSON output with skeleton information that can be used to generate Swift
+ * bindings.
+ */
+
+import { main } from "../src/cli.js"
+
+main(process.argv.slice(2));
diff --git a/Plugins/BridgeJS/Sources/JavaScript/package.json b/Plugins/BridgeJS/Sources/JavaScript/package.json
new file mode 100644
index 000000000..48fb77cfc
--- /dev/null
+++ b/Plugins/BridgeJS/Sources/JavaScript/package.json
@@ -0,0 +1,9 @@
+{
+    "type": "module",
+    "dependencies": {
+        "typescript": "5.8.2"
+    },
+    "bin": {
+        "ts2skeleton": "./bin/ts2skeleton.js"
+    }
+}
diff --git a/Plugins/BridgeJS/Sources/JavaScript/src/cli.js b/Plugins/BridgeJS/Sources/JavaScript/src/cli.js
new file mode 100644
index 000000000..6d2a1ed84
--- /dev/null
+++ b/Plugins/BridgeJS/Sources/JavaScript/src/cli.js
@@ -0,0 +1,139 @@
+// @ts-check
+import * as fs from 'fs';
+import { TypeProcessor } from './processor.js';
+import { parseArgs } from 'util';
+import ts from 'typescript';
+import path from 'path';
+
+class DiagnosticEngine {
+    constructor() {
+        /** @type {ts.FormatDiagnosticsHost} */
+        this.formattHost = {
+            getCanonicalFileName: (fileName) => fileName,
+            getNewLine: () => ts.sys.newLine,
+            getCurrentDirectory: () => ts.sys.getCurrentDirectory(),
+        };
+    }
+    
+    /**
+     * @param {readonly ts.Diagnostic[]} diagnostics
+     */
+    tsDiagnose(diagnostics) {
+        const message = ts.formatDiagnosticsWithColorAndContext(diagnostics, this.formattHost);
+        console.log(message);
+    }
+
+    /**
+     * @param {string} message
+     * @param {ts.Node | undefined} node
+     */
+    info(message, node = undefined) {
+        this.printLog("info", '\x1b[32m', message, node);
+    }
+
+    /**
+     * @param {string} message
+     * @param {ts.Node | undefined} node
+     */
+    warn(message, node = undefined) {
+        this.printLog("warning", '\x1b[33m', message, node);
+    }
+
+    /**
+     * @param {string} message
+     */
+    error(message) {
+        this.printLog("error", '\x1b[31m', message);
+    }
+
+    /**
+     * @param {string} level
+     * @param {string} color
+     * @param {string} message
+     * @param {ts.Node | undefined} node
+     */
+    printLog(level, color, message, node = undefined) {
+        if (node) {
+            const sourceFile = node.getSourceFile();
+            const { line, character } = sourceFile.getLineAndCharacterOfPosition(node.getStart());
+            const location = sourceFile.fileName + ":" + (line + 1) + ":" + (character);
+            process.stderr.write(`${location}: ${color}${level}\x1b[0m: ${message}\n`);
+        } else {
+            process.stderr.write(`${color}${level}\x1b[0m: ${message}\n`);
+        }
+    }
+}
+
+function printUsage() {
+    console.error('Usage: ts2skeleton  -p  [-o output.json]');
+}
+
+/**
+ * Main function to run the CLI
+ * @param {string[]} args - Command-line arguments
+ * @returns {void}
+ */
+export function main(args) {
+    // Parse command line arguments
+    const options = parseArgs({
+        args,
+        options: {
+            output: {
+                type: 'string',
+                short: 'o',
+            },
+            project: {
+                type: 'string',
+                short: 'p',
+            }
+        },
+        allowPositionals: true
+    })
+
+    if (options.positionals.length !== 1) {
+        printUsage();
+        process.exit(1);
+    }
+
+    const tsconfigPath = options.values.project;
+    if (!tsconfigPath) {
+        printUsage();
+        process.exit(1);
+    }
+
+    const filePath = options.positionals[0];
+    const diagnosticEngine = new DiagnosticEngine();
+
+    diagnosticEngine.info(`Processing ${filePath}...`);
+
+    // Create TypeScript program and process declarations
+    const configFile = ts.readConfigFile(tsconfigPath, ts.sys.readFile);
+    const configParseResult = ts.parseJsonConfigFileContent(
+        configFile.config,
+        ts.sys,
+        path.dirname(path.resolve(tsconfigPath))
+    );
+
+    if (configParseResult.errors.length > 0) {
+        diagnosticEngine.tsDiagnose(configParseResult.errors);
+        process.exit(1);
+    }
+
+    const program = TypeProcessor.createProgram(filePath, configParseResult.options);
+    const diagnostics = program.getSemanticDiagnostics();
+    if (diagnostics.length > 0) {
+        diagnosticEngine.tsDiagnose(diagnostics);
+        process.exit(1);
+    }
+
+    const processor = new TypeProcessor(program.getTypeChecker(), diagnosticEngine);
+    const results = processor.processTypeDeclarations(program, filePath);
+
+    // Write results to file or stdout
+    const jsonOutput = JSON.stringify(results, null, 2);
+    if (options.values.output) {
+        fs.writeFileSync(options.values.output, jsonOutput);
+    } else {
+        process.stdout.write(jsonOutput, "utf-8");
+    }
+}
diff --git a/Plugins/BridgeJS/Sources/JavaScript/src/index.d.ts b/Plugins/BridgeJS/Sources/JavaScript/src/index.d.ts
new file mode 100644
index 000000000..e1daa4af2
--- /dev/null
+++ b/Plugins/BridgeJS/Sources/JavaScript/src/index.d.ts
@@ -0,0 +1,44 @@
+export type BridgeType =
+    | { "int": {} }
+    | { "float": {} }
+    | { "double": {} }
+    | { "string": {} }
+    | { "bool": {} }
+    | { "jsObject": { "_0": string } | {} }
+    | { "void": {} }
+
+export type Parameter = {
+    name: string;
+    type: BridgeType;
+}
+
+export type ImportFunctionSkeleton = {
+    name: string;
+    parameters: Parameter[];
+    returnType: BridgeType;
+    documentation: string | undefined;
+}
+
+export type ImportConstructorSkeleton = {
+    parameters: Parameter[];
+}
+
+export type ImportPropertySkeleton = {
+    name: string;
+    type: BridgeType;
+    isReadonly: boolean;
+    documentation: string | undefined;
+}
+
+export type ImportTypeSkeleton = {
+    name: string;
+    documentation: string | undefined;
+    constructor?: ImportConstructorSkeleton;
+    properties: ImportPropertySkeleton[];
+    methods: ImportFunctionSkeleton[];
+}
+
+export type ImportSkeleton = {
+    functions: ImportFunctionSkeleton[];
+    types: ImportTypeSkeleton[];
+}
diff --git a/Plugins/BridgeJS/Sources/JavaScript/src/processor.js b/Plugins/BridgeJS/Sources/JavaScript/src/processor.js
new file mode 100644
index 000000000..e3887b3c1
--- /dev/null
+++ b/Plugins/BridgeJS/Sources/JavaScript/src/processor.js
@@ -0,0 +1,414 @@
+/**
+ * TypeScript type processing functionality
+ * @module processor
+ */
+
+// @ts-check
+import ts from 'typescript';
+
+/** @typedef {import('./index').ImportSkeleton} ImportSkeleton */
+/** @typedef {import('./index').ImportFunctionSkeleton} ImportFunctionSkeleton */
+/** @typedef {import('./index').ImportTypeSkeleton} ImportTypeSkeleton */
+/** @typedef {import('./index').ImportPropertySkeleton} ImportPropertySkeleton */
+/** @typedef {import('./index').ImportConstructorSkeleton} ImportConstructorSkeleton */
+/** @typedef {import('./index').Parameter} Parameter */
+/** @typedef {import('./index').BridgeType} BridgeType */
+
+/**
+ * @typedef {{
+ *   warn: (message: string, node?: ts.Node) => void,
+ *   error: (message: string, node?: ts.Node) => void,
+ * }} DiagnosticEngine
+ */
+
+/**
+ * TypeScript type processor class
+ */
+export class TypeProcessor {
+    /**
+     * Create a TypeScript program from a d.ts file
+     * @param {string} filePath - Path to the d.ts file
+     * @param {ts.CompilerOptions} options - Compiler options
+     * @returns {ts.Program} TypeScript program object
+     */
+    static createProgram(filePath, options) {
+        const host = ts.createCompilerHost(options);
+        return ts.createProgram([filePath], options, host);
+    }
+
+    /**
+     * @param {ts.TypeChecker} checker - TypeScript type checker
+     * @param {DiagnosticEngine} diagnosticEngine - Diagnostic engine
+     */
+    constructor(checker, diagnosticEngine, options = {
+        inheritIterable: true,
+        inheritArraylike: true,
+        inheritPromiselike: true,
+        addAllParentMembersToClass: true,
+        replaceAliasToFunction: true,
+        replaceRankNFunction: true,
+        replaceNewableFunction: true,
+        noExtendsInTyprm: false,
+    }) {
+        this.checker = checker;
+        this.diagnosticEngine = diagnosticEngine;
+        this.options = options;
+
+        /** @type {Map} */
+        this.processedTypes = new Map();
+        /** @type {Map} Seen position by type */
+        this.seenTypes = new Map();
+        /** @type {ImportFunctionSkeleton[]} */
+        this.functions = [];
+        /** @type {ImportTypeSkeleton[]} */
+        this.types = [];
+    }
+
+    /**
+     * Process type declarations from a TypeScript program
+     * @param {ts.Program} program - TypeScript program
+     * @param {string} inputFilePath - Path to the input file
+     * @returns {ImportSkeleton} Processed type declarations
+     */
+    processTypeDeclarations(program, inputFilePath) {
+        const sourceFiles = program.getSourceFiles().filter(
+            sf => !sf.isDeclarationFile || sf.fileName === inputFilePath
+        );
+
+        for (const sourceFile of sourceFiles) {
+            if (sourceFile.fileName.includes('node_modules/typescript/lib')) continue;
+
+            Error.stackTraceLimit = 100;
+
+            try {
+                sourceFile.forEachChild(node => {
+                    this.visitNode(node);
+
+                    for (const [type, node] of this.seenTypes) {
+                        this.seenTypes.delete(type);
+                        const typeString = this.checker.typeToString(type);
+                        const members = type.getProperties();
+                        if (members) {
+                            const type = this.visitStructuredType(typeString, members);
+                            this.types.push(type);
+                        } else {
+                            this.types.push(this.createUnknownType(typeString));
+                        }
+                    }
+                });
+            } catch (error) {
+                this.diagnosticEngine.error(`Error processing ${sourceFile.fileName}: ${error.message}`);
+            }
+        }
+
+        return { functions: this.functions, types: this.types };
+    }
+
+    /**
+     * Create an unknown type
+     * @param {string} typeString - Type string
+     * @returns {ImportTypeSkeleton} Unknown type
+     */
+    createUnknownType(typeString) {
+        return {
+            name: typeString,
+            documentation: undefined,
+            properties: [],
+            methods: [],
+            constructor: undefined,
+        };
+    }
+
+    /**
+     * Visit a node and process it
+     * @param {ts.Node} node - The node to visit
+     */
+    visitNode(node) {
+        if (ts.isFunctionDeclaration(node)) {
+            const func = this.visitFunctionLikeDecl(node);
+            if (func && node.name) {
+                this.functions.push({ ...func, name: node.name.getText() });
+            }
+        } else if (ts.isClassDeclaration(node)) {
+            const cls = this.visitClassDecl(node);
+            if (cls) this.types.push(cls);
+        }
+    }
+
+    /**
+     * Process a function declaration into ImportFunctionSkeleton format
+     * @param {ts.SignatureDeclaration} node - The function node
+     * @returns {ImportFunctionSkeleton | null} Processed function
+     * @private
+     */
+    visitFunctionLikeDecl(node) {
+        if (!node.name) return null;
+
+        const signature = this.checker.getSignatureFromDeclaration(node);
+        if (!signature) return null;
+
+        /** @type {Parameter[]} */
+        const parameters = [];
+        for (const p of signature.getParameters()) {
+            const bridgeType = this.visitSignatureParameter(p, node);
+            parameters.push(bridgeType);
+        }
+
+        const returnType = signature.getReturnType();
+        const bridgeReturnType = this.visitType(returnType, node);
+        const documentation = this.getFullJSDocText(node);
+
+        return {
+            name: node.name.getText(),
+            parameters,
+            returnType: bridgeReturnType,
+            documentation,
+        };
+    }
+
+    /**
+     * Get the full JSDoc text from a node
+     * @param {ts.Node} node - The node to get the JSDoc text from
+     * @returns {string | undefined} The full JSDoc text
+     */
+    getFullJSDocText(node) {
+        const docs = ts.getJSDocCommentsAndTags(node);
+        const parts = [];
+        for (const doc of docs) {
+            if (ts.isJSDoc(doc)) {
+                parts.push(doc.comment ?? "");
+            }
+        }
+        if (parts.length === 0) return undefined;
+        return parts.join("\n");
+    }
+
+    /**
+     * @param {ts.ConstructorDeclaration} node
+     * @returns {ImportConstructorSkeleton | null}
+     */
+    visitConstructorDecl(node) {
+        const signature = this.checker.getSignatureFromDeclaration(node);
+        if (!signature) return null;
+
+        const parameters = [];
+        for (const p of signature.getParameters()) {
+            const bridgeType = this.visitSignatureParameter(p, node);
+            parameters.push(bridgeType);
+        }
+
+        return { parameters };
+    }
+
+    /**
+     * @param {ts.PropertyDeclaration | ts.PropertySignature} node
+     * @returns {ImportPropertySkeleton | null}
+     */
+    visitPropertyDecl(node) {
+        if (!node.name) return null;
+        const type = this.checker.getTypeAtLocation(node)
+        const bridgeType = this.visitType(type, node);
+        const isReadonly = node.modifiers?.some(m => m.kind === ts.SyntaxKind.ReadonlyKeyword) ?? false;
+        const documentation = this.getFullJSDocText(node);
+        return { name: node.name.getText(), type: bridgeType, isReadonly, documentation };
+    }
+
+    /**
+     * @param {ts.Symbol} symbol
+     * @param {ts.Node} node
+     * @returns {Parameter}
+     */
+    visitSignatureParameter(symbol, node) {
+        const type = this.checker.getTypeOfSymbolAtLocation(symbol, node);
+        const bridgeType = this.visitType(type, node);
+        return { name: symbol.name, type: bridgeType };
+    }
+
+    /**
+     * @param {ts.ClassDeclaration} node 
+     * @returns {ImportTypeSkeleton | null}
+     */
+    visitClassDecl(node) {
+        if (!node.name) return null;
+
+        const name = node.name.text;
+        const properties = [];
+        const methods = [];
+        /** @type {ImportConstructorSkeleton | undefined} */
+        let constructor = undefined;
+
+        for (const member of node.members) {
+            if (ts.isPropertyDeclaration(member)) {
+                // TODO
+            } else if (ts.isMethodDeclaration(member)) {
+                const decl = this.visitFunctionLikeDecl(member);
+                if (decl) methods.push(decl);
+            } else if (ts.isConstructorDeclaration(member)) {
+                const decl = this.visitConstructorDecl(member);
+                if (decl) constructor = decl;
+            }
+        }
+
+        const documentation = this.getFullJSDocText(node);
+        return {
+            name,
+            constructor,
+            properties,
+            methods,
+            documentation,
+        };
+    }
+
+    /**
+     * @param {ts.SymbolFlags} flags
+     * @returns {string[]}
+     */
+    debugSymbolFlags(flags) {
+        const result = [];
+        for (const key in ts.SymbolFlags) {
+            const val = (ts.SymbolFlags)[key];
+            if (typeof val === "number" && (flags & val) !== 0) {
+                result.push(key);
+            }
+        }
+        return result;
+    }
+
+    /**
+     * @param {ts.TypeFlags} flags
+     * @returns {string[]}
+     */
+    debugTypeFlags(flags) {
+        const result = [];
+        for (const key in ts.TypeFlags) {
+            const val = (ts.TypeFlags)[key];
+            if (typeof val === "number" && (flags & val) !== 0) {
+                result.push(key);
+            }
+        }
+        return result;
+    }
+
+    /**
+     * @param {string} name
+     * @param {ts.Symbol[]} members
+     * @returns {ImportTypeSkeleton}
+     */
+    visitStructuredType(name, members) {
+        /** @type {ImportPropertySkeleton[]} */
+        const properties = [];
+        /** @type {ImportFunctionSkeleton[]} */
+        const methods = [];
+        /** @type {ImportConstructorSkeleton | undefined} */
+        let constructor = undefined;
+        for (const symbol of members) {
+            if (symbol.flags & ts.SymbolFlags.Property) {
+                for (const decl of symbol.getDeclarations() ?? []) {
+                    if (ts.isPropertyDeclaration(decl) || ts.isPropertySignature(decl)) {
+                        const property = this.visitPropertyDecl(decl);
+                        if (property) properties.push(property);
+                    } else if (ts.isMethodSignature(decl)) {
+                        const method = this.visitFunctionLikeDecl(decl);
+                        if (method) methods.push(method);
+                    }
+                }
+            } else if (symbol.flags & ts.SymbolFlags.Method) {
+                for (const decl of symbol.getDeclarations() ?? []) {
+                    if (!ts.isMethodSignature(decl)) {
+                        continue;
+                    }
+                    const method = this.visitFunctionLikeDecl(decl);
+                    if (method) methods.push(method);
+                }
+            } else if (symbol.flags & ts.SymbolFlags.Constructor) {
+                for (const decl of symbol.getDeclarations() ?? []) {
+                    if (!ts.isConstructorDeclaration(decl)) {
+                        continue;
+                    }
+                    const ctor = this.visitConstructorDecl(decl);
+                    if (ctor) constructor = ctor;
+                }
+            }
+        }
+        return { name, properties, methods, constructor, documentation: undefined };
+    }
+
+    /**
+     * Convert TypeScript type string to BridgeType
+     * @param {ts.Type} type - TypeScript type string
+     * @param {ts.Node} node - Node
+     * @returns {BridgeType} Bridge type
+     * @private
+     */
+    visitType(type, node) {
+        const maybeProcessed = this.processedTypes.get(type);
+        if (maybeProcessed) {
+            return maybeProcessed;
+        }
+        /**
+         * @param {ts.Type} type
+         * @returns {BridgeType}
+         */
+        const convert = (type) => {
+            /** @type {Record} */
+            const typeMap = {
+                "number": { "double": {} },
+                "string": { "string": {} },
+                "boolean": { "bool": {} },
+                "void": { "void": {} },
+                "any": { "jsObject": {} },
+                "unknown": { "jsObject": {} },
+                "null": { "void": {} },
+                "undefined": { "void": {} },
+                "bigint": { "int": {} },
+                "object": { "jsObject": {} },
+                "symbol": { "jsObject": {} },
+                "never": { "void": {} },
+            };
+            const typeString = this.checker.typeToString(type);
+            if (typeMap[typeString]) {
+                return typeMap[typeString];
+            }
+
+            if (this.checker.isArrayType(type) || this.checker.isTupleType(type) || type.getCallSignatures().length > 0) {
+                return { "jsObject": {} };
+            }
+            // "a" | "b" -> string
+            if (this.checker.isTypeAssignableTo(type, this.checker.getStringType())) {
+                return { "string": {} };
+            }
+            if (type.getFlags() & ts.TypeFlags.TypeParameter) {
+                return { "jsObject": {} };
+            }
+
+            const typeName = this.deriveTypeName(type);
+            if (!typeName) {
+                this.diagnosticEngine.warn(`Unknown non-nominal type: ${typeString}`, node);
+                return { "jsObject": {} };
+            }
+            this.seenTypes.set(type, node);
+            return { "jsObject": { "_0": typeName } };
+        }
+        const bridgeType = convert(type);
+        this.processedTypes.set(type, bridgeType);
+        return bridgeType;
+    }
+
+    /**
+     * Derive the type name from a type
+     * @param {ts.Type} type - TypeScript type
+     * @returns {string | undefined} Type name
+     * @private
+     */
+    deriveTypeName(type) {
+        const aliasSymbol = type.aliasSymbol;
+        if (aliasSymbol) {
+            return aliasSymbol.name;
+        }
+        const typeSymbol = type.getSymbol();
+        if (typeSymbol) {
+            return typeSymbol.name;
+        }
+        return undefined;
+    }
+}
diff --git a/Plugins/BridgeJS/Tests/BridgeJSToolTests/BridgeJSLinkTests.swift b/Plugins/BridgeJS/Tests/BridgeJSToolTests/BridgeJSLinkTests.swift
new file mode 100644
index 000000000..e052ed427
--- /dev/null
+++ b/Plugins/BridgeJS/Tests/BridgeJSToolTests/BridgeJSLinkTests.swift
@@ -0,0 +1,80 @@
+import Foundation
+import SwiftSyntax
+import SwiftParser
+import Testing
+
+@testable import BridgeJSLink
+@testable import BridgeJSTool
+
+@Suite struct BridgeJSLinkTests {
+    private func snapshot(
+        bridgeJSLink: BridgeJSLink,
+        name: String? = nil,
+        filePath: String = #filePath,
+        function: String = #function,
+        sourceLocation: Testing.SourceLocation = #_sourceLocation
+    ) throws {
+        let (outputJs, outputDts) = try bridgeJSLink.link()
+        try assertSnapshot(
+            name: name,
+            filePath: filePath,
+            function: function,
+            sourceLocation: sourceLocation,
+            input: outputJs.data(using: .utf8)!,
+            fileExtension: "js"
+        )
+        try assertSnapshot(
+            name: name,
+            filePath: filePath,
+            function: function,
+            sourceLocation: sourceLocation,
+            input: outputDts.data(using: .utf8)!,
+            fileExtension: "d.ts"
+        )
+    }
+
+    static let inputsDirectory = URL(fileURLWithPath: #filePath).deletingLastPathComponent().appendingPathComponent(
+        "Inputs"
+    )
+
+    static func collectInputs(extension: String) -> [String] {
+        let fileManager = FileManager.default
+        let inputs = try! fileManager.contentsOfDirectory(atPath: Self.inputsDirectory.path)
+        return inputs.filter { $0.hasSuffix(`extension`) }
+    }
+
+    @Test(arguments: collectInputs(extension: ".swift"))
+    func snapshotExport(input: String) throws {
+        let url = Self.inputsDirectory.appendingPathComponent(input)
+        let sourceFile = Parser.parse(source: try String(contentsOf: url, encoding: .utf8))
+        let swiftAPI = ExportSwift(progress: .silent)
+        try swiftAPI.addSourceFile(sourceFile, input)
+        let name = url.deletingPathExtension().lastPathComponent
+
+        let (_, outputSkeleton) = try #require(try swiftAPI.finalize())
+        let encoder = JSONEncoder()
+        encoder.outputFormatting = [.prettyPrinted, .sortedKeys]
+        let outputSkeletonData = try encoder.encode(outputSkeleton)
+        var bridgeJSLink = BridgeJSLink()
+        try bridgeJSLink.addExportedSkeletonFile(data: outputSkeletonData)
+        try snapshot(bridgeJSLink: bridgeJSLink, name: name + ".Export")
+    }
+
+    @Test(arguments: collectInputs(extension: ".d.ts"))
+    func snapshotImport(input: String) throws {
+        let url = Self.inputsDirectory.appendingPathComponent(input)
+        let tsconfigPath = url.deletingLastPathComponent().appendingPathComponent("tsconfig.json")
+
+        var importTS = ImportTS(progress: .silent, moduleName: "TestModule")
+        try importTS.addSourceFile(url.path, tsconfigPath: tsconfigPath.path)
+        let name = url.deletingPathExtension().deletingPathExtension().lastPathComponent
+
+        let encoder = JSONEncoder()
+        encoder.outputFormatting = [.prettyPrinted, .sortedKeys]
+        let outputSkeletonData = try encoder.encode(importTS.skeleton)
+
+        var bridgeJSLink = BridgeJSLink()
+        try bridgeJSLink.addImportedSkeletonFile(data: outputSkeletonData)
+        try snapshot(bridgeJSLink: bridgeJSLink, name: name + ".Import")
+    }
+}
diff --git a/Plugins/BridgeJS/Tests/BridgeJSToolTests/ExportSwiftTests.swift b/Plugins/BridgeJS/Tests/BridgeJSToolTests/ExportSwiftTests.swift
new file mode 100644
index 000000000..6064bb28a
--- /dev/null
+++ b/Plugins/BridgeJS/Tests/BridgeJSToolTests/ExportSwiftTests.swift
@@ -0,0 +1,57 @@
+import Foundation
+import SwiftSyntax
+import SwiftParser
+import Testing
+
+@testable import BridgeJSTool
+
+@Suite struct ExportSwiftTests {
+    private func snapshot(
+        swiftAPI: ExportSwift,
+        name: String? = nil,
+        filePath: String = #filePath,
+        function: String = #function,
+        sourceLocation: Testing.SourceLocation = #_sourceLocation
+    ) throws {
+        let (outputSwift, outputSkeleton) = try #require(try swiftAPI.finalize())
+        try assertSnapshot(
+            name: name,
+            filePath: filePath,
+            function: function,
+            sourceLocation: sourceLocation,
+            input: outputSwift.data(using: .utf8)!,
+            fileExtension: "swift"
+        )
+        let encoder = JSONEncoder()
+        encoder.outputFormatting = [.prettyPrinted, .sortedKeys]
+        let outputSkeletonData = try encoder.encode(outputSkeleton)
+        try assertSnapshot(
+            name: name,
+            filePath: filePath,
+            function: function,
+            sourceLocation: sourceLocation,
+            input: outputSkeletonData,
+            fileExtension: "json"
+        )
+    }
+
+    static let inputsDirectory = URL(fileURLWithPath: #filePath).deletingLastPathComponent().appendingPathComponent(
+        "Inputs"
+    )
+
+    static func collectInputs() -> [String] {
+        let fileManager = FileManager.default
+        let inputs = try! fileManager.contentsOfDirectory(atPath: Self.inputsDirectory.path)
+        return inputs.filter { $0.hasSuffix(".swift") }
+    }
+
+    @Test(arguments: collectInputs())
+    func snapshot(input: String) throws {
+        let swiftAPI = ExportSwift(progress: .silent)
+        let url = Self.inputsDirectory.appendingPathComponent(input)
+        let sourceFile = Parser.parse(source: try String(contentsOf: url, encoding: .utf8))
+        try swiftAPI.addSourceFile(sourceFile, input)
+        let name = url.deletingPathExtension().lastPathComponent
+        try snapshot(swiftAPI: swiftAPI, name: name)
+    }
+}
diff --git a/Plugins/BridgeJS/Tests/BridgeJSToolTests/ImportTSTests.swift b/Plugins/BridgeJS/Tests/BridgeJSToolTests/ImportTSTests.swift
new file mode 100644
index 000000000..71b0e005f
--- /dev/null
+++ b/Plugins/BridgeJS/Tests/BridgeJSToolTests/ImportTSTests.swift
@@ -0,0 +1,32 @@
+import Testing
+import Foundation
+@testable import BridgeJSTool
+
+@Suite struct ImportTSTests {
+    static let inputsDirectory = URL(fileURLWithPath: #filePath).deletingLastPathComponent().appendingPathComponent(
+        "Inputs"
+    )
+
+    static func collectInputs() -> [String] {
+        let fileManager = FileManager.default
+        let inputs = try! fileManager.contentsOfDirectory(atPath: Self.inputsDirectory.path)
+        return inputs.filter { $0.hasSuffix(".d.ts") }
+    }
+
+    @Test(arguments: collectInputs())
+    func snapshot(input: String) throws {
+        var api = ImportTS(progress: .silent, moduleName: "Check")
+        let url = Self.inputsDirectory.appendingPathComponent(input)
+        let tsconfigPath = url.deletingLastPathComponent().appendingPathComponent("tsconfig.json")
+        try api.addSourceFile(url.path, tsconfigPath: tsconfigPath.path)
+        let outputSwift = try #require(try api.finalize())
+        let name = url.deletingPathExtension().deletingPathExtension().deletingPathExtension().lastPathComponent
+        try assertSnapshot(
+            name: name,
+            filePath: #filePath,
+            function: #function,
+            input: outputSwift.data(using: .utf8)!,
+            fileExtension: "swift"
+        )
+    }
+}
diff --git a/Plugins/BridgeJS/Tests/BridgeJSToolTests/Inputs/ArrayParameter.d.ts b/Plugins/BridgeJS/Tests/BridgeJSToolTests/Inputs/ArrayParameter.d.ts
new file mode 100644
index 000000000..59674e071
--- /dev/null
+++ b/Plugins/BridgeJS/Tests/BridgeJSToolTests/Inputs/ArrayParameter.d.ts
@@ -0,0 +1,3 @@
+export function checkArray(a: number[]): void;
+export function checkArrayWithLength(a: number[], b: number): void;
+export function checkArray(a: Array): void;
diff --git a/Plugins/BridgeJS/Tests/BridgeJSToolTests/Inputs/Interface.d.ts b/Plugins/BridgeJS/Tests/BridgeJSToolTests/Inputs/Interface.d.ts
new file mode 100644
index 000000000..14a8bfad6
--- /dev/null
+++ b/Plugins/BridgeJS/Tests/BridgeJSToolTests/Inputs/Interface.d.ts
@@ -0,0 +1,6 @@
+interface Animatable {
+    animate(keyframes: any, options: any): any;
+    getAnimations(options: any): any;
+}
+
+export function returnAnimatable(): Animatable;
diff --git a/Plugins/BridgeJS/Tests/BridgeJSToolTests/Inputs/PrimitiveParameters.d.ts b/Plugins/BridgeJS/Tests/BridgeJSToolTests/Inputs/PrimitiveParameters.d.ts
new file mode 100644
index 000000000..81a36c530
--- /dev/null
+++ b/Plugins/BridgeJS/Tests/BridgeJSToolTests/Inputs/PrimitiveParameters.d.ts
@@ -0,0 +1 @@
+export function check(a: number, b: boolean): void;
diff --git a/Plugins/BridgeJS/Tests/BridgeJSToolTests/Inputs/PrimitiveParameters.swift b/Plugins/BridgeJS/Tests/BridgeJSToolTests/Inputs/PrimitiveParameters.swift
new file mode 100644
index 000000000..62e780083
--- /dev/null
+++ b/Plugins/BridgeJS/Tests/BridgeJSToolTests/Inputs/PrimitiveParameters.swift
@@ -0,0 +1 @@
+@JS func check(a: Int, b: Float, c: Double, d: Bool) {}
diff --git a/Plugins/BridgeJS/Tests/BridgeJSToolTests/Inputs/PrimitiveReturn.d.ts b/Plugins/BridgeJS/Tests/BridgeJSToolTests/Inputs/PrimitiveReturn.d.ts
new file mode 100644
index 000000000..ba22fef1f
--- /dev/null
+++ b/Plugins/BridgeJS/Tests/BridgeJSToolTests/Inputs/PrimitiveReturn.d.ts
@@ -0,0 +1,2 @@
+export function checkNumber(): number;
+export function checkBoolean(): boolean;
diff --git a/Plugins/BridgeJS/Tests/BridgeJSToolTests/Inputs/PrimitiveReturn.swift b/Plugins/BridgeJS/Tests/BridgeJSToolTests/Inputs/PrimitiveReturn.swift
new file mode 100644
index 000000000..96a5dbc3c
--- /dev/null
+++ b/Plugins/BridgeJS/Tests/BridgeJSToolTests/Inputs/PrimitiveReturn.swift
@@ -0,0 +1,4 @@
+@JS func checkInt() -> Int { fatalError() }
+@JS func checkFloat() -> Float { fatalError() }
+@JS func checkDouble() -> Double { fatalError() }
+@JS func checkBool() -> Bool { fatalError() }
diff --git a/Plugins/BridgeJS/Tests/BridgeJSToolTests/Inputs/StringParameter.d.ts b/Plugins/BridgeJS/Tests/BridgeJSToolTests/Inputs/StringParameter.d.ts
new file mode 100644
index 000000000..c252c9bb9
--- /dev/null
+++ b/Plugins/BridgeJS/Tests/BridgeJSToolTests/Inputs/StringParameter.d.ts
@@ -0,0 +1,2 @@
+export function checkString(a: string): void;
+export function checkStringWithLength(a: string, b: number): void;
diff --git a/Plugins/BridgeJS/Tests/BridgeJSToolTests/Inputs/StringParameter.swift b/Plugins/BridgeJS/Tests/BridgeJSToolTests/Inputs/StringParameter.swift
new file mode 100644
index 000000000..e6763d4cd
--- /dev/null
+++ b/Plugins/BridgeJS/Tests/BridgeJSToolTests/Inputs/StringParameter.swift
@@ -0,0 +1 @@
+@JS func checkString(a: String) {}
diff --git a/Plugins/BridgeJS/Tests/BridgeJSToolTests/Inputs/StringReturn.d.ts b/Plugins/BridgeJS/Tests/BridgeJSToolTests/Inputs/StringReturn.d.ts
new file mode 100644
index 000000000..0be0ecd58
--- /dev/null
+++ b/Plugins/BridgeJS/Tests/BridgeJSToolTests/Inputs/StringReturn.d.ts
@@ -0,0 +1 @@
+export function checkString(): string;
diff --git a/Plugins/BridgeJS/Tests/BridgeJSToolTests/Inputs/StringReturn.swift b/Plugins/BridgeJS/Tests/BridgeJSToolTests/Inputs/StringReturn.swift
new file mode 100644
index 000000000..fe070f0db
--- /dev/null
+++ b/Plugins/BridgeJS/Tests/BridgeJSToolTests/Inputs/StringReturn.swift
@@ -0,0 +1 @@
+@JS func checkString() -> String { fatalError() }
diff --git a/Plugins/BridgeJS/Tests/BridgeJSToolTests/Inputs/SwiftClass.swift b/Plugins/BridgeJS/Tests/BridgeJSToolTests/Inputs/SwiftClass.swift
new file mode 100644
index 000000000..a803504f9
--- /dev/null
+++ b/Plugins/BridgeJS/Tests/BridgeJSToolTests/Inputs/SwiftClass.swift
@@ -0,0 +1,17 @@
+@JS class Greeter {
+    var name: String
+
+    @JS init(name: String) {
+        self.name = name
+    }
+    @JS func greet() -> String {
+        return "Hello, " + self.name + "!"
+    }
+    @JS func changeName(name: String) {
+        self.name = name
+    }
+}
+
+@JS func takeGreeter(greeter: Greeter) {
+    print(greeter.greet())
+}
diff --git a/Plugins/BridgeJS/Tests/BridgeJSToolTests/Inputs/TypeAlias.d.ts b/Plugins/BridgeJS/Tests/BridgeJSToolTests/Inputs/TypeAlias.d.ts
new file mode 100644
index 000000000..6c74bd3c4
--- /dev/null
+++ b/Plugins/BridgeJS/Tests/BridgeJSToolTests/Inputs/TypeAlias.d.ts
@@ -0,0 +1,3 @@
+export type MyType = number;
+
+export function checkSimple(a: MyType): void;
diff --git a/Plugins/BridgeJS/Tests/BridgeJSToolTests/Inputs/TypeScriptClass.d.ts b/Plugins/BridgeJS/Tests/BridgeJSToolTests/Inputs/TypeScriptClass.d.ts
new file mode 100644
index 000000000..d10c0138b
--- /dev/null
+++ b/Plugins/BridgeJS/Tests/BridgeJSToolTests/Inputs/TypeScriptClass.d.ts
@@ -0,0 +1,5 @@
+export class Greeter {
+    constructor(name: string);
+    greet(): string;
+    changeName(name: string): void;
+}
diff --git a/Plugins/BridgeJS/Tests/BridgeJSToolTests/Inputs/VoidParameterVoidReturn.d.ts b/Plugins/BridgeJS/Tests/BridgeJSToolTests/Inputs/VoidParameterVoidReturn.d.ts
new file mode 100644
index 000000000..048ef7534
--- /dev/null
+++ b/Plugins/BridgeJS/Tests/BridgeJSToolTests/Inputs/VoidParameterVoidReturn.d.ts
@@ -0,0 +1 @@
+export function check(): void;
diff --git a/Plugins/BridgeJS/Tests/BridgeJSToolTests/Inputs/VoidParameterVoidReturn.swift b/Plugins/BridgeJS/Tests/BridgeJSToolTests/Inputs/VoidParameterVoidReturn.swift
new file mode 100644
index 000000000..ba0cf5d23
--- /dev/null
+++ b/Plugins/BridgeJS/Tests/BridgeJSToolTests/Inputs/VoidParameterVoidReturn.swift
@@ -0,0 +1 @@
+@JS func check() {}
diff --git a/Plugins/BridgeJS/Tests/BridgeJSToolTests/SnapshotTesting.swift b/Plugins/BridgeJS/Tests/BridgeJSToolTests/SnapshotTesting.swift
new file mode 100644
index 000000000..28b34bf69
--- /dev/null
+++ b/Plugins/BridgeJS/Tests/BridgeJSToolTests/SnapshotTesting.swift
@@ -0,0 +1,42 @@
+import Testing
+import Foundation
+
+func assertSnapshot(
+    name: String? = nil,
+    filePath: String = #filePath,
+    function: String = #function,
+    sourceLocation: SourceLocation = #_sourceLocation,
+    variant: String? = nil,
+    input: Data,
+    fileExtension: String = "json"
+) throws {
+    let testFileName = URL(fileURLWithPath: filePath).deletingPathExtension().lastPathComponent
+    let snapshotDir = URL(fileURLWithPath: filePath)
+        .deletingLastPathComponent()
+        .appendingPathComponent("__Snapshots__")
+        .appendingPathComponent(testFileName)
+    try FileManager.default.createDirectory(at: snapshotDir, withIntermediateDirectories: true)
+    let snapshotName = name ?? String(function[.. Comment {
+            "Snapshot mismatch: \(actualFilePath) \(snapshotPath.path)"
+        }
+        if !ok {
+            try input.write(to: URL(fileURLWithPath: actualFilePath))
+        }
+        if ProcessInfo.processInfo.environment["UPDATE_SNAPSHOTS"] == nil {
+            #expect(ok, buildComment(), sourceLocation: sourceLocation)
+        } else {
+            try input.write(to: snapshotPath)
+        }
+    } else {
+        try input.write(to: snapshotPath)
+        #expect(Bool(false), "Snapshot created at \(snapshotPath.path)", sourceLocation: sourceLocation)
+    }
+}
diff --git a/Plugins/BridgeJS/Tests/BridgeJSToolTests/TemporaryDirectory.swift b/Plugins/BridgeJS/Tests/BridgeJSToolTests/TemporaryDirectory.swift
new file mode 100644
index 000000000..199380fac
--- /dev/null
+++ b/Plugins/BridgeJS/Tests/BridgeJSToolTests/TemporaryDirectory.swift
@@ -0,0 +1,27 @@
+import Foundation
+
+struct MakeTemporaryDirectoryError: Error {
+    let error: CInt
+}
+
+internal func withTemporaryDirectory(body: (URL, _ retain: inout Bool) throws -> T) throws -> T {
+    // Create a temporary directory using mkdtemp
+    var template = FileManager.default.temporaryDirectory.appendingPathComponent("PackageToJSTests.XXXXXX").path
+    return try template.withUTF8 { template in
+        let copy = UnsafeMutableBufferPointer.allocate(capacity: template.count + 1)
+        template.copyBytes(to: copy)
+        copy[template.count] = 0
+
+        guard let result = mkdtemp(copy.baseAddress!) else {
+            throw MakeTemporaryDirectoryError(error: errno)
+        }
+        let tempDir = URL(fileURLWithPath: String(cString: result))
+        var retain = false
+        defer {
+            if !retain {
+                try? FileManager.default.removeItem(at: tempDir)
+            }
+        }
+        return try body(tempDir, &retain)
+    }
+}
diff --git a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/ArrayParameter.Import.d.ts b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/ArrayParameter.Import.d.ts
new file mode 100644
index 000000000..2a6771ca7
--- /dev/null
+++ b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/ArrayParameter.Import.d.ts
@@ -0,0 +1,20 @@
+// NOTICE: This is auto-generated code by BridgeJS from JavaScriptKit,
+// DO NOT EDIT.
+//
+// To update this file, just rebuild your project or run
+// `swift package bridge-js`.
+
+export type Exports = {
+}
+export type Imports = {
+    checkArray(a: any): void;
+    checkArrayWithLength(a: any, b: number): void;
+    checkArray(a: any): void;
+}
+export function createInstantiator(options: {
+    imports: Imports;
+}, swift: any): Promise<{
+    addImports: (importObject: WebAssembly.Imports) => void;
+    setInstance: (instance: WebAssembly.Instance) => void;
+    createExports: (instance: WebAssembly.Instance) => Exports;
+}>;
\ No newline at end of file
diff --git a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/ArrayParameter.Import.js b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/ArrayParameter.Import.js
new file mode 100644
index 000000000..caad458db
--- /dev/null
+++ b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/ArrayParameter.Import.js
@@ -0,0 +1,62 @@
+// NOTICE: This is auto-generated code by BridgeJS from JavaScriptKit,
+// DO NOT EDIT.
+//
+// To update this file, just rebuild your project or run
+// `swift package bridge-js`.
+
+export async function createInstantiator(options, swift) {
+    let instance;
+    let memory;
+    const textDecoder = new TextDecoder("utf-8");
+    const textEncoder = new TextEncoder("utf-8");
+
+    let tmpRetString;
+    let tmpRetBytes;
+    return {
+        /** @param {WebAssembly.Imports} importObject */
+        addImports: (importObject) => {
+            const bjs = {};
+            importObject["bjs"] = bjs;
+            bjs["return_string"] = function(ptr, len) {
+                const bytes = new Uint8Array(memory.buffer, ptr, len);
+                tmpRetString = textDecoder.decode(bytes);
+            }
+            bjs["init_memory"] = function(sourceId, bytesPtr) {
+                const source = swift.memory.getObject(sourceId);
+                const bytes = new Uint8Array(memory.buffer, bytesPtr);
+                bytes.set(source);
+            }
+            bjs["make_jsstring"] = function(ptr, len) {
+                const bytes = new Uint8Array(memory.buffer, ptr, len);
+                return swift.memory.retain(textDecoder.decode(bytes));
+            }
+            bjs["init_memory_with_result"] = function(ptr, len) {
+                const target = new Uint8Array(memory.buffer, ptr, len);
+                target.set(tmpRetBytes);
+                tmpRetBytes = undefined;
+            }
+            const TestModule = importObject["TestModule"] = {};
+            TestModule["bjs_checkArray"] = function bjs_checkArray(a) {
+                options.imports.checkArray(swift.memory.getObject(a));
+            }
+            TestModule["bjs_checkArrayWithLength"] = function bjs_checkArrayWithLength(a, b) {
+                options.imports.checkArrayWithLength(swift.memory.getObject(a), b);
+            }
+            TestModule["bjs_checkArray"] = function bjs_checkArray(a) {
+                options.imports.checkArray(swift.memory.getObject(a));
+            }
+        },
+        setInstance: (i) => {
+            instance = i;
+            memory = instance.exports.memory;
+        },
+        /** @param {WebAssembly.Instance} instance */
+        createExports: (instance) => {
+            const js = swift.memory.heap;
+
+            return {
+
+            };
+        },
+    }
+}
\ No newline at end of file
diff --git a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/Interface.Import.d.ts b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/Interface.Import.d.ts
new file mode 100644
index 000000000..1e7ca6ab1
--- /dev/null
+++ b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/Interface.Import.d.ts
@@ -0,0 +1,18 @@
+// NOTICE: This is auto-generated code by BridgeJS from JavaScriptKit,
+// DO NOT EDIT.
+//
+// To update this file, just rebuild your project or run
+// `swift package bridge-js`.
+
+export type Exports = {
+}
+export type Imports = {
+    returnAnimatable(): any;
+}
+export function createInstantiator(options: {
+    imports: Imports;
+}, swift: any): Promise<{
+    addImports: (importObject: WebAssembly.Imports) => void;
+    setInstance: (instance: WebAssembly.Instance) => void;
+    createExports: (instance: WebAssembly.Instance) => Exports;
+}>;
\ No newline at end of file
diff --git a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/Interface.Import.js b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/Interface.Import.js
new file mode 100644
index 000000000..4b3811859
--- /dev/null
+++ b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/Interface.Import.js
@@ -0,0 +1,65 @@
+// NOTICE: This is auto-generated code by BridgeJS from JavaScriptKit,
+// DO NOT EDIT.
+//
+// To update this file, just rebuild your project or run
+// `swift package bridge-js`.
+
+export async function createInstantiator(options, swift) {
+    let instance;
+    let memory;
+    const textDecoder = new TextDecoder("utf-8");
+    const textEncoder = new TextEncoder("utf-8");
+
+    let tmpRetString;
+    let tmpRetBytes;
+    return {
+        /** @param {WebAssembly.Imports} importObject */
+        addImports: (importObject) => {
+            const bjs = {};
+            importObject["bjs"] = bjs;
+            bjs["return_string"] = function(ptr, len) {
+                const bytes = new Uint8Array(memory.buffer, ptr, len);
+                tmpRetString = textDecoder.decode(bytes);
+            }
+            bjs["init_memory"] = function(sourceId, bytesPtr) {
+                const source = swift.memory.getObject(sourceId);
+                const bytes = new Uint8Array(memory.buffer, bytesPtr);
+                bytes.set(source);
+            }
+            bjs["make_jsstring"] = function(ptr, len) {
+                const bytes = new Uint8Array(memory.buffer, ptr, len);
+                return swift.memory.retain(textDecoder.decode(bytes));
+            }
+            bjs["init_memory_with_result"] = function(ptr, len) {
+                const target = new Uint8Array(memory.buffer, ptr, len);
+                target.set(tmpRetBytes);
+                tmpRetBytes = undefined;
+            }
+            const TestModule = importObject["TestModule"] = {};
+            TestModule["bjs_returnAnimatable"] = function bjs_returnAnimatable() {
+                let ret = options.imports.returnAnimatable();
+                return swift.memory.retain(ret);
+            }
+            TestModule["bjs_Animatable_animate"] = function bjs_Animatable_animate(self, keyframes, options) {
+                let ret = swift.memory.getObject(self).animate(swift.memory.getObject(keyframes), swift.memory.getObject(options));
+                return swift.memory.retain(ret);
+            }
+            TestModule["bjs_Animatable_getAnimations"] = function bjs_Animatable_getAnimations(self, options) {
+                let ret = swift.memory.getObject(self).getAnimations(swift.memory.getObject(options));
+                return swift.memory.retain(ret);
+            }
+        },
+        setInstance: (i) => {
+            instance = i;
+            memory = instance.exports.memory;
+        },
+        /** @param {WebAssembly.Instance} instance */
+        createExports: (instance) => {
+            const js = swift.memory.heap;
+
+            return {
+
+            };
+        },
+    }
+}
\ No newline at end of file
diff --git a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/PrimitiveParameters.Export.d.ts b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/PrimitiveParameters.Export.d.ts
new file mode 100644
index 000000000..a9c37f378
--- /dev/null
+++ b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/PrimitiveParameters.Export.d.ts
@@ -0,0 +1,18 @@
+// NOTICE: This is auto-generated code by BridgeJS from JavaScriptKit,
+// DO NOT EDIT.
+//
+// To update this file, just rebuild your project or run
+// `swift package bridge-js`.
+
+export type Exports = {
+    check(a: number, b: number, c: number, d: boolean): void;
+}
+export type Imports = {
+}
+export function createInstantiator(options: {
+    imports: Imports;
+}, swift: any): Promise<{
+    addImports: (importObject: WebAssembly.Imports) => void;
+    setInstance: (instance: WebAssembly.Instance) => void;
+    createExports: (instance: WebAssembly.Instance) => Exports;
+}>;
\ No newline at end of file
diff --git a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/PrimitiveParameters.Export.js b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/PrimitiveParameters.Export.js
new file mode 100644
index 000000000..2d9ee4b10
--- /dev/null
+++ b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/PrimitiveParameters.Export.js
@@ -0,0 +1,55 @@
+// NOTICE: This is auto-generated code by BridgeJS from JavaScriptKit,
+// DO NOT EDIT.
+//
+// To update this file, just rebuild your project or run
+// `swift package bridge-js`.
+
+export async function createInstantiator(options, swift) {
+    let instance;
+    let memory;
+    const textDecoder = new TextDecoder("utf-8");
+    const textEncoder = new TextEncoder("utf-8");
+
+    let tmpRetString;
+    let tmpRetBytes;
+    return {
+        /** @param {WebAssembly.Imports} importObject */
+        addImports: (importObject) => {
+            const bjs = {};
+            importObject["bjs"] = bjs;
+            bjs["return_string"] = function(ptr, len) {
+                const bytes = new Uint8Array(memory.buffer, ptr, len);
+                tmpRetString = textDecoder.decode(bytes);
+            }
+            bjs["init_memory"] = function(sourceId, bytesPtr) {
+                const source = swift.memory.getObject(sourceId);
+                const bytes = new Uint8Array(memory.buffer, bytesPtr);
+                bytes.set(source);
+            }
+            bjs["make_jsstring"] = function(ptr, len) {
+                const bytes = new Uint8Array(memory.buffer, ptr, len);
+                return swift.memory.retain(textDecoder.decode(bytes));
+            }
+            bjs["init_memory_with_result"] = function(ptr, len) {
+                const target = new Uint8Array(memory.buffer, ptr, len);
+                target.set(tmpRetBytes);
+                tmpRetBytes = undefined;
+            }
+
+        },
+        setInstance: (i) => {
+            instance = i;
+            memory = instance.exports.memory;
+        },
+        /** @param {WebAssembly.Instance} instance */
+        createExports: (instance) => {
+            const js = swift.memory.heap;
+
+            return {
+                check: function bjs_check(a, b, c, d) {
+                    instance.exports.bjs_check(a, b, c, d);
+                },
+            };
+        },
+    }
+}
\ No newline at end of file
diff --git a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/PrimitiveParameters.Import.d.ts b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/PrimitiveParameters.Import.d.ts
new file mode 100644
index 000000000..5442ebfa2
--- /dev/null
+++ b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/PrimitiveParameters.Import.d.ts
@@ -0,0 +1,18 @@
+// NOTICE: This is auto-generated code by BridgeJS from JavaScriptKit,
+// DO NOT EDIT.
+//
+// To update this file, just rebuild your project or run
+// `swift package bridge-js`.
+
+export type Exports = {
+}
+export type Imports = {
+    check(a: number, b: boolean): void;
+}
+export function createInstantiator(options: {
+    imports: Imports;
+}, swift: any): Promise<{
+    addImports: (importObject: WebAssembly.Imports) => void;
+    setInstance: (instance: WebAssembly.Instance) => void;
+    createExports: (instance: WebAssembly.Instance) => Exports;
+}>;
\ No newline at end of file
diff --git a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/PrimitiveParameters.Import.js b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/PrimitiveParameters.Import.js
new file mode 100644
index 000000000..0d871bbb1
--- /dev/null
+++ b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/PrimitiveParameters.Import.js
@@ -0,0 +1,56 @@
+// NOTICE: This is auto-generated code by BridgeJS from JavaScriptKit,
+// DO NOT EDIT.
+//
+// To update this file, just rebuild your project or run
+// `swift package bridge-js`.
+
+export async function createInstantiator(options, swift) {
+    let instance;
+    let memory;
+    const textDecoder = new TextDecoder("utf-8");
+    const textEncoder = new TextEncoder("utf-8");
+
+    let tmpRetString;
+    let tmpRetBytes;
+    return {
+        /** @param {WebAssembly.Imports} importObject */
+        addImports: (importObject) => {
+            const bjs = {};
+            importObject["bjs"] = bjs;
+            bjs["return_string"] = function(ptr, len) {
+                const bytes = new Uint8Array(memory.buffer, ptr, len);
+                tmpRetString = textDecoder.decode(bytes);
+            }
+            bjs["init_memory"] = function(sourceId, bytesPtr) {
+                const source = swift.memory.getObject(sourceId);
+                const bytes = new Uint8Array(memory.buffer, bytesPtr);
+                bytes.set(source);
+            }
+            bjs["make_jsstring"] = function(ptr, len) {
+                const bytes = new Uint8Array(memory.buffer, ptr, len);
+                return swift.memory.retain(textDecoder.decode(bytes));
+            }
+            bjs["init_memory_with_result"] = function(ptr, len) {
+                const target = new Uint8Array(memory.buffer, ptr, len);
+                target.set(tmpRetBytes);
+                tmpRetBytes = undefined;
+            }
+            const TestModule = importObject["TestModule"] = {};
+            TestModule["bjs_check"] = function bjs_check(a, b) {
+                options.imports.check(a, b);
+            }
+        },
+        setInstance: (i) => {
+            instance = i;
+            memory = instance.exports.memory;
+        },
+        /** @param {WebAssembly.Instance} instance */
+        createExports: (instance) => {
+            const js = swift.memory.heap;
+
+            return {
+
+            };
+        },
+    }
+}
\ No newline at end of file
diff --git a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/PrimitiveReturn.Export.d.ts b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/PrimitiveReturn.Export.d.ts
new file mode 100644
index 000000000..da7f59772
--- /dev/null
+++ b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/PrimitiveReturn.Export.d.ts
@@ -0,0 +1,21 @@
+// NOTICE: This is auto-generated code by BridgeJS from JavaScriptKit,
+// DO NOT EDIT.
+//
+// To update this file, just rebuild your project or run
+// `swift package bridge-js`.
+
+export type Exports = {
+    checkInt(): number;
+    checkFloat(): number;
+    checkDouble(): number;
+    checkBool(): boolean;
+}
+export type Imports = {
+}
+export function createInstantiator(options: {
+    imports: Imports;
+}, swift: any): Promise<{
+    addImports: (importObject: WebAssembly.Imports) => void;
+    setInstance: (instance: WebAssembly.Instance) => void;
+    createExports: (instance: WebAssembly.Instance) => Exports;
+}>;
\ No newline at end of file
diff --git a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/PrimitiveReturn.Export.js b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/PrimitiveReturn.Export.js
new file mode 100644
index 000000000..8a66f0412
--- /dev/null
+++ b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/PrimitiveReturn.Export.js
@@ -0,0 +1,68 @@
+// NOTICE: This is auto-generated code by BridgeJS from JavaScriptKit,
+// DO NOT EDIT.
+//
+// To update this file, just rebuild your project or run
+// `swift package bridge-js`.
+
+export async function createInstantiator(options, swift) {
+    let instance;
+    let memory;
+    const textDecoder = new TextDecoder("utf-8");
+    const textEncoder = new TextEncoder("utf-8");
+
+    let tmpRetString;
+    let tmpRetBytes;
+    return {
+        /** @param {WebAssembly.Imports} importObject */
+        addImports: (importObject) => {
+            const bjs = {};
+            importObject["bjs"] = bjs;
+            bjs["return_string"] = function(ptr, len) {
+                const bytes = new Uint8Array(memory.buffer, ptr, len);
+                tmpRetString = textDecoder.decode(bytes);
+            }
+            bjs["init_memory"] = function(sourceId, bytesPtr) {
+                const source = swift.memory.getObject(sourceId);
+                const bytes = new Uint8Array(memory.buffer, bytesPtr);
+                bytes.set(source);
+            }
+            bjs["make_jsstring"] = function(ptr, len) {
+                const bytes = new Uint8Array(memory.buffer, ptr, len);
+                return swift.memory.retain(textDecoder.decode(bytes));
+            }
+            bjs["init_memory_with_result"] = function(ptr, len) {
+                const target = new Uint8Array(memory.buffer, ptr, len);
+                target.set(tmpRetBytes);
+                tmpRetBytes = undefined;
+            }
+
+        },
+        setInstance: (i) => {
+            instance = i;
+            memory = instance.exports.memory;
+        },
+        /** @param {WebAssembly.Instance} instance */
+        createExports: (instance) => {
+            const js = swift.memory.heap;
+
+            return {
+                checkInt: function bjs_checkInt() {
+                    const ret = instance.exports.bjs_checkInt();
+                    return ret;
+                },
+                checkFloat: function bjs_checkFloat() {
+                    const ret = instance.exports.bjs_checkFloat();
+                    return ret;
+                },
+                checkDouble: function bjs_checkDouble() {
+                    const ret = instance.exports.bjs_checkDouble();
+                    return ret;
+                },
+                checkBool: function bjs_checkBool() {
+                    const ret = instance.exports.bjs_checkBool() !== 0;
+                    return ret;
+                },
+            };
+        },
+    }
+}
\ No newline at end of file
diff --git a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/PrimitiveReturn.Import.d.ts b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/PrimitiveReturn.Import.d.ts
new file mode 100644
index 000000000..ad63bd7d0
--- /dev/null
+++ b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/PrimitiveReturn.Import.d.ts
@@ -0,0 +1,19 @@
+// NOTICE: This is auto-generated code by BridgeJS from JavaScriptKit,
+// DO NOT EDIT.
+//
+// To update this file, just rebuild your project or run
+// `swift package bridge-js`.
+
+export type Exports = {
+}
+export type Imports = {
+    checkNumber(): number;
+    checkBoolean(): boolean;
+}
+export function createInstantiator(options: {
+    imports: Imports;
+}, swift: any): Promise<{
+    addImports: (importObject: WebAssembly.Imports) => void;
+    setInstance: (instance: WebAssembly.Instance) => void;
+    createExports: (instance: WebAssembly.Instance) => Exports;
+}>;
\ No newline at end of file
diff --git a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/PrimitiveReturn.Import.js b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/PrimitiveReturn.Import.js
new file mode 100644
index 000000000..a638f8642
--- /dev/null
+++ b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/PrimitiveReturn.Import.js
@@ -0,0 +1,61 @@
+// NOTICE: This is auto-generated code by BridgeJS from JavaScriptKit,
+// DO NOT EDIT.
+//
+// To update this file, just rebuild your project or run
+// `swift package bridge-js`.
+
+export async function createInstantiator(options, swift) {
+    let instance;
+    let memory;
+    const textDecoder = new TextDecoder("utf-8");
+    const textEncoder = new TextEncoder("utf-8");
+
+    let tmpRetString;
+    let tmpRetBytes;
+    return {
+        /** @param {WebAssembly.Imports} importObject */
+        addImports: (importObject) => {
+            const bjs = {};
+            importObject["bjs"] = bjs;
+            bjs["return_string"] = function(ptr, len) {
+                const bytes = new Uint8Array(memory.buffer, ptr, len);
+                tmpRetString = textDecoder.decode(bytes);
+            }
+            bjs["init_memory"] = function(sourceId, bytesPtr) {
+                const source = swift.memory.getObject(sourceId);
+                const bytes = new Uint8Array(memory.buffer, bytesPtr);
+                bytes.set(source);
+            }
+            bjs["make_jsstring"] = function(ptr, len) {
+                const bytes = new Uint8Array(memory.buffer, ptr, len);
+                return swift.memory.retain(textDecoder.decode(bytes));
+            }
+            bjs["init_memory_with_result"] = function(ptr, len) {
+                const target = new Uint8Array(memory.buffer, ptr, len);
+                target.set(tmpRetBytes);
+                tmpRetBytes = undefined;
+            }
+            const TestModule = importObject["TestModule"] = {};
+            TestModule["bjs_checkNumber"] = function bjs_checkNumber() {
+                let ret = options.imports.checkNumber();
+                return ret;
+            }
+            TestModule["bjs_checkBoolean"] = function bjs_checkBoolean() {
+                let ret = options.imports.checkBoolean();
+                return ret !== 0;
+            }
+        },
+        setInstance: (i) => {
+            instance = i;
+            memory = instance.exports.memory;
+        },
+        /** @param {WebAssembly.Instance} instance */
+        createExports: (instance) => {
+            const js = swift.memory.heap;
+
+            return {
+
+            };
+        },
+    }
+}
\ No newline at end of file
diff --git a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/StringParameter.Export.d.ts b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/StringParameter.Export.d.ts
new file mode 100644
index 000000000..a83fca6f5
--- /dev/null
+++ b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/StringParameter.Export.d.ts
@@ -0,0 +1,18 @@
+// NOTICE: This is auto-generated code by BridgeJS from JavaScriptKit,
+// DO NOT EDIT.
+//
+// To update this file, just rebuild your project or run
+// `swift package bridge-js`.
+
+export type Exports = {
+    checkString(a: string): void;
+}
+export type Imports = {
+}
+export function createInstantiator(options: {
+    imports: Imports;
+}, swift: any): Promise<{
+    addImports: (importObject: WebAssembly.Imports) => void;
+    setInstance: (instance: WebAssembly.Instance) => void;
+    createExports: (instance: WebAssembly.Instance) => Exports;
+}>;
\ No newline at end of file
diff --git a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/StringParameter.Export.js b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/StringParameter.Export.js
new file mode 100644
index 000000000..c13cd3585
--- /dev/null
+++ b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/StringParameter.Export.js
@@ -0,0 +1,58 @@
+// NOTICE: This is auto-generated code by BridgeJS from JavaScriptKit,
+// DO NOT EDIT.
+//
+// To update this file, just rebuild your project or run
+// `swift package bridge-js`.
+
+export async function createInstantiator(options, swift) {
+    let instance;
+    let memory;
+    const textDecoder = new TextDecoder("utf-8");
+    const textEncoder = new TextEncoder("utf-8");
+
+    let tmpRetString;
+    let tmpRetBytes;
+    return {
+        /** @param {WebAssembly.Imports} importObject */
+        addImports: (importObject) => {
+            const bjs = {};
+            importObject["bjs"] = bjs;
+            bjs["return_string"] = function(ptr, len) {
+                const bytes = new Uint8Array(memory.buffer, ptr, len);
+                tmpRetString = textDecoder.decode(bytes);
+            }
+            bjs["init_memory"] = function(sourceId, bytesPtr) {
+                const source = swift.memory.getObject(sourceId);
+                const bytes = new Uint8Array(memory.buffer, bytesPtr);
+                bytes.set(source);
+            }
+            bjs["make_jsstring"] = function(ptr, len) {
+                const bytes = new Uint8Array(memory.buffer, ptr, len);
+                return swift.memory.retain(textDecoder.decode(bytes));
+            }
+            bjs["init_memory_with_result"] = function(ptr, len) {
+                const target = new Uint8Array(memory.buffer, ptr, len);
+                target.set(tmpRetBytes);
+                tmpRetBytes = undefined;
+            }
+
+        },
+        setInstance: (i) => {
+            instance = i;
+            memory = instance.exports.memory;
+        },
+        /** @param {WebAssembly.Instance} instance */
+        createExports: (instance) => {
+            const js = swift.memory.heap;
+
+            return {
+                checkString: function bjs_checkString(a) {
+                    const aBytes = textEncoder.encode(a);
+                    const aId = swift.memory.retain(aBytes);
+                    instance.exports.bjs_checkString(aId, aBytes.length);
+                    swift.memory.release(aId);
+                },
+            };
+        },
+    }
+}
\ No newline at end of file
diff --git a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/StringParameter.Import.d.ts b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/StringParameter.Import.d.ts
new file mode 100644
index 000000000..09fd7b638
--- /dev/null
+++ b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/StringParameter.Import.d.ts
@@ -0,0 +1,19 @@
+// NOTICE: This is auto-generated code by BridgeJS from JavaScriptKit,
+// DO NOT EDIT.
+//
+// To update this file, just rebuild your project or run
+// `swift package bridge-js`.
+
+export type Exports = {
+}
+export type Imports = {
+    checkString(a: string): void;
+    checkStringWithLength(a: string, b: number): void;
+}
+export function createInstantiator(options: {
+    imports: Imports;
+}, swift: any): Promise<{
+    addImports: (importObject: WebAssembly.Imports) => void;
+    setInstance: (instance: WebAssembly.Instance) => void;
+    createExports: (instance: WebAssembly.Instance) => Exports;
+}>;
\ No newline at end of file
diff --git a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/StringParameter.Import.js b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/StringParameter.Import.js
new file mode 100644
index 000000000..6e5d4bdce
--- /dev/null
+++ b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/StringParameter.Import.js
@@ -0,0 +1,63 @@
+// NOTICE: This is auto-generated code by BridgeJS from JavaScriptKit,
+// DO NOT EDIT.
+//
+// To update this file, just rebuild your project or run
+// `swift package bridge-js`.
+
+export async function createInstantiator(options, swift) {
+    let instance;
+    let memory;
+    const textDecoder = new TextDecoder("utf-8");
+    const textEncoder = new TextEncoder("utf-8");
+
+    let tmpRetString;
+    let tmpRetBytes;
+    return {
+        /** @param {WebAssembly.Imports} importObject */
+        addImports: (importObject) => {
+            const bjs = {};
+            importObject["bjs"] = bjs;
+            bjs["return_string"] = function(ptr, len) {
+                const bytes = new Uint8Array(memory.buffer, ptr, len);
+                tmpRetString = textDecoder.decode(bytes);
+            }
+            bjs["init_memory"] = function(sourceId, bytesPtr) {
+                const source = swift.memory.getObject(sourceId);
+                const bytes = new Uint8Array(memory.buffer, bytesPtr);
+                bytes.set(source);
+            }
+            bjs["make_jsstring"] = function(ptr, len) {
+                const bytes = new Uint8Array(memory.buffer, ptr, len);
+                return swift.memory.retain(textDecoder.decode(bytes));
+            }
+            bjs["init_memory_with_result"] = function(ptr, len) {
+                const target = new Uint8Array(memory.buffer, ptr, len);
+                target.set(tmpRetBytes);
+                tmpRetBytes = undefined;
+            }
+            const TestModule = importObject["TestModule"] = {};
+            TestModule["bjs_checkString"] = function bjs_checkString(a) {
+                const aObject = swift.memory.getObject(a);
+                swift.memory.release(a);
+                options.imports.checkString(aObject);
+            }
+            TestModule["bjs_checkStringWithLength"] = function bjs_checkStringWithLength(a, b) {
+                const aObject = swift.memory.getObject(a);
+                swift.memory.release(a);
+                options.imports.checkStringWithLength(aObject, b);
+            }
+        },
+        setInstance: (i) => {
+            instance = i;
+            memory = instance.exports.memory;
+        },
+        /** @param {WebAssembly.Instance} instance */
+        createExports: (instance) => {
+            const js = swift.memory.heap;
+
+            return {
+
+            };
+        },
+    }
+}
\ No newline at end of file
diff --git a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/StringReturn.Export.d.ts b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/StringReturn.Export.d.ts
new file mode 100644
index 000000000..c6a9f65a4
--- /dev/null
+++ b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/StringReturn.Export.d.ts
@@ -0,0 +1,18 @@
+// NOTICE: This is auto-generated code by BridgeJS from JavaScriptKit,
+// DO NOT EDIT.
+//
+// To update this file, just rebuild your project or run
+// `swift package bridge-js`.
+
+export type Exports = {
+    checkString(): string;
+}
+export type Imports = {
+}
+export function createInstantiator(options: {
+    imports: Imports;
+}, swift: any): Promise<{
+    addImports: (importObject: WebAssembly.Imports) => void;
+    setInstance: (instance: WebAssembly.Instance) => void;
+    createExports: (instance: WebAssembly.Instance) => Exports;
+}>;
\ No newline at end of file
diff --git a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/StringReturn.Export.js b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/StringReturn.Export.js
new file mode 100644
index 000000000..0208d8cea
--- /dev/null
+++ b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/StringReturn.Export.js
@@ -0,0 +1,58 @@
+// NOTICE: This is auto-generated code by BridgeJS from JavaScriptKit,
+// DO NOT EDIT.
+//
+// To update this file, just rebuild your project or run
+// `swift package bridge-js`.
+
+export async function createInstantiator(options, swift) {
+    let instance;
+    let memory;
+    const textDecoder = new TextDecoder("utf-8");
+    const textEncoder = new TextEncoder("utf-8");
+
+    let tmpRetString;
+    let tmpRetBytes;
+    return {
+        /** @param {WebAssembly.Imports} importObject */
+        addImports: (importObject) => {
+            const bjs = {};
+            importObject["bjs"] = bjs;
+            bjs["return_string"] = function(ptr, len) {
+                const bytes = new Uint8Array(memory.buffer, ptr, len);
+                tmpRetString = textDecoder.decode(bytes);
+            }
+            bjs["init_memory"] = function(sourceId, bytesPtr) {
+                const source = swift.memory.getObject(sourceId);
+                const bytes = new Uint8Array(memory.buffer, bytesPtr);
+                bytes.set(source);
+            }
+            bjs["make_jsstring"] = function(ptr, len) {
+                const bytes = new Uint8Array(memory.buffer, ptr, len);
+                return swift.memory.retain(textDecoder.decode(bytes));
+            }
+            bjs["init_memory_with_result"] = function(ptr, len) {
+                const target = new Uint8Array(memory.buffer, ptr, len);
+                target.set(tmpRetBytes);
+                tmpRetBytes = undefined;
+            }
+
+        },
+        setInstance: (i) => {
+            instance = i;
+            memory = instance.exports.memory;
+        },
+        /** @param {WebAssembly.Instance} instance */
+        createExports: (instance) => {
+            const js = swift.memory.heap;
+
+            return {
+                checkString: function bjs_checkString() {
+                    instance.exports.bjs_checkString();
+                    const ret = tmpRetString;
+                    tmpRetString = undefined;
+                    return ret;
+                },
+            };
+        },
+    }
+}
\ No newline at end of file
diff --git a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/StringReturn.Import.d.ts b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/StringReturn.Import.d.ts
new file mode 100644
index 000000000..cb7783667
--- /dev/null
+++ b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/StringReturn.Import.d.ts
@@ -0,0 +1,18 @@
+// NOTICE: This is auto-generated code by BridgeJS from JavaScriptKit,
+// DO NOT EDIT.
+//
+// To update this file, just rebuild your project or run
+// `swift package bridge-js`.
+
+export type Exports = {
+}
+export type Imports = {
+    checkString(): string;
+}
+export function createInstantiator(options: {
+    imports: Imports;
+}, swift: any): Promise<{
+    addImports: (importObject: WebAssembly.Imports) => void;
+    setInstance: (instance: WebAssembly.Instance) => void;
+    createExports: (instance: WebAssembly.Instance) => Exports;
+}>;
\ No newline at end of file
diff --git a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/StringReturn.Import.js b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/StringReturn.Import.js
new file mode 100644
index 000000000..26e57959a
--- /dev/null
+++ b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/StringReturn.Import.js
@@ -0,0 +1,58 @@
+// NOTICE: This is auto-generated code by BridgeJS from JavaScriptKit,
+// DO NOT EDIT.
+//
+// To update this file, just rebuild your project or run
+// `swift package bridge-js`.
+
+export async function createInstantiator(options, swift) {
+    let instance;
+    let memory;
+    const textDecoder = new TextDecoder("utf-8");
+    const textEncoder = new TextEncoder("utf-8");
+
+    let tmpRetString;
+    let tmpRetBytes;
+    return {
+        /** @param {WebAssembly.Imports} importObject */
+        addImports: (importObject) => {
+            const bjs = {};
+            importObject["bjs"] = bjs;
+            bjs["return_string"] = function(ptr, len) {
+                const bytes = new Uint8Array(memory.buffer, ptr, len);
+                tmpRetString = textDecoder.decode(bytes);
+            }
+            bjs["init_memory"] = function(sourceId, bytesPtr) {
+                const source = swift.memory.getObject(sourceId);
+                const bytes = new Uint8Array(memory.buffer, bytesPtr);
+                bytes.set(source);
+            }
+            bjs["make_jsstring"] = function(ptr, len) {
+                const bytes = new Uint8Array(memory.buffer, ptr, len);
+                return swift.memory.retain(textDecoder.decode(bytes));
+            }
+            bjs["init_memory_with_result"] = function(ptr, len) {
+                const target = new Uint8Array(memory.buffer, ptr, len);
+                target.set(tmpRetBytes);
+                tmpRetBytes = undefined;
+            }
+            const TestModule = importObject["TestModule"] = {};
+            TestModule["bjs_checkString"] = function bjs_checkString() {
+                let ret = options.imports.checkString();
+                tmpRetBytes = textEncoder.encode(ret);
+                return tmpRetBytes.length;
+            }
+        },
+        setInstance: (i) => {
+            instance = i;
+            memory = instance.exports.memory;
+        },
+        /** @param {WebAssembly.Instance} instance */
+        createExports: (instance) => {
+            const js = swift.memory.heap;
+
+            return {
+
+            };
+        },
+    }
+}
\ No newline at end of file
diff --git a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/SwiftClass.Export.d.ts b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/SwiftClass.Export.d.ts
new file mode 100644
index 000000000..fd376d57b
--- /dev/null
+++ b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/SwiftClass.Export.d.ts
@@ -0,0 +1,32 @@
+// NOTICE: This is auto-generated code by BridgeJS from JavaScriptKit,
+// DO NOT EDIT.
+//
+// To update this file, just rebuild your project or run
+// `swift package bridge-js`.
+
+/// Represents a Swift heap object like a class instance or an actor instance.
+export interface SwiftHeapObject {
+    /// Release the heap object.
+    ///
+    /// Note: Calling this method will release the heap object and it will no longer be accessible.
+    release(): void;
+}
+export interface Greeter extends SwiftHeapObject {
+    greet(): string;
+    changeName(name: string): void;
+}
+export type Exports = {
+    Greeter: {
+        new(name: string): Greeter;
+    }
+    takeGreeter(greeter: Greeter): void;
+}
+export type Imports = {
+}
+export function createInstantiator(options: {
+    imports: Imports;
+}, swift: any): Promise<{
+    addImports: (importObject: WebAssembly.Imports) => void;
+    setInstance: (instance: WebAssembly.Instance) => void;
+    createExports: (instance: WebAssembly.Instance) => Exports;
+}>;
\ No newline at end of file
diff --git a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/SwiftClass.Export.js b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/SwiftClass.Export.js
new file mode 100644
index 000000000..971b9d69d
--- /dev/null
+++ b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/SwiftClass.Export.js
@@ -0,0 +1,92 @@
+// NOTICE: This is auto-generated code by BridgeJS from JavaScriptKit,
+// DO NOT EDIT.
+//
+// To update this file, just rebuild your project or run
+// `swift package bridge-js`.
+
+export async function createInstantiator(options, swift) {
+    let instance;
+    let memory;
+    const textDecoder = new TextDecoder("utf-8");
+    const textEncoder = new TextEncoder("utf-8");
+
+    let tmpRetString;
+    let tmpRetBytes;
+    return {
+        /** @param {WebAssembly.Imports} importObject */
+        addImports: (importObject) => {
+            const bjs = {};
+            importObject["bjs"] = bjs;
+            bjs["return_string"] = function(ptr, len) {
+                const bytes = new Uint8Array(memory.buffer, ptr, len);
+                tmpRetString = textDecoder.decode(bytes);
+            }
+            bjs["init_memory"] = function(sourceId, bytesPtr) {
+                const source = swift.memory.getObject(sourceId);
+                const bytes = new Uint8Array(memory.buffer, bytesPtr);
+                bytes.set(source);
+            }
+            bjs["make_jsstring"] = function(ptr, len) {
+                const bytes = new Uint8Array(memory.buffer, ptr, len);
+                return swift.memory.retain(textDecoder.decode(bytes));
+            }
+            bjs["init_memory_with_result"] = function(ptr, len) {
+                const target = new Uint8Array(memory.buffer, ptr, len);
+                target.set(tmpRetBytes);
+                tmpRetBytes = undefined;
+            }
+
+        },
+        setInstance: (i) => {
+            instance = i;
+            memory = instance.exports.memory;
+        },
+        /** @param {WebAssembly.Instance} instance */
+        createExports: (instance) => {
+            const js = swift.memory.heap;
+            /// Represents a Swift heap object like a class instance or an actor instance.
+            class SwiftHeapObject {
+                constructor(pointer, deinit) {
+                    this.pointer = pointer;
+                    this.hasReleased = false;
+                    this.deinit = deinit;
+                    this.registry = new FinalizationRegistry((pointer) => {
+                        deinit(pointer);
+                    });
+                    this.registry.register(this, this.pointer);
+                }
+            
+                release() {
+                    this.registry.unregister(this);
+                    this.deinit(this.pointer);
+                }
+            }
+            class Greeter extends SwiftHeapObject {
+                constructor(name) {
+                    const nameBytes = textEncoder.encode(name);
+                    const nameId = swift.memory.retain(nameBytes);
+                    super(instance.exports.bjs_Greeter_init(nameId, nameBytes.length), instance.exports.bjs_Greeter_deinit);
+                    swift.memory.release(nameId);
+                }
+                greet() {
+                    instance.exports.bjs_Greeter_greet(this.pointer);
+                    const ret = tmpRetString;
+                    tmpRetString = undefined;
+                    return ret;
+                }
+                changeName(name) {
+                    const nameBytes = textEncoder.encode(name);
+                    const nameId = swift.memory.retain(nameBytes);
+                    instance.exports.bjs_Greeter_changeName(this.pointer, nameId, nameBytes.length);
+                    swift.memory.release(nameId);
+                }
+            }
+            return {
+                Greeter,
+                takeGreeter: function bjs_takeGreeter(greeter) {
+                    instance.exports.bjs_takeGreeter(greeter.pointer);
+                },
+            };
+        },
+    }
+}
\ No newline at end of file
diff --git a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/TypeAlias.Import.d.ts b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/TypeAlias.Import.d.ts
new file mode 100644
index 000000000..da5dfb076
--- /dev/null
+++ b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/TypeAlias.Import.d.ts
@@ -0,0 +1,18 @@
+// NOTICE: This is auto-generated code by BridgeJS from JavaScriptKit,
+// DO NOT EDIT.
+//
+// To update this file, just rebuild your project or run
+// `swift package bridge-js`.
+
+export type Exports = {
+}
+export type Imports = {
+    checkSimple(a: number): void;
+}
+export function createInstantiator(options: {
+    imports: Imports;
+}, swift: any): Promise<{
+    addImports: (importObject: WebAssembly.Imports) => void;
+    setInstance: (instance: WebAssembly.Instance) => void;
+    createExports: (instance: WebAssembly.Instance) => Exports;
+}>;
\ No newline at end of file
diff --git a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/TypeAlias.Import.js b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/TypeAlias.Import.js
new file mode 100644
index 000000000..e5909f6cb
--- /dev/null
+++ b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/TypeAlias.Import.js
@@ -0,0 +1,56 @@
+// NOTICE: This is auto-generated code by BridgeJS from JavaScriptKit,
+// DO NOT EDIT.
+//
+// To update this file, just rebuild your project or run
+// `swift package bridge-js`.
+
+export async function createInstantiator(options, swift) {
+    let instance;
+    let memory;
+    const textDecoder = new TextDecoder("utf-8");
+    const textEncoder = new TextEncoder("utf-8");
+
+    let tmpRetString;
+    let tmpRetBytes;
+    return {
+        /** @param {WebAssembly.Imports} importObject */
+        addImports: (importObject) => {
+            const bjs = {};
+            importObject["bjs"] = bjs;
+            bjs["return_string"] = function(ptr, len) {
+                const bytes = new Uint8Array(memory.buffer, ptr, len);
+                tmpRetString = textDecoder.decode(bytes);
+            }
+            bjs["init_memory"] = function(sourceId, bytesPtr) {
+                const source = swift.memory.getObject(sourceId);
+                const bytes = new Uint8Array(memory.buffer, bytesPtr);
+                bytes.set(source);
+            }
+            bjs["make_jsstring"] = function(ptr, len) {
+                const bytes = new Uint8Array(memory.buffer, ptr, len);
+                return swift.memory.retain(textDecoder.decode(bytes));
+            }
+            bjs["init_memory_with_result"] = function(ptr, len) {
+                const target = new Uint8Array(memory.buffer, ptr, len);
+                target.set(tmpRetBytes);
+                tmpRetBytes = undefined;
+            }
+            const TestModule = importObject["TestModule"] = {};
+            TestModule["bjs_checkSimple"] = function bjs_checkSimple(a) {
+                options.imports.checkSimple(a);
+            }
+        },
+        setInstance: (i) => {
+            instance = i;
+            memory = instance.exports.memory;
+        },
+        /** @param {WebAssembly.Instance} instance */
+        createExports: (instance) => {
+            const js = swift.memory.heap;
+
+            return {
+
+            };
+        },
+    }
+}
\ No newline at end of file
diff --git a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/TypeScriptClass.Import.d.ts b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/TypeScriptClass.Import.d.ts
new file mode 100644
index 000000000..818d57a9d
--- /dev/null
+++ b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/TypeScriptClass.Import.d.ts
@@ -0,0 +1,17 @@
+// NOTICE: This is auto-generated code by BridgeJS from JavaScriptKit,
+// DO NOT EDIT.
+//
+// To update this file, just rebuild your project or run
+// `swift package bridge-js`.
+
+export type Exports = {
+}
+export type Imports = {
+}
+export function createInstantiator(options: {
+    imports: Imports;
+}, swift: any): Promise<{
+    addImports: (importObject: WebAssembly.Imports) => void;
+    setInstance: (instance: WebAssembly.Instance) => void;
+    createExports: (instance: WebAssembly.Instance) => Exports;
+}>;
\ No newline at end of file
diff --git a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/TypeScriptClass.Import.js b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/TypeScriptClass.Import.js
new file mode 100644
index 000000000..c7ae6a228
--- /dev/null
+++ b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/TypeScriptClass.Import.js
@@ -0,0 +1,63 @@
+// NOTICE: This is auto-generated code by BridgeJS from JavaScriptKit,
+// DO NOT EDIT.
+//
+// To update this file, just rebuild your project or run
+// `swift package bridge-js`.
+
+export async function createInstantiator(options, swift) {
+    let instance;
+    let memory;
+    const textDecoder = new TextDecoder("utf-8");
+    const textEncoder = new TextEncoder("utf-8");
+
+    let tmpRetString;
+    let tmpRetBytes;
+    return {
+        /** @param {WebAssembly.Imports} importObject */
+        addImports: (importObject) => {
+            const bjs = {};
+            importObject["bjs"] = bjs;
+            bjs["return_string"] = function(ptr, len) {
+                const bytes = new Uint8Array(memory.buffer, ptr, len);
+                tmpRetString = textDecoder.decode(bytes);
+            }
+            bjs["init_memory"] = function(sourceId, bytesPtr) {
+                const source = swift.memory.getObject(sourceId);
+                const bytes = new Uint8Array(memory.buffer, bytesPtr);
+                bytes.set(source);
+            }
+            bjs["make_jsstring"] = function(ptr, len) {
+                const bytes = new Uint8Array(memory.buffer, ptr, len);
+                return swift.memory.retain(textDecoder.decode(bytes));
+            }
+            bjs["init_memory_with_result"] = function(ptr, len) {
+                const target = new Uint8Array(memory.buffer, ptr, len);
+                target.set(tmpRetBytes);
+                tmpRetBytes = undefined;
+            }
+            const TestModule = importObject["TestModule"] = {};
+            TestModule["bjs_Greeter_greet"] = function bjs_Greeter_greet(self) {
+                let ret = swift.memory.getObject(self).greet();
+                tmpRetBytes = textEncoder.encode(ret);
+                return tmpRetBytes.length;
+            }
+            TestModule["bjs_Greeter_changeName"] = function bjs_Greeter_changeName(self, name) {
+                const nameObject = swift.memory.getObject(name);
+                swift.memory.release(name);
+                swift.memory.getObject(self).changeName(nameObject);
+            }
+        },
+        setInstance: (i) => {
+            instance = i;
+            memory = instance.exports.memory;
+        },
+        /** @param {WebAssembly.Instance} instance */
+        createExports: (instance) => {
+            const js = swift.memory.heap;
+
+            return {
+
+            };
+        },
+    }
+}
\ No newline at end of file
diff --git a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/VoidParameterVoidReturn.Export.d.ts b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/VoidParameterVoidReturn.Export.d.ts
new file mode 100644
index 000000000..be85a00fd
--- /dev/null
+++ b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/VoidParameterVoidReturn.Export.d.ts
@@ -0,0 +1,18 @@
+// NOTICE: This is auto-generated code by BridgeJS from JavaScriptKit,
+// DO NOT EDIT.
+//
+// To update this file, just rebuild your project or run
+// `swift package bridge-js`.
+
+export type Exports = {
+    check(): void;
+}
+export type Imports = {
+}
+export function createInstantiator(options: {
+    imports: Imports;
+}, swift: any): Promise<{
+    addImports: (importObject: WebAssembly.Imports) => void;
+    setInstance: (instance: WebAssembly.Instance) => void;
+    createExports: (instance: WebAssembly.Instance) => Exports;
+}>;
\ No newline at end of file
diff --git a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/VoidParameterVoidReturn.Export.js b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/VoidParameterVoidReturn.Export.js
new file mode 100644
index 000000000..a3dae190f
--- /dev/null
+++ b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/VoidParameterVoidReturn.Export.js
@@ -0,0 +1,55 @@
+// NOTICE: This is auto-generated code by BridgeJS from JavaScriptKit,
+// DO NOT EDIT.
+//
+// To update this file, just rebuild your project or run
+// `swift package bridge-js`.
+
+export async function createInstantiator(options, swift) {
+    let instance;
+    let memory;
+    const textDecoder = new TextDecoder("utf-8");
+    const textEncoder = new TextEncoder("utf-8");
+
+    let tmpRetString;
+    let tmpRetBytes;
+    return {
+        /** @param {WebAssembly.Imports} importObject */
+        addImports: (importObject) => {
+            const bjs = {};
+            importObject["bjs"] = bjs;
+            bjs["return_string"] = function(ptr, len) {
+                const bytes = new Uint8Array(memory.buffer, ptr, len);
+                tmpRetString = textDecoder.decode(bytes);
+            }
+            bjs["init_memory"] = function(sourceId, bytesPtr) {
+                const source = swift.memory.getObject(sourceId);
+                const bytes = new Uint8Array(memory.buffer, bytesPtr);
+                bytes.set(source);
+            }
+            bjs["make_jsstring"] = function(ptr, len) {
+                const bytes = new Uint8Array(memory.buffer, ptr, len);
+                return swift.memory.retain(textDecoder.decode(bytes));
+            }
+            bjs["init_memory_with_result"] = function(ptr, len) {
+                const target = new Uint8Array(memory.buffer, ptr, len);
+                target.set(tmpRetBytes);
+                tmpRetBytes = undefined;
+            }
+
+        },
+        setInstance: (i) => {
+            instance = i;
+            memory = instance.exports.memory;
+        },
+        /** @param {WebAssembly.Instance} instance */
+        createExports: (instance) => {
+            const js = swift.memory.heap;
+
+            return {
+                check: function bjs_check() {
+                    instance.exports.bjs_check();
+                },
+            };
+        },
+    }
+}
\ No newline at end of file
diff --git a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/VoidParameterVoidReturn.Import.d.ts b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/VoidParameterVoidReturn.Import.d.ts
new file mode 100644
index 000000000..8cd1e806e
--- /dev/null
+++ b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/VoidParameterVoidReturn.Import.d.ts
@@ -0,0 +1,18 @@
+// NOTICE: This is auto-generated code by BridgeJS from JavaScriptKit,
+// DO NOT EDIT.
+//
+// To update this file, just rebuild your project or run
+// `swift package bridge-js`.
+
+export type Exports = {
+}
+export type Imports = {
+    check(): void;
+}
+export function createInstantiator(options: {
+    imports: Imports;
+}, swift: any): Promise<{
+    addImports: (importObject: WebAssembly.Imports) => void;
+    setInstance: (instance: WebAssembly.Instance) => void;
+    createExports: (instance: WebAssembly.Instance) => Exports;
+}>;
\ No newline at end of file
diff --git a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/VoidParameterVoidReturn.Import.js b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/VoidParameterVoidReturn.Import.js
new file mode 100644
index 000000000..db9312aa6
--- /dev/null
+++ b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/VoidParameterVoidReturn.Import.js
@@ -0,0 +1,56 @@
+// NOTICE: This is auto-generated code by BridgeJS from JavaScriptKit,
+// DO NOT EDIT.
+//
+// To update this file, just rebuild your project or run
+// `swift package bridge-js`.
+
+export async function createInstantiator(options, swift) {
+    let instance;
+    let memory;
+    const textDecoder = new TextDecoder("utf-8");
+    const textEncoder = new TextEncoder("utf-8");
+
+    let tmpRetString;
+    let tmpRetBytes;
+    return {
+        /** @param {WebAssembly.Imports} importObject */
+        addImports: (importObject) => {
+            const bjs = {};
+            importObject["bjs"] = bjs;
+            bjs["return_string"] = function(ptr, len) {
+                const bytes = new Uint8Array(memory.buffer, ptr, len);
+                tmpRetString = textDecoder.decode(bytes);
+            }
+            bjs["init_memory"] = function(sourceId, bytesPtr) {
+                const source = swift.memory.getObject(sourceId);
+                const bytes = new Uint8Array(memory.buffer, bytesPtr);
+                bytes.set(source);
+            }
+            bjs["make_jsstring"] = function(ptr, len) {
+                const bytes = new Uint8Array(memory.buffer, ptr, len);
+                return swift.memory.retain(textDecoder.decode(bytes));
+            }
+            bjs["init_memory_with_result"] = function(ptr, len) {
+                const target = new Uint8Array(memory.buffer, ptr, len);
+                target.set(tmpRetBytes);
+                tmpRetBytes = undefined;
+            }
+            const TestModule = importObject["TestModule"] = {};
+            TestModule["bjs_check"] = function bjs_check() {
+                options.imports.check();
+            }
+        },
+        setInstance: (i) => {
+            instance = i;
+            memory = instance.exports.memory;
+        },
+        /** @param {WebAssembly.Instance} instance */
+        createExports: (instance) => {
+            const js = swift.memory.heap;
+
+            return {
+
+            };
+        },
+    }
+}
\ No newline at end of file
diff --git a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/ExportSwiftTests/PrimitiveParameters.json b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/ExportSwiftTests/PrimitiveParameters.json
new file mode 100644
index 000000000..4b2dafa1b
--- /dev/null
+++ b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/ExportSwiftTests/PrimitiveParameters.json
@@ -0,0 +1,54 @@
+{
+  "classes" : [
+
+  ],
+  "functions" : [
+    {
+      "abiName" : "bjs_check",
+      "name" : "check",
+      "parameters" : [
+        {
+          "label" : "a",
+          "name" : "a",
+          "type" : {
+            "int" : {
+
+            }
+          }
+        },
+        {
+          "label" : "b",
+          "name" : "b",
+          "type" : {
+            "float" : {
+
+            }
+          }
+        },
+        {
+          "label" : "c",
+          "name" : "c",
+          "type" : {
+            "double" : {
+
+            }
+          }
+        },
+        {
+          "label" : "d",
+          "name" : "d",
+          "type" : {
+            "bool" : {
+
+            }
+          }
+        }
+      ],
+      "returnType" : {
+        "void" : {
+
+        }
+      }
+    }
+  ]
+}
\ No newline at end of file
diff --git a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/ExportSwiftTests/PrimitiveParameters.swift b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/ExportSwiftTests/PrimitiveParameters.swift
new file mode 100644
index 000000000..6df14156d
--- /dev/null
+++ b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/ExportSwiftTests/PrimitiveParameters.swift
@@ -0,0 +1,15 @@
+// NOTICE: This is auto-generated code by BridgeJS from JavaScriptKit,
+// DO NOT EDIT.
+//
+// To update this file, just rebuild your project or run
+// `swift package bridge-js`.
+@_extern(wasm, module: "bjs", name: "return_string")
+private func _return_string(_ ptr: UnsafePointer?, _ len: Int32)
+@_extern(wasm, module: "bjs", name: "init_memory")
+private func _init_memory(_ sourceId: Int32, _ ptr: UnsafeMutablePointer?)
+
+@_expose(wasm, "bjs_check")
+@_cdecl("bjs_check")
+public func _bjs_check(a: Int32, b: Float32, c: Float64, d: Int32) -> Void {
+    check(a: Int(a), b: b, c: c, d: d == 1)
+}
\ No newline at end of file
diff --git a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/ExportSwiftTests/PrimitiveReturn.json b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/ExportSwiftTests/PrimitiveReturn.json
new file mode 100644
index 000000000..ae672cb5e
--- /dev/null
+++ b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/ExportSwiftTests/PrimitiveReturn.json
@@ -0,0 +1,55 @@
+{
+  "classes" : [
+
+  ],
+  "functions" : [
+    {
+      "abiName" : "bjs_checkInt",
+      "name" : "checkInt",
+      "parameters" : [
+
+      ],
+      "returnType" : {
+        "int" : {
+
+        }
+      }
+    },
+    {
+      "abiName" : "bjs_checkFloat",
+      "name" : "checkFloat",
+      "parameters" : [
+
+      ],
+      "returnType" : {
+        "float" : {
+
+        }
+      }
+    },
+    {
+      "abiName" : "bjs_checkDouble",
+      "name" : "checkDouble",
+      "parameters" : [
+
+      ],
+      "returnType" : {
+        "double" : {
+
+        }
+      }
+    },
+    {
+      "abiName" : "bjs_checkBool",
+      "name" : "checkBool",
+      "parameters" : [
+
+      ],
+      "returnType" : {
+        "bool" : {
+
+        }
+      }
+    }
+  ]
+}
\ No newline at end of file
diff --git a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/ExportSwiftTests/PrimitiveReturn.swift b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/ExportSwiftTests/PrimitiveReturn.swift
new file mode 100644
index 000000000..a24b2b312
--- /dev/null
+++ b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/ExportSwiftTests/PrimitiveReturn.swift
@@ -0,0 +1,37 @@
+// NOTICE: This is auto-generated code by BridgeJS from JavaScriptKit,
+// DO NOT EDIT.
+//
+// To update this file, just rebuild your project or run
+// `swift package bridge-js`.
+@_extern(wasm, module: "bjs", name: "return_string")
+private func _return_string(_ ptr: UnsafePointer?, _ len: Int32)
+@_extern(wasm, module: "bjs", name: "init_memory")
+private func _init_memory(_ sourceId: Int32, _ ptr: UnsafeMutablePointer?)
+
+@_expose(wasm, "bjs_checkInt")
+@_cdecl("bjs_checkInt")
+public func _bjs_checkInt() -> Int32 {
+    let ret = checkInt()
+    return Int32(ret)
+}
+
+@_expose(wasm, "bjs_checkFloat")
+@_cdecl("bjs_checkFloat")
+public func _bjs_checkFloat() -> Float32 {
+    let ret = checkFloat()
+    return Float32(ret)
+}
+
+@_expose(wasm, "bjs_checkDouble")
+@_cdecl("bjs_checkDouble")
+public func _bjs_checkDouble() -> Float64 {
+    let ret = checkDouble()
+    return Float64(ret)
+}
+
+@_expose(wasm, "bjs_checkBool")
+@_cdecl("bjs_checkBool")
+public func _bjs_checkBool() -> Int32 {
+    let ret = checkBool()
+    return Int32(ret ? 1 : 0)
+}
\ No newline at end of file
diff --git a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/ExportSwiftTests/StringParameter.json b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/ExportSwiftTests/StringParameter.json
new file mode 100644
index 000000000..0fea9735c
--- /dev/null
+++ b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/ExportSwiftTests/StringParameter.json
@@ -0,0 +1,27 @@
+{
+  "classes" : [
+
+  ],
+  "functions" : [
+    {
+      "abiName" : "bjs_checkString",
+      "name" : "checkString",
+      "parameters" : [
+        {
+          "label" : "a",
+          "name" : "a",
+          "type" : {
+            "string" : {
+
+            }
+          }
+        }
+      ],
+      "returnType" : {
+        "void" : {
+
+        }
+      }
+    }
+  ]
+}
\ No newline at end of file
diff --git a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/ExportSwiftTests/StringParameter.swift b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/ExportSwiftTests/StringParameter.swift
new file mode 100644
index 000000000..080f028ef
--- /dev/null
+++ b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/ExportSwiftTests/StringParameter.swift
@@ -0,0 +1,19 @@
+// NOTICE: This is auto-generated code by BridgeJS from JavaScriptKit,
+// DO NOT EDIT.
+//
+// To update this file, just rebuild your project or run
+// `swift package bridge-js`.
+@_extern(wasm, module: "bjs", name: "return_string")
+private func _return_string(_ ptr: UnsafePointer?, _ len: Int32)
+@_extern(wasm, module: "bjs", name: "init_memory")
+private func _init_memory(_ sourceId: Int32, _ ptr: UnsafeMutablePointer?)
+
+@_expose(wasm, "bjs_checkString")
+@_cdecl("bjs_checkString")
+public func _bjs_checkString(aBytes: Int32, aLen: Int32) -> Void {
+    let a = String(unsafeUninitializedCapacity: Int(aLen)) { b in
+        _init_memory(aBytes, b.baseAddress.unsafelyUnwrapped)
+        return Int(aLen)
+    }
+    checkString(a: a)
+}
\ No newline at end of file
diff --git a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/ExportSwiftTests/StringReturn.json b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/ExportSwiftTests/StringReturn.json
new file mode 100644
index 000000000..c773d0d28
--- /dev/null
+++ b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/ExportSwiftTests/StringReturn.json
@@ -0,0 +1,19 @@
+{
+  "classes" : [
+
+  ],
+  "functions" : [
+    {
+      "abiName" : "bjs_checkString",
+      "name" : "checkString",
+      "parameters" : [
+
+      ],
+      "returnType" : {
+        "string" : {
+
+        }
+      }
+    }
+  ]
+}
\ No newline at end of file
diff --git a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/ExportSwiftTests/StringReturn.swift b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/ExportSwiftTests/StringReturn.swift
new file mode 100644
index 000000000..bf0be042c
--- /dev/null
+++ b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/ExportSwiftTests/StringReturn.swift
@@ -0,0 +1,18 @@
+// NOTICE: This is auto-generated code by BridgeJS from JavaScriptKit,
+// DO NOT EDIT.
+//
+// To update this file, just rebuild your project or run
+// `swift package bridge-js`.
+@_extern(wasm, module: "bjs", name: "return_string")
+private func _return_string(_ ptr: UnsafePointer?, _ len: Int32)
+@_extern(wasm, module: "bjs", name: "init_memory")
+private func _init_memory(_ sourceId: Int32, _ ptr: UnsafeMutablePointer?)
+
+@_expose(wasm, "bjs_checkString")
+@_cdecl("bjs_checkString")
+public func _bjs_checkString() -> Void {
+    var ret = checkString()
+    return ret.withUTF8 { ptr in
+        _return_string(ptr.baseAddress, Int32(ptr.count))
+    }
+}
\ No newline at end of file
diff --git a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/ExportSwiftTests/SwiftClass.json b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/ExportSwiftTests/SwiftClass.json
new file mode 100644
index 000000000..2aff4c931
--- /dev/null
+++ b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/ExportSwiftTests/SwiftClass.json
@@ -0,0 +1,77 @@
+{
+  "classes" : [
+    {
+      "constructor" : {
+        "abiName" : "bjs_Greeter_init",
+        "parameters" : [
+          {
+            "label" : "name",
+            "name" : "name",
+            "type" : {
+              "string" : {
+
+              }
+            }
+          }
+        ]
+      },
+      "methods" : [
+        {
+          "abiName" : "bjs_Greeter_greet",
+          "name" : "greet",
+          "parameters" : [
+
+          ],
+          "returnType" : {
+            "string" : {
+
+            }
+          }
+        },
+        {
+          "abiName" : "bjs_Greeter_changeName",
+          "name" : "changeName",
+          "parameters" : [
+            {
+              "label" : "name",
+              "name" : "name",
+              "type" : {
+                "string" : {
+
+                }
+              }
+            }
+          ],
+          "returnType" : {
+            "void" : {
+
+            }
+          }
+        }
+      ],
+      "name" : "Greeter"
+    }
+  ],
+  "functions" : [
+    {
+      "abiName" : "bjs_takeGreeter",
+      "name" : "takeGreeter",
+      "parameters" : [
+        {
+          "label" : "greeter",
+          "name" : "greeter",
+          "type" : {
+            "swiftHeapObject" : {
+              "_0" : "Greeter"
+            }
+          }
+        }
+      ],
+      "returnType" : {
+        "void" : {
+
+        }
+      }
+    }
+  ]
+}
\ No newline at end of file
diff --git a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/ExportSwiftTests/SwiftClass.swift b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/ExportSwiftTests/SwiftClass.swift
new file mode 100644
index 000000000..20fd9c94f
--- /dev/null
+++ b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/ExportSwiftTests/SwiftClass.swift
@@ -0,0 +1,51 @@
+// NOTICE: This is auto-generated code by BridgeJS from JavaScriptKit,
+// DO NOT EDIT.
+//
+// To update this file, just rebuild your project or run
+// `swift package bridge-js`.
+@_extern(wasm, module: "bjs", name: "return_string")
+private func _return_string(_ ptr: UnsafePointer?, _ len: Int32)
+@_extern(wasm, module: "bjs", name: "init_memory")
+private func _init_memory(_ sourceId: Int32, _ ptr: UnsafeMutablePointer?)
+
+@_expose(wasm, "bjs_takeGreeter")
+@_cdecl("bjs_takeGreeter")
+public func _bjs_takeGreeter(greeter: UnsafeMutableRawPointer) -> Void {
+    takeGreeter(greeter: Unmanaged.fromOpaque(greeter).takeUnretainedValue())
+}
+
+@_expose(wasm, "bjs_Greeter_init")
+@_cdecl("bjs_Greeter_init")
+public func _bjs_Greeter_init(nameBytes: Int32, nameLen: Int32) -> UnsafeMutableRawPointer {
+    let name = String(unsafeUninitializedCapacity: Int(nameLen)) { b in
+        _init_memory(nameBytes, b.baseAddress.unsafelyUnwrapped)
+        return Int(nameLen)
+    }
+    let ret = Greeter(name: name)
+    return Unmanaged.passRetained(ret).toOpaque()
+}
+
+@_expose(wasm, "bjs_Greeter_greet")
+@_cdecl("bjs_Greeter_greet")
+public func _bjs_Greeter_greet(_self: UnsafeMutableRawPointer) -> Void {
+    var ret = Unmanaged.fromOpaque(_self).takeUnretainedValue().greet()
+    return ret.withUTF8 { ptr in
+        _return_string(ptr.baseAddress, Int32(ptr.count))
+    }
+}
+
+@_expose(wasm, "bjs_Greeter_changeName")
+@_cdecl("bjs_Greeter_changeName")
+public func _bjs_Greeter_changeName(_self: UnsafeMutableRawPointer, nameBytes: Int32, nameLen: Int32) -> Void {
+    let name = String(unsafeUninitializedCapacity: Int(nameLen)) { b in
+        _init_memory(nameBytes, b.baseAddress.unsafelyUnwrapped)
+        return Int(nameLen)
+    }
+    Unmanaged.fromOpaque(_self).takeUnretainedValue().changeName(name: name)
+}
+
+@_expose(wasm, "bjs_Greeter_deinit")
+@_cdecl("bjs_Greeter_deinit")
+public func _bjs_Greeter_deinit(pointer: UnsafeMutableRawPointer) {
+    Unmanaged.fromOpaque(pointer).release()
+}
\ No newline at end of file
diff --git a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/ExportSwiftTests/VoidParameterVoidReturn.json b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/ExportSwiftTests/VoidParameterVoidReturn.json
new file mode 100644
index 000000000..f82cdb829
--- /dev/null
+++ b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/ExportSwiftTests/VoidParameterVoidReturn.json
@@ -0,0 +1,19 @@
+{
+  "classes" : [
+
+  ],
+  "functions" : [
+    {
+      "abiName" : "bjs_check",
+      "name" : "check",
+      "parameters" : [
+
+      ],
+      "returnType" : {
+        "void" : {
+
+        }
+      }
+    }
+  ]
+}
\ No newline at end of file
diff --git a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/ExportSwiftTests/VoidParameterVoidReturn.swift b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/ExportSwiftTests/VoidParameterVoidReturn.swift
new file mode 100644
index 000000000..cf4b76fe9
--- /dev/null
+++ b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/ExportSwiftTests/VoidParameterVoidReturn.swift
@@ -0,0 +1,15 @@
+// NOTICE: This is auto-generated code by BridgeJS from JavaScriptKit,
+// DO NOT EDIT.
+//
+// To update this file, just rebuild your project or run
+// `swift package bridge-js`.
+@_extern(wasm, module: "bjs", name: "return_string")
+private func _return_string(_ ptr: UnsafePointer?, _ len: Int32)
+@_extern(wasm, module: "bjs", name: "init_memory")
+private func _init_memory(_ sourceId: Int32, _ ptr: UnsafeMutablePointer?)
+
+@_expose(wasm, "bjs_check")
+@_cdecl("bjs_check")
+public func _bjs_check() -> Void {
+    check()
+}
\ No newline at end of file
diff --git a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/ImportTSTests/ArrayParameter.swift b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/ImportTSTests/ArrayParameter.swift
new file mode 100644
index 000000000..1773223b7
--- /dev/null
+++ b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/ImportTSTests/ArrayParameter.swift
@@ -0,0 +1,34 @@
+// NOTICE: This is auto-generated code by BridgeJS from JavaScriptKit,
+// DO NOT EDIT.
+//
+// To update this file, just rebuild your project or run
+// `swift package bridge-js`.
+
+@_spi(JSObject_id) import JavaScriptKit
+
+@_extern(wasm, module: "bjs", name: "make_jsstring")
+private func _make_jsstring(_ ptr: UnsafePointer?, _ len: Int32) -> Int32
+
+@_extern(wasm, module: "bjs", name: "init_memory_with_result")
+private func _init_memory_with_result(_ ptr: UnsafePointer?, _ len: Int32)
+
+@_extern(wasm, module: "bjs", name: "free_jsobject")
+private func _free_jsobject(_ ptr: Int32) -> Void
+
+func checkArray(_ a: JSObject) -> Void {
+    @_extern(wasm, module: "Check", name: "bjs_checkArray")
+    func bjs_checkArray(_ a: Int32) -> Void
+    bjs_checkArray(Int32(bitPattern: a.id))
+}
+
+func checkArrayWithLength(_ a: JSObject, _ b: Double) -> Void {
+    @_extern(wasm, module: "Check", name: "bjs_checkArrayWithLength")
+    func bjs_checkArrayWithLength(_ a: Int32, _ b: Float64) -> Void
+    bjs_checkArrayWithLength(Int32(bitPattern: a.id), b)
+}
+
+func checkArray(_ a: JSObject) -> Void {
+    @_extern(wasm, module: "Check", name: "bjs_checkArray")
+    func bjs_checkArray(_ a: Int32) -> Void
+    bjs_checkArray(Int32(bitPattern: a.id))
+}
\ No newline at end of file
diff --git a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/ImportTSTests/Interface.swift b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/ImportTSTests/Interface.swift
new file mode 100644
index 000000000..c565a2f8a
--- /dev/null
+++ b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/ImportTSTests/Interface.swift
@@ -0,0 +1,50 @@
+// NOTICE: This is auto-generated code by BridgeJS from JavaScriptKit,
+// DO NOT EDIT.
+//
+// To update this file, just rebuild your project or run
+// `swift package bridge-js`.
+
+@_spi(JSObject_id) import JavaScriptKit
+
+@_extern(wasm, module: "bjs", name: "make_jsstring")
+private func _make_jsstring(_ ptr: UnsafePointer?, _ len: Int32) -> Int32
+
+@_extern(wasm, module: "bjs", name: "init_memory_with_result")
+private func _init_memory_with_result(_ ptr: UnsafePointer?, _ len: Int32)
+
+@_extern(wasm, module: "bjs", name: "free_jsobject")
+private func _free_jsobject(_ ptr: Int32) -> Void
+
+func returnAnimatable() -> Animatable {
+    @_extern(wasm, module: "Check", name: "bjs_returnAnimatable")
+    func bjs_returnAnimatable() -> Int32
+    let ret = bjs_returnAnimatable()
+    return Animatable(takingThis: ret)
+}
+
+struct Animatable {
+    let this: JSObject
+
+    init(this: JSObject) {
+        self.this = this
+    }
+
+    init(takingThis this: Int32) {
+        self.this = JSObject(id: UInt32(bitPattern: this))
+    }
+
+    func animate(_ keyframes: JSObject, _ options: JSObject) -> JSObject {
+        @_extern(wasm, module: "Check", name: "bjs_Animatable_animate")
+        func bjs_Animatable_animate(_ self: Int32, _ keyframes: Int32, _ options: Int32) -> Int32
+        let ret = bjs_Animatable_animate(Int32(bitPattern: self.this.id), Int32(bitPattern: keyframes.id), Int32(bitPattern: options.id))
+        return JSObject(id: UInt32(bitPattern: ret))
+    }
+
+    func getAnimations(_ options: JSObject) -> JSObject {
+        @_extern(wasm, module: "Check", name: "bjs_Animatable_getAnimations")
+        func bjs_Animatable_getAnimations(_ self: Int32, _ options: Int32) -> Int32
+        let ret = bjs_Animatable_getAnimations(Int32(bitPattern: self.this.id), Int32(bitPattern: options.id))
+        return JSObject(id: UInt32(bitPattern: ret))
+    }
+
+}
\ No newline at end of file
diff --git a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/ImportTSTests/PrimitiveParameters.swift b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/ImportTSTests/PrimitiveParameters.swift
new file mode 100644
index 000000000..4ab7f754d
--- /dev/null
+++ b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/ImportTSTests/PrimitiveParameters.swift
@@ -0,0 +1,22 @@
+// NOTICE: This is auto-generated code by BridgeJS from JavaScriptKit,
+// DO NOT EDIT.
+//
+// To update this file, just rebuild your project or run
+// `swift package bridge-js`.
+
+@_spi(JSObject_id) import JavaScriptKit
+
+@_extern(wasm, module: "bjs", name: "make_jsstring")
+private func _make_jsstring(_ ptr: UnsafePointer?, _ len: Int32) -> Int32
+
+@_extern(wasm, module: "bjs", name: "init_memory_with_result")
+private func _init_memory_with_result(_ ptr: UnsafePointer?, _ len: Int32)
+
+@_extern(wasm, module: "bjs", name: "free_jsobject")
+private func _free_jsobject(_ ptr: Int32) -> Void
+
+func check(_ a: Double, _ b: Bool) -> Void {
+    @_extern(wasm, module: "Check", name: "bjs_check")
+    func bjs_check(_ a: Float64, _ b: Int32) -> Void
+    bjs_check(a, Int32(b ? 1 : 0))
+}
\ No newline at end of file
diff --git a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/ImportTSTests/PrimitiveReturn.swift b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/ImportTSTests/PrimitiveReturn.swift
new file mode 100644
index 000000000..a60c93239
--- /dev/null
+++ b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/ImportTSTests/PrimitiveReturn.swift
@@ -0,0 +1,30 @@
+// NOTICE: This is auto-generated code by BridgeJS from JavaScriptKit,
+// DO NOT EDIT.
+//
+// To update this file, just rebuild your project or run
+// `swift package bridge-js`.
+
+@_spi(JSObject_id) import JavaScriptKit
+
+@_extern(wasm, module: "bjs", name: "make_jsstring")
+private func _make_jsstring(_ ptr: UnsafePointer?, _ len: Int32) -> Int32
+
+@_extern(wasm, module: "bjs", name: "init_memory_with_result")
+private func _init_memory_with_result(_ ptr: UnsafePointer?, _ len: Int32)
+
+@_extern(wasm, module: "bjs", name: "free_jsobject")
+private func _free_jsobject(_ ptr: Int32) -> Void
+
+func checkNumber() -> Double {
+    @_extern(wasm, module: "Check", name: "bjs_checkNumber")
+    func bjs_checkNumber() -> Float64
+    let ret = bjs_checkNumber()
+    return Double(ret)
+}
+
+func checkBoolean() -> Bool {
+    @_extern(wasm, module: "Check", name: "bjs_checkBoolean")
+    func bjs_checkBoolean() -> Int32
+    let ret = bjs_checkBoolean()
+    return ret == 1
+}
\ No newline at end of file
diff --git a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/ImportTSTests/StringParameter.swift b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/ImportTSTests/StringParameter.swift
new file mode 100644
index 000000000..491978bc0
--- /dev/null
+++ b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/ImportTSTests/StringParameter.swift
@@ -0,0 +1,36 @@
+// NOTICE: This is auto-generated code by BridgeJS from JavaScriptKit,
+// DO NOT EDIT.
+//
+// To update this file, just rebuild your project or run
+// `swift package bridge-js`.
+
+@_spi(JSObject_id) import JavaScriptKit
+
+@_extern(wasm, module: "bjs", name: "make_jsstring")
+private func _make_jsstring(_ ptr: UnsafePointer?, _ len: Int32) -> Int32
+
+@_extern(wasm, module: "bjs", name: "init_memory_with_result")
+private func _init_memory_with_result(_ ptr: UnsafePointer?, _ len: Int32)
+
+@_extern(wasm, module: "bjs", name: "free_jsobject")
+private func _free_jsobject(_ ptr: Int32) -> Void
+
+func checkString(_ a: String) -> Void {
+    @_extern(wasm, module: "Check", name: "bjs_checkString")
+    func bjs_checkString(_ a: Int32) -> Void
+    var a = a
+    let aId = a.withUTF8 { b in
+        _make_jsstring(b.baseAddress.unsafelyUnwrapped, Int32(b.count))
+    }
+    bjs_checkString(aId)
+}
+
+func checkStringWithLength(_ a: String, _ b: Double) -> Void {
+    @_extern(wasm, module: "Check", name: "bjs_checkStringWithLength")
+    func bjs_checkStringWithLength(_ a: Int32, _ b: Float64) -> Void
+    var a = a
+    let aId = a.withUTF8 { b in
+        _make_jsstring(b.baseAddress.unsafelyUnwrapped, Int32(b.count))
+    }
+    bjs_checkStringWithLength(aId, b)
+}
\ No newline at end of file
diff --git a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/ImportTSTests/StringReturn.swift b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/ImportTSTests/StringReturn.swift
new file mode 100644
index 000000000..ce32a6433
--- /dev/null
+++ b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/ImportTSTests/StringReturn.swift
@@ -0,0 +1,26 @@
+// NOTICE: This is auto-generated code by BridgeJS from JavaScriptKit,
+// DO NOT EDIT.
+//
+// To update this file, just rebuild your project or run
+// `swift package bridge-js`.
+
+@_spi(JSObject_id) import JavaScriptKit
+
+@_extern(wasm, module: "bjs", name: "make_jsstring")
+private func _make_jsstring(_ ptr: UnsafePointer?, _ len: Int32) -> Int32
+
+@_extern(wasm, module: "bjs", name: "init_memory_with_result")
+private func _init_memory_with_result(_ ptr: UnsafePointer?, _ len: Int32)
+
+@_extern(wasm, module: "bjs", name: "free_jsobject")
+private func _free_jsobject(_ ptr: Int32) -> Void
+
+func checkString() -> String {
+    @_extern(wasm, module: "Check", name: "bjs_checkString")
+    func bjs_checkString() -> Int32
+    let ret = bjs_checkString()
+    return String(unsafeUninitializedCapacity: Int(ret)) { b in
+        _init_memory_with_result(b.baseAddress.unsafelyUnwrapped, Int32(ret))
+        return Int(ret)
+    }
+}
\ No newline at end of file
diff --git a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/ImportTSTests/TypeAlias.swift b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/ImportTSTests/TypeAlias.swift
new file mode 100644
index 000000000..79f29c925
--- /dev/null
+++ b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/ImportTSTests/TypeAlias.swift
@@ -0,0 +1,22 @@
+// NOTICE: This is auto-generated code by BridgeJS from JavaScriptKit,
+// DO NOT EDIT.
+//
+// To update this file, just rebuild your project or run
+// `swift package bridge-js`.
+
+@_spi(JSObject_id) import JavaScriptKit
+
+@_extern(wasm, module: "bjs", name: "make_jsstring")
+private func _make_jsstring(_ ptr: UnsafePointer?, _ len: Int32) -> Int32
+
+@_extern(wasm, module: "bjs", name: "init_memory_with_result")
+private func _init_memory_with_result(_ ptr: UnsafePointer?, _ len: Int32)
+
+@_extern(wasm, module: "bjs", name: "free_jsobject")
+private func _free_jsobject(_ ptr: Int32) -> Void
+
+func checkSimple(_ a: Double) -> Void {
+    @_extern(wasm, module: "Check", name: "bjs_checkSimple")
+    func bjs_checkSimple(_ a: Float64) -> Void
+    bjs_checkSimple(a)
+}
\ No newline at end of file
diff --git a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/ImportTSTests/TypeScriptClass.swift b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/ImportTSTests/TypeScriptClass.swift
new file mode 100644
index 000000000..993a14173
--- /dev/null
+++ b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/ImportTSTests/TypeScriptClass.swift
@@ -0,0 +1,60 @@
+// NOTICE: This is auto-generated code by BridgeJS from JavaScriptKit,
+// DO NOT EDIT.
+//
+// To update this file, just rebuild your project or run
+// `swift package bridge-js`.
+
+@_spi(JSObject_id) import JavaScriptKit
+
+@_extern(wasm, module: "bjs", name: "make_jsstring")
+private func _make_jsstring(_ ptr: UnsafePointer?, _ len: Int32) -> Int32
+
+@_extern(wasm, module: "bjs", name: "init_memory_with_result")
+private func _init_memory_with_result(_ ptr: UnsafePointer?, _ len: Int32)
+
+@_extern(wasm, module: "bjs", name: "free_jsobject")
+private func _free_jsobject(_ ptr: Int32) -> Void
+
+struct Greeter {
+    let this: JSObject
+
+    init(this: JSObject) {
+        self.this = this
+    }
+
+    init(takingThis this: Int32) {
+        self.this = JSObject(id: UInt32(bitPattern: this))
+    }
+
+    init(_ name: String) {
+        @_extern(wasm, module: "Check", name: "bjs_Greeter_init")
+        func bjs_Greeter_init(_ name: Int32) -> Int32
+        var name = name
+        let nameId = name.withUTF8 { b in
+            _make_jsstring(b.baseAddress.unsafelyUnwrapped, Int32(b.count))
+        }
+        let ret = bjs_Greeter_init(nameId)
+        self.this = ret
+    }
+
+    func greet() -> String {
+        @_extern(wasm, module: "Check", name: "bjs_Greeter_greet")
+        func bjs_Greeter_greet(_ self: Int32) -> Int32
+        let ret = bjs_Greeter_greet(Int32(bitPattern: self.this.id))
+        return String(unsafeUninitializedCapacity: Int(ret)) { b in
+            _init_memory_with_result(b.baseAddress.unsafelyUnwrapped, Int32(ret))
+            return Int(ret)
+        }
+    }
+
+    func changeName(_ name: String) -> Void {
+        @_extern(wasm, module: "Check", name: "bjs_Greeter_changeName")
+        func bjs_Greeter_changeName(_ self: Int32, _ name: Int32) -> Void
+        var name = name
+        let nameId = name.withUTF8 { b in
+            _make_jsstring(b.baseAddress.unsafelyUnwrapped, Int32(b.count))
+        }
+        bjs_Greeter_changeName(Int32(bitPattern: self.this.id), nameId)
+    }
+
+}
\ No newline at end of file
diff --git a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/ImportTSTests/VoidParameterVoidReturn.swift b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/ImportTSTests/VoidParameterVoidReturn.swift
new file mode 100644
index 000000000..3f2ecc78c
--- /dev/null
+++ b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/ImportTSTests/VoidParameterVoidReturn.swift
@@ -0,0 +1,22 @@
+// NOTICE: This is auto-generated code by BridgeJS from JavaScriptKit,
+// DO NOT EDIT.
+//
+// To update this file, just rebuild your project or run
+// `swift package bridge-js`.
+
+@_spi(JSObject_id) import JavaScriptKit
+
+@_extern(wasm, module: "bjs", name: "make_jsstring")
+private func _make_jsstring(_ ptr: UnsafePointer?, _ len: Int32) -> Int32
+
+@_extern(wasm, module: "bjs", name: "init_memory_with_result")
+private func _init_memory_with_result(_ ptr: UnsafePointer?, _ len: Int32)
+
+@_extern(wasm, module: "bjs", name: "free_jsobject")
+private func _free_jsobject(_ ptr: Int32) -> Void
+
+func check() -> Void {
+    @_extern(wasm, module: "Check", name: "bjs_check")
+    func bjs_check() -> Void
+    bjs_check()
+}
\ No newline at end of file
diff --git a/Plugins/PackageToJS/Fixtures/ContinuationLeakInTest/SwiftTesting/Package.swift b/Plugins/PackageToJS/Fixtures/ContinuationLeakInTest/SwiftTesting/Package.swift
new file mode 100644
index 000000000..84130401a
--- /dev/null
+++ b/Plugins/PackageToJS/Fixtures/ContinuationLeakInTest/SwiftTesting/Package.swift
@@ -0,0 +1,17 @@
+// swift-tools-version: 6.0
+import PackageDescription
+
+let package = Package(
+    name: "Check",
+    dependencies: [.package(name: "JavaScriptKit", path: "../../../../../")],
+    targets: [
+        .testTarget(
+            name: "CheckTests",
+            dependencies: [
+                "JavaScriptKit",
+                .product(name: "JavaScriptEventLoopTestSupport", package: "JavaScriptKit"),
+            ],
+            path: "Tests"
+        )
+    ]
+)
diff --git a/Plugins/PackageToJS/Fixtures/ContinuationLeakInTest/SwiftTesting/Tests/CheckTests.swift b/Plugins/PackageToJS/Fixtures/ContinuationLeakInTest/SwiftTesting/Tests/CheckTests.swift
new file mode 100644
index 000000000..9ed73b7ce
--- /dev/null
+++ b/Plugins/PackageToJS/Fixtures/ContinuationLeakInTest/SwiftTesting/Tests/CheckTests.swift
@@ -0,0 +1,5 @@
+import Testing
+
+@Test func never() async throws {
+    let _: Void = await withUnsafeContinuation { _ in }
+}
diff --git a/Plugins/PackageToJS/Fixtures/ContinuationLeakInTest/XCTest/Package.swift b/Plugins/PackageToJS/Fixtures/ContinuationLeakInTest/XCTest/Package.swift
new file mode 100644
index 000000000..84130401a
--- /dev/null
+++ b/Plugins/PackageToJS/Fixtures/ContinuationLeakInTest/XCTest/Package.swift
@@ -0,0 +1,17 @@
+// swift-tools-version: 6.0
+import PackageDescription
+
+let package = Package(
+    name: "Check",
+    dependencies: [.package(name: "JavaScriptKit", path: "../../../../../")],
+    targets: [
+        .testTarget(
+            name: "CheckTests",
+            dependencies: [
+                "JavaScriptKit",
+                .product(name: "JavaScriptEventLoopTestSupport", package: "JavaScriptKit"),
+            ],
+            path: "Tests"
+        )
+    ]
+)
diff --git a/Plugins/PackageToJS/Fixtures/ContinuationLeakInTest/XCTest/Tests/CheckTests.swift b/Plugins/PackageToJS/Fixtures/ContinuationLeakInTest/XCTest/Tests/CheckTests.swift
new file mode 100644
index 000000000..324df3701
--- /dev/null
+++ b/Plugins/PackageToJS/Fixtures/ContinuationLeakInTest/XCTest/Tests/CheckTests.swift
@@ -0,0 +1,7 @@
+import XCTest
+
+final class CheckTests: XCTestCase {
+    func testNever() async throws {
+        let _: Void = await withUnsafeContinuation { _ in }
+    }
+}
diff --git a/Plugins/PackageToJS/Sources/BridgeJSLink b/Plugins/PackageToJS/Sources/BridgeJSLink
new file mode 120000
index 000000000..41b4d0a41
--- /dev/null
+++ b/Plugins/PackageToJS/Sources/BridgeJSLink
@@ -0,0 +1 @@
+../../BridgeJS/Sources/BridgeJSLink
\ No newline at end of file
diff --git a/Plugins/PackageToJS/Sources/MiniMake.swift b/Plugins/PackageToJS/Sources/MiniMake.swift
index 7c8a320ae..0004af6c0 100644
--- a/Plugins/PackageToJS/Sources/MiniMake.swift
+++ b/Plugins/PackageToJS/Sources/MiniMake.swift
@@ -107,8 +107,11 @@ struct MiniMake {
 
     /// Adds a task to the build system
     mutating func addTask(
-        inputFiles: [BuildPath] = [], inputTasks: [TaskKey] = [], output: BuildPath,
-        attributes: [TaskAttribute] = [], salt: (any Encodable)? = nil,
+        inputFiles: [BuildPath] = [],
+        inputTasks: [TaskKey] = [],
+        output: BuildPath,
+        attributes: [TaskAttribute] = [],
+        salt: (any Encodable)? = nil,
         build: @escaping (_ task: Task, _ scope: VariableScope) throws -> Void = { _, _ in }
     ) -> TaskKey {
         let taskKey = TaskKey(id: output.description)
@@ -118,12 +121,20 @@ struct MiniMake {
             return try encoder.encode($0)
         }
         let info = TaskInfo(
-            wants: inputTasks, inputs: inputFiles, output: output, attributes: attributes,
+            wants: inputTasks,
+            inputs: inputFiles,
+            output: output,
+            attributes: attributes,
             salt: saltData
         )
         self.tasks[taskKey] = Task(
-            info: info, wants: Set(inputTasks), attributes: Set(attributes),
-            key: taskKey, build: build, isDone: false)
+            info: info,
+            wants: Set(inputTasks),
+            attributes: Set(attributes),
+            key: taskKey,
+            build: build,
+            isDone: false
+        )
         return taskKey
     }
 
@@ -234,7 +245,9 @@ struct MiniMake {
                 // Ignore directory modification times
                 var isDirectory: ObjCBool = false
                 let fileExists = FileManager.default.fileExists(
-                    atPath: inputURL.path, isDirectory: &isDirectory)
+                    atPath: inputURL.path,
+                    isDirectory: &isDirectory
+                )
                 if fileExists && isDirectory.boolValue {
                     return false
                 }
diff --git a/Plugins/PackageToJS/Sources/PackageToJS.swift b/Plugins/PackageToJS/Sources/PackageToJS.swift
index 80ad9b805..2b8b4458a 100644
--- a/Plugins/PackageToJS/Sources/PackageToJS.swift
+++ b/Plugins/PackageToJS/Sources/PackageToJS.swift
@@ -4,6 +4,8 @@ struct PackageToJS {
     struct PackageOptions {
         /// Path to the output directory
         var outputPath: String?
+        /// The build configuration to use (default: debug)
+        var configuration: String?
         /// Name of the package (default: lowercased Package.swift name)
         var packageName: String?
         /// Whether to explain the build plan (default: false)
@@ -73,7 +75,10 @@ struct PackageToJS {
             testLibraryArguments.append("--list-tests")
         }
         if let prelude = testOptions.prelude {
-            let preludeURL = URL(fileURLWithPath: prelude, relativeTo: URL(fileURLWithPath: FileManager.default.currentDirectoryPath))
+            let preludeURL = URL(
+                fileURLWithPath: prelude,
+                relativeTo: URL(fileURLWithPath: FileManager.default.currentDirectoryPath)
+            )
             testJsArguments.append("--prelude")
             testJsArguments.append(preludeURL.path)
         }
@@ -97,9 +102,11 @@ struct PackageToJS {
             extraArguments.append(contentsOf: testOptions.filter)
 
             try PackageToJS.runSingleTestingLibrary(
-                testRunner: testRunner, currentDirectoryURL: currentDirectoryURL,
+                testRunner: testRunner,
+                currentDirectoryURL: currentDirectoryURL,
                 extraArguments: extraArguments,
-                testParser: testOptions.packageOptions.verbose ? nil : FancyTestsParser(write: { print($0, terminator: "") }),
+                testParser: testOptions.packageOptions.verbose
+                    ? nil : FancyTestsParser(write: { print($0, terminator: "") }),
                 testOptions: testOptions
             )
         }
@@ -120,14 +127,17 @@ struct PackageToJS {
             }
 
             try PackageToJS.runSingleTestingLibrary(
-                testRunner: testRunner, currentDirectoryURL: currentDirectoryURL,
+                testRunner: testRunner,
+                currentDirectoryURL: currentDirectoryURL,
                 extraArguments: extraArguments,
                 testOptions: testOptions
             )
         }
 
         if testOptions.packageOptions.enableCodeCoverage {
-            let profrawFiles = [xctestCoverageFile.path, swiftTestingCoverageFile.path].filter { FileManager.default.fileExists(atPath: $0) }
+            let profrawFiles = [xctestCoverageFile.path, swiftTestingCoverageFile.path].filter {
+                FileManager.default.fileExists(atPath: $0)
+            }
             do {
                 try PackageToJS.postProcessCoverageFiles(outputDir: outputDir, profrawFiles: profrawFiles)
             } catch {
@@ -254,7 +264,9 @@ extension PackagingSystem {
     func createDirectory(atPath: String) throws {
         guard !FileManager.default.fileExists(atPath: atPath) else { return }
         try FileManager.default.createDirectory(
-            atPath: atPath, withIntermediateDirectories: true, attributes: nil
+            atPath: atPath,
+            withIntermediateDirectories: true,
+            attributes: nil
         )
     }
 
@@ -264,7 +276,8 @@ extension PackagingSystem {
         }
         try FileManager.default.copyItem(atPath: from, toPath: to)
         try FileManager.default.setAttributes(
-            [.modificationDate: Date()], ofItemAtPath: to
+            [.modificationDate: Date()],
+            ofItemAtPath: to
         )
     }
 
@@ -316,9 +329,9 @@ internal func which(_ executable: String) throws -> URL {
     }
     let pathSeparator: Character
     #if os(Windows)
-        pathSeparator = ";"
+    pathSeparator = ";"
     #else
-        pathSeparator = ":"
+    pathSeparator = ":"
     #endif
     let paths = ProcessInfo.processInfo.environment["PATH"]!.split(separator: pathSeparator)
     for path in paths {
@@ -352,6 +365,10 @@ struct PackagingPlanner {
     let selfPackageDir: BuildPath
     /// The path of this file itself, used to capture changes of planner code
     let selfPath: BuildPath
+    /// The exported API skeletons source files
+    let exportedSkeletons: [BuildPath]
+    /// The imported API skeletons source files
+    let importedSkeletons: [BuildPath]
     /// The directory for the final output
     let outputDir: BuildPath
     /// The directory for intermediate files
@@ -372,6 +389,8 @@ struct PackagingPlanner {
         packageId: String,
         intermediatesDir: BuildPath,
         selfPackageDir: BuildPath,
+        exportedSkeletons: [BuildPath],
+        importedSkeletons: [BuildPath],
         outputDir: BuildPath,
         wasmProductArtifact: BuildPath,
         wasmFilename: String,
@@ -383,6 +402,8 @@ struct PackagingPlanner {
         self.options = options
         self.packageId = packageId
         self.selfPackageDir = selfPackageDir
+        self.exportedSkeletons = exportedSkeletons
+        self.importedSkeletons = importedSkeletons
         self.outputDir = outputDir
         self.intermediatesDir = intermediatesDir
         self.wasmFilename = wasmFilename
@@ -401,10 +422,14 @@ struct PackagingPlanner {
         buildOptions: PackageToJS.BuildOptions
     ) throws -> MiniMake.TaskKey {
         let (allTasks, _, _, _) = try planBuildInternal(
-            make: &make, noOptimize: buildOptions.noOptimize, debugInfoFormat: buildOptions.debugInfoFormat
+            make: &make,
+            noOptimize: buildOptions.noOptimize,
+            debugInfoFormat: buildOptions.debugInfoFormat
         )
         return make.addTask(
-            inputTasks: allTasks, output: BuildPath(phony: "all"), attributes: [.phony, .silent]
+            inputTasks: allTasks,
+            output: BuildPath(phony: "all"),
+            attributes: [.phony, .silent]
         )
     }
 
@@ -420,7 +445,9 @@ struct PackagingPlanner {
     ) {
         // Prepare output directory
         let outputDirTask = make.addTask(
-            inputFiles: [selfPath], output: outputDir, attributes: [.silent]
+            inputFiles: [selfPath],
+            output: outputDir,
+            attributes: [.silent]
         ) {
             try system.createDirectory(atPath: $1.resolve(path: $0.output).path)
         }
@@ -438,7 +465,9 @@ struct PackagingPlanner {
         }
 
         let intermediatesDirTask = make.addTask(
-            inputFiles: [selfPath], output: intermediatesDir, attributes: [.silent]
+            inputFiles: [selfPath],
+            output: intermediatesDir,
+            attributes: [.silent]
         ) {
             try system.createDirectory(atPath: $1.resolve(path: $0.output).path)
         }
@@ -458,35 +487,50 @@ struct PackagingPlanner {
                 wasmOptInputFile = intermediatesDir.appending(path: wasmFilename + ".no-dwarf")
                 // First, strip DWARF sections as their existence enables DWARF preserving mode in wasm-opt
                 wasmOptInputTask = make.addTask(
-                    inputFiles: [selfPath, wasmProductArtifact], inputTasks: [outputDirTask, intermediatesDirTask],
+                    inputFiles: [selfPath, wasmProductArtifact],
+                    inputTasks: [outputDirTask, intermediatesDirTask],
                     output: wasmOptInputFile
                 ) {
                     print("Stripping DWARF debug info...")
-                    try system.wasmOpt(["--strip-dwarf", "--debuginfo"], input: $1.resolve(path: wasmProductArtifact).path, output: $1.resolve(path: $0.output).path)
+                    try system.wasmOpt(
+                        ["--strip-dwarf", "--debuginfo"],
+                        input: $1.resolve(path: wasmProductArtifact).path,
+                        output: $1.resolve(path: $0.output).path
+                    )
                 }
             }
             // Then, run wasm-opt with all optimizations
             wasm = make.addTask(
-                inputFiles: [selfPath, wasmOptInputFile], inputTasks: [outputDirTask] + (wasmOptInputTask.map { [$0] } ?? []),
+                inputFiles: [selfPath, wasmOptInputFile],
+                inputTasks: [outputDirTask] + (wasmOptInputTask.map { [$0] } ?? []),
                 output: finalWasmPath
             ) {
                 print("Optimizing the wasm file...")
-                try system.wasmOpt(["-Os"] + (debugInfoFormat != .none ? ["--debuginfo"] : []), input: $1.resolve(path: wasmOptInputFile).path, output: $1.resolve(path: $0.output).path)
+                try system.wasmOpt(
+                    ["-Os"] + (debugInfoFormat != .none ? ["--debuginfo"] : []),
+                    input: $1.resolve(path: wasmOptInputFile).path,
+                    output: $1.resolve(path: $0.output).path
+                )
             }
         } else {
             // Copy the wasm product artifact
             wasm = make.addTask(
-                inputFiles: [selfPath, wasmProductArtifact], inputTasks: [outputDirTask],
+                inputFiles: [selfPath, wasmProductArtifact],
+                inputTasks: [outputDirTask],
                 output: finalWasmPath
             ) {
-                try system.syncFile(from: $1.resolve(path: wasmProductArtifact).path, to: $1.resolve(path: $0.output).path)
+                try system.syncFile(
+                    from: $1.resolve(path: wasmProductArtifact).path,
+                    to: $1.resolve(path: $0.output).path
+                )
             }
         }
         packageInputs.append(wasm)
 
         let wasmImportsPath = intermediatesDir.appending(path: "wasm-imports.json")
         let wasmImportsTask = make.addTask(
-            inputFiles: [selfPath, finalWasmPath], inputTasks: [outputDirTask, intermediatesDirTask, wasm],
+            inputFiles: [selfPath, finalWasmPath],
+            inputTasks: [outputDirTask, intermediatesDirTask, wasm],
             output: wasmImportsPath
         ) {
             let metadata = try parseImports(
@@ -502,17 +546,52 @@ struct PackagingPlanner {
 
         let platformsDir = outputDir.appending(path: "platforms")
         let platformsDirTask = make.addTask(
-            inputFiles: [selfPath], output: platformsDir, attributes: [.silent]
+            inputFiles: [selfPath],
+            output: platformsDir,
+            attributes: [.silent]
         ) {
             try system.createDirectory(atPath: $1.resolve(path: $0.output).path)
         }
 
         let packageJsonTask = planCopyTemplateFile(
-            make: &make, file: "Plugins/PackageToJS/Templates/package.json", output: "package.json", outputDirTask: outputDirTask,
-            inputFiles: [], inputTasks: []
+            make: &make,
+            file: "Plugins/PackageToJS/Templates/package.json",
+            output: "package.json",
+            outputDirTask: outputDirTask,
+            inputFiles: [],
+            inputTasks: []
         )
         packageInputs.append(packageJsonTask)
 
+        if exportedSkeletons.count > 0 || importedSkeletons.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")
+            let bridgeDts = outputDir.appending(path: "bridge.d.ts")
+            packageInputs.append(
+                make.addTask(inputFiles: exportedSkeletons + importedSkeletons, output: bridgeJs) { _, scope in
+                    let link = try BridgeJSLink(
+                        exportedSkeletons: exportedSkeletons.map {
+                            let decoder = JSONDecoder()
+                            let data = try Data(contentsOf: URL(fileURLWithPath: scope.resolve(path: $0).path))
+                            return try decoder.decode(ExportedSkeleton.self, from: data)
+                        },
+                        importedSkeletons: importedSkeletons.map {
+                            let decoder = JSONDecoder()
+                            let data = try Data(contentsOf: URL(fileURLWithPath: scope.resolve(path: $0).path))
+                            return try decoder.decode(ImportedModuleSkeleton.self, from: data)
+                        }
+                    )
+                    let (outputJs, outputDts) = try link.link()
+                    try system.writeFile(atPath: scope.resolve(path: bridgeJs).path, content: Data(outputJs.utf8))
+                    try system.writeFile(atPath: scope.resolve(path: bridgeDts).path, content: Data(outputDts.utf8))
+                }
+            )
+        }
+
         // Copy the template files
         for (file, output) in [
             ("Plugins/PackageToJS/Templates/index.js", "index.js"),
@@ -525,12 +604,19 @@ struct PackagingPlanner {
             ("Plugins/PackageToJS/Templates/platforms/node.js", "platforms/node.js"),
             ("Plugins/PackageToJS/Templates/platforms/node.d.ts", "platforms/node.d.ts"),
             ("Sources/JavaScriptKit/Runtime/index.mjs", "runtime.js"),
+            ("Sources/JavaScriptKit/Runtime/index.d.ts", "runtime.d.ts"),
         ] {
-            packageInputs.append(planCopyTemplateFile(
-                make: &make, file: file, output: output, outputDirTask: outputDirTask,
-                inputFiles: [wasmImportsPath], inputTasks: [platformsDirTask, wasmImportsTask],
-                wasmImportsPath: wasmImportsPath
-            ))
+            packageInputs.append(
+                planCopyTemplateFile(
+                    make: &make,
+                    file: file,
+                    output: output,
+                    outputDirTask: outputDirTask,
+                    inputFiles: [wasmImportsPath],
+                    inputTasks: [platformsDirTask, wasmImportsTask],
+                    wasmImportsPath: wasmImportsPath
+                )
+            )
         }
         return (packageInputs, outputDirTask, intermediatesDirTask, packageJsonTask)
     }
@@ -540,24 +626,30 @@ struct PackagingPlanner {
         make: inout MiniMake
     ) throws -> (rootTask: MiniMake.TaskKey, binDir: BuildPath) {
         var (allTasks, outputDirTask, intermediatesDirTask, packageJsonTask) = try planBuildInternal(
-            make: &make, noOptimize: false, debugInfoFormat: .dwarf
+            make: &make,
+            noOptimize: false,
+            debugInfoFormat: .dwarf
         )
 
         // Install npm dependencies used in the test harness
-        allTasks.append(make.addTask(
-            inputFiles: [
-                selfPath,
-                outputDir.appending(path: "package.json"),
-            ], inputTasks: [intermediatesDirTask, packageJsonTask],
-            output: intermediatesDir.appending(path: "npm-install.stamp")
-        ) {
-            try system.npmInstall(packageDir: $1.resolve(path: outputDir).path)
-            try system.writeFile(atPath: $1.resolve(path: $0.output).path, content: Data())
-        })
+        allTasks.append(
+            make.addTask(
+                inputFiles: [
+                    selfPath,
+                    outputDir.appending(path: "package.json"),
+                ],
+                inputTasks: [intermediatesDirTask, packageJsonTask],
+                output: intermediatesDir.appending(path: "npm-install.stamp")
+            ) {
+                try system.npmInstall(packageDir: $1.resolve(path: outputDir).path)
+                try system.writeFile(atPath: $1.resolve(path: $0.output).path, content: Data())
+            }
+        )
 
         let binDir = outputDir.appending(path: "bin")
         let binDirTask = make.addTask(
-            inputFiles: [selfPath], inputTasks: [outputDirTask],
+            inputFiles: [selfPath],
+            inputTasks: [outputDirTask],
             output: binDir
         ) {
             try system.createDirectory(atPath: $1.resolve(path: $0.output).path)
@@ -571,13 +663,21 @@ struct PackagingPlanner {
             ("Plugins/PackageToJS/Templates/test.browser.html", "test.browser.html"),
             ("Plugins/PackageToJS/Templates/bin/test.js", "bin/test.js"),
         ] {
-            allTasks.append(planCopyTemplateFile(
-                make: &make, file: file, output: output, outputDirTask: outputDirTask,
-                inputFiles: [], inputTasks: [binDirTask]
-            ))
+            allTasks.append(
+                planCopyTemplateFile(
+                    make: &make,
+                    file: file,
+                    output: output,
+                    outputDirTask: outputDirTask,
+                    inputFiles: [],
+                    inputTasks: [binDirTask]
+                )
+            )
         }
         let rootTask = make.addTask(
-            inputTasks: allTasks, output: BuildPath(phony: "all"), attributes: [.phony, .silent]
+            inputTasks: allTasks,
+            output: BuildPath(phony: "all"),
+            attributes: [.phony, .silent]
         )
         return (rootTask, binDir)
     }
@@ -602,6 +702,8 @@ struct PackagingPlanner {
             "USE_SHARED_MEMORY": triple == "wasm32-unknown-wasip1-threads",
             "IS_WASI": triple.hasPrefix("wasm32-unknown-wasi"),
             "USE_WASI_CDN": options.useCDN,
+            "HAS_BRIDGE": exportedSkeletons.count > 0 || importedSkeletons.count > 0,
+            "HAS_IMPORTS": importedSkeletons.count > 0,
         ]
         let constantSubstitutions: [String: String] = [
             "PACKAGE_TO_JS_MODULE_PATH": wasmFilename,
@@ -610,14 +712,19 @@ struct PackagingPlanner {
         let salt = Salt(conditions: conditions, substitutions: constantSubstitutions)
 
         return make.addTask(
-            inputFiles: [selfPath, inputPath] + inputFiles, inputTasks: [outputDirTask] + inputTasks,
-            output: outputDir.appending(path: output), salt: salt
+            inputFiles: [selfPath, inputPath] + inputFiles,
+            inputTasks: [outputDirTask] + inputTasks,
+            output: outputDir.appending(path: output),
+            salt: salt
         ) {
             var substitutions = constantSubstitutions
 
             if let wasmImportsPath = wasmImportsPath {
                 let wasmImportsPath = $1.resolve(path: wasmImportsPath)
-                let importEntries = try JSONDecoder().decode([ImportEntry].self, from: Data(contentsOf: wasmImportsPath))
+                let importEntries = try JSONDecoder().decode(
+                    [ImportEntry].self,
+                    from: Data(contentsOf: wasmImportsPath)
+                )
                 let memoryImport = importEntries.first {
                     $0.module == "env" && $0.name == "memory"
                 }
diff --git a/Plugins/PackageToJS/Sources/PackageToJSPlugin.swift b/Plugins/PackageToJS/Sources/PackageToJSPlugin.swift
index 62e7dc16e..04f4dcd45 100644
--- a/Plugins/PackageToJS/Sources/PackageToJSPlugin.swift
+++ b/Plugins/PackageToJS/Sources/PackageToJSPlugin.swift
@@ -20,8 +20,8 @@ struct PackageToJSPlugin: CommandPlugin {
                 // In case user misses the `--swift-sdk` option
                 { build, arguments in
                     guard
-                        build.logText.contains("ld.gold: --export-if-defined=__main_argc_argv: unknown option") ||
-                        build.logText.contains("-static-stdlib is no longer supported for Apple platforms")
+                        build.logText.contains("ld.gold: --export-if-defined=__main_argc_argv: unknown option")
+                            || build.logText.contains("-static-stdlib is no longer supported for Apple platforms")
                     else { return nil }
                     let didYouMean =
                         [
@@ -57,7 +57,11 @@ struct PackageToJSPlugin: CommandPlugin {
             (
                 // In case selected toolchain is a Xcode toolchain, not OSS toolchain
                 { build, arguments in
-                    guard build.logText.contains("No available targets are compatible with triple \"wasm32-unknown-wasi\"") else {
+                    guard
+                        build.logText.contains(
+                            "No available targets are compatible with triple \"wasm32-unknown-wasi\""
+                        )
+                    else {
                         return nil
                     }
                     return """
@@ -67,6 +71,27 @@ struct PackageToJSPlugin: CommandPlugin {
                         See https://book.swiftwasm.org/getting-started/setup.html for more information.
                         """
                 }),
+            (
+                // In case the SwiftPM target using BridgeJS didn't specify `.enableExperimentalFeature("Extern")`
+                { build, arguments in
+                    guard
+                        build.logText.contains("@_extern requires '-enable-experimental-feature Extern'")
+                    else {
+                        return nil
+                    }
+                    return """
+                        The SwiftPM target using BridgeJS didn't specify `.enableExperimentalFeature("Extern")`.
+                        Please add it to the target's `swiftSettings` configuration.
+
+                        For example:
+                        ```swift
+                        dependencies: [...],
+                        swiftSettings: [
+                            .enableExperimentalFeature("Extern"),
+                        ]
+                        ```
+                        """
+                }),
         ]
 
     private func emitHintMessage(_ message: String) {
@@ -74,7 +99,8 @@ struct PackageToJSPlugin: CommandPlugin {
     }
 
     private func reportBuildFailure(
-        _ build: PackageManager.BuildResult, _ arguments: [String]
+        _ build: PackageManager.BuildResult,
+        _ arguments: [String]
     ) {
         for diagnostic in Self.friendlyBuildDiagnostics {
             if let message = diagnostic(build, arguments) {
@@ -100,11 +126,12 @@ struct PackageToJSPlugin: CommandPlugin {
             if filePath.hasPrefix(packageDir.path) {
                 // Emit hint for --allow-writing-to-package-directory if the destination path
                 // is under the package directory
-                let didYouMean = [
-                    "swift", "package", "--swift-sdk", "wasm32-unknown-wasi",
-                    "plugin", "--allow-writing-to-package-directory",
-                    "js",
-                ] + arguments
+                let didYouMean =
+                    [
+                        "swift", "package", "--swift-sdk", "wasm32-unknown-wasi",
+                        "plugin", "--allow-writing-to-package-directory",
+                        "js",
+                    ] + arguments
                 emitHintMessage(
                     """
                     Please pass `--allow-writing-to-package-directory` to "swift package".
@@ -116,11 +143,12 @@ struct PackageToJSPlugin: CommandPlugin {
             } else {
                 // Emit hint for --allow-writing-to-directory 
                 // if the destination path is outside the package directory
-                let didYouMean = [
-                    "swift", "package", "--swift-sdk", "wasm32-unknown-wasi",
-                    "plugin", "--allow-writing-to-directory", "\(filePath)",
-                    "js",
-                ] + arguments
+                let didYouMean =
+                    [
+                        "swift", "package", "--swift-sdk", "wasm32-unknown-wasi",
+                        "plugin", "--allow-writing-to-directory", "\(filePath)",
+                        "js",
+                    ] + arguments
                 emitHintMessage(
                     """
                     Please pass `--allow-writing-to-directory ` to "swift package".
@@ -147,21 +175,27 @@ struct PackageToJSPlugin: CommandPlugin {
 
         if extractor.remainingArguments.count > 0 {
             printStderr(
-                "Unexpected arguments: \(extractor.remainingArguments.joined(separator: " "))")
+                "Unexpected arguments: \(extractor.remainingArguments.joined(separator: " "))"
+            )
             printStderr(PackageToJS.BuildOptions.help())
             exit(1)
         }
 
         // Build products
+        let selfPackage = try findSelfPackage(in: context.package)
         let productName = try buildOptions.product ?? deriveDefaultProduct(package: context.package)
         let build = try buildWasm(
-            productName: productName, context: context,
+            productName: productName,
+            selfPackage: selfPackage,
+            context: context,
             options: buildOptions.packageOptions
         )
         guard build.succeeded else {
             reportBuildFailure(build, arguments)
             exit(1)
         }
+        let skeletonCollector = SkeletonCollector(context: context)
+        let (exportedSkeletons, importedSkeletons) = skeletonCollector.collectFromProduct(name: productName)
         let productArtifact = try build.findWasmArtifact(for: productName)
         let outputDir =
             if let outputPath = buildOptions.packageOptions.outputPath {
@@ -169,23 +203,24 @@ struct PackageToJSPlugin: CommandPlugin {
             } else {
                 context.pluginWorkDirectoryURL.appending(path: "Package")
             }
-        guard
-            let selfPackage = findPackageInDependencies(
-                package: context.package, id: Self.JAVASCRIPTKIT_PACKAGE_ID)
-        else {
-            throw PackageToJSError("Failed to find JavaScriptKit in dependencies!?")
-        }
         var make = MiniMake(
             explain: buildOptions.packageOptions.explain,
             printProgress: self.printProgress
         )
         let planner = PackagingPlanner(
-            options: buildOptions.packageOptions, context: context, selfPackage: selfPackage,
-            outputDir: outputDir, wasmProductArtifact: productArtifact,
+            options: buildOptions.packageOptions,
+            context: context,
+            selfPackage: selfPackage,
+            exportedSkeletons: exportedSkeletons,
+            importedSkeletons: importedSkeletons,
+            outputDir: outputDir,
+            wasmProductArtifact: productArtifact,
             wasmFilename: productArtifact.lastPathComponent
         )
         let rootTask = try planner.planBuild(
-            make: &make, buildOptions: buildOptions)
+            make: &make,
+            buildOptions: buildOptions
+        )
         cleanIfBuildGraphChanged(root: rootTask, make: make, context: context)
         print("Packaging...")
         let scope = MiniMake.VariableScope(variables: [:])
@@ -204,14 +239,18 @@ struct PackageToJSPlugin: CommandPlugin {
 
         if extractor.remainingArguments.count > 0 {
             printStderr(
-                "Unexpected arguments: \(extractor.remainingArguments.joined(separator: " "))")
+                "Unexpected arguments: \(extractor.remainingArguments.joined(separator: " "))"
+            )
             printStderr(PackageToJS.TestOptions.help())
             exit(1)
         }
 
+        let selfPackage = try findSelfPackage(in: context.package)
         let productName = "\(context.package.displayName)PackageTests"
         let build = try buildWasm(
-            productName: productName, context: context,
+            productName: productName,
+            selfPackage: selfPackage,
+            context: context,
             options: testOptions.packageOptions
         )
         guard build.succeeded else {
@@ -219,6 +258,9 @@ struct PackageToJSPlugin: CommandPlugin {
             exit(1)
         }
 
+        let skeletonCollector = SkeletonCollector(context: context)
+        let (exportedSkeletons, importedSkeletons) = skeletonCollector.collectFromTests()
+
         // NOTE: Find the product artifact from the default build directory
         //       because PackageManager.BuildResult doesn't include the
         //       product artifact for tests.
@@ -237,7 +279,8 @@ struct PackageToJSPlugin: CommandPlugin {
         }
         guard let productArtifact = productArtifact else {
             throw PackageToJSError(
-                "Failed to find '\(productName).wasm' or '\(productName).xctest'")
+                "Failed to find '\(productName).wasm' or '\(productName).xctest'"
+            )
         }
         let outputDir =
             if let outputPath = testOptions.packageOptions.outputPath {
@@ -245,19 +288,18 @@ struct PackageToJSPlugin: CommandPlugin {
             } else {
                 context.pluginWorkDirectoryURL.appending(path: "PackageTests")
             }
-        guard
-            let selfPackage = findPackageInDependencies(
-                package: context.package, id: Self.JAVASCRIPTKIT_PACKAGE_ID)
-        else {
-            throw PackageToJSError("Failed to find JavaScriptKit in dependencies!?")
-        }
         var make = MiniMake(
             explain: testOptions.packageOptions.explain,
             printProgress: self.printProgress
         )
         let planner = PackagingPlanner(
-            options: testOptions.packageOptions, context: context, selfPackage: selfPackage,
-            outputDir: outputDir, wasmProductArtifact: productArtifact,
+            options: testOptions.packageOptions,
+            context: context,
+            selfPackage: selfPackage,
+            exportedSkeletons: exportedSkeletons,
+            importedSkeletons: importedSkeletons,
+            outputDir: outputDir,
+            wasmProductArtifact: productArtifact,
             // If the product artifact doesn't have a .wasm extension, add it
             // to deliver it with the correct MIME type when serving the test
             // files for browser tests.
@@ -266,7 +308,8 @@ struct PackageToJSPlugin: CommandPlugin {
                 : productArtifact.lastPathComponent + ".wasm"
         )
         let (rootTask, binDir) = try planner.planTestBuild(
-            make: &make)
+            make: &make
+        )
         cleanIfBuildGraphChanged(root: rootTask, make: make, context: context)
         print("Packaging tests...")
         let scope = MiniMake.VariableScope(variables: [:])
@@ -284,19 +327,30 @@ struct PackageToJSPlugin: CommandPlugin {
         }
     }
 
-    private func buildWasm(productName: String, context: PluginContext, options: PackageToJS.PackageOptions) throws
+    private func buildWasm(
+        productName: String,
+        selfPackage: Package,
+        context: PluginContext,
+        options: PackageToJS.PackageOptions
+    ) throws
         -> PackageManager.BuildResult
     {
+        let buildConfiguration: PackageManager.BuildConfiguration
+        if let configuration = options.configuration {
+            guard let _buildConfiguration = PackageManager.BuildConfiguration(rawValue: configuration) else {
+                fatalError("Invalid build configuration: \(configuration)")
+            }
+            buildConfiguration = _buildConfiguration
+        } else {
+            buildConfiguration = .inherit
+        }
         var parameters = PackageManager.BuildParameters(
-            configuration: .inherit,
+            configuration: buildConfiguration,
             logging: options.verbose ? .verbose : .concise
         )
         parameters.echoLogs = true
         parameters.otherSwiftcFlags = ["-color-diagnostics"]
-        let buildingForEmbedded =
-            ProcessInfo.processInfo.environment["JAVASCRIPTKIT_EXPERIMENTAL_EMBEDDED_WASM"].flatMap(
-                Bool.init) ?? false
-        if !buildingForEmbedded {
+        if !isBuildingForEmbedded(selfPackage: selfPackage) {
             // NOTE: We only support static linking for now, and the new SwiftDriver
             // does not infer `-static-stdlib` for WebAssembly targets intentionally
             // for future dynamic linking support.
@@ -316,6 +370,36 @@ struct PackageToJSPlugin: CommandPlugin {
         return try self.packageManager.build(.product(productName), parameters: parameters)
     }
 
+    /// Check if the build is for embedded WebAssembly
+    private func isBuildingForEmbedded(selfPackage: Package) -> Bool {
+        if let rawValue = ProcessInfo.processInfo.environment["JAVASCRIPTKIT_EXPERIMENTAL_EMBEDDED_WASM"],
+            let value = Bool(rawValue), value
+        {
+            return true
+        }
+        let coreTarget = selfPackage.targets.first { $0.name == "JavaScriptKit" }
+        guard let swiftTarget = coreTarget as? SwiftSourceModuleTarget else {
+            return false
+        }
+        // SwiftPM defines "Embedded" compilation condition when `Embedded` experimental
+        // feature is enabled.
+        // TODO: This should be replaced with a proper trait-based solution in the future.
+        return swiftTarget.compilationConditions.contains("Embedded")
+    }
+
+    /// Find JavaScriptKit package in the dependencies of the given package recursively
+    private func findSelfPackage(in package: Package) throws -> Package {
+        guard
+            let selfPackage = findPackageInDependencies(
+                package: package,
+                id: Self.JAVASCRIPTKIT_PACKAGE_ID
+            )
+        else {
+            throw PackageToJSError("Failed to find JavaScriptKit in dependencies!?")
+        }
+        return selfPackage
+    }
+
     /// Clean if the build graph of the packaging process has changed
     ///
     /// This is especially important to detect user changes debug/release
@@ -323,7 +407,8 @@ struct PackageToJSPlugin: CommandPlugin {
     /// path.
     private func cleanIfBuildGraphChanged(
         root: MiniMake.TaskKey,
-        make: MiniMake, context: PluginContext
+        make: MiniMake,
+        context: PluginContext
     ) {
         let buildFingerprint = context.pluginWorkDirectoryURL.appending(path: "minimake.json")
         let lastBuildFingerprint = try? Data(contentsOf: buildFingerprint)
@@ -338,7 +423,8 @@ struct PackageToJSPlugin: CommandPlugin {
     private func printProgress(context: MiniMake.ProgressPrinter.Context, message: String) {
         let buildCwd = FileManager.default.currentDirectoryPath
         let outputPath = context.scope.resolve(path: context.subject.output).path
-        let displayName = outputPath.hasPrefix(buildCwd + "/")
+        let displayName =
+            outputPath.hasPrefix(buildCwd + "/")
             ? String(outputPath.dropFirst(buildCwd.count + 1)) : outputPath
         printStderr("[\(context.built + 1)/\(context.total)] \(displayName): \(message)")
     }
@@ -353,15 +439,35 @@ private func printStderr(_ message: String) {
 extension PackageToJS.PackageOptions {
     static func parse(from extractor: inout ArgumentExtractor) -> PackageToJS.PackageOptions {
         let outputPath = extractor.extractOption(named: "output").last
+        let configuration: String? =
+            (extractor.extractOption(named: "configuration") + extractor.extractSingleDashOption(named: "c")).last
         let packageName = extractor.extractOption(named: "package-name").last
         let explain = extractor.extractFlag(named: "explain")
         let useCDN = extractor.extractFlag(named: "use-cdn")
         let verbose = extractor.extractFlag(named: "verbose")
         let enableCodeCoverage = extractor.extractFlag(named: "enable-code-coverage")
         return PackageToJS.PackageOptions(
-            outputPath: outputPath, packageName: packageName, explain: explain != 0, verbose: verbose != 0, useCDN: useCDN != 0, enableCodeCoverage: enableCodeCoverage != 0
+            outputPath: outputPath,
+            configuration: configuration,
+            packageName: packageName,
+            explain: explain != 0,
+            verbose: verbose != 0,
+            useCDN: useCDN != 0,
+            enableCodeCoverage: enableCodeCoverage != 0
         )
     }
+
+    static func optionsHelp() -> String {
+        return """
+              --output             Path to the output directory (default: .build/plugins/PackageToJS/outputs/Package)
+              -c, --configuration  The build configuration to use (values: debug, release; default: debug)
+              --package-name       Name of the package (default: lowercased Package.swift name)
+              --use-cdn                  Whether to use CDN for dependency packages
+              --enable-code-coverage     Whether to enable code coverage collection
+              --explain                  Whether to explain the build plan
+              --verbose                  Whether to print verbose output
+            """
+    }
 }
 
 extension PackageToJS.BuildOptions {
@@ -372,12 +478,19 @@ extension PackageToJS.BuildOptions {
         var debugInfoFormat: PackageToJS.DebugInfoFormat = .none
         if let rawDebugInfoFormat = rawDebugInfoFormat {
             guard let format = PackageToJS.DebugInfoFormat(rawValue: rawDebugInfoFormat) else {
-                fatalError("Invalid debug info format: \(rawDebugInfoFormat), expected one of \(PackageToJS.DebugInfoFormat.allCases.map(\.rawValue).joined(separator: ", "))")
+                fatalError(
+                    "Invalid debug info format: \(rawDebugInfoFormat), expected one of \(PackageToJS.DebugInfoFormat.allCases.map(\.rawValue).joined(separator: ", "))"
+                )
             }
             debugInfoFormat = format
         }
         let packageOptions = PackageToJS.PackageOptions.parse(from: &extractor)
-        return PackageToJS.BuildOptions(product: product, noOptimize: noOptimize != 0, debugInfoFormat: debugInfoFormat, packageOptions: packageOptions)
+        return PackageToJS.BuildOptions(
+            product: product,
+            noOptimize: noOptimize != 0,
+            debugInfoFormat: debugInfoFormat,
+            packageOptions: packageOptions
+        )
     }
 
     static func help() -> String {
@@ -387,15 +500,10 @@ extension PackageToJS.BuildOptions {
             USAGE: swift package --swift-sdk  [SwiftPM options] js [options] [subcommand]
 
             OPTIONS:
-              --product     Product to build (default: executable target if there's only one)
-              --output         Path to the output directory (default: .build/plugins/PackageToJS/outputs/Package)
-              --package-name   Name of the package (default: lowercased Package.swift name)
-              --explain              Whether to explain the build plan
-              --verbose              Whether to print verbose output
-              --no-optimize          Whether to disable wasm-opt optimization
-              --use-cdn              Whether to use CDN for dependency packages
-              --enable-code-coverage Whether to enable code coverage collection
-              --debug-info-format    The format of debug info to keep in the final wasm file (values: none, dwarf, name; default: none)
+              --product         Product to build (default: executable target if there's only one)
+              --no-optimize              Whether to disable wasm-opt optimization
+              --debug-info-format        The format of debug info to keep in the final wasm file (values: none, dwarf, name; default: none)
+            \(PackageToJS.PackageOptions.optionsHelp())
 
             SUBCOMMANDS:
               test  Builds and runs tests
@@ -405,7 +513,7 @@ extension PackageToJS.BuildOptions {
               # Build a specific product
               $ swift package --swift-sdk wasm32-unknown-wasi js --product Example
               # Build in release configuration
-              $ swift package --swift-sdk wasm32-unknown-wasi -c release plugin js
+              $ swift package --swift-sdk wasm32-unknown-wasi js -c release
 
               # Run tests
               $ swift package --swift-sdk wasm32-unknown-wasi js test
@@ -424,8 +532,12 @@ extension PackageToJS.TestOptions {
         let extraNodeArguments = extractor.extractSingleDashOption(named: "Xnode")
         let packageOptions = PackageToJS.PackageOptions.parse(from: &extractor)
         var options = PackageToJS.TestOptions(
-            buildOnly: buildOnly != 0, listTests: listTests != 0,
-            filter: filter, prelude: prelude, environment: environment, inspect: inspect != 0,
+            buildOnly: buildOnly != 0,
+            listTests: listTests != 0,
+            filter: filter,
+            prelude: prelude,
+            environment: environment,
+            inspect: inspect != 0,
             extraNodeArguments: extraNodeArguments,
             packageOptions: packageOptions
         )
@@ -444,15 +556,12 @@ extension PackageToJS.TestOptions {
             USAGE: swift package --swift-sdk  [SwiftPM options] js test [options]
 
             OPTIONS:
-              --build-only           Whether to build only
-              --prelude        Path to the prelude script
-              --environment    The environment to use for the tests (values: node, browser; default: node)
-              --inspect              Whether to run tests in the browser with inspector enabled
-              --explain              Whether to explain the build plan
-              --verbose              Whether to print verbose output
-              --use-cdn              Whether to use CDN for dependency packages
-              --enable-code-coverage Whether to enable code coverage collection
-              -Xnode           Extra arguments to pass to Node.js
+              --build-only               Whether to build only
+              --prelude            Path to the prelude script
+              --environment        The environment to use for the tests (values: node, browser; default: node)
+              --inspect                  Whether to run tests in the browser with inspector enabled
+              -Xnode               Extra arguments to pass to Node.js
+            \(PackageToJS.PackageOptions.optionsHelp())
 
             EXAMPLES:
               $ swift package --swift-sdk wasm32-unknown-wasi js test
@@ -467,36 +576,34 @@ extension PackageToJS.TestOptions {
 // MARK: - PackagePlugin helpers
 
 extension ArgumentExtractor {
-  fileprivate mutating func extractSingleDashOption(named name: String) -> [String] {
-    let parts = remainingArguments.split(separator: "--", maxSplits: 1, omittingEmptySubsequences: false)
-    var args = Array(parts[0])
-    let literals = Array(parts.count == 2 ? parts[1] : [])
-
-    var values: [String] = []
-    var idx = 0
-    while idx < args.count {
-      var arg = args[idx]
-      if arg == "-\(name)" {
-        args.remove(at: idx)
-        if idx < args.count {
-          let val = args[idx]
-          values.append(val)
-          args.remove(at: idx)
+    fileprivate mutating func extractSingleDashOption(named name: String) -> [String] {
+        let parts = remainingArguments.split(separator: "--", maxSplits: 1, omittingEmptySubsequences: false)
+        var args = Array(parts[0])
+        let literals = Array(parts.count == 2 ? parts[1] : [])
+
+        var values: [String] = []
+        var idx = 0
+        while idx < args.count {
+            var arg = args[idx]
+            if arg == "-\(name)" {
+                args.remove(at: idx)
+                if idx < args.count {
+                    let val = args[idx]
+                    values.append(val)
+                    args.remove(at: idx)
+                }
+            } else if arg.starts(with: "-\(name)=") {
+                args.remove(at: idx)
+                arg.removeFirst(2 + name.count)
+                values.append(arg)
+            } else {
+                idx += 1
+            }
         }
-      }
-      else if arg.starts(with: "-\(name)=") {
-        args.remove(at: idx)
-        arg.removeFirst(2 + name.count)
-        values.append(arg)
-      }
-      else {
-        idx += 1
-      }
-    }
 
-    self = ArgumentExtractor(args + literals)
-    return values
-  }
+        self = ArgumentExtractor(args + literals)
+        return values
+    }
 }
 
 /// Derive default product from the package
@@ -506,7 +613,8 @@ internal func deriveDefaultProduct(package: Package) throws -> String {
     let executableProducts = package.products(ofType: ExecutableProduct.self)
     guard !executableProducts.isEmpty else {
         throw PackageToJSError(
-            "Make sure there's at least one executable product in your Package.swift")
+            "Make sure there's at least one executable product in your Package.swift"
+        )
     }
     guard executableProducts.count == 1 else {
         throw PackageToJSError(
@@ -553,11 +661,97 @@ private func findPackageInDependencies(package: Package, id: Package.ID) -> Pack
     return visit(package: package)
 }
 
+class SkeletonCollector {
+    private var visitedProducts: Set = []
+    private var visitedTargets: Set = []
+
+    var exportedSkeletons: [URL] = []
+    var importedSkeletons: [URL] = []
+    let exportedSkeletonFile = "ExportSwift.json"
+    let importedSkeletonFile = "ImportTS.json"
+    let context: PluginContext
+
+    init(context: PluginContext) {
+        self.context = context
+    }
+
+    func collectFromProduct(name: String) -> (exportedSkeletons: [URL], importedSkeletons: [URL]) {
+        guard let product = context.package.products.first(where: { $0.name == name }) else {
+            return ([], [])
+        }
+        visit(product: product, package: context.package)
+        return (exportedSkeletons, importedSkeletons)
+    }
+
+    func collectFromTests() -> (exportedSkeletons: [URL], importedSkeletons: [URL]) {
+        let tests = context.package.targets.filter {
+            guard let target = $0 as? SwiftSourceModuleTarget else { return false }
+            return target.kind == .test
+        }
+        for test in tests {
+            visit(target: test, package: context.package)
+        }
+        return (exportedSkeletons, importedSkeletons)
+    }
+
+    private func visit(product: Product, package: Package) {
+        if visitedProducts.contains(product.id) { return }
+        visitedProducts.insert(product.id)
+        for target in product.targets {
+            visit(target: target, package: package)
+        }
+    }
+
+    private func visit(target: Target, package: Package) {
+        if visitedTargets.contains(target.id) { return }
+        visitedTargets.insert(target.id)
+        if let target = target as? SwiftSourceModuleTarget {
+            let directories = [
+                target.directoryURL.appending(path: "Generated/JavaScript"),
+                // context.pluginWorkDirectoryURL: ".build/plugins/PackageToJS/outputs/"
+                // .build/plugins/outputs/exportswift/MyApp/destination/BridgeJS/ExportSwift.json
+                context.pluginWorkDirectoryURL.deletingLastPathComponent().deletingLastPathComponent()
+                    .appending(path: "outputs/\(package.id)/\(target.name)/destination/BridgeJS"),
+            ]
+            for directory in directories {
+                let exportedSkeletonURL = directory.appending(path: exportedSkeletonFile)
+                let importedSkeletonURL = directory.appending(path: importedSkeletonFile)
+                if FileManager.default.fileExists(atPath: exportedSkeletonURL.path) {
+                    exportedSkeletons.append(exportedSkeletonURL)
+                }
+                if FileManager.default.fileExists(atPath: importedSkeletonURL.path) {
+                    importedSkeletons.append(importedSkeletonURL)
+                }
+            }
+        }
+
+        var packageByProduct: [Product.ID: Package] = [:]
+        for packageDependency in package.dependencies {
+            for product in packageDependency.package.products {
+                packageByProduct[product.id] = packageDependency.package
+            }
+        }
+
+        for dependency in target.dependencies {
+            switch dependency {
+            case .product(let product):
+                visit(product: product, package: packageByProduct[product.id]!)
+            case .target(let target):
+                visit(target: target, package: package)
+            @unknown default:
+                continue
+            }
+        }
+    }
+}
+
 extension PackagingPlanner {
     init(
         options: PackageToJS.PackageOptions,
         context: PluginContext,
         selfPackage: Package,
+        exportedSkeletons: [URL],
+        importedSkeletons: [URL],
         outputDir: URL,
         wasmProductArtifact: URL,
         wasmFilename: String
@@ -568,8 +762,12 @@ extension PackagingPlanner {
         self.init(
             options: options,
             packageId: context.package.id,
-            intermediatesDir: BuildPath(absolute: context.pluginWorkDirectoryURL.appending(path: outputBaseName + ".tmp").path),
+            intermediatesDir: BuildPath(
+                absolute: context.pluginWorkDirectoryURL.appending(path: outputBaseName + ".tmp").path
+            ),
             selfPackageDir: BuildPath(absolute: selfPackage.directoryURL.path),
+            exportedSkeletons: exportedSkeletons.map { BuildPath(absolute: $0.path) },
+            importedSkeletons: importedSkeletons.map { BuildPath(absolute: $0.path) },
             outputDir: BuildPath(absolute: outputDir.path),
             wasmProductArtifact: BuildPath(absolute: wasmProductArtifact.path),
             wasmFilename: wasmFilename,
diff --git a/Plugins/PackageToJS/Sources/ParseWasm.swift b/Plugins/PackageToJS/Sources/ParseWasm.swift
index 8cfb6c66c..4372b32c5 100644
--- a/Plugins/PackageToJS/Sources/ParseWasm.swift
+++ b/Plugins/PackageToJS/Sources/ParseWasm.swift
@@ -199,7 +199,8 @@ func parseImports(moduleBytes: Data) throws -> [ImportEntry] {
                 case 0x02:  // Memory
                     let limits = try parseLimits(parseState)
                     imports.append(
-                        ImportEntry(module: module, name: name, kind: .memory(type: limits)))
+                        ImportEntry(module: module, name: name, kind: .memory(type: limits))
+                    )
 
                 case 0x03:  // Global
                     _ = try parseGlobalType(parseState)
diff --git a/Plugins/PackageToJS/Sources/Preprocess.swift b/Plugins/PackageToJS/Sources/Preprocess.swift
index 835dd31a6..bafa2aae5 100644
--- a/Plugins/PackageToJS/Sources/Preprocess.swift
+++ b/Plugins/PackageToJS/Sources/Preprocess.swift
@@ -66,7 +66,10 @@ private struct Preprocessor {
         }
 
         /// Get the 1-indexed line and column
-        private static func computeLineAndColumn(from index: String.Index, in source: String) -> (line: Int, column: Int) {
+        private static func computeLineAndColumn(
+            from index: String.Index,
+            in source: String
+        ) -> (line: Int, column: Int) {
             var line = 1
             var column = 1
             for char in source[.. PreprocessorError {
+    func unexpectedCharacterError(
+        expected: CustomStringConvertible,
+        character: Character,
+        at index: String.Index
+    ) -> PreprocessorError {
         return PreprocessorError(
             file: file,
-            message: "Expected \(expected) but got \(character)", source: source, index: index)
+            message: "Expected \(expected) but got \(character)",
+            source: source,
+            index: index
+        )
     }
 
     func unexpectedDirectiveError(at index: String.Index) -> PreprocessorError {
         return PreprocessorError(
             file: file,
-            message: "Unexpected directive", source: source, index: index)
+            message: "Unexpected directive",
+            source: source,
+            index: index
+        )
     }
 
     func eofError(at index: String.Index) -> PreprocessorError {
         return PreprocessorError(
             file: file,
-            message: "Unexpected end of input", source: source, index: index)
+            message: "Unexpected end of input",
+            source: source,
+            index: index
+        )
     }
 
     func undefinedVariableError(name: String, at index: String.Index) -> PreprocessorError {
         return PreprocessorError(
             file: file,
-            message: "Undefined variable \(name)", source: source, index: index)
+            message: "Undefined variable \(name)",
+            source: source,
+            index: index
+        )
     }
 
     func tokenize() throws -> [TokenInfo] {
@@ -188,7 +210,10 @@ private struct Preprocessor {
         func expect(_ expected: String) throws {
             guard
                 let endIndex = source.index(
-                    cursor, offsetBy: expected.count, limitedBy: source.endIndex)
+                    cursor,
+                    offsetBy: expected.count,
+                    limitedBy: source.endIndex
+                )
             else {
                 throw eofError(at: cursor)
             }
@@ -281,7 +306,11 @@ private struct Preprocessor {
     enum ParseResult {
         case block(String)
         indirect case `if`(
-            condition: String, then: [ParseResult], else: [ParseResult], position: String.Index)
+            condition: String,
+            then: [ParseResult],
+            else: [ParseResult],
+            position: String.Index
+        )
     }
 
     func parse(tokens: [TokenInfo]) throws -> [ParseResult] {
@@ -314,13 +343,19 @@ private struct Preprocessor {
                 }
                 guard case .endif = tokens[cursor].token else {
                     throw unexpectedTokenError(
-                        expected: .endif, token: tokens[cursor].token, at: tokens[cursor].position)
+                        expected: .endif,
+                        token: tokens[cursor].token,
+                        at: tokens[cursor].position
+                    )
                 }
                 consume()
                 return .if(condition: condition, then: then, else: `else`, position: ifPosition)
             case .else, .endif:
                 throw unexpectedTokenError(
-                    expected: nil, token: tokens[cursor].token, at: tokens[cursor].position)
+                    expected: nil,
+                    token: tokens[cursor].token,
+                    at: tokens[cursor].position
+                )
             }
         }
         var results: [ParseResult] = []
@@ -338,9 +373,13 @@ private struct Preprocessor {
             var substitutedContent = content
             for (key, value) in options.substitutions {
                 substitutedContent = substitutedContent.replacingOccurrences(
-                    of: "@" + key + "@", with: value)
+                    of: "@" + key + "@",
+                    with: value
+                )
                 substitutedContent = substitutedContent.replacingOccurrences(
-                    of: "import.meta." + key, with: value)
+                    of: "import.meta." + key,
+                    with: value
+                )
             }
             result.append(substitutedContent)
         }
diff --git a/Plugins/PackageToJS/Sources/TestsParser.swift b/Plugins/PackageToJS/Sources/TestsParser.swift
index 61d417521..72afd6b07 100644
--- a/Plugins/PackageToJS/Sources/TestsParser.swift
+++ b/Plugins/PackageToJS/Sources/TestsParser.swift
@@ -186,7 +186,8 @@ class FancyTestsParser {
         write("\n")
 
         func formatCategory(
-            label: String, statuses: [Status]
+            label: String,
+            statuses: [Status]
         ) -> String {
             var passed = 0
             var skipped = 0
@@ -220,7 +221,8 @@ class FancyTestsParser {
         let suitesWithCases = suites.filter { $0.cases.count > 0 }
         write(
             formatCategory(
-                label: "Test Suites:", statuses: suitesWithCases.map(\.status)
+                label: "Test Suites:",
+                statuses: suitesWithCases.map(\.status)
             )
         )
         let allCaseStatuses = suitesWithCases.flatMap {
@@ -228,7 +230,8 @@ class FancyTestsParser {
         }
         write(
             formatCategory(
-                label: "Tests:      ", statuses: allCaseStatuses
+                label: "Tests:      ",
+                statuses: allCaseStatuses
             )
         )
 
diff --git a/Plugins/PackageToJS/Templates/bin/test.js b/Plugins/PackageToJS/Templates/bin/test.js
index b31d82086..340316288 100644
--- a/Plugins/PackageToJS/Templates/bin/test.js
+++ b/Plugins/PackageToJS/Templates/bin/test.js
@@ -38,35 +38,54 @@ const args = parseArgs({
 
 const harnesses = {
     node: async ({ preludeScript }) => {
-        let options = await nodePlatform.defaultNodeSetup({
-            args: testFrameworkArgs,
-            onExit: (code) => {
-                if (code !== 0) { return }
-                // Extract the coverage file from the wasm module
-                const filePath = "default.profraw"
-                const destinationPath = args.values["coverage-file"] ?? filePath
-                const profraw = options.wasi.extractFile?.(filePath)
-                if (profraw) {
-                    console.log(`Saved ${filePath} to ${destinationPath}`);
-                    writeFileSync(destinationPath, profraw);
+        try {
+            let options = await nodePlatform.defaultNodeSetup({
+                args: testFrameworkArgs,
+                onExit: (code) => {
+                    // swift-testing returns EX_UNAVAILABLE (which is 69 in wasi-libc) for "no tests found"
+                    if (code !== 0 && code !== 69) {
+                        const stack = new Error().stack
+                        console.error(`Test failed with exit code ${code}`)
+                        console.error(stack)
+                        return
+                    }
+                    // Extract the coverage file from the wasm module
+                    const filePath = "default.profraw"
+                    const destinationPath = args.values["coverage-file"] ?? filePath
+                    const profraw = options.wasi.extractFile?.(filePath)
+                    if (profraw) {
+                        console.log(`Saved ${filePath} to ${destinationPath}`);
+                        writeFileSync(destinationPath, profraw);
+                    }
+                },
+/* #if USE_SHARED_MEMORY */
+                spawnWorker: nodePlatform.createDefaultWorkerFactory(preludeScript)
+/* #endif */
+            })
+            if (preludeScript) {
+                const prelude = await import(preludeScript)
+                if (prelude.setupOptions) {
+                    options = prelude.setupOptions(options, { isMainThread: true })
                 }
-            },
-            /* #if USE_SHARED_MEMORY */
-            spawnWorker: nodePlatform.createDefaultWorkerFactory(preludeScript)
-            /* #endif */
-        })
-        if (preludeScript) {
-            const prelude = await import(preludeScript)
-            if (prelude.setupOptions) {
-                options = prelude.setupOptions(options, { isMainThread: true })
             }
-        }
-        try {
+            process.on("beforeExit", () => {
+                // NOTE: "beforeExit" is fired when the process exits gracefully without calling `process.exit`
+                // Either XCTest or swift-testing should always call `process.exit` through `proc_exit` even
+                // if the test succeeds. So exiting gracefully means something went wrong (e.g. withUnsafeContinuation is leaked)
+                // Therefore, we exit with code 1 to indicate that the test execution failed.
+                console.error(`
+
+=================================================================================================
+Detected that the test execution ended without a termination signal from the testing framework.
+Hint: This typically means that a continuation leak occurred.
+=================================================================================================`)
+                process.exit(1)
+            })
             await instantiate(options)
         } catch (e) {
             if (e instanceof WebAssembly.CompileError) {
                 // Check Node.js major version
-                const nodeVersion = process.version.split(".")[0]
+                const nodeVersion = process.versions.node.split(".")[0]
                 const minNodeVersion = 20
                 if (nodeVersion < minNodeVersion) {
                     console.error(`Hint: Node.js version ${nodeVersion} is not supported, please use version ${minNodeVersion} or later.`)
diff --git a/Plugins/PackageToJS/Templates/index.d.ts b/Plugins/PackageToJS/Templates/index.d.ts
index 11d5908c2..77d68efd9 100644
--- a/Plugins/PackageToJS/Templates/index.d.ts
+++ b/Plugins/PackageToJS/Templates/index.d.ts
@@ -1,4 +1,4 @@
-import type { Export, ModuleSource } from './instantiate.js'
+import type { Exports, Imports, ModuleSource } from './instantiate.js'
 
 export type Options = {
     /**
@@ -7,6 +7,12 @@ export type Options = {
      * If not provided, the module will be fetched from the default path.
      */
     module?: ModuleSource
+/* #if HAS_IMPORTS */
+    /**
+     * The imports to use for the module
+     */
+    imports: Imports
+/* #endif */
 }
 
 /**
@@ -17,5 +23,5 @@ export type Options = {
  */
 export declare function init(options?: Options): Promise<{
     instance: WebAssembly.Instance,
-    exports: Export
+    exports: Exports
 }>
diff --git a/Plugins/PackageToJS/Templates/index.js b/Plugins/PackageToJS/Templates/index.js
index 4b8d90f6b..76721511a 100644
--- a/Plugins/PackageToJS/Templates/index.js
+++ b/Plugins/PackageToJS/Templates/index.js
@@ -3,13 +3,23 @@ import { instantiate } from './instantiate.js';
 import { defaultBrowserSetup /* #if USE_SHARED_MEMORY */, createDefaultWorkerFactory /* #endif */} from './platforms/browser.js';
 
 /** @type {import('./index.d').init} */
-export async function init(options = {}) {
+export async function init(_options) {
+    /** @type {import('./index.d').Options} */
+    const options = _options || {
+/* #if HAS_IMPORTS */
+        /** @returns {import('./instantiate.d').Imports} */
+        get imports() { (() => { throw new Error("No imports provided") })() }
+/* #endif */
+    };
     let module = options.module;
     if (!module) {
         module = fetch(new URL("@PACKAGE_TO_JS_MODULE_PATH@", import.meta.url))
     }
     const instantiateOptions = await defaultBrowserSetup({
         module,
+/* #if HAS_IMPORTS */
+        imports: options.imports,
+/* #endif */
 /* #if USE_SHARED_MEMORY */
         spawnWorker: createDefaultWorkerFactory()
 /* #endif */
diff --git a/Plugins/PackageToJS/Templates/instantiate.d.ts b/Plugins/PackageToJS/Templates/instantiate.d.ts
index 424d35175..2d81ddde3 100644
--- a/Plugins/PackageToJS/Templates/instantiate.d.ts
+++ b/Plugins/PackageToJS/Templates/instantiate.d.ts
@@ -1,13 +1,12 @@
-/* #if USE_SHARED_MEMORY */
-import type { SwiftRuntimeThreadChannel, SwiftRuntime } from "./runtime.js";
-/* #endif */
+import type { /* #if USE_SHARED_MEMORY */SwiftRuntimeThreadChannel, /* #endif */SwiftRuntime } from "./runtime.js";
 
-export type Import = {
-    // TODO: Generate type from imported .d.ts files
-}
-export type Export = {
-    // TODO: Generate type from .swift files
-}
+/* #if HAS_BRIDGE */
+// @ts-ignore
+export type { Imports, Exports } from "./bridge.js";
+/* #else */
+export type Imports = {}
+export type Exports = {}
+/* #endif */
 
 /**
  * The path to the WebAssembly module relative to the root of the package
@@ -57,14 +56,21 @@ export type ModuleSource = WebAssembly.Module | ArrayBufferView | ArrayBuffer |
  * The options for instantiating a WebAssembly module
  */
 export type InstantiateOptions = {
+    /**
+     * The WebAssembly namespace to use for instantiation.
+     * Defaults to the globalThis.WebAssembly object.
+     */
+    WebAssembly?: typeof globalThis.WebAssembly,
     /**
      * The WebAssembly module to instantiate
      */
     module: ModuleSource,
+/* #if HAS_IMPORTS */
     /**
      * The imports provided by the embedder
      */
-    imports: Import,
+    imports: Imports,
+/* #endif */
 /* #if IS_WASI */
     /**
      * The WASI implementation to use
@@ -87,8 +93,30 @@ export type InstantiateOptions = {
     /**
      * Add imports to the WebAssembly import object
      * @param imports - The imports to add
+     * @param context - The context object
+     */
+    addToCoreImports?: (
+        imports: WebAssembly.Imports,
+        context: {
+            getInstance: () => WebAssembly.Instance | null,
+            getExports: () => Exports | null,
+            _swift: SwiftRuntime,
+        }
+    ) => void
+
+    /**
+     * Instrument the WebAssembly instance
+     *
+     * @param instance - The instance of the WebAssembly module
+     * @param context - The context object
+     * @returns The instrumented instance
      */
-    addToCoreImports?: (imports: WebAssembly.Imports) => void
+    instrumentInstance?: (
+        instance: WebAssembly.Instance,
+        context: {
+            _swift: SwiftRuntime
+        }
+    ) => WebAssembly.Instance
 }
 
 /**
@@ -97,7 +125,7 @@ export type InstantiateOptions = {
 export declare function instantiate(options: InstantiateOptions): Promise<{
     instance: WebAssembly.Instance,
     swift: SwiftRuntime,
-    exports: Export
+    exports: Exports
 }>
 
 /**
@@ -106,5 +134,5 @@ export declare function instantiate(options: InstantiateOptions): Promise<{
 export declare function instantiateForThread(tid: number, startArg: number, options: InstantiateOptions): Promise<{
     instance: WebAssembly.Instance,
     swift: SwiftRuntime,
-    exports: Export
+    exports: Exports
 }>
diff --git a/Plugins/PackageToJS/Templates/instantiate.js b/Plugins/PackageToJS/Templates/instantiate.js
index d786c31ef..4a3a32221 100644
--- a/Plugins/PackageToJS/Templates/instantiate.js
+++ b/Plugins/PackageToJS/Templates/instantiate.js
@@ -1,5 +1,4 @@
 // @ts-check
-// @ts-ignore
 import { SwiftRuntime } from "./runtime.js"
 
 export const MODULE_PATH = "@PACKAGE_TO_JS_MODULE_PATH@";
@@ -14,19 +13,28 @@ export const MEMORY_TYPE = {
 }
 /* #endif */
 
+/* #if HAS_BRIDGE */
+// @ts-ignore
+import { createInstantiator } from "./bridge.js"
+/* #else */
 /**
  * @param {import('./instantiate.d').InstantiateOptions} options
+ * @param {any} swift
  */
-async function createInstantiator(options) {
+async function createInstantiator(options, swift) {
     return {
         /** @param {WebAssembly.Imports} importObject */
         addImports: (importObject) => {},
         /** @param {WebAssembly.Instance} instance */
+        setInstance: (instance) => {},
+        /** @param {WebAssembly.Instance} instance */
         createExports: (instance) => {
             return {};
         },
     }
 }
+/* #endif */
+
 /** @type {import('./instantiate.d').instantiate} */
 export async function instantiate(
     options
@@ -55,17 +63,18 @@ export async function instantiateForThread(
 async function _instantiate(
     options
 ) {
+    const _WebAssembly = options.WebAssembly || WebAssembly;
     const moduleSource = options.module;
 /* #if IS_WASI */
     const { wasi } = options;
 /* #endif */
-    const instantiator = await createInstantiator(options);
     const swift = new SwiftRuntime({
 /* #if USE_SHARED_MEMORY */
         sharedMemory: true,
         threadChannel: options.threadChannel,
 /* #endif */
     });
+    const instantiator = await createInstantiator(options, swift);
 
     /** @type {WebAssembly.Imports} */
     const importObject = {
@@ -85,34 +94,42 @@ async function _instantiate(
 /* #endif */
     };
     instantiator.addImports(importObject);
-    options.addToCoreImports?.(importObject);
+    options.addToCoreImports?.(importObject, {
+        getInstance: () => instance,
+        getExports: () => exports,
+        _swift: swift,
+    });
 
     let module;
     let instance;
-    if (moduleSource instanceof WebAssembly.Module) {
+    let exports;
+    if (moduleSource instanceof _WebAssembly.Module) {
         module = moduleSource;
-        instance = await WebAssembly.instantiate(module, importObject);
+        instance = await _WebAssembly.instantiate(module, importObject);
     } else if (typeof Response === "function" && (moduleSource instanceof Response || moduleSource instanceof Promise)) {
-        if (typeof WebAssembly.instantiateStreaming === "function") {
-            const result = await WebAssembly.instantiateStreaming(moduleSource, importObject);
+        if (typeof _WebAssembly.instantiateStreaming === "function") {
+            const result = await _WebAssembly.instantiateStreaming(moduleSource, importObject);
             module = result.module;
             instance = result.instance;
         } else {
             const moduleBytes = await (await moduleSource).arrayBuffer();
-            module = await WebAssembly.compile(moduleBytes);
-            instance = await WebAssembly.instantiate(module, importObject);
+            module = await _WebAssembly.compile(moduleBytes);
+            instance = await _WebAssembly.instantiate(module, importObject);
         }
     } else {
         // @ts-expect-error: Type 'Response' is not assignable to type 'BufferSource'
-        module = await WebAssembly.compile(moduleSource);
-        instance = await WebAssembly.instantiate(module, importObject);
+        module = await _WebAssembly.compile(moduleSource);
+        instance = await _WebAssembly.instantiate(module, importObject);
     }
+    instance = options.instrumentInstance?.(instance, { _swift: swift }) ?? instance;
 
     swift.setInstance(instance);
+    instantiator.setInstance(instance);
+    exports = instantiator.createExports(instance);
 
     return {
         instance,
         swift,
-        exports: instantiator.createExports(instance),
+        exports,
     }
 }
diff --git a/Plugins/PackageToJS/Templates/package.json b/Plugins/PackageToJS/Templates/package.json
index 79562784a..a41e6db28 100644
--- a/Plugins/PackageToJS/Templates/package.json
+++ b/Plugins/PackageToJS/Templates/package.json
@@ -10,7 +10,12 @@
     "dependencies": {
         "@bjorn3/browser_wasi_shim": "0.3.0"
     },
-    "devDependencies": {
+    "peerDependencies": {
         "playwright": "^1.51.0"
+    },
+    "peerDependenciesMeta": {
+        "playwright": {
+            "optional": true
+        }
     }
 }
diff --git a/Plugins/PackageToJS/Templates/platforms/browser.d.ts b/Plugins/PackageToJS/Templates/platforms/browser.d.ts
index 5b27cc903..b851c2283 100644
--- a/Plugins/PackageToJS/Templates/platforms/browser.d.ts
+++ b/Plugins/PackageToJS/Templates/platforms/browser.d.ts
@@ -1,12 +1,15 @@
-import type { InstantiateOptions, ModuleSource } from "../instantiate.js"
+import type { InstantiateOptions, ModuleSource/* #if HAS_IMPORTS */, Imports/* #endif */ } from "../instantiate.js"
 
-export async function defaultBrowserSetup(options: {
+export function defaultBrowserSetup(options: {
     module: ModuleSource,
 /* #if IS_WASI */
     args?: string[],
     onStdoutLine?: (line: string) => void,
     onStderrLine?: (line: string) => void,
 /* #endif */
+/* #if HAS_IMPORTS */
+    imports: Imports,
+/* #endif */
 /* #if USE_SHARED_MEMORY */
     spawnWorker: (module: WebAssembly.Module, memory: WebAssembly.Memory, startArg: any) => Worker,
 /* #endif */
diff --git a/Plugins/PackageToJS/Templates/platforms/browser.js b/Plugins/PackageToJS/Templates/platforms/browser.js
index 672c274db..9afd5c94a 100644
--- a/Plugins/PackageToJS/Templates/platforms/browser.js
+++ b/Plugins/PackageToJS/Templates/platforms/browser.js
@@ -2,8 +2,10 @@
 import { MODULE_PATH /* #if USE_SHARED_MEMORY */, MEMORY_TYPE /* #endif */} from "../instantiate.js"
 /* #if IS_WASI */
 /* #if USE_WASI_CDN */
+// @ts-ignore
 import { WASI, File, OpenFile, ConsoleStdout, PreopenDirectory } from 'https://cdn.jsdelivr.net/npm/@bjorn3/browser_wasi_shim@0.4.1/+esm';
 /* #else */
+// @ts-ignore
 import { WASI, File, OpenFile, ConsoleStdout, PreopenDirectory } from '@bjorn3/browser_wasi_shim';
 /* #endif */
 /* #endif */
@@ -121,7 +123,9 @@ export async function defaultBrowserSetup(options) {
 
     return {
         module: options.module,
-        imports: {},
+/* #if HAS_IMPORTS */
+        imports: options.imports,
+/* #endif */
 /* #if IS_WASI */
         wasi: Object.assign(wasi, {
             setInstance(instance) {
diff --git a/Plugins/PackageToJS/Templates/platforms/node.d.ts b/Plugins/PackageToJS/Templates/platforms/node.d.ts
index 636ad0eea..9d80205fc 100644
--- a/Plugins/PackageToJS/Templates/platforms/node.d.ts
+++ b/Plugins/PackageToJS/Templates/platforms/node.d.ts
@@ -1,7 +1,7 @@
 import type { InstantiateOptions } from "../instantiate.js"
 import type { Worker } from "node:worker_threads"
 
-export async function defaultNodeSetup(options: {
+export function defaultNodeSetup(options: {
 /* #if IS_WASI */
     args?: string[],
 /* #endif */
diff --git a/Plugins/PackageToJS/Templates/runtime.d.ts b/Plugins/PackageToJS/Templates/runtime.d.ts
new file mode 100644
index 000000000..9613004cc
--- /dev/null
+++ b/Plugins/PackageToJS/Templates/runtime.d.ts
@@ -0,0 +1,217 @@
+type ref = number;
+type pointer = number;
+
+declare class Memory {
+    readonly rawMemory: WebAssembly.Memory;
+    private readonly heap;
+    constructor(exports: WebAssembly.Exports);
+    retain: (value: any) => number;
+    getObject: (ref: number) => any;
+    release: (ref: number) => void;
+    bytes: () => Uint8Array;
+    dataView: () => DataView;
+    writeBytes: (ptr: pointer, bytes: Uint8Array) => void;
+    readUint32: (ptr: pointer) => number;
+    readUint64: (ptr: pointer) => bigint;
+    readInt64: (ptr: pointer) => bigint;
+    readFloat64: (ptr: pointer) => number;
+    writeUint32: (ptr: pointer, value: number) => void;
+    writeUint64: (ptr: pointer, value: bigint) => void;
+    writeInt64: (ptr: pointer, value: bigint) => void;
+    writeFloat64: (ptr: pointer, value: number) => void;
+}
+
+/**
+ * A thread channel is a set of functions that are used to communicate between
+ * the main thread and the worker thread. The main thread and the worker thread
+ * can send messages to each other using these functions.
+ *
+ * @example
+ * ```javascript
+ * // worker.js
+ * const runtime = new SwiftRuntime({
+ *   threadChannel: {
+ *     postMessageToMainThread: postMessage,
+ *     listenMessageFromMainThread: (listener) => {
+ *       self.onmessage = (event) => {
+ *         listener(event.data);
+ *       };
+ *     }
+ *   }
+ * });
+ *
+ * // main.js
+ * const worker = new Worker("worker.js");
+ * const runtime = new SwiftRuntime({
+ *   threadChannel: {
+ *     postMessageToWorkerThread: (tid, data) => {
+ *       worker.postMessage(data);
+ *     },
+ *     listenMessageFromWorkerThread: (tid, listener) => {
+ *       worker.onmessage = (event) => {
+           listener(event.data);
+ *       };
+ *     }
+ *   }
+ * });
+ * ```
+ */
+type SwiftRuntimeThreadChannel = {
+    /**
+     * This function is used to send messages from the worker thread to the main thread.
+     * The message submitted by this function is expected to be listened by `listenMessageFromWorkerThread`.
+     * @param message The message to be sent to the main thread.
+     * @param transfer The array of objects to be transferred to the main thread.
+     */
+    postMessageToMainThread: (message: WorkerToMainMessage, transfer: any[]) => void;
+    /**
+     * This function is expected to be set in the worker thread and should listen
+     * to messages from the main thread sent by `postMessageToWorkerThread`.
+     * @param listener The listener function to be called when a message is received from the main thread.
+     */
+    listenMessageFromMainThread: (listener: (message: MainToWorkerMessage) => void) => void;
+} | {
+    /**
+     * This function is expected to be set in the main thread.
+     * The message submitted by this function is expected to be listened by `listenMessageFromMainThread`.
+     * @param tid The thread ID of the worker thread.
+     * @param message The message to be sent to the worker thread.
+     * @param transfer The array of objects to be transferred to the worker thread.
+     */
+    postMessageToWorkerThread: (tid: number, message: MainToWorkerMessage, transfer: any[]) => void;
+    /**
+     * This function is expected to be set in the main thread and should listen
+     * to messages sent by `postMessageToMainThread` from the worker thread.
+     * @param tid The thread ID of the worker thread.
+     * @param listener The listener function to be called when a message is received from the worker thread.
+     */
+    listenMessageFromWorkerThread: (tid: number, listener: (message: WorkerToMainMessage) => void) => void;
+    /**
+     * This function is expected to be set in the main thread and called
+     * when the worker thread is terminated.
+     * @param tid The thread ID of the worker thread.
+     */
+    terminateWorkerThread?: (tid: number) => void;
+};
+declare class ITCInterface {
+    private memory;
+    constructor(memory: Memory);
+    send(sendingObject: ref, transferringObjects: ref[], sendingContext: pointer): {
+        object: any;
+        sendingContext: pointer;
+        transfer: Transferable[];
+    };
+    sendObjects(sendingObjects: ref[], transferringObjects: ref[], sendingContext: pointer): {
+        object: any[];
+        sendingContext: pointer;
+        transfer: Transferable[];
+    };
+    release(objectRef: ref): {
+        object: undefined;
+        transfer: Transferable[];
+    };
+}
+type AllRequests> = {
+    [K in keyof Interface]: {
+        method: K;
+        parameters: Parameters;
+    };
+};
+type ITCRequest> = AllRequests[keyof AllRequests];
+type AllResponses> = {
+    [K in keyof Interface]: ReturnType;
+};
+type ITCResponse> = AllResponses[keyof AllResponses];
+type RequestMessage = {
+    type: "request";
+    data: {
+        /** The TID of the thread that sent the request */
+        sourceTid: number;
+        /** The TID of the thread that should respond to the request */
+        targetTid: number;
+        /** The context pointer of the request */
+        context: pointer;
+        /** The request content */
+        request: ITCRequest;
+    };
+};
+type SerializedError = {
+    isError: true;
+    value: Error;
+} | {
+    isError: false;
+    value: unknown;
+};
+type ResponseMessage = {
+    type: "response";
+    data: {
+        /** The TID of the thread that sent the response */
+        sourceTid: number;
+        /** The context pointer of the request */
+        context: pointer;
+        /** The response content */
+        response: {
+            ok: true;
+            value: ITCResponse;
+        } | {
+            ok: false;
+            error: SerializedError;
+        };
+    };
+};
+type MainToWorkerMessage = {
+    type: "wake";
+} | RequestMessage | ResponseMessage;
+type WorkerToMainMessage = {
+    type: "job";
+    data: number;
+} | RequestMessage | ResponseMessage;
+
+type SwiftRuntimeOptions = {
+    /**
+     * If `true`, the memory space of the WebAssembly instance can be shared
+     * between the main thread and the worker thread.
+     */
+    sharedMemory?: boolean;
+    /**
+     * The thread channel is a set of functions that are used to communicate
+     * between the main thread and the worker thread.
+     */
+    threadChannel?: SwiftRuntimeThreadChannel;
+};
+declare class SwiftRuntime {
+    private _instance;
+    private _memory;
+    private _closureDeallocator;
+    private options;
+    private version;
+    private textDecoder;
+    private textEncoder;
+    /** The thread ID of the current thread. */
+    private tid;
+    UnsafeEventLoopYield: typeof UnsafeEventLoopYield;
+    constructor(options?: SwiftRuntimeOptions);
+    setInstance(instance: WebAssembly.Instance): void;
+    main(): void;
+    /**
+     * Start a new thread with the given `tid` and `startArg`, which
+     * is forwarded to the `wasi_thread_start` function.
+     * This function is expected to be called from the spawned Web Worker thread.
+     */
+    startThread(tid: number, startArg: number): void;
+    private get instance();
+    private get exports();
+    private get memory();
+    private get closureDeallocator();
+    private callHostFunction;
+    /** @deprecated Use `wasmImports` instead */
+    importObjects: () => WebAssembly.ModuleImports;
+    get wasmImports(): WebAssembly.ModuleImports;
+    private postMessageToMainThread;
+    private postMessageToWorkerThread;
+}
+declare class UnsafeEventLoopYield extends Error {
+}
+
+export { SwiftRuntime };
+export type { SwiftRuntimeOptions, SwiftRuntimeThreadChannel };
diff --git a/Plugins/PackageToJS/Templates/runtime.mjs b/Plugins/PackageToJS/Templates/runtime.mjs
new file mode 100644
index 000000000..71f7f9a30
--- /dev/null
+++ b/Plugins/PackageToJS/Templates/runtime.mjs
@@ -0,0 +1,829 @@
+/// Memory lifetime of closures in Swift are managed by Swift side
+class SwiftClosureDeallocator {
+    constructor(exports) {
+        if (typeof FinalizationRegistry === "undefined") {
+            throw new Error("The Swift part of JavaScriptKit was configured to require " +
+                "the availability of JavaScript WeakRefs. Please build " +
+                "with `-Xswiftc -DJAVASCRIPTKIT_WITHOUT_WEAKREFS` to " +
+                "disable features that use WeakRefs.");
+        }
+        this.functionRegistry = new FinalizationRegistry((id) => {
+            exports.swjs_free_host_function(id);
+        });
+    }
+    track(func, func_ref) {
+        this.functionRegistry.register(func, func_ref);
+    }
+}
+
+function assertNever(x, message) {
+    throw new Error(message);
+}
+const MAIN_THREAD_TID = -1;
+
+const decode = (kind, payload1, payload2, memory) => {
+    switch (kind) {
+        case 0 /* Kind.Boolean */:
+            switch (payload1) {
+                case 0:
+                    return false;
+                case 1:
+                    return true;
+            }
+        case 2 /* Kind.Number */:
+            return payload2;
+        case 1 /* Kind.String */:
+        case 3 /* Kind.Object */:
+        case 6 /* Kind.Function */:
+        case 7 /* Kind.Symbol */:
+        case 8 /* Kind.BigInt */:
+            return memory.getObject(payload1);
+        case 4 /* Kind.Null */:
+            return null;
+        case 5 /* Kind.Undefined */:
+            return undefined;
+        default:
+            assertNever(kind, `JSValue Type kind "${kind}" is not supported`);
+    }
+};
+// Note:
+// `decodeValues` assumes that the size of RawJSValue is 16.
+const decodeArray = (ptr, length, memory) => {
+    // fast path for empty array
+    if (length === 0) {
+        return [];
+    }
+    let result = [];
+    // It's safe to hold DataView here because WebAssembly.Memory.buffer won't
+    // change within this function.
+    const view = memory.dataView();
+    for (let index = 0; index < length; index++) {
+        const base = ptr + 16 * index;
+        const kind = view.getUint32(base, true);
+        const payload1 = view.getUint32(base + 4, true);
+        const payload2 = view.getFloat64(base + 8, true);
+        result.push(decode(kind, payload1, payload2, memory));
+    }
+    return result;
+};
+// A helper function to encode a RawJSValue into a pointers.
+// Please prefer to use `writeAndReturnKindBits` to avoid unnecessary
+// memory stores.
+// This function should be used only when kind flag is stored in memory.
+const write = (value, kind_ptr, payload1_ptr, payload2_ptr, is_exception, memory) => {
+    const kind = writeAndReturnKindBits(value, payload1_ptr, payload2_ptr, is_exception, memory);
+    memory.writeUint32(kind_ptr, kind);
+};
+const writeAndReturnKindBits = (value, payload1_ptr, payload2_ptr, is_exception, memory) => {
+    const exceptionBit = (is_exception ? 1 : 0) << 31;
+    if (value === null) {
+        return exceptionBit | 4 /* Kind.Null */;
+    }
+    const writeRef = (kind) => {
+        memory.writeUint32(payload1_ptr, memory.retain(value));
+        return exceptionBit | kind;
+    };
+    const type = typeof value;
+    switch (type) {
+        case "boolean": {
+            memory.writeUint32(payload1_ptr, value ? 1 : 0);
+            return exceptionBit | 0 /* Kind.Boolean */;
+        }
+        case "number": {
+            memory.writeFloat64(payload2_ptr, value);
+            return exceptionBit | 2 /* Kind.Number */;
+        }
+        case "string": {
+            return writeRef(1 /* Kind.String */);
+        }
+        case "undefined": {
+            return exceptionBit | 5 /* Kind.Undefined */;
+        }
+        case "object": {
+            return writeRef(3 /* Kind.Object */);
+        }
+        case "function": {
+            return writeRef(6 /* Kind.Function */);
+        }
+        case "symbol": {
+            return writeRef(7 /* Kind.Symbol */);
+        }
+        case "bigint": {
+            return writeRef(8 /* Kind.BigInt */);
+        }
+        default:
+            assertNever(type, `Type "${type}" is not supported yet`);
+    }
+    throw new Error("Unreachable");
+};
+function decodeObjectRefs(ptr, length, memory) {
+    const result = new Array(length);
+    for (let i = 0; i < length; i++) {
+        result[i] = memory.readUint32(ptr + 4 * i);
+    }
+    return result;
+}
+
+let globalVariable;
+if (typeof globalThis !== "undefined") {
+    globalVariable = globalThis;
+}
+else if (typeof window !== "undefined") {
+    globalVariable = window;
+}
+else if (typeof global !== "undefined") {
+    globalVariable = global;
+}
+else if (typeof self !== "undefined") {
+    globalVariable = self;
+}
+
+class SwiftRuntimeHeap {
+    constructor() {
+        this._heapValueById = new Map();
+        this._heapValueById.set(0, globalVariable);
+        this._heapEntryByValue = new Map();
+        this._heapEntryByValue.set(globalVariable, { id: 0, rc: 1 });
+        // Note: 0 is preserved for global
+        this._heapNextKey = 1;
+    }
+    retain(value) {
+        const entry = this._heapEntryByValue.get(value);
+        if (entry) {
+            entry.rc++;
+            return entry.id;
+        }
+        const id = this._heapNextKey++;
+        this._heapValueById.set(id, value);
+        this._heapEntryByValue.set(value, { id: id, rc: 1 });
+        return id;
+    }
+    release(ref) {
+        const value = this._heapValueById.get(ref);
+        const entry = this._heapEntryByValue.get(value);
+        entry.rc--;
+        if (entry.rc != 0)
+            return;
+        this._heapEntryByValue.delete(value);
+        this._heapValueById.delete(ref);
+    }
+    referenceHeap(ref) {
+        const value = this._heapValueById.get(ref);
+        if (value === undefined) {
+            throw new ReferenceError("Attempted to read invalid reference " + ref);
+        }
+        return value;
+    }
+}
+
+class Memory {
+    constructor(exports) {
+        this.heap = new SwiftRuntimeHeap();
+        this.retain = (value) => this.heap.retain(value);
+        this.getObject = (ref) => this.heap.referenceHeap(ref);
+        this.release = (ref) => this.heap.release(ref);
+        this.bytes = () => new Uint8Array(this.rawMemory.buffer);
+        this.dataView = () => new DataView(this.rawMemory.buffer);
+        this.writeBytes = (ptr, bytes) => this.bytes().set(bytes, ptr);
+        this.readUint32 = (ptr) => this.dataView().getUint32(ptr, true);
+        this.readUint64 = (ptr) => this.dataView().getBigUint64(ptr, true);
+        this.readInt64 = (ptr) => this.dataView().getBigInt64(ptr, true);
+        this.readFloat64 = (ptr) => this.dataView().getFloat64(ptr, true);
+        this.writeUint32 = (ptr, value) => this.dataView().setUint32(ptr, value, true);
+        this.writeUint64 = (ptr, value) => this.dataView().setBigUint64(ptr, value, true);
+        this.writeInt64 = (ptr, value) => this.dataView().setBigInt64(ptr, value, true);
+        this.writeFloat64 = (ptr, value) => this.dataView().setFloat64(ptr, value, true);
+        this.rawMemory = exports.memory;
+    }
+}
+
+class ITCInterface {
+    constructor(memory) {
+        this.memory = memory;
+    }
+    send(sendingObject, transferringObjects, sendingContext) {
+        const object = this.memory.getObject(sendingObject);
+        const transfer = transferringObjects.map(ref => this.memory.getObject(ref));
+        return { object, sendingContext, transfer };
+    }
+    sendObjects(sendingObjects, transferringObjects, sendingContext) {
+        const objects = sendingObjects.map(ref => this.memory.getObject(ref));
+        const transfer = transferringObjects.map(ref => this.memory.getObject(ref));
+        return { object: objects, sendingContext, transfer };
+    }
+    release(objectRef) {
+        this.memory.release(objectRef);
+        return { object: undefined, transfer: [] };
+    }
+}
+class MessageBroker {
+    constructor(selfTid, threadChannel, handlers) {
+        this.selfTid = selfTid;
+        this.threadChannel = threadChannel;
+        this.handlers = handlers;
+    }
+    request(message) {
+        if (message.data.targetTid == this.selfTid) {
+            // The request is for the current thread
+            this.handlers.onRequest(message);
+        }
+        else if ("postMessageToWorkerThread" in this.threadChannel) {
+            // The request is for another worker thread sent from the main thread
+            this.threadChannel.postMessageToWorkerThread(message.data.targetTid, message, []);
+        }
+        else if ("postMessageToMainThread" in this.threadChannel) {
+            // The request is for other worker threads or the main thread sent from a worker thread
+            this.threadChannel.postMessageToMainThread(message, []);
+        }
+        else {
+            throw new Error("unreachable");
+        }
+    }
+    reply(message) {
+        if (message.data.sourceTid == this.selfTid) {
+            // The response is for the current thread
+            this.handlers.onResponse(message);
+            return;
+        }
+        const transfer = message.data.response.ok ? message.data.response.value.transfer : [];
+        if ("postMessageToWorkerThread" in this.threadChannel) {
+            // The response is for another worker thread sent from the main thread
+            this.threadChannel.postMessageToWorkerThread(message.data.sourceTid, message, transfer);
+        }
+        else if ("postMessageToMainThread" in this.threadChannel) {
+            // The response is for other worker threads or the main thread sent from a worker thread
+            this.threadChannel.postMessageToMainThread(message, transfer);
+        }
+        else {
+            throw new Error("unreachable");
+        }
+    }
+    onReceivingRequest(message) {
+        if (message.data.targetTid == this.selfTid) {
+            this.handlers.onRequest(message);
+        }
+        else if ("postMessageToWorkerThread" in this.threadChannel) {
+            // Receive a request from a worker thread to other worker on main thread. 
+            // Proxy the request to the target worker thread.
+            this.threadChannel.postMessageToWorkerThread(message.data.targetTid, message, []);
+        }
+        else if ("postMessageToMainThread" in this.threadChannel) {
+            // A worker thread won't receive a request for other worker threads
+            throw new Error("unreachable");
+        }
+    }
+    onReceivingResponse(message) {
+        if (message.data.sourceTid == this.selfTid) {
+            this.handlers.onResponse(message);
+        }
+        else if ("postMessageToWorkerThread" in this.threadChannel) {
+            // Receive a response from a worker thread to other worker on main thread.
+            // Proxy the response to the target worker thread.
+            const transfer = message.data.response.ok ? message.data.response.value.transfer : [];
+            this.threadChannel.postMessageToWorkerThread(message.data.sourceTid, message, transfer);
+        }
+        else if ("postMessageToMainThread" in this.threadChannel) {
+            // A worker thread won't receive a response for other worker threads
+            throw new Error("unreachable");
+        }
+    }
+}
+function serializeError(error) {
+    if (error instanceof Error) {
+        return { isError: true, value: { message: error.message, name: error.name, stack: error.stack } };
+    }
+    return { isError: false, value: error };
+}
+function deserializeError(error) {
+    if (error.isError) {
+        return Object.assign(new Error(error.value.message), error.value);
+    }
+    return error.value;
+}
+
+class SwiftRuntime {
+    constructor(options) {
+        this.version = 708;
+        this.textDecoder = new TextDecoder("utf-8");
+        this.textEncoder = new TextEncoder(); // Only support utf-8
+        this.UnsafeEventLoopYield = UnsafeEventLoopYield;
+        /** @deprecated Use `wasmImports` instead */
+        this.importObjects = () => this.wasmImports;
+        this._instance = null;
+        this._memory = null;
+        this._closureDeallocator = null;
+        this.tid = null;
+        this.options = options || {};
+    }
+    setInstance(instance) {
+        this._instance = instance;
+        if (typeof this.exports._start === "function") {
+            throw new Error(`JavaScriptKit supports only WASI reactor ABI.
+                Please make sure you are building with:
+                -Xswiftc -Xclang-linker -Xswiftc -mexec-model=reactor
+                `);
+        }
+        if (this.exports.swjs_library_version() != this.version) {
+            throw new Error(`The versions of JavaScriptKit are incompatible.
+                WebAssembly runtime ${this.exports.swjs_library_version()} != JS runtime ${this.version}`);
+        }
+    }
+    main() {
+        const instance = this.instance;
+        try {
+            if (typeof instance.exports.main === "function") {
+                instance.exports.main();
+            }
+            else if (typeof instance.exports.__main_argc_argv === "function") {
+                // Swift 6.0 and later use `__main_argc_argv` instead of `main`.
+                instance.exports.__main_argc_argv(0, 0);
+            }
+        }
+        catch (error) {
+            if (error instanceof UnsafeEventLoopYield) {
+                // Ignore the error
+                return;
+            }
+            // Rethrow other errors
+            throw error;
+        }
+    }
+    /**
+     * Start a new thread with the given `tid` and `startArg`, which
+     * is forwarded to the `wasi_thread_start` function.
+     * This function is expected to be called from the spawned Web Worker thread.
+     */
+    startThread(tid, startArg) {
+        this.tid = tid;
+        const instance = this.instance;
+        try {
+            if (typeof instance.exports.wasi_thread_start === "function") {
+                instance.exports.wasi_thread_start(tid, startArg);
+            }
+            else {
+                throw new Error(`The WebAssembly module is not built for wasm32-unknown-wasip1-threads target.`);
+            }
+        }
+        catch (error) {
+            if (error instanceof UnsafeEventLoopYield) {
+                // Ignore the error
+                return;
+            }
+            // Rethrow other errors
+            throw error;
+        }
+    }
+    get instance() {
+        if (!this._instance)
+            throw new Error("WebAssembly instance is not set yet");
+        return this._instance;
+    }
+    get exports() {
+        return this.instance.exports;
+    }
+    get memory() {
+        if (!this._memory) {
+            this._memory = new Memory(this.instance.exports);
+        }
+        return this._memory;
+    }
+    get closureDeallocator() {
+        if (this._closureDeallocator)
+            return this._closureDeallocator;
+        const features = this.exports.swjs_library_features();
+        const librarySupportsWeakRef = (features & 1 /* LibraryFeatures.WeakRefs */) != 0;
+        if (librarySupportsWeakRef) {
+            this._closureDeallocator = new SwiftClosureDeallocator(this.exports);
+        }
+        return this._closureDeallocator;
+    }
+    callHostFunction(host_func_id, line, file, args) {
+        const argc = args.length;
+        const argv = this.exports.swjs_prepare_host_function_call(argc);
+        const memory = this.memory;
+        for (let index = 0; index < args.length; index++) {
+            const argument = args[index];
+            const base = argv + 16 * index;
+            write(argument, base, base + 4, base + 8, false, memory);
+        }
+        let output;
+        // This ref is released by the swjs_call_host_function implementation
+        const callback_func_ref = memory.retain((result) => {
+            output = result;
+        });
+        const alreadyReleased = this.exports.swjs_call_host_function(host_func_id, argv, argc, callback_func_ref);
+        if (alreadyReleased) {
+            throw new Error(`The JSClosure has been already released by Swift side. The closure is created at ${file}:${line}`);
+        }
+        this.exports.swjs_cleanup_host_function_call(argv);
+        return output;
+    }
+    get wasmImports() {
+        let broker = null;
+        const getMessageBroker = (threadChannel) => {
+            var _a;
+            if (broker)
+                return broker;
+            const itcInterface = new ITCInterface(this.memory);
+            const newBroker = new MessageBroker((_a = this.tid) !== null && _a !== void 0 ? _a : -1, threadChannel, {
+                onRequest: (message) => {
+                    let returnValue;
+                    try {
+                        // @ts-ignore
+                        const result = itcInterface[message.data.request.method](...message.data.request.parameters);
+                        returnValue = { ok: true, value: result };
+                    }
+                    catch (error) {
+                        returnValue = { ok: false, error: serializeError(error) };
+                    }
+                    const responseMessage = {
+                        type: "response",
+                        data: {
+                            sourceTid: message.data.sourceTid,
+                            context: message.data.context,
+                            response: returnValue,
+                        },
+                    };
+                    try {
+                        newBroker.reply(responseMessage);
+                    }
+                    catch (error) {
+                        responseMessage.data.response = {
+                            ok: false,
+                            error: serializeError(new TypeError(`Failed to serialize message: ${error}`))
+                        };
+                        newBroker.reply(responseMessage);
+                    }
+                },
+                onResponse: (message) => {
+                    if (message.data.response.ok) {
+                        const object = this.memory.retain(message.data.response.value.object);
+                        this.exports.swjs_receive_response(object, message.data.context);
+                    }
+                    else {
+                        const error = deserializeError(message.data.response.error);
+                        const errorObject = this.memory.retain(error);
+                        this.exports.swjs_receive_error(errorObject, message.data.context);
+                    }
+                }
+            });
+            broker = newBroker;
+            return newBroker;
+        };
+        return {
+            swjs_set_prop: (ref, name, kind, payload1, payload2) => {
+                const memory = this.memory;
+                const obj = memory.getObject(ref);
+                const key = memory.getObject(name);
+                const value = decode(kind, payload1, payload2, memory);
+                obj[key] = value;
+            },
+            swjs_get_prop: (ref, name, payload1_ptr, payload2_ptr) => {
+                const memory = this.memory;
+                const obj = memory.getObject(ref);
+                const key = memory.getObject(name);
+                const result = obj[key];
+                return writeAndReturnKindBits(result, payload1_ptr, payload2_ptr, false, memory);
+            },
+            swjs_set_subscript: (ref, index, kind, payload1, payload2) => {
+                const memory = this.memory;
+                const obj = memory.getObject(ref);
+                const value = decode(kind, payload1, payload2, memory);
+                obj[index] = value;
+            },
+            swjs_get_subscript: (ref, index, payload1_ptr, payload2_ptr) => {
+                const obj = this.memory.getObject(ref);
+                const result = obj[index];
+                return writeAndReturnKindBits(result, payload1_ptr, payload2_ptr, false, this.memory);
+            },
+            swjs_encode_string: (ref, bytes_ptr_result) => {
+                const memory = this.memory;
+                const bytes = this.textEncoder.encode(memory.getObject(ref));
+                const bytes_ptr = memory.retain(bytes);
+                memory.writeUint32(bytes_ptr_result, bytes_ptr);
+                return bytes.length;
+            },
+            swjs_decode_string: (
+            // NOTE: TextDecoder can't decode typed arrays backed by SharedArrayBuffer
+            this.options.sharedMemory == true
+                ? ((bytes_ptr, length) => {
+                    const memory = this.memory;
+                    const bytes = memory
+                        .bytes()
+                        .slice(bytes_ptr, bytes_ptr + length);
+                    const string = this.textDecoder.decode(bytes);
+                    return memory.retain(string);
+                })
+                : ((bytes_ptr, length) => {
+                    const memory = this.memory;
+                    const bytes = memory
+                        .bytes()
+                        .subarray(bytes_ptr, bytes_ptr + length);
+                    const string = this.textDecoder.decode(bytes);
+                    return memory.retain(string);
+                })),
+            swjs_load_string: (ref, buffer) => {
+                const memory = this.memory;
+                const bytes = memory.getObject(ref);
+                memory.writeBytes(buffer, bytes);
+            },
+            swjs_call_function: (ref, argv, argc, payload1_ptr, payload2_ptr) => {
+                const memory = this.memory;
+                const func = memory.getObject(ref);
+                let result = undefined;
+                try {
+                    const args = decodeArray(argv, argc, memory);
+                    result = func(...args);
+                }
+                catch (error) {
+                    return writeAndReturnKindBits(error, payload1_ptr, payload2_ptr, true, this.memory);
+                }
+                return writeAndReturnKindBits(result, payload1_ptr, payload2_ptr, false, this.memory);
+            },
+            swjs_call_function_no_catch: (ref, argv, argc, payload1_ptr, payload2_ptr) => {
+                const memory = this.memory;
+                const func = memory.getObject(ref);
+                const args = decodeArray(argv, argc, memory);
+                const result = func(...args);
+                return writeAndReturnKindBits(result, payload1_ptr, payload2_ptr, false, this.memory);
+            },
+            swjs_call_function_with_this: (obj_ref, func_ref, argv, argc, payload1_ptr, payload2_ptr) => {
+                const memory = this.memory;
+                const obj = memory.getObject(obj_ref);
+                const func = memory.getObject(func_ref);
+                let result;
+                try {
+                    const args = decodeArray(argv, argc, memory);
+                    result = func.apply(obj, args);
+                }
+                catch (error) {
+                    return writeAndReturnKindBits(error, payload1_ptr, payload2_ptr, true, this.memory);
+                }
+                return writeAndReturnKindBits(result, payload1_ptr, payload2_ptr, false, this.memory);
+            },
+            swjs_call_function_with_this_no_catch: (obj_ref, func_ref, argv, argc, payload1_ptr, payload2_ptr) => {
+                const memory = this.memory;
+                const obj = memory.getObject(obj_ref);
+                const func = memory.getObject(func_ref);
+                let result = undefined;
+                const args = decodeArray(argv, argc, memory);
+                result = func.apply(obj, args);
+                return writeAndReturnKindBits(result, payload1_ptr, payload2_ptr, false, this.memory);
+            },
+            swjs_call_new: (ref, argv, argc) => {
+                const memory = this.memory;
+                const constructor = memory.getObject(ref);
+                const args = decodeArray(argv, argc, memory);
+                const instance = new constructor(...args);
+                return this.memory.retain(instance);
+            },
+            swjs_call_throwing_new: (ref, argv, argc, exception_kind_ptr, exception_payload1_ptr, exception_payload2_ptr) => {
+                let memory = this.memory;
+                const constructor = memory.getObject(ref);
+                let result;
+                try {
+                    const args = decodeArray(argv, argc, memory);
+                    result = new constructor(...args);
+                }
+                catch (error) {
+                    write(error, exception_kind_ptr, exception_payload1_ptr, exception_payload2_ptr, true, this.memory);
+                    return -1;
+                }
+                memory = this.memory;
+                write(null, exception_kind_ptr, exception_payload1_ptr, exception_payload2_ptr, false, memory);
+                return memory.retain(result);
+            },
+            swjs_instanceof: (obj_ref, constructor_ref) => {
+                const memory = this.memory;
+                const obj = memory.getObject(obj_ref);
+                const constructor = memory.getObject(constructor_ref);
+                return obj instanceof constructor;
+            },
+            swjs_value_equals: (lhs_ref, rhs_ref) => {
+                const memory = this.memory;
+                const lhs = memory.getObject(lhs_ref);
+                const rhs = memory.getObject(rhs_ref);
+                return lhs == rhs;
+            },
+            swjs_create_function: (host_func_id, line, file) => {
+                var _a;
+                const fileString = this.memory.getObject(file);
+                const func = (...args) => this.callHostFunction(host_func_id, line, fileString, args);
+                const func_ref = this.memory.retain(func);
+                (_a = this.closureDeallocator) === null || _a === void 0 ? void 0 : _a.track(func, func_ref);
+                return func_ref;
+            },
+            swjs_create_typed_array: (constructor_ref, elementsPtr, length) => {
+                const ArrayType = this.memory.getObject(constructor_ref);
+                if (length == 0) {
+                    // The elementsPtr can be unaligned in Swift's Array
+                    // implementation when the array is empty. However,
+                    // TypedArray requires the pointer to be aligned.
+                    // So, we need to create a new empty array without
+                    // using the elementsPtr.
+                    // See https://github.com/swiftwasm/swift/issues/5599
+                    return this.memory.retain(new ArrayType());
+                }
+                const array = new ArrayType(this.memory.rawMemory.buffer, elementsPtr, length);
+                // Call `.slice()` to copy the memory
+                return this.memory.retain(array.slice());
+            },
+            swjs_create_object: () => { return this.memory.retain({}); },
+            swjs_load_typed_array: (ref, buffer) => {
+                const memory = this.memory;
+                const typedArray = memory.getObject(ref);
+                const bytes = new Uint8Array(typedArray.buffer);
+                memory.writeBytes(buffer, bytes);
+            },
+            swjs_release: (ref) => {
+                this.memory.release(ref);
+            },
+            swjs_release_remote: (tid, ref) => {
+                var _a;
+                if (!this.options.threadChannel) {
+                    throw new Error("threadChannel is not set in options given to SwiftRuntime. Please set it to release objects on remote threads.");
+                }
+                const broker = getMessageBroker(this.options.threadChannel);
+                broker.request({
+                    type: "request",
+                    data: {
+                        sourceTid: (_a = this.tid) !== null && _a !== void 0 ? _a : MAIN_THREAD_TID,
+                        targetTid: tid,
+                        context: 0,
+                        request: {
+                            method: "release",
+                            parameters: [ref],
+                        }
+                    }
+                });
+            },
+            swjs_i64_to_bigint: (value, signed) => {
+                return this.memory.retain(signed ? value : BigInt.asUintN(64, value));
+            },
+            swjs_bigint_to_i64: (ref, signed) => {
+                const object = this.memory.getObject(ref);
+                if (typeof object !== "bigint") {
+                    throw new Error(`Expected a BigInt, but got ${typeof object}`);
+                }
+                if (signed) {
+                    return object;
+                }
+                else {
+                    if (object < BigInt(0)) {
+                        return BigInt(0);
+                    }
+                    return BigInt.asIntN(64, object);
+                }
+            },
+            swjs_i64_to_bigint_slow: (lower, upper, signed) => {
+                const value = BigInt.asUintN(32, BigInt(lower)) +
+                    (BigInt.asUintN(32, BigInt(upper)) << BigInt(32));
+                return this.memory.retain(signed ? BigInt.asIntN(64, value) : BigInt.asUintN(64, value));
+            },
+            swjs_unsafe_event_loop_yield: () => {
+                throw new UnsafeEventLoopYield();
+            },
+            swjs_send_job_to_main_thread: (unowned_job) => {
+                this.postMessageToMainThread({ type: "job", data: unowned_job });
+            },
+            swjs_listen_message_from_main_thread: () => {
+                const threadChannel = this.options.threadChannel;
+                if (!(threadChannel && "listenMessageFromMainThread" in threadChannel)) {
+                    throw new Error("listenMessageFromMainThread is not set in options given to SwiftRuntime. Please set it to listen to wake events from the main thread.");
+                }
+                const broker = getMessageBroker(threadChannel);
+                threadChannel.listenMessageFromMainThread((message) => {
+                    switch (message.type) {
+                        case "wake":
+                            this.exports.swjs_wake_worker_thread();
+                            break;
+                        case "request": {
+                            broker.onReceivingRequest(message);
+                            break;
+                        }
+                        case "response": {
+                            broker.onReceivingResponse(message);
+                            break;
+                        }
+                        default:
+                            const unknownMessage = message;
+                            throw new Error(`Unknown message type: ${unknownMessage}`);
+                    }
+                });
+            },
+            swjs_wake_up_worker_thread: (tid) => {
+                this.postMessageToWorkerThread(tid, { type: "wake" });
+            },
+            swjs_listen_message_from_worker_thread: (tid) => {
+                const threadChannel = this.options.threadChannel;
+                if (!(threadChannel && "listenMessageFromWorkerThread" in threadChannel)) {
+                    throw new Error("listenMessageFromWorkerThread is not set in options given to SwiftRuntime. Please set it to listen to jobs from worker threads.");
+                }
+                const broker = getMessageBroker(threadChannel);
+                threadChannel.listenMessageFromWorkerThread(tid, (message) => {
+                    switch (message.type) {
+                        case "job":
+                            this.exports.swjs_enqueue_main_job_from_worker(message.data);
+                            break;
+                        case "request": {
+                            broker.onReceivingRequest(message);
+                            break;
+                        }
+                        case "response": {
+                            broker.onReceivingResponse(message);
+                            break;
+                        }
+                        default:
+                            const unknownMessage = message;
+                            throw new Error(`Unknown message type: ${unknownMessage}`);
+                    }
+                });
+            },
+            swjs_terminate_worker_thread: (tid) => {
+                var _a;
+                const threadChannel = this.options.threadChannel;
+                if (threadChannel && "terminateWorkerThread" in threadChannel) {
+                    (_a = threadChannel.terminateWorkerThread) === null || _a === void 0 ? void 0 : _a.call(threadChannel, tid);
+                } // Otherwise, just ignore the termination request
+            },
+            swjs_get_worker_thread_id: () => {
+                // Main thread's tid is always -1
+                return this.tid || -1;
+            },
+            swjs_request_sending_object: (sending_object, transferring_objects, transferring_objects_count, object_source_tid, sending_context) => {
+                var _a;
+                if (!this.options.threadChannel) {
+                    throw new Error("threadChannel is not set in options given to SwiftRuntime. Please set it to request transferring objects.");
+                }
+                const broker = getMessageBroker(this.options.threadChannel);
+                const memory = this.memory;
+                const transferringObjects = decodeObjectRefs(transferring_objects, transferring_objects_count, memory);
+                broker.request({
+                    type: "request",
+                    data: {
+                        sourceTid: (_a = this.tid) !== null && _a !== void 0 ? _a : MAIN_THREAD_TID,
+                        targetTid: object_source_tid,
+                        context: sending_context,
+                        request: {
+                            method: "send",
+                            parameters: [sending_object, transferringObjects, sending_context],
+                        }
+                    }
+                });
+            },
+            swjs_request_sending_objects: (sending_objects, sending_objects_count, transferring_objects, transferring_objects_count, object_source_tid, sending_context) => {
+                var _a;
+                if (!this.options.threadChannel) {
+                    throw new Error("threadChannel is not set in options given to SwiftRuntime. Please set it to request transferring objects.");
+                }
+                const broker = getMessageBroker(this.options.threadChannel);
+                const memory = this.memory;
+                const sendingObjects = decodeObjectRefs(sending_objects, sending_objects_count, memory);
+                const transferringObjects = decodeObjectRefs(transferring_objects, transferring_objects_count, memory);
+                broker.request({
+                    type: "request",
+                    data: {
+                        sourceTid: (_a = this.tid) !== null && _a !== void 0 ? _a : MAIN_THREAD_TID,
+                        targetTid: object_source_tid,
+                        context: sending_context,
+                        request: {
+                            method: "sendObjects",
+                            parameters: [sendingObjects, transferringObjects, sending_context],
+                        }
+                    }
+                });
+            },
+        };
+    }
+    postMessageToMainThread(message, transfer = []) {
+        const threadChannel = this.options.threadChannel;
+        if (!(threadChannel && "postMessageToMainThread" in threadChannel)) {
+            throw new Error("postMessageToMainThread is not set in options given to SwiftRuntime. Please set it to send messages to the main thread.");
+        }
+        threadChannel.postMessageToMainThread(message, transfer);
+    }
+    postMessageToWorkerThread(tid, message, transfer = []) {
+        const threadChannel = this.options.threadChannel;
+        if (!(threadChannel && "postMessageToWorkerThread" in threadChannel)) {
+            throw new Error("postMessageToWorkerThread is not set in options given to SwiftRuntime. Please set it to send messages to worker threads.");
+        }
+        threadChannel.postMessageToWorkerThread(tid, message, transfer);
+    }
+}
+/// This error is thrown when yielding event loop control from `swift_task_asyncMainDrainQueue`
+/// to JavaScript. This is usually thrown when:
+/// - The entry point of the Swift program is `func main() async`
+/// - The Swift Concurrency's global executor is hooked by `JavaScriptEventLoop.installGlobalExecutor()`
+/// - Calling exported `main` or `__main_argc_argv` function from JavaScript
+///
+/// This exception must be caught by the caller of the exported function and the caller should
+/// catch this exception and just ignore it.
+///
+/// FAQ: Why this error is thrown?
+/// This error is thrown to unwind the call stack of the Swift program and return the control to
+/// the JavaScript side. Otherwise, the `swift_task_asyncMainDrainQueue` ends up with `abort()`
+/// because the event loop expects `exit()` call before the end of the event loop.
+class UnsafeEventLoopYield extends Error {
+}
+
+export { SwiftRuntime };
diff --git a/Plugins/PackageToJS/Templates/test.d.ts b/Plugins/PackageToJS/Templates/test.d.ts
index b3bbe54dd..2968f6dd9 100644
--- a/Plugins/PackageToJS/Templates/test.d.ts
+++ b/Plugins/PackageToJS/Templates/test.d.ts
@@ -1,12 +1,12 @@
 import type { InstantiateOptions, instantiate } from "./instantiate";
 
-export async function testBrowser(
+export function testBrowser(
     options: {
         preludeScript?: string,
         args?: string[],
     }
 ): Promise
 
-export async function testBrowserInPage(
+export function testBrowserInPage(
     options: InstantiateOptions
 ): ReturnType
diff --git a/Plugins/PackageToJS/Templates/test.js b/Plugins/PackageToJS/Templates/test.js
index 8c4432492..b44b0d6e7 100644
--- a/Plugins/PackageToJS/Templates/test.js
+++ b/Plugins/PackageToJS/Templates/test.js
@@ -171,8 +171,8 @@ export async function testBrowserInPage(options, processInfo) {
         // Instantiate the WebAssembly file
         return await instantiate({
             ...options,
-            addToCoreImports: (imports) => {
-                options.addToCoreImports?.(imports);
+            addToCoreImports: (imports, context) => {
+                options.addToCoreImports?.(imports, context);
                 imports["wasi_snapshot_preview1"]["proc_exit"] = (code) => {
                     exitTest(code);
                     throw new ExitError(code);
diff --git a/Plugins/PackageToJS/Templates/tsconfig.json b/Plugins/PackageToJS/Templates/tsconfig.json
new file mode 100644
index 000000000..ac3a2b01e
--- /dev/null
+++ b/Plugins/PackageToJS/Templates/tsconfig.json
@@ -0,0 +1,10 @@
+{
+    "compilerOptions": {
+        "module": "esnext",
+        "noEmit": true,
+        "allowJs": true,
+        "skipLibCheck": true,
+        "moduleResolution": "node"
+    },
+    "include": ["**/*.d.ts", "**/*.js"]
+}
diff --git a/Plugins/PackageToJS/Tests/ExampleTests.swift b/Plugins/PackageToJS/Tests/ExampleTests.swift
index 508062297..d860a685f 100644
--- a/Plugins/PackageToJS/Tests/ExampleTests.swift
+++ b/Plugins/PackageToJS/Tests/ExampleTests.swift
@@ -54,7 +54,10 @@ extension Trait where Self == ConditionTrait {
 
     static func copyRepository(to destination: URL) throws {
         try FileManager.default.createDirectory(
-            atPath: destination.path, withIntermediateDirectories: true, attributes: nil)
+            atPath: destination.path,
+            withIntermediateDirectories: true,
+            attributes: nil
+        )
         let ignore = [
             ".git",
             ".vscode",
@@ -70,6 +73,24 @@ extension Trait where Self == ConditionTrait {
                 enumerator.skipDescendants()
                 continue
             }
+
+            // Copy symbolic links
+            if let resourceValues = try? sourcePath.resourceValues(forKeys: [.isSymbolicLinkKey]),
+                resourceValues.isSymbolicLink == true
+            {
+                try FileManager.default.createDirectory(
+                    at: destinationPath.deletingLastPathComponent(),
+                    withIntermediateDirectories: true,
+                    attributes: nil
+                )
+                let linkDestination = try! FileManager.default.destinationOfSymbolicLink(atPath: sourcePath.path)
+                try FileManager.default.createSymbolicLink(
+                    atPath: destinationPath.path,
+                    withDestinationPath: linkDestination
+                )
+                continue
+            }
+
             // Skip directories
             var isDirectory: ObjCBool = false
             if FileManager.default.fileExists(atPath: sourcePath.path, isDirectory: &isDirectory) {
@@ -81,7 +102,9 @@ extension Trait where Self == ConditionTrait {
             do {
                 try FileManager.default.createDirectory(
                     at: destinationPath.deletingLastPathComponent(),
-                    withIntermediateDirectories: true, attributes: nil)
+                    withIntermediateDirectories: true,
+                    attributes: nil
+                )
                 try FileManager.default.copyItem(at: sourcePath, to: destinationPath)
             } catch {
                 print("Failed to copy \(sourcePath) to \(destinationPath): \(error)")
@@ -90,18 +113,20 @@ extension Trait where Self == ConditionTrait {
         }
     }
 
+    typealias RunProcess = (_ executableURL: URL, _ args: [String], _ env: [String: String]) throws -> Void
     typealias RunSwift = (_ args: [String], _ env: [String: String]) throws -> Void
 
-    func withPackage(at path: String, body: (URL, _ runSwift: RunSwift) throws -> Void) throws {
+    func withPackage(
+        at path: String,
+        assertTerminationStatus: (Int32) -> Bool = { $0 == 0 },
+        body: @escaping (URL, _ runProcess: RunProcess, _ runSwift: RunSwift) throws -> Void
+    ) throws {
         try withTemporaryDirectory { tempDir, retain in
             let destination = tempDir.appending(path: Self.repoPath.lastPathComponent)
             try Self.copyRepository(to: destination)
-            try body(destination.appending(path: path)) { args, env in
+            func runProcess(_ executableURL: URL, _ args: [String], _ env: [String: String]) throws {
                 let process = Process()
-                process.executableURL = URL(
-                    fileURLWithPath: "swift",
-                    relativeTo: URL(
-                        fileURLWithPath: try #require(Self.getSwiftPath())))
+                process.executableURL = executableURL
                 process.arguments = args
                 process.currentDirectoryURL = destination.appending(path: path)
                 process.environment = ProcessInfo.processInfo.environment.merging(env) { _, new in
@@ -116,11 +141,11 @@ extension Trait where Self == ConditionTrait {
 
                 try process.run()
                 process.waitUntilExit()
-                if process.terminationStatus != 0 {
+                if !assertTerminationStatus(process.terminationStatus) {
                     retain = true
                 }
                 try #require(
-                    process.terminationStatus == 0,
+                    assertTerminationStatus(process.terminationStatus),
                     """
                     Swift package should build successfully, check \(destination.appending(path: path).path) for details
                     stdout: \(stdoutPath.path)
@@ -131,38 +156,58 @@ extension Trait where Self == ConditionTrait {
                     """
                 )
             }
+            func runSwift(_ args: [String], _ env: [String: String]) throws {
+                let swiftExecutable = URL(
+                    fileURLWithPath: "swift",
+                    relativeTo: URL(fileURLWithPath: try #require(Self.getSwiftPath()))
+                )
+                try runProcess(swiftExecutable, args, env)
+            }
+            try body(destination.appending(path: path), runProcess, runSwift)
         }
     }
 
     @Test(.requireSwiftSDK)
     func basic() throws {
         let swiftSDKID = try #require(Self.getSwiftSDKID())
-        try withPackage(at: "Examples/Basic") { packageDir, runSwift in
+        try withPackage(at: "Examples/Basic") { packageDir, _, runSwift in
             try runSwift(["package", "--swift-sdk", swiftSDKID, "js"], [:])
             try runSwift(["package", "--swift-sdk", swiftSDKID, "js", "--debug-info-format", "dwarf"], [:])
             try runSwift(["package", "--swift-sdk", swiftSDKID, "js", "--debug-info-format", "name"], [:])
-            try runSwift(["package", "--swift-sdk", swiftSDKID, "-Xswiftc", "-DJAVASCRIPTKIT_WITHOUT_WEAKREFS", "js"], [:])
+            try runSwift(
+                ["package", "--swift-sdk", swiftSDKID, "-Xswiftc", "-DJAVASCRIPTKIT_WITHOUT_WEAKREFS", "js"],
+                [:]
+            )
         }
     }
 
     @Test(.requireSwiftSDK)
     func testing() throws {
         let swiftSDKID = try #require(Self.getSwiftSDKID())
-        try withPackage(at: "Examples/Testing") { packageDir, runSwift in
+        try withPackage(at: "Examples/Testing") { packageDir, runProcess, runSwift in
+            try runProcess(which("npm"), ["install"], [:])
+            try runProcess(which("npx"), ["playwright", "install", "chromium-headless-shell"], [:])
+
             try runSwift(["package", "--swift-sdk", swiftSDKID, "js", "test"], [:])
             try withTemporaryDirectory(body: { tempDir, _ in
                 let scriptContent = """
-                const fs = require('fs');
-                const path = require('path');
-                const scriptPath = path.join(__dirname, 'test.txt');
-                fs.writeFileSync(scriptPath, 'Hello, world!');
-                """
+                    const fs = require('fs');
+                    const path = require('path');
+                    const scriptPath = path.join(__dirname, 'test.txt');
+                    fs.writeFileSync(scriptPath, 'Hello, world!');
+                    """
                 try scriptContent.write(to: tempDir.appending(path: "script.js"), atomically: true, encoding: .utf8)
                 let scriptPath = tempDir.appending(path: "script.js")
-                try runSwift(["package", "--swift-sdk", swiftSDKID, "js", "test", "-Xnode=--require=\(scriptPath.path)"], [:])
+                try runSwift(
+                    ["package", "--swift-sdk", swiftSDKID, "js", "test", "-Xnode=--require=\(scriptPath.path)"],
+                    [:]
+                )
                 let testPath = tempDir.appending(path: "test.txt")
                 try #require(FileManager.default.fileExists(atPath: testPath.path), "test.txt should exist")
-                try #require(try String(contentsOf: testPath, encoding: .utf8) == "Hello, world!", "test.txt should be created by the script")
+                try #require(
+                    try String(contentsOf: testPath, encoding: .utf8) == "Hello, world!",
+                    "test.txt should be created by the script"
+                )
             })
             try runSwift(["package", "--swift-sdk", swiftSDKID, "js", "test", "--environment", "browser"], [:])
         }
@@ -173,20 +218,26 @@ extension Trait where Self == ConditionTrait {
     func testingWithCoverage() throws {
         let swiftSDKID = try #require(Self.getSwiftSDKID())
         let swiftPath = try #require(Self.getSwiftPath())
-        try withPackage(at: "Examples/Testing") { packageDir, runSwift in
-            try runSwift(["package", "--swift-sdk", swiftSDKID, "js", "test", "--enable-code-coverage"], [
-                "LLVM_PROFDATA_PATH": URL(fileURLWithPath: swiftPath).appending(path: "llvm-profdata").path
-            ])
+        try withPackage(at: "Examples/Testing") { packageDir, runProcess, runSwift in
+            try runSwift(
+                ["package", "--swift-sdk", swiftSDKID, "js", "test", "--enable-code-coverage"],
+                [
+                    "LLVM_PROFDATA_PATH": URL(fileURLWithPath: swiftPath).appending(path: "llvm-profdata").path
+                ]
+            )
             do {
+                let profdata = packageDir.appending(
+                    path: ".build/plugins/PackageToJS/outputs/PackageTests/default.profdata"
+                )
+                let possibleWasmPaths = ["CounterPackageTests.xctest.wasm", "CounterPackageTests.wasm"].map {
+                    packageDir.appending(path: ".build/plugins/PackageToJS/outputs/PackageTests/\($0)")
+                }
+                let wasmPath = try #require(
+                    possibleWasmPaths.first(where: { FileManager.default.fileExists(atPath: $0.path) }),
+                    "No wasm file found"
+                )
                 let llvmCov = try which("llvm-cov")
-                let process = Process()
-                process.executableURL = llvmCov
-                let profdata = packageDir.appending(path: ".build/plugins/PackageToJS/outputs/PackageTests/default.profdata")
-                let wasm = packageDir.appending(path: ".build/plugins/PackageToJS/outputs/PackageTests/TestingPackageTests.wasm")
-                process.arguments = ["report", "-instr-profile", profdata.path, wasm.path]
-                process.standardOutput = FileHandle.nullDevice
-                try process.run()
-                process.waitUntilExit()
+                try runProcess(llvmCov, ["report", "-instr-profile", profdata.path, wasmPath.path], [:])
             }
         }
     }
@@ -195,7 +246,7 @@ extension Trait where Self == ConditionTrait {
     @Test(.requireSwiftSDK(triple: "wasm32-unknown-wasip1-threads"))
     func multithreading() throws {
         let swiftSDKID = try #require(Self.getSwiftSDKID())
-        try withPackage(at: "Examples/Multithreading") { packageDir, runSwift in
+        try withPackage(at: "Examples/Multithreading") { packageDir, _, runSwift in
             try runSwift(["package", "--swift-sdk", swiftSDKID, "js"], [:])
         }
     }
@@ -203,7 +254,7 @@ extension Trait where Self == ConditionTrait {
     @Test(.requireSwiftSDK(triple: "wasm32-unknown-wasip1-threads"))
     func offscreenCanvas() throws {
         let swiftSDKID = try #require(Self.getSwiftSDKID())
-        try withPackage(at: "Examples/OffscrenCanvas") { packageDir, runSwift in
+        try withPackage(at: "Examples/OffscrenCanvas") { packageDir, _, runSwift in
             try runSwift(["package", "--swift-sdk", swiftSDKID, "js"], [:])
         }
     }
@@ -211,19 +262,44 @@ extension Trait where Self == ConditionTrait {
     @Test(.requireSwiftSDK(triple: "wasm32-unknown-wasip1-threads"))
     func actorOnWebWorker() throws {
         let swiftSDKID = try #require(Self.getSwiftSDKID())
-        try withPackage(at: "Examples/ActorOnWebWorker") { packageDir, runSwift in
+        try withPackage(at: "Examples/ActorOnWebWorker") { packageDir, _, runSwift in
             try runSwift(["package", "--swift-sdk", swiftSDKID, "js"], [:])
         }
     }
 
     @Test(.requireEmbeddedSwift) func embedded() throws {
-        try withPackage(at: "Examples/Embedded") { packageDir, runSwift in
+        try withPackage(at: "Examples/Embedded") { packageDir, _, runSwift in
             try runSwift(
-                ["package", "--triple", "wasm32-unknown-none-wasm", "-c", "release", "js"],
+                ["package", "--triple", "wasm32-unknown-none-wasm", "js", "-c", "release"],
                 [
                     "JAVASCRIPTKIT_EXPERIMENTAL_EMBEDDED_WASM": "true"
                 ]
             )
         }
     }
+
+    @Test(.requireSwiftSDK)
+    func continuationLeakInTest_XCTest() throws {
+        let swiftSDKID = try #require(Self.getSwiftSDKID())
+        try withPackage(
+            at: "Plugins/PackageToJS/Fixtures/ContinuationLeakInTest/XCTest",
+            assertTerminationStatus: { $0 != 0 }
+        ) { packageDir, _, runSwift in
+            try runSwift(["package", "--disable-sandbox", "--swift-sdk", swiftSDKID, "js", "test"], [:])
+        }
+    }
+
+    #if compiler(>=6.1)
+    // TODO: Remove triple restriction once swift-testing is shipped in p1-threads SDK
+    @Test(.requireSwiftSDK(triple: "wasm32-unknown-wasi"))
+    func continuationLeakInTest_SwiftTesting() throws {
+        let swiftSDKID = try #require(Self.getSwiftSDKID())
+        try withPackage(
+            at: "Plugins/PackageToJS/Fixtures/ContinuationLeakInTest/SwiftTesting",
+            assertTerminationStatus: { $0 != 0 }
+        ) { packageDir, _, runSwift in
+            try runSwift(["package", "--disable-sandbox", "--swift-sdk", swiftSDKID, "js", "test"], [:])
+        }
+    }
+    #endif
 }
diff --git a/Plugins/PackageToJS/Tests/MiniMakeTests.swift b/Plugins/PackageToJS/Tests/MiniMakeTests.swift
index b15a87607..c0bba29c7 100644
--- a/Plugins/PackageToJS/Tests/MiniMakeTests.swift
+++ b/Plugins/PackageToJS/Tests/MiniMakeTests.swift
@@ -14,9 +14,12 @@ import Testing
                 try "Hello".write(toFile: $1.resolve(path: $0.output).path, atomically: true, encoding: .utf8)
             }
 
-            try make.build(output: task, scope: MiniMake.VariableScope(variables: [
-                "OUTPUT": tempDir.path,
-            ]))
+            try make.build(
+                output: task,
+                scope: MiniMake.VariableScope(variables: [
+                    "OUTPUT": tempDir.path
+                ])
+            )
             let content = try String(contentsOfFile: tempDir.appendingPathComponent("output.txt").path, encoding: .utf8)
             #expect(content == "Hello")
         }
@@ -28,7 +31,7 @@ import Testing
             var make = MiniMake(printProgress: { _, _ in })
             let prefix = BuildPath(prefix: "PREFIX")
             let scope = MiniMake.VariableScope(variables: [
-                "PREFIX": tempDir.path,
+                "PREFIX": tempDir.path
             ])
             let input = prefix.appending(path: "input.txt")
             let intermediate = prefix.appending(path: "intermediate.txt")
@@ -39,15 +42,23 @@ import Testing
             let intermediateTask = make.addTask(inputFiles: [input], output: intermediate) { task, outputURL in
                 let content = try String(contentsOfFile: scope.resolve(path: task.inputs[0]).path, encoding: .utf8)
                 try (content + " processed").write(
-                    toFile: scope.resolve(path: task.output).path, atomically: true, encoding: .utf8)
+                    toFile: scope.resolve(path: task.output).path,
+                    atomically: true,
+                    encoding: .utf8
+                )
             }
 
             let finalTask = make.addTask(
-                inputFiles: [intermediate], inputTasks: [intermediateTask], output: output
+                inputFiles: [intermediate],
+                inputTasks: [intermediateTask],
+                output: output
             ) { task, scope in
                 let content = try String(contentsOfFile: scope.resolve(path: task.inputs[0]).path, encoding: .utf8)
                 try (content + " final").write(
-                    toFile: scope.resolve(path: task.output).path, atomically: true, encoding: .utf8)
+                    toFile: scope.resolve(path: task.output).path,
+                    atomically: true,
+                    encoding: .utf8
+                )
             }
 
             try make.build(output: finalTask, scope: scope)
@@ -67,11 +78,15 @@ import Testing
 
             let task = make.addTask(output: outputPath, attributes: [.phony]) { task, scope in
                 buildCount += 1
-                try String(buildCount).write(toFile: scope.resolve(path: task.output).path, atomically: true, encoding: .utf8)
+                try String(buildCount).write(
+                    toFile: scope.resolve(path: task.output).path,
+                    atomically: true,
+                    encoding: .utf8
+                )
             }
 
             let scope = MiniMake.VariableScope(variables: [
-                "OUTPUT": tempDir.path,
+                "OUTPUT": tempDir.path
             ])
             try make.build(output: task, scope: scope)
             try make.build(output: task, scope: scope)
@@ -102,7 +117,7 @@ import Testing
             var make = MiniMake(printProgress: { _, _ in })
             let prefix = BuildPath(prefix: "PREFIX")
             let scope = MiniMake.VariableScope(variables: [
-                "PREFIX": tempDir.path,
+                "PREFIX": tempDir.path
             ])
             let input = prefix.appending(path: "input.txt")
             let output = prefix.appending(path: "output.txt")
@@ -142,7 +157,7 @@ import Testing
             )
             let prefix = BuildPath(prefix: "PREFIX")
             let scope = MiniMake.VariableScope(variables: [
-                "PREFIX": tempDir.path,
+                "PREFIX": tempDir.path
             ])
             let silentOutputPath = prefix.appending(path: "silent.txt")
             let silentTask = make.addTask(output: silentOutputPath, attributes: [.silent]) { task, scope in
@@ -150,14 +165,21 @@ import Testing
             }
             let finalOutputPath = prefix.appending(path: "output.txt")
             let task = make.addTask(
-                inputTasks: [silentTask], output: finalOutputPath
+                inputTasks: [silentTask],
+                output: finalOutputPath
             ) { task, scope in
                 try "Hello".write(toFile: scope.resolve(path: task.output).path, atomically: true, encoding: .utf8)
             }
 
             try make.build(output: task, scope: scope)
-            #expect(FileManager.default.fileExists(atPath: scope.resolve(path: silentOutputPath).path), "Silent task should still create output file")
-            #expect(FileManager.default.fileExists(atPath: scope.resolve(path: finalOutputPath).path), "Final task should create output file")
+            #expect(
+                FileManager.default.fileExists(atPath: scope.resolve(path: silentOutputPath).path),
+                "Silent task should still create output file"
+            )
+            #expect(
+                FileManager.default.fileExists(atPath: scope.resolve(path: finalOutputPath).path),
+                "Final task should create output file"
+            )
             try #require(messages.count == 1, "Should print progress for the final task")
             #expect(messages[0] == ("$PREFIX/output.txt", 1, 0, "\u{1B}[32mbuilding\u{1B}[0m"))
         }
@@ -170,7 +192,7 @@ import Testing
             var make = MiniMake(printProgress: { _, _ in })
             let prefix = BuildPath(prefix: "PREFIX")
             let scope = MiniMake.VariableScope(variables: [
-                "PREFIX": tempDir.path,
+                "PREFIX": tempDir.path
             ])
             let output = prefix.appending(path: "error.txt")
 
@@ -190,7 +212,7 @@ import Testing
             var make = MiniMake(printProgress: { _, _ in })
             let prefix = BuildPath(prefix: "PREFIX")
             let scope = MiniMake.VariableScope(variables: [
-                "PREFIX": tempDir.path,
+                "PREFIX": tempDir.path
             ])
             let outputs = [
                 prefix.appending(path: "clean1.txt"),
@@ -200,7 +222,11 @@ import Testing
             // Create tasks and build them
             let tasks = outputs.map { output in
                 make.addTask(output: output) { task, scope in
-                    try "Content".write(toFile: scope.resolve(path: task.output).path, atomically: true, encoding: .utf8)
+                    try "Content".write(
+                        toFile: scope.resolve(path: task.output).path,
+                        atomically: true,
+                        encoding: .utf8
+                    )
                 }
             }
 
@@ -212,7 +238,8 @@ import Testing
             for output in outputs {
                 #expect(
                     FileManager.default.fileExists(atPath: scope.resolve(path: output).path),
-                    "Output file should exist before cleanup")
+                    "Output file should exist before cleanup"
+                )
             }
 
             // Clean everything
@@ -222,7 +249,8 @@ import Testing
             for output in outputs {
                 #expect(
                     !FileManager.default.fileExists(atPath: scope.resolve(path: output).path),
-                    "Output file should not exist after cleanup")
+                    "Output file should not exist after cleanup"
+                )
             }
         }
     }
diff --git a/Plugins/PackageToJS/Tests/PackagingPlannerTests.swift b/Plugins/PackageToJS/Tests/PackagingPlannerTests.swift
index 6392ca664..03fc4c9cc 100644
--- a/Plugins/PackageToJS/Tests/PackagingPlannerTests.swift
+++ b/Plugins/PackageToJS/Tests/PackagingPlannerTests.swift
@@ -15,12 +15,15 @@ import Testing
 
         func wasmOpt(_ arguments: [String], input: String, output: String) throws {
             try FileManager.default.copyItem(
-                at: URL(fileURLWithPath: input), to: URL(fileURLWithPath: output))
+                at: URL(fileURLWithPath: input),
+                to: URL(fileURLWithPath: output)
+            )
         }
     }
 
     func snapshotBuildPlan(
-        filePath: String = #filePath, function: String = #function,
+        filePath: String = #filePath,
+        function: String = #function,
         sourceLocation: SourceLocation = #_sourceLocation,
         variant: String? = nil,
         body: (inout MiniMake) throws -> MiniMake.TaskKey
@@ -29,8 +32,11 @@ import Testing
         let rootKey = try body(&make)
         let fingerprint = try make.computeFingerprint(root: rootKey, prettyPrint: true)
         try assertSnapshot(
-            filePath: filePath, function: function, sourceLocation: sourceLocation,
-            variant: variant, input: fingerprint
+            filePath: filePath,
+            function: function,
+            sourceLocation: sourceLocation,
+            variant: variant,
+            input: fingerprint
         )
     }
 
@@ -39,11 +45,19 @@ import Testing
     @Test(arguments: [
         (variant: "debug", configuration: "debug", noOptimize: false, debugInfoFormat: DebugInfoFormat.none),
         (variant: "release", configuration: "release", noOptimize: false, debugInfoFormat: DebugInfoFormat.none),
-        (variant: "release_no_optimize", configuration: "release", noOptimize: true, debugInfoFormat: DebugInfoFormat.none),
+        (
+            variant: "release_no_optimize", configuration: "release", noOptimize: true,
+            debugInfoFormat: DebugInfoFormat.none
+        ),
         (variant: "release_dwarf", configuration: "release", noOptimize: false, debugInfoFormat: DebugInfoFormat.dwarf),
         (variant: "release_name", configuration: "release", noOptimize: false, debugInfoFormat: DebugInfoFormat.name),
     ])
-    func planBuild(variant: String, configuration: String, noOptimize: Bool, debugInfoFormat: PackageToJS.DebugInfoFormat) throws {
+    func planBuild(
+        variant: String,
+        configuration: String,
+        noOptimize: Bool,
+        debugInfoFormat: PackageToJS.DebugInfoFormat
+    ) throws {
         let options = PackageToJS.PackageOptions()
         let system = TestPackagingSystem()
         let planner = PackagingPlanner(
@@ -51,6 +65,8 @@ import Testing
             packageId: "test",
             intermediatesDir: BuildPath(prefix: "INTERMEDIATES"),
             selfPackageDir: BuildPath(prefix: "SELF_PACKAGE"),
+            exportedSkeletons: [],
+            importedSkeletons: [],
             outputDir: BuildPath(prefix: "OUTPUT"),
             wasmProductArtifact: BuildPath(prefix: "WASM_PRODUCT_ARTIFACT"),
             wasmFilename: "main.wasm",
@@ -80,6 +96,8 @@ import Testing
             packageId: "test",
             intermediatesDir: BuildPath(prefix: "INTERMEDIATES"),
             selfPackageDir: BuildPath(prefix: "SELF_PACKAGE"),
+            exportedSkeletons: [],
+            importedSkeletons: [],
             outputDir: BuildPath(prefix: "OUTPUT"),
             wasmProductArtifact: BuildPath(prefix: "WASM_PRODUCT_ARTIFACT"),
             wasmFilename: "main.wasm",
@@ -88,7 +106,7 @@ import Testing
             selfPath: BuildPath(prefix: "PLANNER_SOURCE_PATH"),
             system: system
         )
-        try snapshotBuildPlan() { make in
+        try snapshotBuildPlan { make in
             let (root, binDir) = try planner.planTestBuild(make: &make)
             #expect(binDir.description == "$OUTPUT/bin")
             return root
diff --git a/Plugins/PackageToJS/Tests/PreprocessTests.swift b/Plugins/PackageToJS/Tests/PreprocessTests.swift
index 9ebb7a161..6e7e4a1b9 100644
--- a/Plugins/PackageToJS/Tests/PreprocessTests.swift
+++ b/Plugins/PackageToJS/Tests/PreprocessTests.swift
@@ -1,15 +1,16 @@
 import Testing
+
 @testable import PackageToJS
 
 @Suite struct PreprocessTests {
     @Test func thenBlock() throws {
         let source = """
-        /* #if FOO */
-        console.log("FOO");
-        /* #else */
-        console.log("BAR");
-        /* #endif */
-        """
+            /* #if FOO */
+            console.log("FOO");
+            /* #else */
+            console.log("BAR");
+            /* #endif */
+            """
         let options = PreprocessOptions(conditions: ["FOO": true])
         let result = try preprocess(source: source, options: options)
         #expect(result == "console.log(\"FOO\");\n")
@@ -17,12 +18,12 @@ import Testing
 
     @Test func elseBlock() throws {
         let source = """
-        /* #if FOO */
-        console.log("FOO");
-        /* #else */
-        console.log("BAR");
-        /* #endif */
-        """
+            /* #if FOO */
+            console.log("FOO");
+            /* #else */
+            console.log("BAR");
+            /* #endif */
+            """
         let options = PreprocessOptions(conditions: ["FOO": false])
         let result = try preprocess(source: source, options: options)
         #expect(result == "console.log(\"BAR\");\n")
@@ -30,8 +31,8 @@ import Testing
 
     @Test func onelineIf() throws {
         let source = """
-        /* #if FOO */console.log("FOO");/* #endif */
-        """
+            /* #if FOO */console.log("FOO");/* #endif */
+            """
         let options = PreprocessOptions(conditions: ["FOO": true])
         let result = try preprocess(source: source, options: options)
         #expect(result == "console.log(\"FOO\");")
@@ -39,9 +40,9 @@ import Testing
 
     @Test func undefinedVariable() throws {
         let source = """
-        /* #if FOO */
-        /* #endif */
-        """
+            /* #if FOO */
+            /* #endif */
+            """
         let options = PreprocessOptions(conditions: [:])
         #expect(throws: Error.self) {
             try preprocess(source: source, options: options)
@@ -57,8 +58,8 @@ import Testing
 
     @Test func missingEndOfDirective() throws {
         let source = """
-        /* #if FOO
-        """
+            /* #if FOO
+            """
         #expect(throws: Error.self) {
             try preprocess(source: source, options: PreprocessOptions())
         }
@@ -72,17 +73,17 @@ import Testing
     ])
     func nestedIfDirectives(foo: Bool, bar: Bool, expected: String) throws {
         let source = """
-        /* #if FOO */
-        console.log("FOO");
-        /* #if BAR */
-        console.log("FOO & BAR");
-        /* #else */
-        console.log("FOO & !BAR");
-        /* #endif */
-        /* #else */
-        console.log("!FOO");
-        /* #endif */
-        """
+            /* #if FOO */
+            console.log("FOO");
+            /* #if BAR */
+            console.log("FOO & BAR");
+            /* #else */
+            console.log("FOO & !BAR");
+            /* #endif */
+            /* #else */
+            console.log("!FOO");
+            /* #endif */
+            """
         let options = PreprocessOptions(conditions: ["FOO": foo, "BAR": bar])
         let result = try preprocess(source: source, options: options)
         #expect(result == expected)
@@ -90,26 +91,28 @@ import Testing
 
     @Test func multipleSubstitutions() throws {
         let source = """
-        const name = "@NAME@";
-        const version = "@VERSION@";
-        """
+            const name = "@NAME@";
+            const version = "@VERSION@";
+            """
         let options = PreprocessOptions(substitutions: [
             "NAME": "MyApp",
-            "VERSION": "1.0.0"
+            "VERSION": "1.0.0",
         ])
         let result = try preprocess(source: source, options: options)
-        #expect(result == """
-        const name = "MyApp";
-        const version = "1.0.0";
-        """)
+        #expect(
+            result == """
+                const name = "MyApp";
+                const version = "1.0.0";
+                """
+        )
     }
 
     @Test func invalidVariableName() throws {
         let source = """
-        /* #if invalid-name */
-        console.log("error");
-        /* #endif */
-        """
+            /* #if invalid-name */
+            console.log("error");
+            /* #endif */
+            """
         #expect(throws: Error.self) {
             try preprocess(source: source, options: PreprocessOptions())
         }
@@ -117,10 +120,10 @@ import Testing
 
     @Test func emptyBlocks() throws {
         let source = """
-        /* #if FOO */
-        /* #else */
-        /* #endif */
-        """
+            /* #if FOO */
+            /* #else */
+            /* #endif */
+            """
         let options = PreprocessOptions(conditions: ["FOO": true])
         let result = try preprocess(source: source, options: options)
         #expect(result == "")
@@ -128,9 +131,9 @@ import Testing
 
     @Test func ignoreNonDirectiveComments() throws {
         let source = """
-        /* Normal comment */
-        /** Doc comment */
-        """
+            /* Normal comment */
+            /** Doc comment */
+            """
         let result = try preprocess(source: source, options: PreprocessOptions())
         #expect(result == source)
     }
diff --git a/Plugins/PackageToJS/Tests/SnapshotTesting.swift b/Plugins/PackageToJS/Tests/SnapshotTesting.swift
index 4732cfce8..e900954ff 100644
--- a/Plugins/PackageToJS/Tests/SnapshotTesting.swift
+++ b/Plugins/PackageToJS/Tests/SnapshotTesting.swift
@@ -1,11 +1,13 @@
-import Testing
 import Foundation
+import Testing
 
 func assertSnapshot(
-    filePath: String = #filePath, function: String = #function,
+    filePath: String = #filePath,
+    function: String = #function,
     sourceLocation: SourceLocation = #_sourceLocation,
     variant: String? = nil,
-    input: Data, fileExtension: String = "json"
+    input: Data,
+    fileExtension: String = "json"
 ) throws {
     let testFileName = URL(fileURLWithPath: filePath).deletingPathExtension().lastPathComponent
     let snapshotDir = URL(fileURLWithPath: filePath)
@@ -13,7 +15,8 @@ func assertSnapshot(
         .appendingPathComponent("__Snapshots__")
         .appendingPathComponent(testFileName)
     try FileManager.default.createDirectory(at: snapshotDir, withIntermediateDirectories: true)
-    let snapshotFileName: String = "\(function[.. this.wasmImports;
 
-    get wasmImports(): ImportedFunctions {
+    get wasmImports(): WebAssembly.ModuleImports {
         let broker: MessageBroker | null = null;
         const getMessageBroker = (threadChannel: SwiftRuntimeThreadChannel) => {
             if (broker) return broker;
@@ -479,6 +480,13 @@ export class SwiftRuntime {
                 return obj instanceof constructor;
             },
 
+            swjs_value_equals: (lhs_ref: ref, rhs_ref: ref) => {
+                const memory = this.memory;
+                const lhs = memory.getObject(lhs_ref);
+                const rhs = memory.getObject(rhs_ref);
+                return lhs == rhs;
+            },
+
             swjs_create_function: (
                 host_func_id: number,
                 line: number,
@@ -492,12 +500,16 @@ export class SwiftRuntime {
                 return func_ref;
             },
 
-            swjs_create_typed_array: (
+            swjs_create_typed_array: (
                 constructor_ref: ref,
                 elementsPtr: pointer,
                 length: number
             ) => {
-                const ArrayType: TypedArray =
+                type TypedArrayConstructor = {
+                    new (buffer: ArrayBuffer, byteOffset: number, length: number): T;
+                    new (): T;
+                };
+                const ArrayType: TypedArrayConstructor =
                     this.memory.getObject(constructor_ref);
                 if (length == 0) {
                     // The elementsPtr can be unaligned in Swift's Array
@@ -517,6 +529,8 @@ export class SwiftRuntime {
                 return this.memory.retain(array.slice());
             },
 
+            swjs_create_object: () => { return this.memory.retain({}); },
+
             swjs_load_typed_array: (ref: ref, buffer: pointer) => {
                 const memory = this.memory;
                 const typedArray = memory.getObject(ref);
@@ -566,7 +580,7 @@ export class SwiftRuntime {
                     return BigInt.asIntN(64, object);
                 }
             },
-            swjs_i64_to_bigint_slow: (lower, upper, signed) => {
+            swjs_i64_to_bigint_slow: (lower: number, upper: number, signed: number) => {
                 const value =
                     BigInt.asUintN(32, BigInt(lower)) +
                     (BigInt.asUintN(32, BigInt(upper)) << BigInt(32));
@@ -577,7 +591,7 @@ export class SwiftRuntime {
             swjs_unsafe_event_loop_yield: () => {
                 throw new UnsafeEventLoopYield();
             },
-            swjs_send_job_to_main_thread: (unowned_job) => {
+            swjs_send_job_to_main_thread: (unowned_job: number) => {
                 this.postMessageToMainThread({ type: "job", data: unowned_job });
             },
             swjs_listen_message_from_main_thread: () => {
@@ -607,10 +621,10 @@ export class SwiftRuntime {
                     }
                 });
             },
-            swjs_wake_up_worker_thread: (tid) => {
+            swjs_wake_up_worker_thread: (tid: number) => {
                 this.postMessageToWorkerThread(tid, { type: "wake" });
             },
-            swjs_listen_message_from_worker_thread: (tid) => {
+            swjs_listen_message_from_worker_thread: (tid: number) => {
                 const threadChannel = this.options.threadChannel;
                 if (!(threadChannel && "listenMessageFromWorkerThread" in threadChannel)) {
                     throw new Error(
@@ -639,7 +653,7 @@ export class SwiftRuntime {
                     },
                 );
             },
-            swjs_terminate_worker_thread: (tid) => {
+            swjs_terminate_worker_thread: (tid: number) => {
                 const threadChannel = this.options.threadChannel;
                 if (threadChannel && "terminateWorkerThread" in threadChannel) {
                     threadChannel.terminateWorkerThread?.(tid);
diff --git a/Runtime/src/types.ts b/Runtime/src/types.ts
index 587b60770..b8345cdfa 100644
--- a/Runtime/src/types.ts
+++ b/Runtime/src/types.ts
@@ -1,5 +1,3 @@
-import * as JSValue from "./js-value.js";
-
 export type ref = number;
 export type pointer = number;
 export type bool = number;
@@ -26,128 +24,10 @@ export interface ExportedFunctions {
     swjs_receive_error(error: ref, context: number): void;
 }
 
-export interface ImportedFunctions {
-    swjs_set_prop(
-        ref: number,
-        name: number,
-        kind: JSValue.Kind,
-        payload1: number,
-        payload2: number
-    ): void;
-    swjs_get_prop(
-        ref: number,
-        name: number,
-        payload1_ptr: pointer,
-        payload2_ptr: pointer
-    ): JavaScriptValueKind;
-    swjs_set_subscript(
-        ref: number,
-        index: number,
-        kind: JSValue.Kind,
-        payload1: number,
-        payload2: number
-    ): void;
-    swjs_get_subscript(
-        ref: number,
-        index: number,
-        payload1_ptr: pointer,
-        payload2_ptr: pointer
-    ): JavaScriptValueKind;
-    swjs_encode_string(ref: number, bytes_ptr_result: pointer): number;
-    swjs_decode_string(bytes_ptr: pointer, length: number): number;
-    swjs_load_string(ref: number, buffer: pointer): void;
-    swjs_call_function(
-        ref: number,
-        argv: pointer,
-        argc: number,
-        payload1_ptr: pointer,
-        payload2_ptr: pointer
-    ): JavaScriptValueKindAndFlags;
-    swjs_call_function_no_catch(
-        ref: number,
-        argv: pointer,
-        argc: number,
-        payload1_ptr: pointer,
-        payload2_ptr: pointer
-    ): JavaScriptValueKindAndFlags;
-    swjs_call_function_with_this(
-        obj_ref: ref,
-        func_ref: ref,
-        argv: pointer,
-        argc: number,
-        payload1_ptr: pointer,
-        payload2_ptr: pointer
-    ): JavaScriptValueKindAndFlags;
-    swjs_call_function_with_this_no_catch(
-        obj_ref: ref,
-        func_ref: ref,
-        argv: pointer,
-        argc: number,
-        payload1_ptr: pointer,
-        payload2_ptr: pointer
-    ): JavaScriptValueKindAndFlags;
-    swjs_call_new(ref: number, argv: pointer, argc: number): number;
-    swjs_call_throwing_new(
-        ref: number,
-        argv: pointer,
-        argc: number,
-        exception_kind_ptr: pointer,
-        exception_payload1_ptr: pointer,
-        exception_payload2_ptr: pointer
-    ): number;
-    swjs_instanceof(obj_ref: ref, constructor_ref: ref): boolean;
-    swjs_create_function(host_func_id: number, line: number, file: ref): number;
-    swjs_create_typed_array(
-        constructor_ref: ref,
-        elementsPtr: pointer,
-        length: number
-    ): number;
-    swjs_load_typed_array(ref: ref, buffer: pointer): void;
-    swjs_release(ref: number): void;
-    swjs_release_remote(tid: number, ref: number): void;
-    swjs_i64_to_bigint(value: bigint, signed: bool): ref;
-    swjs_bigint_to_i64(ref: ref, signed: bool): bigint;
-    swjs_i64_to_bigint_slow(lower: number, upper: number, signed: bool): ref;
-    swjs_unsafe_event_loop_yield: () => void;
-    swjs_send_job_to_main_thread: (unowned_job: number) => void;
-    swjs_listen_message_from_main_thread: () => void;
-    swjs_wake_up_worker_thread: (tid: number) => void;
-    swjs_listen_message_from_worker_thread: (tid: number) => void;
-    swjs_terminate_worker_thread: (tid: number) => void;
-    swjs_get_worker_thread_id: () => number;
-    swjs_request_sending_object: (
-        sending_object: ref,
-        transferring_objects: pointer,
-        transferring_objects_count: number,
-        object_source_tid: number,
-        sending_context: pointer,
-    ) => void;
-    swjs_request_sending_objects: (
-        sending_objects: pointer,
-        sending_objects_count: number,
-        transferring_objects: pointer,
-        transferring_objects_count: number,
-        object_source_tid: number,
-        sending_context: pointer,
-    ) => void;
-}
-
 export const enum LibraryFeatures {
     WeakRefs = 1 << 0,
 }
 
-export type TypedArray =
-    | Int8ArrayConstructor
-    | Uint8ArrayConstructor
-    | Int16ArrayConstructor
-    | Uint16ArrayConstructor
-    | Int32ArrayConstructor
-    | Uint32ArrayConstructor
-    | BigInt64ArrayConstructor
-    | BigUint64ArrayConstructor
-    | Float32ArrayConstructor
-    | Float64ArrayConstructor;
-
 export function assertNever(x: never, message: string) {
     throw new Error(message);
 }
diff --git a/Runtime/tsconfig.json b/Runtime/tsconfig.json
index 8a53ba2c0..bd4f8a1a1 100644
--- a/Runtime/tsconfig.json
+++ b/Runtime/tsconfig.json
@@ -1,7 +1,6 @@
 {
     "compilerOptions": {
-        "declaration": true,
-        "declarationDir": "lib",
+        "declaration": false,
         "importHelpers": true,
         "module": "esnext",
         "noEmit": true,
diff --git a/Sources/JavaScriptBigIntSupport/JSBigInt+I64.swift b/Sources/JavaScriptBigIntSupport/JSBigInt+I64.swift
index a8ac18cf6..0fe1561d0 100644
--- a/Sources/JavaScriptBigIntSupport/JSBigInt+I64.swift
+++ b/Sources/JavaScriptBigIntSupport/JSBigInt+I64.swift
@@ -1,5 +1,5 @@
-import _CJavaScriptBigIntSupport
 @_spi(JSObject_id) import JavaScriptKit
+import _CJavaScriptBigIntSupport
 
 extension JSBigInt: JavaScriptKit.JSBigIntExtended {
     public var int64Value: Int64 {
diff --git a/Sources/JavaScriptEventLoop/JSSending.swift b/Sources/JavaScriptEventLoop/JSSending.swift
index b4458d53a..3408b232f 100644
--- a/Sources/JavaScriptEventLoop/JSSending.swift
+++ b/Sources/JavaScriptEventLoop/JSSending.swift
@@ -1,8 +1,9 @@
+import _Concurrency
 @_spi(JSObject_id) import JavaScriptKit
 import _CJavaScriptKit
 
 #if canImport(Synchronization)
-    import Synchronization
+import Synchronization
 #endif
 
 /// A temporary object intended to send a JavaScript object from one thread to another.
@@ -105,11 +106,11 @@ extension JSSending where T == JSObject {
             deconstruct: { $0 },
             getSourceTid: {
                 #if compiler(>=6.1) && _runtime(_multithreaded)
-                    return $0.ownerTid
+                return $0.ownerTid
                 #else
-                    _ = $0
-                    // On single-threaded runtime, source and destination threads are always the main thread (TID = -1).
-                    return -1
+                _ = $0
+                // On single-threaded runtime, source and destination threads are always the main thread (TID = -1).
+                return -1
                 #endif
             },
             transferring: transferring
@@ -226,7 +227,11 @@ extension JSSending {
     /// - Returns: The received object of type `T`.
     /// - Throws: `JSSendingError` if the sending operation fails, or `JSException` if a JavaScript error occurs.
     @available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *)
-    public func receive(isolation: isolated (any Actor)? = #isolation, file: StaticString = #file, line: UInt = #line) async throws -> T {
+    public func receive(
+        isolation: isolated (any Actor)? = #isolation,
+        file: StaticString = #file,
+        line: UInt = #line
+    ) async throws -> T {
         #if compiler(>=6.1) && _runtime(_multithreaded)
         let idInDestination = try await withCheckedThrowingContinuation { continuation in
             let context = _JSSendingContext(continuation: continuation)
@@ -281,7 +286,9 @@ extension JSSending {
     @available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *)
     public static func receive(
         _ sendings: repeat JSSending,
-        isolation: isolated (any Actor)? = #isolation, file: StaticString = #file, line: UInt = #line
+        isolation: isolated (any Actor)? = #isolation,
+        file: StaticString = #file,
+        line: UInt = #line
     ) async throws -> (repeat each U) where T == (repeat each U) {
         #if compiler(>=6.1) && _runtime(_multithreaded)
         var sendingObjects: [JavaScriptObjectRef] = []
@@ -329,11 +336,11 @@ extension JSSending {
         return try await (repeat (each sendings).receive())
         #endif
     }
-    #endif // compiler(>=6.1)
+    #endif  // compiler(>=6.1)
 }
 
 @available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *)
-fileprivate final class _JSSendingContext: Sendable {
+private final class _JSSendingContext: Sendable {
     let continuation: CheckedContinuation
 
     init(continuation: CheckedContinuation) {
@@ -361,6 +368,7 @@ public struct JSSendingError: Error, CustomStringConvertible {
 /// - Parameters:
 ///   - object: The `JSObject` to be received.
 ///   - contextPtr: A pointer to the `_JSSendingContext` instance.
+// swift-format-ignore
 #if compiler(>=6.1)  // @_expose and @_extern are only available in Swift 6.1+
 @_expose(wasm, "swjs_receive_response")
 @_cdecl("swjs_receive_response")
@@ -380,6 +388,7 @@ func _swjs_receive_response(_ object: JavaScriptObjectRef, _ contextPtr: UnsafeR
 /// - Parameters:
 ///   - error: The error to be received.
 ///   - contextPtr: A pointer to the `_JSSendingContext` instance.
+// swift-format-ignore
 #if compiler(>=6.1)  // @_expose and @_extern are only available in Swift 6.1+
 @_expose(wasm, "swjs_receive_error")
 @_cdecl("swjs_receive_error")
diff --git a/Sources/JavaScriptEventLoop/JavaScriptEventLoop+ExecutorFactory.swift b/Sources/JavaScriptEventLoop/JavaScriptEventLoop+ExecutorFactory.swift
new file mode 100644
index 000000000..ed60eae76
--- /dev/null
+++ b/Sources/JavaScriptEventLoop/JavaScriptEventLoop+ExecutorFactory.swift
@@ -0,0 +1,92 @@
+// Implementation of custom executors for JavaScript event loop
+// This file implements the ExecutorFactory protocol to provide custom main and global executors
+// for Swift concurrency in JavaScript environment.
+// See: https://github.com/swiftlang/swift/pull/80266
+// See: https://forums.swift.org/t/pitch-2-custom-main-and-global-executors/78437
+
+import _Concurrency
+import _CJavaScriptKit
+
+#if compiler(>=6.2)
+
+// MARK: - MainExecutor Implementation
+// MainExecutor is used by the main actor to execute tasks on the main thread
+@available(macOS 9999, iOS 9999, watchOS 9999, tvOS 9999, visionOS 9999, *)
+extension JavaScriptEventLoop: MainExecutor {
+    public func run() throws {
+        // This method is called from `swift_task_asyncMainDrainQueueImpl`.
+        // https://github.com/swiftlang/swift/blob/swift-DEVELOPMENT-SNAPSHOT-2025-04-12-a/stdlib/public/Concurrency/ExecutorImpl.swift#L28
+        // Yield control to the JavaScript event loop to skip the `exit(0)`
+        // call by `swift_task_asyncMainDrainQueueImpl`.
+        swjs_unsafe_event_loop_yield()
+    }
+    public func stop() {}
+}
+
+@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *)
+extension JavaScriptEventLoop: TaskExecutor {}
+
+@available(macOS 9999, iOS 9999, watchOS 9999, tvOS 9999, visionOS 9999, *)
+extension JavaScriptEventLoop: SchedulableExecutor {
+    public func enqueue(
+        _ job: consuming ExecutorJob,
+        after delay: C.Duration,
+        tolerance: C.Duration?,
+        clock: C
+    ) {
+        let milliseconds = Self.delayInMilliseconds(from: delay, clock: clock)
+        self.enqueue(
+            UnownedJob(job),
+            withDelay: milliseconds
+        )
+    }
+
+    private static func delayInMilliseconds(from duration: C.Duration, clock: C) -> Double {
+        let swiftDuration = clock.convert(from: duration)!
+        let (seconds, attoseconds) = swiftDuration.components
+        return Double(seconds) * 1_000 + (Double(attoseconds) / 1_000_000_000_000_000)
+    }
+}
+
+// MARK: - ExecutorFactory Implementation
+@available(macOS 9999, iOS 9999, watchOS 9999, tvOS 9999, visionOS 9999, *)
+extension JavaScriptEventLoop: ExecutorFactory {
+    // Forward all operations to the current thread's JavaScriptEventLoop instance
+    final class CurrentThread: TaskExecutor, SchedulableExecutor, MainExecutor, SerialExecutor {
+        func checkIsolated() {}
+
+        func enqueue(_ job: consuming ExecutorJob) {
+            JavaScriptEventLoop.shared.enqueue(job)
+        }
+
+        func enqueue(
+            _ job: consuming ExecutorJob,
+            after delay: C.Duration,
+            tolerance: C.Duration?,
+            clock: C
+        ) {
+            JavaScriptEventLoop.shared.enqueue(
+                job,
+                after: delay,
+                tolerance: tolerance,
+                clock: clock
+            )
+        }
+        func run() throws {
+            try JavaScriptEventLoop.shared.run()
+        }
+        func stop() {
+            JavaScriptEventLoop.shared.stop()
+        }
+    }
+
+    public static var mainExecutor: any MainExecutor {
+        CurrentThread()
+    }
+
+    public static var defaultExecutor: any TaskExecutor {
+        CurrentThread()
+    }
+}
+
+#endif  // compiler(>=6.2)
diff --git a/Sources/JavaScriptEventLoop/JavaScriptEventLoop+LegacyHooks.swift b/Sources/JavaScriptEventLoop/JavaScriptEventLoop+LegacyHooks.swift
new file mode 100644
index 000000000..bcab9a3d1
--- /dev/null
+++ b/Sources/JavaScriptEventLoop/JavaScriptEventLoop+LegacyHooks.swift
@@ -0,0 +1,107 @@
+import _Concurrency
+import _CJavaScriptEventLoop
+import _CJavaScriptKit
+
+@available(macOS 14.0, iOS 17.0, watchOS 10.0, tvOS 17.0, *)
+extension JavaScriptEventLoop {
+
+    static func installByLegacyHook() {
+        #if compiler(>=5.9)
+        typealias swift_task_asyncMainDrainQueue_hook_Fn = @convention(thin) (
+            swift_task_asyncMainDrainQueue_original, swift_task_asyncMainDrainQueue_override
+        ) -> Void
+        let swift_task_asyncMainDrainQueue_hook_impl: swift_task_asyncMainDrainQueue_hook_Fn = { _, _ in
+            swjs_unsafe_event_loop_yield()
+        }
+        swift_task_asyncMainDrainQueue_hook = unsafeBitCast(
+            swift_task_asyncMainDrainQueue_hook_impl,
+            to: UnsafeMutableRawPointer?.self
+        )
+        #endif
+
+        typealias swift_task_enqueueGlobal_hook_Fn = @convention(thin) (UnownedJob, swift_task_enqueueGlobal_original)
+            -> Void
+        let swift_task_enqueueGlobal_hook_impl: swift_task_enqueueGlobal_hook_Fn = { job, original in
+            JavaScriptEventLoop.shared.unsafeEnqueue(job)
+        }
+        swift_task_enqueueGlobal_hook = unsafeBitCast(
+            swift_task_enqueueGlobal_hook_impl,
+            to: UnsafeMutableRawPointer?.self
+        )
+
+        typealias swift_task_enqueueGlobalWithDelay_hook_Fn = @convention(thin) (
+            UInt64, UnownedJob, swift_task_enqueueGlobalWithDelay_original
+        ) -> Void
+        let swift_task_enqueueGlobalWithDelay_hook_impl: swift_task_enqueueGlobalWithDelay_hook_Fn = {
+            nanoseconds,
+            job,
+            original in
+            let milliseconds = Double(nanoseconds / 1_000_000)
+            JavaScriptEventLoop.shared.enqueue(job, withDelay: milliseconds)
+        }
+        swift_task_enqueueGlobalWithDelay_hook = unsafeBitCast(
+            swift_task_enqueueGlobalWithDelay_hook_impl,
+            to: UnsafeMutableRawPointer?.self
+        )
+
+        #if compiler(>=5.7)
+        typealias swift_task_enqueueGlobalWithDeadline_hook_Fn = @convention(thin) (
+            Int64, Int64, Int64, Int64, Int32, UnownedJob, swift_task_enqueueGlobalWithDelay_original
+        ) -> Void
+        let swift_task_enqueueGlobalWithDeadline_hook_impl: swift_task_enqueueGlobalWithDeadline_hook_Fn = {
+            sec,
+            nsec,
+            tsec,
+            tnsec,
+            clock,
+            job,
+            original in
+            JavaScriptEventLoop.shared.enqueue(job, withDelay: sec, nsec, tsec, tnsec, clock)
+        }
+        swift_task_enqueueGlobalWithDeadline_hook = unsafeBitCast(
+            swift_task_enqueueGlobalWithDeadline_hook_impl,
+            to: UnsafeMutableRawPointer?.self
+        )
+        #endif
+
+        typealias swift_task_enqueueMainExecutor_hook_Fn = @convention(thin) (
+            UnownedJob, swift_task_enqueueMainExecutor_original
+        ) -> Void
+        let swift_task_enqueueMainExecutor_hook_impl: swift_task_enqueueMainExecutor_hook_Fn = { job, original in
+            JavaScriptEventLoop.shared.unsafeEnqueue(job)
+        }
+        swift_task_enqueueMainExecutor_hook = unsafeBitCast(
+            swift_task_enqueueMainExecutor_hook_impl,
+            to: UnsafeMutableRawPointer?.self
+        )
+
+    }
+}
+
+#if compiler(>=5.7)
+/// Taken from https://github.com/apple/swift/blob/d375c972f12128ec6055ed5f5337bfcae3ec67d8/stdlib/public/Concurrency/Clock.swift#L84-L88
+@_silgen_name("swift_get_time")
+internal func swift_get_time(
+    _ seconds: UnsafeMutablePointer,
+    _ nanoseconds: UnsafeMutablePointer,
+    _ clock: CInt
+)
+
+@available(macOS 14.0, iOS 17.0, watchOS 10.0, tvOS 17.0, *)
+extension JavaScriptEventLoop {
+    fileprivate func enqueue(
+        _ job: UnownedJob,
+        withDelay seconds: Int64,
+        _ nanoseconds: Int64,
+        _ toleranceSec: Int64,
+        _ toleranceNSec: Int64,
+        _ clock: Int32
+    ) {
+        var nowSec: Int64 = 0
+        var nowNSec: Int64 = 0
+        swift_get_time(&nowSec, &nowNSec, clock)
+        let delayMilliseconds = (seconds - nowSec) * 1_000 + (nanoseconds - nowNSec) / 1_000_000
+        enqueue(job, withDelay: delayMilliseconds <= 0 ? 0 : Double(delayMilliseconds))
+    }
+}
+#endif
diff --git a/Sources/JavaScriptEventLoop/JavaScriptEventLoop.swift b/Sources/JavaScriptEventLoop/JavaScriptEventLoop.swift
index ce4fb1047..1cb90f8d8 100644
--- a/Sources/JavaScriptEventLoop/JavaScriptEventLoop.swift
+++ b/Sources/JavaScriptEventLoop/JavaScriptEventLoop.swift
@@ -1,4 +1,5 @@
 import JavaScriptKit
+import _Concurrency
 import _CJavaScriptEventLoop
 import _CJavaScriptKit
 
@@ -6,35 +7,34 @@ import _CJavaScriptKit
 
 #if compiler(>=5.5)
 
-/** Singleton type responsible for integrating JavaScript event loop as a Swift concurrency executor, conforming to
-`SerialExecutor` protocol from the standard library. To utilize it:
-
-1. Make sure that your target depends on `JavaScriptEventLoop` in your `Packages.swift`:
-
-```swift
-.target(
-    name: "JavaScriptKitExample",
-    dependencies: [
-        "JavaScriptKit",
-        .product(name: "JavaScriptEventLoop", package: "JavaScriptKit")
-    ]
-)
-```
-
-2. Add an explicit import in the code that executes **before* you start using `await` and/or `Task`
-APIs (most likely in `main.swift`):
-
-```swift
-import JavaScriptEventLoop
-```
-
-3. Run this function **before* you start using `await` and/or `Task` APIs (again, most likely in
-`main.swift`):
-
-```swift
-JavaScriptEventLoop.installGlobalExecutor()
-```
-*/
+/// Singleton type responsible for integrating JavaScript event loop as a Swift concurrency executor, conforming to
+/// `SerialExecutor` protocol from the standard library. To utilize it:
+///
+/// 1. Make sure that your target depends on `JavaScriptEventLoop` in your `Packages.swift`:
+///
+/// ```swift
+/// .target(
+///    name: "JavaScriptKitExample",
+///    dependencies: [
+///        "JavaScriptKit",
+///        .product(name: "JavaScriptEventLoop", package: "JavaScriptKit")
+///    ]
+/// )
+/// ```
+///
+/// 2. Add an explicit import in the code that executes **before* you start using `await` and/or `Task`
+/// APIs (most likely in `main.swift`):
+///
+/// ```swift
+/// import JavaScriptEventLoop
+/// ```
+///
+/// 3. Run this function **before* you start using `await` and/or `Task` APIs (again, most likely in
+/// `main.swift`):
+///
+/// ```swift
+/// JavaScriptEventLoop.installGlobalExecutor()
+/// ```
 @available(macOS 14.0, iOS 17.0, watchOS 10.0, tvOS 17.0, *)
 public final class JavaScriptEventLoop: SerialExecutor, @unchecked Sendable {
 
@@ -93,80 +93,69 @@ public final class JavaScriptEventLoop: SerialExecutor, @unchecked Sendable {
                 }
             },
             setTimeout: { delay, job in
-                setTimeout(JSOneshotClosure { _ in
-                    job()
-                    return JSValue.undefined
-                }, delay)
+                setTimeout(
+                    JSOneshotClosure { _ in
+                        job()
+                        return JSValue.undefined
+                    },
+                    delay
+                )
             }
         )
         return eventLoop
     }
 
-    @MainActor private static var didInstallGlobalExecutor = false
+    private nonisolated(unsafe) static var didInstallGlobalExecutor = false
 
     /// Set JavaScript event loop based executor to be the global executor
     /// Note that this should be called before any of the jobs are created.
     /// This installation step will be unnecessary after custom executor are
-    /// introduced officially. See also [a draft proposal for custom 
+    /// introduced officially. See also [a draft proposal for custom
     /// executors](https://github.com/rjmccall/swift-evolution/blob/custom-executors/proposals/0000-custom-executors.md#the-default-global-concurrent-executor)
     public static func installGlobalExecutor() {
-        MainActor.assumeIsolated {
-            Self.installGlobalExecutorIsolated()
-        }
+        Self.installGlobalExecutorIsolated()
     }
 
-    @MainActor private static func installGlobalExecutorIsolated() {
+    private static func installGlobalExecutorIsolated() {
         guard !didInstallGlobalExecutor else { return }
-
-        #if compiler(>=5.9)
-        typealias swift_task_asyncMainDrainQueue_hook_Fn = @convention(thin) (swift_task_asyncMainDrainQueue_original, swift_task_asyncMainDrainQueue_override) -> Void
-        let swift_task_asyncMainDrainQueue_hook_impl: swift_task_asyncMainDrainQueue_hook_Fn = { _, _ in
-            swjs_unsafe_event_loop_yield()
-        }
-        swift_task_asyncMainDrainQueue_hook = unsafeBitCast(swift_task_asyncMainDrainQueue_hook_impl, to: UnsafeMutableRawPointer?.self)
-        #endif
-
-        typealias swift_task_enqueueGlobal_hook_Fn = @convention(thin) (UnownedJob, swift_task_enqueueGlobal_original) -> Void
-        let swift_task_enqueueGlobal_hook_impl: swift_task_enqueueGlobal_hook_Fn = { job, original in
-            JavaScriptEventLoop.shared.unsafeEnqueue(job)
-        }
-        swift_task_enqueueGlobal_hook = unsafeBitCast(swift_task_enqueueGlobal_hook_impl, to: UnsafeMutableRawPointer?.self)
-
-        typealias swift_task_enqueueGlobalWithDelay_hook_Fn = @convention(thin) (UInt64, UnownedJob, swift_task_enqueueGlobalWithDelay_original) -> Void
-        let swift_task_enqueueGlobalWithDelay_hook_impl: swift_task_enqueueGlobalWithDelay_hook_Fn = { delay, job, original in
-            JavaScriptEventLoop.shared.enqueue(job, withDelay: delay)
-        }
-        swift_task_enqueueGlobalWithDelay_hook = unsafeBitCast(swift_task_enqueueGlobalWithDelay_hook_impl, to: UnsafeMutableRawPointer?.self)
-
-        #if compiler(>=5.7)
-        typealias swift_task_enqueueGlobalWithDeadline_hook_Fn = @convention(thin) (Int64, Int64, Int64, Int64, Int32, UnownedJob, swift_task_enqueueGlobalWithDelay_original) -> Void
-        let swift_task_enqueueGlobalWithDeadline_hook_impl: swift_task_enqueueGlobalWithDeadline_hook_Fn = { sec, nsec, tsec, tnsec, clock, job, original in
-            JavaScriptEventLoop.shared.enqueue(job, withDelay: sec, nsec, tsec, tnsec, clock)
+        didInstallGlobalExecutor = true
+        #if compiler(>=6.2)
+        if #available(macOS 9999, iOS 9999, watchOS 9999, tvOS 9999, visionOS 9999, *) {
+            // For Swift 6.2 and above, we can use the new `ExecutorFactory` API
+            _Concurrency._createExecutors(factory: JavaScriptEventLoop.self)
         }
-        swift_task_enqueueGlobalWithDeadline_hook = unsafeBitCast(swift_task_enqueueGlobalWithDeadline_hook_impl, to: UnsafeMutableRawPointer?.self)
+        #else
+        // For Swift 6.1 and below, we need to install the global executor by hook API
+        installByLegacyHook()
         #endif
-
-        typealias swift_task_enqueueMainExecutor_hook_Fn = @convention(thin) (UnownedJob, swift_task_enqueueMainExecutor_original) -> Void
-        let swift_task_enqueueMainExecutor_hook_impl: swift_task_enqueueMainExecutor_hook_Fn = { job, original in
-            JavaScriptEventLoop.shared.unsafeEnqueue(job)
-        }
-        swift_task_enqueueMainExecutor_hook = unsafeBitCast(swift_task_enqueueMainExecutor_hook_impl, to: UnsafeMutableRawPointer?.self)
-
-        didInstallGlobalExecutor = true
     }
 
-    private func enqueue(_ job: UnownedJob, withDelay nanoseconds: UInt64) {
-        let milliseconds = nanoseconds / 1_000_000
-        setTimeout(Double(milliseconds), {
-            #if compiler(>=5.9)
-            job.runSynchronously(on: self.asUnownedSerialExecutor())
-            #else
-            job._runSynchronously(on: self.asUnownedSerialExecutor())
-            #endif
-        })
+    internal func enqueue(_ job: UnownedJob, withDelay milliseconds: Double) {
+        setTimeout(
+            milliseconds,
+            {
+                #if compiler(>=5.9)
+                job.runSynchronously(on: self.asUnownedSerialExecutor())
+                #else
+                job._runSynchronously(on: self.asUnownedSerialExecutor())
+                #endif
+            }
+        )
     }
 
-    private func unsafeEnqueue(_ job: UnownedJob) {
+    internal func unsafeEnqueue(_ job: UnownedJob) {
+        #if canImport(wasi_pthread) && compiler(>=6.1) && _runtime(_multithreaded)
+        guard swjs_get_worker_thread_id_cached() == SWJS_MAIN_THREAD_ID else {
+            // Notify the main thread to execute the job when a job is
+            // enqueued from a Web Worker thread but without an executor preference.
+            // This is usually the case when hopping back to the main thread
+            // at the end of a task.
+            let jobBitPattern = unsafeBitCast(job, to: UInt.self)
+            swjs_send_job_to_main_thread(jobBitPattern)
+            return
+        }
+        // If the current thread is the main thread, do nothing special.
+        #endif
         insertJobQueue(job: job)
     }
 
@@ -188,70 +177,46 @@ public final class JavaScriptEventLoop: SerialExecutor, @unchecked Sendable {
     }
 }
 
-#if compiler(>=5.7)
-/// Taken from https://github.com/apple/swift/blob/d375c972f12128ec6055ed5f5337bfcae3ec67d8/stdlib/public/Concurrency/Clock.swift#L84-L88
-@_silgen_name("swift_get_time")
-internal func swift_get_time(
-  _ seconds: UnsafeMutablePointer,
-  _ nanoseconds: UnsafeMutablePointer,
-  _ clock: CInt)
-
-@available(macOS 14.0, iOS 17.0, watchOS 10.0, tvOS 17.0, *)
-extension JavaScriptEventLoop {
-    fileprivate func enqueue(
-        _ job: UnownedJob, withDelay seconds: Int64, _ nanoseconds: Int64,
-        _ toleranceSec: Int64, _ toleranceNSec: Int64,
-        _ clock: Int32
-    ) {
-        var nowSec: Int64 = 0
-        var nowNSec: Int64 = 0
-        swift_get_time(&nowSec, &nowNSec, clock)
-        let delayNanosec = (seconds - nowSec) * 1_000_000_000 + (nanoseconds - nowNSec)
-        enqueue(job, withDelay: delayNanosec <= 0 ? 0 : UInt64(delayNanosec))
-    }
-}
-#endif
-
 @available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *)
-public extension JSPromise {
+extension JSPromise {
     /// Wait for the promise to complete, returning (or throwing) its result.
-    var value: JSValue {
-        get async throws {
-            try await withUnsafeThrowingContinuation { [self] continuation in
+    public var value: JSValue {
+        get async throws(JSException) {
+            try await withUnsafeContinuation { [self] continuation in
                 self.then(
                     success: {
-                        continuation.resume(returning: $0)
+                        continuation.resume(returning: Swift.Result.success($0))
                         return JSValue.undefined
                     },
                     failure: {
-                        continuation.resume(throwing: JSException($0))
+                        continuation.resume(returning: Swift.Result.failure(.init($0)))
                         return JSValue.undefined
                     }
                 )
-            }
+            }.get()
         }
     }
 
     /// Wait for the promise to complete, returning its result or exception as a Result.
     ///
     /// - Note: Calling this function does not switch from the caller's isolation domain.
-    func value(isolation: isolated (any Actor)? = #isolation) async throws -> JSValue {
-        try await withUnsafeThrowingContinuation(isolation: isolation) { [self] continuation in
+    public func value(isolation: isolated (any Actor)? = #isolation) async throws(JSException) -> JSValue {
+        try await withUnsafeContinuation(isolation: isolation) { [self] continuation in
             self.then(
                 success: {
-                    continuation.resume(returning: $0)
+                    continuation.resume(returning: Swift.Result.success($0))
                     return JSValue.undefined
                 },
                 failure: {
-                    continuation.resume(throwing: JSException($0))
+                    continuation.resume(returning: Swift.Result.failure(.init($0)))
                     return JSValue.undefined
                 }
             )
-        }
+        }.get()
     }
 
     /// Wait for the promise to complete, returning its result or exception as a Result.
-    var result: JSPromise.Result {
+    public var result: JSPromise.Result {
         get async {
             await withUnsafeContinuation { [self] continuation in
                 self.then(
diff --git a/Sources/JavaScriptEventLoop/JobQueue.swift b/Sources/JavaScriptEventLoop/JobQueue.swift
index 5ad71f0a0..a0f2c4bbb 100644
--- a/Sources/JavaScriptEventLoop/JobQueue.swift
+++ b/Sources/JavaScriptEventLoop/JobQueue.swift
@@ -2,6 +2,7 @@
 // The current implementation is much simple to be easily debugged, but should be re-implemented
 // using priority queue ideally.
 
+import _Concurrency
 import _CJavaScriptEventLoop
 
 #if compiler(>=5.5)
@@ -63,18 +64,18 @@ extension JavaScriptEventLoop {
 }
 
 @available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *)
-fileprivate extension UnownedJob {
+extension UnownedJob {
     private func asImpl() -> UnsafeMutablePointer<_CJavaScriptEventLoop.Job> {
         unsafeBitCast(self, to: UnsafeMutablePointer<_CJavaScriptEventLoop.Job>.self)
     }
 
-    var flags: JobFlags {
+    fileprivate var flags: JobFlags {
         JobFlags(bits: asImpl().pointee.Flags)
     }
 
-    var rawPriority: UInt32 { flags.priority }
+    fileprivate var rawPriority: UInt32 { flags.priority }
 
-    func nextInQueue() -> UnsafeMutablePointer {
+    fileprivate func nextInQueue() -> UnsafeMutablePointer {
         return withUnsafeMutablePointer(to: &asImpl().pointee.SchedulerPrivate.0) { rawNextJobPtr in
             let nextJobPtr = UnsafeMutableRawPointer(rawNextJobPtr).bindMemory(to: UnownedJob?.self, capacity: 1)
             return nextJobPtr
@@ -83,13 +84,11 @@ fileprivate extension UnownedJob {
 
 }
 
-fileprivate struct JobFlags {
-  var bits: UInt32 = 0
+private struct JobFlags {
+    var bits: UInt32 = 0
 
-  var priority: UInt32 {
-    get {
-      (bits & 0xFF00) >> 8
+    var priority: UInt32 {
+        (bits & 0xFF00) >> 8
     }
-  }
 }
 #endif
diff --git a/Sources/JavaScriptEventLoop/WebWorkerDedicatedExecutor.swift b/Sources/JavaScriptEventLoop/WebWorkerDedicatedExecutor.swift
index 695eb9c61..82cc593bd 100644
--- a/Sources/JavaScriptEventLoop/WebWorkerDedicatedExecutor.swift
+++ b/Sources/JavaScriptEventLoop/WebWorkerDedicatedExecutor.swift
@@ -1,12 +1,14 @@
+#if !hasFeature(Embedded)
 import JavaScriptKit
 import _CJavaScriptEventLoop
+import _Concurrency
 
 #if canImport(Synchronization)
-    import Synchronization
+import Synchronization
 #endif
 #if canImport(wasi_pthread)
-    import wasi_pthread
-    import WASILibc
+import wasi_pthread
+import WASILibc
 #endif
 
 /// A serial executor that runs on a dedicated web worker thread.
@@ -32,7 +34,7 @@ import _CJavaScriptEventLoop
 ///
 /// - SeeAlso: ``WebWorkerTaskExecutor``
 @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *)
-public final class WebWorkerDedicatedExecutor: SerialExecutor {
+public final class WebWorkerDedicatedExecutor: SerialExecutor, TaskExecutor {
 
     private let underlying: WebWorkerTaskExecutor
 
@@ -42,7 +44,9 @@ public final class WebWorkerDedicatedExecutor: SerialExecutor {
     /// - Throws: An error if any worker thread fails to initialize within the timeout period.
     public init(timeout: Duration = .seconds(3), checkInterval: Duration = .microseconds(5)) async throws {
         let underlying = try await WebWorkerTaskExecutor(
-            numberOfThreads: 1, timeout: timeout, checkInterval: checkInterval
+            numberOfThreads: 1,
+            timeout: timeout,
+            checkInterval: checkInterval
         )
         self.underlying = underlying
     }
@@ -58,3 +62,4 @@ public final class WebWorkerDedicatedExecutor: SerialExecutor {
         self.underlying.enqueue(job)
     }
 }
+#endif
diff --git a/Sources/JavaScriptEventLoop/WebWorkerTaskExecutor.swift b/Sources/JavaScriptEventLoop/WebWorkerTaskExecutor.swift
index 7373b9604..1078244f9 100644
--- a/Sources/JavaScriptEventLoop/WebWorkerTaskExecutor.swift
+++ b/Sources/JavaScriptEventLoop/WebWorkerTaskExecutor.swift
@@ -1,15 +1,15 @@
-#if compiler(>=6.0) // `TaskExecutor` is available since Swift 6.0
+#if compiler(>=6.0) && !hasFeature(Embedded)  // `TaskExecutor` is available since Swift 6.0, no multi-threading for embedded Wasm yet.
 
 import JavaScriptKit
 import _CJavaScriptKit
 import _CJavaScriptEventLoop
 
 #if canImport(Synchronization)
-    import Synchronization
+import Synchronization
 #endif
 #if canImport(wasi_pthread)
-    import wasi_pthread
-    import WASILibc
+import wasi_pthread
+import WASILibc
 #endif
 
 // MARK: - Web Worker Task Executor
@@ -23,13 +23,13 @@ import _CJavaScriptEventLoop
 ///
 /// ## Multithreading Model
 ///
-/// Each task submitted to the executor runs on one of the available worker threads. By default, 
+/// Each task submitted to the executor runs on one of the available worker threads. By default,
 /// child tasks created within a worker thread continue to run on the same worker thread,
 /// maintaining thread locality and avoiding excessive context switching.
 ///
 /// ## Object Sharing Between Threads
 ///
-/// When working with JavaScript objects across threads, you must use the `JSSending` API to 
+/// When working with JavaScript objects across threads, you must use the `JSSending` API to
 /// explicitly transfer or clone objects:
 ///
 /// ```swift
@@ -80,13 +80,17 @@ import _CJavaScriptEventLoop
 ///             return fibonacci(i)
 ///         }
 ///     }
-///     
+///
 ///     for await result in group {
 ///         // Process results as they complete
 ///     }
 /// }
 /// ```
 ///
+/// ## Scheduling invariants
+///
+/// * Jobs enqueued on a worker are guaranteed to run within the same macrotask in which they were scheduled.
+///
 /// ## Known limitations
 ///
 /// Currently, the Cooperative Global Executor of Swift runtime has a bug around
@@ -106,10 +110,20 @@ import _CJavaScriptEventLoop
 ///   // Back to the main thread.
 /// }
 /// ````
-/// 
-@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) // For `Atomic` and `TaskExecutor` types
+///
+@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *)  // For `Atomic` and `TaskExecutor` types
 public final class WebWorkerTaskExecutor: TaskExecutor {
 
+    /// An error that occurs when spawning a worker thread fails.
+    public struct SpawnError: Error {
+        /// The reason for the error.
+        public let reason: String
+
+        internal init(reason: String) {
+            self.reason = reason
+        }
+    }
+
     /// A job worker dedicated to a single Web Worker thread.
     ///
     /// ## Lifetime
@@ -125,22 +139,26 @@ public final class WebWorkerTaskExecutor: TaskExecutor {
         ///              +---------+                +------------+
         ///       +----->|  Idle   |--[terminate]-->| Terminated |
         ///       |      +---+-----+                +------------+
-        ///       |          |
-        ///       |      [enqueue]
-        ///       |          |
-        ///  [no more job]   |
-        ///       |          v
-        ///       |      +---------+
-        ///       +------| Running |
-        ///              +---------+
+        ///       |          |  \
+        ///       |          |   \------------------+
+        ///       |          |                      |
+        ///       |      [enqueue]              [enqueue] (on other thread)
+        ///       |          |                      |
+        ///  [no more job]   |                      |
+        ///       |          v                      v
+        ///       |      +---------+           +---------+
+        ///       +------| Running |<--[wake]--|  Ready  |
+        ///              +---------+           +---------+
         ///
         enum State: UInt32, AtomicRepresentable {
             /// The worker is idle and waiting for a new job.
             case idle = 0
+            /// A wake message is sent to the worker, but it has not been received it yet
+            case ready = 1
             /// The worker is processing a job.
-            case running = 1
+            case running = 2
             /// The worker is terminated.
-            case terminated = 2
+            case terminated = 3
         }
         let state: Atomic = Atomic(.idle)
         /// TODO: Rewrite it to use real queue :-)
@@ -187,30 +205,46 @@ public final class WebWorkerTaskExecutor: TaskExecutor {
         func enqueue(_ job: UnownedJob) {
             statsIncrement(\.enqueuedJobs)
             var locked: Bool
+            let onTargetThread = Self.currentThread === self
+            // If it's on the thread and it's idle, we can directly schedule a `Worker/run` microtask.
+            let desiredState: State = onTargetThread ? .running : .ready
             repeat {
                 let result: Void? = jobQueue.withLockIfAvailable { queue in
                     queue.append(job)
+                    trace("Worker.enqueue idle -> running")
                     // Wake up the worker to process a job.
-                    switch state.exchange(.running, ordering: .sequentiallyConsistent) {
-                    case .idle:
-                        if Self.currentThread === self {
+                    trace("Worker.enqueue idle -> \(desiredState)")
+                    switch state.compareExchange(
+                        expected: .idle,
+                        desired: desiredState,
+                        ordering: .sequentiallyConsistent
+                    ) {
+                    case (true, _):
+                        if onTargetThread {
                             // Enqueueing a new job to the current worker thread, but it's idle now.
                             // This is usually the case when a continuation is resumed by JS events
                             // like `setTimeout` or `addEventListener`.
                             // We can run the job and subsequently spawned jobs immediately.
-                            // JSPromise.resolve(JSValue.undefined).then { _ in
-                            _ = JSObject.global.queueMicrotask!(JSOneshotClosure { _ in
-                                self.run()
-                                return JSValue.undefined
-                            })
+                            scheduleRunWithinMacroTask()
                         } else {
                             let tid = self.tid.load(ordering: .sequentiallyConsistent)
                             swjs_wake_up_worker_thread(tid)
                         }
-                    case .running:
+                    case (false, .idle):
+                        preconditionFailure("unreachable: idle -> \(desiredState) should return exchanged=true")
+                    case (false, .ready):
+                        // A wake message is sent to the worker, but it has not been received it yet
+                        if onTargetThread {
+                            // This means the job is enqueued outside of `Worker/run` (typically triggered
+                            // JS microtasks not awaited by Swift), then schedule a `Worker/run` within
+                            // the same macrotask.
+                            state.store(.running, ordering: .sequentiallyConsistent)
+                            scheduleRunWithinMacroTask()
+                        }
+                    case (false, .running):
                         // The worker is already running, no need to wake up.
                         break
-                    case .terminated:
+                    case (false, .terminated):
                         // Will not wake up the worker because it's already terminated.
                         break
                     }
@@ -219,11 +253,13 @@ public final class WebWorkerTaskExecutor: TaskExecutor {
             } while !locked
         }
 
-        func scheduleNextRun() {
-            _ = JSObject.global.queueMicrotask!(JSOneshotClosure { _ in
-                self.run()
-                return JSValue.undefined
-            })
+        func scheduleRunWithinMacroTask() {
+            _ = JSObject.global.queueMicrotask!(
+                JSOneshotClosure { _ in
+                    self.run()
+                    return JSValue.undefined
+                }
+            )
         }
 
         /// Run the worker
@@ -251,12 +287,27 @@ public final class WebWorkerTaskExecutor: TaskExecutor {
             trace("Worker.start tid=\(tid)")
         }
 
+        /// On receiving a wake-up message from other thread
+        func wakeUpFromOtherThread() {
+            let (exchanged, _) = state.compareExchange(
+                expected: .ready,
+                desired: .running,
+                ordering: .sequentiallyConsistent
+            )
+            guard exchanged else {
+                // `Worker/run` was scheduled on the thread before JS event loop starts
+                // a macrotask handling wake-up message.
+                return
+            }
+            run()
+        }
+
         /// Process jobs in the queue.
         ///
         /// Return when the worker has no more jobs to run or terminated.
         /// This method must be called from the worker thread after the worker
         /// is started by `start(executor:)`.
-        func run() {
+        private func run() {
             trace("Worker.run")
             guard let executor = parentTaskExecutor else {
                 preconditionFailure("The worker must be started with a parent executor.")
@@ -276,19 +327,23 @@ public final class WebWorkerTaskExecutor: TaskExecutor {
                         queue.removeFirst()
                         return job
                     }
-                    // No more jobs to run now. Wait for a new job to be enqueued.
-                    let (exchanged, original) = state.compareExchange(expected: .running, desired: .idle, ordering: .sequentiallyConsistent)
+                    // No more jobs to run now.
+                    let (exchanged, original) = state.compareExchange(
+                        expected: .running,
+                        desired: .idle,
+                        ordering: .sequentiallyConsistent
+                    )
 
                     switch (exchanged, original) {
                     case (true, _):
                         trace("Worker.run exited \(original) -> idle")
-                        return nil // Regular case
-                    case (false, .idle):
+                        return nil  // Regular case
+                    case (false, .idle), (false, .ready):
                         preconditionFailure("unreachable: Worker/run running in multiple threads!?")
                     case (false, .running):
                         preconditionFailure("unreachable: running -> idle should return exchanged=true")
                     case (false, .terminated):
-                        return nil // The worker is terminated, exit the loop.
+                        return nil  // The worker is terminated, exit the loop.
                     }
                 }
                 guard let job else { return }
@@ -340,24 +395,43 @@ public final class WebWorkerTaskExecutor: TaskExecutor {
                 }
             }
             trace("Executor.start")
+
+            // Hold over-retained contexts until all worker threads are started.
+            var contexts: [Unmanaged] = []
+            defer {
+                for context in contexts {
+                    context.release()
+                }
+            }
             // Start worker threads via pthread_create.
             for worker in workers {
                 // NOTE: The context must be allocated on the heap because
                 // `pthread_create` on WASI does not guarantee the thread is started
                 // immediately. The context must be retained until the thread is started.
                 let context = Context(executor: self, worker: worker)
-                let ptr = Unmanaged.passRetained(context).toOpaque()
-                let ret = pthread_create(nil, nil, { ptr in
-                    // Cast to a optional pointer to absorb nullability variations between platforms.
-                    let ptr: UnsafeMutableRawPointer? = ptr
-                    let context = Unmanaged.fromOpaque(ptr!).takeRetainedValue()
-                    context.worker.start(executor: context.executor)
-                    // The worker is started. Throw JS exception to unwind the call stack without
-                    // reaching the `pthread_exit`, which is called immediately after this block.
-                    swjs_unsafe_event_loop_yield()
-                    return nil
-                }, ptr)
-                precondition(ret == 0, "Failed to create a thread")
+                let unmanagedContext = Unmanaged.passRetained(context)
+                contexts.append(unmanagedContext)
+                let ptr = unmanagedContext.toOpaque()
+                var thread = pthread_t(bitPattern: 0)
+                let ret = pthread_create(
+                    &thread,
+                    nil,
+                    { ptr in
+                        // Cast to a optional pointer to absorb nullability variations between platforms.
+                        let ptr: UnsafeMutableRawPointer? = ptr
+                        let context = Unmanaged.fromOpaque(ptr!).takeUnretainedValue()
+                        context.worker.start(executor: context.executor)
+                        // The worker is started. Throw JS exception to unwind the call stack without
+                        // reaching the `pthread_exit`, which is called immediately after this block.
+                        swjs_unsafe_event_loop_yield()
+                        return nil
+                    },
+                    ptr
+                )
+                guard ret == 0 else {
+                    let strerror = String(cString: strerror(ret))
+                    throw SpawnError(reason: "Failed to create a thread (\(ret): \(strerror))")
+                }
             }
             // Wait until all worker threads are started and wire up messaging channels
             // between the main thread and workers to notify job enqueuing events each other.
@@ -367,7 +441,9 @@ public final class WebWorkerTaskExecutor: TaskExecutor {
                 var tid: pid_t
                 repeat {
                     if workerInitStarted.duration(to: .now) > timeout {
-                        fatalError("Worker thread initialization timeout exceeded (\(timeout))")
+                        throw SpawnError(
+                            reason: "Worker thread initialization timeout exceeded (\(timeout))"
+                        )
                     }
                     tid = worker.tid.load(ordering: .sequentiallyConsistent)
                     try await clock.sleep(for: checkInterval)
@@ -432,15 +508,19 @@ public final class WebWorkerTaskExecutor: TaskExecutor {
     ///   - timeout: The maximum time to wait for all worker threads to be started. Default is 3 seconds.
     ///   - checkInterval: The interval to check if all worker threads are started. Default is 5 microseconds.
     /// - Throws: An error if any worker thread fails to initialize within the timeout period.
-    public init(numberOfThreads: Int, timeout: Duration = .seconds(3), checkInterval: Duration = .microseconds(5)) async throws {
+    public init(
+        numberOfThreads: Int,
+        timeout: Duration = .seconds(3),
+        checkInterval: Duration = .microseconds(5)
+    ) async throws {
         self.executor = Executor(numberOfThreads: numberOfThreads)
         try await self.executor.start(timeout: timeout, checkInterval: checkInterval)
     }
 
     /// Terminates all worker threads managed by this executor.
     ///
-    /// This method should be called when the executor is no longer needed to free up 
-    /// resources. After calling this method, any tasks enqueued to this executor will 
+    /// This method should be called when the executor is no longer needed to free up
+    /// resources. After calling this method, any tasks enqueued to this executor will
     /// be ignored and may never complete.
     ///
     /// It's recommended to use a `defer` statement immediately after creating the executor
@@ -523,77 +603,14 @@ public final class WebWorkerTaskExecutor: TaskExecutor {
     internal func dumpStats() {}
     #endif
 
-    // MARK: Global Executor hack
-
-    @MainActor private static var _mainThread: pthread_t?
-    @MainActor private static var _swift_task_enqueueGlobal_hook_original: UnsafeMutableRawPointer?
-    @MainActor private static var _swift_task_enqueueGlobalWithDelay_hook_original: UnsafeMutableRawPointer?
-    @MainActor private static var _swift_task_enqueueGlobalWithDeadline_hook_original: UnsafeMutableRawPointer?
-
-    /// Installs a global executor that forwards jobs from Web Worker threads to the main thread.
-    ///
-    /// This method sets up the necessary hooks to ensure proper task scheduling between
-    /// the main thread and worker threads. It must be called once (typically at application 
-    /// startup) before using any `WebWorkerTaskExecutor` instances.
-    ///
-    /// ## Example
-    ///
-    /// ```swift
-    /// // At application startup
-    /// WebWorkerTaskExecutor.installGlobalExecutor()
-    ///
-    /// // Later, create and use executor instances
-    /// let executor = try await WebWorkerTaskExecutor(numberOfThreads: 4)
-    /// ```
-    ///
-    /// - Important: This method must be called from the main thread.
-    public static func installGlobalExecutor() {
-        MainActor.assumeIsolated {
-            installGlobalExecutorIsolated()
-        }
-    }
-
-    @MainActor
-    static func installGlobalExecutorIsolated() {
-        #if canImport(wasi_pthread) && compiler(>=6.1) && _runtime(_multithreaded)
-        // Ensure this function is called only once.
-        guard _mainThread == nil else { return }
-
-        _mainThread = pthread_self()
-        assert(swjs_get_worker_thread_id() == -1, "\(#function) must be called on the main thread")
-
-        _swift_task_enqueueGlobal_hook_original = swift_task_enqueueGlobal_hook
-
-        typealias swift_task_enqueueGlobal_hook_Fn = @convention(thin) (UnownedJob, swift_task_enqueueGlobal_original) -> Void
-        let swift_task_enqueueGlobal_hook_impl: swift_task_enqueueGlobal_hook_Fn = { job, base in
-            WebWorkerTaskExecutor.traceStatsIncrement(\.enqueueGlobal)
-            // Enter this block only if the current Task has no executor preference.
-            if pthread_equal(pthread_self(), WebWorkerTaskExecutor._mainThread) != 0 {
-                // If the current thread is the main thread, delegate the job
-                // execution to the original hook of JavaScriptEventLoop.
-                let original = unsafeBitCast(WebWorkerTaskExecutor._swift_task_enqueueGlobal_hook_original, to: swift_task_enqueueGlobal_hook_Fn.self)
-                original(job, base)
-            } else {
-                // Notify the main thread to execute the job when a job is
-                // enqueued from a Web Worker thread but without an executor preference.
-                // This is usually the case when hopping back to the main thread
-                // at the end of a task.
-                WebWorkerTaskExecutor.traceStatsIncrement(\.sendJobToMainThread)
-                let jobBitPattern = unsafeBitCast(job, to: UInt.self)
-                swjs_send_job_to_main_thread(jobBitPattern)
-            }
-        }
-        swift_task_enqueueGlobal_hook = unsafeBitCast(swift_task_enqueueGlobal_hook_impl, to: UnsafeMutableRawPointer?.self)
-        #else
-        fatalError("Unsupported platform")
-        #endif
-    }
+    @available(*, deprecated, message: "Not needed anymore, just use `JavaScriptEventLoop.installGlobalExecutor()`.")
+    public static func installGlobalExecutor() {}
 }
 
 /// Enqueue a job scheduled from a Web Worker thread to the main thread.
 /// This function is called when a job is enqueued from a Web Worker thread.
 @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *)
-#if compiler(>=6.1) // @_expose and @_extern are only available in Swift 6.1+
+#if compiler(>=6.1)  // @_expose and @_extern are only available in Swift 6.1+
 @_expose(wasm, "swjs_enqueue_main_job_from_worker")
 #endif
 func _swjs_enqueue_main_job_from_worker(_ job: UnownedJob) {
@@ -604,17 +621,17 @@ func _swjs_enqueue_main_job_from_worker(_ job: UnownedJob) {
 /// Wake up the worker thread.
 /// This function is called when a job is enqueued from the main thread to a worker thread.
 @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *)
-#if compiler(>=6.1) // @_expose and @_extern are only available in Swift 6.1+
+#if compiler(>=6.1)  // @_expose and @_extern are only available in Swift 6.1+
 @_expose(wasm, "swjs_wake_worker_thread")
 #endif
 func _swjs_wake_worker_thread() {
-    WebWorkerTaskExecutor.Worker.currentThread!.run()
+    WebWorkerTaskExecutor.Worker.currentThread!.wakeUpFromOtherThread()
 }
 
-fileprivate func trace(_ message: String) {
-#if JAVASCRIPTKIT_TRACE
-    JSObject.global.process.stdout.write("[trace tid=\(swjs_get_worker_thread_id())] \(message)\n")
-#endif
+private func trace(_ message: String) {
+    #if JAVASCRIPTKIT_TRACE
+    _ = JSObject.global.console.warn("[trace tid=\(swjs_get_worker_thread_id())] \(message)\n")
+    #endif
 }
 
-#endif // compiler(>=6.0)
+#endif  // compiler(>=6.0)
diff --git a/Sources/JavaScriptEventLoopTestSupport/JavaScriptEventLoopTestSupport.swift b/Sources/JavaScriptEventLoopTestSupport/JavaScriptEventLoopTestSupport.swift
index 0582fe8c4..4c441f3c4 100644
--- a/Sources/JavaScriptEventLoopTestSupport/JavaScriptEventLoopTestSupport.swift
+++ b/Sources/JavaScriptEventLoopTestSupport/JavaScriptEventLoopTestSupport.swift
@@ -27,11 +27,6 @@ import JavaScriptEventLoop
 func swift_javascriptkit_activate_js_executor_impl() {
     MainActor.assumeIsolated {
         JavaScriptEventLoop.installGlobalExecutor()
-        #if canImport(wasi_pthread) && compiler(>=6.1) && _runtime(_multithreaded)
-        if #available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) {
-            WebWorkerTaskExecutor.installGlobalExecutor()
-        }
-        #endif
     }
 }
 
diff --git a/Sources/JavaScriptKit/BasicObjects/JSArray.swift b/Sources/JavaScriptKit/BasicObjects/JSArray.swift
index 56345d085..fad602465 100644
--- a/Sources/JavaScriptKit/BasicObjects/JSArray.swift
+++ b/Sources/JavaScriptKit/BasicObjects/JSArray.swift
@@ -98,8 +98,8 @@ private func getObjectValuesLength(_ object: JSObject) -> Int {
     return Int(values.length.number!)
 }
 
-public extension JSValue {
-    var array: JSArray? {
+extension JSValue {
+    public var array: JSArray? {
         object.flatMap(JSArray.init)
     }
 }
diff --git a/Sources/JavaScriptKit/BasicObjects/JSDate.swift b/Sources/JavaScriptKit/BasicObjects/JSDate.swift
index c8a6623a1..9157796bc 100644
--- a/Sources/JavaScriptKit/BasicObjects/JSDate.swift
+++ b/Sources/JavaScriptKit/BasicObjects/JSDate.swift
@@ -1,11 +1,10 @@
-/** A wrapper around the [JavaScript `Date`
- class](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date) that
- exposes its properties in a type-safe way. This doesn't 100% match the JS API, for example
- `getMonth`/`setMonth` etc accessor methods are converted to properties, but the rest of it matches
- in the naming. Parts of the JavaScript `Date` API that are not consistent across browsers and JS
- implementations are not exposed in a type-safe manner, you should access the underlying `jsObject`
- property if you need those.
- */
+/// A wrapper around the [JavaScript `Date`
+/// class](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date) that
+/// exposes its properties in a type-safe way. This doesn't 100% match the JS API, for example
+/// `getMonth`/`setMonth` etc accessor methods are converted to properties, but the rest of it matches
+/// in the naming. Parts of the JavaScript `Date` API that are not consistent across browsers and JS
+/// implementations are not exposed in a type-safe manner, you should access the underlying `jsObject`
+/// property if you need those.
 public final class JSDate: JSBridgedClass {
     /// The constructor function used to create new `Date` objects.
     public static var constructor: JSFunction? { _constructor.wrappedValue }
diff --git a/Sources/JavaScriptKit/BasicObjects/JSError.swift b/Sources/JavaScriptKit/BasicObjects/JSError.swift
index 0f87d3c67..38accb97b 100644
--- a/Sources/JavaScriptKit/BasicObjects/JSError.swift
+++ b/Sources/JavaScriptKit/BasicObjects/JSError.swift
@@ -1,7 +1,6 @@
-/** A wrapper around [the JavaScript `Error`
- class](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Error) that
- exposes its properties in a type-safe way.
- */
+/// A wrapper around [the JavaScript `Error`
+/// class](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Error) that
+/// exposes its properties in a type-safe way.
 public final class JSError: JSBridgedClass {
     /// The constructor function used to create new JavaScript `Error` objects.
     public static var constructor: JSFunction? { _constructor.wrappedValue }
diff --git a/Sources/JavaScriptKit/BasicObjects/JSPromise.swift b/Sources/JavaScriptKit/BasicObjects/JSPromise.swift
index cfe32d515..24a9ae482 100644
--- a/Sources/JavaScriptKit/BasicObjects/JSPromise.swift
+++ b/Sources/JavaScriptKit/BasicObjects/JSPromise.swift
@@ -27,7 +27,7 @@ public final class JSPromise: JSBridgedClass {
     /// is not an object and is not an instance of JavaScript `Promise`, this function will
     /// return `nil`.
     public static func construct(from value: JSValue) -> Self? {
-        guard case let .object(jsObject) = value else { return nil }
+        guard case .object(let jsObject) = value else { return nil }
         return Self(jsObject)
     }
 
@@ -55,9 +55,9 @@ public final class JSPromise: JSBridgedClass {
 
             resolver {
                 switch $0 {
-                case let .success(success):
+                case .success(let success):
                     resolve(success)
-                case let .failure(error):
+                case .failure(let error):
                     reject(error)
                 }
             }
@@ -66,7 +66,7 @@ public final class JSPromise: JSBridgedClass {
         self.init(unsafelyWrapping: Self.constructor!.new(closure))
     }
 
-#if !hasFeature(Embedded)
+    #if !hasFeature(Embedded)
     public static func resolve(_ value: ConvertibleToJSValue) -> JSPromise {
         self.init(unsafelyWrapping: Self.constructor!.resolve!(value).object!)
     }
@@ -74,7 +74,7 @@ public final class JSPromise: JSBridgedClass {
     public static func reject(_ reason: ConvertibleToJSValue) -> JSPromise {
         self.init(unsafelyWrapping: Self.constructor!.reject!(reason).object!)
     }
-#else
+    #else
     public static func resolve(_ value: some ConvertibleToJSValue) -> JSPromise {
         self.init(unsafelyWrapping: constructor!.resolve!(value).object!)
     }
@@ -82,35 +82,36 @@ public final class JSPromise: JSBridgedClass {
     public static func reject(_ reason: some ConvertibleToJSValue) -> JSPromise {
         self.init(unsafelyWrapping: constructor!.reject!(reason).object!)
     }
-#endif
+    #endif
 
-#if !hasFeature(Embedded)
     /// Schedules the `success` closure to be invoked on successful completion of `self`.
     @discardableResult
-    public func then(success: @escaping (JSValue) -> ConvertibleToJSValue) -> JSPromise {
+    public func then(success: @escaping (JSValue) -> JSValue) -> JSPromise {
         let closure = JSOneshotClosure {
             success($0[0]).jsValue
         }
         return JSPromise(unsafelyWrapping: jsObject.then!(closure).object!)
     }
 
-    #if compiler(>=5.5)
-        /// Schedules the `success` closure to be invoked on successful completion of `self`.
-        @available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *)
-        @discardableResult
-        public func then(success: sending @escaping (sending JSValue) async throws -> ConvertibleToJSValue) -> JSPromise {
-            let closure = JSOneshotClosure.async {
-                try await success($0[0]).jsValue
-            }
-            return JSPromise(unsafelyWrapping: jsObject.then!(closure).object!)
+    #if compiler(>=5.5) && (!hasFeature(Embedded) || os(WASI))
+    /// Schedules the `success` closure to be invoked on successful completion of `self`.
+    @available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *)
+    @discardableResult
+    public func then(
+        success: sending @escaping (sending JSValue) async throws(JSException) -> JSValue
+    ) -> JSPromise {
+        let closure = JSOneshotClosure.async { arguments throws(JSException) -> JSValue in
+            return try await success(arguments[0])
         }
+        return JSPromise(unsafelyWrapping: jsObject.then!(closure).object!)
+    }
     #endif
 
     /// Schedules the `success` closure to be invoked on successful completion of `self`.
     @discardableResult
     public func then(
-        success: @escaping (sending JSValue) -> ConvertibleToJSValue,
-        failure: @escaping (sending JSValue) -> ConvertibleToJSValue
+        success: @escaping (sending JSValue) -> JSValue,
+        failure: @escaping (sending JSValue) -> JSValue
     ) -> JSPromise {
         let successClosure = JSOneshotClosure {
             success($0[0]).jsValue
@@ -121,42 +122,49 @@ public final class JSPromise: JSBridgedClass {
         return JSPromise(unsafelyWrapping: jsObject.then!(successClosure, failureClosure).object!)
     }
 
-    #if compiler(>=5.5)
-        /// Schedules the `success` closure to be invoked on successful completion of `self`.
-        @available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *)
-        @discardableResult
-        public func then(success: sending @escaping (sending JSValue) async throws -> ConvertibleToJSValue,
-                         failure: sending @escaping (sending JSValue) async throws -> ConvertibleToJSValue) -> JSPromise
-        {
-            let successClosure = JSOneshotClosure.async {
-                try await success($0[0]).jsValue
-            }
-            let failureClosure = JSOneshotClosure.async {
-                try await failure($0[0]).jsValue
-            }
-            return JSPromise(unsafelyWrapping: jsObject.then!(successClosure, failureClosure).object!)
+    #if compiler(>=5.5) && (!hasFeature(Embedded) || os(WASI))
+    /// Schedules the `success` closure to be invoked on successful completion of `self`.
+    @available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *)
+    @discardableResult
+    public func then(
+        success: sending @escaping (sending JSValue) async throws(JSException) -> JSValue,
+        failure: sending @escaping (sending JSValue) async throws(JSException) -> JSValue
+    ) -> JSPromise {
+        let successClosure = JSOneshotClosure.async { arguments throws(JSException) -> JSValue in
+            try await success(arguments[0]).jsValue
+        }
+        let failureClosure = JSOneshotClosure.async { arguments throws(JSException) -> JSValue in
+            try await failure(arguments[0]).jsValue
         }
+        return JSPromise(unsafelyWrapping: jsObject.then!(successClosure, failureClosure).object!)
+    }
     #endif
 
     /// Schedules the `failure` closure to be invoked on rejected completion of `self`.
     @discardableResult
-    public func `catch`(failure: @escaping (sending JSValue) -> ConvertibleToJSValue) -> JSPromise {
+    public func `catch`(
+        failure: @escaping (sending JSValue) -> JSValue
+    )
+        -> JSPromise
+    {
         let closure = JSOneshotClosure {
             failure($0[0]).jsValue
         }
         return .init(unsafelyWrapping: jsObject.catch!(closure).object!)
     }
 
-    #if compiler(>=5.5)
-        /// Schedules the `failure` closure to be invoked on rejected completion of `self`.
-        @available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *)
-        @discardableResult
-        public func `catch`(failure: sending @escaping (sending JSValue) async throws -> ConvertibleToJSValue) -> JSPromise {
-            let closure = JSOneshotClosure.async {
-                try await failure($0[0]).jsValue
-            }
-            return .init(unsafelyWrapping: jsObject.catch!(closure).object!)
+    #if compiler(>=5.5) && (!hasFeature(Embedded) || os(WASI))
+    /// Schedules the `failure` closure to be invoked on rejected completion of `self`.
+    @available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *)
+    @discardableResult
+    public func `catch`(
+        failure: sending @escaping (sending JSValue) async throws(JSException) -> JSValue
+    ) -> JSPromise {
+        let closure = JSOneshotClosure.async { arguments throws(JSException) -> JSValue in
+            try await failure(arguments[0]).jsValue
         }
+        return .init(unsafelyWrapping: jsObject.catch!(closure).object!)
+    }
     #endif
 
     /// Schedules the `failure` closure to be invoked on either successful or rejected
@@ -169,5 +177,4 @@ public final class JSPromise: JSBridgedClass {
         }
         return .init(unsafelyWrapping: jsObject.finally!(closure).object!)
     }
-#endif
 }
diff --git a/Sources/JavaScriptKit/BasicObjects/JSTimer.swift b/Sources/JavaScriptKit/BasicObjects/JSTimer.swift
index 231792a84..3655a185c 100644
--- a/Sources/JavaScriptKit/BasicObjects/JSTimer.swift
+++ b/Sources/JavaScriptKit/BasicObjects/JSTimer.swift
@@ -1,15 +1,14 @@
-/** This timer is an abstraction over [`setInterval`](https://developer.mozilla.org/en-US/docs/Web/API/WindowOrWorkerGlobalScope/setInterval)
-/ [`clearInterval`](https://developer.mozilla.org/en-US/docs/Web/API/WindowOrWorkerGlobalScope/clearInterval) and
-[`setTimeout`](https://developer.mozilla.org/en-US/docs/Web/API/WindowOrWorkerGlobalScope/setTimeout)
-/ [`clearTimeout`](https://developer.mozilla.org/en-US/docs/Web/API/WindowOrWorkerGlobalScope/setTimeout)
-JavaScript functions. It intentionally doesn't match the JavaScript API, as a special care is
-needed to hold a reference to the timer closure and to call `JSClosure.release()` on it when the
-timer is deallocated. As a user, you have to hold a reference to a `JSTimer` instance for it to stay
-valid. The `JSTimer` API is also intentionally trivial, the timer is started right away, and the
-only way to invalidate the timer is to bring the reference count of the `JSTimer` instance to zero.
-For invalidation you should either store the timer in an optional property and assign `nil` to it,
-or deallocate the object that owns the timer.
-*/
+/// This timer is an abstraction over [`setInterval`](https://developer.mozilla.org/en-US/docs/Web/API/WindowOrWorkerGlobalScope/setInterval)
+/// / [`clearInterval`](https://developer.mozilla.org/en-US/docs/Web/API/WindowOrWorkerGlobalScope/clearInterval) and
+/// [`setTimeout`](https://developer.mozilla.org/en-US/docs/Web/API/WindowOrWorkerGlobalScope/setTimeout)
+/// / [`clearTimeout`](https://developer.mozilla.org/en-US/docs/Web/API/WindowOrWorkerGlobalScope/setTimeout)
+/// JavaScript functions. It intentionally doesn't match the JavaScript API, as a special care is
+/// needed to hold a reference to the timer closure and to call `JSClosure.release()` on it when the
+/// timer is deallocated. As a user, you have to hold a reference to a `JSTimer` instance for it to stay
+/// valid. The `JSTimer` API is also intentionally trivial, the timer is started right away, and the
+/// only way to invalidate the timer is to bring the reference count of the `JSTimer` instance to zero.
+/// For invalidation you should either store the timer in an optional property and assign `nil` to it,
+/// or deallocate the object that owns the timer.
 public final class JSTimer {
     enum ClosureStorage {
         case oneshot(JSOneshotClosure)
@@ -27,11 +26,11 @@ public final class JSTimer {
             case .oneshot(let closure):
                 closure.release()
             case .repeating(let closure):
-#if JAVASCRIPTKIT_WITHOUT_WEAKREFS
+                #if JAVASCRIPTKIT_WITHOUT_WEAKREFS
                 closure.release()
-#else
+                #else
                 break  // no-op
-#endif
+                #endif
             }
         }
     }
@@ -58,17 +57,21 @@ public final class JSTimer {
      `millisecondsDelay` intervals indefinitely until the timer is deallocated.
      - callback: the closure to be executed after a given `millisecondsDelay` interval.
      */
-    public init(millisecondsDelay: Double, isRepeating: Bool = false, callback: @escaping () -> ()) {
+    public init(millisecondsDelay: Double, isRepeating: Bool = false, callback: @escaping () -> Void) {
         if isRepeating {
-            closure = .repeating(JSClosure { _ in
-                callback()
-                return .undefined
-            })
+            closure = .repeating(
+                JSClosure { _ in
+                    callback()
+                    return .undefined
+                }
+            )
         } else {
-            closure = .oneshot(JSOneshotClosure { _ in
-                callback()
-                return .undefined
-            })
+            closure = .oneshot(
+                JSOneshotClosure { _ in
+                    callback()
+                    return .undefined
+                }
+            )
         }
         self.isRepeating = isRepeating
         if isRepeating {
diff --git a/Sources/JavaScriptKit/BasicObjects/JSTypedArray.swift b/Sources/JavaScriptKit/BasicObjects/JSTypedArray.swift
index dec834bbd..47919b17d 100644
--- a/Sources/JavaScriptKit/BasicObjects/JSTypedArray.swift
+++ b/Sources/JavaScriptKit/BasicObjects/JSTypedArray.swift
@@ -1,11 +1,11 @@
 //
 //  Created by Manuel Burghard. Licensed unter MIT.
 //
-#if !hasFeature(Embedded)
 import _CJavaScriptKit
 
 /// A protocol that allows a Swift numeric type to be mapped to the JavaScript TypedArray that holds integers of its type
-public protocol TypedArrayElement: ConvertibleToJSValue, ConstructibleFromJSValue {
+public protocol TypedArrayElement {
+    associatedtype Element: ConvertibleToJSValue, ConstructibleFromJSValue = Self
     /// The constructor function for the TypedArray class for this particular kind of number
     static var typedArrayClass: JSFunction { get }
 }
@@ -13,8 +13,9 @@ public protocol TypedArrayElement: ConvertibleToJSValue, ConstructibleFromJSValu
 /// A wrapper around all [JavaScript `TypedArray`
 /// classes](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/TypedArray)
 /// that exposes their properties in a type-safe way.
-public class JSTypedArray: JSBridgedClass, ExpressibleByArrayLiteral where Element: TypedArrayElement {
-    public class var constructor: JSFunction? { Element.typedArrayClass }
+public final class JSTypedArray: JSBridgedClass, ExpressibleByArrayLiteral where Traits: TypedArrayElement {
+    public typealias Element = Traits.Element
+    public class var constructor: JSFunction? { Traits.typedArrayClass }
     public var jsObject: JSObject
 
     public subscript(_ index: Int) -> Element {
@@ -46,13 +47,10 @@ public class JSTypedArray: JSBridgedClass, ExpressibleByArrayLiteral wh
     ///
     /// - Parameter array: The array that will be copied to create a new instance of TypedArray
     public convenience init(_ array: [Element]) {
-        let jsArrayRef = array.withUnsafeBufferPointer { ptr in
-            // Retain the constructor function to avoid it being released before calling `swjs_create_typed_array`
-            withExtendedLifetime(Self.constructor!) { ctor in
-                swjs_create_typed_array(ctor.id, ptr.baseAddress, Int32(array.count))
-            }
+        let object = array.withUnsafeBufferPointer { buffer in
+            Self.createTypedArray(from: buffer)
         }
-        self.init(unsafelyWrapping: JSObject(id: jsArrayRef))
+        self.init(unsafelyWrapping: object)
     }
 
     /// Convenience initializer for `Sequence`.
@@ -60,6 +58,21 @@ public class JSTypedArray: JSBridgedClass, ExpressibleByArrayLiteral wh
         self.init(Array(sequence))
     }
 
+    /// Initialize a new instance of TypedArray in JavaScript environment with given buffer contents.
+    ///
+    /// - Parameter buffer: The buffer that will be copied to create a new instance of TypedArray
+    public convenience init(buffer: UnsafeBufferPointer) {
+        self.init(unsafelyWrapping: Self.createTypedArray(from: buffer))
+    }
+
+    private static func createTypedArray(from buffer: UnsafeBufferPointer) -> JSObject {
+        // Retain the constructor function to avoid it being released before calling `swjs_create_typed_array`
+        let jsArrayRef = withExtendedLifetime(Self.constructor!) { ctor in
+            swjs_create_typed_array(ctor.id, buffer.baseAddress, Int32(buffer.count))
+        }
+        return JSObject(id: jsArrayRef)
+    }
+
     /// Length (in bytes) of the typed array.
     /// The value is established when a TypedArray is constructed and cannot be changed.
     /// If the TypedArray is not specifying a `byteOffset` or a `length`, the `length` of the referenced `ArrayBuffer` will be returned.
@@ -67,6 +80,11 @@ public class JSTypedArray: JSBridgedClass, ExpressibleByArrayLiteral wh
         Int(jsObject["byteLength"].number!)
     }
 
+    /// Length (in elements) of the typed array.
+    public var length: Int {
+        Int(jsObject["length"].number!)
+    }
+
     /// Calls the given closure with a pointer to a copy of the underlying bytes of the
     /// array's storage.
     ///
@@ -81,79 +99,69 @@ public class JSTypedArray: JSBridgedClass, ExpressibleByArrayLiteral wh
     ///   argument is valid only for the duration of the closure's execution.
     /// - Returns: The return value, if any, of the `body` closure parameter.
     public func withUnsafeBytes(_ body: (UnsafeBufferPointer) throws -> R) rethrows -> R {
-        let bytesLength = lengthInBytes
-        let rawBuffer = UnsafeMutableBufferPointer.allocate(capacity: bytesLength)
-        defer { rawBuffer.deallocate() }
-        let baseAddress = rawBuffer.baseAddress!
-        swjs_load_typed_array(jsObject.id, baseAddress)
-        let length = bytesLength / MemoryLayout.size
-        let rawBaseAddress = UnsafeRawPointer(baseAddress)
-        let bufferPtr = UnsafeBufferPointer(
-            start: rawBaseAddress.assumingMemoryBound(to: Element.self),
-            count: length
-        )
-        let result = try body(bufferPtr)
+        let buffer = UnsafeMutableBufferPointer.allocate(capacity: length)
+        defer { buffer.deallocate() }
+        copyMemory(to: buffer)
+        let result = try body(UnsafeBufferPointer(buffer))
         return result
     }
 
     #if compiler(>=5.5)
-        /// Calls the given async closure with a pointer to a copy of the underlying bytes of the
-        /// array's storage.
-        ///
-        /// - Note: The pointer passed as an argument to `body` is valid only for the
-        /// lifetime of the closure. Do not escape it from the async closure for later
-        /// use.
-        ///
-        /// - Parameter body: A closure with an `UnsafeBufferPointer` parameter
-        ///   that points to the contiguous storage for the array.
-        ///    If `body` has a return value, that value is also
-        ///   used as the return value for the `withUnsafeBytes(_:)` method. The
-        ///   argument is valid only for the duration of the closure's execution.
-        /// - Returns: The return value, if any, of the `body`async closure parameter.
-        @available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *)
-        public func withUnsafeBytesAsync(_ body: (UnsafeBufferPointer) async throws -> R) async rethrows -> R {
-            let bytesLength = lengthInBytes
-            let rawBuffer = UnsafeMutableBufferPointer.allocate(capacity: bytesLength)
-            defer { rawBuffer.deallocate() }
-            let baseAddress = rawBuffer.baseAddress!
-            swjs_load_typed_array(jsObject.id, baseAddress)
-            let length = bytesLength / MemoryLayout.size
-            let rawBaseAddress = UnsafeRawPointer(baseAddress)
-            let bufferPtr = UnsafeBufferPointer(
-                start: rawBaseAddress.assumingMemoryBound(to: Element.self),
-                count: length
-            )
-            let result = try await body(bufferPtr)
-            return result
-        }
+    /// Calls the given async closure with a pointer to a copy of the underlying bytes of the
+    /// array's storage.
+    ///
+    /// - Note: The pointer passed as an argument to `body` is valid only for the
+    /// lifetime of the closure. Do not escape it from the async closure for later
+    /// use.
+    ///
+    /// - Parameter body: A closure with an `UnsafeBufferPointer` parameter
+    ///   that points to the contiguous storage for the array.
+    ///    If `body` has a return value, that value is also
+    ///   used as the return value for the `withUnsafeBytes(_:)` method. The
+    ///   argument is valid only for the duration of the closure's execution.
+    /// - Returns: The return value, if any, of the `body`async closure parameter.
+    @available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *)
+    public func withUnsafeBytesAsync(_ body: (UnsafeBufferPointer) async throws -> R) async rethrows -> R {
+        let buffer = UnsafeMutableBufferPointer.allocate(capacity: length)
+        defer { buffer.deallocate() }
+        copyMemory(to: buffer)
+        let result = try await body(UnsafeBufferPointer(buffer))
+        return result
+    }
     #endif
-}
 
-// MARK: - Int and UInt support
-
-// FIXME: Should be updated to support wasm64 when that becomes available.
-func valueForBitWidth(typeName: String, bitWidth: Int, when32: T) -> T {
-    if bitWidth == 32 {
-        return when32
-    } else if bitWidth == 64 {
-        fatalError("64-bit \(typeName)s are not yet supported in JSTypedArray")
-    } else {
-        fatalError("Unsupported bit width for type \(typeName): \(bitWidth) (hint: stick to fixed-size \(typeName)s to avoid this issue)")
+    /// Copies the contents of the array to the given buffer.
+    ///
+    /// - Parameter buffer: The buffer to copy the contents of the array to.
+    ///   The buffer must have enough space to accommodate the contents of the array.
+    public func copyMemory(to buffer: UnsafeMutableBufferPointer) {
+        precondition(buffer.count >= length, "Buffer is too small to hold the contents of the array")
+        swjs_load_typed_array(jsObject.id, buffer.baseAddress!)
     }
 }
 
 extension Int: TypedArrayElement {
-    public static var typedArrayClass: JSFunction { _typedArrayClass.wrappedValue }
-    private static let _typedArrayClass = LazyThreadLocal(initialize: {
-        valueForBitWidth(typeName: "Int", bitWidth: Int.bitWidth, when32: JSObject.global.Int32Array).function!
-    })
+    public static var typedArrayClass: JSFunction {
+        #if _pointerBitWidth(_32)
+        return JSObject.global.Int32Array.function!
+        #elseif _pointerBitWidth(_64)
+        return JSObject.global.Int64Array.function!
+        #else
+        #error("Unsupported pointer width")
+        #endif
+    }
 }
 
 extension UInt: TypedArrayElement {
-    public static var typedArrayClass: JSFunction { _typedArrayClass.wrappedValue }
-    private static let _typedArrayClass = LazyThreadLocal(initialize: {
-        valueForBitWidth(typeName: "UInt", bitWidth: Int.bitWidth, when32: JSObject.global.Uint32Array).function!
-    })
+    public static var typedArrayClass: JSFunction {
+        #if _pointerBitWidth(_32)
+        return JSObject.global.Uint32Array.function!
+        #elseif _pointerBitWidth(_64)
+        return JSObject.global.Uint64Array.function!
+        #else
+        #error("Unsupported pointer width")
+        #endif
+    }
 }
 
 extension Int8: TypedArrayElement {
@@ -164,13 +172,6 @@ extension UInt8: TypedArrayElement {
     public static var typedArrayClass: JSFunction { JSObject.global.Uint8Array.function! }
 }
 
-/// A wrapper around [the JavaScript `Uint8ClampedArray`
-/// class](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Array)
-/// that exposes its properties in a type-safe and Swifty way.
-public class JSUInt8ClampedArray: JSTypedArray {
-    override public class var constructor: JSFunction? { JSObject.global.Uint8ClampedArray.function! }
-}
-
 extension Int16: TypedArrayElement {
     public static var typedArrayClass: JSFunction { JSObject.global.Int16Array.function! }
 }
@@ -194,4 +195,10 @@ extension Float32: TypedArrayElement {
 extension Float64: TypedArrayElement {
     public static var typedArrayClass: JSFunction { JSObject.global.Float64Array.function! }
 }
-#endif
+
+public enum JSUInt8Clamped: TypedArrayElement {
+    public typealias Element = UInt8
+    public static var typedArrayClass: JSFunction { JSObject.global.Uint8ClampedArray.function! }
+}
+
+public typealias JSUInt8ClampedArray = JSTypedArray
diff --git a/Sources/JavaScriptKit/ConstructibleFromJSValue.swift b/Sources/JavaScriptKit/ConstructibleFromJSValue.swift
index f0e0ad431..d4a5921cd 100644
--- a/Sources/JavaScriptKit/ConstructibleFromJSValue.swift
+++ b/Sources/JavaScriptKit/ConstructibleFromJSValue.swift
@@ -68,11 +68,11 @@ extension SignedInteger where Self: ConstructibleFromJSValue {
         if let number = value.number {
             return Self(exactly: number.rounded(.towardZero))
         }
-#if !hasFeature(Embedded)
+        #if !hasFeature(Embedded)
         if let bigInt = value.bigInt as? JSBigIntExtended {
             return Self(exactly: bigInt)
         }
-#endif
+        #endif
         return nil
     }
 }
@@ -116,11 +116,11 @@ extension UnsignedInteger where Self: ConstructibleFromJSValue {
         if let number = value.number {
             return Self(exactly: number.rounded(.towardZero))
         }
-#if !hasFeature(Embedded)
+        #if !hasFeature(Embedded)
         if let bigInt = value.bigInt as? JSBigIntExtended {
             return Self(exactly: bigInt)
         }
-#endif
+        #endif
         return nil
     }
 }
diff --git a/Sources/JavaScriptKit/ConvertibleToJSValue.swift b/Sources/JavaScriptKit/ConvertibleToJSValue.swift
index a7f7da8b6..afa632745 100644
--- a/Sources/JavaScriptKit/ConvertibleToJSValue.swift
+++ b/Sources/JavaScriptKit/ConvertibleToJSValue.swift
@@ -63,7 +63,6 @@ extension UInt32: ConvertibleToJSValue {
     public var jsValue: JSValue { .number(Double(self)) }
 }
 
-
 extension Int8: ConvertibleToJSValue {
     public var jsValue: JSValue { .number(Double(self)) }
 }
@@ -147,7 +146,7 @@ extension Optional: ConvertibleToJSValue where Wrapped: ConvertibleToJSValue {
     public var jsValue: JSValue {
         switch self {
         case .none: return .null
-        case let .some(wrapped): return wrapped.jsValue
+        case .some(let wrapped): return wrapped.jsValue
         }
     }
 }
@@ -185,7 +184,7 @@ extension Array: ConstructibleFromJSValue where Element: ConstructibleFromJSValu
         var array = [Element]()
         array.reserveCapacity(count)
 
-        for i in 0 ..< count {
+        for i in 0..(_ body: (RawJSValue) -> T) -> T {
+        body(convertToRawJSValue())
+    }
+
+    fileprivate func convertToRawJSValue() -> RawJSValue {
         let kind: JavaScriptValueKind
         let payload1: JavaScriptPayload1
         var payload2: JavaScriptPayload2 = 0
         switch self {
-        case let .boolean(boolValue):
+        case .boolean(let boolValue):
             kind = .boolean
             payload1 = boolValue ? 1 : 0
-        case let .number(numberValue):
+        case .number(let numberValue):
             kind = .number
             payload1 = 0
             payload2 = numberValue
-        case let .string(string):
-            return string.withRawJSValue(body)
-        case let .object(ref):
+        case .string(let string):
+            kind = .string
+            payload1 = string.asInternalJSRef()
+            payload2 = 0
+        case .object(let ref):
             kind = .object
             payload1 = JavaScriptPayload1(ref.id)
         case .null:
@@ -243,59 +248,38 @@ extension JSValue {
         case .undefined:
             kind = .undefined
             payload1 = 0
-        case let .function(functionRef):
+        case .function(let functionRef):
             kind = .function
             payload1 = JavaScriptPayload1(functionRef.id)
-        case let .symbol(symbolRef):
+        case .symbol(let symbolRef):
             kind = .symbol
             payload1 = JavaScriptPayload1(symbolRef.id)
-        case let .bigInt(bigIntRef):
+        case .bigInt(let bigIntRef):
             kind = .bigInt
             payload1 = JavaScriptPayload1(bigIntRef.id)
         }
-        let rawValue = RawJSValue(kind: kind, payload1: payload1, payload2: payload2)
-        return body(rawValue)
+        return RawJSValue(kind: kind, payload1: payload1, payload2: payload2)
     }
 }
 
 extension Array where Element: ConvertibleToJSValue {
     func withRawJSValues(_ body: ([RawJSValue]) -> T) -> T {
-        // fast path for empty array
-        guard self.count != 0 else { return body([]) }
-
-        func _withRawJSValues(
-            _ values: Self, _ index: Int,
-            _ results: inout [RawJSValue], _ body: ([RawJSValue]) -> T
-        ) -> T {
-            if index == values.count { return body(results) }
-            return values[index].jsValue.withRawJSValue { (rawValue) -> T in
-                results.append(rawValue)
-                return _withRawJSValues(values, index + 1, &results, body)
-            }
+        let jsValues = map { $0.jsValue }
+        // Ensure the jsValues live longer than the temporary raw JS values
+        return withExtendedLifetime(jsValues) {
+            body(jsValues.map { $0.convertToRawJSValue() })
         }
-        var _results = [RawJSValue]()
-        return _withRawJSValues(self, 0, &_results, body)
     }
 }
 
 #if !hasFeature(Embedded)
 extension Array where Element == ConvertibleToJSValue {
     func withRawJSValues(_ body: ([RawJSValue]) -> T) -> T {
-        // fast path for empty array
-        guard self.count != 0 else { return body([]) }
-
-        func _withRawJSValues(
-            _ values: [ConvertibleToJSValue], _ index: Int,
-            _ results: inout [RawJSValue], _ body: ([RawJSValue]) -> T
-        ) -> T {
-            if index == values.count { return body(results) }
-            return values[index].jsValue.withRawJSValue { (rawValue) -> T in
-                results.append(rawValue)
-                return _withRawJSValues(values, index + 1, &results, body)
-            }
+        let jsValues = map { $0.jsValue }
+        // Ensure the jsValues live longer than the temporary raw JS values
+        return withExtendedLifetime(jsValues) {
+            body(jsValues.map { $0.convertToRawJSValue() })
         }
-        var _results = [RawJSValue]()
-        return _withRawJSValues(self, 0, &_results, body)
     }
 }
 #endif
diff --git a/Sources/JavaScriptKit/Documentation.docc/Articles/Ahead-of-Time-Code-Generation.md b/Sources/JavaScriptKit/Documentation.docc/Articles/Ahead-of-Time-Code-Generation.md
new file mode 100644
index 000000000..755f68b91
--- /dev/null
+++ b/Sources/JavaScriptKit/Documentation.docc/Articles/Ahead-of-Time-Code-Generation.md
@@ -0,0 +1,169 @@
+# Ahead-of-Time Code Generation with BridgeJS
+
+Learn how to improve build times by generating BridgeJS code ahead of time.
+
+## Overview
+
+> 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.
+
+## Using the Command Plugin
+
+The `swift package plugin bridge-js` command provides an alternative to the build plugin approach. By generating code once and committing it to your repository, you can:
+
+1. **Reduce build times**: Skip code generation during normal builds
+2. **Inspect generated code**: Review and version control the generated Swift code
+3. **Create reproducible builds**: Ensure consistent builds across different environments
+
+### Step 1: Configure Your Package
+
+Configure your package to use JavaScriptKit, but without including the BridgeJS build 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: [
+                // Still required for the generated code
+                .enableExperimentalFeature("Extern")
+            ]
+            // Notice we DON'T include the BridgeJS build plugin here
+        )
+    ]
+)
+```
+
+### Step 2: Create Your Swift Code with @JS Annotations
+
+Write your Swift code with `@JS` annotations as usual:
+
+```swift
+import JavaScriptKit
+
+@JS public func calculateTotal(price: Double, quantity: Int) -> Double {
+    return price * Double(quantity)
+}
+
+@JS class Counter {
+    private var count = 0
+    
+    @JS init() {}
+    
+    @JS func increment() {
+        count += 1
+    }
+    
+    @JS func getValue() -> Int {
+        return count
+    }
+}
+```
+
+### Step 3: Create Your TypeScript Definitions
+
+If you're importing JavaScript APIs, create your `bridge.d.ts` file as usual:
+
+```typescript
+// Sources/MyApp/bridge.d.ts
+export function consoleLog(message: string): void;
+
+export interface Document {
+    title: string;
+    getElementById(id: string): HTMLElement;
+}
+
+export function getDocument(): Document;
+```
+
+### Step 4: Generate the Bridge Code
+
+Run the command plugin to generate the bridge code:
+
+```bash
+swift package plugin bridge-js
+```
+
+This command will:
+
+1. Process all Swift files with `@JS` 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/ExportSwift.swift  # Generated code for Swift exports
+Sources/MyApp/Generated/ImportTS.swift     # Generated code for TypeScript imports
+Sources/MyApp/Generated/JavaScript/        # Generated JSON skeletons
+```
+
+### Step 5: Add Generated Files to Version Control
+
+Add these generated files to your version control system:
+
+```bash
+git add Sources/MyApp/Generated
+git commit -m "Add generated BridgeJS code"
+```
+
+### Step 6: Build Your Package
+
+Now you can build your package as usual:
+
+```bash
+swift package --swift-sdk $SWIFT_SDK_ID js
+```
+
+Since the bridge code is already generated, the build will be faster.
+
+## Options for Selective Code Generation
+
+The command plugin supports targeting specific modules in your package:
+
+```bash
+# Generate bridge code only for the specified target
+swift package plugin bridge-js --target MyApp
+```
+
+## Updating Generated Code
+
+When you change your Swift code or TypeScript definitions, you'll need to regenerate the bridge code:
+
+```bash
+# Regenerate bridge code
+swift package plugin bridge-js
+git add Sources/MyApp/Generated
+git commit -m "Update generated BridgeJS code"
+```
+
+## When to Use Each Approach
+
+**Use the build plugin** when:
+- You're developing a small project or prototype
+- You frequently change your API boundaries
+- You want the simplest setup
+
+**Use the command plugin** when:
+- You're developing a larger project
+- Build time is a concern
+- You want to inspect and version control the generated code
+- You're working in a team and want to ensure consistent builds
+
+## Best Practices
+
+1. **Consistency**: Choose either the build plugin or the command plugin approach for your project
+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 
diff --git a/Sources/JavaScriptKit/Documentation.docc/Articles/Deploying-Pages.md b/Sources/JavaScriptKit/Documentation.docc/Articles/Deploying-Pages.md
new file mode 100644
index 000000000..96789f206
--- /dev/null
+++ b/Sources/JavaScriptKit/Documentation.docc/Articles/Deploying-Pages.md
@@ -0,0 +1,97 @@
+# Deploying Pages
+
+Deploy your applications built with JavaScriptKit to the web.
+
+## Overview
+
+Once you've built your application with JavaScriptKit, you'll need to deploy it to make it accessible on the web. This guide covers the deployment process, including building your application and deploying it to various hosting platforms.
+
+## Building Your Application with Vite
+
+Build your application using [Vite](https://vite.dev/) build tool:
+
+```bash
+# Build the Swift package for WebAssembly
+$ swift package --swift-sdk wasm32-unknown-wasi js -c release
+
+# Create a minimal HTML file (if you don't have one)
+$ cat < index.html
+
+
+
+  
+
+
+EOS
+
+# Install Vite and add the WebAssembly output as a dependency
+$ npm install -D vite .build/plugins/PackageToJS/outputs/Package
+
+# Build optimized assets
+$ npx vite build
+```
+
+This will generate optimized static assets in the `dist` directory, ready for deployment.
+
+## Deployment Options
+
+### GitHub Pages
+
+1. Set up your repository for GitHub Pages in your repository settings and select "GitHub Actions" as source.
+2. Create a GitHub Actions workflow to build and deploy your application:
+
+```yaml
+name: Deploy to GitHub Pages
+
+on:
+  # Runs on pushes targeting the default branch
+  push:
+    branches: [main]
+
+# Sets the GITHUB_TOKEN permissions to allow deployment to GitHub Pages
+permissions:
+  contents: read
+  pages: write
+  id-token: write
+
+jobs:
+  deploy:
+    environment:
+      name: github-pages
+      url: ${{ steps.deployment.outputs.page_url }}
+    runs-on: ubuntu-latest
+    container: swift:6.0.3
+    steps:
+      - uses: actions/checkout@v4
+      - uses: actions/setup-node@v4
+        with:
+          node-version: 20
+      - uses: actions/configure-pages@v4
+        id: pages
+      # Install Swift SDK for WebAssembly
+      - uses: swiftwasm/setup-swiftwasm@v2
+      - name: Build
+        run: |
+          swift package --swift-sdk wasm32-unknown-wasi js -c release
+          npm install
+          npx vite build --base "${{ steps.pages.outputs.base_path }}"
+      - uses: actions/upload-pages-artifact@v3
+        with:
+          path: './dist'
+      - uses: actions/deploy-pages@v4
+        id: deployment
+```
+
+## Cross-Origin Isolation Requirements
+
+When using `wasm32-unknown-wasip1-threads` target, you must enable [Cross-Origin Isolation](https://developer.mozilla.org/en-US/docs/Web/API/Window/crossOriginIsolated) by setting the following HTTP headers:
+
+```
+Cross-Origin-Embedder-Policy: require-corp
+Cross-Origin-Opener-Policy: same-origin
+```
+
+These headers are required for SharedArrayBuffer support, which is used by the threading implementation.
diff --git a/Sources/JavaScriptKit/Documentation.docc/Articles/Exporting-Swift-to-JavaScript.md b/Sources/JavaScriptKit/Documentation.docc/Articles/Exporting-Swift-to-JavaScript.md
new file mode 100644
index 000000000..08504c08d
--- /dev/null
+++ b/Sources/JavaScriptKit/Documentation.docc/Articles/Exporting-Swift-to-JavaScript.md
@@ -0,0 +1,164 @@
+# Exporting Swift to JavaScript
+
+Learn how to make your Swift code callable from JavaScript.
+
+## Overview
+
+> Important: This feature is still experimental. No API stability is guaranteed, and the API may change in future releases.
+
+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
+4. 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.
+
+## Marking Swift Code for Export
+
+### Functions
+
+To export a Swift function to JavaScript, mark it with the `@JS` attribute and make it `public`:
+
+```swift
+import JavaScriptKit
+
+@JS public func calculateTotal(price: Double, quantity: Int) -> Double {
+    return price * Double(quantity)
+}
+
+@JS public func formatCurrency(amount: Double) -> String {
+    return "$\(String(format: "%.2f", amount))"
+}
+```
+
+These functions will be accessible from JavaScript:
+
+```javascript
+const total = exports.calculateTotal(19.99, 3);
+const formattedTotal = exports.formatCurrency(total);
+console.log(formattedTotal); // "$59.97"
+```
+
+The generated TypeScript declarations for these functions would look like:
+
+```typescript
+export type Exports = {
+    calculateTotal(price: number, quantity: number): number;
+    formatCurrency(amount: number): string;
+}
+```
+
+### Classes
+
+To export a Swift class, mark both the class and any members you want to expose:
+
+```swift
+import JavaScriptKit
+
+@JS class ShoppingCart {
+    private var items: [(name: String, price: Double, quantity: Int)] = []
+
+    @JS init() {}
+
+    @JS public func addItem(name: String, price: Double, quantity: Int) {
+        items.append((name, price, quantity))
+    }
+
+    @JS public func removeItem(atIndex index: Int) {
+        guard index >= 0 && index < items.count else { return }
+        items.remove(at: index)
+    }
+
+    @JS public func getTotal() -> Double {
+        return items.reduce(0) { $0 + $1.price * Double($1.quantity) }
+    }
+
+    @JS public func getItemCount() -> Int {
+        return items.count
+    }
+
+    // This method won't be accessible from JavaScript (no @JS)
+    var debugDescription: String {
+        return "Cart with \(items.count) items, total: \(getTotal())"
+    }
+}
+```
+
+In JavaScript:
+
+```javascript
+import { init } from "./.build/plugins/PackageToJS/outputs/Package/index.js";
+const { exports } = await init({});
+
+const cart = new exports.ShoppingCart();
+cart.addItem("Laptop", 999.99, 1);
+cart.addItem("Mouse", 24.99, 2);
+console.log(`Items in cart: ${cart.getItemCount()}`);
+console.log(`Total: $${cart.getTotal().toFixed(2)}`);
+```
+
+The generated TypeScript declarations for this class would look like:
+
+```typescript
+// Base interface for Swift reference types
+export interface SwiftHeapObject {
+    release(): void;
+}
+
+// ShoppingCart interface with all exported methods
+export interface ShoppingCart extends SwiftHeapObject {
+    addItem(name: string, price: number, quantity: number): void;
+    removeItem(atIndex: number): void;
+    getTotal(): number;
+    getItemCount(): number;
+}
+
+export type Exports = {
+    ShoppingCart: {
+        new(): ShoppingCart;
+    }
+}
+```
diff --git a/Sources/JavaScriptKit/Documentation.docc/Articles/Importing-TypeScript-into-Swift.md b/Sources/JavaScriptKit/Documentation.docc/Articles/Importing-TypeScript-into-Swift.md
new file mode 100644
index 000000000..5f9bb4a12
--- /dev/null
+++ b/Sources/JavaScriptKit/Documentation.docc/Articles/Importing-TypeScript-into-Swift.md
@@ -0,0 +1,174 @@
+# 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.
+
+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.d.ts` in your target source directory (e.g. `Sources//bridge.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({
+    imports: {
+        consoleLog: (message) => {
+            console.log(message);
+        },
+        getDocument: () => document,
+    }
+});
+
+// Call the entry point of your Swift application
+exports.run();
+```
diff --git a/Sources/JavaScriptKit/Documentation.docc/Documentation.md b/Sources/JavaScriptKit/Documentation.docc/Documentation.md
index 94d5ba3c5..ffc168431 100644
--- a/Sources/JavaScriptKit/Documentation.docc/Documentation.md
+++ b/Sources/JavaScriptKit/Documentation.docc/Documentation.md
@@ -49,8 +49,16 @@ Check out the [examples](https://github.com/swiftwasm/JavaScriptKit/tree/main/Ex
 
 - 
 
-### Core Types
+### Articles
 
-- 
-- 
-- 
+- 
+- 
+- 
+- 
+- 
+
+### Core APIs
+
+- ``JSValue``
+- ``JSObject``
+- ``JS()``
diff --git a/Sources/JavaScriptKit/Documentation.docc/Tutorials/Hello-World/Hello-World.tutorial b/Sources/JavaScriptKit/Documentation.docc/Tutorials/Hello-World/Hello-World.tutorial
index f5ede8f19..c054e3a48 100644
--- a/Sources/JavaScriptKit/Documentation.docc/Tutorials/Hello-World/Hello-World.tutorial
+++ b/Sources/JavaScriptKit/Documentation.docc/Tutorials/Hello-World/Hello-World.tutorial
@@ -84,7 +84,7 @@
       @Step {
         Start a local web server to serve your application:
         This starts a simple HTTP server that serves files from your current directory.
-
+        > Note: If you are building your app with `wasm32-unknown-wasip1-threads` target, you need to enable [Cross-Origin Isolation](https://developer.mozilla.org/en-US/docs/Web/API/Window/crossOriginIsolated) for `SharedArrayBuffer`. See "Cross-Origin Isolation Requirements" in 
         @Code(name: "Console", file: "hello-world-3-2-server.txt")
       }
 
diff --git a/Sources/JavaScriptKit/Documentation.docc/Tutorials/Hello-World/Resources/hello-world-2-1-main-swift.swift b/Sources/JavaScriptKit/Documentation.docc/Tutorials/Hello-World/Resources/hello-world-2-1-main-swift.swift
index 156ac0540..a528e65b1 100644
--- a/Sources/JavaScriptKit/Documentation.docc/Tutorials/Hello-World/Resources/hello-world-2-1-main-swift.swift
+++ b/Sources/JavaScriptKit/Documentation.docc/Tutorials/Hello-World/Resources/hello-world-2-1-main-swift.swift
@@ -3,4 +3,4 @@ import JavaScriptKit
 let document = JSObject.global.document
 var div = document.createElement("div")
 div.innerText = "Hello from Swift!"
-document.body.appendChild(div) 
+document.body.appendChild(div)
diff --git a/Sources/JavaScriptKit/Documentation.docc/Tutorials/Hello-World/Resources/hello-world-3-2-server.txt b/Sources/JavaScriptKit/Documentation.docc/Tutorials/Hello-World/Resources/hello-world-3-2-server.txt
index 569396481..ad560a635 100644
--- a/Sources/JavaScriptKit/Documentation.docc/Tutorials/Hello-World/Resources/hello-world-3-2-server.txt
+++ b/Sources/JavaScriptKit/Documentation.docc/Tutorials/Hello-World/Resources/hello-world-3-2-server.txt
@@ -4,5 +4,4 @@ Build of product 'Hello' complete! (5.16s)
 Packaging...
 ...
 Packaging finished
-$ python3 -m http.server
-Serving HTTP on 0.0.0.0 port 8000 (http://0.0.0.0:8000/) ... 
+$ npx serve
diff --git a/Sources/JavaScriptKit/Documentation.docc/Tutorials/Hello-World/Resources/hello-world-3-3-open.txt b/Sources/JavaScriptKit/Documentation.docc/Tutorials/Hello-World/Resources/hello-world-3-3-open.txt
index f4df8ec2f..8abe30b7c 100644
--- a/Sources/JavaScriptKit/Documentation.docc/Tutorials/Hello-World/Resources/hello-world-3-3-open.txt
+++ b/Sources/JavaScriptKit/Documentation.docc/Tutorials/Hello-World/Resources/hello-world-3-3-open.txt
@@ -4,6 +4,5 @@ Build of product 'Hello' complete! (5.16s)
 Packaging...
 ...
 Packaging finished
-$ python3 -m http.server
-Serving HTTP on 0.0.0.0 port 8000 (http://0.0.0.0:8000/) ... 
-$ open http://localhost:8000 
+$ npx serve
+$ open http://localhost:3000
diff --git a/Sources/JavaScriptKit/Features.swift b/Sources/JavaScriptKit/Features.swift
index db6e00f26..313edab40 100644
--- a/Sources/JavaScriptKit/Features.swift
+++ b/Sources/JavaScriptKit/Features.swift
@@ -5,9 +5,9 @@ enum LibraryFeatures {
 @_cdecl("_library_features")
 func _library_features() -> Int32 {
     var features: Int32 = 0
-#if !JAVASCRIPTKIT_WITHOUT_WEAKREFS
+    #if !JAVASCRIPTKIT_WITHOUT_WEAKREFS
     features |= LibraryFeatures.weakRefs
-#endif
+    #endif
     return features
 }
 
@@ -15,4 +15,4 @@ func _library_features() -> Int32 {
 // cdecls currently don't work in embedded, and expose for wasm only works >=6.0
 @_expose(wasm, "swjs_library_features")
 public func _swjs_library_features() -> Int32 { _library_features() }
-#endif
\ No newline at end of file
+#endif
diff --git a/Sources/JavaScriptKit/FundamentalObjects/JSBigInt.swift b/Sources/JavaScriptKit/FundamentalObjects/JSBigInt.swift
index a8867f95c..3f0c2632b 100644
--- a/Sources/JavaScriptKit/FundamentalObjects/JSBigInt.swift
+++ b/Sources/JavaScriptKit/FundamentalObjects/JSBigInt.swift
@@ -10,18 +10,18 @@ public final class JSBigInt: JSObject {
     override public init(id: JavaScriptObjectRef) {
         super.init(id: id)
     }
-    
+
     /// Instantiate a new `JSBigInt` with given Int64 value in a slow path
     /// This doesn't require [JS-BigInt-integration](https://github.com/WebAssembly/JS-BigInt-integration) feature.
     public init(_slowBridge value: Int64) {
         let value = UInt64(bitPattern: value)
-        super.init(id: swjs_i64_to_bigint_slow(UInt32(value & 0xffffffff), UInt32(value >> 32), true))
+        super.init(id: swjs_i64_to_bigint_slow(UInt32(value & 0xffff_ffff), UInt32(value >> 32), true))
     }
 
     /// Instantiate a new `JSBigInt` with given UInt64 value in a slow path
     /// This doesn't require [JS-BigInt-integration](https://github.com/WebAssembly/JS-BigInt-integration) feature.
     public init(_slowBridge value: UInt64) {
-        super.init(id: swjs_i64_to_bigint_slow(UInt32(value & 0xffffffff), UInt32(value >> 32), false))
+        super.init(id: swjs_i64_to_bigint_slow(UInt32(value & 0xffff_ffff), UInt32(value >> 32), false))
     }
 
     override public var jsValue: JSValue {
diff --git a/Sources/JavaScriptKit/FundamentalObjects/JSClosure.swift b/Sources/JavaScriptKit/FundamentalObjects/JSClosure.swift
index 828cb40f2..18a400786 100644
--- a/Sources/JavaScriptKit/FundamentalObjects/JSClosure.swift
+++ b/Sources/JavaScriptKit/FundamentalObjects/JSClosure.swift
@@ -1,4 +1,7 @@
 import _CJavaScriptKit
+#if hasFeature(Embedded) && os(WASI)
+import _Concurrency
+#endif
 
 /// `JSClosureProtocol` wraps Swift closure objects for use in JavaScript. Conforming types
 /// are responsible for managing the lifetime of the closure they wrap, but can delegate that
@@ -26,15 +29,25 @@ public class JSOneshotClosure: JSObject, JSClosureProtocol {
         }
 
         // 3. Retain the given body in static storage by `funcRef`.
-        JSClosure.sharedClosures.wrappedValue[hostFuncRef] = (self, {
-            defer { self.release() }
-            return body($0)
-        })
+        JSClosure.sharedClosures.wrappedValue[hostFuncRef] = (
+            self,
+            {
+                defer { self.release() }
+                return body($0)
+            }
+        )
     }
 
-    #if compiler(>=5.5) && !hasFeature(Embedded)
+    @available(*, unavailable, message: "JSOneshotClosure does not support dictionary literal initialization")
+    public required init(dictionaryLiteral elements: (String, JSValue)...) {
+        fatalError("JSOneshotClosure does not support dictionary literal initialization")
+    }
+
+    #if compiler(>=5.5) && (!hasFeature(Embedded) || os(WASI))
     @available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *)
-    public static func async(_ body: sending @escaping (sending [JSValue]) async throws -> JSValue) -> JSOneshotClosure {
+    public static func async(
+        _ body: sending @escaping (sending [JSValue]) async throws(JSException) -> JSValue
+    ) -> JSOneshotClosure {
         JSOneshotClosure(makeAsyncClosure(body))
     }
     #endif
@@ -90,9 +103,14 @@ public class JSClosure: JSFunction, JSClosureProtocol {
     private var isReleased: Bool = false
     #endif
 
-    @available(*, deprecated, message: "This initializer will be removed in the next minor version update. Please use `init(_ body: @escaping ([JSValue]) -> JSValue)` and add `return .undefined` to the end of your closure")
+    @available(
+        *,
+        deprecated,
+        message:
+            "This initializer will be removed in the next minor version update. Please use `init(_ body: @escaping ([JSValue]) -> JSValue)` and add `return .undefined` to the end of your closure"
+    )
     @_disfavoredOverload
-    public convenience init(_ body: @escaping ([JSValue]) -> ()) {
+    public convenience init(_ body: @escaping ([JSValue]) -> Void) {
         self.init({
             body($0)
             return .undefined
@@ -113,9 +131,16 @@ public class JSClosure: JSFunction, JSClosureProtocol {
         Self.sharedClosures.wrappedValue[hostFuncRef] = (self, body)
     }
 
-    #if compiler(>=5.5) && !hasFeature(Embedded)
+    @available(*, unavailable, message: "JSClosure does not support dictionary literal initialization")
+    public required init(dictionaryLiteral elements: (String, JSValue)...) {
+        fatalError("JSClosure does not support dictionary literal initialization")
+    }
+
+    #if compiler(>=5.5) && (!hasFeature(Embedded) || os(WASI))
     @available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *)
-    public static func async(_ body: @Sendable @escaping (sending [JSValue]) async throws -> JSValue) -> JSClosure {
+    public static func async(
+        _ body: @Sendable @escaping (sending [JSValue]) async throws(JSException) -> JSValue
+    ) -> JSClosure {
         JSClosure(makeAsyncClosure(body))
     }
     #endif
@@ -129,10 +154,10 @@ public class JSClosure: JSFunction, JSClosureProtocol {
     #endif
 }
 
-#if compiler(>=5.5) && !hasFeature(Embedded)
+#if compiler(>=5.5) && (!hasFeature(Embedded) || os(WASI))
 @available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *)
 private func makeAsyncClosure(
-    _ body: sending @escaping (sending [JSValue]) async throws -> JSValue
+    _ body: sending @escaping (sending [JSValue]) async throws(JSException) -> JSValue
 ) -> ((sending [JSValue]) -> JSValue) {
     { arguments in
         JSPromise { resolver in
@@ -142,19 +167,15 @@ private func makeAsyncClosure(
             struct Context: @unchecked Sendable {
                 let resolver: (JSPromise.Result) -> Void
                 let arguments: [JSValue]
-                let body: (sending [JSValue]) async throws -> JSValue
+                let body: (sending [JSValue]) async throws(JSException) -> JSValue
             }
             let context = Context(resolver: resolver, arguments: arguments, body: body)
             Task {
-                do {
+                do throws(JSException) {
                     let result = try await context.body(context.arguments)
                     context.resolver(.success(result))
                 } catch {
-                    if let jsError = error as? JSException {
-                        context.resolver(.failure(jsError.thrownValue))
-                    } else {
-                        context.resolver(.failure(JSError(message: String(describing: error)).jsValue))
-                    }
+                    context.resolver(.failure(error.thrownValue))
                 }
             }
         }.jsValue()
@@ -200,7 +221,8 @@ private func makeAsyncClosure(
 @_cdecl("_call_host_function_impl")
 func _call_host_function_impl(
     _ hostFuncRef: JavaScriptHostFuncRef,
-    _ argv: UnsafePointer, _ argc: Int32,
+    _ argv: UnsafePointer,
+    _ argc: Int32,
     _ callbackFuncRef: JavaScriptObjectRef
 ) -> Bool {
     guard let (_, hostFunc) = JSClosure.sharedClosures.wrappedValue[hostFuncRef] else {
@@ -216,7 +238,6 @@ func _call_host_function_impl(
     return false
 }
 
-
 /// [WeakRefs](https://github.com/tc39/proposal-weakrefs) are already Stage 4,
 /// but was added recently enough that older browser versions don’t support it.
 /// Build with `-Xswiftc -DJAVASCRIPTKIT_WITHOUT_WEAKREFS` to disable the relevant behavior.
@@ -231,7 +252,6 @@ extension JSClosure {
     }
 }
 
-
 @_cdecl("_free_host_function_impl")
 func _free_host_function_impl(_ hostFuncRef: JavaScriptHostFuncRef) {}
 
@@ -254,11 +274,13 @@ func _free_host_function_impl(_ hostFuncRef: JavaScriptHostFuncRef) {
 // cdecls currently don't work in embedded, and expose for wasm only works >=6.0
 @_expose(wasm, "swjs_call_host_function")
 public func _swjs_call_host_function(
-        _ hostFuncRef: JavaScriptHostFuncRef,
-        _ argv: UnsafePointer, _ argc: Int32,
-        _ callbackFuncRef: JavaScriptObjectRef) -> Bool {
+    _ hostFuncRef: JavaScriptHostFuncRef,
+    _ argv: UnsafePointer,
+    _ argc: Int32,
+    _ callbackFuncRef: JavaScriptObjectRef
+) -> Bool {
 
-    _call_host_function_impl(hostFuncRef, argv, argc, callbackFuncRef) 
+    _call_host_function_impl(hostFuncRef, argv, argc, callbackFuncRef)
 }
 
 @_expose(wasm, "swjs_free_host_function")
diff --git a/Sources/JavaScriptKit/FundamentalObjects/JSFunction.swift b/Sources/JavaScriptKit/FundamentalObjects/JSFunction.swift
index 498bbc3ea..172483612 100644
--- a/Sources/JavaScriptKit/FundamentalObjects/JSFunction.swift
+++ b/Sources/JavaScriptKit/FundamentalObjects/JSFunction.swift
@@ -11,7 +11,7 @@ import _CJavaScriptKit
 /// ```
 ///
 public class JSFunction: JSObject {
-#if !hasFeature(Embedded)
+    #if !hasFeature(Embedded)
     /// Call this function with given `arguments` and binding given `this` as context.
     /// - Parameters:
     ///   - this: The value to be passed as the `this` parameter to this function.
@@ -84,7 +84,7 @@ public class JSFunction: JSObject {
     public var `throws`: JSThrowingFunction {
         JSThrowingFunction(self)
     }
-#endif
+    #endif
 
     @discardableResult
     public func callAsFunction(arguments: [JSValue]) -> JSValue {
@@ -116,7 +116,7 @@ public class JSFunction: JSObject {
         arguments.withRawJSValues { invokeNonThrowingJSFunction(rawValues: $0, this: this) }
     }
 
-#if !hasFeature(Embedded)
+    #if !hasFeature(Embedded)
     final func invokeNonThrowingJSFunction(arguments: [ConvertibleToJSValue]) -> RawJSValue {
         arguments.withRawJSValues { invokeNonThrowingJSFunction(rawValues: $0) }
     }
@@ -124,7 +124,7 @@ public class JSFunction: JSObject {
     final func invokeNonThrowingJSFunction(arguments: [ConvertibleToJSValue], this: JSObject) -> RawJSValue {
         arguments.withRawJSValues { invokeNonThrowingJSFunction(rawValues: $0, this: this) }
     }
-#endif
+    #endif
 
     final private func invokeNonThrowingJSFunction(rawValues: [RawJSValue]) -> RawJSValue {
         rawValues.withUnsafeBufferPointer { [id] bufferPointer in
@@ -133,8 +133,11 @@ public class JSFunction: JSObject {
             var payload1 = JavaScriptPayload1()
             var payload2 = JavaScriptPayload2()
             let resultBitPattern = swjs_call_function_no_catch(
-                id, argv, Int32(argc),
-                &payload1, &payload2
+                id,
+                argv,
+                Int32(argc),
+                &payload1,
+                &payload2
             )
             let kindAndFlags = JavaScriptValueKindAndFlags(bitPattern: resultBitPattern)
             assert(!kindAndFlags.isException)
@@ -149,9 +152,13 @@ public class JSFunction: JSObject {
             let argc = bufferPointer.count
             var payload1 = JavaScriptPayload1()
             var payload2 = JavaScriptPayload2()
-            let resultBitPattern = swjs_call_function_with_this_no_catch(this.id,
-                id, argv, Int32(argc),
-                &payload1, &payload2
+            let resultBitPattern = swjs_call_function_with_this_no_catch(
+                this.id,
+                id,
+                argv,
+                Int32(argc),
+                &payload1,
+                &payload2
             )
             let kindAndFlags = JavaScriptValueKindAndFlags(bitPattern: resultBitPattern)
             #if !hasFeature(Embedded)
@@ -172,20 +179,20 @@ public class JSFunction: JSObject {
 //
 // Once Embedded Swift supports parameter packs/variadic generics, we can
 // replace all variants with a single method each that takes a generic pack.
-public extension JSFunction {
+extension JSFunction {
 
     @discardableResult
-    func callAsFunction(this: JSObject) -> JSValue {
+    public func callAsFunction(this: JSObject) -> JSValue {
         invokeNonThrowingJSFunction(arguments: [], this: this).jsValue
     }
 
     @discardableResult
-    func callAsFunction(this: JSObject, _ arg0: some ConvertibleToJSValue) -> JSValue {
+    public func callAsFunction(this: JSObject, _ arg0: some ConvertibleToJSValue) -> JSValue {
         invokeNonThrowingJSFunction(arguments: [arg0.jsValue], this: this).jsValue
     }
 
     @discardableResult
-    func callAsFunction(
+    public func callAsFunction(
         this: JSObject,
         _ arg0: some ConvertibleToJSValue,
         _ arg1: some ConvertibleToJSValue
@@ -194,7 +201,7 @@ public extension JSFunction {
     }
 
     @discardableResult
-    func callAsFunction(
+    public func callAsFunction(
         this: JSObject,
         _ arg0: some ConvertibleToJSValue,
         _ arg1: some ConvertibleToJSValue,
@@ -204,18 +211,19 @@ public extension JSFunction {
     }
 
     @discardableResult
-    func callAsFunction(
+    public func callAsFunction(
         this: JSObject,
         _ arg0: some ConvertibleToJSValue,
         _ arg1: some ConvertibleToJSValue,
         _ arg2: some ConvertibleToJSValue,
         _ arg3: some ConvertibleToJSValue
     ) -> JSValue {
-        invokeNonThrowingJSFunction(arguments: [arg0.jsValue, arg1.jsValue, arg2.jsValue, arg3.jsValue], this: this).jsValue
+        invokeNonThrowingJSFunction(arguments: [arg0.jsValue, arg1.jsValue, arg2.jsValue, arg3.jsValue], this: this)
+            .jsValue
     }
 
     @discardableResult
-    func callAsFunction(
+    public func callAsFunction(
         this: JSObject,
         _ arg0: some ConvertibleToJSValue,
         _ arg1: some ConvertibleToJSValue,
@@ -223,11 +231,14 @@ public extension JSFunction {
         _ arg3: some ConvertibleToJSValue,
         _ arg4: some ConvertibleToJSValue
     ) -> JSValue {
-        invokeNonThrowingJSFunction(arguments: [arg0.jsValue, arg1.jsValue, arg2.jsValue, arg3.jsValue, arg4.jsValue], this: this).jsValue
+        invokeNonThrowingJSFunction(
+            arguments: [arg0.jsValue, arg1.jsValue, arg2.jsValue, arg3.jsValue, arg4.jsValue],
+            this: this
+        ).jsValue
     }
 
     @discardableResult
-    func callAsFunction(
+    public func callAsFunction(
         this: JSObject,
         _ arg0: some ConvertibleToJSValue,
         _ arg1: some ConvertibleToJSValue,
@@ -236,11 +247,14 @@ public extension JSFunction {
         _ arg4: some ConvertibleToJSValue,
         _ arg5: some ConvertibleToJSValue
     ) -> JSValue {
-        invokeNonThrowingJSFunction(arguments: [arg0.jsValue, arg1.jsValue, arg2.jsValue, arg3.jsValue, arg4.jsValue, arg5.jsValue], this: this).jsValue
+        invokeNonThrowingJSFunction(
+            arguments: [arg0.jsValue, arg1.jsValue, arg2.jsValue, arg3.jsValue, arg4.jsValue, arg5.jsValue],
+            this: this
+        ).jsValue
     }
 
     @discardableResult
-    func callAsFunction(
+    public func callAsFunction(
         this: JSObject,
         _ arg0: some ConvertibleToJSValue,
         _ arg1: some ConvertibleToJSValue,
@@ -250,26 +264,31 @@ public extension JSFunction {
         _ arg5: some ConvertibleToJSValue,
         _ arg6: some ConvertibleToJSValue
     ) -> JSValue {
-        invokeNonThrowingJSFunction(arguments: [arg0.jsValue, arg1.jsValue, arg2.jsValue, arg3.jsValue, arg4.jsValue, arg5.jsValue, arg6.jsValue], this: this).jsValue
+        invokeNonThrowingJSFunction(
+            arguments: [
+                arg0.jsValue, arg1.jsValue, arg2.jsValue, arg3.jsValue, arg4.jsValue, arg5.jsValue, arg6.jsValue,
+            ],
+            this: this
+        ).jsValue
     }
 
     @discardableResult
-    func callAsFunction(this: JSObject, arguments: [JSValue]) -> JSValue {
+    public func callAsFunction(this: JSObject, arguments: [JSValue]) -> JSValue {
         invokeNonThrowingJSFunction(arguments: arguments, this: this).jsValue
     }
 
     @discardableResult
-    func callAsFunction() -> JSValue {
+    public func callAsFunction() -> JSValue {
         invokeNonThrowingJSFunction(arguments: []).jsValue
     }
 
     @discardableResult
-    func callAsFunction(_ arg0: some ConvertibleToJSValue) -> JSValue {
+    public func callAsFunction(_ arg0: some ConvertibleToJSValue) -> JSValue {
         invokeNonThrowingJSFunction(arguments: [arg0.jsValue]).jsValue
     }
 
     @discardableResult
-    func callAsFunction(
+    public func callAsFunction(
         _ arg0: some ConvertibleToJSValue,
         _ arg1: some ConvertibleToJSValue
     ) -> JSValue {
@@ -277,7 +296,7 @@ public extension JSFunction {
     }
 
     @discardableResult
-    func callAsFunction(
+    public func callAsFunction(
         _ arg0: some ConvertibleToJSValue,
         _ arg1: some ConvertibleToJSValue,
         _ arg2: some ConvertibleToJSValue
@@ -286,7 +305,7 @@ public extension JSFunction {
     }
 
     @discardableResult
-    func callAsFunction(
+    public func callAsFunction(
         _ arg0: some ConvertibleToJSValue,
         _ arg1: some ConvertibleToJSValue,
         _ arg2: some ConvertibleToJSValue,
@@ -296,18 +315,19 @@ public extension JSFunction {
     }
 
     @discardableResult
-    func callAsFunction(
+    public func callAsFunction(
         _ arg0: some ConvertibleToJSValue,
         _ arg1: some ConvertibleToJSValue,
         _ arg2: some ConvertibleToJSValue,
         _ arg3: some ConvertibleToJSValue,
         _ arg4: some ConvertibleToJSValue
     ) -> JSValue {
-        invokeNonThrowingJSFunction(arguments: [arg0.jsValue, arg1.jsValue, arg2.jsValue, arg3.jsValue, arg4.jsValue]).jsValue
+        invokeNonThrowingJSFunction(arguments: [arg0.jsValue, arg1.jsValue, arg2.jsValue, arg3.jsValue, arg4.jsValue])
+            .jsValue
     }
 
     @discardableResult
-    func callAsFunction(
+    public func callAsFunction(
         _ arg0: some ConvertibleToJSValue,
         _ arg1: some ConvertibleToJSValue,
         _ arg2: some ConvertibleToJSValue,
@@ -315,11 +335,13 @@ public extension JSFunction {
         _ arg4: some ConvertibleToJSValue,
         _ arg5: some ConvertibleToJSValue
     ) -> JSValue {
-        invokeNonThrowingJSFunction(arguments: [arg0.jsValue, arg1.jsValue, arg2.jsValue, arg3.jsValue, arg4.jsValue, arg5.jsValue]).jsValue
+        invokeNonThrowingJSFunction(arguments: [
+            arg0.jsValue, arg1.jsValue, arg2.jsValue, arg3.jsValue, arg4.jsValue, arg5.jsValue,
+        ]).jsValue
     }
 
     @discardableResult
-    func callAsFunction(
+    public func callAsFunction(
         _ arg0: some ConvertibleToJSValue,
         _ arg1: some ConvertibleToJSValue,
         _ arg2: some ConvertibleToJSValue,
@@ -328,25 +350,27 @@ public extension JSFunction {
         _ arg5: some ConvertibleToJSValue,
         _ arg6: some ConvertibleToJSValue
     ) -> JSValue {
-        invokeNonThrowingJSFunction(arguments: [arg0.jsValue, arg1.jsValue, arg2.jsValue, arg3.jsValue, arg4.jsValue, arg5.jsValue, arg6.jsValue]).jsValue
+        invokeNonThrowingJSFunction(arguments: [
+            arg0.jsValue, arg1.jsValue, arg2.jsValue, arg3.jsValue, arg4.jsValue, arg5.jsValue, arg6.jsValue,
+        ]).jsValue
     }
 
-    func new() -> JSObject {
+    public func new() -> JSObject {
         new(arguments: [])
     }
 
-    func new(_ arg0: some ConvertibleToJSValue) -> JSObject {
+    public func new(_ arg0: some ConvertibleToJSValue) -> JSObject {
         new(arguments: [arg0.jsValue])
     }
 
-    func new(
+    public func new(
         _ arg0: some ConvertibleToJSValue,
         _ arg1: some ConvertibleToJSValue
     ) -> JSObject {
         new(arguments: [arg0.jsValue, arg1.jsValue])
     }
 
-    func new(
+    public func new(
         _ arg0: some ConvertibleToJSValue,
         _ arg1: some ConvertibleToJSValue,
         _ arg2: some ConvertibleToJSValue
@@ -354,7 +378,7 @@ public extension JSFunction {
         new(arguments: [arg0.jsValue, arg1.jsValue, arg2.jsValue])
     }
 
-    func new(
+    public func new(
         _ arg0: some ConvertibleToJSValue,
         _ arg1: some ConvertibleToJSValue,
         _ arg2: some ConvertibleToJSValue,
@@ -363,7 +387,7 @@ public extension JSFunction {
         new(arguments: [arg0.jsValue, arg1.jsValue, arg2.jsValue, arg3.jsValue])
     }
 
-    func new(
+    public func new(
         _ arg0: some ConvertibleToJSValue,
         _ arg1: some ConvertibleToJSValue,
         _ arg2: some ConvertibleToJSValue,
@@ -373,7 +397,7 @@ public extension JSFunction {
         new(arguments: [arg0.jsValue, arg1.jsValue, arg2.jsValue, arg3.jsValue, arg4.jsValue])
     }
 
-    func new(
+    public func new(
         _ arg0: some ConvertibleToJSValue,
         _ arg1: some ConvertibleToJSValue,
         _ arg2: some ConvertibleToJSValue,
@@ -384,7 +408,7 @@ public extension JSFunction {
         new(arguments: [arg0.jsValue, arg1.jsValue, arg2.jsValue, arg3.jsValue, arg4.jsValue, arg5.jsValue])
     }
 
-    func new(
+    public func new(
         _ arg0: some ConvertibleToJSValue,
         _ arg1: some ConvertibleToJSValue,
         _ arg2: some ConvertibleToJSValue,
@@ -393,7 +417,9 @@ public extension JSFunction {
         _ arg5: some ConvertibleToJSValue,
         _ arg6: some ConvertibleToJSValue
     ) -> JSObject {
-        new(arguments: [arg0.jsValue, arg1.jsValue, arg2.jsValue, arg3.jsValue, arg4.jsValue, arg5.jsValue, arg6.jsValue])
+        new(arguments: [
+            arg0.jsValue, arg1.jsValue, arg2.jsValue, arg3.jsValue, arg4.jsValue, arg5.jsValue, arg6.jsValue,
+        ])
     }
 }
 #endif
diff --git a/Sources/JavaScriptKit/FundamentalObjects/JSObject.swift b/Sources/JavaScriptKit/FundamentalObjects/JSObject.swift
index 9006ec7b7..12dbf9e02 100644
--- a/Sources/JavaScriptKit/FundamentalObjects/JSObject.swift
+++ b/Sources/JavaScriptKit/FundamentalObjects/JSObject.swift
@@ -15,16 +15,16 @@ import _CJavaScriptKit
 /// The lifetime of this object is managed by the JavaScript and Swift runtime bridge library with
 /// reference counting system.
 @dynamicMemberLookup
-public class JSObject: Equatable {
+public class JSObject: Equatable, ExpressibleByDictionaryLiteral {
     internal static var constructor: JSFunction { _constructor.wrappedValue }
     private static let _constructor = LazyThreadLocal(initialize: { JSObject.global.Object.function! })
 
     @usableFromInline
     internal var _id: JavaScriptObjectRef
 
-#if compiler(>=6.1) && _runtime(_multithreaded)
+    #if compiler(>=6.1) && _runtime(_multithreaded)
     package let ownerTid: Int32
-#endif
+    #endif
 
     @_spi(JSObject_id)
     @inlinable
@@ -33,9 +33,22 @@ public class JSObject: Equatable {
     @_spi(JSObject_id)
     public init(id: JavaScriptObjectRef) {
         self._id = id
-#if compiler(>=6.1) && _runtime(_multithreaded)
+        #if compiler(>=6.1) && _runtime(_multithreaded)
         self.ownerTid = swjs_get_worker_thread_id_cached()
-#endif
+        #endif
+    }
+
+    /// Creates an empty JavaScript object.
+    public convenience init() {
+        self.init(id: swjs_create_object())
+    }
+
+    /// Creates a new object with the key-value pairs in the dictionary literal.
+    ///
+    /// - Parameter elements: A variadic list of key-value pairs where all keys are strings
+    public convenience required init(dictionaryLiteral elements: (String, JSValue)...) {
+        self.init()
+        for (key, value) in elements { self[key] = value }
     }
 
     /// Asserts that the object is being accessed from the owner thread.
@@ -47,18 +60,24 @@ public class JSObject: Equatable {
     /// object spaces are not shared across threads backed by Web Workers.
     private func assertOnOwnerThread(hint: @autoclosure () -> String) {
         #if compiler(>=6.1) && _runtime(_multithreaded)
-        precondition(ownerTid == swjs_get_worker_thread_id_cached(), "JSObject is being accessed from a thread other than the owner thread: \(hint())")
+        precondition(
+            ownerTid == swjs_get_worker_thread_id_cached(),
+            "JSObject is being accessed from a thread other than the owner thread: \(hint())"
+        )
         #endif
     }
 
     /// Asserts that the two objects being compared are owned by the same thread.
     private static func assertSameOwnerThread(lhs: JSObject, rhs: JSObject, hint: @autoclosure () -> String) {
         #if compiler(>=6.1) && _runtime(_multithreaded)
-        precondition(lhs.ownerTid == rhs.ownerTid, "JSObject is being accessed from a thread other than the owner thread: \(hint())")
+        precondition(
+            lhs.ownerTid == rhs.ownerTid,
+            "JSObject is being accessed from a thread other than the owner thread: \(hint())"
+        )
         #endif
     }
 
-#if !hasFeature(Embedded)
+    #if !hasFeature(Embedded)
     /// Returns the `name` member method binding this object as `this` context.
     ///
     /// e.g.
@@ -101,7 +120,7 @@ public class JSObject: Equatable {
     public subscript(dynamicMember name: String) -> ((ConvertibleToJSValue...) -> JSValue)? {
         self[name]
     }
-#endif
+    #endif
 
     /// A convenience method of `subscript(_ name: String) -> JSValue`
     /// to access the member through Dynamic Member Lookup.
@@ -166,7 +185,7 @@ public class JSObject: Equatable {
         }
     }
 
-#if !hasFeature(Embedded)
+    #if !hasFeature(Embedded)
     /// A modifier to call methods as throwing methods capturing `this`
     ///
     ///
@@ -187,7 +206,7 @@ public class JSObject: Equatable {
     public var throwing: JSThrowingObject {
         JSThrowingObject(self)
     }
-#endif
+    #endif
 
     /// Return `true` if this value is an instance of the passed `constructor` function.
     /// - Parameter constructor: The constructor function to check.
@@ -230,10 +249,11 @@ public class JSObject: Equatable {
     public static func construct(from value: JSValue) -> Self? {
         switch value {
         case .boolean,
-                .string,
-                .number,
-                .null,
-                .undefined: return nil
+            .string,
+            .number,
+            .null,
+            .undefined:
+            return nil
         case .object(let object):
             return object as? Self
         case .function(let function):
@@ -295,7 +315,6 @@ public class JSThrowingObject {
 }
 #endif
 
-
 #if hasFeature(Embedded)
 // Overloads of `JSObject.subscript(_ name: String) -> ((ConvertibleToJSValue...) -> JSValue)?`
 // for 0 through 7 arguments for Embedded Swift.
@@ -305,33 +324,33 @@ public class JSThrowingObject {
 //
 // NOTE: Once Embedded Swift supports parameter packs/variadic generics, we can
 // replace all of these with a single method that takes a generic pack.
-public extension JSObject {
+extension JSObject {
     @_disfavoredOverload
-    subscript(dynamicMember name: String) -> (() -> JSValue)? {
-        self[name].function.map { function in 
+    public subscript(dynamicMember name: String) -> (() -> JSValue)? {
+        self[name].function.map { function in
             { function(this: self) }
         }
     }
 
     @_disfavoredOverload
-    subscript(dynamicMember name: String) -> ((A0) -> JSValue)? {
-        self[name].function.map { function in 
+    public subscript(dynamicMember name: String) -> ((A0) -> JSValue)? {
+        self[name].function.map { function in
             { function(this: self, $0) }
         }
     }
 
     @_disfavoredOverload
-    subscript<
+    public subscript<
         A0: ConvertibleToJSValue,
         A1: ConvertibleToJSValue
     >(dynamicMember name: String) -> ((A0, A1) -> JSValue)? {
-        self[name].function.map { function in 
+        self[name].function.map { function in
             { function(this: self, $0, $1) }
         }
     }
 
     @_disfavoredOverload
-    subscript<
+    public subscript<
         A0: ConvertibleToJSValue,
         A1: ConvertibleToJSValue,
         A2: ConvertibleToJSValue
@@ -342,7 +361,7 @@ public extension JSObject {
     }
 
     @_disfavoredOverload
-    subscript<
+    public subscript<
         A0: ConvertibleToJSValue,
         A1: ConvertibleToJSValue,
         A2: ConvertibleToJSValue,
@@ -354,7 +373,7 @@ public extension JSObject {
     }
 
     @_disfavoredOverload
-    subscript<
+    public subscript<
         A0: ConvertibleToJSValue,
         A1: ConvertibleToJSValue,
         A2: ConvertibleToJSValue,
@@ -367,7 +386,7 @@ public extension JSObject {
     }
 
     @_disfavoredOverload
-    subscript<
+    public subscript<
         A0: ConvertibleToJSValue,
         A1: ConvertibleToJSValue,
         A2: ConvertibleToJSValue,
@@ -381,7 +400,7 @@ public extension JSObject {
     }
 
     @_disfavoredOverload
-    subscript<
+    public subscript<
         A0: ConvertibleToJSValue,
         A1: ConvertibleToJSValue,
         A2: ConvertibleToJSValue,
diff --git a/Sources/JavaScriptKit/FundamentalObjects/JSString.swift b/Sources/JavaScriptKit/FundamentalObjects/JSString.swift
index 686d1ba11..4e6a0a085 100644
--- a/Sources/JavaScriptKit/FundamentalObjects/JSString.swift
+++ b/Sources/JavaScriptKit/FundamentalObjects/JSString.swift
@@ -66,7 +66,7 @@ public struct JSString: LosslessStringConvertible, Equatable {
     public init(_ stringValue: String) {
         self.guts = Guts(from: stringValue)
     }
-    
+
     /// A Swift representation of this `JSString`.
     /// Note that this accessor may copy the JS string value into Swift side memory.
     public var description: String { guts.buffer }
@@ -77,7 +77,11 @@ public struct JSString: LosslessStringConvertible, Equatable {
     ///   - lhs: A string to compare.
     ///   - rhs: Another string to compare.
     public static func == (lhs: JSString, rhs: JSString) -> Bool {
-        return lhs.guts.buffer == rhs.guts.buffer
+        withExtendedLifetime(lhs.guts) { lhsGuts in
+            withExtendedLifetime(rhs.guts) { rhsGuts in
+                return swjs_value_equals(lhsGuts.jsRef, rhsGuts.jsRef)
+            }
+        }
     }
 }
 
@@ -87,18 +91,10 @@ extension JSString: ExpressibleByStringLiteral {
     }
 }
 
-
 // MARK: - Internal Helpers
 extension JSString {
 
     func asInternalJSRef() -> JavaScriptObjectRef {
         guts.jsRef
     }
-
-    func withRawJSValue(_ body: (RawJSValue) -> T) -> T {
-        let rawValue = RawJSValue(
-            kind: .string, payload1: guts.jsRef, payload2: 0
-        )
-        return body(rawValue)
-    }
 }
diff --git a/Sources/JavaScriptKit/FundamentalObjects/JSSymbol.swift b/Sources/JavaScriptKit/FundamentalObjects/JSSymbol.swift
index 42f63e010..a9461317b 100644
--- a/Sources/JavaScriptKit/FundamentalObjects/JSSymbol.swift
+++ b/Sources/JavaScriptKit/FundamentalObjects/JSSymbol.swift
@@ -24,6 +24,11 @@ public class JSSymbol: JSObject {
         super.init(id: id)
     }
 
+    @available(*, unavailable, message: "JSSymbol does not support dictionary literal initialization")
+    public required init(dictionaryLiteral elements: (String, JSValue)...) {
+        fatalError("JSSymbol does not support dictionary literal initialization")
+    }
+
     public static func `for`(key: JSString) -> JSSymbol {
         Symbol.for!(key).symbol!
     }
diff --git a/Sources/JavaScriptKit/FundamentalObjects/JSThrowingFunction.swift b/Sources/JavaScriptKit/FundamentalObjects/JSThrowingFunction.swift
index 17b61090f..aee17fd69 100644
--- a/Sources/JavaScriptKit/FundamentalObjects/JSThrowingFunction.swift
+++ b/Sources/JavaScriptKit/FundamentalObjects/JSThrowingFunction.swift
@@ -1,7 +1,6 @@
 #if !hasFeature(Embedded)
 import _CJavaScriptKit
 
-
 /// A `JSFunction` wrapper that enables throwing function calls.
 /// Exceptions produced by JavaScript functions will be thrown as `JSValue`.
 public class JSThrowingFunction {
@@ -46,12 +45,20 @@ public class JSThrowingFunction {
                 var exceptionPayload1 = JavaScriptPayload1()
                 var exceptionPayload2 = JavaScriptPayload2()
                 let resultObj = swjs_call_throwing_new(
-                    self.base.id, argv, Int32(argc),
-                    &exceptionRawKind, &exceptionPayload1, &exceptionPayload2
+                    self.base.id,
+                    argv,
+                    Int32(argc),
+                    &exceptionRawKind,
+                    &exceptionPayload1,
+                    &exceptionPayload2
                 )
                 let exceptionKind = JavaScriptValueKindAndFlags(bitPattern: exceptionRawKind)
                 if exceptionKind.isException {
-                    let exception = RawJSValue(kind: exceptionKind.kind, payload1: exceptionPayload1, payload2: exceptionPayload2)
+                    let exception = RawJSValue(
+                        kind: exceptionKind.kind,
+                        payload1: exceptionPayload1,
+                        payload2: exceptionPayload2
+                    )
                     return .failure(JSException(exception.jsValue))
                 }
                 return .success(JSObject(id: resultObj))
@@ -65,7 +72,11 @@ public class JSThrowingFunction {
     }
 }
 
-private func invokeJSFunction(_ jsFunc: JSFunction, arguments: [ConvertibleToJSValue], this: JSObject?) throws -> JSValue {
+private func invokeJSFunction(
+    _ jsFunc: JSFunction,
+    arguments: [ConvertibleToJSValue],
+    this: JSObject?
+) throws -> JSValue {
     let id = jsFunc.id
     let (result, isException) = arguments.withRawJSValues { rawValues in
         rawValues.withUnsafeBufferPointer { bufferPointer -> (JSValue, Bool) in
@@ -76,14 +87,21 @@ private func invokeJSFunction(_ jsFunc: JSFunction, arguments: [ConvertibleToJSV
             var payload2 = JavaScriptPayload2()
             if let thisId = this?.id {
                 let resultBitPattern = swjs_call_function_with_this(
-                    thisId, id, argv, Int32(argc),
-                    &payload1, &payload2
+                    thisId,
+                    id,
+                    argv,
+                    Int32(argc),
+                    &payload1,
+                    &payload2
                 )
                 kindAndFlags = JavaScriptValueKindAndFlags(bitPattern: resultBitPattern)
             } else {
                 let resultBitPattern = swjs_call_function(
-                    id, argv, Int32(argc),
-                    &payload1, &payload2
+                    id,
+                    argv,
+                    Int32(argc),
+                    &payload1,
+                    &payload2
                 )
                 kindAndFlags = JavaScriptValueKindAndFlags(bitPattern: resultBitPattern)
             }
diff --git a/Sources/JavaScriptKit/JSBridgedType.swift b/Sources/JavaScriptKit/JSBridgedType.swift
index dcc0a3857..92739079e 100644
--- a/Sources/JavaScriptKit/JSBridgedType.swift
+++ b/Sources/JavaScriptKit/JSBridgedType.swift
@@ -5,12 +5,12 @@ public protocol JSBridgedType: JSValueCompatible, CustomStringConvertible {
     init?(from value: JSValue)
 }
 
-public extension JSBridgedType {
-    static func construct(from value: JSValue) -> Self? {
+extension JSBridgedType {
+    public static func construct(from value: JSValue) -> Self? {
         Self(from: value)
     }
 
-    var description: String { jsValue.description }
+    public var description: String { jsValue.description }
 }
 
 /// Conform to this protocol when your Swift class wraps a JavaScript class.
@@ -27,15 +27,15 @@ public protocol JSBridgedClass: JSBridgedType {
     init(unsafelyWrapping jsObject: JSObject)
 }
 
-public extension JSBridgedClass {
-    var jsValue: JSValue { jsObject.jsValue }
+extension JSBridgedClass {
+    public var jsValue: JSValue { jsObject.jsValue }
 
-    init?(from value: JSValue) {
+    public init?(from value: JSValue) {
         guard let object = value.object else { return nil }
         self.init(from: object)
     }
 
-    init?(from object: JSObject) {
+    public init?(from object: JSObject) {
         guard let constructor = Self.constructor, object.isInstanceOf(constructor) else { return nil }
         self.init(unsafelyWrapping: object)
     }
diff --git a/Sources/JavaScriptKit/JSException.swift b/Sources/JavaScriptKit/JSException.swift
index 393ae9615..1b9e311fd 100644
--- a/Sources/JavaScriptKit/JSException.swift
+++ b/Sources/JavaScriptKit/JSException.swift
@@ -12,11 +12,11 @@
 ///     let jsErrorValue = error.thrownValue
 /// }
 /// ```
-public struct JSException: Error, Equatable {
+public struct JSException: Error, Equatable, CustomStringConvertible {
     /// The value thrown from JavaScript.
     /// This can be any JavaScript value (error object, string, number, etc.).
     public var thrownValue: JSValue {
-      return _thrownValue
+        return _thrownValue
     }
 
     /// The actual JavaScript value that was thrown.
@@ -25,10 +25,25 @@ public struct JSException: Error, Equatable {
     /// from `Error` protocol.
     private nonisolated(unsafe) let _thrownValue: JSValue
 
+    /// A description of the exception.
+    public let description: String
+
+    /// The stack trace of the exception.
+    public let stack: String?
+
     /// Initializes a new JSException instance with a value thrown from JavaScript.
     ///
-    /// Only available within the package.
+    /// Only available within the package. This must be called on the thread where the exception object created.
     package init(_ thrownValue: JSValue) {
         self._thrownValue = thrownValue
+        // Capture the stringified representation on the object owner thread
+        // to bring useful info to the catching thread even if they are different threads.
+        if let errorObject = thrownValue.object, let stack = errorObject.stack.string {
+            self.description = "JSException(\(stack))"
+            self.stack = stack
+        } else {
+            self.description = "JSException(\(thrownValue))"
+            self.stack = nil
+        }
     }
 }
diff --git a/Sources/JavaScriptKit/JSValue.swift b/Sources/JavaScriptKit/JSValue.swift
index 2562daac8..b9f8dd4a7 100644
--- a/Sources/JavaScriptKit/JSValue.swift
+++ b/Sources/JavaScriptKit/JSValue.swift
@@ -17,7 +17,7 @@ public enum JSValue: Equatable {
     /// If not, returns `nil`.
     public var boolean: Bool? {
         switch self {
-        case let .boolean(boolean): return boolean
+        case .boolean(let boolean): return boolean
         default: return nil
         }
     }
@@ -37,7 +37,7 @@ public enum JSValue: Equatable {
     ///
     public var jsString: JSString? {
         switch self {
-        case let .string(string): return string
+        case .string(let string): return string
         default: return nil
         }
     }
@@ -46,7 +46,7 @@ public enum JSValue: Equatable {
     /// If not, returns `nil`.
     public var number: Double? {
         switch self {
-        case let .number(number): return number
+        case .number(let number): return number
         default: return nil
         }
     }
@@ -55,7 +55,7 @@ public enum JSValue: Equatable {
     /// If not, returns `nil`.
     public var object: JSObject? {
         switch self {
-        case let .object(object): return object
+        case .object(let object): return object
         default: return nil
         }
     }
@@ -64,7 +64,7 @@ public enum JSValue: Equatable {
     /// If not, returns `nil`.
     public var function: JSFunction? {
         switch self {
-        case let .function(function): return function
+        case .function(let function): return function
         default: return nil
         }
     }
@@ -73,7 +73,7 @@ public enum JSValue: Equatable {
     /// If not, returns `nil`.
     public var symbol: JSSymbol? {
         switch self {
-        case let .symbol(symbol): return symbol
+        case .symbol(let symbol): return symbol
         default: return nil
         }
     }
@@ -82,7 +82,7 @@ public enum JSValue: Equatable {
     /// If not, returns `nil`.
     public var bigInt: JSBigInt? {
         switch self {
-        case let .bigInt(bigInt): return bigInt
+        case .bigInt(let bigInt): return bigInt
         default: return nil
         }
     }
@@ -107,38 +107,38 @@ public enum JSValue: Equatable {
 @available(*, unavailable)
 extension JSValue: Sendable {}
 
-public extension JSValue {
-#if !hasFeature(Embedded)
+extension JSValue {
+    #if !hasFeature(Embedded)
     /// An unsafe convenience method of `JSObject.subscript(_ name: String) -> ((ConvertibleToJSValue...) -> JSValue)?`
     /// - Precondition: `self` must be a JavaScript Object and specified member should be a callable object.
-    subscript(dynamicMember name: String) -> ((ConvertibleToJSValue...) -> JSValue) {
+    public subscript(dynamicMember name: String) -> ((ConvertibleToJSValue...) -> JSValue) {
         object![dynamicMember: name]!
     }
-#endif
+    #endif
 
     /// An unsafe convenience method of `JSObject.subscript(_ index: Int) -> JSValue`
     /// - Precondition: `self` must be a JavaScript Object.
-    subscript(dynamicMember name: String) -> JSValue {
+    public subscript(dynamicMember name: String) -> JSValue {
         get { self.object![name] }
-        set { self.object![name] = newValue }
+        nonmutating set { self.object![name] = newValue }
     }
 
     /// An unsafe convenience method of `JSObject.subscript(_ index: Int) -> JSValue`
     /// - Precondition: `self` must be a JavaScript Object.
-    subscript(_ index: Int) -> JSValue {
+    public subscript(_ index: Int) -> JSValue {
         get { object![index] }
-        set { object![index] = newValue }
+        nonmutating set { object![index] = newValue }
     }
 }
 
-public extension JSValue {
-    func fromJSValue() -> Type? where Type: ConstructibleFromJSValue {
+extension JSValue {
+    public func fromJSValue() -> Type? where Type: ConstructibleFromJSValue {
         return Type.construct(from: self)
     }
 }
 
-public extension JSValue {
-    static func string(_ value: String) -> JSValue {
+extension JSValue {
+    public static func string(_ value: String) -> JSValue {
         .string(JSString(value))
     }
 
@@ -167,12 +167,17 @@ public extension JSValue {
     /// eventListener.release()
     /// ```
     @available(*, deprecated, message: "Please create JSClosure directly and manage its lifetime manually.")
-    static func function(_ body: @escaping ([JSValue]) -> JSValue) -> JSValue {
+    public static func function(_ body: @escaping ([JSValue]) -> JSValue) -> JSValue {
         .object(JSClosure(body))
     }
 
-    @available(*, deprecated, renamed: "object", message: "JSClosure is no longer a subclass of JSFunction. Use .object(closure) instead.")
-    static func function(_ closure: JSClosure) -> JSValue {
+    @available(
+        *,
+        deprecated,
+        renamed: "object",
+        message: "JSClosure is no longer a subclass of JSFunction. Use .object(closure) instead."
+    )
+    public static func function(_ closure: JSClosure) -> JSValue {
         .object(closure)
     }
 }
@@ -204,8 +209,10 @@ extension JSValue: ExpressibleByNilLiteral {
 public func getJSValue(this: JSObject, name: JSString) -> JSValue {
     var rawValue = RawJSValue()
     let rawBitPattern = swjs_get_prop(
-        this.id, name.asInternalJSRef(),
-        &rawValue.payload1, &rawValue.payload2
+        this.id,
+        name.asInternalJSRef(),
+        &rawValue.payload1,
+        &rawValue.payload2
     )
     rawValue.kind = unsafeBitCast(rawBitPattern, to: JavaScriptValueKind.self)
     return rawValue.jsValue
@@ -220,8 +227,10 @@ public func setJSValue(this: JSObject, name: JSString, value: JSValue) {
 public func getJSValue(this: JSObject, index: Int32) -> JSValue {
     var rawValue = RawJSValue()
     let rawBitPattern = swjs_get_subscript(
-        this.id, index,
-        &rawValue.payload1, &rawValue.payload2
+        this.id,
+        index,
+        &rawValue.payload1,
+        &rawValue.payload2
     )
     rawValue.kind = unsafeBitCast(rawBitPattern, to: JavaScriptValueKind.self)
     return rawValue.jsValue
@@ -229,17 +238,23 @@ public func getJSValue(this: JSObject, index: Int32) -> JSValue {
 
 public func setJSValue(this: JSObject, index: Int32, value: JSValue) {
     value.withRawJSValue { rawValue in
-        swjs_set_subscript(this.id, index,
-                       rawValue.kind,
-                       rawValue.payload1, rawValue.payload2)
+        swjs_set_subscript(
+            this.id,
+            index,
+            rawValue.kind,
+            rawValue.payload1,
+            rawValue.payload2
+        )
     }
 }
 
 public func getJSValue(this: JSObject, symbol: JSSymbol) -> JSValue {
     var rawValue = RawJSValue()
     let rawBitPattern = swjs_get_prop(
-        this.id, symbol.id,
-        &rawValue.payload1, &rawValue.payload2
+        this.id,
+        symbol.id,
+        &rawValue.payload1,
+        &rawValue.payload2
     )
     rawValue.kind = unsafeBitCast(rawBitPattern, to: JavaScriptValueKind.self)
     return rawValue.jsValue
@@ -251,18 +266,18 @@ public func setJSValue(this: JSObject, symbol: JSSymbol, value: JSValue) {
     }
 }
 
-public extension JSValue {
+extension JSValue {
     /// Return `true` if this value is an instance of the passed `constructor` function.
     /// Returns `false` for everything except objects and functions.
     /// - Parameter constructor: The constructor function to check.
     /// - Returns: The result of `instanceof` in the JavaScript environment.
-    func isInstanceOf(_ constructor: JSFunction) -> Bool {
+    public func isInstanceOf(_ constructor: JSFunction) -> Bool {
         switch self {
         case .boolean, .string, .number, .null, .undefined, .symbol, .bigInt:
             return false
-        case let .object(ref):
+        case .object(let ref):
             return ref.isInstanceOf(constructor)
-        case let .function(ref):
+        case .function(let ref):
             return ref.isInstanceOf(constructor)
         }
     }
@@ -285,19 +300,19 @@ extension JSValue: CustomStringConvertible {
 //
 // Note: Once Embedded Swift supports parameter packs/variadic generics, we can
 // replace all of these with a single method that takes a generic pack.
-public extension JSValue {
+extension JSValue {
     @_disfavoredOverload
-    subscript(dynamicMember name: String) -> (() -> JSValue) {
+    public subscript(dynamicMember name: String) -> (() -> JSValue) {
         object![dynamicMember: name]!
     }
 
     @_disfavoredOverload
-    subscript(dynamicMember name: String) -> ((A0) -> JSValue) {
+    public subscript(dynamicMember name: String) -> ((A0) -> JSValue) {
         object![dynamicMember: name]!
     }
 
     @_disfavoredOverload
-    subscript<
+    public subscript<
         A0: ConvertibleToJSValue,
         A1: ConvertibleToJSValue
     >(dynamicMember name: String) -> ((A0, A1) -> JSValue) {
@@ -305,7 +320,7 @@ public extension JSValue {
     }
 
     @_disfavoredOverload
-    subscript<
+    public subscript<
         A0: ConvertibleToJSValue,
         A1: ConvertibleToJSValue,
         A2: ConvertibleToJSValue
@@ -314,7 +329,7 @@ public extension JSValue {
     }
 
     @_disfavoredOverload
-    subscript<
+    public subscript<
         A0: ConvertibleToJSValue,
         A1: ConvertibleToJSValue,
         A2: ConvertibleToJSValue,
@@ -324,7 +339,7 @@ public extension JSValue {
     }
 
     @_disfavoredOverload
-    subscript<
+    public subscript<
         A0: ConvertibleToJSValue,
         A1: ConvertibleToJSValue,
         A2: ConvertibleToJSValue,
@@ -335,7 +350,7 @@ public extension JSValue {
     }
 
     @_disfavoredOverload
-    subscript<
+    public subscript<
         A0: ConvertibleToJSValue,
         A1: ConvertibleToJSValue,
         A2: ConvertibleToJSValue,
@@ -347,7 +362,7 @@ public extension JSValue {
     }
 
     @_disfavoredOverload
-    subscript<
+    public subscript<
         A0: ConvertibleToJSValue,
         A1: ConvertibleToJSValue,
         A2: ConvertibleToJSValue,
diff --git a/Sources/JavaScriptKit/JSValueDecoder.swift b/Sources/JavaScriptKit/JSValueDecoder.swift
index b2cf7b2a3..08e29915c 100644
--- a/Sources/JavaScriptKit/JSValueDecoder.swift
+++ b/Sources/JavaScriptKit/JSValueDecoder.swift
@@ -121,7 +121,10 @@ private struct _KeyedDecodingContainer: KeyedDecodingContainerPr
         return try T(from: _decoder(forKey: key))
     }
 
-    func nestedContainer(keyedBy _: NestedKey.Type, forKey key: Key) throws -> KeyedDecodingContainer where NestedKey: CodingKey {
+    func nestedContainer(
+        keyedBy _: NestedKey.Type,
+        forKey key: Key
+    ) throws -> KeyedDecodingContainer where NestedKey: CodingKey {
         try _decoder(forKey: key).container(keyedBy: NestedKey.self)
     }
 
@@ -185,7 +188,8 @@ private struct _UnkeyedDecodingContainer: UnkeyedDecodingContainer {
         return try T(from: _decoder())
     }
 
-    mutating func nestedContainer(keyedBy _: NestedKey.Type) throws -> KeyedDecodingContainer where NestedKey: CodingKey {
+    mutating func nestedContainer(keyedBy _: NestedKey.Type) throws -> KeyedDecodingContainer
+    where NestedKey: CodingKey {
         return try _decoder().container(keyedBy: NestedKey.self)
     }
 
diff --git a/Sources/JavaScriptKit/Macros.swift b/Sources/JavaScriptKit/Macros.swift
new file mode 100644
index 000000000..bddd8c7cd
--- /dev/null
+++ b/Sources/JavaScriptKit/Macros.swift
@@ -0,0 +1,35 @@
+/// A macro that exposes Swift functions, classes, and methods to JavaScript.
+///
+/// Apply this macro to Swift declarations that you want to make callable from JavaScript:
+///
+/// ```swift
+/// // Export a function to JavaScript
+/// @JS public func greet(name: String) -> String {
+///     return "Hello, \(name)!"
+/// }
+///
+/// // Export a class and its members
+/// @JS class Counter {
+///     private var count = 0
+///
+///     @JS init() {}
+///
+///     @JS func increment() {
+///         count += 1
+///     }
+///
+///     @JS func getValue() -> Int {
+///         return count
+///     }
+/// }
+/// ```
+///
+/// When you build your project with the BridgeJS plugin, these declarations will be
+/// accessible from JavaScript, and TypeScript declaration files (`.d.ts`) will be
+/// automatically generated to provide type safety.
+///
+/// For detailed usage information, see the article .
+///
+/// - Important: This feature is still experimental. No API stability is guaranteed, and the API may change in future releases.
+@attached(peer)
+public macro JS() = Builtin.ExternalMacro
diff --git a/Sources/JavaScriptKit/Runtime/index.d.ts b/Sources/JavaScriptKit/Runtime/index.d.ts
new file mode 120000
index 000000000..0d94264b6
--- /dev/null
+++ b/Sources/JavaScriptKit/Runtime/index.d.ts
@@ -0,0 +1 @@
+../../../Plugins/PackageToJS/Templates/runtime.d.ts
\ No newline at end of file
diff --git a/Sources/JavaScriptKit/Runtime/index.js b/Sources/JavaScriptKit/Runtime/index.js
deleted file mode 100644
index 25b6af3c9..000000000
--- a/Sources/JavaScriptKit/Runtime/index.js
+++ /dev/null
@@ -1,831 +0,0 @@
-(function (global, factory) {
-    typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports) :
-    typeof define === 'function' && define.amd ? define(['exports'], factory) :
-    (global = typeof globalThis !== 'undefined' ? globalThis : global || self, factory(global.JavaScriptKit = {}));
-})(this, (function (exports) { 'use strict';
-
-    /// Memory lifetime of closures in Swift are managed by Swift side
-    class SwiftClosureDeallocator {
-        constructor(exports) {
-            if (typeof FinalizationRegistry === "undefined") {
-                throw new Error("The Swift part of JavaScriptKit was configured to require " +
-                    "the availability of JavaScript WeakRefs. Please build " +
-                    "with `-Xswiftc -DJAVASCRIPTKIT_WITHOUT_WEAKREFS` to " +
-                    "disable features that use WeakRefs.");
-            }
-            this.functionRegistry = new FinalizationRegistry((id) => {
-                exports.swjs_free_host_function(id);
-            });
-        }
-        track(func, func_ref) {
-            this.functionRegistry.register(func, func_ref);
-        }
-    }
-
-    function assertNever(x, message) {
-        throw new Error(message);
-    }
-    const MAIN_THREAD_TID = -1;
-
-    const decode = (kind, payload1, payload2, memory) => {
-        switch (kind) {
-            case 0 /* Boolean */:
-                switch (payload1) {
-                    case 0:
-                        return false;
-                    case 1:
-                        return true;
-                }
-            case 2 /* Number */:
-                return payload2;
-            case 1 /* String */:
-            case 3 /* Object */:
-            case 6 /* Function */:
-            case 7 /* Symbol */:
-            case 8 /* BigInt */:
-                return memory.getObject(payload1);
-            case 4 /* Null */:
-                return null;
-            case 5 /* Undefined */:
-                return undefined;
-            default:
-                assertNever(kind, `JSValue Type kind "${kind}" is not supported`);
-        }
-    };
-    // Note:
-    // `decodeValues` assumes that the size of RawJSValue is 16.
-    const decodeArray = (ptr, length, memory) => {
-        // fast path for empty array
-        if (length === 0) {
-            return [];
-        }
-        let result = [];
-        // It's safe to hold DataView here because WebAssembly.Memory.buffer won't
-        // change within this function.
-        const view = memory.dataView();
-        for (let index = 0; index < length; index++) {
-            const base = ptr + 16 * index;
-            const kind = view.getUint32(base, true);
-            const payload1 = view.getUint32(base + 4, true);
-            const payload2 = view.getFloat64(base + 8, true);
-            result.push(decode(kind, payload1, payload2, memory));
-        }
-        return result;
-    };
-    // A helper function to encode a RawJSValue into a pointers.
-    // Please prefer to use `writeAndReturnKindBits` to avoid unnecessary
-    // memory stores.
-    // This function should be used only when kind flag is stored in memory.
-    const write = (value, kind_ptr, payload1_ptr, payload2_ptr, is_exception, memory) => {
-        const kind = writeAndReturnKindBits(value, payload1_ptr, payload2_ptr, is_exception, memory);
-        memory.writeUint32(kind_ptr, kind);
-    };
-    const writeAndReturnKindBits = (value, payload1_ptr, payload2_ptr, is_exception, memory) => {
-        const exceptionBit = (is_exception ? 1 : 0) << 31;
-        if (value === null) {
-            return exceptionBit | 4 /* Null */;
-        }
-        const writeRef = (kind) => {
-            memory.writeUint32(payload1_ptr, memory.retain(value));
-            return exceptionBit | kind;
-        };
-        const type = typeof value;
-        switch (type) {
-            case "boolean": {
-                memory.writeUint32(payload1_ptr, value ? 1 : 0);
-                return exceptionBit | 0 /* Boolean */;
-            }
-            case "number": {
-                memory.writeFloat64(payload2_ptr, value);
-                return exceptionBit | 2 /* Number */;
-            }
-            case "string": {
-                return writeRef(1 /* String */);
-            }
-            case "undefined": {
-                return exceptionBit | 5 /* Undefined */;
-            }
-            case "object": {
-                return writeRef(3 /* Object */);
-            }
-            case "function": {
-                return writeRef(6 /* Function */);
-            }
-            case "symbol": {
-                return writeRef(7 /* Symbol */);
-            }
-            case "bigint": {
-                return writeRef(8 /* BigInt */);
-            }
-            default:
-                assertNever(type, `Type "${type}" is not supported yet`);
-        }
-        throw new Error("Unreachable");
-    };
-    function decodeObjectRefs(ptr, length, memory) {
-        const result = new Array(length);
-        for (let i = 0; i < length; i++) {
-            result[i] = memory.readUint32(ptr + 4 * i);
-        }
-        return result;
-    }
-
-    let globalVariable;
-    if (typeof globalThis !== "undefined") {
-        globalVariable = globalThis;
-    }
-    else if (typeof window !== "undefined") {
-        globalVariable = window;
-    }
-    else if (typeof global !== "undefined") {
-        globalVariable = global;
-    }
-    else if (typeof self !== "undefined") {
-        globalVariable = self;
-    }
-
-    class SwiftRuntimeHeap {
-        constructor() {
-            this._heapValueById = new Map();
-            this._heapValueById.set(0, globalVariable);
-            this._heapEntryByValue = new Map();
-            this._heapEntryByValue.set(globalVariable, { id: 0, rc: 1 });
-            // Note: 0 is preserved for global
-            this._heapNextKey = 1;
-        }
-        retain(value) {
-            const entry = this._heapEntryByValue.get(value);
-            if (entry) {
-                entry.rc++;
-                return entry.id;
-            }
-            const id = this._heapNextKey++;
-            this._heapValueById.set(id, value);
-            this._heapEntryByValue.set(value, { id: id, rc: 1 });
-            return id;
-        }
-        release(ref) {
-            const value = this._heapValueById.get(ref);
-            const entry = this._heapEntryByValue.get(value);
-            entry.rc--;
-            if (entry.rc != 0)
-                return;
-            this._heapEntryByValue.delete(value);
-            this._heapValueById.delete(ref);
-        }
-        referenceHeap(ref) {
-            const value = this._heapValueById.get(ref);
-            if (value === undefined) {
-                throw new ReferenceError("Attempted to read invalid reference " + ref);
-            }
-            return value;
-        }
-    }
-
-    class Memory {
-        constructor(exports) {
-            this.heap = new SwiftRuntimeHeap();
-            this.retain = (value) => this.heap.retain(value);
-            this.getObject = (ref) => this.heap.referenceHeap(ref);
-            this.release = (ref) => this.heap.release(ref);
-            this.bytes = () => new Uint8Array(this.rawMemory.buffer);
-            this.dataView = () => new DataView(this.rawMemory.buffer);
-            this.writeBytes = (ptr, bytes) => this.bytes().set(bytes, ptr);
-            this.readUint32 = (ptr) => this.dataView().getUint32(ptr, true);
-            this.readUint64 = (ptr) => this.dataView().getBigUint64(ptr, true);
-            this.readInt64 = (ptr) => this.dataView().getBigInt64(ptr, true);
-            this.readFloat64 = (ptr) => this.dataView().getFloat64(ptr, true);
-            this.writeUint32 = (ptr, value) => this.dataView().setUint32(ptr, value, true);
-            this.writeUint64 = (ptr, value) => this.dataView().setBigUint64(ptr, value, true);
-            this.writeInt64 = (ptr, value) => this.dataView().setBigInt64(ptr, value, true);
-            this.writeFloat64 = (ptr, value) => this.dataView().setFloat64(ptr, value, true);
-            this.rawMemory = exports.memory;
-        }
-    }
-
-    class ITCInterface {
-        constructor(memory) {
-            this.memory = memory;
-        }
-        send(sendingObject, transferringObjects, sendingContext) {
-            const object = this.memory.getObject(sendingObject);
-            const transfer = transferringObjects.map(ref => this.memory.getObject(ref));
-            return { object, sendingContext, transfer };
-        }
-        sendObjects(sendingObjects, transferringObjects, sendingContext) {
-            const objects = sendingObjects.map(ref => this.memory.getObject(ref));
-            const transfer = transferringObjects.map(ref => this.memory.getObject(ref));
-            return { object: objects, sendingContext, transfer };
-        }
-        release(objectRef) {
-            this.memory.release(objectRef);
-            return { object: undefined, transfer: [] };
-        }
-    }
-    class MessageBroker {
-        constructor(selfTid, threadChannel, handlers) {
-            this.selfTid = selfTid;
-            this.threadChannel = threadChannel;
-            this.handlers = handlers;
-        }
-        request(message) {
-            if (message.data.targetTid == this.selfTid) {
-                // The request is for the current thread
-                this.handlers.onRequest(message);
-            }
-            else if ("postMessageToWorkerThread" in this.threadChannel) {
-                // The request is for another worker thread sent from the main thread
-                this.threadChannel.postMessageToWorkerThread(message.data.targetTid, message, []);
-            }
-            else if ("postMessageToMainThread" in this.threadChannel) {
-                // The request is for other worker threads or the main thread sent from a worker thread
-                this.threadChannel.postMessageToMainThread(message, []);
-            }
-            else {
-                throw new Error("unreachable");
-            }
-        }
-        reply(message) {
-            if (message.data.sourceTid == this.selfTid) {
-                // The response is for the current thread
-                this.handlers.onResponse(message);
-                return;
-            }
-            const transfer = message.data.response.ok ? message.data.response.value.transfer : [];
-            if ("postMessageToWorkerThread" in this.threadChannel) {
-                // The response is for another worker thread sent from the main thread
-                this.threadChannel.postMessageToWorkerThread(message.data.sourceTid, message, transfer);
-            }
-            else if ("postMessageToMainThread" in this.threadChannel) {
-                // The response is for other worker threads or the main thread sent from a worker thread
-                this.threadChannel.postMessageToMainThread(message, transfer);
-            }
-            else {
-                throw new Error("unreachable");
-            }
-        }
-        onReceivingRequest(message) {
-            if (message.data.targetTid == this.selfTid) {
-                this.handlers.onRequest(message);
-            }
-            else if ("postMessageToWorkerThread" in this.threadChannel) {
-                // Receive a request from a worker thread to other worker on main thread. 
-                // Proxy the request to the target worker thread.
-                this.threadChannel.postMessageToWorkerThread(message.data.targetTid, message, []);
-            }
-            else if ("postMessageToMainThread" in this.threadChannel) {
-                // A worker thread won't receive a request for other worker threads
-                throw new Error("unreachable");
-            }
-        }
-        onReceivingResponse(message) {
-            if (message.data.sourceTid == this.selfTid) {
-                this.handlers.onResponse(message);
-            }
-            else if ("postMessageToWorkerThread" in this.threadChannel) {
-                // Receive a response from a worker thread to other worker on main thread.
-                // Proxy the response to the target worker thread.
-                const transfer = message.data.response.ok ? message.data.response.value.transfer : [];
-                this.threadChannel.postMessageToWorkerThread(message.data.sourceTid, message, transfer);
-            }
-            else if ("postMessageToMainThread" in this.threadChannel) {
-                // A worker thread won't receive a response for other worker threads
-                throw new Error("unreachable");
-            }
-        }
-    }
-    function serializeError(error) {
-        if (error instanceof Error) {
-            return { isError: true, value: { message: error.message, name: error.name, stack: error.stack } };
-        }
-        return { isError: false, value: error };
-    }
-    function deserializeError(error) {
-        if (error.isError) {
-            return Object.assign(new Error(error.value.message), error.value);
-        }
-        return error.value;
-    }
-
-    class SwiftRuntime {
-        constructor(options) {
-            this.version = 708;
-            this.textDecoder = new TextDecoder("utf-8");
-            this.textEncoder = new TextEncoder(); // Only support utf-8
-            /** @deprecated Use `wasmImports` instead */
-            this.importObjects = () => this.wasmImports;
-            this._instance = null;
-            this._memory = null;
-            this._closureDeallocator = null;
-            this.tid = null;
-            this.options = options || {};
-        }
-        setInstance(instance) {
-            this._instance = instance;
-            if (typeof this.exports._start === "function") {
-                throw new Error(`JavaScriptKit supports only WASI reactor ABI.
-                Please make sure you are building with:
-                -Xswiftc -Xclang-linker -Xswiftc -mexec-model=reactor
-                `);
-            }
-            if (this.exports.swjs_library_version() != this.version) {
-                throw new Error(`The versions of JavaScriptKit are incompatible.
-                WebAssembly runtime ${this.exports.swjs_library_version()} != JS runtime ${this.version}`);
-            }
-        }
-        main() {
-            const instance = this.instance;
-            try {
-                if (typeof instance.exports.main === "function") {
-                    instance.exports.main();
-                }
-                else if (typeof instance.exports.__main_argc_argv === "function") {
-                    // Swift 6.0 and later use `__main_argc_argv` instead of `main`.
-                    instance.exports.__main_argc_argv(0, 0);
-                }
-            }
-            catch (error) {
-                if (error instanceof UnsafeEventLoopYield) {
-                    // Ignore the error
-                    return;
-                }
-                // Rethrow other errors
-                throw error;
-            }
-        }
-        /**
-         * Start a new thread with the given `tid` and `startArg`, which
-         * is forwarded to the `wasi_thread_start` function.
-         * This function is expected to be called from the spawned Web Worker thread.
-         */
-        startThread(tid, startArg) {
-            this.tid = tid;
-            const instance = this.instance;
-            try {
-                if (typeof instance.exports.wasi_thread_start === "function") {
-                    instance.exports.wasi_thread_start(tid, startArg);
-                }
-                else {
-                    throw new Error(`The WebAssembly module is not built for wasm32-unknown-wasip1-threads target.`);
-                }
-            }
-            catch (error) {
-                if (error instanceof UnsafeEventLoopYield) {
-                    // Ignore the error
-                    return;
-                }
-                // Rethrow other errors
-                throw error;
-            }
-        }
-        get instance() {
-            if (!this._instance)
-                throw new Error("WebAssembly instance is not set yet");
-            return this._instance;
-        }
-        get exports() {
-            return this.instance.exports;
-        }
-        get memory() {
-            if (!this._memory) {
-                this._memory = new Memory(this.instance.exports);
-            }
-            return this._memory;
-        }
-        get closureDeallocator() {
-            if (this._closureDeallocator)
-                return this._closureDeallocator;
-            const features = this.exports.swjs_library_features();
-            const librarySupportsWeakRef = (features & 1 /* WeakRefs */) != 0;
-            if (librarySupportsWeakRef) {
-                this._closureDeallocator = new SwiftClosureDeallocator(this.exports);
-            }
-            return this._closureDeallocator;
-        }
-        callHostFunction(host_func_id, line, file, args) {
-            const argc = args.length;
-            const argv = this.exports.swjs_prepare_host_function_call(argc);
-            const memory = this.memory;
-            for (let index = 0; index < args.length; index++) {
-                const argument = args[index];
-                const base = argv + 16 * index;
-                write(argument, base, base + 4, base + 8, false, memory);
-            }
-            let output;
-            // This ref is released by the swjs_call_host_function implementation
-            const callback_func_ref = memory.retain((result) => {
-                output = result;
-            });
-            const alreadyReleased = this.exports.swjs_call_host_function(host_func_id, argv, argc, callback_func_ref);
-            if (alreadyReleased) {
-                throw new Error(`The JSClosure has been already released by Swift side. The closure is created at ${file}:${line}`);
-            }
-            this.exports.swjs_cleanup_host_function_call(argv);
-            return output;
-        }
-        get wasmImports() {
-            let broker = null;
-            const getMessageBroker = (threadChannel) => {
-                var _a;
-                if (broker)
-                    return broker;
-                const itcInterface = new ITCInterface(this.memory);
-                const newBroker = new MessageBroker((_a = this.tid) !== null && _a !== void 0 ? _a : -1, threadChannel, {
-                    onRequest: (message) => {
-                        let returnValue;
-                        try {
-                            // @ts-ignore
-                            const result = itcInterface[message.data.request.method](...message.data.request.parameters);
-                            returnValue = { ok: true, value: result };
-                        }
-                        catch (error) {
-                            returnValue = { ok: false, error: serializeError(error) };
-                        }
-                        const responseMessage = {
-                            type: "response",
-                            data: {
-                                sourceTid: message.data.sourceTid,
-                                context: message.data.context,
-                                response: returnValue,
-                            },
-                        };
-                        try {
-                            newBroker.reply(responseMessage);
-                        }
-                        catch (error) {
-                            responseMessage.data.response = {
-                                ok: false,
-                                error: serializeError(new TypeError(`Failed to serialize message: ${error}`))
-                            };
-                            newBroker.reply(responseMessage);
-                        }
-                    },
-                    onResponse: (message) => {
-                        if (message.data.response.ok) {
-                            const object = this.memory.retain(message.data.response.value.object);
-                            this.exports.swjs_receive_response(object, message.data.context);
-                        }
-                        else {
-                            const error = deserializeError(message.data.response.error);
-                            const errorObject = this.memory.retain(error);
-                            this.exports.swjs_receive_error(errorObject, message.data.context);
-                        }
-                    }
-                });
-                broker = newBroker;
-                return newBroker;
-            };
-            return {
-                swjs_set_prop: (ref, name, kind, payload1, payload2) => {
-                    const memory = this.memory;
-                    const obj = memory.getObject(ref);
-                    const key = memory.getObject(name);
-                    const value = decode(kind, payload1, payload2, memory);
-                    obj[key] = value;
-                },
-                swjs_get_prop: (ref, name, payload1_ptr, payload2_ptr) => {
-                    const memory = this.memory;
-                    const obj = memory.getObject(ref);
-                    const key = memory.getObject(name);
-                    const result = obj[key];
-                    return writeAndReturnKindBits(result, payload1_ptr, payload2_ptr, false, memory);
-                },
-                swjs_set_subscript: (ref, index, kind, payload1, payload2) => {
-                    const memory = this.memory;
-                    const obj = memory.getObject(ref);
-                    const value = decode(kind, payload1, payload2, memory);
-                    obj[index] = value;
-                },
-                swjs_get_subscript: (ref, index, payload1_ptr, payload2_ptr) => {
-                    const obj = this.memory.getObject(ref);
-                    const result = obj[index];
-                    return writeAndReturnKindBits(result, payload1_ptr, payload2_ptr, false, this.memory);
-                },
-                swjs_encode_string: (ref, bytes_ptr_result) => {
-                    const memory = this.memory;
-                    const bytes = this.textEncoder.encode(memory.getObject(ref));
-                    const bytes_ptr = memory.retain(bytes);
-                    memory.writeUint32(bytes_ptr_result, bytes_ptr);
-                    return bytes.length;
-                },
-                swjs_decode_string: (
-                // NOTE: TextDecoder can't decode typed arrays backed by SharedArrayBuffer
-                this.options.sharedMemory == true
-                    ? ((bytes_ptr, length) => {
-                        const memory = this.memory;
-                        const bytes = memory
-                            .bytes()
-                            .slice(bytes_ptr, bytes_ptr + length);
-                        const string = this.textDecoder.decode(bytes);
-                        return memory.retain(string);
-                    })
-                    : ((bytes_ptr, length) => {
-                        const memory = this.memory;
-                        const bytes = memory
-                            .bytes()
-                            .subarray(bytes_ptr, bytes_ptr + length);
-                        const string = this.textDecoder.decode(bytes);
-                        return memory.retain(string);
-                    })),
-                swjs_load_string: (ref, buffer) => {
-                    const memory = this.memory;
-                    const bytes = memory.getObject(ref);
-                    memory.writeBytes(buffer, bytes);
-                },
-                swjs_call_function: (ref, argv, argc, payload1_ptr, payload2_ptr) => {
-                    const memory = this.memory;
-                    const func = memory.getObject(ref);
-                    let result = undefined;
-                    try {
-                        const args = decodeArray(argv, argc, memory);
-                        result = func(...args);
-                    }
-                    catch (error) {
-                        return writeAndReturnKindBits(error, payload1_ptr, payload2_ptr, true, this.memory);
-                    }
-                    return writeAndReturnKindBits(result, payload1_ptr, payload2_ptr, false, this.memory);
-                },
-                swjs_call_function_no_catch: (ref, argv, argc, payload1_ptr, payload2_ptr) => {
-                    const memory = this.memory;
-                    const func = memory.getObject(ref);
-                    const args = decodeArray(argv, argc, memory);
-                    const result = func(...args);
-                    return writeAndReturnKindBits(result, payload1_ptr, payload2_ptr, false, this.memory);
-                },
-                swjs_call_function_with_this: (obj_ref, func_ref, argv, argc, payload1_ptr, payload2_ptr) => {
-                    const memory = this.memory;
-                    const obj = memory.getObject(obj_ref);
-                    const func = memory.getObject(func_ref);
-                    let result;
-                    try {
-                        const args = decodeArray(argv, argc, memory);
-                        result = func.apply(obj, args);
-                    }
-                    catch (error) {
-                        return writeAndReturnKindBits(error, payload1_ptr, payload2_ptr, true, this.memory);
-                    }
-                    return writeAndReturnKindBits(result, payload1_ptr, payload2_ptr, false, this.memory);
-                },
-                swjs_call_function_with_this_no_catch: (obj_ref, func_ref, argv, argc, payload1_ptr, payload2_ptr) => {
-                    const memory = this.memory;
-                    const obj = memory.getObject(obj_ref);
-                    const func = memory.getObject(func_ref);
-                    let result = undefined;
-                    const args = decodeArray(argv, argc, memory);
-                    result = func.apply(obj, args);
-                    return writeAndReturnKindBits(result, payload1_ptr, payload2_ptr, false, this.memory);
-                },
-                swjs_call_new: (ref, argv, argc) => {
-                    const memory = this.memory;
-                    const constructor = memory.getObject(ref);
-                    const args = decodeArray(argv, argc, memory);
-                    const instance = new constructor(...args);
-                    return this.memory.retain(instance);
-                },
-                swjs_call_throwing_new: (ref, argv, argc, exception_kind_ptr, exception_payload1_ptr, exception_payload2_ptr) => {
-                    let memory = this.memory;
-                    const constructor = memory.getObject(ref);
-                    let result;
-                    try {
-                        const args = decodeArray(argv, argc, memory);
-                        result = new constructor(...args);
-                    }
-                    catch (error) {
-                        write(error, exception_kind_ptr, exception_payload1_ptr, exception_payload2_ptr, true, this.memory);
-                        return -1;
-                    }
-                    memory = this.memory;
-                    write(null, exception_kind_ptr, exception_payload1_ptr, exception_payload2_ptr, false, memory);
-                    return memory.retain(result);
-                },
-                swjs_instanceof: (obj_ref, constructor_ref) => {
-                    const memory = this.memory;
-                    const obj = memory.getObject(obj_ref);
-                    const constructor = memory.getObject(constructor_ref);
-                    return obj instanceof constructor;
-                },
-                swjs_create_function: (host_func_id, line, file) => {
-                    var _a;
-                    const fileString = this.memory.getObject(file);
-                    const func = (...args) => this.callHostFunction(host_func_id, line, fileString, args);
-                    const func_ref = this.memory.retain(func);
-                    (_a = this.closureDeallocator) === null || _a === void 0 ? void 0 : _a.track(func, func_ref);
-                    return func_ref;
-                },
-                swjs_create_typed_array: (constructor_ref, elementsPtr, length) => {
-                    const ArrayType = this.memory.getObject(constructor_ref);
-                    if (length == 0) {
-                        // The elementsPtr can be unaligned in Swift's Array
-                        // implementation when the array is empty. However,
-                        // TypedArray requires the pointer to be aligned.
-                        // So, we need to create a new empty array without
-                        // using the elementsPtr.
-                        // See https://github.com/swiftwasm/swift/issues/5599
-                        return this.memory.retain(new ArrayType());
-                    }
-                    const array = new ArrayType(this.memory.rawMemory.buffer, elementsPtr, length);
-                    // Call `.slice()` to copy the memory
-                    return this.memory.retain(array.slice());
-                },
-                swjs_load_typed_array: (ref, buffer) => {
-                    const memory = this.memory;
-                    const typedArray = memory.getObject(ref);
-                    const bytes = new Uint8Array(typedArray.buffer);
-                    memory.writeBytes(buffer, bytes);
-                },
-                swjs_release: (ref) => {
-                    this.memory.release(ref);
-                },
-                swjs_release_remote: (tid, ref) => {
-                    var _a;
-                    if (!this.options.threadChannel) {
-                        throw new Error("threadChannel is not set in options given to SwiftRuntime. Please set it to release objects on remote threads.");
-                    }
-                    const broker = getMessageBroker(this.options.threadChannel);
-                    broker.request({
-                        type: "request",
-                        data: {
-                            sourceTid: (_a = this.tid) !== null && _a !== void 0 ? _a : MAIN_THREAD_TID,
-                            targetTid: tid,
-                            context: 0,
-                            request: {
-                                method: "release",
-                                parameters: [ref],
-                            }
-                        }
-                    });
-                },
-                swjs_i64_to_bigint: (value, signed) => {
-                    return this.memory.retain(signed ? value : BigInt.asUintN(64, value));
-                },
-                swjs_bigint_to_i64: (ref, signed) => {
-                    const object = this.memory.getObject(ref);
-                    if (typeof object !== "bigint") {
-                        throw new Error(`Expected a BigInt, but got ${typeof object}`);
-                    }
-                    if (signed) {
-                        return object;
-                    }
-                    else {
-                        if (object < BigInt(0)) {
-                            return BigInt(0);
-                        }
-                        return BigInt.asIntN(64, object);
-                    }
-                },
-                swjs_i64_to_bigint_slow: (lower, upper, signed) => {
-                    const value = BigInt.asUintN(32, BigInt(lower)) +
-                        (BigInt.asUintN(32, BigInt(upper)) << BigInt(32));
-                    return this.memory.retain(signed ? BigInt.asIntN(64, value) : BigInt.asUintN(64, value));
-                },
-                swjs_unsafe_event_loop_yield: () => {
-                    throw new UnsafeEventLoopYield();
-                },
-                swjs_send_job_to_main_thread: (unowned_job) => {
-                    this.postMessageToMainThread({ type: "job", data: unowned_job });
-                },
-                swjs_listen_message_from_main_thread: () => {
-                    const threadChannel = this.options.threadChannel;
-                    if (!(threadChannel && "listenMessageFromMainThread" in threadChannel)) {
-                        throw new Error("listenMessageFromMainThread is not set in options given to SwiftRuntime. Please set it to listen to wake events from the main thread.");
-                    }
-                    const broker = getMessageBroker(threadChannel);
-                    threadChannel.listenMessageFromMainThread((message) => {
-                        switch (message.type) {
-                            case "wake":
-                                this.exports.swjs_wake_worker_thread();
-                                break;
-                            case "request": {
-                                broker.onReceivingRequest(message);
-                                break;
-                            }
-                            case "response": {
-                                broker.onReceivingResponse(message);
-                                break;
-                            }
-                            default:
-                                const unknownMessage = message;
-                                throw new Error(`Unknown message type: ${unknownMessage}`);
-                        }
-                    });
-                },
-                swjs_wake_up_worker_thread: (tid) => {
-                    this.postMessageToWorkerThread(tid, { type: "wake" });
-                },
-                swjs_listen_message_from_worker_thread: (tid) => {
-                    const threadChannel = this.options.threadChannel;
-                    if (!(threadChannel && "listenMessageFromWorkerThread" in threadChannel)) {
-                        throw new Error("listenMessageFromWorkerThread is not set in options given to SwiftRuntime. Please set it to listen to jobs from worker threads.");
-                    }
-                    const broker = getMessageBroker(threadChannel);
-                    threadChannel.listenMessageFromWorkerThread(tid, (message) => {
-                        switch (message.type) {
-                            case "job":
-                                this.exports.swjs_enqueue_main_job_from_worker(message.data);
-                                break;
-                            case "request": {
-                                broker.onReceivingRequest(message);
-                                break;
-                            }
-                            case "response": {
-                                broker.onReceivingResponse(message);
-                                break;
-                            }
-                            default:
-                                const unknownMessage = message;
-                                throw new Error(`Unknown message type: ${unknownMessage}`);
-                        }
-                    });
-                },
-                swjs_terminate_worker_thread: (tid) => {
-                    var _a;
-                    const threadChannel = this.options.threadChannel;
-                    if (threadChannel && "terminateWorkerThread" in threadChannel) {
-                        (_a = threadChannel.terminateWorkerThread) === null || _a === void 0 ? void 0 : _a.call(threadChannel, tid);
-                    } // Otherwise, just ignore the termination request
-                },
-                swjs_get_worker_thread_id: () => {
-                    // Main thread's tid is always -1
-                    return this.tid || -1;
-                },
-                swjs_request_sending_object: (sending_object, transferring_objects, transferring_objects_count, object_source_tid, sending_context) => {
-                    var _a;
-                    if (!this.options.threadChannel) {
-                        throw new Error("threadChannel is not set in options given to SwiftRuntime. Please set it to request transferring objects.");
-                    }
-                    const broker = getMessageBroker(this.options.threadChannel);
-                    const memory = this.memory;
-                    const transferringObjects = decodeObjectRefs(transferring_objects, transferring_objects_count, memory);
-                    broker.request({
-                        type: "request",
-                        data: {
-                            sourceTid: (_a = this.tid) !== null && _a !== void 0 ? _a : MAIN_THREAD_TID,
-                            targetTid: object_source_tid,
-                            context: sending_context,
-                            request: {
-                                method: "send",
-                                parameters: [sending_object, transferringObjects, sending_context],
-                            }
-                        }
-                    });
-                },
-                swjs_request_sending_objects: (sending_objects, sending_objects_count, transferring_objects, transferring_objects_count, object_source_tid, sending_context) => {
-                    var _a;
-                    if (!this.options.threadChannel) {
-                        throw new Error("threadChannel is not set in options given to SwiftRuntime. Please set it to request transferring objects.");
-                    }
-                    const broker = getMessageBroker(this.options.threadChannel);
-                    const memory = this.memory;
-                    const sendingObjects = decodeObjectRefs(sending_objects, sending_objects_count, memory);
-                    const transferringObjects = decodeObjectRefs(transferring_objects, transferring_objects_count, memory);
-                    broker.request({
-                        type: "request",
-                        data: {
-                            sourceTid: (_a = this.tid) !== null && _a !== void 0 ? _a : MAIN_THREAD_TID,
-                            targetTid: object_source_tid,
-                            context: sending_context,
-                            request: {
-                                method: "sendObjects",
-                                parameters: [sendingObjects, transferringObjects, sending_context],
-                            }
-                        }
-                    });
-                },
-            };
-        }
-        postMessageToMainThread(message, transfer = []) {
-            const threadChannel = this.options.threadChannel;
-            if (!(threadChannel && "postMessageToMainThread" in threadChannel)) {
-                throw new Error("postMessageToMainThread is not set in options given to SwiftRuntime. Please set it to send messages to the main thread.");
-            }
-            threadChannel.postMessageToMainThread(message, transfer);
-        }
-        postMessageToWorkerThread(tid, message, transfer = []) {
-            const threadChannel = this.options.threadChannel;
-            if (!(threadChannel && "postMessageToWorkerThread" in threadChannel)) {
-                throw new Error("postMessageToWorkerThread is not set in options given to SwiftRuntime. Please set it to send messages to worker threads.");
-            }
-            threadChannel.postMessageToWorkerThread(tid, message, transfer);
-        }
-    }
-    /// This error is thrown when yielding event loop control from `swift_task_asyncMainDrainQueue`
-    /// to JavaScript. This is usually thrown when:
-    /// - The entry point of the Swift program is `func main() async`
-    /// - The Swift Concurrency's global executor is hooked by `JavaScriptEventLoop.installGlobalExecutor()`
-    /// - Calling exported `main` or `__main_argc_argv` function from JavaScript
-    ///
-    /// This exception must be caught by the caller of the exported function and the caller should
-    /// catch this exception and just ignore it.
-    ///
-    /// FAQ: Why this error is thrown?
-    /// This error is thrown to unwind the call stack of the Swift program and return the control to
-    /// the JavaScript side. Otherwise, the `swift_task_asyncMainDrainQueue` ends up with `abort()`
-    /// because the event loop expects `exit()` call before the end of the event loop.
-    class UnsafeEventLoopYield extends Error {
-    }
-
-    exports.SwiftRuntime = SwiftRuntime;
-
-    Object.defineProperty(exports, '__esModule', { value: true });
-
-}));
diff --git a/Sources/JavaScriptKit/Runtime/index.mjs b/Sources/JavaScriptKit/Runtime/index.mjs
deleted file mode 100644
index 668368203..000000000
--- a/Sources/JavaScriptKit/Runtime/index.mjs
+++ /dev/null
@@ -1,821 +0,0 @@
-/// Memory lifetime of closures in Swift are managed by Swift side
-class SwiftClosureDeallocator {
-    constructor(exports) {
-        if (typeof FinalizationRegistry === "undefined") {
-            throw new Error("The Swift part of JavaScriptKit was configured to require " +
-                "the availability of JavaScript WeakRefs. Please build " +
-                "with `-Xswiftc -DJAVASCRIPTKIT_WITHOUT_WEAKREFS` to " +
-                "disable features that use WeakRefs.");
-        }
-        this.functionRegistry = new FinalizationRegistry((id) => {
-            exports.swjs_free_host_function(id);
-        });
-    }
-    track(func, func_ref) {
-        this.functionRegistry.register(func, func_ref);
-    }
-}
-
-function assertNever(x, message) {
-    throw new Error(message);
-}
-const MAIN_THREAD_TID = -1;
-
-const decode = (kind, payload1, payload2, memory) => {
-    switch (kind) {
-        case 0 /* Boolean */:
-            switch (payload1) {
-                case 0:
-                    return false;
-                case 1:
-                    return true;
-            }
-        case 2 /* Number */:
-            return payload2;
-        case 1 /* String */:
-        case 3 /* Object */:
-        case 6 /* Function */:
-        case 7 /* Symbol */:
-        case 8 /* BigInt */:
-            return memory.getObject(payload1);
-        case 4 /* Null */:
-            return null;
-        case 5 /* Undefined */:
-            return undefined;
-        default:
-            assertNever(kind, `JSValue Type kind "${kind}" is not supported`);
-    }
-};
-// Note:
-// `decodeValues` assumes that the size of RawJSValue is 16.
-const decodeArray = (ptr, length, memory) => {
-    // fast path for empty array
-    if (length === 0) {
-        return [];
-    }
-    let result = [];
-    // It's safe to hold DataView here because WebAssembly.Memory.buffer won't
-    // change within this function.
-    const view = memory.dataView();
-    for (let index = 0; index < length; index++) {
-        const base = ptr + 16 * index;
-        const kind = view.getUint32(base, true);
-        const payload1 = view.getUint32(base + 4, true);
-        const payload2 = view.getFloat64(base + 8, true);
-        result.push(decode(kind, payload1, payload2, memory));
-    }
-    return result;
-};
-// A helper function to encode a RawJSValue into a pointers.
-// Please prefer to use `writeAndReturnKindBits` to avoid unnecessary
-// memory stores.
-// This function should be used only when kind flag is stored in memory.
-const write = (value, kind_ptr, payload1_ptr, payload2_ptr, is_exception, memory) => {
-    const kind = writeAndReturnKindBits(value, payload1_ptr, payload2_ptr, is_exception, memory);
-    memory.writeUint32(kind_ptr, kind);
-};
-const writeAndReturnKindBits = (value, payload1_ptr, payload2_ptr, is_exception, memory) => {
-    const exceptionBit = (is_exception ? 1 : 0) << 31;
-    if (value === null) {
-        return exceptionBit | 4 /* Null */;
-    }
-    const writeRef = (kind) => {
-        memory.writeUint32(payload1_ptr, memory.retain(value));
-        return exceptionBit | kind;
-    };
-    const type = typeof value;
-    switch (type) {
-        case "boolean": {
-            memory.writeUint32(payload1_ptr, value ? 1 : 0);
-            return exceptionBit | 0 /* Boolean */;
-        }
-        case "number": {
-            memory.writeFloat64(payload2_ptr, value);
-            return exceptionBit | 2 /* Number */;
-        }
-        case "string": {
-            return writeRef(1 /* String */);
-        }
-        case "undefined": {
-            return exceptionBit | 5 /* Undefined */;
-        }
-        case "object": {
-            return writeRef(3 /* Object */);
-        }
-        case "function": {
-            return writeRef(6 /* Function */);
-        }
-        case "symbol": {
-            return writeRef(7 /* Symbol */);
-        }
-        case "bigint": {
-            return writeRef(8 /* BigInt */);
-        }
-        default:
-            assertNever(type, `Type "${type}" is not supported yet`);
-    }
-    throw new Error("Unreachable");
-};
-function decodeObjectRefs(ptr, length, memory) {
-    const result = new Array(length);
-    for (let i = 0; i < length; i++) {
-        result[i] = memory.readUint32(ptr + 4 * i);
-    }
-    return result;
-}
-
-let globalVariable;
-if (typeof globalThis !== "undefined") {
-    globalVariable = globalThis;
-}
-else if (typeof window !== "undefined") {
-    globalVariable = window;
-}
-else if (typeof global !== "undefined") {
-    globalVariable = global;
-}
-else if (typeof self !== "undefined") {
-    globalVariable = self;
-}
-
-class SwiftRuntimeHeap {
-    constructor() {
-        this._heapValueById = new Map();
-        this._heapValueById.set(0, globalVariable);
-        this._heapEntryByValue = new Map();
-        this._heapEntryByValue.set(globalVariable, { id: 0, rc: 1 });
-        // Note: 0 is preserved for global
-        this._heapNextKey = 1;
-    }
-    retain(value) {
-        const entry = this._heapEntryByValue.get(value);
-        if (entry) {
-            entry.rc++;
-            return entry.id;
-        }
-        const id = this._heapNextKey++;
-        this._heapValueById.set(id, value);
-        this._heapEntryByValue.set(value, { id: id, rc: 1 });
-        return id;
-    }
-    release(ref) {
-        const value = this._heapValueById.get(ref);
-        const entry = this._heapEntryByValue.get(value);
-        entry.rc--;
-        if (entry.rc != 0)
-            return;
-        this._heapEntryByValue.delete(value);
-        this._heapValueById.delete(ref);
-    }
-    referenceHeap(ref) {
-        const value = this._heapValueById.get(ref);
-        if (value === undefined) {
-            throw new ReferenceError("Attempted to read invalid reference " + ref);
-        }
-        return value;
-    }
-}
-
-class Memory {
-    constructor(exports) {
-        this.heap = new SwiftRuntimeHeap();
-        this.retain = (value) => this.heap.retain(value);
-        this.getObject = (ref) => this.heap.referenceHeap(ref);
-        this.release = (ref) => this.heap.release(ref);
-        this.bytes = () => new Uint8Array(this.rawMemory.buffer);
-        this.dataView = () => new DataView(this.rawMemory.buffer);
-        this.writeBytes = (ptr, bytes) => this.bytes().set(bytes, ptr);
-        this.readUint32 = (ptr) => this.dataView().getUint32(ptr, true);
-        this.readUint64 = (ptr) => this.dataView().getBigUint64(ptr, true);
-        this.readInt64 = (ptr) => this.dataView().getBigInt64(ptr, true);
-        this.readFloat64 = (ptr) => this.dataView().getFloat64(ptr, true);
-        this.writeUint32 = (ptr, value) => this.dataView().setUint32(ptr, value, true);
-        this.writeUint64 = (ptr, value) => this.dataView().setBigUint64(ptr, value, true);
-        this.writeInt64 = (ptr, value) => this.dataView().setBigInt64(ptr, value, true);
-        this.writeFloat64 = (ptr, value) => this.dataView().setFloat64(ptr, value, true);
-        this.rawMemory = exports.memory;
-    }
-}
-
-class ITCInterface {
-    constructor(memory) {
-        this.memory = memory;
-    }
-    send(sendingObject, transferringObjects, sendingContext) {
-        const object = this.memory.getObject(sendingObject);
-        const transfer = transferringObjects.map(ref => this.memory.getObject(ref));
-        return { object, sendingContext, transfer };
-    }
-    sendObjects(sendingObjects, transferringObjects, sendingContext) {
-        const objects = sendingObjects.map(ref => this.memory.getObject(ref));
-        const transfer = transferringObjects.map(ref => this.memory.getObject(ref));
-        return { object: objects, sendingContext, transfer };
-    }
-    release(objectRef) {
-        this.memory.release(objectRef);
-        return { object: undefined, transfer: [] };
-    }
-}
-class MessageBroker {
-    constructor(selfTid, threadChannel, handlers) {
-        this.selfTid = selfTid;
-        this.threadChannel = threadChannel;
-        this.handlers = handlers;
-    }
-    request(message) {
-        if (message.data.targetTid == this.selfTid) {
-            // The request is for the current thread
-            this.handlers.onRequest(message);
-        }
-        else if ("postMessageToWorkerThread" in this.threadChannel) {
-            // The request is for another worker thread sent from the main thread
-            this.threadChannel.postMessageToWorkerThread(message.data.targetTid, message, []);
-        }
-        else if ("postMessageToMainThread" in this.threadChannel) {
-            // The request is for other worker threads or the main thread sent from a worker thread
-            this.threadChannel.postMessageToMainThread(message, []);
-        }
-        else {
-            throw new Error("unreachable");
-        }
-    }
-    reply(message) {
-        if (message.data.sourceTid == this.selfTid) {
-            // The response is for the current thread
-            this.handlers.onResponse(message);
-            return;
-        }
-        const transfer = message.data.response.ok ? message.data.response.value.transfer : [];
-        if ("postMessageToWorkerThread" in this.threadChannel) {
-            // The response is for another worker thread sent from the main thread
-            this.threadChannel.postMessageToWorkerThread(message.data.sourceTid, message, transfer);
-        }
-        else if ("postMessageToMainThread" in this.threadChannel) {
-            // The response is for other worker threads or the main thread sent from a worker thread
-            this.threadChannel.postMessageToMainThread(message, transfer);
-        }
-        else {
-            throw new Error("unreachable");
-        }
-    }
-    onReceivingRequest(message) {
-        if (message.data.targetTid == this.selfTid) {
-            this.handlers.onRequest(message);
-        }
-        else if ("postMessageToWorkerThread" in this.threadChannel) {
-            // Receive a request from a worker thread to other worker on main thread. 
-            // Proxy the request to the target worker thread.
-            this.threadChannel.postMessageToWorkerThread(message.data.targetTid, message, []);
-        }
-        else if ("postMessageToMainThread" in this.threadChannel) {
-            // A worker thread won't receive a request for other worker threads
-            throw new Error("unreachable");
-        }
-    }
-    onReceivingResponse(message) {
-        if (message.data.sourceTid == this.selfTid) {
-            this.handlers.onResponse(message);
-        }
-        else if ("postMessageToWorkerThread" in this.threadChannel) {
-            // Receive a response from a worker thread to other worker on main thread.
-            // Proxy the response to the target worker thread.
-            const transfer = message.data.response.ok ? message.data.response.value.transfer : [];
-            this.threadChannel.postMessageToWorkerThread(message.data.sourceTid, message, transfer);
-        }
-        else if ("postMessageToMainThread" in this.threadChannel) {
-            // A worker thread won't receive a response for other worker threads
-            throw new Error("unreachable");
-        }
-    }
-}
-function serializeError(error) {
-    if (error instanceof Error) {
-        return { isError: true, value: { message: error.message, name: error.name, stack: error.stack } };
-    }
-    return { isError: false, value: error };
-}
-function deserializeError(error) {
-    if (error.isError) {
-        return Object.assign(new Error(error.value.message), error.value);
-    }
-    return error.value;
-}
-
-class SwiftRuntime {
-    constructor(options) {
-        this.version = 708;
-        this.textDecoder = new TextDecoder("utf-8");
-        this.textEncoder = new TextEncoder(); // Only support utf-8
-        /** @deprecated Use `wasmImports` instead */
-        this.importObjects = () => this.wasmImports;
-        this._instance = null;
-        this._memory = null;
-        this._closureDeallocator = null;
-        this.tid = null;
-        this.options = options || {};
-    }
-    setInstance(instance) {
-        this._instance = instance;
-        if (typeof this.exports._start === "function") {
-            throw new Error(`JavaScriptKit supports only WASI reactor ABI.
-                Please make sure you are building with:
-                -Xswiftc -Xclang-linker -Xswiftc -mexec-model=reactor
-                `);
-        }
-        if (this.exports.swjs_library_version() != this.version) {
-            throw new Error(`The versions of JavaScriptKit are incompatible.
-                WebAssembly runtime ${this.exports.swjs_library_version()} != JS runtime ${this.version}`);
-        }
-    }
-    main() {
-        const instance = this.instance;
-        try {
-            if (typeof instance.exports.main === "function") {
-                instance.exports.main();
-            }
-            else if (typeof instance.exports.__main_argc_argv === "function") {
-                // Swift 6.0 and later use `__main_argc_argv` instead of `main`.
-                instance.exports.__main_argc_argv(0, 0);
-            }
-        }
-        catch (error) {
-            if (error instanceof UnsafeEventLoopYield) {
-                // Ignore the error
-                return;
-            }
-            // Rethrow other errors
-            throw error;
-        }
-    }
-    /**
-     * Start a new thread with the given `tid` and `startArg`, which
-     * is forwarded to the `wasi_thread_start` function.
-     * This function is expected to be called from the spawned Web Worker thread.
-     */
-    startThread(tid, startArg) {
-        this.tid = tid;
-        const instance = this.instance;
-        try {
-            if (typeof instance.exports.wasi_thread_start === "function") {
-                instance.exports.wasi_thread_start(tid, startArg);
-            }
-            else {
-                throw new Error(`The WebAssembly module is not built for wasm32-unknown-wasip1-threads target.`);
-            }
-        }
-        catch (error) {
-            if (error instanceof UnsafeEventLoopYield) {
-                // Ignore the error
-                return;
-            }
-            // Rethrow other errors
-            throw error;
-        }
-    }
-    get instance() {
-        if (!this._instance)
-            throw new Error("WebAssembly instance is not set yet");
-        return this._instance;
-    }
-    get exports() {
-        return this.instance.exports;
-    }
-    get memory() {
-        if (!this._memory) {
-            this._memory = new Memory(this.instance.exports);
-        }
-        return this._memory;
-    }
-    get closureDeallocator() {
-        if (this._closureDeallocator)
-            return this._closureDeallocator;
-        const features = this.exports.swjs_library_features();
-        const librarySupportsWeakRef = (features & 1 /* WeakRefs */) != 0;
-        if (librarySupportsWeakRef) {
-            this._closureDeallocator = new SwiftClosureDeallocator(this.exports);
-        }
-        return this._closureDeallocator;
-    }
-    callHostFunction(host_func_id, line, file, args) {
-        const argc = args.length;
-        const argv = this.exports.swjs_prepare_host_function_call(argc);
-        const memory = this.memory;
-        for (let index = 0; index < args.length; index++) {
-            const argument = args[index];
-            const base = argv + 16 * index;
-            write(argument, base, base + 4, base + 8, false, memory);
-        }
-        let output;
-        // This ref is released by the swjs_call_host_function implementation
-        const callback_func_ref = memory.retain((result) => {
-            output = result;
-        });
-        const alreadyReleased = this.exports.swjs_call_host_function(host_func_id, argv, argc, callback_func_ref);
-        if (alreadyReleased) {
-            throw new Error(`The JSClosure has been already released by Swift side. The closure is created at ${file}:${line}`);
-        }
-        this.exports.swjs_cleanup_host_function_call(argv);
-        return output;
-    }
-    get wasmImports() {
-        let broker = null;
-        const getMessageBroker = (threadChannel) => {
-            var _a;
-            if (broker)
-                return broker;
-            const itcInterface = new ITCInterface(this.memory);
-            const newBroker = new MessageBroker((_a = this.tid) !== null && _a !== void 0 ? _a : -1, threadChannel, {
-                onRequest: (message) => {
-                    let returnValue;
-                    try {
-                        // @ts-ignore
-                        const result = itcInterface[message.data.request.method](...message.data.request.parameters);
-                        returnValue = { ok: true, value: result };
-                    }
-                    catch (error) {
-                        returnValue = { ok: false, error: serializeError(error) };
-                    }
-                    const responseMessage = {
-                        type: "response",
-                        data: {
-                            sourceTid: message.data.sourceTid,
-                            context: message.data.context,
-                            response: returnValue,
-                        },
-                    };
-                    try {
-                        newBroker.reply(responseMessage);
-                    }
-                    catch (error) {
-                        responseMessage.data.response = {
-                            ok: false,
-                            error: serializeError(new TypeError(`Failed to serialize message: ${error}`))
-                        };
-                        newBroker.reply(responseMessage);
-                    }
-                },
-                onResponse: (message) => {
-                    if (message.data.response.ok) {
-                        const object = this.memory.retain(message.data.response.value.object);
-                        this.exports.swjs_receive_response(object, message.data.context);
-                    }
-                    else {
-                        const error = deserializeError(message.data.response.error);
-                        const errorObject = this.memory.retain(error);
-                        this.exports.swjs_receive_error(errorObject, message.data.context);
-                    }
-                }
-            });
-            broker = newBroker;
-            return newBroker;
-        };
-        return {
-            swjs_set_prop: (ref, name, kind, payload1, payload2) => {
-                const memory = this.memory;
-                const obj = memory.getObject(ref);
-                const key = memory.getObject(name);
-                const value = decode(kind, payload1, payload2, memory);
-                obj[key] = value;
-            },
-            swjs_get_prop: (ref, name, payload1_ptr, payload2_ptr) => {
-                const memory = this.memory;
-                const obj = memory.getObject(ref);
-                const key = memory.getObject(name);
-                const result = obj[key];
-                return writeAndReturnKindBits(result, payload1_ptr, payload2_ptr, false, memory);
-            },
-            swjs_set_subscript: (ref, index, kind, payload1, payload2) => {
-                const memory = this.memory;
-                const obj = memory.getObject(ref);
-                const value = decode(kind, payload1, payload2, memory);
-                obj[index] = value;
-            },
-            swjs_get_subscript: (ref, index, payload1_ptr, payload2_ptr) => {
-                const obj = this.memory.getObject(ref);
-                const result = obj[index];
-                return writeAndReturnKindBits(result, payload1_ptr, payload2_ptr, false, this.memory);
-            },
-            swjs_encode_string: (ref, bytes_ptr_result) => {
-                const memory = this.memory;
-                const bytes = this.textEncoder.encode(memory.getObject(ref));
-                const bytes_ptr = memory.retain(bytes);
-                memory.writeUint32(bytes_ptr_result, bytes_ptr);
-                return bytes.length;
-            },
-            swjs_decode_string: (
-            // NOTE: TextDecoder can't decode typed arrays backed by SharedArrayBuffer
-            this.options.sharedMemory == true
-                ? ((bytes_ptr, length) => {
-                    const memory = this.memory;
-                    const bytes = memory
-                        .bytes()
-                        .slice(bytes_ptr, bytes_ptr + length);
-                    const string = this.textDecoder.decode(bytes);
-                    return memory.retain(string);
-                })
-                : ((bytes_ptr, length) => {
-                    const memory = this.memory;
-                    const bytes = memory
-                        .bytes()
-                        .subarray(bytes_ptr, bytes_ptr + length);
-                    const string = this.textDecoder.decode(bytes);
-                    return memory.retain(string);
-                })),
-            swjs_load_string: (ref, buffer) => {
-                const memory = this.memory;
-                const bytes = memory.getObject(ref);
-                memory.writeBytes(buffer, bytes);
-            },
-            swjs_call_function: (ref, argv, argc, payload1_ptr, payload2_ptr) => {
-                const memory = this.memory;
-                const func = memory.getObject(ref);
-                let result = undefined;
-                try {
-                    const args = decodeArray(argv, argc, memory);
-                    result = func(...args);
-                }
-                catch (error) {
-                    return writeAndReturnKindBits(error, payload1_ptr, payload2_ptr, true, this.memory);
-                }
-                return writeAndReturnKindBits(result, payload1_ptr, payload2_ptr, false, this.memory);
-            },
-            swjs_call_function_no_catch: (ref, argv, argc, payload1_ptr, payload2_ptr) => {
-                const memory = this.memory;
-                const func = memory.getObject(ref);
-                const args = decodeArray(argv, argc, memory);
-                const result = func(...args);
-                return writeAndReturnKindBits(result, payload1_ptr, payload2_ptr, false, this.memory);
-            },
-            swjs_call_function_with_this: (obj_ref, func_ref, argv, argc, payload1_ptr, payload2_ptr) => {
-                const memory = this.memory;
-                const obj = memory.getObject(obj_ref);
-                const func = memory.getObject(func_ref);
-                let result;
-                try {
-                    const args = decodeArray(argv, argc, memory);
-                    result = func.apply(obj, args);
-                }
-                catch (error) {
-                    return writeAndReturnKindBits(error, payload1_ptr, payload2_ptr, true, this.memory);
-                }
-                return writeAndReturnKindBits(result, payload1_ptr, payload2_ptr, false, this.memory);
-            },
-            swjs_call_function_with_this_no_catch: (obj_ref, func_ref, argv, argc, payload1_ptr, payload2_ptr) => {
-                const memory = this.memory;
-                const obj = memory.getObject(obj_ref);
-                const func = memory.getObject(func_ref);
-                let result = undefined;
-                const args = decodeArray(argv, argc, memory);
-                result = func.apply(obj, args);
-                return writeAndReturnKindBits(result, payload1_ptr, payload2_ptr, false, this.memory);
-            },
-            swjs_call_new: (ref, argv, argc) => {
-                const memory = this.memory;
-                const constructor = memory.getObject(ref);
-                const args = decodeArray(argv, argc, memory);
-                const instance = new constructor(...args);
-                return this.memory.retain(instance);
-            },
-            swjs_call_throwing_new: (ref, argv, argc, exception_kind_ptr, exception_payload1_ptr, exception_payload2_ptr) => {
-                let memory = this.memory;
-                const constructor = memory.getObject(ref);
-                let result;
-                try {
-                    const args = decodeArray(argv, argc, memory);
-                    result = new constructor(...args);
-                }
-                catch (error) {
-                    write(error, exception_kind_ptr, exception_payload1_ptr, exception_payload2_ptr, true, this.memory);
-                    return -1;
-                }
-                memory = this.memory;
-                write(null, exception_kind_ptr, exception_payload1_ptr, exception_payload2_ptr, false, memory);
-                return memory.retain(result);
-            },
-            swjs_instanceof: (obj_ref, constructor_ref) => {
-                const memory = this.memory;
-                const obj = memory.getObject(obj_ref);
-                const constructor = memory.getObject(constructor_ref);
-                return obj instanceof constructor;
-            },
-            swjs_create_function: (host_func_id, line, file) => {
-                var _a;
-                const fileString = this.memory.getObject(file);
-                const func = (...args) => this.callHostFunction(host_func_id, line, fileString, args);
-                const func_ref = this.memory.retain(func);
-                (_a = this.closureDeallocator) === null || _a === void 0 ? void 0 : _a.track(func, func_ref);
-                return func_ref;
-            },
-            swjs_create_typed_array: (constructor_ref, elementsPtr, length) => {
-                const ArrayType = this.memory.getObject(constructor_ref);
-                if (length == 0) {
-                    // The elementsPtr can be unaligned in Swift's Array
-                    // implementation when the array is empty. However,
-                    // TypedArray requires the pointer to be aligned.
-                    // So, we need to create a new empty array without
-                    // using the elementsPtr.
-                    // See https://github.com/swiftwasm/swift/issues/5599
-                    return this.memory.retain(new ArrayType());
-                }
-                const array = new ArrayType(this.memory.rawMemory.buffer, elementsPtr, length);
-                // Call `.slice()` to copy the memory
-                return this.memory.retain(array.slice());
-            },
-            swjs_load_typed_array: (ref, buffer) => {
-                const memory = this.memory;
-                const typedArray = memory.getObject(ref);
-                const bytes = new Uint8Array(typedArray.buffer);
-                memory.writeBytes(buffer, bytes);
-            },
-            swjs_release: (ref) => {
-                this.memory.release(ref);
-            },
-            swjs_release_remote: (tid, ref) => {
-                var _a;
-                if (!this.options.threadChannel) {
-                    throw new Error("threadChannel is not set in options given to SwiftRuntime. Please set it to release objects on remote threads.");
-                }
-                const broker = getMessageBroker(this.options.threadChannel);
-                broker.request({
-                    type: "request",
-                    data: {
-                        sourceTid: (_a = this.tid) !== null && _a !== void 0 ? _a : MAIN_THREAD_TID,
-                        targetTid: tid,
-                        context: 0,
-                        request: {
-                            method: "release",
-                            parameters: [ref],
-                        }
-                    }
-                });
-            },
-            swjs_i64_to_bigint: (value, signed) => {
-                return this.memory.retain(signed ? value : BigInt.asUintN(64, value));
-            },
-            swjs_bigint_to_i64: (ref, signed) => {
-                const object = this.memory.getObject(ref);
-                if (typeof object !== "bigint") {
-                    throw new Error(`Expected a BigInt, but got ${typeof object}`);
-                }
-                if (signed) {
-                    return object;
-                }
-                else {
-                    if (object < BigInt(0)) {
-                        return BigInt(0);
-                    }
-                    return BigInt.asIntN(64, object);
-                }
-            },
-            swjs_i64_to_bigint_slow: (lower, upper, signed) => {
-                const value = BigInt.asUintN(32, BigInt(lower)) +
-                    (BigInt.asUintN(32, BigInt(upper)) << BigInt(32));
-                return this.memory.retain(signed ? BigInt.asIntN(64, value) : BigInt.asUintN(64, value));
-            },
-            swjs_unsafe_event_loop_yield: () => {
-                throw new UnsafeEventLoopYield();
-            },
-            swjs_send_job_to_main_thread: (unowned_job) => {
-                this.postMessageToMainThread({ type: "job", data: unowned_job });
-            },
-            swjs_listen_message_from_main_thread: () => {
-                const threadChannel = this.options.threadChannel;
-                if (!(threadChannel && "listenMessageFromMainThread" in threadChannel)) {
-                    throw new Error("listenMessageFromMainThread is not set in options given to SwiftRuntime. Please set it to listen to wake events from the main thread.");
-                }
-                const broker = getMessageBroker(threadChannel);
-                threadChannel.listenMessageFromMainThread((message) => {
-                    switch (message.type) {
-                        case "wake":
-                            this.exports.swjs_wake_worker_thread();
-                            break;
-                        case "request": {
-                            broker.onReceivingRequest(message);
-                            break;
-                        }
-                        case "response": {
-                            broker.onReceivingResponse(message);
-                            break;
-                        }
-                        default:
-                            const unknownMessage = message;
-                            throw new Error(`Unknown message type: ${unknownMessage}`);
-                    }
-                });
-            },
-            swjs_wake_up_worker_thread: (tid) => {
-                this.postMessageToWorkerThread(tid, { type: "wake" });
-            },
-            swjs_listen_message_from_worker_thread: (tid) => {
-                const threadChannel = this.options.threadChannel;
-                if (!(threadChannel && "listenMessageFromWorkerThread" in threadChannel)) {
-                    throw new Error("listenMessageFromWorkerThread is not set in options given to SwiftRuntime. Please set it to listen to jobs from worker threads.");
-                }
-                const broker = getMessageBroker(threadChannel);
-                threadChannel.listenMessageFromWorkerThread(tid, (message) => {
-                    switch (message.type) {
-                        case "job":
-                            this.exports.swjs_enqueue_main_job_from_worker(message.data);
-                            break;
-                        case "request": {
-                            broker.onReceivingRequest(message);
-                            break;
-                        }
-                        case "response": {
-                            broker.onReceivingResponse(message);
-                            break;
-                        }
-                        default:
-                            const unknownMessage = message;
-                            throw new Error(`Unknown message type: ${unknownMessage}`);
-                    }
-                });
-            },
-            swjs_terminate_worker_thread: (tid) => {
-                var _a;
-                const threadChannel = this.options.threadChannel;
-                if (threadChannel && "terminateWorkerThread" in threadChannel) {
-                    (_a = threadChannel.terminateWorkerThread) === null || _a === void 0 ? void 0 : _a.call(threadChannel, tid);
-                } // Otherwise, just ignore the termination request
-            },
-            swjs_get_worker_thread_id: () => {
-                // Main thread's tid is always -1
-                return this.tid || -1;
-            },
-            swjs_request_sending_object: (sending_object, transferring_objects, transferring_objects_count, object_source_tid, sending_context) => {
-                var _a;
-                if (!this.options.threadChannel) {
-                    throw new Error("threadChannel is not set in options given to SwiftRuntime. Please set it to request transferring objects.");
-                }
-                const broker = getMessageBroker(this.options.threadChannel);
-                const memory = this.memory;
-                const transferringObjects = decodeObjectRefs(transferring_objects, transferring_objects_count, memory);
-                broker.request({
-                    type: "request",
-                    data: {
-                        sourceTid: (_a = this.tid) !== null && _a !== void 0 ? _a : MAIN_THREAD_TID,
-                        targetTid: object_source_tid,
-                        context: sending_context,
-                        request: {
-                            method: "send",
-                            parameters: [sending_object, transferringObjects, sending_context],
-                        }
-                    }
-                });
-            },
-            swjs_request_sending_objects: (sending_objects, sending_objects_count, transferring_objects, transferring_objects_count, object_source_tid, sending_context) => {
-                var _a;
-                if (!this.options.threadChannel) {
-                    throw new Error("threadChannel is not set in options given to SwiftRuntime. Please set it to request transferring objects.");
-                }
-                const broker = getMessageBroker(this.options.threadChannel);
-                const memory = this.memory;
-                const sendingObjects = decodeObjectRefs(sending_objects, sending_objects_count, memory);
-                const transferringObjects = decodeObjectRefs(transferring_objects, transferring_objects_count, memory);
-                broker.request({
-                    type: "request",
-                    data: {
-                        sourceTid: (_a = this.tid) !== null && _a !== void 0 ? _a : MAIN_THREAD_TID,
-                        targetTid: object_source_tid,
-                        context: sending_context,
-                        request: {
-                            method: "sendObjects",
-                            parameters: [sendingObjects, transferringObjects, sending_context],
-                        }
-                    }
-                });
-            },
-        };
-    }
-    postMessageToMainThread(message, transfer = []) {
-        const threadChannel = this.options.threadChannel;
-        if (!(threadChannel && "postMessageToMainThread" in threadChannel)) {
-            throw new Error("postMessageToMainThread is not set in options given to SwiftRuntime. Please set it to send messages to the main thread.");
-        }
-        threadChannel.postMessageToMainThread(message, transfer);
-    }
-    postMessageToWorkerThread(tid, message, transfer = []) {
-        const threadChannel = this.options.threadChannel;
-        if (!(threadChannel && "postMessageToWorkerThread" in threadChannel)) {
-            throw new Error("postMessageToWorkerThread is not set in options given to SwiftRuntime. Please set it to send messages to worker threads.");
-        }
-        threadChannel.postMessageToWorkerThread(tid, message, transfer);
-    }
-}
-/// This error is thrown when yielding event loop control from `swift_task_asyncMainDrainQueue`
-/// to JavaScript. This is usually thrown when:
-/// - The entry point of the Swift program is `func main() async`
-/// - The Swift Concurrency's global executor is hooked by `JavaScriptEventLoop.installGlobalExecutor()`
-/// - Calling exported `main` or `__main_argc_argv` function from JavaScript
-///
-/// This exception must be caught by the caller of the exported function and the caller should
-/// catch this exception and just ignore it.
-///
-/// FAQ: Why this error is thrown?
-/// This error is thrown to unwind the call stack of the Swift program and return the control to
-/// the JavaScript side. Otherwise, the `swift_task_asyncMainDrainQueue` ends up with `abort()`
-/// because the event loop expects `exit()` call before the end of the event loop.
-class UnsafeEventLoopYield extends Error {
-}
-
-export { SwiftRuntime };
diff --git a/Sources/JavaScriptKit/Runtime/index.mjs b/Sources/JavaScriptKit/Runtime/index.mjs
new file mode 120000
index 000000000..596131017
--- /dev/null
+++ b/Sources/JavaScriptKit/Runtime/index.mjs
@@ -0,0 +1 @@
+../../../Plugins/PackageToJS/Templates/runtime.mjs
\ No newline at end of file
diff --git a/Sources/JavaScriptKit/ThreadLocal.swift b/Sources/JavaScriptKit/ThreadLocal.swift
index 9f5751c96..e92ca32ac 100644
--- a/Sources/JavaScriptKit/ThreadLocal.swift
+++ b/Sources/JavaScriptKit/ThreadLocal.swift
@@ -15,7 +15,7 @@ import Glibc
 /// The value is stored in a thread-local variable, which is a separate copy for each thread.
 @propertyWrapper
 final class ThreadLocal: Sendable {
-#if compiler(>=6.1) && _runtime(_multithreaded)
+    #if compiler(>=6.1) && _runtime(_multithreaded)
     /// The wrapped value stored in the thread-local storage.
     /// The initial value is `nil` for each thread.
     var wrappedValue: Value? {
@@ -76,7 +76,7 @@ final class ThreadLocal: Sendable {
         }
         self.release = { Unmanaged.fromOpaque($0).release() }
     }
-#else
+    #else
     // Fallback implementation for platforms that don't support pthread
     private class SendableBox: @unchecked Sendable {
         var value: Value? = nil
@@ -93,7 +93,7 @@ final class ThreadLocal: Sendable {
     init(boxing _: Void) {
         wrappedValue = nil
     }
-#endif
+    #endif
 
     deinit {
         preconditionFailure("ThreadLocal can only be used as an immortal storage, cannot be deallocated")
diff --git a/Sources/_CJavaScriptKit/_CJavaScriptKit.c b/Sources/_CJavaScriptKit/_CJavaScriptKit.c
index ed8240ca1..a32881804 100644
--- a/Sources/_CJavaScriptKit/_CJavaScriptKit.c
+++ b/Sources/_CJavaScriptKit/_CJavaScriptKit.c
@@ -1,18 +1,18 @@
 #include "_CJavaScriptKit.h"
 #if __wasm32__
-#ifndef __wasi__
-#if __has_include("malloc.h")
-#include 
-#endif
+# ifndef __wasi__
+# if __has_include("malloc.h")
+#  include 
+# endif
 extern void *malloc(size_t size);
 extern void free(void *ptr);
 extern void *memset (void *, int, size_t);
 extern void *memcpy (void *__restrict, const void *__restrict, size_t);
-#else
-#include 
-#include 
+# else
+#  include 
+#  include 
 
-#endif
+# endif
 /// The compatibility runtime library version.
 /// Notes: If you change any interface of runtime library, please increment
 /// this and `SwiftRuntime.version` in `./Runtime/src/index.ts`.
@@ -34,7 +34,7 @@ void swjs_cleanup_host_function_call(void *argv_buffer) {
 // NOTE: This __wasi__ check is a hack for Embedded compatibility (assuming that if __wasi__ is defined, we are not building for Embedded)
 // cdecls don't work in Embedded, but @_expose(wasm) can be used with Swift >=6.0
 // the previously used `#if __Embedded` did not play well with SwiftPM (defines needed to be on every target up the chain)
-#ifdef __wasi__
+# ifdef __wasi__
 bool _call_host_function_impl(const JavaScriptHostFuncRef host_func_ref,
                               const RawJSValue *argv, const int argc,
                               const JavaScriptObjectRef callback_func);
@@ -59,6 +59,8 @@ __attribute__((export_name("swjs_library_features")))
 int swjs_library_features(void) {
     return _library_features();
 }
+# endif
+#endif
 
 int swjs_get_worker_thread_id_cached(void) {
     _Thread_local static int tid = 0;
@@ -67,5 +69,3 @@ int swjs_get_worker_thread_id_cached(void) {
     }
     return tid;
 }
-#endif
-#endif
diff --git a/Sources/_CJavaScriptKit/include/_CJavaScriptKit.h b/Sources/_CJavaScriptKit/include/_CJavaScriptKit.h
index 2b96a81ea..d587478a5 100644
--- a/Sources/_CJavaScriptKit/include/_CJavaScriptKit.h
+++ b/Sources/_CJavaScriptKit/include/_CJavaScriptKit.h
@@ -257,6 +257,16 @@ IMPORT_JS_FUNCTION(swjs_call_throwing_new, JavaScriptObjectRef, (const JavaScrip
 IMPORT_JS_FUNCTION(swjs_instanceof, bool, (const JavaScriptObjectRef obj,
                                            const JavaScriptObjectRef constructor))
 
+/// Acts like JavaScript `==` operator.
+/// Performs "==" comparison, a.k.a the "Abstract Equality Comparison"
+/// algorithm defined in the ECMAScript.
+/// https://262.ecma-international.org/11.0/#sec-abstract-equality-comparison
+///
+/// @param lhs The left-hand side value to compare.
+/// @param rhs The right-hand side value to compare.
+/// @result Return `true` if `lhs` is `==` to `rhs`. Return `false` if not.
+IMPORT_JS_FUNCTION(swjs_value_equals, bool, (const JavaScriptObjectRef lhs, const JavaScriptObjectRef rhs))
+
 /// Creates a JavaScript thunk function that calls Swift side closure.
 /// See also comments on JSFunction.swift
 ///
@@ -314,6 +324,10 @@ IMPORT_JS_FUNCTION(swjs_terminate_worker_thread, void, (int tid))
 
 IMPORT_JS_FUNCTION(swjs_get_worker_thread_id, int, (void))
 
+IMPORT_JS_FUNCTION(swjs_create_object, JavaScriptObjectRef, (void))
+
+#define SWJS_MAIN_THREAD_ID -1
+
 int swjs_get_worker_thread_id_cached(void);
 
 /// Requests sending a JavaScript object to another worker thread.
diff --git a/Tests/BridgeJSRuntimeTests/ExportAPITests.swift b/Tests/BridgeJSRuntimeTests/ExportAPITests.swift
new file mode 100644
index 000000000..8449b06da
--- /dev/null
+++ b/Tests/BridgeJSRuntimeTests/ExportAPITests.swift
@@ -0,0 +1,65 @@
+import XCTest
+import JavaScriptKit
+
+@_extern(wasm, module: "BridgeJSRuntimeTests", name: "runJsWorks")
+@_extern(c)
+func runJsWorks() -> Void
+
+@JS func roundTripVoid() -> Void {
+    return
+}
+
+@JS func roundTripInt(v: Int) -> Int {
+    return v
+}
+@JS func roundTripFloat(v: Float) -> Float {
+    return v
+}
+@JS func roundTripDouble(v: Double) -> Double {
+    return v
+}
+@JS func roundTripBool(v: Bool) -> Bool {
+    return v
+}
+@JS func roundTripString(v: String) -> String {
+    return v
+}
+@JS func roundTripSwiftHeapObject(v: Greeter) -> Greeter {
+    return v
+}
+
+@JS class Greeter {
+    var name: String
+
+    nonisolated(unsafe) static var onDeinit: () -> Void = {}
+
+    @JS init(name: String) {
+        self.name = name
+    }
+
+    @JS func greet() -> String {
+        return "Hello, \(name)!"
+    }
+    @JS func changeName(name: String) {
+        self.name = name
+    }
+
+    deinit {
+        Self.onDeinit()
+    }
+}
+
+@JS func takeGreeter(g: Greeter, name: String) {
+    g.changeName(name: name)
+}
+
+class ExportAPITests: XCTestCase {
+    func testAll() {
+        var hasDeinitGreeter = false
+        Greeter.onDeinit = {
+            hasDeinitGreeter = true
+        }
+        runJsWorks()
+        XCTAssertTrue(hasDeinitGreeter)
+    }
+}
diff --git a/Tests/BridgeJSRuntimeTests/Generated/ExportSwift.swift b/Tests/BridgeJSRuntimeTests/Generated/ExportSwift.swift
new file mode 100644
index 000000000..4a7c262c1
--- /dev/null
+++ b/Tests/BridgeJSRuntimeTests/Generated/ExportSwift.swift
@@ -0,0 +1,109 @@
+// NOTICE: This is auto-generated code by BridgeJS from JavaScriptKit,
+// DO NOT EDIT.
+//
+// To update this file, just rebuild your project or run
+// `swift package bridge-js`.
+@_extern(wasm, module: "bjs", name: "return_string")
+private func _return_string(_ ptr: UnsafePointer?, _ len: Int32)
+@_extern(wasm, module: "bjs", name: "init_memory")
+private func _init_memory(_ sourceId: Int32, _ ptr: UnsafeMutablePointer?)
+
+@_expose(wasm, "bjs_roundTripVoid")
+@_cdecl("bjs_roundTripVoid")
+public func _bjs_roundTripVoid() -> Void {
+    roundTripVoid()
+}
+
+@_expose(wasm, "bjs_roundTripInt")
+@_cdecl("bjs_roundTripInt")
+public func _bjs_roundTripInt(v: Int32) -> Int32 {
+    let ret = roundTripInt(v: Int(v))
+    return Int32(ret)
+}
+
+@_expose(wasm, "bjs_roundTripFloat")
+@_cdecl("bjs_roundTripFloat")
+public func _bjs_roundTripFloat(v: Float32) -> Float32 {
+    let ret = roundTripFloat(v: v)
+    return Float32(ret)
+}
+
+@_expose(wasm, "bjs_roundTripDouble")
+@_cdecl("bjs_roundTripDouble")
+public func _bjs_roundTripDouble(v: Float64) -> Float64 {
+    let ret = roundTripDouble(v: v)
+    return Float64(ret)
+}
+
+@_expose(wasm, "bjs_roundTripBool")
+@_cdecl("bjs_roundTripBool")
+public func _bjs_roundTripBool(v: Int32) -> Int32 {
+    let ret = roundTripBool(v: v == 1)
+    return Int32(ret ? 1 : 0)
+}
+
+@_expose(wasm, "bjs_roundTripString")
+@_cdecl("bjs_roundTripString")
+public func _bjs_roundTripString(vBytes: Int32, vLen: Int32) -> Void {
+    let v = String(unsafeUninitializedCapacity: Int(vLen)) { b in
+        _init_memory(vBytes, b.baseAddress.unsafelyUnwrapped)
+        return Int(vLen)
+    }
+    var ret = roundTripString(v: v)
+    return ret.withUTF8 { ptr in
+        _return_string(ptr.baseAddress, Int32(ptr.count))
+    }
+}
+
+@_expose(wasm, "bjs_roundTripSwiftHeapObject")
+@_cdecl("bjs_roundTripSwiftHeapObject")
+public func _bjs_roundTripSwiftHeapObject(v: UnsafeMutableRawPointer) -> UnsafeMutableRawPointer {
+    let ret = roundTripSwiftHeapObject(v: Unmanaged.fromOpaque(v).takeUnretainedValue())
+    return Unmanaged.passRetained(ret).toOpaque()
+}
+
+@_expose(wasm, "bjs_takeGreeter")
+@_cdecl("bjs_takeGreeter")
+public func _bjs_takeGreeter(g: UnsafeMutableRawPointer, nameBytes: Int32, nameLen: Int32) -> Void {
+    let name = String(unsafeUninitializedCapacity: Int(nameLen)) { b in
+        _init_memory(nameBytes, b.baseAddress.unsafelyUnwrapped)
+        return Int(nameLen)
+    }
+    takeGreeter(g: Unmanaged.fromOpaque(g).takeUnretainedValue(), name: name)
+}
+
+@_expose(wasm, "bjs_Greeter_init")
+@_cdecl("bjs_Greeter_init")
+public func _bjs_Greeter_init(nameBytes: Int32, nameLen: Int32) -> UnsafeMutableRawPointer {
+    let name = String(unsafeUninitializedCapacity: Int(nameLen)) { b in
+        _init_memory(nameBytes, b.baseAddress.unsafelyUnwrapped)
+        return Int(nameLen)
+    }
+    let ret = Greeter(name: name)
+    return Unmanaged.passRetained(ret).toOpaque()
+}
+
+@_expose(wasm, "bjs_Greeter_greet")
+@_cdecl("bjs_Greeter_greet")
+public func _bjs_Greeter_greet(_self: UnsafeMutableRawPointer) -> Void {
+    var ret = Unmanaged.fromOpaque(_self).takeUnretainedValue().greet()
+    return ret.withUTF8 { ptr in
+        _return_string(ptr.baseAddress, Int32(ptr.count))
+    }
+}
+
+@_expose(wasm, "bjs_Greeter_changeName")
+@_cdecl("bjs_Greeter_changeName")
+public func _bjs_Greeter_changeName(_self: UnsafeMutableRawPointer, nameBytes: Int32, nameLen: Int32) -> Void {
+    let name = String(unsafeUninitializedCapacity: Int(nameLen)) { b in
+        _init_memory(nameBytes, b.baseAddress.unsafelyUnwrapped)
+        return Int(nameLen)
+    }
+    Unmanaged.fromOpaque(_self).takeUnretainedValue().changeName(name: name)
+}
+
+@_expose(wasm, "bjs_Greeter_deinit")
+@_cdecl("bjs_Greeter_deinit")
+public func _bjs_Greeter_deinit(pointer: UnsafeMutableRawPointer) {
+    Unmanaged.fromOpaque(pointer).release()
+}
\ No newline at end of file
diff --git a/Tests/BridgeJSRuntimeTests/Generated/JavaScript/ExportSwift.json b/Tests/BridgeJSRuntimeTests/Generated/JavaScript/ExportSwift.json
new file mode 100644
index 000000000..b4ab97012
--- /dev/null
+++ b/Tests/BridgeJSRuntimeTests/Generated/JavaScript/ExportSwift.json
@@ -0,0 +1,218 @@
+{
+  "classes" : [
+    {
+      "constructor" : {
+        "abiName" : "bjs_Greeter_init",
+        "parameters" : [
+          {
+            "label" : "name",
+            "name" : "name",
+            "type" : {
+              "string" : {
+
+              }
+            }
+          }
+        ]
+      },
+      "methods" : [
+        {
+          "abiName" : "bjs_Greeter_greet",
+          "name" : "greet",
+          "parameters" : [
+
+          ],
+          "returnType" : {
+            "string" : {
+
+            }
+          }
+        },
+        {
+          "abiName" : "bjs_Greeter_changeName",
+          "name" : "changeName",
+          "parameters" : [
+            {
+              "label" : "name",
+              "name" : "name",
+              "type" : {
+                "string" : {
+
+                }
+              }
+            }
+          ],
+          "returnType" : {
+            "void" : {
+
+            }
+          }
+        }
+      ],
+      "name" : "Greeter"
+    }
+  ],
+  "functions" : [
+    {
+      "abiName" : "bjs_roundTripVoid",
+      "name" : "roundTripVoid",
+      "parameters" : [
+
+      ],
+      "returnType" : {
+        "void" : {
+
+        }
+      }
+    },
+    {
+      "abiName" : "bjs_roundTripInt",
+      "name" : "roundTripInt",
+      "parameters" : [
+        {
+          "label" : "v",
+          "name" : "v",
+          "type" : {
+            "int" : {
+
+            }
+          }
+        }
+      ],
+      "returnType" : {
+        "int" : {
+
+        }
+      }
+    },
+    {
+      "abiName" : "bjs_roundTripFloat",
+      "name" : "roundTripFloat",
+      "parameters" : [
+        {
+          "label" : "v",
+          "name" : "v",
+          "type" : {
+            "float" : {
+
+            }
+          }
+        }
+      ],
+      "returnType" : {
+        "float" : {
+
+        }
+      }
+    },
+    {
+      "abiName" : "bjs_roundTripDouble",
+      "name" : "roundTripDouble",
+      "parameters" : [
+        {
+          "label" : "v",
+          "name" : "v",
+          "type" : {
+            "double" : {
+
+            }
+          }
+        }
+      ],
+      "returnType" : {
+        "double" : {
+
+        }
+      }
+    },
+    {
+      "abiName" : "bjs_roundTripBool",
+      "name" : "roundTripBool",
+      "parameters" : [
+        {
+          "label" : "v",
+          "name" : "v",
+          "type" : {
+            "bool" : {
+
+            }
+          }
+        }
+      ],
+      "returnType" : {
+        "bool" : {
+
+        }
+      }
+    },
+    {
+      "abiName" : "bjs_roundTripString",
+      "name" : "roundTripString",
+      "parameters" : [
+        {
+          "label" : "v",
+          "name" : "v",
+          "type" : {
+            "string" : {
+
+            }
+          }
+        }
+      ],
+      "returnType" : {
+        "string" : {
+
+        }
+      }
+    },
+    {
+      "abiName" : "bjs_roundTripSwiftHeapObject",
+      "name" : "roundTripSwiftHeapObject",
+      "parameters" : [
+        {
+          "label" : "v",
+          "name" : "v",
+          "type" : {
+            "swiftHeapObject" : {
+              "_0" : "Greeter"
+            }
+          }
+        }
+      ],
+      "returnType" : {
+        "swiftHeapObject" : {
+          "_0" : "Greeter"
+        }
+      }
+    },
+    {
+      "abiName" : "bjs_takeGreeter",
+      "name" : "takeGreeter",
+      "parameters" : [
+        {
+          "label" : "g",
+          "name" : "g",
+          "type" : {
+            "swiftHeapObject" : {
+              "_0" : "Greeter"
+            }
+          }
+        },
+        {
+          "label" : "name",
+          "name" : "name",
+          "type" : {
+            "string" : {
+
+            }
+          }
+        }
+      ],
+      "returnType" : {
+        "void" : {
+
+        }
+      }
+    }
+  ]
+}
\ No newline at end of file
diff --git a/Tests/JavaScriptBigIntSupportTests/JavaScriptBigIntSupportTests.swift b/Tests/JavaScriptBigIntSupportTests/JavaScriptBigIntSupportTests.swift
index e1fb8a96f..73aacf1bb 100644
--- a/Tests/JavaScriptBigIntSupportTests/JavaScriptBigIntSupportTests.swift
+++ b/Tests/JavaScriptBigIntSupportTests/JavaScriptBigIntSupportTests.swift
@@ -1,6 +1,6 @@
-import XCTest
 import JavaScriptBigIntSupport
 import JavaScriptKit
+import XCTest
 
 class JavaScriptBigIntSupportTests: XCTestCase {
     func testBigIntSupport() {
@@ -11,7 +11,7 @@ class JavaScriptBigIntSupportTests: XCTestCase {
             let bigInt2 = JSBigInt(_slowBridge: value)
             XCTAssertEqual(bigInt2.description, value.description, file: file, line: line)
         }
-        
+
         // Test unsigned values
         func testUnsignedValue(_ value: UInt64, file: StaticString = #filePath, line: UInt = #line) {
             let bigInt = JSBigInt(unsigned: value)
@@ -19,21 +19,21 @@ class JavaScriptBigIntSupportTests: XCTestCase {
             let bigInt2 = JSBigInt(_slowBridge: value)
             XCTAssertEqual(bigInt2.description, value.description, file: file, line: line)
         }
-        
+
         // Test specific signed values
         testSignedValue(0)
         testSignedValue(1 << 62)
         testSignedValue(-2305)
-        
+
         // Test random signed values
         for _ in 0..<100 {
             testSignedValue(.random(in: .min ... .max))
         }
-        
+
         // Test edge signed values
         testSignedValue(.min)
         testSignedValue(.max)
-        
+
         // Test specific unsigned values
         testUnsignedValue(0)
         testUnsignedValue(1 << 62)
@@ -41,7 +41,7 @@ class JavaScriptBigIntSupportTests: XCTestCase {
         testUnsignedValue(.min)
         testUnsignedValue(.max)
         testUnsignedValue(~0)
-        
+
         // Test random unsigned values
         for _ in 0..<100 {
             testUnsignedValue(.random(in: .min ... .max))
diff --git a/Tests/JavaScriptEventLoopTestSupportTests/JavaScriptEventLoopTestSupportTests.swift b/Tests/JavaScriptEventLoopTestSupportTests/JavaScriptEventLoopTestSupportTests.swift
index cca303a09..d2b776304 100644
--- a/Tests/JavaScriptEventLoopTestSupportTests/JavaScriptEventLoopTestSupportTests.swift
+++ b/Tests/JavaScriptEventLoopTestSupportTests/JavaScriptEventLoopTestSupportTests.swift
@@ -1,5 +1,5 @@
-import XCTest
 import JavaScriptKit
+import XCTest
 
 final class JavaScriptEventLoopTestSupportTests: XCTestCase {
     func testAwaitMicrotask() async {
diff --git a/Tests/JavaScriptEventLoopTests/JSPromiseTests.swift b/Tests/JavaScriptEventLoopTests/JSPromiseTests.swift
index e19d356e5..c3429e8c9 100644
--- a/Tests/JavaScriptEventLoopTests/JSPromiseTests.swift
+++ b/Tests/JavaScriptEventLoopTests/JSPromiseTests.swift
@@ -1,4 +1,5 @@
 import XCTest
+
 @testable import JavaScriptKit
 
 final class JSPromiseTests: XCTestCase {
@@ -8,14 +9,14 @@ final class JSPromiseTests: XCTestCase {
             p1 = p1.then { value in
                 XCTAssertEqual(value, .null)
                 continuation.resume()
-                return JSValue.number(1.0)
+                return JSValue.number(1.0).jsValue
             }
         }
         await withCheckedContinuation { continuation in
             p1 = p1.then { value in
                 XCTAssertEqual(value, .number(1.0))
                 continuation.resume()
-                return JSPromise.resolve(JSValue.boolean(true))
+                return JSPromise.resolve(JSValue.boolean(true)).jsValue
             }
         }
         await withCheckedContinuation { continuation in
@@ -47,7 +48,7 @@ final class JSPromiseTests: XCTestCase {
             p2 = p2.then { value in
                 XCTAssertEqual(value, .boolean(true))
                 continuation.resume()
-                return JSPromise.reject(JSValue.number(2.0))
+                return JSPromise.reject(JSValue.number(2.0)).jsValue
             }
         }
         await withCheckedContinuation { continuation in
diff --git a/Tests/JavaScriptEventLoopTests/JavaScriptEventLoopTests.swift b/Tests/JavaScriptEventLoopTests/JavaScriptEventLoopTests.swift
index 1cd628338..8fbbd817f 100644
--- a/Tests/JavaScriptEventLoopTests/JavaScriptEventLoopTests.swift
+++ b/Tests/JavaScriptEventLoopTests/JavaScriptEventLoopTests.swift
@@ -18,7 +18,9 @@ final class JavaScriptEventLoopTests: XCTestCase {
     }
 
     func expectAsyncThrow(
-        _ body: @autoclosure () async throws -> T, file: StaticString = #file, line: UInt = #line,
+        _ body: @autoclosure () async throws -> T,
+        file: StaticString = #file,
+        line: UInt = #line,
         column: UInt = #column
     ) async throws -> Error {
         do {
@@ -148,14 +150,14 @@ final class JavaScriptEventLoopTests: XCTestCase {
             )
         }
         let promise2 = promise.then { result in
-            try await Task.sleep(nanoseconds: 100_000_000)
-            return String(result.number!)
+            try! await Task.sleep(nanoseconds: 100_000_000)
+            return .string(String(result.number!))
         }
         let thenDiff = try await measureTime {
             let result = try await promise2.value
             XCTAssertEqual(result, .string("3.0"))
         }
-        XCTAssertGreaterThanOrEqual(thenDiff, 200)
+        XCTAssertGreaterThanOrEqual(thenDiff, 150)
     }
 
     func testPromiseThenWithFailure() async throws {
@@ -169,8 +171,8 @@ final class JavaScriptEventLoopTests: XCTestCase {
                 100
             )
         }
-        let failingPromise2 = failingPromise.then { _ in
-            throw MessageError("Should not be called", file: #file, line: #line, column: #column)
+        let failingPromise2 = failingPromise.then { _ -> JSValue in
+            fatalError("Should not be called")
         } failure: { err in
             return err
         }
@@ -190,7 +192,7 @@ final class JavaScriptEventLoopTests: XCTestCase {
             )
         }
         let catchPromise2 = catchPromise.catch { err in
-            try await Task.sleep(nanoseconds: 100_000_000)
+            try! await Task.sleep(nanoseconds: 100_000_000)
             return err
         }
         let catchDiff = try await measureTime {
@@ -223,7 +225,7 @@ final class JavaScriptEventLoopTests: XCTestCase {
     func testAsyncJSClosure() async throws {
         // Test Async JSClosure
         let delayClosure = JSClosure.async { _ -> JSValue in
-            try await Task.sleep(nanoseconds: 200_000_000)
+            try! await Task.sleep(nanoseconds: 200_000_000)
             return JSValue.number(3)
         }
         let delayObject = JSObject.global.Object.function!.new()
@@ -241,20 +243,20 @@ final class JavaScriptEventLoopTests: XCTestCase {
     // MARK: - Clock Tests
 
     #if compiler(>=5.7)
-        func testClockSleep() async throws {
-            // Test ContinuousClock.sleep
-            let continuousClockDiff = try await measureTime {
-                let c = ContinuousClock()
-                try await c.sleep(until: .now + .milliseconds(100))
-            }
-            XCTAssertGreaterThanOrEqual(continuousClockDiff, 50)
+    func testClockSleep() async throws {
+        // Test ContinuousClock.sleep
+        let continuousClockDiff = try await measureTime {
+            let c = ContinuousClock()
+            try await c.sleep(until: .now + .milliseconds(100))
+        }
+        XCTAssertGreaterThanOrEqual(continuousClockDiff, 50)
 
-            // Test SuspendingClock.sleep
-            let suspendingClockDiff = try await measureTime {
-                let c = SuspendingClock()
-                try await c.sleep(until: .now + .milliseconds(100))
-            }
-            XCTAssertGreaterThanOrEqual(suspendingClockDiff, 50)
+        // Test SuspendingClock.sleep
+        let suspendingClockDiff = try await measureTime {
+            let c = SuspendingClock()
+            try await c.sleep(until: .now + .milliseconds(100))
         }
+        XCTAssertGreaterThanOrEqual(suspendingClockDiff, 50)
+    }
     #endif
 }
diff --git a/Tests/JavaScriptEventLoopTests/WebWorkerTaskExecutorTests.swift b/Tests/JavaScriptEventLoopTests/WebWorkerTaskExecutorTests.swift
index 1696224df..f743d8ef0 100644
--- a/Tests/JavaScriptEventLoopTests/WebWorkerTaskExecutorTests.swift
+++ b/Tests/JavaScriptEventLoopTests/WebWorkerTaskExecutorTests.swift
@@ -1,6 +1,7 @@
 #if compiler(>=6.1) && _runtime(_multithreaded)
+import Synchronization
 import XCTest
-import _CJavaScriptKit // For swjs_get_worker_thread_id
+import _CJavaScriptKit  // For swjs_get_worker_thread_id
 @testable import JavaScriptKit
 @testable import JavaScriptEventLoop
 
@@ -22,6 +23,7 @@ func pthread_mutex_lock(_ mutex: UnsafeMutablePointer) -> Int32
 }
 #endif
 
+@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *)
 final class WebWorkerTaskExecutorTests: XCTestCase {
     func testTaskRunOnMainThread() async throws {
         let executor = try await WebWorkerTaskExecutor(numberOfThreads: 1)
@@ -88,15 +90,189 @@ final class WebWorkerTaskExecutorTests: XCTestCase {
             }
         }
         let taskRunOnMainThread = await task.value
-        // FIXME: The block passed to `MainActor.run` should run on the main thread
-        // XCTAssertTrue(taskRunOnMainThread)
-        XCTAssertFalse(taskRunOnMainThread)
+        XCTAssertTrue(taskRunOnMainThread)
         // After the task is done, back to the main thread
         XCTAssertTrue(isMainThread())
 
         executor.terminate()
     }
 
+    func testScheduleJobWithinMacroTask1() async throws {
+        let executor = try await WebWorkerTaskExecutor(numberOfThreads: 1)
+        defer { executor.terminate() }
+
+        final class Context: @unchecked Sendable {
+            let hasEndedFirstWorkerWakeLoop = Atomic(false)
+            let hasEnqueuedFromMain = Atomic(false)
+            let hasReachedNextMacroTask = Atomic(false)
+            let hasJobBEnded = Atomic(false)
+            let hasJobCEnded = Atomic(false)
+        }
+
+        // Scenario 1.
+        //      |         Main        |           Worker         |
+        //  |   +---------------------+--------------------------+
+        //  |   |                     | Start JS macrotask       |
+        //  |   |                     |   Start 1st wake-loop    |
+        //  |   |                     |     Enq JS microtask A   |
+        //  |   |                     |   End 1st wake-loop      |
+        //  |   |                     |   Start a JS microtask A |
+        // time | Enq job B to Worker |     [PAUSE]              |
+        //  |   |                     |     Enq Swift job C      |
+        //  |   |                     |   End JS microtask A     |
+        //  |   |                     |   Start 2nd wake-loop    |
+        //  |   |                     |     Run Swift job B      |
+        //  |   |                     |     Run Swift job C      |
+        //  |   |                     |   End 2nd wake-loop      |
+        //  v   |                     | End JS macrotask         |
+        //      +---------------------+--------------------------+
+
+        let context = Context()
+        Task {
+            while !context.hasEndedFirstWorkerWakeLoop.load(ordering: .sequentiallyConsistent) {
+                try! await Task.sleep(nanoseconds: 1_000)
+            }
+            // Enqueue job B to Worker
+            Task(executorPreference: executor) {
+                XCTAssertFalse(isMainThread())
+                XCTAssertFalse(context.hasReachedNextMacroTask.load(ordering: .sequentiallyConsistent))
+                context.hasJobBEnded.store(true, ordering: .sequentiallyConsistent)
+            }
+            XCTAssertTrue(isMainThread())
+            // Resume worker thread to let it enqueue job C
+            context.hasEnqueuedFromMain.store(true, ordering: .sequentiallyConsistent)
+        }
+
+        // Start worker
+        await Task(executorPreference: executor) {
+            // Schedule a new macrotask to detect if the current macrotask has completed
+            JSObject.global.setTimeout.function!(
+                JSOneshotClosure { _ in
+                    context.hasReachedNextMacroTask.store(true, ordering: .sequentiallyConsistent)
+                    return .undefined
+                },
+                0
+            )
+
+            // Enqueue a microtask, not managed by WebWorkerTaskExecutor
+            JSObject.global.queueMicrotask.function!(
+                JSOneshotClosure { _ in
+                    // Resume the main thread and let it enqueue job B
+                    context.hasEndedFirstWorkerWakeLoop.store(true, ordering: .sequentiallyConsistent)
+                    // Wait until the enqueue has completed
+                    while !context.hasEnqueuedFromMain.load(ordering: .sequentiallyConsistent) {}
+                    // Should be still in the same macrotask
+                    XCTAssertFalse(context.hasReachedNextMacroTask.load(ordering: .sequentiallyConsistent))
+                    // Enqueue job C
+                    Task(executorPreference: executor) {
+                        // Should be still in the same macrotask
+                        XCTAssertFalse(context.hasReachedNextMacroTask.load(ordering: .sequentiallyConsistent))
+                        // Notify that job C has completed
+                        context.hasJobCEnded.store(true, ordering: .sequentiallyConsistent)
+                    }
+                    return .undefined
+                },
+                0
+            )
+            // Wait until job B, C and the next macrotask have completed
+            while !context.hasJobBEnded.load(ordering: .sequentiallyConsistent)
+                || !context.hasJobCEnded.load(ordering: .sequentiallyConsistent)
+                || !context.hasReachedNextMacroTask.load(ordering: .sequentiallyConsistent)
+            {
+                try! await Task.sleep(nanoseconds: 1_000)
+            }
+        }.value
+    }
+
+    func testScheduleJobWithinMacroTask2() async throws {
+        let executor = try await WebWorkerTaskExecutor(numberOfThreads: 1)
+        defer { executor.terminate() }
+
+        final class Context: @unchecked Sendable {
+            let hasEndedFirstWorkerWakeLoop = Atomic(false)
+            let hasEnqueuedFromMain = Atomic(false)
+            let hasReachedNextMacroTask = Atomic(false)
+            let hasJobBEnded = Atomic(false)
+            let hasJobCEnded = Atomic(false)
+        }
+
+        // Scenario 2.
+        // (The order of enqueue of job B and C are reversed from Scenario 1)
+        //
+        //      |         Main        |           Worker         |
+        //  |   +---------------------+--------------------------+
+        //  |   |                     | Start JS macrotask       |
+        //  |   |                     |   Start 1st wake-loop    |
+        //  |   |                     |     Enq JS microtask A   |
+        //  |   |                     |   End 1st wake-loop      |
+        //  |   |                     |   Start a JS microtask A |
+        //  |   |                     |     Enq Swift job C      |
+        // time | Enq job B to Worker |     [PAUSE]              |
+        //  |   |                     |   End JS microtask A     |
+        //  |   |                     |   Start 2nd wake-loop    |
+        //  |   |                     |     Run Swift job B      |
+        //  |   |                     |     Run Swift job C      |
+        //  |   |                     |   End 2nd wake-loop      |
+        //  v   |                     | End JS macrotask         |
+        //      +---------------------+--------------------------+
+
+        let context = Context()
+        Task {
+            while !context.hasEndedFirstWorkerWakeLoop.load(ordering: .sequentiallyConsistent) {
+                try! await Task.sleep(nanoseconds: 1_000)
+            }
+            // Enqueue job B to Worker
+            Task(executorPreference: executor) {
+                XCTAssertFalse(isMainThread())
+                XCTAssertFalse(context.hasReachedNextMacroTask.load(ordering: .sequentiallyConsistent))
+                context.hasJobBEnded.store(true, ordering: .sequentiallyConsistent)
+            }
+            XCTAssertTrue(isMainThread())
+            // Resume worker thread to let it enqueue job C
+            context.hasEnqueuedFromMain.store(true, ordering: .sequentiallyConsistent)
+        }
+
+        // Start worker
+        await Task(executorPreference: executor) {
+            // Schedule a new macrotask to detect if the current macrotask has completed
+            JSObject.global.setTimeout.function!(
+                JSOneshotClosure { _ in
+                    context.hasReachedNextMacroTask.store(true, ordering: .sequentiallyConsistent)
+                    return .undefined
+                },
+                0
+            )
+
+            // Enqueue a microtask, not managed by WebWorkerTaskExecutor
+            JSObject.global.queueMicrotask.function!(
+                JSOneshotClosure { _ in
+                    // Enqueue job C
+                    Task(executorPreference: executor) {
+                        // Should be still in the same macrotask
+                        XCTAssertFalse(context.hasReachedNextMacroTask.load(ordering: .sequentiallyConsistent))
+                        // Notify that job C has completed
+                        context.hasJobCEnded.store(true, ordering: .sequentiallyConsistent)
+                    }
+                    // Resume the main thread and let it enqueue job B
+                    context.hasEndedFirstWorkerWakeLoop.store(true, ordering: .sequentiallyConsistent)
+                    // Wait until the enqueue has completed
+                    while !context.hasEnqueuedFromMain.load(ordering: .sequentiallyConsistent) {}
+                    // Should be still in the same macrotask
+                    XCTAssertFalse(context.hasReachedNextMacroTask.load(ordering: .sequentiallyConsistent))
+                    return .undefined
+                },
+                0
+            )
+            // Wait until job B, C and the next macrotask have completed
+            while !context.hasJobBEnded.load(ordering: .sequentiallyConsistent)
+                || !context.hasJobCEnded.load(ordering: .sequentiallyConsistent)
+                || !context.hasReachedNextMacroTask.load(ordering: .sequentiallyConsistent)
+            {
+                try! await Task.sleep(nanoseconds: 1_000)
+            }
+        }.value
+    }
+
     func testTaskGroupRunOnSameThread() async throws {
         let executor = try await WebWorkerTaskExecutor(numberOfThreads: 3)
 
@@ -226,17 +402,17 @@ final class WebWorkerTaskExecutorTests: XCTestCase {
 
         func decodeJob() throws {
             let json = """
-            {
-                "prop_1": {
-                    "nested_prop": 42
-                },
-                "prop_2": 100,
-                "prop_3": true,
-                "prop_7": 3.14,
-                "prop_8": "Hello, World!",
-                "prop_9": ["a", "b", "c"]
-            }
-            """
+                {
+                    "prop_1": {
+                        "nested_prop": 42
+                    },
+                    "prop_2": 100,
+                    "prop_3": true,
+                    "prop_7": 3.14,
+                    "prop_8": "Hello, World!",
+                    "prop_9": ["a", "b", "c"]
+                }
+                """
             let object = JSObject.global.JSON.parse(json)
             let decoder = JSValueDecoder()
             let result = try decoder.decode(DecodeMe.self, from: object)
@@ -329,37 +505,35 @@ final class WebWorkerTaskExecutorTests: XCTestCase {
         XCTAssertTrue(jsErrorMessage.contains("Failed to serialize message"), jsErrorMessage)
     }
 
-    /*
-    // Node.js 20 and below doesn't throw exception when transferring the same ArrayBuffer
-    // multiple times.
-    // See https://github.com/nodejs/node/commit/38dee8a1c04237bd231a01410f42e9d172f4c162
-    func testTransferMultipleTimes() async throws {
-        let executor = try await WebWorkerTaskExecutor(numberOfThreads: 1)
-        let Uint8Array = JSObject.global.Uint8Array.function!
-        let buffer = Uint8Array.new(100).buffer.object!
-        let transferring = JSSending.transfer(buffer)
-        let task1 = Task(executorPreference: executor) {
-            let buffer = try await transferring.receive()
-            return buffer.byteLength.number!
-        }
-        let byteLength1 = try await task1.value
-        XCTAssertEqual(byteLength1, 100)
-
-        let task2 = Task(executorPreference: executor) {
-            do {
-                _ = try await transferring.receive()
-                return nil
-            } catch {
-                return String(describing: error)
-            }
-        }
-        guard let jsErrorMessage = await task2.value else {
-            XCTFail("Should throw an error")
-            return
-        }
-        XCTAssertTrue(jsErrorMessage.contains("Failed to serialize message"))
-    }
-    */
+    // // Node.js 20 and below doesn't throw exception when transferring the same ArrayBuffer
+    // // multiple times.
+    // // See https://github.com/nodejs/node/commit/38dee8a1c04237bd231a01410f42e9d172f4c162
+    // func testTransferMultipleTimes() async throws {
+    //     let executor = try await WebWorkerTaskExecutor(numberOfThreads: 1)
+    //     let Uint8Array = JSObject.global.Uint8Array.function!
+    //     let buffer = Uint8Array.new(100).buffer.object!
+    //     let transferring = JSSending.transfer(buffer)
+    //     let task1 = Task(executorPreference: executor) {
+    //         let buffer = try await transferring.receive()
+    //         return buffer.byteLength.number!
+    //     }
+    //     let byteLength1 = try await task1.value
+    //     XCTAssertEqual(byteLength1, 100)
+    //
+    //     let task2 = Task(executorPreference: executor) {
+    //         do {
+    //             _ = try await transferring.receive()
+    //             return nil
+    //         } catch {
+    //             return String(describing: error)
+    //         }
+    //     }
+    //     guard let jsErrorMessage = await task2.value else {
+    //         XCTFail("Should throw an error")
+    //         return
+    //     }
+    //     XCTAssertTrue(jsErrorMessage.contains("Failed to serialize message"))
+    // }
 
     func testCloneMultipleTimes() async throws {
         let executor = try await WebWorkerTaskExecutor(numberOfThreads: 1)
@@ -444,18 +618,30 @@ final class WebWorkerTaskExecutorTests: XCTestCase {
         XCTAssertEqual(object["test"].string!, "Hello, World!")
     }
 
-/*
-    func testDeinitJSObjectOnDifferentThread() async throws {
+    func testThrowJSExceptionAcrossThreads() async throws {
         let executor = try await WebWorkerTaskExecutor(numberOfThreads: 1)
-
-        var object: JSObject? = JSObject.global.Object.function!.new()
         let task = Task(executorPreference: executor) {
-            object = nil
-            _ = object
+            _ = try JSObject.global.eval.function!.throws("throw new Error()")
+        }
+        do {
+            try await task.value
+            XCTFail()
+        } catch let error as JSException {
+            // Stringify JSException coming from worker should be allowed
+            _ = String(describing: error)
         }
-        await task.value
-        executor.terminate()
     }
-*/
+
+    // func testDeinitJSObjectOnDifferentThread() async throws {
+    //     let executor = try await WebWorkerTaskExecutor(numberOfThreads: 1)
+    //
+    //     var object: JSObject? = JSObject.global.Object.function!.new()
+    //     let task = Task(executorPreference: executor) {
+    //         object = nil
+    //         _ = object
+    //     }
+    //     await task.value
+    //     executor.terminate()
+    // }
 }
 #endif
diff --git a/Tests/JavaScriptKitTests/JSObjectTests.swift b/Tests/JavaScriptKitTests/JSObjectTests.swift
new file mode 100644
index 000000000..e283da608
--- /dev/null
+++ b/Tests/JavaScriptKitTests/JSObjectTests.swift
@@ -0,0 +1,28 @@
+import JavaScriptKit
+import XCTest
+
+final class JSObjectTests: XCTestCase {
+    func testEmptyObject() {
+        let object = JSObject()
+        let keys = JSObject.global.Object.function!.keys.function!(object)
+        XCTAssertEqual(keys.array?.count, 0)
+    }
+
+    func testInitWithDictionaryLiteral() {
+        let object: JSObject = [
+            "key1": 1,
+            "key2": "value2",
+            "key3": .boolean(true),
+            "key4": .object(JSObject()),
+            "key5": [1, 2, 3].jsValue,
+            "key6": ["key": "value"].jsValue,
+        ]
+        XCTAssertEqual(object.key1, .number(1))
+        XCTAssertEqual(object.key2, "value2")
+        XCTAssertEqual(object.key3, .boolean(true))
+        let getKeys = JSObject.global.Object.function!.keys.function!
+        XCTAssertEqual(getKeys(object.key4).array?.count, 0)
+        XCTAssertEqual(object.key5.array.map(Array.init), [1, 2, 3])
+        XCTAssertEqual(object.key6.object?.key, "value")
+    }
+}
diff --git a/Tests/JavaScriptKitTests/JSStringTests.swift b/Tests/JavaScriptKitTests/JSStringTests.swift
new file mode 100644
index 000000000..456c24147
--- /dev/null
+++ b/Tests/JavaScriptKitTests/JSStringTests.swift
@@ -0,0 +1,13 @@
+import JavaScriptKit
+import XCTest
+
+final class JSStringTests: XCTestCase {
+    func testEquatable() {
+        let string1 = JSString("Hello, world!")
+        let string2 = JSString("Hello, world!")
+        let string3 = JSString("Hello, world")
+        XCTAssertEqual(string1, string1)
+        XCTAssertEqual(string1, string2)
+        XCTAssertNotEqual(string1, string3)
+    }
+}
diff --git a/Tests/JavaScriptKitTests/JSTypedArrayTests.swift b/Tests/JavaScriptKitTests/JSTypedArrayTests.swift
index 8e2556f8d..a4649879e 100644
--- a/Tests/JavaScriptKitTests/JSTypedArrayTests.swift
+++ b/Tests/JavaScriptKitTests/JSTypedArrayTests.swift
@@ -17,8 +17,8 @@ final class JSTypedArrayTests: XCTestCase {
     }
 
     func testTypedArray() {
-        func checkArray(_ array: [T]) where T: TypedArrayElement & Equatable {
-            XCTAssertEqual(toString(JSTypedArray(array).jsValue.object!), jsStringify(array))
+        func checkArray(_ array: [T]) where T: TypedArrayElement & Equatable, T.Element == T {
+            XCTAssertEqual(toString(JSTypedArray(array).jsValue.object!), jsStringify(array))
             checkArrayUnsafeBytes(array)
         }
 
@@ -30,20 +30,20 @@ final class JSTypedArrayTests: XCTestCase {
             array.map({ String(describing: $0) }).joined(separator: ",")
         }
 
-        func checkArrayUnsafeBytes(_ array: [T]) where T: TypedArrayElement & Equatable {
-            let copyOfArray: [T] = JSTypedArray(array).withUnsafeBytes { buffer in
+        func checkArrayUnsafeBytes(_ array: [T]) where T: TypedArrayElement & Equatable, T.Element == T {
+            let copyOfArray: [T] = JSTypedArray(array).withUnsafeBytes { buffer in
                 Array(buffer)
             }
             XCTAssertEqual(copyOfArray, array)
         }
 
         let numbers = [UInt8](0...255)
-        let typedArray = JSTypedArray(numbers)
+        let typedArray = JSTypedArray(numbers)
         XCTAssertEqual(typedArray[12], 12)
         XCTAssertEqual(numbers.count, typedArray.lengthInBytes)
 
         let numbersSet = Set(0...255)
-        let typedArrayFromSet = JSTypedArray(numbersSet)
+        let typedArrayFromSet = JSTypedArray(numbersSet)
         XCTAssertEqual(typedArrayFromSet.jsObject.length, 256)
         XCTAssertEqual(typedArrayFromSet.lengthInBytes, 256 * MemoryLayout.size)
 
@@ -63,7 +63,7 @@ final class JSTypedArrayTests: XCTestCase {
             0, 1, .pi, .greatestFiniteMagnitude, .infinity, .leastNonzeroMagnitude,
             .leastNormalMagnitude, 42,
         ]
-        let jsFloat32Array = JSTypedArray(float32Array)
+        let jsFloat32Array = JSTypedArray(float32Array)
         for (i, num) in float32Array.enumerated() {
             XCTAssertEqual(num, jsFloat32Array[i])
         }
@@ -72,7 +72,7 @@ final class JSTypedArrayTests: XCTestCase {
             0, 1, .pi, .greatestFiniteMagnitude, .infinity, .leastNonzeroMagnitude,
             .leastNormalMagnitude, 42,
         ]
-        let jsFloat64Array = JSTypedArray(float64Array)
+        let jsFloat64Array = JSTypedArray(float64Array)
         for (i, num) in float64Array.enumerated() {
             XCTAssertEqual(num, jsFloat64Array[i])
         }
@@ -97,4 +97,30 @@ final class JSTypedArrayTests: XCTestCase {
 
         XCTAssertEqual(toString(array.jsValue.object!), jsStringify(Array(0..<100)))
     }
+
+    func testInitWithBufferPointer() {
+        let buffer = UnsafeMutableBufferPointer.allocate(capacity: 20)
+        defer { buffer.deallocate() }
+        for i in 0..<20 {
+            buffer[i] = Float32(i)
+        }
+        let typedArray = JSTypedArray(buffer: UnsafeBufferPointer(buffer))
+        for i in 0..<20 {
+            XCTAssertEqual(typedArray[i], Float32(i))
+        }
+    }
+
+    func testCopyMemory() {
+        let array = JSTypedArray(length: 100)
+        for i in 0..<100 {
+            array[i] = i
+        }
+        let destination = UnsafeMutableBufferPointer.allocate(capacity: 100)
+        defer { destination.deallocate() }
+        array.copyMemory(to: destination)
+
+        for i in 0..<100 {
+            XCTAssertEqual(destination[i], i)
+        }
+    }
 }
diff --git a/Tests/JavaScriptKitTests/JavaScriptKitTests.swift b/Tests/JavaScriptKitTests/JavaScriptKitTests.swift
index 6c90afead..246df522a 100644
--- a/Tests/JavaScriptKitTests/JavaScriptKitTests.swift
+++ b/Tests/JavaScriptKitTests/JavaScriptKitTests.swift
@@ -1,5 +1,5 @@
-import XCTest
 import JavaScriptKit
+import XCTest
 
 class JavaScriptKitTests: XCTestCase {
     func testLiteralConversion() {
@@ -22,7 +22,7 @@ class JavaScriptKitTests: XCTestCase {
             setJSValue(this: global, name: prop, value: input)
             let got = getJSValue(this: global, name: prop)
             switch (got, input) {
-            case let (.number(lhs), .number(rhs)):
+            case (.number(let lhs), .number(let rhs)):
                 // Compare bitPattern because nan == nan is always false
                 XCTAssertEqual(lhs.bitPattern, rhs.bitPattern)
             default:
@@ -30,7 +30,7 @@ class JavaScriptKitTests: XCTestCase {
             }
         }
     }
-    
+
     func testObjectConversion() {
         // Notes: globalObject1 is defined in JavaScript environment
         //
@@ -47,7 +47,7 @@ class JavaScriptKitTests: XCTestCase {
         //   ...
         // }
         // ```
-        
+
         let globalObject1 = getJSValue(this: .global, name: "globalObject1")
         let globalObject1Ref = try! XCTUnwrap(globalObject1.object)
         let prop_1 = getJSValue(this: globalObject1Ref, name: "prop_1")
@@ -67,10 +67,10 @@ class JavaScriptKitTests: XCTestCase {
             let actualElement = getJSValue(this: prop_4Array, index: Int32(index))
             XCTAssertEqual(actualElement, expectedElement)
         }
-        
+
         XCTAssertEqual(getJSValue(this: globalObject1Ref, name: "undefined_prop"), .undefined)
     }
-    
+
     func testValueConstruction() {
         let globalObject1 = getJSValue(this: .global, name: "globalObject1")
         let globalObject1Ref = try! XCTUnwrap(globalObject1.object)
@@ -81,10 +81,10 @@ class JavaScriptKitTests: XCTestCase {
         let prop_7 = getJSValue(this: globalObject1Ref, name: "prop_7")
         XCTAssertEqual(Double.construct(from: prop_7), 3.14)
         XCTAssertEqual(Float.construct(from: prop_7), 3.14)
-        
+
         for source: JSValue in [
             .number(.infinity), .number(.nan),
-            .number(Double(UInt64.max).nextUp), .number(Double(Int64.min).nextDown)
+            .number(Double(UInt64.max).nextUp), .number(Double(Int64.min).nextDown),
         ] {
             XCTAssertNil(Int.construct(from: source))
             XCTAssertNil(Int8.construct(from: source))
@@ -98,7 +98,7 @@ class JavaScriptKitTests: XCTestCase {
             XCTAssertNil(UInt64.construct(from: source))
         }
     }
-    
+
     func testArrayIterator() {
         let globalObject1 = getJSValue(this: .global, name: "globalObject1")
         let globalObject1Ref = try! XCTUnwrap(globalObject1.object)
@@ -108,14 +108,14 @@ class JavaScriptKitTests: XCTestCase {
             .number(3), .number(4), .string("str_elm_1"), .null, .undefined, .number(5),
         ]
         XCTAssertEqual(Array(array1), expectedProp_4)
-        
+
         // Ensure that iterator skips empty hole as JavaScript does.
         let prop_8 = getJSValue(this: globalObject1Ref, name: "prop_8")
         let array2 = try! XCTUnwrap(prop_8.array)
         let expectedProp_8: [JSValue] = [0, 2, 3, 6]
         XCTAssertEqual(Array(array2), expectedProp_8)
     }
-    
+
     func testArrayRandomAccessCollection() {
         let globalObject1 = getJSValue(this: .global, name: "globalObject1")
         let globalObject1Ref = try! XCTUnwrap(globalObject1.object)
@@ -125,22 +125,22 @@ class JavaScriptKitTests: XCTestCase {
             .number(3), .number(4), .string("str_elm_1"), .null, .undefined, .number(5),
         ]
         XCTAssertEqual([array1[0], array1[1], array1[2], array1[3], array1[4], array1[5]], expectedProp_4)
-        
+
         // Ensure that subscript can access empty hole
         let prop_8 = getJSValue(this: globalObject1Ref, name: "prop_8")
         let array2 = try! XCTUnwrap(prop_8.array)
         let expectedProp_8: [JSValue] = [
-            0, .undefined, 2, 3, .undefined, .undefined, 6
+            0, .undefined, 2, 3, .undefined, .undefined, 6,
         ]
         XCTAssertEqual([array2[0], array2[1], array2[2], array2[3], array2[4], array2[5], array2[6]], expectedProp_8)
     }
-    
+
     func testValueDecoder() {
         struct GlobalObject1: Codable {
             struct Prop1: Codable {
                 let nested_prop: Int
             }
-            
+
             let prop_1: Prop1
             let prop_2: Int
             let prop_3: Bool
@@ -154,7 +154,7 @@ class JavaScriptKitTests: XCTestCase {
         XCTAssertEqual(globalObject1.prop_3, true)
         XCTAssertEqual(globalObject1.prop_7, 3.14)
     }
-    
+
     func testFunctionCall() {
         // Notes: globalObject1 is defined in JavaScript environment
         //
@@ -174,12 +174,12 @@ class JavaScriptKitTests: XCTestCase {
         //   ...
         // }
         // ```
-        
+
         let globalObject1 = getJSValue(this: .global, name: "globalObject1")
         let globalObject1Ref = try! XCTUnwrap(globalObject1.object)
         let prop_5 = getJSValue(this: globalObject1Ref, name: "prop_5")
         let prop_5Ref = try! XCTUnwrap(prop_5.object)
-        
+
         let func1 = try! XCTUnwrap(getJSValue(this: prop_5Ref, name: "func1").function)
         XCTAssertEqual(func1(), .undefined)
         let func2 = try! XCTUnwrap(getJSValue(this: prop_5Ref, name: "func2").function)
@@ -196,30 +196,30 @@ class JavaScriptKitTests: XCTestCase {
         XCTAssertEqual(func6(false, 1, 2), .number(2))
         XCTAssertEqual(func6(true, "OK", 2), .string("OK"))
     }
-    
+
     func testClosureLifetime() {
         let evalClosure = JSObject.global.globalObject1.eval_closure.function!
-        
+
         do {
             let c1 = JSClosure { arguments in
                 return arguments[0]
             }
             XCTAssertEqual(evalClosure(c1, JSValue.number(1.0)), .number(1.0))
-#if JAVASCRIPTKIT_WITHOUT_WEAKREFS
+            #if JAVASCRIPTKIT_WITHOUT_WEAKREFS
             c1.release()
-#endif
+            #endif
         }
-        
+
         do {
             let array = JSObject.global.Array.function!.new()
             let c1 = JSClosure { _ in .number(3) }
             _ = array.push!(c1)
             XCTAssertEqual(array[0].function!().number, 3.0)
-#if JAVASCRIPTKIT_WITHOUT_WEAKREFS
+            #if JAVASCRIPTKIT_WITHOUT_WEAKREFS
             c1.release()
-#endif
+            #endif
         }
-        
+
         do {
             let c1 = JSClosure { _ in .undefined }
             XCTAssertEqual(c1(), .undefined)
@@ -230,7 +230,7 @@ class JavaScriptKitTests: XCTestCase {
             XCTAssertEqual(c1(), .number(4))
         }
     }
-    
+
     func testHostFunctionRegistration() {
         // ```js
         // global.globalObject1 = {
@@ -246,24 +246,24 @@ class JavaScriptKitTests: XCTestCase {
         let globalObject1Ref = try! XCTUnwrap(globalObject1.object)
         let prop_6 = getJSValue(this: globalObject1Ref, name: "prop_6")
         let prop_6Ref = try! XCTUnwrap(prop_6.object)
-        
+
         var isHostFunc1Called = false
         let hostFunc1 = JSClosure { (_) -> JSValue in
             isHostFunc1Called = true
             return .number(1)
         }
-        
+
         setJSValue(this: prop_6Ref, name: "host_func_1", value: .object(hostFunc1))
-        
+
         let call_host_1 = getJSValue(this: prop_6Ref, name: "call_host_1")
         let call_host_1Func = try! XCTUnwrap(call_host_1.function)
         XCTAssertEqual(call_host_1Func(), .number(1))
         XCTAssertEqual(isHostFunc1Called, true)
-        
-#if JAVASCRIPTKIT_WITHOUT_WEAKREFS
+
+        #if JAVASCRIPTKIT_WITHOUT_WEAKREFS
         hostFunc1.release()
-#endif
-        
+        #endif
+
         let evalClosure = JSObject.global.globalObject1.eval_closure.function!
         let hostFunc2 = JSClosure { (arguments) -> JSValue in
             if let input = arguments[0].number {
@@ -272,15 +272,15 @@ class JavaScriptKitTests: XCTestCase {
                 return .string(String(describing: arguments[0]))
             }
         }
-        
+
         XCTAssertEqual(evalClosure(hostFunc2, 3), .number(6))
         XCTAssertTrue(evalClosure(hostFunc2, true).string != nil)
-        
-#if JAVASCRIPTKIT_WITHOUT_WEAKREFS
+
+        #if JAVASCRIPTKIT_WITHOUT_WEAKREFS
         hostFunc2.release()
-#endif
+        #endif
     }
-    
+
     func testNewObjectConstruction() {
         // ```js
         // global.Animal = function(name, age, isCat) {
@@ -299,14 +299,14 @@ class JavaScriptKitTests: XCTestCase {
         XCTAssertEqual(cat1.isInstanceOf(try! XCTUnwrap(getJSValue(this: .global, name: "Array").function)), false)
         let cat1Bark = try! XCTUnwrap(getJSValue(this: cat1, name: "bark").function)
         XCTAssertEqual(cat1Bark(), .string("nyan"))
-        
+
         let dog1 = objectConstructor.new("Pochi", 3, false)
         let dog1Bark = try! XCTUnwrap(getJSValue(this: dog1, name: "bark").function)
         XCTAssertEqual(dog1Bark(), .string("wan"))
     }
-    
+
     func testObjectDecoding() {
-        /* 
+        /*
          ```js
          global.objectDecodingTest = {
              obj: {},
@@ -317,7 +317,7 @@ class JavaScriptKitTests: XCTestCase {
          ```
          */
         let js: JSValue = JSObject.global.objectDecodingTest
-        
+
         // I can't use regular name like `js.object` here
         // cz its conflicting with case name and DML.
         // so I use abbreviated names
@@ -325,28 +325,28 @@ class JavaScriptKitTests: XCTestCase {
         let function: JSValue = js.fn
         let symbol: JSValue = js.sym
         let bigInt: JSValue = js.bi
-        
+
         XCTAssertNotNil(JSObject.construct(from: object))
         XCTAssertEqual(JSObject.construct(from: function).map { $0 is JSFunction }, .some(true))
         XCTAssertEqual(JSObject.construct(from: symbol).map { $0 is JSSymbol }, .some(true))
         XCTAssertEqual(JSObject.construct(from: bigInt).map { $0 is JSBigInt }, .some(true))
-        
+
         XCTAssertNil(JSFunction.construct(from: object))
         XCTAssertNotNil(JSFunction.construct(from: function))
         XCTAssertNil(JSFunction.construct(from: symbol))
         XCTAssertNil(JSFunction.construct(from: bigInt))
-        
+
         XCTAssertNil(JSSymbol.construct(from: object))
         XCTAssertNil(JSSymbol.construct(from: function))
         XCTAssertNotNil(JSSymbol.construct(from: symbol))
         XCTAssertNil(JSSymbol.construct(from: bigInt))
-        
+
         XCTAssertNil(JSBigInt.construct(from: object))
         XCTAssertNil(JSBigInt.construct(from: function))
         XCTAssertNil(JSBigInt.construct(from: symbol))
         XCTAssertNotNil(JSBigInt.construct(from: bigInt))
     }
-    
+
     func testCallFunctionWithThis() {
         // ```js
         // global.Animal = function(name, age, isCat) {
@@ -366,16 +366,16 @@ class JavaScriptKitTests: XCTestCase {
         let cat1Value = JSValue.object(cat1)
         let getIsCat = try! XCTUnwrap(getJSValue(this: cat1, name: "getIsCat").function)
         let setName = try! XCTUnwrap(getJSValue(this: cat1, name: "setName").function)
-        
+
         // Direct call without this
         XCTAssertThrowsError(try getIsCat.throws())
-        
+
         // Call with this
         let gotIsCat = getIsCat(this: cat1)
         XCTAssertEqual(gotIsCat, .boolean(true))
         XCTAssertEqual(cat1.getIsCat!(), .boolean(true))
         XCTAssertEqual(cat1Value.getIsCat(), .boolean(true))
-        
+
         // Call with this and argument
         setName(this: cat1, JSValue.string("Shiro"))
         XCTAssertEqual(getJSValue(this: cat1, name: "name"), .string("Shiro"))
@@ -384,7 +384,7 @@ class JavaScriptKitTests: XCTestCase {
         _ = cat1Value.setName("Chibi")
         XCTAssertEqual(getJSValue(this: cat1, name: "name"), .string("Chibi"))
     }
-    
+
     func testJSObjectConversion() {
         let array1 = [1, 2, 3]
         let jsArray1 = array1.jsValue.object!
@@ -392,7 +392,7 @@ class JavaScriptKitTests: XCTestCase {
         XCTAssertEqual(jsArray1[0], .number(1))
         XCTAssertEqual(jsArray1[1], .number(2))
         XCTAssertEqual(jsArray1[2], .number(3))
-        
+
         let array2: [ConvertibleToJSValue] = [1, "str", false]
         let jsArray2 = array2.jsValue.object!
         XCTAssertEqual(jsArray2.length, .number(3))
@@ -402,9 +402,9 @@ class JavaScriptKitTests: XCTestCase {
         _ = jsArray2.push!(5)
         XCTAssertEqual(jsArray2.length, .number(4))
         _ = jsArray2.push!(jsArray1)
-        
+
         XCTAssertEqual(jsArray2[4], .object(jsArray1))
-        
+
         let dict1: [String: JSValue] = [
             "prop1": 1.jsValue,
             "prop2": "foo".jsValue,
@@ -413,7 +413,7 @@ class JavaScriptKitTests: XCTestCase {
         XCTAssertEqual(jsDict1.prop1, .number(1))
         XCTAssertEqual(jsDict1.prop2, .string("foo"))
     }
-    
+
     func testObjectRefLifetime() {
         // ```js
         // global.globalObject1 = {
@@ -428,24 +428,24 @@ class JavaScriptKitTests: XCTestCase {
         //   ...
         // }
         // ```
-        
+
         let evalClosure = JSObject.global.globalObject1.eval_closure.function!
         let identity = JSClosure { $0[0] }
         let ref1 = getJSValue(this: .global, name: "globalObject1").object!
         let ref2 = evalClosure(identity, ref1).object!
         XCTAssertEqual(ref1.prop_2, .number(2))
         XCTAssertEqual(ref2.prop_2, .number(2))
-        
-#if JAVASCRIPTKIT_WITHOUT_WEAKREFS
+
+        #if JAVASCRIPTKIT_WITHOUT_WEAKREFS
         identity.release()
-#endif
+        #endif
     }
-    
+
     func testDate() {
         let date1Milliseconds = JSDate.now()
         let date1 = JSDate(millisecondsSinceEpoch: date1Milliseconds)
         let date2 = JSDate(millisecondsSinceEpoch: date1.valueOf())
-        
+
         XCTAssertEqual(date1.valueOf(), date2.valueOf())
         XCTAssertEqual(date1.fullYear, date2.fullYear)
         XCTAssertEqual(date1.month, date2.month)
@@ -464,7 +464,7 @@ class JavaScriptKitTests: XCTestCase {
         XCTAssertEqual(date1.utcSeconds, date2.utcSeconds)
         XCTAssertEqual(date1.utcMilliseconds, date2.utcMilliseconds)
         XCTAssertEqual(date1, date2)
-        
+
         let date3 = JSDate(millisecondsSinceEpoch: 0)
         XCTAssertEqual(date3.valueOf(), 0)
         XCTAssertEqual(date3.utcFullYear, 1970)
@@ -477,10 +477,10 @@ class JavaScriptKitTests: XCTestCase {
         XCTAssertEqual(date3.utcSeconds, 0)
         XCTAssertEqual(date3.utcMilliseconds, 0)
         XCTAssertEqual(date3.toISOString(), "1970-01-01T00:00:00.000Z")
-        
+
         XCTAssertTrue(date3 < date1)
     }
-    
+
     func testError() {
         let message = "test error"
         let expectedDescription = "Error: test error"
@@ -492,21 +492,21 @@ class JavaScriptKitTests: XCTestCase {
         XCTAssertNil(JSError(from: .string("error"))?.description)
         XCTAssertEqual(JSError(from: .object(error.jsObject))?.description, expectedDescription)
     }
-    
+
     func testJSValueAccessor() {
-        var globalObject1 = JSObject.global.globalObject1
+        let globalObject1 = JSObject.global.globalObject1
         XCTAssertEqual(globalObject1.prop_1.nested_prop, .number(1))
         XCTAssertEqual(globalObject1.object!.prop_1.object!.nested_prop, .number(1))
-        
+
         XCTAssertEqual(globalObject1.prop_4[0], .number(3))
         XCTAssertEqual(globalObject1.prop_4[1], .number(4))
-        
+
         let originalProp1 = globalObject1.prop_1.object!.nested_prop
         globalObject1.prop_1.nested_prop = "bar"
         XCTAssertEqual(globalObject1.prop_1.nested_prop, .string("bar"))
         globalObject1.prop_1.nested_prop = originalProp1
     }
-    
+
     func testException() {
         // ```js
         // global.globalObject1 = {
@@ -528,33 +528,33 @@ class JavaScriptKitTests: XCTestCase {
         //
         let globalObject1 = JSObject.global.globalObject1
         let prop_9: JSValue = globalObject1.prop_9
-        
+
         // MARK: Throwing method calls
         XCTAssertThrowsError(try prop_9.object!.throwing.func1!()) { error in
             XCTAssertTrue(error is JSException)
             let errorObject = JSError(from: (error as! JSException).thrownValue)
             XCTAssertNotNil(errorObject)
         }
-        
+
         XCTAssertThrowsError(try prop_9.object!.throwing.func2!()) { error in
             XCTAssertTrue(error is JSException)
             let thrownValue = (error as! JSException).thrownValue
             XCTAssertEqual(thrownValue.string, "String Error")
         }
-        
+
         XCTAssertThrowsError(try prop_9.object!.throwing.func3!()) { error in
             XCTAssertTrue(error is JSException)
             let thrownValue = (error as! JSException).thrownValue
             XCTAssertEqual(thrownValue.number, 3.0)
         }
-        
+
         // MARK: Simple function calls
         XCTAssertThrowsError(try prop_9.func1.function!.throws()) { error in
             XCTAssertTrue(error is JSException)
             let errorObject = JSError(from: (error as! JSException).thrownValue)
             XCTAssertNotNil(errorObject)
         }
-        
+
         // MARK: Throwing constructor call
         let Animal = JSObject.global.Animal.function!
         XCTAssertNoThrow(try Animal.throws.new("Tama", 3, true))
@@ -564,16 +564,16 @@ class JavaScriptKitTests: XCTestCase {
             XCTAssertNotNil(errorObject)
         }
     }
-    
+
     func testSymbols() {
         let symbol1 = JSSymbol("abc")
         let symbol2 = JSSymbol("abc")
         XCTAssertNotEqual(symbol1, symbol2)
         XCTAssertEqual(symbol1.name, symbol2.name)
         XCTAssertEqual(symbol1.name, "abc")
-        
+
         XCTAssertEqual(JSSymbol.iterator, JSSymbol.iterator)
-        
+
         // let hasInstanceClass = {
         //   prop: function () {}
         // }.prop
@@ -584,35 +584,36 @@ class JavaScriptKitTests: XCTestCase {
         let propertyDescriptor = JSObject.global.Object.function!.new()
         propertyDescriptor.value = JSClosure { _ in .boolean(true) }.jsValue
         _ = JSObject.global.Object.function!.defineProperty!(
-            hasInstanceClass, JSSymbol.hasInstance,
+            hasInstanceClass,
+            JSSymbol.hasInstance,
             propertyDescriptor
         )
         XCTAssertEqual(hasInstanceClass[JSSymbol.hasInstance].function!().boolean, true)
         XCTAssertEqual(JSObject.global.Object.isInstanceOf(hasInstanceClass), true)
     }
-    
+
     func testJSValueDecoder() {
         struct AnimalStruct: Decodable {
             let name: String
             let age: Int
             let isCat: Bool
         }
-        
+
         let Animal = JSObject.global.Animal.function!
         let tama = try! Animal.throws.new("Tama", 3, true)
         let decoder = JSValueDecoder()
         let decodedTama = try! decoder.decode(AnimalStruct.self, from: tama.jsValue)
-        
+
         XCTAssertEqual(decodedTama.name, tama.name.string)
         XCTAssertEqual(decodedTama.name, "Tama")
-        
+
         XCTAssertEqual(decodedTama.age, tama.age.number.map(Int.init))
         XCTAssertEqual(decodedTama.age, 3)
-        
+
         XCTAssertEqual(decodedTama.isCat, tama.isCat.boolean)
         XCTAssertEqual(decodedTama.isCat, true)
     }
-    
+
     func testConvertibleToJSValue() {
         let array1 = [1, 2, 3]
         let jsArray1 = array1.jsValue.object!
@@ -620,7 +621,7 @@ class JavaScriptKitTests: XCTestCase {
         XCTAssertEqual(jsArray1[0], .number(1))
         XCTAssertEqual(jsArray1[1], .number(2))
         XCTAssertEqual(jsArray1[2], .number(3))
-        
+
         let array2: [ConvertibleToJSValue] = [1, "str", false]
         let jsArray2 = array2.jsValue.object!
         XCTAssertEqual(jsArray2.length, .number(3))
@@ -630,9 +631,9 @@ class JavaScriptKitTests: XCTestCase {
         _ = jsArray2.push!(5)
         XCTAssertEqual(jsArray2.length, .number(4))
         _ = jsArray2.push!(jsArray1)
-        
+
         XCTAssertEqual(jsArray2[4], .object(jsArray1))
-        
+
         let dict1: [String: JSValue] = [
             "prop1": 1.jsValue,
             "prop2": "foo".jsValue,
diff --git a/Tests/JavaScriptKitTests/ThreadLocalTests.swift b/Tests/JavaScriptKitTests/ThreadLocalTests.swift
index 55fcdadb4..d1d736b8b 100644
--- a/Tests/JavaScriptKitTests/ThreadLocalTests.swift
+++ b/Tests/JavaScriptKitTests/ThreadLocalTests.swift
@@ -1,4 +1,5 @@
 import XCTest
+
 @testable import JavaScriptKit
 
 final class ThreadLocalTests: XCTestCase {
diff --git a/Tests/prelude.mjs b/Tests/prelude.mjs
index ab5723587..2501bd584 100644
--- a/Tests/prelude.mjs
+++ b/Tests/prelude.mjs
@@ -4,15 +4,73 @@ export function setupOptions(options, context) {
     setupTestGlobals(globalThis);
     return {
         ...options,
-        addToCoreImports(importObject) {
-            options.addToCoreImports?.(importObject);
+        addToCoreImports(importObject, importsContext) {
+            const { getInstance, getExports } = importsContext;
+            options.addToCoreImports?.(importObject, importsContext);
             importObject["JavaScriptEventLoopTestSupportTests"] = {
                 "isMainThread": () => context.isMainThread,
             }
+            importObject["BridgeJSRuntimeTests"] = {
+                "runJsWorks": () => {
+                    return BridgeJSRuntimeTests_runJsWorks(getInstance(), getExports());
+                },
+            }
         }
     }
 }
 
+import assert from "node:assert";
+
+/** @param {import('./../.build/plugins/PackageToJS/outputs/PackageTests/bridge.d.ts').Exports} exports */
+function BridgeJSRuntimeTests_runJsWorks(instance, exports) {
+    exports.roundTripVoid();
+    for (const v of [0, 1, -1, 2147483647, -2147483648]) {
+        assert.equal(exports.roundTripInt(v), v);
+    }
+    for (const v of [
+        0.0, 1.0, -1.0,
+        NaN,
+        Infinity,
+        /* .pi */ 3.141592502593994,
+        /* .greatestFiniteMagnitude */ 3.4028234663852886e+38,
+        /* .leastNonzeroMagnitude */ 1.401298464324817e-45
+    ]) {
+        assert.equal(exports.roundTripFloat(v), v);
+    }
+    for (const v of [
+        0.0, 1.0, -1.0,
+        NaN,
+        Infinity,
+        /* .pi */ 3.141592502593994,
+        /* .greatestFiniteMagnitude */ 3.4028234663852886e+38,
+        /* .leastNonzeroMagnitude */ 1.401298464324817e-45
+    ]) {
+        assert.equal(exports.roundTripDouble(v), v);
+    }
+    for (const v of [true, false]) {
+        assert.equal(exports.roundTripBool(v), v);
+    }
+    for (const v of [
+        "Hello, world!",
+        "😄",
+        "こんにちは",
+        "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.",
+    ]) {
+        assert.equal(exports.roundTripString(v), v);
+    }
+
+    const g = new exports.Greeter("John");
+    const g2 = exports.roundTripSwiftHeapObject(g)
+    g2.release();
+
+    assert.equal(g.greet(), "Hello, John!");
+    g.changeName("Jane");
+    assert.equal(g.greet(), "Hello, Jane!");
+    exports.takeGreeter(g, "Jay");
+    assert.equal(g.greet(), "Hello, Jay!");
+    g.release();
+}
+
 function setupTestGlobals(global) {
     global.globalObject1 = {
         prop_1: {
diff --git a/Utilities/format.swift b/Utilities/format.swift
new file mode 100755
index 000000000..9df282ad7
--- /dev/null
+++ b/Utilities/format.swift
@@ -0,0 +1,101 @@
+#!/usr/bin/env swift
+
+import class Foundation.FileManager
+import class Foundation.Process
+import class Foundation.ProcessInfo
+import struct Foundation.URL
+import func Foundation.exit
+
+/// The root directory of the project.
+let projectRoot = URL(fileURLWithPath: #filePath).deletingLastPathComponent().deletingLastPathComponent()
+
+/// Returns the path to the executable if it is found in the PATH environment variable.
+func which(_ executable: String) -> URL? {
+    do {
+        // Check overriding environment variable
+        let envVariable = executable.uppercased().replacingOccurrences(of: "-", with: "_") + "_PATH"
+        if let path = ProcessInfo.processInfo.environment[envVariable] {
+            if FileManager.default.isExecutableFile(atPath: path) {
+                return URL(fileURLWithPath: path)
+            }
+        }
+    }
+    let pathSeparator: Character
+    #if os(Windows)
+    pathSeparator = ";"
+    #else
+    pathSeparator = ":"
+    #endif
+    let paths = ProcessInfo.processInfo.environment["PATH"]!.split(separator: pathSeparator)
+    for path in paths {
+        let url = URL(fileURLWithPath: String(path)).appendingPathComponent(executable)
+        if FileManager.default.isExecutableFile(atPath: url.path) {
+            return url
+        }
+    }
+    return nil
+}
+
+/// Runs the `swift-format` command with the given arguments in the project root.
+func swiftFormat(_ arguments: [String]) throws {
+    guard let swiftFormat = which("swift-format") else {
+        print("swift-format not found in PATH")
+        exit(1)
+    }
+    print("[Utilities/format.swift] Running \(swiftFormat.path)")
+    let task = Process()
+    task.executableURL = swiftFormat
+    task.arguments = arguments
+    task.currentDirectoryURL = projectRoot
+    try task.run()
+    task.waitUntilExit()
+    if task.terminationStatus != 0 {
+        print("swift-format failed with status \(task.terminationStatus)")
+        exit(1)
+    }
+    print("[Utilities/format.swift] Done")
+}
+
+/// Patterns to exclude from formatting.
+let excluded: Set = [
+    ".git",
+    ".build",
+    ".index-build",
+    "node_modules",
+    "__Snapshots__",
+    "Generated",
+    // Exclude the script itself to avoid changing its file mode.
+    URL(fileURLWithPath: #filePath).lastPathComponent,
+]
+
+/// Returns a list of file paths to format.
+func filesToFormat() -> [String] {
+    var files: [String] = []
+    let fileManager = FileManager.default
+    let enumerator = fileManager.enumerator(
+        at: projectRoot, includingPropertiesForKeys: nil
+    )!
+    for case let fileURL as URL in enumerator {
+        if excluded.contains(fileURL.lastPathComponent) {
+            if fileURL.hasDirectoryPath {
+                enumerator.skipDescendants()
+            }
+            continue
+        }
+        guard fileURL.pathExtension == "swift" else { continue }
+        files.append(fileURL.path)
+    }
+    return files
+}
+
+let arguments = CommandLine.arguments[1...]
+switch arguments.first {
+case "lint":
+    try swiftFormat(["lint", "--parallel", "--recursive"] + filesToFormat())
+case "format", nil:
+    try swiftFormat(["format", "--parallel", "--in-place", "--recursive"] + filesToFormat())
+case let subcommand?:
+    print("Unknown subcommand: \(subcommand)")
+    print("Usage: format.swift lint|format")
+    exit(1)
+}
diff --git a/ci/perf-tester/package-lock.json b/ci/perf-tester/package-lock.json
deleted file mode 100644
index 82918bd59..000000000
--- a/ci/perf-tester/package-lock.json
+++ /dev/null
@@ -1,924 +0,0 @@
-{
-  "name": "perf-tester",
-  "lockfileVersion": 2,
-  "requires": true,
-  "packages": {
-    "": {
-      "devDependencies": {
-        "@actions/core": "^1.9.1",
-        "@actions/exec": "^1.0.3",
-        "@actions/github": "^2.0.1"
-      }
-    },
-    "node_modules/@actions/core": {
-      "version": "1.9.1",
-      "resolved": "https://registry.npmjs.org/@actions/core/-/core-1.9.1.tgz",
-      "integrity": "sha512-5ad+U2YGrmmiw6du20AQW5XuWo7UKN2052FjSV7MX+Wfjf8sCqcsZe62NfgHys4QI4/Y+vQvLKYL8jWtA1ZBTA==",
-      "dev": true,
-      "dependencies": {
-        "@actions/http-client": "^2.0.1",
-        "uuid": "^8.3.2"
-      }
-    },
-    "node_modules/@actions/exec": {
-      "version": "1.0.3",
-      "resolved": "https://registry.npmjs.org/@actions/exec/-/exec-1.0.3.tgz",
-      "integrity": "sha512-TogJGnueOmM7ntCi0ASTUj4LapRRtDfj57Ja4IhPmg2fls28uVOPbAn8N+JifaOumN2UG3oEO/Ixek2A4NcYSA==",
-      "dev": true,
-      "dependencies": {
-        "@actions/io": "^1.0.1"
-      }
-    },
-    "node_modules/@actions/github": {
-      "version": "2.0.1",
-      "resolved": "https://registry.npmjs.org/@actions/github/-/github-2.0.1.tgz",
-      "integrity": "sha512-C7dAsCkpPi1HxTzLldz+oY+9c5G+nnaK7xgk8KA83VVGlrGK7d603E3snUAFocWrqEu/uvdYD82ytggjcpYSQA==",
-      "dev": true,
-      "dependencies": {
-        "@octokit/graphql": "^4.3.1",
-        "@octokit/rest": "^16.15.0"
-      }
-    },
-    "node_modules/@actions/http-client": {
-      "version": "2.0.1",
-      "resolved": "https://registry.npmjs.org/@actions/http-client/-/http-client-2.0.1.tgz",
-      "integrity": "sha512-PIXiMVtz6VvyaRsGY268qvj57hXQEpsYogYOu2nrQhlf+XCGmZstmuZBbAybUl1nQGnvS1k1eEsQ69ZoD7xlSw==",
-      "dev": true,
-      "dependencies": {
-        "tunnel": "^0.0.6"
-      }
-    },
-    "node_modules/@actions/io": {
-      "version": "1.0.2",
-      "resolved": "https://registry.npmjs.org/@actions/io/-/io-1.0.2.tgz",
-      "integrity": "sha512-J8KuFqVPr3p6U8W93DOXlXW6zFvrQAJANdS+vw0YhusLIq+bszW8zmK2Fh1C2kDPX8FMvwIl1OUcFgvJoXLbAg==",
-      "dev": true
-    },
-    "node_modules/@octokit/endpoint": {
-      "version": "5.5.1",
-      "resolved": "https://registry.npmjs.org/@octokit/endpoint/-/endpoint-5.5.1.tgz",
-      "integrity": "sha512-nBFhRUb5YzVTCX/iAK1MgQ4uWo89Gu0TH00qQHoYRCsE12dWcG1OiLd7v2EIo2+tpUKPMOQ62QFy9hy9Vg2ULg==",
-      "dev": true,
-      "dependencies": {
-        "@octokit/types": "^2.0.0",
-        "is-plain-object": "^3.0.0",
-        "universal-user-agent": "^4.0.0"
-      }
-    },
-    "node_modules/@octokit/graphql": {
-      "version": "4.3.1",
-      "resolved": "https://registry.npmjs.org/@octokit/graphql/-/graphql-4.3.1.tgz",
-      "integrity": "sha512-hCdTjfvrK+ilU2keAdqNBWOk+gm1kai1ZcdjRfB30oA3/T6n53UVJb7w0L5cR3/rhU91xT3HSqCd+qbvH06yxA==",
-      "dev": true,
-      "dependencies": {
-        "@octokit/request": "^5.3.0",
-        "@octokit/types": "^2.0.0",
-        "universal-user-agent": "^4.0.0"
-      }
-    },
-    "node_modules/@octokit/request": {
-      "version": "5.3.1",
-      "resolved": "https://registry.npmjs.org/@octokit/request/-/request-5.3.1.tgz",
-      "integrity": "sha512-5/X0AL1ZgoU32fAepTfEoggFinO3rxsMLtzhlUX+RctLrusn/CApJuGFCd0v7GMFhF+8UiCsTTfsu7Fh1HnEJg==",
-      "dev": true,
-      "dependencies": {
-        "@octokit/endpoint": "^5.5.0",
-        "@octokit/request-error": "^1.0.1",
-        "@octokit/types": "^2.0.0",
-        "deprecation": "^2.0.0",
-        "is-plain-object": "^3.0.0",
-        "node-fetch": "^2.3.0",
-        "once": "^1.4.0",
-        "universal-user-agent": "^4.0.0"
-      }
-    },
-    "node_modules/@octokit/request-error": {
-      "version": "1.2.0",
-      "resolved": "https://registry.npmjs.org/@octokit/request-error/-/request-error-1.2.0.tgz",
-      "integrity": "sha512-DNBhROBYjjV/I9n7A8kVkmQNkqFAMem90dSxqvPq57e2hBr7mNTX98y3R2zDpqMQHVRpBDjsvsfIGgBzy+4PAg==",
-      "dev": true,
-      "dependencies": {
-        "@octokit/types": "^2.0.0",
-        "deprecation": "^2.0.0",
-        "once": "^1.4.0"
-      }
-    },
-    "node_modules/@octokit/rest": {
-      "version": "16.37.0",
-      "resolved": "https://registry.npmjs.org/@octokit/rest/-/rest-16.37.0.tgz",
-      "integrity": "sha512-qLPK9FOCK4iVpn6ghknNuv/gDDxXQG6+JBQvoCwWjQESyis9uemakjzN36nvvp8SCny7JuzHI2RV8ChbV5mYdQ==",
-      "dev": true,
-      "dependencies": {
-        "@octokit/request": "^5.2.0",
-        "@octokit/request-error": "^1.0.2",
-        "atob-lite": "^2.0.0",
-        "before-after-hook": "^2.0.0",
-        "btoa-lite": "^1.0.0",
-        "deprecation": "^2.0.0",
-        "lodash.get": "^4.4.2",
-        "lodash.set": "^4.3.2",
-        "lodash.uniq": "^4.5.0",
-        "octokit-pagination-methods": "^1.1.0",
-        "once": "^1.4.0",
-        "universal-user-agent": "^4.0.0"
-      }
-    },
-    "node_modules/@octokit/types": {
-      "version": "2.1.0",
-      "resolved": "https://registry.npmjs.org/@octokit/types/-/types-2.1.0.tgz",
-      "integrity": "sha512-n1GUYFgKm5glcy0E+U5jnqAFY2p04rnK4A0YhuM70C7Vm9Vyx+xYwd/WOTEr8nUJcbPSR/XL+/26+rirY6jJQA==",
-      "dev": true,
-      "dependencies": {
-        "@types/node": ">= 8"
-      }
-    },
-    "node_modules/@types/node": {
-      "version": "13.1.8",
-      "resolved": "https://registry.npmjs.org/@types/node/-/node-13.1.8.tgz",
-      "integrity": "sha512-6XzyyNM9EKQW4HKuzbo/CkOIjn/evtCmsU+MUM1xDfJ+3/rNjBttM1NgN7AOQvN6tP1Sl1D1PIKMreTArnxM9A==",
-      "dev": true
-    },
-    "node_modules/atob-lite": {
-      "version": "2.0.0",
-      "resolved": "https://registry.npmjs.org/atob-lite/-/atob-lite-2.0.0.tgz",
-      "integrity": "sha1-D+9a1G8b16hQLGVyfwNn1e5D1pY=",
-      "dev": true
-    },
-    "node_modules/before-after-hook": {
-      "version": "2.1.0",
-      "resolved": "https://registry.npmjs.org/before-after-hook/-/before-after-hook-2.1.0.tgz",
-      "integrity": "sha512-IWIbu7pMqyw3EAJHzzHbWa85b6oud/yfKYg5rqB5hNE8CeMi3nX+2C2sj0HswfblST86hpVEOAb9x34NZd6P7A==",
-      "dev": true
-    },
-    "node_modules/btoa-lite": {
-      "version": "1.0.0",
-      "resolved": "https://registry.npmjs.org/btoa-lite/-/btoa-lite-1.0.0.tgz",
-      "integrity": "sha1-M3dm2hWAEhD92VbCLpxokaudAzc=",
-      "dev": true
-    },
-    "node_modules/cross-spawn": {
-      "version": "6.0.5",
-      "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.5.tgz",
-      "integrity": "sha512-eTVLrBSt7fjbDygz805pMnstIs2VTBNkRm0qxZd+M7A5XDdxVRWO5MxGBXZhjY4cqLYLdtrGqRf8mBPmzwSpWQ==",
-      "dev": true,
-      "dependencies": {
-        "nice-try": "^1.0.4",
-        "path-key": "^2.0.1",
-        "semver": "^5.5.0",
-        "shebang-command": "^1.2.0",
-        "which": "^1.2.9"
-      },
-      "engines": {
-        "node": ">=4.8"
-      }
-    },
-    "node_modules/deprecation": {
-      "version": "2.3.1",
-      "resolved": "https://registry.npmjs.org/deprecation/-/deprecation-2.3.1.tgz",
-      "integrity": "sha512-xmHIy4F3scKVwMsQ4WnVaS8bHOx0DmVwRywosKhaILI0ywMDWPtBSku2HNxRvF7jtwDRsoEwYQSfbxj8b7RlJQ==",
-      "dev": true
-    },
-    "node_modules/end-of-stream": {
-      "version": "1.4.4",
-      "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz",
-      "integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==",
-      "dev": true,
-      "dependencies": {
-        "once": "^1.4.0"
-      }
-    },
-    "node_modules/execa": {
-      "version": "1.0.0",
-      "resolved": "https://registry.npmjs.org/execa/-/execa-1.0.0.tgz",
-      "integrity": "sha512-adbxcyWV46qiHyvSp50TKt05tB4tK3HcmF7/nxfAdhnox83seTDbwnaqKO4sXRy7roHAIFqJP/Rw/AuEbX61LA==",
-      "dev": true,
-      "dependencies": {
-        "cross-spawn": "^6.0.0",
-        "get-stream": "^4.0.0",
-        "is-stream": "^1.1.0",
-        "npm-run-path": "^2.0.0",
-        "p-finally": "^1.0.0",
-        "signal-exit": "^3.0.0",
-        "strip-eof": "^1.0.0"
-      },
-      "engines": {
-        "node": ">=6"
-      }
-    },
-    "node_modules/get-stream": {
-      "version": "4.1.0",
-      "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-4.1.0.tgz",
-      "integrity": "sha512-GMat4EJ5161kIy2HevLlr4luNjBgvmj413KaQA7jt4V8B4RDsfpHk7WQ9GVqfYyyx8OS/L66Kox+rJRNklLK7w==",
-      "dev": true,
-      "dependencies": {
-        "pump": "^3.0.0"
-      },
-      "engines": {
-        "node": ">=6"
-      }
-    },
-    "node_modules/is-plain-object": {
-      "version": "3.0.0",
-      "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-3.0.0.tgz",
-      "integrity": "sha512-tZIpofR+P05k8Aocp7UI/2UTa9lTJSebCXpFFoR9aibpokDj/uXBsJ8luUu0tTVYKkMU6URDUuOfJZ7koewXvg==",
-      "dev": true,
-      "dependencies": {
-        "isobject": "^4.0.0"
-      },
-      "engines": {
-        "node": ">=0.10.0"
-      }
-    },
-    "node_modules/is-stream": {
-      "version": "1.1.0",
-      "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-1.1.0.tgz",
-      "integrity": "sha1-EtSj3U5o4Lec6428hBc66A2RykQ=",
-      "dev": true,
-      "engines": {
-        "node": ">=0.10.0"
-      }
-    },
-    "node_modules/isexe": {
-      "version": "2.0.0",
-      "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
-      "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=",
-      "dev": true
-    },
-    "node_modules/isobject": {
-      "version": "4.0.0",
-      "resolved": "https://registry.npmjs.org/isobject/-/isobject-4.0.0.tgz",
-      "integrity": "sha512-S/2fF5wH8SJA/kmwr6HYhK/RI/OkhD84k8ntalo0iJjZikgq1XFvR5M8NPT1x5F7fBwCG3qHfnzeP/Vh/ZxCUA==",
-      "dev": true,
-      "engines": {
-        "node": ">=0.10.0"
-      }
-    },
-    "node_modules/lodash.get": {
-      "version": "4.4.2",
-      "resolved": "https://registry.npmjs.org/lodash.get/-/lodash.get-4.4.2.tgz",
-      "integrity": "sha1-LRd/ZS+jHpObRDjVNBSZ36OCXpk=",
-      "dev": true
-    },
-    "node_modules/lodash.set": {
-      "version": "4.3.2",
-      "resolved": "https://registry.npmjs.org/lodash.set/-/lodash.set-4.3.2.tgz",
-      "integrity": "sha1-2HV7HagH3eJIFrDWqEvqGnYjCyM=",
-      "dev": true
-    },
-    "node_modules/lodash.uniq": {
-      "version": "4.5.0",
-      "resolved": "https://registry.npmjs.org/lodash.uniq/-/lodash.uniq-4.5.0.tgz",
-      "integrity": "sha1-0CJTc662Uq3BvILklFM5qEJ1R3M=",
-      "dev": true
-    },
-    "node_modules/macos-release": {
-      "version": "2.3.0",
-      "resolved": "https://registry.npmjs.org/macos-release/-/macos-release-2.3.0.tgz",
-      "integrity": "sha512-OHhSbtcviqMPt7yfw5ef5aghS2jzFVKEFyCJndQt2YpSQ9qRVSEv2axSJI1paVThEu+FFGs584h/1YhxjVqajA==",
-      "dev": true,
-      "engines": {
-        "node": ">=6"
-      }
-    },
-    "node_modules/nice-try": {
-      "version": "1.0.5",
-      "resolved": "https://registry.npmjs.org/nice-try/-/nice-try-1.0.5.tgz",
-      "integrity": "sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==",
-      "dev": true
-    },
-    "node_modules/node-fetch": {
-      "version": "2.6.7",
-      "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.7.tgz",
-      "integrity": "sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ==",
-      "dev": true,
-      "dependencies": {
-        "whatwg-url": "^5.0.0"
-      },
-      "engines": {
-        "node": "4.x || >=6.0.0"
-      },
-      "peerDependencies": {
-        "encoding": "^0.1.0"
-      },
-      "peerDependenciesMeta": {
-        "encoding": {
-          "optional": true
-        }
-      }
-    },
-    "node_modules/npm-run-path": {
-      "version": "2.0.2",
-      "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-2.0.2.tgz",
-      "integrity": "sha1-NakjLfo11wZ7TLLd8jV7GHFTbF8=",
-      "dev": true,
-      "dependencies": {
-        "path-key": "^2.0.0"
-      },
-      "engines": {
-        "node": ">=4"
-      }
-    },
-    "node_modules/octokit-pagination-methods": {
-      "version": "1.1.0",
-      "resolved": "https://registry.npmjs.org/octokit-pagination-methods/-/octokit-pagination-methods-1.1.0.tgz",
-      "integrity": "sha512-fZ4qZdQ2nxJvtcasX7Ghl+WlWS/d9IgnBIwFZXVNNZUmzpno91SX5bc5vuxiuKoCtK78XxGGNuSCrDC7xYB3OQ==",
-      "dev": true
-    },
-    "node_modules/once": {
-      "version": "1.4.0",
-      "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
-      "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=",
-      "dev": true,
-      "dependencies": {
-        "wrappy": "1"
-      }
-    },
-    "node_modules/os-name": {
-      "version": "3.1.0",
-      "resolved": "https://registry.npmjs.org/os-name/-/os-name-3.1.0.tgz",
-      "integrity": "sha512-h8L+8aNjNcMpo/mAIBPn5PXCM16iyPGjHNWo6U1YO8sJTMHtEtyczI6QJnLoplswm6goopQkqc7OAnjhWcugVg==",
-      "dev": true,
-      "dependencies": {
-        "macos-release": "^2.2.0",
-        "windows-release": "^3.1.0"
-      },
-      "engines": {
-        "node": ">=6"
-      }
-    },
-    "node_modules/p-finally": {
-      "version": "1.0.0",
-      "resolved": "https://registry.npmjs.org/p-finally/-/p-finally-1.0.0.tgz",
-      "integrity": "sha1-P7z7FbiZpEEjs0ttzBi3JDNqLK4=",
-      "dev": true,
-      "engines": {
-        "node": ">=4"
-      }
-    },
-    "node_modules/path-key": {
-      "version": "2.0.1",
-      "resolved": "https://registry.npmjs.org/path-key/-/path-key-2.0.1.tgz",
-      "integrity": "sha1-QRyttXTFoUDTpLGRDUDYDMn0C0A=",
-      "dev": true,
-      "engines": {
-        "node": ">=4"
-      }
-    },
-    "node_modules/pump": {
-      "version": "3.0.0",
-      "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz",
-      "integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==",
-      "dev": true,
-      "dependencies": {
-        "end-of-stream": "^1.1.0",
-        "once": "^1.3.1"
-      }
-    },
-    "node_modules/semver": {
-      "version": "5.7.1",
-      "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz",
-      "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==",
-      "dev": true,
-      "bin": {
-        "semver": "bin/semver"
-      }
-    },
-    "node_modules/shebang-command": {
-      "version": "1.2.0",
-      "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-1.2.0.tgz",
-      "integrity": "sha1-RKrGW2lbAzmJaMOfNj/uXer98eo=",
-      "dev": true,
-      "dependencies": {
-        "shebang-regex": "^1.0.0"
-      },
-      "engines": {
-        "node": ">=0.10.0"
-      }
-    },
-    "node_modules/shebang-regex": {
-      "version": "1.0.0",
-      "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-1.0.0.tgz",
-      "integrity": "sha1-2kL0l0DAtC2yypcoVxyxkMmO/qM=",
-      "dev": true,
-      "engines": {
-        "node": ">=0.10.0"
-      }
-    },
-    "node_modules/signal-exit": {
-      "version": "3.0.2",
-      "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.2.tgz",
-      "integrity": "sha1-tf3AjxKH6hF4Yo5BXiUTK3NkbG0=",
-      "dev": true
-    },
-    "node_modules/strip-eof": {
-      "version": "1.0.0",
-      "resolved": "https://registry.npmjs.org/strip-eof/-/strip-eof-1.0.0.tgz",
-      "integrity": "sha1-u0P/VZim6wXYm1n80SnJgzE2Br8=",
-      "dev": true,
-      "engines": {
-        "node": ">=0.10.0"
-      }
-    },
-    "node_modules/tr46": {
-      "version": "0.0.3",
-      "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz",
-      "integrity": "sha1-gYT9NH2snNwYWZLzpmIuFLnZq2o=",
-      "dev": true
-    },
-    "node_modules/tunnel": {
-      "version": "0.0.6",
-      "resolved": "https://registry.npmjs.org/tunnel/-/tunnel-0.0.6.tgz",
-      "integrity": "sha512-1h/Lnq9yajKY2PEbBadPXj3VxsDDu844OnaAo52UVmIzIvwwtBPIuNvkjuzBlTWpfJyUbG3ez0KSBibQkj4ojg==",
-      "dev": true,
-      "engines": {
-        "node": ">=0.6.11 <=0.7.0 || >=0.7.3"
-      }
-    },
-    "node_modules/universal-user-agent": {
-      "version": "4.0.0",
-      "resolved": "https://registry.npmjs.org/universal-user-agent/-/universal-user-agent-4.0.0.tgz",
-      "integrity": "sha512-eM8knLpev67iBDizr/YtqkJsF3GK8gzDc6st/WKzrTuPtcsOKW/0IdL4cnMBsU69pOx0otavLWBDGTwg+dB0aA==",
-      "dev": true,
-      "dependencies": {
-        "os-name": "^3.1.0"
-      }
-    },
-    "node_modules/uuid": {
-      "version": "8.3.2",
-      "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz",
-      "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==",
-      "dev": true,
-      "bin": {
-        "uuid": "dist/bin/uuid"
-      }
-    },
-    "node_modules/webidl-conversions": {
-      "version": "3.0.1",
-      "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz",
-      "integrity": "sha1-JFNCdeKnvGvnvIZhHMFq4KVlSHE=",
-      "dev": true
-    },
-    "node_modules/whatwg-url": {
-      "version": "5.0.0",
-      "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz",
-      "integrity": "sha1-lmRU6HZUYuN2RNNib2dCzotwll0=",
-      "dev": true,
-      "dependencies": {
-        "tr46": "~0.0.3",
-        "webidl-conversions": "^3.0.0"
-      }
-    },
-    "node_modules/which": {
-      "version": "1.3.1",
-      "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz",
-      "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==",
-      "dev": true,
-      "dependencies": {
-        "isexe": "^2.0.0"
-      },
-      "bin": {
-        "which": "bin/which"
-      }
-    },
-    "node_modules/windows-release": {
-      "version": "3.2.0",
-      "resolved": "https://registry.npmjs.org/windows-release/-/windows-release-3.2.0.tgz",
-      "integrity": "sha512-QTlz2hKLrdqukrsapKsINzqMgOUpQW268eJ0OaOpJN32h272waxR9fkB9VoWRtK7uKHG5EHJcTXQBD8XZVJkFA==",
-      "dev": true,
-      "dependencies": {
-        "execa": "^1.0.0"
-      },
-      "engines": {
-        "node": ">=6"
-      }
-    },
-    "node_modules/wrappy": {
-      "version": "1.0.2",
-      "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
-      "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=",
-      "dev": true
-    }
-  },
-  "dependencies": {
-    "@actions/core": {
-      "version": "1.9.1",
-      "resolved": "https://registry.npmjs.org/@actions/core/-/core-1.9.1.tgz",
-      "integrity": "sha512-5ad+U2YGrmmiw6du20AQW5XuWo7UKN2052FjSV7MX+Wfjf8sCqcsZe62NfgHys4QI4/Y+vQvLKYL8jWtA1ZBTA==",
-      "dev": true,
-      "requires": {
-        "@actions/http-client": "^2.0.1",
-        "uuid": "^8.3.2"
-      }
-    },
-    "@actions/exec": {
-      "version": "1.0.3",
-      "resolved": "https://registry.npmjs.org/@actions/exec/-/exec-1.0.3.tgz",
-      "integrity": "sha512-TogJGnueOmM7ntCi0ASTUj4LapRRtDfj57Ja4IhPmg2fls28uVOPbAn8N+JifaOumN2UG3oEO/Ixek2A4NcYSA==",
-      "dev": true,
-      "requires": {
-        "@actions/io": "^1.0.1"
-      }
-    },
-    "@actions/github": {
-      "version": "2.0.1",
-      "resolved": "https://registry.npmjs.org/@actions/github/-/github-2.0.1.tgz",
-      "integrity": "sha512-C7dAsCkpPi1HxTzLldz+oY+9c5G+nnaK7xgk8KA83VVGlrGK7d603E3snUAFocWrqEu/uvdYD82ytggjcpYSQA==",
-      "dev": true,
-      "requires": {
-        "@octokit/graphql": "^4.3.1",
-        "@octokit/rest": "^16.15.0"
-      }
-    },
-    "@actions/http-client": {
-      "version": "2.0.1",
-      "resolved": "https://registry.npmjs.org/@actions/http-client/-/http-client-2.0.1.tgz",
-      "integrity": "sha512-PIXiMVtz6VvyaRsGY268qvj57hXQEpsYogYOu2nrQhlf+XCGmZstmuZBbAybUl1nQGnvS1k1eEsQ69ZoD7xlSw==",
-      "dev": true,
-      "requires": {
-        "tunnel": "^0.0.6"
-      }
-    },
-    "@actions/io": {
-      "version": "1.0.2",
-      "resolved": "https://registry.npmjs.org/@actions/io/-/io-1.0.2.tgz",
-      "integrity": "sha512-J8KuFqVPr3p6U8W93DOXlXW6zFvrQAJANdS+vw0YhusLIq+bszW8zmK2Fh1C2kDPX8FMvwIl1OUcFgvJoXLbAg==",
-      "dev": true
-    },
-    "@octokit/endpoint": {
-      "version": "5.5.1",
-      "resolved": "https://registry.npmjs.org/@octokit/endpoint/-/endpoint-5.5.1.tgz",
-      "integrity": "sha512-nBFhRUb5YzVTCX/iAK1MgQ4uWo89Gu0TH00qQHoYRCsE12dWcG1OiLd7v2EIo2+tpUKPMOQ62QFy9hy9Vg2ULg==",
-      "dev": true,
-      "requires": {
-        "@octokit/types": "^2.0.0",
-        "is-plain-object": "^3.0.0",
-        "universal-user-agent": "^4.0.0"
-      }
-    },
-    "@octokit/graphql": {
-      "version": "4.3.1",
-      "resolved": "https://registry.npmjs.org/@octokit/graphql/-/graphql-4.3.1.tgz",
-      "integrity": "sha512-hCdTjfvrK+ilU2keAdqNBWOk+gm1kai1ZcdjRfB30oA3/T6n53UVJb7w0L5cR3/rhU91xT3HSqCd+qbvH06yxA==",
-      "dev": true,
-      "requires": {
-        "@octokit/request": "^5.3.0",
-        "@octokit/types": "^2.0.0",
-        "universal-user-agent": "^4.0.0"
-      }
-    },
-    "@octokit/request": {
-      "version": "5.3.1",
-      "resolved": "https://registry.npmjs.org/@octokit/request/-/request-5.3.1.tgz",
-      "integrity": "sha512-5/X0AL1ZgoU32fAepTfEoggFinO3rxsMLtzhlUX+RctLrusn/CApJuGFCd0v7GMFhF+8UiCsTTfsu7Fh1HnEJg==",
-      "dev": true,
-      "requires": {
-        "@octokit/endpoint": "^5.5.0",
-        "@octokit/request-error": "^1.0.1",
-        "@octokit/types": "^2.0.0",
-        "deprecation": "^2.0.0",
-        "is-plain-object": "^3.0.0",
-        "node-fetch": "^2.3.0",
-        "once": "^1.4.0",
-        "universal-user-agent": "^4.0.0"
-      }
-    },
-    "@octokit/request-error": {
-      "version": "1.2.0",
-      "resolved": "https://registry.npmjs.org/@octokit/request-error/-/request-error-1.2.0.tgz",
-      "integrity": "sha512-DNBhROBYjjV/I9n7A8kVkmQNkqFAMem90dSxqvPq57e2hBr7mNTX98y3R2zDpqMQHVRpBDjsvsfIGgBzy+4PAg==",
-      "dev": true,
-      "requires": {
-        "@octokit/types": "^2.0.0",
-        "deprecation": "^2.0.0",
-        "once": "^1.4.0"
-      }
-    },
-    "@octokit/rest": {
-      "version": "16.37.0",
-      "resolved": "https://registry.npmjs.org/@octokit/rest/-/rest-16.37.0.tgz",
-      "integrity": "sha512-qLPK9FOCK4iVpn6ghknNuv/gDDxXQG6+JBQvoCwWjQESyis9uemakjzN36nvvp8SCny7JuzHI2RV8ChbV5mYdQ==",
-      "dev": true,
-      "requires": {
-        "@octokit/request": "^5.2.0",
-        "@octokit/request-error": "^1.0.2",
-        "atob-lite": "^2.0.0",
-        "before-after-hook": "^2.0.0",
-        "btoa-lite": "^1.0.0",
-        "deprecation": "^2.0.0",
-        "lodash.get": "^4.4.2",
-        "lodash.set": "^4.3.2",
-        "lodash.uniq": "^4.5.0",
-        "octokit-pagination-methods": "^1.1.0",
-        "once": "^1.4.0",
-        "universal-user-agent": "^4.0.0"
-      }
-    },
-    "@octokit/types": {
-      "version": "2.1.0",
-      "resolved": "https://registry.npmjs.org/@octokit/types/-/types-2.1.0.tgz",
-      "integrity": "sha512-n1GUYFgKm5glcy0E+U5jnqAFY2p04rnK4A0YhuM70C7Vm9Vyx+xYwd/WOTEr8nUJcbPSR/XL+/26+rirY6jJQA==",
-      "dev": true,
-      "requires": {
-        "@types/node": ">= 8"
-      }
-    },
-    "@types/node": {
-      "version": "13.1.8",
-      "resolved": "https://registry.npmjs.org/@types/node/-/node-13.1.8.tgz",
-      "integrity": "sha512-6XzyyNM9EKQW4HKuzbo/CkOIjn/evtCmsU+MUM1xDfJ+3/rNjBttM1NgN7AOQvN6tP1Sl1D1PIKMreTArnxM9A==",
-      "dev": true
-    },
-    "atob-lite": {
-      "version": "2.0.0",
-      "resolved": "https://registry.npmjs.org/atob-lite/-/atob-lite-2.0.0.tgz",
-      "integrity": "sha1-D+9a1G8b16hQLGVyfwNn1e5D1pY=",
-      "dev": true
-    },
-    "before-after-hook": {
-      "version": "2.1.0",
-      "resolved": "https://registry.npmjs.org/before-after-hook/-/before-after-hook-2.1.0.tgz",
-      "integrity": "sha512-IWIbu7pMqyw3EAJHzzHbWa85b6oud/yfKYg5rqB5hNE8CeMi3nX+2C2sj0HswfblST86hpVEOAb9x34NZd6P7A==",
-      "dev": true
-    },
-    "btoa-lite": {
-      "version": "1.0.0",
-      "resolved": "https://registry.npmjs.org/btoa-lite/-/btoa-lite-1.0.0.tgz",
-      "integrity": "sha1-M3dm2hWAEhD92VbCLpxokaudAzc=",
-      "dev": true
-    },
-    "cross-spawn": {
-      "version": "6.0.5",
-      "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.5.tgz",
-      "integrity": "sha512-eTVLrBSt7fjbDygz805pMnstIs2VTBNkRm0qxZd+M7A5XDdxVRWO5MxGBXZhjY4cqLYLdtrGqRf8mBPmzwSpWQ==",
-      "dev": true,
-      "requires": {
-        "nice-try": "^1.0.4",
-        "path-key": "^2.0.1",
-        "semver": "^5.5.0",
-        "shebang-command": "^1.2.0",
-        "which": "^1.2.9"
-      }
-    },
-    "deprecation": {
-      "version": "2.3.1",
-      "resolved": "https://registry.npmjs.org/deprecation/-/deprecation-2.3.1.tgz",
-      "integrity": "sha512-xmHIy4F3scKVwMsQ4WnVaS8bHOx0DmVwRywosKhaILI0ywMDWPtBSku2HNxRvF7jtwDRsoEwYQSfbxj8b7RlJQ==",
-      "dev": true
-    },
-    "end-of-stream": {
-      "version": "1.4.4",
-      "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz",
-      "integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==",
-      "dev": true,
-      "requires": {
-        "once": "^1.4.0"
-      }
-    },
-    "execa": {
-      "version": "1.0.0",
-      "resolved": "https://registry.npmjs.org/execa/-/execa-1.0.0.tgz",
-      "integrity": "sha512-adbxcyWV46qiHyvSp50TKt05tB4tK3HcmF7/nxfAdhnox83seTDbwnaqKO4sXRy7roHAIFqJP/Rw/AuEbX61LA==",
-      "dev": true,
-      "requires": {
-        "cross-spawn": "^6.0.0",
-        "get-stream": "^4.0.0",
-        "is-stream": "^1.1.0",
-        "npm-run-path": "^2.0.0",
-        "p-finally": "^1.0.0",
-        "signal-exit": "^3.0.0",
-        "strip-eof": "^1.0.0"
-      }
-    },
-    "get-stream": {
-      "version": "4.1.0",
-      "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-4.1.0.tgz",
-      "integrity": "sha512-GMat4EJ5161kIy2HevLlr4luNjBgvmj413KaQA7jt4V8B4RDsfpHk7WQ9GVqfYyyx8OS/L66Kox+rJRNklLK7w==",
-      "dev": true,
-      "requires": {
-        "pump": "^3.0.0"
-      }
-    },
-    "is-plain-object": {
-      "version": "3.0.0",
-      "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-3.0.0.tgz",
-      "integrity": "sha512-tZIpofR+P05k8Aocp7UI/2UTa9lTJSebCXpFFoR9aibpokDj/uXBsJ8luUu0tTVYKkMU6URDUuOfJZ7koewXvg==",
-      "dev": true,
-      "requires": {
-        "isobject": "^4.0.0"
-      }
-    },
-    "is-stream": {
-      "version": "1.1.0",
-      "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-1.1.0.tgz",
-      "integrity": "sha1-EtSj3U5o4Lec6428hBc66A2RykQ=",
-      "dev": true
-    },
-    "isexe": {
-      "version": "2.0.0",
-      "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
-      "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=",
-      "dev": true
-    },
-    "isobject": {
-      "version": "4.0.0",
-      "resolved": "https://registry.npmjs.org/isobject/-/isobject-4.0.0.tgz",
-      "integrity": "sha512-S/2fF5wH8SJA/kmwr6HYhK/RI/OkhD84k8ntalo0iJjZikgq1XFvR5M8NPT1x5F7fBwCG3qHfnzeP/Vh/ZxCUA==",
-      "dev": true
-    },
-    "lodash.get": {
-      "version": "4.4.2",
-      "resolved": "https://registry.npmjs.org/lodash.get/-/lodash.get-4.4.2.tgz",
-      "integrity": "sha1-LRd/ZS+jHpObRDjVNBSZ36OCXpk=",
-      "dev": true
-    },
-    "lodash.set": {
-      "version": "4.3.2",
-      "resolved": "https://registry.npmjs.org/lodash.set/-/lodash.set-4.3.2.tgz",
-      "integrity": "sha1-2HV7HagH3eJIFrDWqEvqGnYjCyM=",
-      "dev": true
-    },
-    "lodash.uniq": {
-      "version": "4.5.0",
-      "resolved": "https://registry.npmjs.org/lodash.uniq/-/lodash.uniq-4.5.0.tgz",
-      "integrity": "sha1-0CJTc662Uq3BvILklFM5qEJ1R3M=",
-      "dev": true
-    },
-    "macos-release": {
-      "version": "2.3.0",
-      "resolved": "https://registry.npmjs.org/macos-release/-/macos-release-2.3.0.tgz",
-      "integrity": "sha512-OHhSbtcviqMPt7yfw5ef5aghS2jzFVKEFyCJndQt2YpSQ9qRVSEv2axSJI1paVThEu+FFGs584h/1YhxjVqajA==",
-      "dev": true
-    },
-    "nice-try": {
-      "version": "1.0.5",
-      "resolved": "https://registry.npmjs.org/nice-try/-/nice-try-1.0.5.tgz",
-      "integrity": "sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==",
-      "dev": true
-    },
-    "node-fetch": {
-      "version": "2.6.7",
-      "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.7.tgz",
-      "integrity": "sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ==",
-      "dev": true,
-      "requires": {
-        "whatwg-url": "^5.0.0"
-      }
-    },
-    "npm-run-path": {
-      "version": "2.0.2",
-      "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-2.0.2.tgz",
-      "integrity": "sha1-NakjLfo11wZ7TLLd8jV7GHFTbF8=",
-      "dev": true,
-      "requires": {
-        "path-key": "^2.0.0"
-      }
-    },
-    "octokit-pagination-methods": {
-      "version": "1.1.0",
-      "resolved": "https://registry.npmjs.org/octokit-pagination-methods/-/octokit-pagination-methods-1.1.0.tgz",
-      "integrity": "sha512-fZ4qZdQ2nxJvtcasX7Ghl+WlWS/d9IgnBIwFZXVNNZUmzpno91SX5bc5vuxiuKoCtK78XxGGNuSCrDC7xYB3OQ==",
-      "dev": true
-    },
-    "once": {
-      "version": "1.4.0",
-      "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
-      "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=",
-      "dev": true,
-      "requires": {
-        "wrappy": "1"
-      }
-    },
-    "os-name": {
-      "version": "3.1.0",
-      "resolved": "https://registry.npmjs.org/os-name/-/os-name-3.1.0.tgz",
-      "integrity": "sha512-h8L+8aNjNcMpo/mAIBPn5PXCM16iyPGjHNWo6U1YO8sJTMHtEtyczI6QJnLoplswm6goopQkqc7OAnjhWcugVg==",
-      "dev": true,
-      "requires": {
-        "macos-release": "^2.2.0",
-        "windows-release": "^3.1.0"
-      }
-    },
-    "p-finally": {
-      "version": "1.0.0",
-      "resolved": "https://registry.npmjs.org/p-finally/-/p-finally-1.0.0.tgz",
-      "integrity": "sha1-P7z7FbiZpEEjs0ttzBi3JDNqLK4=",
-      "dev": true
-    },
-    "path-key": {
-      "version": "2.0.1",
-      "resolved": "https://registry.npmjs.org/path-key/-/path-key-2.0.1.tgz",
-      "integrity": "sha1-QRyttXTFoUDTpLGRDUDYDMn0C0A=",
-      "dev": true
-    },
-    "pump": {
-      "version": "3.0.0",
-      "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz",
-      "integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==",
-      "dev": true,
-      "requires": {
-        "end-of-stream": "^1.1.0",
-        "once": "^1.3.1"
-      }
-    },
-    "semver": {
-      "version": "5.7.1",
-      "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz",
-      "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==",
-      "dev": true
-    },
-    "shebang-command": {
-      "version": "1.2.0",
-      "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-1.2.0.tgz",
-      "integrity": "sha1-RKrGW2lbAzmJaMOfNj/uXer98eo=",
-      "dev": true,
-      "requires": {
-        "shebang-regex": "^1.0.0"
-      }
-    },
-    "shebang-regex": {
-      "version": "1.0.0",
-      "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-1.0.0.tgz",
-      "integrity": "sha1-2kL0l0DAtC2yypcoVxyxkMmO/qM=",
-      "dev": true
-    },
-    "signal-exit": {
-      "version": "3.0.2",
-      "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.2.tgz",
-      "integrity": "sha1-tf3AjxKH6hF4Yo5BXiUTK3NkbG0=",
-      "dev": true
-    },
-    "strip-eof": {
-      "version": "1.0.0",
-      "resolved": "https://registry.npmjs.org/strip-eof/-/strip-eof-1.0.0.tgz",
-      "integrity": "sha1-u0P/VZim6wXYm1n80SnJgzE2Br8=",
-      "dev": true
-    },
-    "tr46": {
-      "version": "0.0.3",
-      "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz",
-      "integrity": "sha1-gYT9NH2snNwYWZLzpmIuFLnZq2o=",
-      "dev": true
-    },
-    "tunnel": {
-      "version": "0.0.6",
-      "resolved": "https://registry.npmjs.org/tunnel/-/tunnel-0.0.6.tgz",
-      "integrity": "sha512-1h/Lnq9yajKY2PEbBadPXj3VxsDDu844OnaAo52UVmIzIvwwtBPIuNvkjuzBlTWpfJyUbG3ez0KSBibQkj4ojg==",
-      "dev": true
-    },
-    "universal-user-agent": {
-      "version": "4.0.0",
-      "resolved": "https://registry.npmjs.org/universal-user-agent/-/universal-user-agent-4.0.0.tgz",
-      "integrity": "sha512-eM8knLpev67iBDizr/YtqkJsF3GK8gzDc6st/WKzrTuPtcsOKW/0IdL4cnMBsU69pOx0otavLWBDGTwg+dB0aA==",
-      "dev": true,
-      "requires": {
-        "os-name": "^3.1.0"
-      }
-    },
-    "uuid": {
-      "version": "8.3.2",
-      "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz",
-      "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==",
-      "dev": true
-    },
-    "webidl-conversions": {
-      "version": "3.0.1",
-      "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz",
-      "integrity": "sha1-JFNCdeKnvGvnvIZhHMFq4KVlSHE=",
-      "dev": true
-    },
-    "whatwg-url": {
-      "version": "5.0.0",
-      "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz",
-      "integrity": "sha1-lmRU6HZUYuN2RNNib2dCzotwll0=",
-      "dev": true,
-      "requires": {
-        "tr46": "~0.0.3",
-        "webidl-conversions": "^3.0.0"
-      }
-    },
-    "which": {
-      "version": "1.3.1",
-      "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz",
-      "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==",
-      "dev": true,
-      "requires": {
-        "isexe": "^2.0.0"
-      }
-    },
-    "windows-release": {
-      "version": "3.2.0",
-      "resolved": "https://registry.npmjs.org/windows-release/-/windows-release-3.2.0.tgz",
-      "integrity": "sha512-QTlz2hKLrdqukrsapKsINzqMgOUpQW268eJ0OaOpJN32h272waxR9fkB9VoWRtK7uKHG5EHJcTXQBD8XZVJkFA==",
-      "dev": true,
-      "requires": {
-        "execa": "^1.0.0"
-      }
-    },
-    "wrappy": {
-      "version": "1.0.2",
-      "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
-      "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=",
-      "dev": true
-    }
-  }
-}
diff --git a/ci/perf-tester/package.json b/ci/perf-tester/package.json
deleted file mode 100644
index 7a00de44d..000000000
--- a/ci/perf-tester/package.json
+++ /dev/null
@@ -1,9 +0,0 @@
-{
-  "private": true,
-  "main": "src/index.js",
-  "devDependencies": {
-    "@actions/core": "^1.9.1",
-    "@actions/exec": "^1.0.3",
-    "@actions/github": "^2.0.1"
-  }
-}
diff --git a/ci/perf-tester/src/index.js b/ci/perf-tester/src/index.js
deleted file mode 100644
index 6dd4a5e61..000000000
--- a/ci/perf-tester/src/index.js
+++ /dev/null
@@ -1,212 +0,0 @@
-/*
-Adapted from preactjs/compressed-size-action, which is available under this license:
-
-MIT License
-Copyright (c) 2020 Preact
-Permission is hereby granted, free of charge, to any person obtaining a copy
-of this software and associated documentation files (the "Software"), to deal
-in the Software without restriction, including without limitation the rights
-to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
-copies of the Software, and to permit persons to whom the Software is
-furnished to do so, subject to the following conditions:
-The above copyright notice and this permission notice shall be included in all
-copies or substantial portions of the Software.
-THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
-IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
-FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
-AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
-LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
-OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
-SOFTWARE.
-*/
-
-const { setFailed, startGroup, endGroup, debug } = require("@actions/core");
-const { GitHub, context } = require("@actions/github");
-const { exec } = require("@actions/exec");
-const {
-    config,
-    runBenchmark,
-    averageBenchmarks,
-    toDiff,
-    diffTable,
-} = require("./utils.js");
-
-const benchmarkParallel = 4;
-const benchmarkSerial = 4;
-const runBenchmarks = async () => {
-    let results = [];
-    for (let i = 0; i < benchmarkSerial; i++) {
-        results = results.concat(
-            await Promise.all(Array(benchmarkParallel).fill().map(runBenchmark))
-        );
-    }
-    return averageBenchmarks(results);
-};
-
-const perfActionComment =
-    "";
-
-async function run(octokit, context) {
-    const { number: pull_number } = context.issue;
-
-    const pr = context.payload.pull_request;
-    try {
-        debug("pr" + JSON.stringify(pr, null, 2));
-    } catch (e) {}
-    if (!pr) {
-        throw Error(
-            'Could not retrieve PR information. Only "pull_request" triggered workflows are currently supported.'
-        );
-    }
-
-    console.log(
-        `PR #${pull_number} is targeted at ${pr.base.ref} (${pr.base.sha})`
-    );
-
-    startGroup(`[current] Build using '${config.buildScript}'`);
-    await exec(config.buildScript);
-    endGroup();
-
-    startGroup(`[current] Running benchmark`);
-    const newBenchmarks = await runBenchmarks();
-    endGroup();
-
-    startGroup(`[base] Checkout target branch`);
-    let baseRef;
-    try {
-        baseRef = context.payload.base.ref;
-        if (!baseRef)
-            throw Error("missing context.payload.pull_request.base.ref");
-        await exec(
-            `git fetch -n origin ${context.payload.pull_request.base.ref}`
-        );
-        console.log("successfully fetched base.ref");
-    } catch (e) {
-        console.log("fetching base.ref failed", e.message);
-        try {
-            await exec(`git fetch -n origin ${pr.base.sha}`);
-            console.log("successfully fetched base.sha");
-        } catch (e) {
-            console.log("fetching base.sha failed", e.message);
-            try {
-                await exec(`git fetch -n`);
-            } catch (e) {
-                console.log("fetch failed", e.message);
-            }
-        }
-    }
-
-    console.log("checking out and building base commit");
-    try {
-        if (!baseRef) throw Error("missing context.payload.base.ref");
-        await exec(`git reset --hard ${baseRef}`);
-    } catch (e) {
-        await exec(`git reset --hard ${pr.base.sha}`);
-    }
-    endGroup();
-
-    startGroup(`[base] Build using '${config.buildScript}'`);
-    await exec(config.buildScript);
-    endGroup();
-
-    startGroup(`[base] Running benchmark`);
-    const oldBenchmarks = await runBenchmarks();
-    endGroup();
-
-    const diff = toDiff(oldBenchmarks, newBenchmarks);
-
-    const markdownDiff = diffTable(diff, {
-        collapseUnchanged: true,
-        omitUnchanged: false,
-        showTotal: true,
-        minimumChangeThreshold: config.minimumChangeThreshold,
-    });
-
-    let outputRawMarkdown = false;
-
-    const commentInfo = {
-        ...context.repo,
-        issue_number: pull_number,
-    };
-
-    const comment = {
-        ...commentInfo,
-        body: markdownDiff + "\n\n" + perfActionComment,
-    };
-
-    startGroup(`Updating stats PR comment`);
-    let commentId;
-    try {
-        const comments = (await octokit.issues.listComments(commentInfo)).data;
-        for (let i = comments.length; i--; ) {
-            const c = comments[i];
-            if (c.user.type === "Bot" && c.body.includes(perfActionComment)) {
-                commentId = c.id;
-                break;
-            }
-        }
-    } catch (e) {
-        console.log("Error checking for previous comments: " + e.message);
-    }
-
-    if (commentId) {
-        console.log(`Updating previous comment #${commentId}`);
-        try {
-            await octokit.issues.updateComment({
-                ...context.repo,
-                comment_id: commentId,
-                body: comment.body,
-            });
-        } catch (e) {
-            console.log("Error editing previous comment: " + e.message);
-            commentId = null;
-        }
-    }
-
-    // no previous or edit failed
-    if (!commentId) {
-        console.log("Creating new comment");
-        try {
-            await octokit.issues.createComment(comment);
-        } catch (e) {
-            console.log(`Error creating comment: ${e.message}`);
-            console.log(`Submitting a PR review comment instead...`);
-            try {
-                const issue = context.issue || pr;
-                await octokit.pulls.createReview({
-                    owner: issue.owner,
-                    repo: issue.repo,
-                    pull_number: issue.number,
-                    event: "COMMENT",
-                    body: comment.body,
-                });
-            } catch (e) {
-                console.log("Error creating PR review.");
-                outputRawMarkdown = true;
-            }
-        }
-        endGroup();
-    }
-
-    if (outputRawMarkdown) {
-        console.log(
-            `
-			Error: performance-action was unable to comment on your PR.
-			This can happen for PR's originating from a fork without write permissions.
-			You can copy the size table directly into a comment using the markdown below:
-			\n\n${comment.body}\n\n
-		`.replace(/^(\t|  )+/gm, "")
-        );
-    }
-
-    console.log("All done!");
-}
-
-(async () => {
-    try {
-        const octokit = new GitHub(process.env.GITHUB_TOKEN);
-        await run(octokit, context);
-    } catch (e) {
-        setFailed(e.message);
-    }
-})();
diff --git a/ci/perf-tester/src/utils.js b/ci/perf-tester/src/utils.js
deleted file mode 100644
index c7ecd662b..000000000
--- a/ci/perf-tester/src/utils.js
+++ /dev/null
@@ -1,221 +0,0 @@
-/*
-Adapted from preactjs/compressed-size-action, which is available under this license:
-
-MIT License
-Copyright (c) 2020 Preact
-Permission is hereby granted, free of charge, to any person obtaining a copy
-of this software and associated documentation files (the "Software"), to deal
-in the Software without restriction, including without limitation the rights
-to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
-copies of the Software, and to permit persons to whom the Software is
-furnished to do so, subject to the following conditions:
-The above copyright notice and this permission notice shall be included in all
-copies or substantial portions of the Software.
-THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
-IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
-FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
-AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
-LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
-OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
-SOFTWARE.
-*/
-
-const { exec } = require("@actions/exec");
-
-const formatMS = (ms) =>
-    `${ms.toLocaleString("en-US", {
-        maximumFractionDigits: 0,
-    })}ms`;
-
-const config = {
-    buildScript: "make bootstrap benchmark_setup",
-    benchmark: "make -s run_benchmark",
-    minimumChangeThreshold: 5,
-};
-exports.config = config;
-
-exports.runBenchmark = async () => {
-    let benchmarkBuffers = [];
-    await exec(config.benchmark, [], {
-        listeners: {
-            stdout: (data) => benchmarkBuffers.push(data),
-        },
-    });
-    const output = Buffer.concat(benchmarkBuffers).toString("utf8");
-    return parse(output);
-};
-
-const firstLineRe = /^Running '(.+)' \.\.\.$/;
-const secondLineRe = /^done ([\d.]+) ms$/;
-
-function parse(benchmarkData) {
-    const lines = benchmarkData.trim().split("\n");
-    const benchmarks = Object.create(null);
-    for (let i = 0; i < lines.length - 1; i += 2) {
-        const [, name] = firstLineRe.exec(lines[i]);
-        const [, time] = secondLineRe.exec(lines[i + 1]);
-        benchmarks[name] = Math.round(parseFloat(time));
-    }
-    return benchmarks;
-}
-
-exports.averageBenchmarks = (benchmarks) => {
-    const result = Object.create(null);
-    for (const key of Object.keys(benchmarks[0])) {
-        result[key] =
-            benchmarks.reduce((acc, bench) => acc + bench[key], 0) /
-            benchmarks.length;
-    }
-    return result;
-};
-
-/**
- * @param {{[key: string]: number}} before
- * @param {{[key: string]: number}} after
- * @return {Diff[]}
- */
-exports.toDiff = (before, after) => {
-    const names = [...new Set([...Object.keys(before), ...Object.keys(after)])];
-    return names.map((name) => {
-        const timeBefore = before[name] || 0;
-        const timeAfter = after[name] || 0;
-        const delta = timeAfter - timeBefore;
-        return { name, time: timeAfter, delta };
-    });
-};
-
-/**
- * @param {number} delta
- * @param {number} difference
- */
-function getDeltaText(delta, difference) {
-    let deltaText = (delta > 0 ? "+" : "") + formatMS(delta);
-    if (delta && Math.abs(delta) > 1) {
-        deltaText += ` (${Math.abs(difference)}%)`;
-    }
-    return deltaText;
-}
-
-/**
- * @param {number} difference
- */
-function iconForDifference(difference) {
-    let icon = "";
-    if (difference >= 50) icon = "🆘";
-    else if (difference >= 20) icon = "🚨";
-    else if (difference >= 10) icon = "⚠️";
-    else if (difference >= 5) icon = "🔍";
-    else if (difference <= -50) icon = "🏆";
-    else if (difference <= -20) icon = "🎉";
-    else if (difference <= -10) icon = "👏";
-    else if (difference <= -5) icon = "✅";
-    return icon;
-}
-
-/**
- * Create a Markdown table from text rows
- * @param {string[]} rows
- */
-function markdownTable(rows) {
-    if (rows.length == 0) {
-        return "";
-    }
-
-    // Skip all empty columns
-    while (rows.every((columns) => !columns[columns.length - 1])) {
-        for (const columns of rows) {
-            columns.pop();
-        }
-    }
-
-    const [firstRow] = rows;
-    const columnLength = firstRow.length;
-    if (columnLength === 0) {
-        return "";
-    }
-
-    return [
-        // Header
-        ["Test name", "Duration", "Change", ""].slice(0, columnLength),
-        // Align
-        [":---", ":---:", ":---:", ":---:"].slice(0, columnLength),
-        // Body
-        ...rows,
-    ]
-        .map((columns) => `| ${columns.join(" | ")} |`)
-        .join("\n");
-}
-
-/**
- * @typedef {Object} Diff
- * @property {string} name
- * @property {number} time
- * @property {number} delta
- */
-
-/**
- * Create a Markdown table showing diff data
- * @param {Diff[]} tests
- * @param {object} options
- * @param {boolean} [options.showTotal]
- * @param {boolean} [options.collapseUnchanged]
- * @param {boolean} [options.omitUnchanged]
- * @param {number} [options.minimumChangeThreshold]
- */
-exports.diffTable = (
-    tests,
-    { showTotal, collapseUnchanged, omitUnchanged, minimumChangeThreshold }
-) => {
-    let changedRows = [];
-    let unChangedRows = [];
-    let baselineRows = [];
-
-    let totalTime = 0;
-    let totalDelta = 0;
-    for (const file of tests) {
-        const { name, time, delta } = file;
-        totalTime += time;
-        totalDelta += delta;
-
-        const difference = ((delta / time) * 100) | 0;
-        const isUnchanged = Math.abs(difference) < minimumChangeThreshold;
-
-        if (isUnchanged && omitUnchanged) continue;
-
-        const columns = [
-            name,
-            formatMS(time),
-            getDeltaText(delta, difference),
-            iconForDifference(difference),
-        ];
-        if (name.includes('directly')) {
-            baselineRows.push(columns);
-        } else if (isUnchanged && collapseUnchanged) {
-            unChangedRows.push(columns);
-        } else {
-            changedRows.push(columns);
-        }
-    }
-
-    let out = markdownTable(changedRows);
-
-    if (unChangedRows.length !== 0) {
-        const outUnchanged = markdownTable(unChangedRows);
-        out += `\n\n
View Unchanged\n\n${outUnchanged}\n\n
\n\n`; - } - - if (baselineRows.length !== 0) { - const outBaseline = markdownTable(baselineRows.map(line => line.slice(0, 2))); - out += `\n\n
View Baselines\n\n${outBaseline}\n\n
\n\n`; - } - - if (showTotal) { - const totalDifference = ((totalDelta / totalTime) * 100) | 0; - let totalDeltaText = getDeltaText(totalDelta, totalDifference); - let totalIcon = iconForDifference(totalDifference); - out = `**Total Time:** ${formatMS(totalTime)}\n\n${out}`; - out = `**Time Change:** ${totalDeltaText} ${totalIcon}\n\n${out}`; - } - - return out; -}; diff --git a/package-lock.json b/package-lock.json index bb5718d1b..e12af9c97 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,7 +1,7 @@ { "name": "javascript-kit-swift", "version": "0.0.0", - "lockfileVersion": 2, + "lockfileVersion": 3, "requires": true, "packages": { "": { @@ -9,60 +9,405 @@ "version": "0.0.0", "license": "MIT", "devDependencies": { - "@rollup/plugin-typescript": "^8.3.1", - "playwright": "^1.51.0", - "prettier": "2.6.1", - "rollup": "^2.70.0", - "tslib": "^2.3.1", - "typescript": "^4.6.3" + "@bjorn3/browser_wasi_shim": "^0.4.1", + "@rollup/plugin-typescript": "^12.1.2", + "@types/node": "^22.13.14", + "playwright": "^1.52.0", + "prettier": "3.5.3", + "rollup": "^4.37.0", + "rollup-plugin-dts": "^6.2.1", + "typescript": "^5.8.2" } }, + "node_modules/@babel/code-frame": { + "version": "7.26.2", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.26.2.tgz", + "integrity": "sha512-RJlIHRueQgwWitWgF8OdFYGZX328Ax5BCemNGlqHfplnRT9ESi8JkFlvaVYbS+UubVY6dpv87Fs2u5M29iNFVQ==", + "dev": true, + "optional": true, + "dependencies": { + "@babel/helper-validator-identifier": "^7.25.9", + "js-tokens": "^4.0.0", + "picocolors": "^1.0.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.25.9.tgz", + "integrity": "sha512-Ed61U6XJc3CVRfkERJWDz4dJwKe7iLmmJsbOGu9wSloNSFttHV0I8g6UAgb7qnK5ly5bGLPd4oXZlxCdANBOWQ==", + "dev": true, + "optional": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@bjorn3/browser_wasi_shim": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@bjorn3/browser_wasi_shim/-/browser_wasi_shim-0.4.1.tgz", + "integrity": "sha512-54kpBQX69TZ8I1zyDC8sziv/zPT1zoIadv3CmdIZNZ5WDF1houMjAzRZ3dwWvhXObiEBjOxXyS8Ja7vA0EfGEQ==", + "dev": true + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz", + "integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==", + "dev": true + }, "node_modules/@rollup/plugin-typescript": { - "version": "8.3.1", - "resolved": "https://registry.npmjs.org/@rollup/plugin-typescript/-/plugin-typescript-8.3.1.tgz", - "integrity": "sha512-84rExe3ICUBXzqNX48WZV2Jp3OddjTMX97O2Py6D1KJaGSwWp0mDHXj+bCGNJqWHIEKDIT2U0sDjhP4czKi6cA==", + "version": "12.1.2", + "resolved": "https://registry.npmjs.org/@rollup/plugin-typescript/-/plugin-typescript-12.1.2.tgz", + "integrity": "sha512-cdtSp154H5sv637uMr1a8OTWB0L1SWDSm1rDGiyfcGcvQ6cuTs4MDk2BVEBGysUWago4OJN4EQZqOTl/QY3Jgg==", "dev": true, + "license": "MIT", "dependencies": { - "@rollup/pluginutils": "^3.1.0", - "resolve": "^1.17.0" + "@rollup/pluginutils": "^5.1.0", + "resolve": "^1.22.1" }, "engines": { - "node": ">=8.0.0" + "node": ">=14.0.0" }, "peerDependencies": { - "rollup": "^2.14.0", + "rollup": "^2.14.0||^3.0.0||^4.0.0", "tslib": "*", "typescript": ">=3.7.0" + }, + "peerDependenciesMeta": { + "rollup": { + "optional": true + }, + "tslib": { + "optional": true + } } }, "node_modules/@rollup/pluginutils": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-3.1.0.tgz", - "integrity": "sha512-GksZ6pr6TpIjHm8h9lSQ8pi8BE9VeubNT0OMJ3B5uZJ8pz73NPiqOtCog/x2/QzM1ENChPKxMDhiQuRHsqc+lg==", + "version": "5.1.4", + "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-5.1.4.tgz", + "integrity": "sha512-USm05zrsFxYLPdWWq+K3STlWiT/3ELn3RcV5hJMghpeAIhxfsUIg6mt12CBJBInWMV4VneoV7SfGv8xIwo2qNQ==", "dev": true, + "license": "MIT", "dependencies": { - "@types/estree": "0.0.39", - "estree-walker": "^1.0.1", - "picomatch": "^2.2.2" + "@types/estree": "^1.0.0", + "estree-walker": "^2.0.2", + "picomatch": "^4.0.2" }, "engines": { - "node": ">= 8.0.0" + "node": ">=14.0.0" }, "peerDependencies": { - "rollup": "^1.20.0||^2.0.0" + "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0" + }, + "peerDependenciesMeta": { + "rollup": { + "optional": true + } } }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.37.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.37.0.tgz", + "integrity": "sha512-l7StVw6WAa8l3vA1ov80jyetOAEo1FtHvZDbzXDO/02Sq/QVvqlHkYoFwDJPIMj0GKiistsBudfx5tGFnwYWDQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.37.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.37.0.tgz", + "integrity": "sha512-6U3SlVyMxezt8Y+/iEBcbp945uZjJwjZimu76xoG7tO1av9VO691z8PkhzQ85ith2I8R2RddEPeSfcbyPfD4hA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.37.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.37.0.tgz", + "integrity": "sha512-+iTQ5YHuGmPt10NTzEyMPbayiNTcOZDWsbxZYR1ZnmLnZxG17ivrPSWFO9j6GalY0+gV3Jtwrrs12DBscxnlYA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.37.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.37.0.tgz", + "integrity": "sha512-m8W2UbxLDcmRKVjgl5J/k4B8d7qX2EcJve3Sut7YGrQoPtCIQGPH5AMzuFvYRWZi0FVS0zEY4c8uttPfX6bwYQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.37.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.37.0.tgz", + "integrity": "sha512-FOMXGmH15OmtQWEt174v9P1JqqhlgYge/bUjIbiVD1nI1NeJ30HYT9SJlZMqdo1uQFyt9cz748F1BHghWaDnVA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.37.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.37.0.tgz", + "integrity": "sha512-SZMxNttjPKvV14Hjck5t70xS3l63sbVwl98g3FlVVx2YIDmfUIy29jQrsw06ewEYQ8lQSuY9mpAPlmgRD2iSsA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.37.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.37.0.tgz", + "integrity": "sha512-hhAALKJPidCwZcj+g+iN+38SIOkhK2a9bqtJR+EtyxrKKSt1ynCBeqrQy31z0oWU6thRZzdx53hVgEbRkuI19w==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.37.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.37.0.tgz", + "integrity": "sha512-jUb/kmn/Gd8epbHKEqkRAxq5c2EwRt0DqhSGWjPFxLeFvldFdHQs/n8lQ9x85oAeVb6bHcS8irhTJX2FCOd8Ag==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.37.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.37.0.tgz", + "integrity": "sha512-oNrJxcQT9IcbcmKlkF+Yz2tmOxZgG9D9GRq+1OE6XCQwCVwxixYAa38Z8qqPzQvzt1FCfmrHX03E0pWoXm1DqA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.37.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.37.0.tgz", + "integrity": "sha512-pfxLBMls+28Ey2enpX3JvjEjaJMBX5XlPCZNGxj4kdJyHduPBXtxYeb8alo0a7bqOoWZW2uKynhHxF/MWoHaGQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loongarch64-gnu": { + "version": "4.37.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loongarch64-gnu/-/rollup-linux-loongarch64-gnu-4.37.0.tgz", + "integrity": "sha512-yCE0NnutTC/7IGUq/PUHmoeZbIwq3KRh02e9SfFh7Vmc1Z7atuJRYWhRME5fKgT8aS20mwi1RyChA23qSyRGpA==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-powerpc64le-gnu": { + "version": "4.37.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.37.0.tgz", + "integrity": "sha512-NxcICptHk06E2Lh3a4Pu+2PEdZ6ahNHuK7o6Np9zcWkrBMuv21j10SQDJW3C9Yf/A/P7cutWoC/DptNLVsZ0VQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.37.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.37.0.tgz", + "integrity": "sha512-PpWwHMPCVpFZLTfLq7EWJWvrmEuLdGn1GMYcm5MV7PaRgwCEYJAwiN94uBuZev0/J/hFIIJCsYw4nLmXA9J7Pw==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.37.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.37.0.tgz", + "integrity": "sha512-DTNwl6a3CfhGTAOYZ4KtYbdS8b+275LSLqJVJIrPa5/JuIufWWZ/QFvkxp52gpmguN95eujrM68ZG+zVxa8zHA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.37.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.37.0.tgz", + "integrity": "sha512-hZDDU5fgWvDdHFuExN1gBOhCuzo/8TMpidfOR+1cPZJflcEzXdCy1LjnklQdW8/Et9sryOPJAKAQRw8Jq7Tg+A==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.37.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.37.0.tgz", + "integrity": "sha512-pKivGpgJM5g8dwj0ywBwe/HeVAUSuVVJhUTa/URXjxvoyTT/AxsLTAbkHkDHG7qQxLoW2s3apEIl26uUe08LVQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.37.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.37.0.tgz", + "integrity": "sha512-E2lPrLKE8sQbY/2bEkVTGDEk4/49UYRVWgj90MY8yPjpnGBQ+Xi1Qnr7b7UIWw1NOggdFQFOLZ8+5CzCiz143w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.37.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.37.0.tgz", + "integrity": "sha512-Jm7biMazjNzTU4PrQtr7VS8ibeys9Pn29/1bm4ph7CP2kf21950LgN+BaE2mJ1QujnvOc6p54eWWiVvn05SOBg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.37.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.37.0.tgz", + "integrity": "sha512-e3/1SFm1OjefWICB2Ucstg2dxYDkDTZGDYgwufcbsxTHyqQps1UQf33dFEChBNmeSsTOyrjw2JJq0zbG5GF6RA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.37.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.37.0.tgz", + "integrity": "sha512-LWbXUBwn/bcLx2sSsqy7pK5o+Nr+VCoRoAohfJ5C/aBio9nfJmGQqHAhU6pwxV/RmyTk5AqdySma7uwWGlmeuA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, "node_modules/@types/estree": { - "version": "0.0.39", - "resolved": "https://registry.npmjs.org/@types/estree/-/estree-0.0.39.tgz", - "integrity": "sha512-EYNwp3bU+98cpU4lAWYYL7Zz+2gryWH1qbdDTidVd6hkiR6weksdbMadyXKXNPEkQFhXM+hVO9ZygomHXp+AIw==", - "dev": true + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.6.tgz", + "integrity": "sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "22.13.14", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.13.14.tgz", + "integrity": "sha512-Zs/Ollc1SJ8nKUAgc7ivOEdIBM8JAKgrqqUYi2J997JuKO7/tpQC+WCetQ1sypiKCQWHdvdg9wBNpUPEWZae7w==", + "dev": true, + "dependencies": { + "undici-types": "~6.20.0" + } }, "node_modules/estree-walker": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-1.0.1.tgz", - "integrity": "sha512-1fMXF3YP4pZZVozF8j/ZLfvnR8NSIljt56UhbZ5PeeDmmGHpgpdwQt7ITlGvYaQukCvuBRMLEiKiYC+oeIg4cg==", - "dev": true + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz", + "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==", + "dev": true, + "license": "MIT" }, "node_modules/fsevents": { "version": "2.3.2", @@ -70,6 +415,7 @@ "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", "dev": true, "hasInstallScript": true, + "license": "MIT", "optional": true, "os": [ "darwin" @@ -79,61 +425,94 @@ } }, "node_modules/function-bind": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", - "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==", - "dev": true + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } }, - "node_modules/has": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", - "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", "dev": true, + "license": "MIT", "dependencies": { - "function-bind": "^1.1.1" + "function-bind": "^1.1.2" }, "engines": { - "node": ">= 0.4.0" + "node": ">= 0.4" } }, "node_modules/is-core-module": { - "version": "2.8.1", - "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.8.1.tgz", - "integrity": "sha512-SdNCUs284hr40hFTFP6l0IfZ/RSrMXF3qgoRHd3/79unUTvrFO/JoXwkGm+5J/Oe3E/b5GsnG330uUNgRpu1PA==", + "version": "2.16.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", + "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==", "dev": true, + "license": "MIT", "dependencies": { - "has": "^1.0.3" + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" }, "funding": { "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true, + "optional": true + }, + "node_modules/magic-string": { + "version": "0.30.17", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.17.tgz", + "integrity": "sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA==", + "dev": true, + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0" + } + }, "node_modules/path-parse": { "version": "1.0.7", "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", - "dev": true + "dev": true, + "license": "MIT" + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "optional": true }, "node_modules/picomatch": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", - "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.2.tgz", + "integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==", "dev": true, + "license": "MIT", "engines": { - "node": ">=8.6" + "node": ">=12" }, "funding": { "url": "https://github.com/sponsors/jonschlinkert" } }, "node_modules/playwright": { - "version": "1.51.0", - "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.51.0.tgz", - "integrity": "sha512-442pTfGM0xxfCYxuBa/Pu6B2OqxqqaYq39JS8QDMGThUvIOCd6s0ANDog3uwA0cHavVlnTQzGCN7Id2YekDSXA==", + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.52.0.tgz", + "integrity": "sha512-JAwMNMBlxJ2oD1kce4KPtMkDeKGHQstdpFPcPH3maElAXon/QZeTvtsfXmTMRyO9TslfoYOXkSsvao2nE1ilTw==", "dev": true, - "license": "Apache-2.0", "dependencies": { - "playwright-core": "1.51.0" + "playwright-core": "1.52.0" }, "bin": { "playwright": "cli.js" @@ -146,11 +525,10 @@ } }, "node_modules/playwright-core": { - "version": "1.51.0", - "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.51.0.tgz", - "integrity": "sha512-x47yPE3Zwhlil7wlNU/iktF7t2r/URR3VLbH6EknJd/04Qc/PSJ0EY3CMXipmglLG+zyRxW6HNo2EGbKLHPWMg==", + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.52.0.tgz", + "integrity": "sha512-l2osTgLXSMeuLZOML9qYODUQoPPnUsKsb5/P6LJ2e6uPKXUdPK5WYhN4z03G+YNbWmGDY4YENauNu4ZKczreHg==", "dev": true, - "license": "Apache-2.0", "bin": { "playwright-core": "cli.js" }, @@ -159,57 +537,110 @@ } }, "node_modules/prettier": { - "version": "2.6.1", - "resolved": "https://registry.npmjs.org/prettier/-/prettier-2.6.1.tgz", - "integrity": "sha512-8UVbTBYGwN37Bs9LERmxCPjdvPxlEowx2urIL6urHzdb3SDq4B/Z6xLFCblrSnE4iKWcS6ziJ3aOYrc1kz/E2A==", + "version": "3.5.3", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.5.3.tgz", + "integrity": "sha512-QQtaxnoDJeAkDvDKWCLiwIXkTgRhwYDEQCghU9Z6q03iyek/rxRh/2lC3HB7P8sWT2xC/y5JDctPLBIGzHKbhw==", "dev": true, + "license": "MIT", "bin": { - "prettier": "bin-prettier.js" + "prettier": "bin/prettier.cjs" }, "engines": { - "node": ">=10.13.0" + "node": ">=14" }, "funding": { "url": "https://github.com/prettier/prettier?sponsor=1" } }, "node_modules/resolve": { - "version": "1.21.0", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.21.0.tgz", - "integrity": "sha512-3wCbTpk5WJlyE4mSOtDLhqQmGFi0/TD9VPwmiolnk8U0wRgMEktqCXd3vy5buTO3tljvalNvKrjHEfrd2WpEKA==", + "version": "1.22.10", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.10.tgz", + "integrity": "sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w==", "dev": true, + "license": "MIT", "dependencies": { - "is-core-module": "^2.8.0", + "is-core-module": "^2.16.0", "path-parse": "^1.0.7", "supports-preserve-symlinks-flag": "^1.0.0" }, "bin": { "resolve": "bin/resolve" }, + "engines": { + "node": ">= 0.4" + }, "funding": { "url": "https://github.com/sponsors/ljharb" } }, "node_modules/rollup": { - "version": "2.70.1", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-2.70.1.tgz", - "integrity": "sha512-CRYsI5EuzLbXdxC6RnYhOuRdtz4bhejPMSWjsFLfVM/7w/85n2szZv6yExqUXsBdz5KT8eoubeyDUDjhLHEslA==", + "version": "4.37.0", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.37.0.tgz", + "integrity": "sha512-iAtQy/L4QFU+rTJ1YUjXqJOJzuwEghqWzCEYD2FEghT7Gsy1VdABntrO4CLopA5IkflTyqNiLNwPcOJ3S7UKLg==", "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.6" + }, "bin": { "rollup": "dist/bin/rollup" }, "engines": { - "node": ">=10.0.0" + "node": ">=18.0.0", + "npm": ">=8.0.0" }, "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.37.0", + "@rollup/rollup-android-arm64": "4.37.0", + "@rollup/rollup-darwin-arm64": "4.37.0", + "@rollup/rollup-darwin-x64": "4.37.0", + "@rollup/rollup-freebsd-arm64": "4.37.0", + "@rollup/rollup-freebsd-x64": "4.37.0", + "@rollup/rollup-linux-arm-gnueabihf": "4.37.0", + "@rollup/rollup-linux-arm-musleabihf": "4.37.0", + "@rollup/rollup-linux-arm64-gnu": "4.37.0", + "@rollup/rollup-linux-arm64-musl": "4.37.0", + "@rollup/rollup-linux-loongarch64-gnu": "4.37.0", + "@rollup/rollup-linux-powerpc64le-gnu": "4.37.0", + "@rollup/rollup-linux-riscv64-gnu": "4.37.0", + "@rollup/rollup-linux-riscv64-musl": "4.37.0", + "@rollup/rollup-linux-s390x-gnu": "4.37.0", + "@rollup/rollup-linux-x64-gnu": "4.37.0", + "@rollup/rollup-linux-x64-musl": "4.37.0", + "@rollup/rollup-win32-arm64-msvc": "4.37.0", + "@rollup/rollup-win32-ia32-msvc": "4.37.0", + "@rollup/rollup-win32-x64-msvc": "4.37.0", "fsevents": "~2.3.2" } }, + "node_modules/rollup-plugin-dts": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/rollup-plugin-dts/-/rollup-plugin-dts-6.2.1.tgz", + "integrity": "sha512-sR3CxYUl7i2CHa0O7bA45mCrgADyAQ0tVtGSqi3yvH28M+eg1+g5d7kQ9hLvEz5dorK3XVsH5L2jwHLQf72DzA==", + "dev": true, + "dependencies": { + "magic-string": "^0.30.17" + }, + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/Swatinem" + }, + "optionalDependencies": { + "@babel/code-frame": "^7.26.2" + }, + "peerDependencies": { + "rollup": "^3.29.4 || ^4", + "typescript": "^4.5 || ^5.0" + } + }, "node_modules/supports-preserve-symlinks-flag": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", "dev": true, + "license": "MIT", "engines": { "node": ">= 0.4" }, @@ -218,160 +649,32 @@ } }, "node_modules/tslib": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.3.1.tgz", - "integrity": "sha512-77EbyPPpMz+FRFRuAFlWMtmgUWGe9UOG2Z25NqCwiIjRhOf5iKGuzSe5P2w1laq+FkRy4p+PCuVkJSGkzTEKVw==", - "dev": true + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "dev": true, + "license": "0BSD", + "optional": true, + "peer": true }, "node_modules/typescript": { - "version": "4.6.3", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.6.3.tgz", - "integrity": "sha512-yNIatDa5iaofVozS/uQJEl3JRWLKKGJKh6Yaiv0GLGSuhpFJe7P3SbHZ8/yjAHRQwKRoA6YZqlfjXWmVzoVSMw==", + "version": "5.8.2", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.2.tgz", + "integrity": "sha512-aJn6wq13/afZp/jT9QZmwEjDqqvSGp1VT5GVg+f/t6/oVyrgXM6BY1h9BRh/O5p3PlUPAe+WuiEZOmb/49RqoQ==", "dev": true, + "license": "Apache-2.0", "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" }, "engines": { - "node": ">=4.2.0" - } - } - }, - "dependencies": { - "@rollup/plugin-typescript": { - "version": "8.3.1", - "resolved": "https://registry.npmjs.org/@rollup/plugin-typescript/-/plugin-typescript-8.3.1.tgz", - "integrity": "sha512-84rExe3ICUBXzqNX48WZV2Jp3OddjTMX97O2Py6D1KJaGSwWp0mDHXj+bCGNJqWHIEKDIT2U0sDjhP4czKi6cA==", - "dev": true, - "requires": { - "@rollup/pluginutils": "^3.1.0", - "resolve": "^1.17.0" - } - }, - "@rollup/pluginutils": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-3.1.0.tgz", - "integrity": "sha512-GksZ6pr6TpIjHm8h9lSQ8pi8BE9VeubNT0OMJ3B5uZJ8pz73NPiqOtCog/x2/QzM1ENChPKxMDhiQuRHsqc+lg==", - "dev": true, - "requires": { - "@types/estree": "0.0.39", - "estree-walker": "^1.0.1", - "picomatch": "^2.2.2" - } - }, - "@types/estree": { - "version": "0.0.39", - "resolved": "https://registry.npmjs.org/@types/estree/-/estree-0.0.39.tgz", - "integrity": "sha512-EYNwp3bU+98cpU4lAWYYL7Zz+2gryWH1qbdDTidVd6hkiR6weksdbMadyXKXNPEkQFhXM+hVO9ZygomHXp+AIw==", - "dev": true - }, - "estree-walker": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-1.0.1.tgz", - "integrity": "sha512-1fMXF3YP4pZZVozF8j/ZLfvnR8NSIljt56UhbZ5PeeDmmGHpgpdwQt7ITlGvYaQukCvuBRMLEiKiYC+oeIg4cg==", - "dev": true - }, - "fsevents": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", - "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", - "dev": true, - "optional": true - }, - "function-bind": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", - "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==", - "dev": true - }, - "has": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", - "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", - "dev": true, - "requires": { - "function-bind": "^1.1.1" - } - }, - "is-core-module": { - "version": "2.8.1", - "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.8.1.tgz", - "integrity": "sha512-SdNCUs284hr40hFTFP6l0IfZ/RSrMXF3qgoRHd3/79unUTvrFO/JoXwkGm+5J/Oe3E/b5GsnG330uUNgRpu1PA==", - "dev": true, - "requires": { - "has": "^1.0.3" + "node": ">=14.17" } }, - "path-parse": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", - "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", - "dev": true - }, - "picomatch": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", - "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", - "dev": true - }, - "playwright": { - "version": "1.51.0", - "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.51.0.tgz", - "integrity": "sha512-442pTfGM0xxfCYxuBa/Pu6B2OqxqqaYq39JS8QDMGThUvIOCd6s0ANDog3uwA0cHavVlnTQzGCN7Id2YekDSXA==", - "dev": true, - "requires": { - "fsevents": "2.3.2", - "playwright-core": "1.51.0" - } - }, - "playwright-core": { - "version": "1.51.0", - "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.51.0.tgz", - "integrity": "sha512-x47yPE3Zwhlil7wlNU/iktF7t2r/URR3VLbH6EknJd/04Qc/PSJ0EY3CMXipmglLG+zyRxW6HNo2EGbKLHPWMg==", - "dev": true - }, - "prettier": { - "version": "2.6.1", - "resolved": "https://registry.npmjs.org/prettier/-/prettier-2.6.1.tgz", - "integrity": "sha512-8UVbTBYGwN37Bs9LERmxCPjdvPxlEowx2urIL6urHzdb3SDq4B/Z6xLFCblrSnE4iKWcS6ziJ3aOYrc1kz/E2A==", - "dev": true - }, - "resolve": { - "version": "1.21.0", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.21.0.tgz", - "integrity": "sha512-3wCbTpk5WJlyE4mSOtDLhqQmGFi0/TD9VPwmiolnk8U0wRgMEktqCXd3vy5buTO3tljvalNvKrjHEfrd2WpEKA==", - "dev": true, - "requires": { - "is-core-module": "^2.8.0", - "path-parse": "^1.0.7", - "supports-preserve-symlinks-flag": "^1.0.0" - } - }, - "rollup": { - "version": "2.70.1", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-2.70.1.tgz", - "integrity": "sha512-CRYsI5EuzLbXdxC6RnYhOuRdtz4bhejPMSWjsFLfVM/7w/85n2szZv6yExqUXsBdz5KT8eoubeyDUDjhLHEslA==", - "dev": true, - "requires": { - "fsevents": "~2.3.2" - } - }, - "supports-preserve-symlinks-flag": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", - "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", - "dev": true - }, - "tslib": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.3.1.tgz", - "integrity": "sha512-77EbyPPpMz+FRFRuAFlWMtmgUWGe9UOG2Z25NqCwiIjRhOf5iKGuzSe5P2w1laq+FkRy4p+PCuVkJSGkzTEKVw==", - "dev": true - }, - "typescript": { - "version": "4.6.3", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.6.3.tgz", - "integrity": "sha512-yNIatDa5iaofVozS/uQJEl3JRWLKKGJKh6Yaiv0GLGSuhpFJe7P3SbHZ8/yjAHRQwKRoA6YZqlfjXWmVzoVSMw==", + "node_modules/undici-types": { + "version": "6.20.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.20.0.tgz", + "integrity": "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==", "dev": true } } diff --git a/package.json b/package.json index 0c67b2705..96443ad9a 100644 --- a/package.json +++ b/package.json @@ -34,11 +34,13 @@ "author": "swiftwasm", "license": "MIT", "devDependencies": { - "@rollup/plugin-typescript": "^8.3.1", - "playwright": "^1.51.0", - "prettier": "2.6.1", - "rollup": "^2.70.0", - "tslib": "^2.3.1", - "typescript": "^4.6.3" + "@bjorn3/browser_wasi_shim": "^0.4.1", + "@rollup/plugin-typescript": "^12.1.2", + "@types/node": "^22.13.14", + "playwright": "^1.52.0", + "prettier": "3.5.3", + "rollup": "^4.37.0", + "rollup-plugin-dts": "^6.2.1", + "typescript": "^5.8.2" } }