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/.gitattributes b/.gitattributes new file mode 100644 index 000000000..95204dad7 --- /dev/null +++ b/.gitattributes @@ -0,0 +1 @@ +Sources/JavaScriptKit/Runtime/** linguist-generated diff --git a/.github/actions/install-swift/action.yml b/.github/actions/install-swift/action.yml new file mode 100644 index 000000000..d6fbcc969 --- /dev/null +++ b/.github/actions/install-swift/action.yml @@ -0,0 +1,37 @@ +name: 'Install Swift toolchain' +description: 'Install Swift toolchain tarball from URL' +inputs: + download-url: + description: 'URL to download Swift toolchain tarball' + required: true + +runs: + using: composite + steps: + # https://www.swift.org/install/linux/#installation-via-tarball + - name: Install dependent packages for Swift + shell: bash + run: > + sudo apt-get -q update && + sudo apt-get install -y + binutils + git + gnupg2 + libc6-dev + libcurl4-openssl-dev + libedit2 + libgcc-9-dev + libpython3.8 + libsqlite3-0 + libstdc++-9-dev + libxml2-dev + libz3-dev + pkg-config + tzdata + unzip + zlib1g-dev + curl + + - name: Install Swift + shell: bash + run: curl -fL ${{ inputs.download-url }} | sudo tar xfz - --strip-components=2 -C /usr/local diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 000000000..8a923bf7a --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,11 @@ +# To get started with Dependabot version updates, you'll need to specify which +# package ecosystems to update and where the package manifests are located. +# Please see the documentation for all configuration options: +# https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates + +version: 2 +updates: + - package-ecosystem: 'github-actions' + directory: '/' + schedule: + interval: 'weekly' diff --git a/.github/workflows/compatibility.yml b/.github/workflows/compatibility.yml deleted file mode 100644 index e4534507e..000000000 --- a/.github/workflows/compatibility.yml +++ /dev/null @@ -1,24 +0,0 @@ -name: Check compatibility -on: - pull_request: - push: - branches: [main] -jobs: - test: - name: Check source code compatibility - runs-on: Ubuntu-18.04 - steps: - - name: Checkout - uses: actions/checkout@v2 - with: - fetch-depth: 1 - - name: Run Test - run: | - set -eux - git clone https://github.com/kylef/swiftenv.git ~/.swiftenv - export SWIFTENV_ROOT="$HOME/.swiftenv" - export PATH="$SWIFTENV_ROOT/bin:$PATH" - eval "$(swiftenv init -)" - make bootstrap - cd Example/JavaScriptKitExample - swift build --triple wasm32-unknown-wasi diff --git a/.github/workflows/documentation.yml b/.github/workflows/documentation.yml deleted file mode 100644 index a8a31234c..000000000 --- a/.github/workflows/documentation.yml +++ /dev/null @@ -1,26 +0,0 @@ -name: Documentation - -on: - push: - branches: [main] - -jobs: - swift-doc: - runs-on: ubuntu-latest - - steps: - - uses: actions/checkout@v1 - - name: Generate Documentation - uses: SwiftDocOrg/swift-doc@master - with: - inputs: "Sources" - module-name: JavaScriptKit - format: html - base-url: "/JavaScriptKit" - output: ./.build/documentation - - run: sudo chmod o+r -R ./.build/documentation - - name: Deploy - uses: peaceiris/actions-gh-pages@v3 - with: - github_token: ${{ secrets.GITHUB_TOKEN }} - publish_dir: ./.build/documentation diff --git a/.github/workflows/npm-publish.yml b/.github/workflows/npm-publish.yml deleted file mode 100644 index e6a887d3d..000000000 --- a/.github/workflows/npm-publish.yml +++ /dev/null @@ -1,22 +0,0 @@ -# This workflow will run tests using node and then publish a package to npm when a release is created -# For more information see: https://help.github.com/actions/language-and-framework-guides/publishing-nodejs-packages - -name: Publish to npm - -on: - release: - types: [created] - -jobs: - publish-npm: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v2 - - uses: actions/setup-node@v1 - with: - node-version: 12 - registry-url: https://registry.npmjs.org/ - - run: npm ci - - run: npm publish - env: - NODE_AUTH_TOKEN: ${{secrets.npm_token}} diff --git a/.github/workflows/perf.yml b/.github/workflows/perf.yml deleted file mode 100644 index f2014323a..000000000 --- a/.github/workflows/perf.yml +++ /dev/null @@ -1,23 +0,0 @@ -name: Performance - -on: [pull_request] - -jobs: - perf: - runs-on: Ubuntu-18.04 - steps: - - name: Checkout - uses: actions/checkout@master - with: - fetch-depth: 1 - - name: Run Benchmark - run: | - git clone https://github.com/kylef/swiftenv.git ~/.swiftenv - export SWIFTENV_ROOT="$HOME/.swiftenv" - export PATH="$SWIFTENV_ROOT/bin:$PATH" - eval "$(swiftenv init -)" - 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 b3992b7dc..cd9c68493 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -8,30 +8,83 @@ jobs: name: Build and Test strategy: matrix: - os: [macos-10.15, macos-11, ubuntu-18.04, ubuntu-20.04] - toolchain: - - wasm-5.5.0-RELEASE - runs-on: ${{ matrix.os }} + entry: + - os: ubuntu-22.04 + toolchain: + download-url: https://download.swift.org/swift-6.0.2-release/ubuntu2204/swift-6.0.2-RELEASE/swift-6.0.2-RELEASE-ubuntu22.04.tar.gz + wasi-backend: Node + target: "wasm32-unknown-wasi" + - os: ubuntu-22.04 + toolchain: + 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 + 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 + wasi-backend: Node + target: "wasm32-unknown-wasip1-threads" + + runs-on: ${{ matrix.entry.os }} + env: + JAVASCRIPTKIT_WASI_BACKEND: ${{ matrix.entry.wasi-backend }} steps: - name: Checkout - uses: actions/checkout@master + uses: actions/checkout@v4 + - uses: ./.github/actions/install-swift + with: + download-url: ${{ matrix.entry.toolchain.download-url }} + - uses: swiftwasm/setup-swiftwasm@v2 + id: setup-swiftwasm with: - fetch-depth: 1 - - name: Run Test + target: ${{ matrix.entry.target }} + - name: Configure environment variables run: | - git clone https://github.com/kylef/swiftenv.git ~/.swiftenv - export SWIFTENV_ROOT="$HOME/.swiftenv" - export PATH="$SWIFTENV_ROOT/bin:$PATH" - eval "$(swiftenv init -)" - SWIFT_VERSION=${{ matrix.toolchain }} make bootstrap - echo ${{ matrix.toolchain }} > .swift-version - make test + echo "SWIFT_SDK_ID=${{ steps.setup-swiftwasm.outputs.swift-sdk-id }}" >> $GITHUB_ENV + echo "SWIFT_PATH=$(dirname $(which swiftc))" >> $GITHUB_ENV + - run: make bootstrap + - run: make unittest + # Skip unit tests with uwasi because its proc_exit throws + # unhandled promise rejection. + if: ${{ matrix.entry.wasi-backend != 'MicroWASI' }} + - name: Check if SwiftPM resources are stale + run: | + 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 name: Build for native target - runs-on: macos-11 + strategy: + matrix: + include: + - os: macos-15 + xcode: Xcode_16 + runs-on: ${{ matrix.os }} steps: - - uses: actions/checkout@v2 - - run: swift build + - uses: actions/checkout@v4 + - run: swift build --package-path ./Examples/Basic env: - DEVELOPER_DIR: /Applications/Xcode_13.2.1.app/Contents/Developer/ + 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 a24baa7da..5aac0048c 100644 --- a/.gitignore +++ b/.gitignore @@ -6,3 +6,8 @@ node_modules /*.xcodeproj xcuserdata/ .swiftpm +.vscode +Examples/*/Bundle +Examples/*/package-lock.json +Package.resolved +Plugins/BridgeJS/Sources/JavaScript/package-lock.json diff --git a/.spi.yml b/.spi.yml new file mode 100644 index 000000000..94203e1d3 --- /dev/null +++ b/.spi.yml @@ -0,0 +1,7 @@ +version: 1 +builder: + configs: + - documentation_targets: + - JavaScriptKit + - JavaScriptEventLoop + - JavaScriptBigIntSupport 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/.swift-version b/.swift-version deleted file mode 100644 index 871073e10..000000000 --- a/.swift-version +++ /dev/null @@ -1 +0,0 @@ -wasm-5.5.0-RELEASE 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/CHANGELOG.md b/CHANGELOG.md index cef363202..9e9e0bd6f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,204 @@ +> [!IMPORTANT] +> For future releases, please refer to the [GitHub releases page](https://github.com/swiftwasm/JavaScriptKit/releases) + +---- + +# 0.20.0 (11 July 2024) + +This release adds an initial multi-threading support. + +## What's Changed +* Start migrating imported functions to the new definition style by @kateinoigakukun in https://github.com/swiftwasm/JavaScriptKit/pull/252 +* Allocate JavaScriptEventLoop per thread in multi-threaded environment by @kateinoigakukun in https://github.com/swiftwasm/JavaScriptKit/pull/255 +* Add `WebWorkerTaskExecutor` by @kateinoigakukun in https://github.com/swiftwasm/JavaScriptKit/pull/256 + +**Full Changelog**: https://github.com/swiftwasm/JavaScriptKit/compare/0.19.3...0.20.0 + +# 0.19.3 (6 Jun 2024) + +## What's Changed +* Fix `JSClosure` leak by @kateinoigakukun in https://github.com/swiftwasm/JavaScriptKit/pull/240 +* Update README file to include new carton 1.0 implementation. by @kuhl in https://github.com/swiftwasm/JavaScriptKit/pull/243 +* Update Carton context on README. by @kuhl in https://github.com/swiftwasm/JavaScriptKit/pull/245 +* Support latest nightly snapshot by @kateinoigakukun in https://github.com/swiftwasm/JavaScriptKit/pull/246 +* Use Swift SDK for development snapshot testing in CI by @kateinoigakukun in https://github.com/swiftwasm/JavaScriptKit/pull/248 +* Add `sharedMemory` option to allow threads with shared memory by @kateinoigakukun in https://github.com/swiftwasm/JavaScriptKit/pull/247 +* Check 5.10 toolchain in CI by @kateinoigakukun in https://github.com/swiftwasm/JavaScriptKit/pull/249 + +## New Contributors +* @kuhl made their first contribution in https://github.com/swiftwasm/JavaScriptKit/pull/243 + +**Full Changelog**: https://github.com/swiftwasm/JavaScriptKit/compare/0.19.2...0.19.3 + +# 0.19.2 (11 Apr 2024) + +## What's Changed +* [CI] macos-14 by @ikesyo in https://github.com/swiftwasm/JavaScriptKit/pull/233 +* [CI] Drop macos-11 since that is deprecated and will be removed in Q2 2024 by @ikesyo in https://github.com/swiftwasm/JavaScriptKit/pull/234 +* Update swift-tools-version to reflect the supported Swift versions by @ikesyo in https://github.com/swiftwasm/JavaScriptKit/pull/235 +* [CI] Update actions and configure Dependabot by @ikesyo in https://github.com/swiftwasm/JavaScriptKit/pull/236 +* Fix Optional implementation for ConstructibleFromJSValue by @omochi in https://github.com/swiftwasm/JavaScriptKit/pull/238 +* Inherit JSFunction from JSClosure by @kateinoigakukun in https://github.com/swiftwasm/JavaScriptKit/pull/239 +* Fix object decode by @omochi in https://github.com/swiftwasm/JavaScriptKit/pull/241 + +## New Contributors +* @ikesyo made their first contribution in https://github.com/swiftwasm/JavaScriptKit/pull/233 +* @omochi made their first contribution in https://github.com/swiftwasm/JavaScriptKit/pull/238 + +**Full Changelog**: https://github.com/swiftwasm/JavaScriptKit/compare/0.19.1...0.19.2 + +# 0.19.1 (6 Feb 2024) + +## What's Changed +* Fix availability marker for Swift 5.9 compiler targeting host machine by @kateinoigakukun in https://github.com/swiftwasm/JavaScriptKit/pull/232 + +**Full Changelog**: https://github.com/swiftwasm/JavaScriptKit/compare/0.19.0...0.19.1 + + +# 0.19.0 (16 Jan 2024) + +## What's Changed +* Update 5.7 patch version by @kateinoigakukun in https://github.com/swiftwasm/JavaScriptKit/pull/226 +* Add 5.8 toolchain matrix by @kateinoigakukun in https://github.com/swiftwasm/JavaScriptKit/pull/227 +* Fix warnings Aug 5, 2023 by @STREGA in https://github.com/swiftwasm/JavaScriptKit/pull/228 +* Swift 5.9 Changes by @STREGA in https://github.com/swiftwasm/JavaScriptKit/pull/229 + +## New Contributors +* @STREGA made their first contribution in https://github.com/swiftwasm/JavaScriptKit/pull/228 + +**Full Changelog**: https://github.com/swiftwasm/JavaScriptKit/compare/0.18.0...0.19.0 + + +# 0.18.0 (13 Mar 2023) + +## What's Changed +* Use swiftwasm/setup-swiftwasm instead of swiftenv on CI by @kateinoigakukun in https://github.com/swiftwasm/JavaScriptKit/pull/215 +* Support Clock-based sleep APIs by @kateinoigakukun in https://github.com/swiftwasm/JavaScriptKit/pull/216 +* Prefer `UInt(bitPattern:)` for object id to guarantee uniqueness by @kateinoigakukun in https://github.com/swiftwasm/JavaScriptKit/pull/219 +* Fix wrong markdown in documentation by @gibachan in https://github.com/swiftwasm/JavaScriptKit/pull/221 +* Add `withUnsafeBytesAsync` function to `JSTypedArray` by @fjtrujy in https://github.com/swiftwasm/JavaScriptKit/pull/222 +* Trivial fixes to JSTypedArray by @kateinoigakukun in https://github.com/swiftwasm/JavaScriptKit/pull/223 + +## New Contributors +* @gibachan made their first contribution in https://github.com/swiftwasm/JavaScriptKit/pull/221 +* @fjtrujy made their first contribution in https://github.com/swiftwasm/JavaScriptKit/pull/222 + +**Full Changelog**: https://github.com/swiftwasm/JavaScriptKit/compare/0.17.0...0.18.0 + + +# 0.17.0 (4 Oct 2022) + +This release introduces testing support module, minor API enhancements for `JavaScriptEventLoop`. + +Linking the new `JavaScriptEventLoopTestSupport` module automatically activates JS event loop based global executor. +This automatic activation is just for XCTest integration since XCTest with SwiftPM doesn't allow to call `JavaScriptEventLoop.installGlobalExecutor()` at first. + +## What's Changed + +* Bump @actions/core from 1.2.6 to 1.9.1 in /ci/perf-tester by @dependabot in https://github.com/swiftwasm/JavaScriptKit/pull/209 +* Remove baseline tests (e.g. “Call JavaScript function directly”) from comparison by @j-f1 in https://github.com/swiftwasm/JavaScriptKit/pull/211 +* Add 5.7 toolchain matrix by @kateinoigakukun in https://github.com/swiftwasm/JavaScriptKit/pull/210 +* Add JavaScriptEventLoopTestSupport module to install executor by @kateinoigakukun in https://github.com/swiftwasm/JavaScriptKit/pull/213 +* Expose `JavaScriptEventLoop.queueMicrotask` and `.setTimeout` by @kateinoigakukun in https://github.com/swiftwasm/JavaScriptKit/pull/214 + + +**Full Changelog**: https://github.com/swiftwasm/JavaScriptKit/compare/0.16.0...0.17.0 + +# 0.16.0 (22 Aug 2022) + +This release contains significant performance improvements, API enhancements for `JSPromise` / `JSBigInt` / `JSClosure`, and documentation improvements. + +**Merged pull requests:** + +- Runtime Performance Optimization ([#207](https://github.com/swiftwasm/JavaScriptKit/pull/207)) via [@kateinoigakukun](https://github.com/kateinoigakukun) +- Add missing doc comments for more types ([#208](https://github.com/swiftwasm/JavaScriptKit/pull/208)) via [@MaxDesiatov](https://github.com/MaxDesiatov) +- Add Int64/UInt64 to Bigint slow conversion ([#204](https://github.com/swiftwasm/JavaScriptKit/pull/204)) via [@kateinoigakukun](https://github.com/kateinoigakukun) +- Test native builds with Xcode 14.0 ([#206](https://github.com/swiftwasm/JavaScriptKit/pull/206)) via [@MaxDesiatov](https://github.com/MaxDesiatov) +- Support DocC generation in Swift Package Index ([#205](https://github.com/swiftwasm/JavaScriptKit/pull/205)) via [@MaxDesiatov](https://github.com/MaxDesiatov) +- Refine benchmark suite ([#203](https://github.com/swiftwasm/JavaScriptKit/pull/203)) via [@kateinoigakukun](https://github.com/kateinoigakukun) +- Add diagnostics for those who build with WASI command line ABI ([#202](https://github.com/swiftwasm/JavaScriptKit/pull/202)) via [@kateinoigakukun](https://github.com/kateinoigakukun) +- Bump terser from 5.10.0 to 5.14.2 in /Example ([#201](https://github.com/swiftwasm/JavaScriptKit/pull/201)) via [@dependabot[bot]](https://github.com/dependabot[bot]) +- Test with uwasi implementation ([#198](https://github.com/swiftwasm/JavaScriptKit/pull/198)) via [@kateinoigakukun](https://github.com/kateinoigakukun) +- Add async JSPromise.result property ([#200](https://github.com/swiftwasm/JavaScriptKit/pull/200)) via [@kateinoigakukun](https://github.com/kateinoigakukun) +- Asynchronous calls in JSClosure ([#157](https://github.com/swiftwasm/JavaScriptKit/issues/157)) via [@j-f1](https://github.com/j-f1) +- JSPromise(resolver:) usage ([#156](https://github.com/swiftwasm/JavaScriptKit/issues/156)) via [@j-f1](https://github.com/j-f1) + + +# 0.15.0 (17 May 2022) + +This is a major release that adds new features and fixes issues. Specifically: +* `BigInt` and `BigInt`-based `JSTypedArray` types are now supported. Now, when passing `Int64` values from Swift, +they will be mapped to `BigInt` values on the JavaScript side. +* The `constructor` property on `JSBridgedClass` is now an Optional, which allows bridging JavaScript classes that aren't +available in every browser or environment. +* JavaScriptKit runtime files are now supplied as SwiftPM resources. This allows us to resolve a long-standing issue +in `carton` that could lead to a version mismatch between JavaScriptKit dependency in `Package.swift` or +`Package.resolved` and carton’s bundled JavaScriptKit runtime version. +* The `JSSymbol` type has been added, enabling support for [JavaScript `Symbol` values](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Symbol), including accessing `Symbol`-keyed properties on objects. + +**Source breaking changes** + +`UInt64.jsValue` and `Int64.jsValue`, which are a part of `JavaScriptKit` module, have been moved into `JavaScriptBigIntSupport` module since their implementation changed to require [JS-BigInt-integration](https://github.com/WebAssembly/JS-BigInt-integration) to avoid implicit casts from 64-bit integer to JS number type. + +If you want to keep the behavior so far, please cast the 64-bit integer values to `Double`. + +**Merged pull requests:** + +- Improve JSKit diagnostics for use-after-free of JSClosure ([#195](https://github.com/swiftwasm/JavaScriptKit/pull/195)) via [@kateinoigakukun](https://github.com/kateinoigakukun) +- Gracefully handle unavailable `JSBridgedClass` ([#190](https://github.com/swiftwasm/JavaScriptKit/pull/190)) via [@MaxDesiatov](https://github.com/MaxDesiatov) +- Supply JSKit runtime in SwiftPM resources ([#193](https://github.com/swiftwasm/JavaScriptKit/pull/193)) via [@MaxDesiatov](https://github.com/MaxDesiatov) +- Test with Node.js's WASI implementation ([#192](https://github.com/swiftwasm/JavaScriptKit/pull/192)) via [@kateinoigakukun](https://github.com/kateinoigakukun) +- Add support for BigInts and BigInt-based TypedArrays ([#184](https://github.com/swiftwasm/JavaScriptKit/pull/184)) via [@j-f1](https://github.com/j-f1) +- Update toolchain references to 5.6.0 in `README.md` ([#189](https://github.com/swiftwasm/JavaScriptKit/pull/189)) via [@MaxDesiatov](https://github.com/MaxDesiatov) +- Bump async from 2.6.3 to 2.6.4 in /Example ([#188](https://github.com/swiftwasm/JavaScriptKit/pull/188)) via [@dependabot[bot]](https://github.com/dependabot[bot]) +- Remove outdated `BigInt` support `FIXME` from `JSTypedArray` ([#187](https://github.com/swiftwasm/JavaScriptKit/pull/187)) via [@MaxDesiatov](https://github.com/MaxDesiatov) +- Cleanup & improvements to perf-tester ([#186](https://github.com/swiftwasm/JavaScriptKit/pull/186)) via [@j-f1](https://github.com/j-f1) +- Re-add support for Symbol objects via JSSymbol ([#183](https://github.com/swiftwasm/JavaScriptKit/pull/183)) via [@j-f1](https://github.com/j-f1) +- Fix JSValueDecoder ([#185](https://github.com/swiftwasm/JavaScriptKit/pull/185)) via [@j-f1](https://github.com/j-f1) +- Fix deprecation warning in `JSFunction.swift` ([#182](https://github.com/swiftwasm/JavaScriptKit/pull/182)) via [@MaxDesiatov](https://github.com/MaxDesiatov) + +# 0.14.0 (8 April 2022) + +This is a breaking release that enables full support for SwiftWasm 5.6 and lays groundwork for future updates to [DOMKit](https://github.com/swiftwasm/DOMKit/). + +- The `ConvertibleToJSValue` conformance on `Array` and `Dictionary` has been swapped from the `== ConvertibleToJSValue` case to the `: ConvertibleToJSValue` case. + - This means that e.g. `[String]` is now `ConvertibleToJSValue`, but `[ConvertibleToJSValue]` no longer conforms; + - the `jsValue()` method still works in both cases; + - to adapt existing code, use one of these approaches: + - use generics where possible (for single-type arrays) + - call `.map { $0.jsValue() }` (or `mapValues`) to get an array/dictionary of `JSValue` which you can then use as `ConvertibleToJSValue` + - add `.jsValue` to the end of all of the values in the array/dictionary literal. + +**Merged pull requests:** + +- Reenable integration tests ([#180](https://github.com/swiftwasm/JavaScriptKit/pull/180)) via [@kateinoigakukun](https://github.com/kateinoigakukun) +- Updates for DOMKit ([#174](https://github.com/swiftwasm/JavaScriptKit/pull/174)) via [@j-f1](https://github.com/j-f1) +- Add 5.6 release and macOS 12 with Xcode 13.3 to CI matrix ([#176](https://github.com/swiftwasm/JavaScriptKit/pull/176)) via [@MaxDesiatov](https://github.com/MaxDesiatov) + +# 0.13.0 (31 March 2022) + +This release improves handling of JavaScript exceptions and compatibility with Xcode. + +Thanks to [@kateinoigakukun](https://github.com/kateinoigakukun), [@pedrovgs](https://github.com/pedrovgs), and +[@valeriyvan](https://github.com/valeriyvan) for contributions! + +**Closed issues:** + +- UserAgent support? ([#169](https://github.com/swiftwasm/JavaScriptKit/issues/169)) +- Compile error on macOS 12.2.1 ([#167](https://github.com/swiftwasm/JavaScriptKit/issues/167)) + +**Merged pull requests:** + +- Improve error messages when JS code throws exceptions ([#173](https://github.com/swiftwasm/JavaScriptKit/pull/173)) via [@pedrovgs](https://github.com/pedrovgs) +- Update npm dependencies ([#175](https://github.com/swiftwasm/JavaScriptKit/pull/175)) via [@MaxDesiatov](https://github.com/MaxDesiatov) +- Bump minimist from 1.2.5 to 1.2.6 in /Example ([#172](https://github.com/swiftwasm/JavaScriptKit/pull/172)) via [@dependabot[bot]](https://github.com/dependabot[bot]) +- Use availability guarded APIs under @available for Xcode development ([#171](https://github.com/swiftwasm/JavaScriptKit/pull/171)) via [@kateinoigakukun](https://github.com/kateinoigakukun) +- Fix warning in snippet ([#166](https://github.com/swiftwasm/JavaScriptKit/pull/166)) via [@valeriyvan](https://github.com/valeriyvan) +- Bump follow-redirects from 1.14.5 to 1.14.8 in /Example ([#165](https://github.com/swiftwasm/JavaScriptKit/pull/165)) via [@dependabot[bot]](https://github.com/dependabot[bot]) + # 0.12.0 (08 February 2022) -This release introduces a [major refactor](https://github.com/swiftwasm/JavaScriptKit/pull/150) of the JavaScript runtime by [@j-f1] and several performance enhancements. +This release introduces a [major refactor](https://github.com/swiftwasm/JavaScriptKit/pull/150) of the JavaScript runtime by [@j-f1] and several performance enhancements. **Merged pull requests:** @@ -19,9 +217,10 @@ This is a bugfix release that removes a requirement for macOS Monterey in `Packa package. `README.md` was updated to explicitly specify that if you're building an app or a library that depends on JavaScriptKit for macOS (i.e. cross-platform code that supports both WebAssembly and macOS), you need either -* macOS Monterey that has the new Swift concurrency runtime available, or -* any version of macOS that supports Swift concurrency back-deployment with Xcode 13.2 or later, or -* add `.unsafeFlags(["-Xfrontend", "-disable-availability-checking"])` in `Package.swift` manifest. + +- macOS Monterey that has the new Swift concurrency runtime available, or +- any version of macOS that supports Swift concurrency back-deployment with Xcode 13.2 or later, or +- add `.unsafeFlags(["-Xfrontend", "-disable-availability-checking"])` in `Package.swift` manifest. **Merged pull requests:** @@ -31,7 +230,7 @@ and macOS), you need either This release adds support for `async`/`await` and SwiftWasm 5.5. Use the new `value` async property on a `JSPromise` instance to `await` for its result. You'll have to add a dependency on the new -`JavaScriptEventLoop` target in your `Package.swift`, `import JavaScriptEventLoop`, and call +`JavaScriptEventLoop` target in your `Package.swift`, `import JavaScriptEventLoop`, and call `JavaScriptEventLoop.installGlobalExecutor()` in your code before you start using `await` and `Task` APIs. @@ -43,12 +242,12 @@ compiler flags, see [`README.md`](./README.md) for more details. This new release of JavaScriptKit may work with SwiftWasm 5.4 and 5.3, but is no longer tested with those versions due to compatibility issues introduced on macOS by latest versions of Xcode. -Many thanks to [@j-f1], [@kateinoigakukun], +Many thanks to [@j-f1], [@kateinoigakukun], and [@PatrickPijnappel] for their contributions to this release! **Closed issues:** -- Enchancement: Add a link to the docs ([#136](https://github.com/swiftwasm/JavaScriptKit/issues/136)) +- Enchancement: Add a link to the docs ([#136](https://github.com/swiftwasm/JavaScriptKit/issues/136)) - Use `FinalizationRegistry` to auto-deinit `JSClosure` ([#131](https://github.com/swiftwasm/JavaScriptKit/issues/131)) - `make test` crashes due to `JSClosure` memory issues ([#129](https://github.com/swiftwasm/JavaScriptKit/issues/129)) - Avoid manual memory management with `JSClosure` ([#106](https://github.com/swiftwasm/JavaScriptKit/issues/106)) @@ -76,7 +275,7 @@ tweaks. **Merged pull requests:** -- Update JS dependencies in package-lock.json ([#126](https://github.com/swiftwasm/JavaScriptKit/pull/126)) via [@MaxDesiatov] +- Update JS dependencies in package-lock.json ([#126](https://github.com/swiftwasm/JavaScriptKit/pull/126)) via [@MaxDesiatov] - Fix typo in method documentation ([#125](https://github.com/swiftwasm/JavaScriptKit/pull/125)) via [@revolter] - Update exported func name to match exported name ([#123](https://github.com/swiftwasm/JavaScriptKit/pull/123)) via [@kateinoigakukun] - Fix incorrect link in `JSDate` documentation ([#122](https://github.com/swiftwasm/JavaScriptKit/pull/122)) via [@revolter] @@ -86,18 +285,18 @@ tweaks. This release contains multiple breaking changes in preparation for enabling `async`/`await`, when this feature is available in a stable SwiftWasm release. Namely: -* `JSClosure.init(_ body: @escaping ([JSValue]) -> ())` overload is deprecated to simplify type -checking. Its presence requires explicit type signatures at the place of use. It will be removed -in a future version of JavaScriptKit. -* `JSClosure` is no longer a subclass of `JSFunction`. These classes are not related enough to keep -them in the same class hierarchy. -As a result, you can no longer call `JSClosure` objects directly from Swift. -* Introduced `JSOneshotClosure` for closures that are going to be called only once. You don't need -to manage references to these closures manually, as opposed to `JSClosure`. -However, they can only be called a single time from the JS side. Subsequent invocation attempts will raise a fatal error on the Swift side. -* Removed generic parameters on `JSPromise`, now both success and failure values are always assumed -to be of `JSValue` type. This also significantly simplifies type checking and allows callers to -fully control type casting if needed. +- `JSClosure.init(_ body: @escaping ([JSValue]) -> ())` overload is deprecated to simplify type + checking. Its presence requires explicit type signatures at the place of use. It will be removed + in a future version of JavaScriptKit. +- `JSClosure` is no longer a subclass of `JSFunction`. These classes are not related enough to keep + them in the same class hierarchy. + As a result, you can no longer call `JSClosure` objects directly from Swift. +- Introduced `JSOneshotClosure` for closures that are going to be called only once. You don't need + to manage references to these closures manually, as opposed to `JSClosure`. + However, they can only be called a single time from the JS side. Subsequent invocation attempts will raise a fatal error on the Swift side. +- Removed generic parameters on `JSPromise`, now both success and failure values are always assumed + to be of `JSValue` type. This also significantly simplifies type checking and allows callers to + fully control type casting if needed. **Closed issues:** @@ -179,7 +378,7 @@ with idiomatic Swift code. - Update toolchain version, script, and `README.md` ([#96](https://github.com/swiftwasm/JavaScriptKit/pull/96)) via [@MaxDesiatov] - [Proposal] Add unsafe convenience methods for JSValue ([#98](https://github.com/swiftwasm/JavaScriptKit/pull/98)) via [@kateinoigakukun] - Remove all unsafe linker flags from Package.swift ([#91](https://github.com/swiftwasm/JavaScriptKit/pull/91)) via [@kateinoigakukun] -- Sync package.json and package-lock.json ([#90](https://github.com/swiftwasm/JavaScriptKit/pull/90)) via [@kateinoigakukun] +- Sync package.json and package-lock.json ([#90](https://github.com/swiftwasm/JavaScriptKit/pull/90)) via [@kateinoigakukun] - Rename JSValueConvertible/Constructible/Codable ([#88](https://github.com/swiftwasm/JavaScriptKit/pull/88)) via [@j-f1] - Bump @actions/core from 1.2.2 to 1.2.6 in /ci/perf-tester ([#89](https://github.com/swiftwasm/JavaScriptKit/pull/89)) via [@dependabot] - Make `JSError` conform to `JSBridgedClass` ([#86](https://github.com/swiftwasm/JavaScriptKit/pull/86)) via [@MaxDesiatov] @@ -257,10 +456,10 @@ This release adds `JSTypedArray` generic type, renames `JSObjectRef` to `JSObjec - Clean up the `JSObjectRef` API ([#28](https://github.com/swiftwasm/JavaScriptKit/pull/28)) via [@j-f1] - Remove unused `Tests` directory ([#32](https://github.com/swiftwasm/JavaScriptKit/pull/32)) via [@MaxDesiatov] -[@MaxDesiatov]: https://github.com/MaxDesiatov +[@maxdesiatov]: https://github.com/MaxDesiatov [@j-f1]: https://github.com/j-f1 [@kateinoigakukun]: https://github.com/kateinoigakukun [@yonihemi]: https://github.com/yonihemi -[@PatrickPijnappel]: https://github.com/PatrickPijnappel +[@patrickpijnappel]: https://github.com/PatrickPijnappel [@revolter]: https://github.com/revolter [@dependabot]: https://github.com/dependabot diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 000000000..f71ca83ae --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,85 @@ +# Contributing to JavaScriptKit + +Thank you for considering contributing to JavaScriptKit! We welcome contributions of all kinds and value your time and effort. + +## Getting Started + +### Reporting Issues +- If you find a bug, have a feature request, or need help, please [open an issue](https://github.com/swiftwasm/JavaScriptKit/issues). +- Provide as much detail as possible: + - Steps to reproduce the issue + - Expected vs. actual behavior + - Relevant error messages or logs + +### Setting Up the Development Environment +1. Clone the repository: + ```bash + git clone https://github.com/swiftwasm/JavaScriptKit.git + cd JavaScriptKit + ``` + +2. Install **OSS** Swift toolchain (not the one from Xcode): +
+ For macOS users + + ```bash + ( + SWIFT_TOOLCHAIN_CHANNEL=swift-6.0.2-release; + SWIFT_TOOLCHAIN_TAG="swift-6.0.2-RELEASE"; + SWIFT_SDK_TAG="swift-wasm-6.0.2-RELEASE"; + SWIFT_SDK_CHECKSUM="6ffedb055cb9956395d9f435d03d53ebe9f6a8d45106b979d1b7f53358e1dcb4"; + pkg="$(mktemp -d)/InstallMe.pkg"; set -ex; + curl -o "$pkg" "https://download.swift.org/$SWIFT_TOOLCHAIN_CHANNEL/xcode/$SWIFT_TOOLCHAIN_TAG/$SWIFT_TOOLCHAIN_TAG-osx.pkg"; + installer -pkg "$pkg" -target CurrentUserHomeDirectory; + export TOOLCHAINS="$(plutil -extract CFBundleIdentifier raw ~/Library/Developer/Toolchains/$SWIFT_TOOLCHAIN_TAG.xctoolchain/Info.plist)"; + swift sdk install "https://github.com/swiftwasm/swift/releases/download/$SWIFT_SDK_TAG/$SWIFT_SDK_TAG-wasm32-unknown-wasi.artifactbundle.zip" --checksum "$SWIFT_SDK_CHECKSUM"; + ) + ``` + +
+ +
+ For Linux users + Install Swift 6.0.2 by following the instructions on the official Swift website. + + ```bash + ( + SWIFT_SDK_TAG="swift-wasm-6.0.2-RELEASE"; + SWIFT_SDK_CHECKSUM="6ffedb055cb9956395d9f435d03d53ebe9f6a8d45106b979d1b7f53358e1dcb4"; + swift sdk install "https://github.com/swiftwasm/swift/releases/download/$SWIFT_SDK_TAG/$SWIFT_SDK_TAG-wasm32-unknown-wasi.artifactbundle.zip" --checksum "$SWIFT_SDK_CHECKSUM"; + ) + ``` + +
+ +3. Install dependencies: + ```bash + make bootstrap + ``` + +### Running Tests + +Unit tests running on WebAssembly: + +```bash +make unittest SWIFT_SDK_ID=wasm32-unknown-wasi +``` + +Tests for `PackageToJS` plugin: + +```bash +swift test --package-path ./Plugins/PackageToJS +``` + +### Editing `./Runtime` directory + +The `./Runtime` directory contains the JavaScript runtime that interacts with the JavaScript environment and Swift code. +The runtime is written in TypeScript and is checked into the repository as compiled JavaScript files. +To make changes to the runtime, you need to edit the TypeScript files and regenerate the JavaScript files by running: + +```bash +make regenerate_swiftpm_resources +``` + +## Support +If you have any questions or need assistance, feel free to reach out via [GitHub Issues](https://github.com/swiftwasm/JavaScriptKit/issues) or [Discord](https://discord.gg/ashJW8T8yp). diff --git a/Example/JavaScriptKitExample/.gitignore b/Example/JavaScriptKitExample/.gitignore deleted file mode 100644 index 95c432091..000000000 --- a/Example/JavaScriptKitExample/.gitignore +++ /dev/null @@ -1,5 +0,0 @@ -.DS_Store -/.build -/Packages -/*.xcodeproj -xcuserdata/ diff --git a/Example/JavaScriptKitExample/Sources/JavaScriptKitExample/main.swift b/Example/JavaScriptKitExample/Sources/JavaScriptKitExample/main.swift deleted file mode 100644 index 98b8a6bb5..000000000 --- a/Example/JavaScriptKitExample/Sources/JavaScriptKitExample/main.swift +++ /dev/null @@ -1,48 +0,0 @@ -import JavaScriptKit -import JavaScriptEventLoop - -let alert = JSObject.global.alert.function! -let document = JSObject.global.document - -var divElement = document.createElement("div") -divElement.innerText = "Hello, world" -_ = document.body.appendChild(divElement) - -var buttonElement = document.createElement("button") -buttonElement.innerText = "Alert demo" -buttonElement.onclick = .object(JSClosure { _ in - alert("Swift is running on browser!") - return .undefined -}) - -_ = document.body.appendChild(buttonElement) - -private let jsFetch = JSObject.global.fetch.function! -func fetch(_ url: String) -> JSPromise { - JSPromise(jsFetch(url).object!)! -} - -JavaScriptEventLoop.installGlobalExecutor() - -struct Response: Decodable { - let uuid: String -} - -var 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) - } - } - - return .undefined -}) - -_ = document.body.appendChild(asyncButtonElement) diff --git a/Example/Makefile b/Example/Makefile deleted file mode 100644 index acfc5eadc..000000000 --- a/Example/Makefile +++ /dev/null @@ -1,16 +0,0 @@ -MAKEFILE_DIR := $(dir $(lastword $(MAKEFILE_LIST))) - -.PHONY: JavaScriptKitExample -JavaScriptKitExample: - cd JavaScriptKitExample && \ - swift build --triple wasm32-unknown-wasi - -dist/JavaScriptKitExample.wasm: JavaScriptKitExample - mkdir -p dist - cp ./JavaScriptKitExample/.build/debug/JavaScriptKitExample.wasm $@ - -node_modules: - npm ci -build: node_modules dist/JavaScriptKitExample.wasm - cd ../Runtime && npm run build - npm run build diff --git a/Example/README.md b/Example/README.md deleted file mode 100644 index aa1775a54..000000000 --- a/Example/README.md +++ /dev/null @@ -1,9 +0,0 @@ -# Example project of JavaScriptKit - -## Bootstrap - -```sh -$ make build -$ npm run start -$ # Open http://localhost:8080 on your browser -``` diff --git a/Example/package-lock.json b/Example/package-lock.json deleted file mode 100644 index 4c2c31c2e..000000000 --- a/Example/package-lock.json +++ /dev/null @@ -1,6502 +0,0 @@ -{ - "name": "javascript-kit-example", - "version": "1.0.0", - "lockfileVersion": 2, - "requires": true, - "packages": { - "": { - "name": "javascript-kit-example", - "version": "1.0.0", - "license": "MIT", - "dependencies": { - "@wasmer/wasi": "^0.12.0", - "@wasmer/wasmfs": "^0.12.0", - "javascript-kit-swift": "file:.." - }, - "devDependencies": { - "webpack": "^5.64.2", - "webpack-cli": "^4.9.1", - "webpack-dev-server": "^4.5.0" - } - }, - "..": { - "name": "javascript-kit-swift", - "version": "0.12.0", - "license": "MIT", - "devDependencies": { - "@rollup/plugin-typescript": "^8.3.0", - "prettier": "2.5.1", - "rollup": "^2.63.0", - "tslib": "^2.3.1", - "typescript": "^4.5.5" - } - }, - "../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.2.4", - "integrity": "sha512-V+evlYHZnQkaz8TRBuxTA92yZBPotr5H+WhQ7bD3hZUndx5tGOa1fuCgeSjxAzM1RiN5IzvadIXTVefuuwZCRg==", - "dev": true, - "bin": { - "tsc": "bin/tsc", - "tsserver": "bin/tsserver" - }, - "engines": { - "node": ">=4.2.0" - } - }, - "node_modules/@discoveryjs/json-ext": { - "version": "0.5.5", - "resolved": "https://registry.npmjs.org/@discoveryjs/json-ext/-/json-ext-0.5.5.tgz", - "integrity": "sha512-6nFkfkmSeV/rqSaS4oWHgmpnYw194f6hmWF5is6b0J1naJZoiD0NTc9AiUwPHvWsowkjuHErCZT1wa0jg+BLIA==", - "dev": true, - "engines": { - "node": ">=10.0.0" - } - }, - "node_modules/@nodelib/fs.scandir": { - "version": "2.1.5", - "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", - "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", - "dev": true, - "dependencies": { - "@nodelib/fs.stat": "2.0.5", - "run-parallel": "^1.1.9" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/@nodelib/fs.stat": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", - "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", - "dev": true, - "engines": { - "node": ">= 8" - } - }, - "node_modules/@nodelib/fs.walk": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", - "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", - "dev": true, - "dependencies": { - "@nodelib/fs.scandir": "2.1.5", - "fastq": "^1.6.0" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/@types/eslint": { - "version": "8.2.0", - "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-8.2.0.tgz", - "integrity": "sha512-74hbvsnc+7TEDa1z5YLSe4/q8hGYB3USNvCuzHUJrjPV6hXaq8IXcngCrHkuvFt0+8rFz7xYXrHgNayIX0UZvQ==", - "dev": true, - "dependencies": { - "@types/estree": "*", - "@types/json-schema": "*" - } - }, - "node_modules/@types/eslint-scope": { - "version": "3.7.1", - "resolved": "https://registry.npmjs.org/@types/eslint-scope/-/eslint-scope-3.7.1.tgz", - "integrity": "sha512-SCFeogqiptms4Fg29WpOTk5nHIzfpKCemSN63ksBQYKTcXoJEmJagV+DhVmbapZzY4/5YaOV1nZwrsU79fFm1g==", - "dev": true, - "dependencies": { - "@types/eslint": "*", - "@types/estree": "*" - } - }, - "node_modules/@types/estree": { - "version": "0.0.50", - "resolved": "https://registry.npmjs.org/@types/estree/-/estree-0.0.50.tgz", - "integrity": "sha512-C6N5s2ZFtuZRj54k2/zyRhNDjJwwcViAM3Nbm8zjBpbqAdZ00mr0CFxvSKeO8Y/e03WVFLpQMdHYVfUd6SB+Hw==", - "dev": true - }, - "node_modules/@types/http-proxy": { - "version": "1.17.7", - "resolved": "https://registry.npmjs.org/@types/http-proxy/-/http-proxy-1.17.7.tgz", - "integrity": "sha512-9hdj6iXH64tHSLTY+Vt2eYOGzSogC+JQ2H7bdPWkuh7KXP5qLllWx++t+K9Wk556c3dkDdPws/SpMRi0sdCT1w==", - "dev": true, - "dependencies": { - "@types/node": "*" - } - }, - "node_modules/@types/json-schema": { - "version": "7.0.9", - "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.9.tgz", - "integrity": "sha512-qcUXuemtEu+E5wZSJHNxUXeCZhAfXKQ41D+duX+VYPde7xyEVZci+/oXKJL13tnRs9lR2pr4fod59GT6/X1/yQ==", - "dev": true - }, - "node_modules/@types/node": { - "version": "16.11.9", - "resolved": "https://registry.npmjs.org/@types/node/-/node-16.11.9.tgz", - "integrity": "sha512-MKmdASMf3LtPzwLyRrFjtFFZ48cMf8jmX5VRYrDQiJa8Ybu5VAmkqBWqKU8fdCwD8ysw4mQ9nrEHvzg6gunR7A==", - "dev": true - }, - "node_modules/@types/retry": { - "version": "0.12.1", - "resolved": "https://registry.npmjs.org/@types/retry/-/retry-0.12.1.tgz", - "integrity": "sha512-xoDlM2S4ortawSWORYqsdU+2rxdh4LRW9ytc3zmT37RIKQh6IHyKwwtKhKis9ah8ol07DCkZxPt8BBvPjC6v4g==", - "dev": true - }, - "node_modules/@wasmer/wasi": { - "version": "0.12.0", - "integrity": "sha512-FJhLZKAfLWm/yjQI7eCRHNbA8ezmb7LSpUYFkHruZXs2mXk2+DaQtSElEtOoNrVQ4vApTyVaAd5/b7uEu8w6wQ==", - "dependencies": { - "browser-process-hrtime": "^1.0.0", - "buffer-es6": "^4.9.3", - "path-browserify": "^1.0.0", - "randomfill": "^1.0.4" - } - }, - "node_modules/@wasmer/wasmfs": { - "version": "0.12.0", - "integrity": "sha512-m1ftchyQ1DfSenm5XbbdGIpb6KJHH5z0gODo3IZr6lATkj4WXfX/UeBTZ0aG9YVShBp+kHLdUHvOkqjy6p/GWw==", - "dependencies": { - "memfs": "3.0.4", - "pako": "^1.0.11", - "tar-stream": "^2.1.0" - } - }, - "node_modules/@webassemblyjs/ast": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.11.1.tgz", - "integrity": "sha512-ukBh14qFLjxTQNTXocdyksN5QdM28S1CxHt2rdskFyL+xFV7VremuBLVbmCePj+URalXBENx/9Lm7lnhihtCSw==", - "dev": true, - "dependencies": { - "@webassemblyjs/helper-numbers": "1.11.1", - "@webassemblyjs/helper-wasm-bytecode": "1.11.1" - } - }, - "node_modules/@webassemblyjs/floating-point-hex-parser": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.11.1.tgz", - "integrity": "sha512-iGRfyc5Bq+NnNuX8b5hwBrRjzf0ocrJPI6GWFodBFzmFnyvrQ83SHKhmilCU/8Jv67i4GJZBMhEzltxzcNagtQ==", - "dev": true - }, - "node_modules/@webassemblyjs/helper-api-error": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-api-error/-/helper-api-error-1.11.1.tgz", - "integrity": "sha512-RlhS8CBCXfRUR/cwo2ho9bkheSXG0+NwooXcc3PAILALf2QLdFyj7KGsKRbVc95hZnhnERon4kW/D3SZpp6Tcg==", - "dev": true - }, - "node_modules/@webassemblyjs/helper-buffer": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-buffer/-/helper-buffer-1.11.1.tgz", - "integrity": "sha512-gwikF65aDNeeXa8JxXa2BAk+REjSyhrNC9ZwdT0f8jc4dQQeDQ7G4m0f2QCLPJiMTTO6wfDmRmj/pW0PsUvIcA==", - "dev": true - }, - "node_modules/@webassemblyjs/helper-numbers": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-numbers/-/helper-numbers-1.11.1.tgz", - "integrity": "sha512-vDkbxiB8zfnPdNK9Rajcey5C0w+QJugEglN0of+kmO8l7lDb77AnlKYQF7aarZuCrv+l0UvqL+68gSDr3k9LPQ==", - "dev": true, - "dependencies": { - "@webassemblyjs/floating-point-hex-parser": "1.11.1", - "@webassemblyjs/helper-api-error": "1.11.1", - "@xtuc/long": "4.2.2" - } - }, - "node_modules/@webassemblyjs/helper-wasm-bytecode": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.11.1.tgz", - "integrity": "sha512-PvpoOGiJwXeTrSf/qfudJhwlvDQxFgelbMqtq52WWiXC6Xgg1IREdngmPN3bs4RoO83PnL/nFrxucXj1+BX62Q==", - "dev": true - }, - "node_modules/@webassemblyjs/helper-wasm-section": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.11.1.tgz", - "integrity": "sha512-10P9No29rYX1j7F3EVPX3JvGPQPae+AomuSTPiF9eBQeChHI6iqjMIwR9JmOJXwpnn/oVGDk7I5IlskuMwU/pg==", - "dev": true, - "dependencies": { - "@webassemblyjs/ast": "1.11.1", - "@webassemblyjs/helper-buffer": "1.11.1", - "@webassemblyjs/helper-wasm-bytecode": "1.11.1", - "@webassemblyjs/wasm-gen": "1.11.1" - } - }, - "node_modules/@webassemblyjs/ieee754": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/ieee754/-/ieee754-1.11.1.tgz", - "integrity": "sha512-hJ87QIPtAMKbFq6CGTkZYJivEwZDbQUgYd3qKSadTNOhVY7p+gfP6Sr0lLRVTaG1JjFj+r3YchoqRYxNH3M0GQ==", - "dev": true, - "dependencies": { - "@xtuc/ieee754": "^1.2.0" - } - }, - "node_modules/@webassemblyjs/leb128": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/leb128/-/leb128-1.11.1.tgz", - "integrity": "sha512-BJ2P0hNZ0u+Th1YZXJpzW6miwqQUGcIHT1G/sf72gLVD9DZ5AdYTqPNbHZh6K1M5VmKvFXwGSWZADz+qBWxeRw==", - "dev": true, - "dependencies": { - "@xtuc/long": "4.2.2" - } - }, - "node_modules/@webassemblyjs/utf8": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/utf8/-/utf8-1.11.1.tgz", - "integrity": "sha512-9kqcxAEdMhiwQkHpkNiorZzqpGrodQQ2IGrHHxCy+Ozng0ofyMA0lTqiLkVs1uzTRejX+/O0EOT7KxqVPuXosQ==", - "dev": true - }, - "node_modules/@webassemblyjs/wasm-edit": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-edit/-/wasm-edit-1.11.1.tgz", - "integrity": "sha512-g+RsupUC1aTHfR8CDgnsVRVZFJqdkFHpsHMfJuWQzWU3tvnLC07UqHICfP+4XyL2tnr1amvl1Sdp06TnYCmVkA==", - "dev": true, - "dependencies": { - "@webassemblyjs/ast": "1.11.1", - "@webassemblyjs/helper-buffer": "1.11.1", - "@webassemblyjs/helper-wasm-bytecode": "1.11.1", - "@webassemblyjs/helper-wasm-section": "1.11.1", - "@webassemblyjs/wasm-gen": "1.11.1", - "@webassemblyjs/wasm-opt": "1.11.1", - "@webassemblyjs/wasm-parser": "1.11.1", - "@webassemblyjs/wast-printer": "1.11.1" - } - }, - "node_modules/@webassemblyjs/wasm-gen": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-gen/-/wasm-gen-1.11.1.tgz", - "integrity": "sha512-F7QqKXwwNlMmsulj6+O7r4mmtAlCWfO/0HdgOxSklZfQcDu0TpLiD1mRt/zF25Bk59FIjEuGAIyn5ei4yMfLhA==", - "dev": true, - "dependencies": { - "@webassemblyjs/ast": "1.11.1", - "@webassemblyjs/helper-wasm-bytecode": "1.11.1", - "@webassemblyjs/ieee754": "1.11.1", - "@webassemblyjs/leb128": "1.11.1", - "@webassemblyjs/utf8": "1.11.1" - } - }, - "node_modules/@webassemblyjs/wasm-opt": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-opt/-/wasm-opt-1.11.1.tgz", - "integrity": "sha512-VqnkNqnZlU5EB64pp1l7hdm3hmQw7Vgqa0KF/KCNO9sIpI6Fk6brDEiX+iCOYrvMuBWDws0NkTOxYEb85XQHHw==", - "dev": true, - "dependencies": { - "@webassemblyjs/ast": "1.11.1", - "@webassemblyjs/helper-buffer": "1.11.1", - "@webassemblyjs/wasm-gen": "1.11.1", - "@webassemblyjs/wasm-parser": "1.11.1" - } - }, - "node_modules/@webassemblyjs/wasm-parser": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-parser/-/wasm-parser-1.11.1.tgz", - "integrity": "sha512-rrBujw+dJu32gYB7/Lup6UhdkPx9S9SnobZzRVL7VcBH9Bt9bCBLEuX/YXOOtBsOZ4NQrRykKhffRWHvigQvOA==", - "dev": true, - "dependencies": { - "@webassemblyjs/ast": "1.11.1", - "@webassemblyjs/helper-api-error": "1.11.1", - "@webassemblyjs/helper-wasm-bytecode": "1.11.1", - "@webassemblyjs/ieee754": "1.11.1", - "@webassemblyjs/leb128": "1.11.1", - "@webassemblyjs/utf8": "1.11.1" - } - }, - "node_modules/@webassemblyjs/wast-printer": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wast-printer/-/wast-printer-1.11.1.tgz", - "integrity": "sha512-IQboUWM4eKzWW+N/jij2sRatKMh99QEelo3Eb2q0qXkvPRISAj8Qxtmw5itwqK+TTkBuUIE45AxYPToqPtL5gg==", - "dev": true, - "dependencies": { - "@webassemblyjs/ast": "1.11.1", - "@xtuc/long": "4.2.2" - } - }, - "node_modules/@webpack-cli/configtest": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@webpack-cli/configtest/-/configtest-1.1.0.tgz", - "integrity": "sha512-ttOkEkoalEHa7RaFYpM0ErK1xc4twg3Am9hfHhL7MVqlHebnkYd2wuI/ZqTDj0cVzZho6PdinY0phFZV3O0Mzg==", - "dev": true, - "peerDependencies": { - "webpack": "4.x.x || 5.x.x", - "webpack-cli": "4.x.x" - } - }, - "node_modules/@webpack-cli/info": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/@webpack-cli/info/-/info-1.4.0.tgz", - "integrity": "sha512-F6b+Man0rwE4n0409FyAJHStYA5OIZERxmnUfLVwv0mc0V1wLad3V7jqRlMkgKBeAq07jUvglacNaa6g9lOpuw==", - "dev": true, - "dependencies": { - "envinfo": "^7.7.3" - }, - "peerDependencies": { - "webpack-cli": "4.x.x" - } - }, - "node_modules/@webpack-cli/serve": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/@webpack-cli/serve/-/serve-1.6.0.tgz", - "integrity": "sha512-ZkVeqEmRpBV2GHvjjUZqEai2PpUbuq8Bqd//vEYsp63J8WyexI8ppCqVS3Zs0QADf6aWuPdU+0XsPI647PVlQA==", - "dev": true, - "peerDependencies": { - "webpack-cli": "4.x.x" - }, - "peerDependenciesMeta": { - "webpack-dev-server": { - "optional": true - } - } - }, - "node_modules/@xtuc/ieee754": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/@xtuc/ieee754/-/ieee754-1.2.0.tgz", - "integrity": "sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA==", - "dev": true - }, - "node_modules/@xtuc/long": { - "version": "4.2.2", - "resolved": "https://registry.npmjs.org/@xtuc/long/-/long-4.2.2.tgz", - "integrity": "sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==", - "dev": true - }, - "node_modules/accepts": { - "version": "1.3.7", - "integrity": "sha512-Il80Qs2WjYlJIBNzNkK6KYqlVMTbZLXgHx2oT0pU/fjRHyEp+PEfEPY0R3WCwAGVOtauxh1hOxNgIf5bv7dQpA==", - "dev": true, - "dependencies": { - "mime-types": "~2.1.24", - "negotiator": "0.6.2" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/acorn": { - "version": "8.6.0", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.6.0.tgz", - "integrity": "sha512-U1riIR+lBSNi3IbxtaHOIKdH8sLFv3NYfNv8sg7ZsNhcfl4HF2++BfqqrNAxoCLQW1iiylOj76ecnaUxz+z9yw==", - "dev": true, - "bin": { - "acorn": "bin/acorn" - }, - "engines": { - "node": ">=0.4.0" - } - }, - "node_modules/acorn-import-assertions": { - "version": "1.8.0", - "resolved": "https://registry.npmjs.org/acorn-import-assertions/-/acorn-import-assertions-1.8.0.tgz", - "integrity": "sha512-m7VZ3jwz4eK6A4Vtt8Ew1/mNbP24u0FhdyfA7fSvnJR6LMdfOYnmuIrrJAgrYfYJ10F/otaHTtrtrtmHdMNzEw==", - "dev": true, - "peerDependencies": { - "acorn": "^8" - } - }, - "node_modules/aggregate-error": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/aggregate-error/-/aggregate-error-3.1.0.tgz", - "integrity": "sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA==", - "dev": true, - "dependencies": { - "clean-stack": "^2.0.0", - "indent-string": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/ajv": { - "version": "6.12.6", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", - "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", - "dev": true, - "dependencies": { - "fast-deep-equal": "^3.1.1", - "fast-json-stable-stringify": "^2.0.0", - "json-schema-traverse": "^0.4.1", - "uri-js": "^4.2.2" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" - } - }, - "node_modules/ajv-formats": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-2.1.1.tgz", - "integrity": "sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==", - "dev": true, - "dependencies": { - "ajv": "^8.0.0" - }, - "peerDependencies": { - "ajv": "^8.0.0" - }, - "peerDependenciesMeta": { - "ajv": { - "optional": true - } - } - }, - "node_modules/ajv-formats/node_modules/ajv": { - "version": "8.8.2", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.8.2.tgz", - "integrity": "sha512-x9VuX+R/jcFj1DHo/fCp99esgGDWiHENrKxaCENuCxpoMCmAt/COCGVDwA7kleEpEzJjDnvh3yGoOuLu0Dtllw==", - "dev": true, - "dependencies": { - "fast-deep-equal": "^3.1.1", - "json-schema-traverse": "^1.0.0", - "require-from-string": "^2.0.2", - "uri-js": "^4.2.2" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" - } - }, - "node_modules/ajv-formats/node_modules/json-schema-traverse": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", - "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", - "dev": true - }, - "node_modules/ajv-keywords": { - "version": "3.5.2", - "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz", - "integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==", - "dev": true, - "peerDependencies": { - "ajv": "^6.9.1" - } - }, - "node_modules/ansi-html-community": { - "version": "0.0.8", - "resolved": "https://registry.npmjs.org/ansi-html-community/-/ansi-html-community-0.0.8.tgz", - "integrity": "sha512-1APHAyr3+PCamwNw3bXCPp4HFLONZt/yIH0sZp0/469KWNTEy+qN5jQ3GVX6DMZ1UXAi34yVwtTeaG/HpBuuzw==", - "dev": true, - "engines": [ - "node >= 0.8.0" - ], - "bin": { - "ansi-html": "bin/ansi-html" - } - }, - "node_modules/ansi-regex": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.0.1.tgz", - "integrity": "sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==", - "dev": true, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-regex?sponsor=1" - } - }, - "node_modules/anymatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.2.tgz", - "integrity": "sha512-P43ePfOAIupkguHUycrc4qJ9kz8ZiuOUijaETwX7THt0Y/GNK7v0aa8rY816xWjZ7rJdA5XdMcpVFTKMq+RvWg==", - "dev": true, - "dependencies": { - "normalize-path": "^3.0.0", - "picomatch": "^2.0.4" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/array-flatten": { - "version": "2.1.2", - "integrity": "sha512-hNfzcOV8W4NdualtqBFPyVO+54DSJuZGY9qT4pRroB6S9e3iiido2ISIC5h9R2sPJ8H3FHCIiEnsv1lPXO3KtQ==", - "dev": true - }, - "node_modules/array-union": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz", - "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/async": { - "version": "2.6.3", - "integrity": "sha512-zflvls11DCy+dQWzTW2dzuilv8Z5X/pjfmZOWba6TNIVDm+2UDaJmXSOXlasHKfNBs8oo3M0aT50fDEWfKZjXg==", - "dev": true, - "dependencies": { - "lodash": "^4.17.14" - } - }, - "node_modules/balanced-match": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", - "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", - "dev": true - }, - "node_modules/base64-js": { - "version": "1.5.1", - "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ] - }, - "node_modules/batch": { - "version": "0.6.1", - "integrity": "sha1-3DQxT05nkxgJP8dgJyUl+UvyXBY=", - "dev": true - }, - "node_modules/binary-extensions": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz", - "integrity": "sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/bl": { - "version": "4.1.0", - "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", - "dependencies": { - "buffer": "^5.5.0", - "inherits": "^2.0.4", - "readable-stream": "^3.4.0" - } - }, - "node_modules/body-parser": { - "version": "1.19.0", - "integrity": "sha512-dhEPs72UPbDnAQJ9ZKMNTP6ptJaionhP5cBb541nXPlW60Jepo9RV/a4fX4XWW9CuFNK22krhrj1+rgzifNCsw==", - "dev": true, - "dependencies": { - "bytes": "3.1.0", - "content-type": "~1.0.4", - "debug": "2.6.9", - "depd": "~1.1.2", - "http-errors": "1.7.2", - "iconv-lite": "0.4.24", - "on-finished": "~2.3.0", - "qs": "6.7.0", - "raw-body": "2.4.0", - "type-is": "~1.6.17" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/body-parser/node_modules/bytes": { - "version": "3.1.0", - "integrity": "sha512-zauLjrfCG+xvoyaqLoV8bLVXXNGC4JqlxFCutSDWA6fJrTo2ZuvLYTqZ7aHBLZSMOopbzwv8f+wZcVzfVTI2Dg==", - "dev": true, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/bonjour": { - "version": "3.5.0", - "integrity": "sha1-jokKGD2O6aI5OzhExpGkK897yfU=", - "dev": true, - "dependencies": { - "array-flatten": "^2.1.0", - "deep-equal": "^1.0.1", - "dns-equal": "^1.0.0", - "dns-txt": "^2.0.2", - "multicast-dns": "^6.0.1", - "multicast-dns-service-types": "^1.1.0" - } - }, - "node_modules/brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", - "dev": true, - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "node_modules/braces": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", - "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", - "dev": true, - "dependencies": { - "fill-range": "^7.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/browser-process-hrtime": { - "version": "1.0.0", - "integrity": "sha512-9o5UecI3GhkpM6DrXr69PblIuWxPKk9Y0jHBRhdocZ2y7YECBFCsHm79Pr3OyR2AvjhDkabFJaDJMYRazHgsow==" - }, - "node_modules/browserslist": { - "version": "4.18.1", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.18.1.tgz", - "integrity": "sha512-8ScCzdpPwR2wQh8IT82CA2VgDwjHyqMovPBZSNH54+tm4Jk2pCuv90gmAdH6J84OCRWi0b4gMe6O6XPXuJnjgQ==", - "dev": true, - "dependencies": { - "caniuse-lite": "^1.0.30001280", - "electron-to-chromium": "^1.3.896", - "escalade": "^3.1.1", - "node-releases": "^2.0.1", - "picocolors": "^1.0.0" - }, - "bin": { - "browserslist": "cli.js" - }, - "engines": { - "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/browserslist" - } - }, - "node_modules/buffer": { - "version": "5.7.1", - "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "dependencies": { - "base64-js": "^1.3.1", - "ieee754": "^1.1.13" - } - }, - "node_modules/buffer-es6": { - "version": "4.9.3", - "integrity": "sha1-8mNHuC33b9N+GLy1KIxJcM/VxAQ=" - }, - "node_modules/buffer-from": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", - "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", - "dev": true - }, - "node_modules/buffer-indexof": { - "version": "1.1.1", - "integrity": "sha512-4/rOEg86jivtPTeOUUT61jJO1Ya1TrR/OkqCSZDyq84WJh3LuuiphBYJN+fm5xufIk4XAFcEwte/8WzC8If/1g==", - "dev": true - }, - "node_modules/bytes": { - "version": "3.0.0", - "integrity": "sha1-0ygVQE1olpn4Wk6k+odV3ROpYEg=", - "dev": true, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/call-bind": { - "version": "1.0.2", - "integrity": "sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==", - "dev": true, - "dependencies": { - "function-bind": "^1.1.1", - "get-intrinsic": "^1.0.2" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/caniuse-lite": { - "version": "1.0.30001282", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001282.tgz", - "integrity": "sha512-YhF/hG6nqBEllymSIjLtR2iWDDnChvhnVJqp+vloyt2tEHFG1yBR+ac2B/rOw0qOK0m0lEXU2dv4E/sMk5P9Kg==", - "dev": true, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/browserslist" - } - }, - "node_modules/chokidar": { - "version": "3.5.2", - "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.2.tgz", - "integrity": "sha512-ekGhOnNVPgT77r4K/U3GDhu+FQ2S8TnK/s2KbIGXi0SZWuwkZ2QNyfWdZW+TVfn84DpEP7rLeCt2UI6bJ8GwbQ==", - "dev": true, - "dependencies": { - "anymatch": "~3.1.2", - "braces": "~3.0.2", - "glob-parent": "~5.1.2", - "is-binary-path": "~2.1.0", - "is-glob": "~4.0.1", - "normalize-path": "~3.0.0", - "readdirp": "~3.6.0" - }, - "engines": { - "node": ">= 8.10.0" - }, - "optionalDependencies": { - "fsevents": "~2.3.2" - } - }, - "node_modules/chrome-trace-event": { - "version": "1.0.3", - "integrity": "sha512-p3KULyQg4S7NIHixdwbGX+nFHkoBiA4YQmyWtjb8XngSKV124nJmRysgAeujbUVb15vh+RvFUfCPqU7rXk+hZg==", - "dev": true, - "engines": { - "node": ">=6.0" - } - }, - "node_modules/clean-stack": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/clean-stack/-/clean-stack-2.2.0.tgz", - "integrity": "sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A==", - "dev": true, - "engines": { - "node": ">=6" - } - }, - "node_modules/clone-deep": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/clone-deep/-/clone-deep-4.0.1.tgz", - "integrity": "sha512-neHB9xuzh/wk0dIHweyAXv2aPGZIVk3pLMe+/RNzINf17fe0OG96QroktYAUm7SM1PBnzTabaLboqqxDyMU+SQ==", - "dev": true, - "dependencies": { - "is-plain-object": "^2.0.4", - "kind-of": "^6.0.2", - "shallow-clone": "^3.0.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/colorette": { - "version": "2.0.16", - "resolved": "https://registry.npmjs.org/colorette/-/colorette-2.0.16.tgz", - "integrity": "sha512-hUewv7oMjCp+wkBv5Rm0v87eJhq4woh5rSR+42YSQJKecCqgIqNkZ6lAlQms/BwHPJA5NKMRlpxPRv0n8HQW6g==", - "dev": true - }, - "node_modules/commander": { - "version": "2.20.3", - "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", - "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", - "dev": true - }, - "node_modules/compressible": { - "version": "2.0.18", - "integrity": "sha512-AF3r7P5dWxL8MxyITRMlORQNaOA2IkAFaTr4k7BUumjPtRpGDTZpl0Pb1XCO6JeDCBdp126Cgs9sMxqSjgYyRg==", - "dev": true, - "dependencies": { - "mime-db": ">= 1.43.0 < 2" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/compression": { - "version": "1.7.4", - "integrity": "sha512-jaSIDzP9pZVS4ZfQ+TzvtiWhdpFhE2RDHz8QJkpX9SIpLq88VueF5jJw6t+6CUQcAoA6t+x89MLrWAqpfDE8iQ==", - "dev": true, - "dependencies": { - "accepts": "~1.3.5", - "bytes": "3.0.0", - "compressible": "~2.0.16", - "debug": "2.6.9", - "on-headers": "~1.0.2", - "safe-buffer": "5.1.2", - "vary": "~1.1.2" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/compression/node_modules/safe-buffer": { - "version": "5.1.2", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", - "dev": true - }, - "node_modules/concat-map": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=", - "dev": true - }, - "node_modules/connect-history-api-fallback": { - "version": "1.6.0", - "integrity": "sha512-e54B99q/OUoH64zYYRf3HBP5z24G38h5D3qXu23JGRoigpX5Ss4r9ZnDk3g0Z8uQC2x2lPaJ+UlWBc1ZWBWdLg==", - "dev": true, - "engines": { - "node": ">=0.8" - } - }, - "node_modules/content-disposition": { - "version": "0.5.3", - "integrity": "sha512-ExO0774ikEObIAEV9kDo50o+79VCUdEB6n6lzKgGwupcVeRlhrj3qGAfwq8G6uBJjkqLrhT0qEYFcWng8z1z0g==", - "dev": true, - "dependencies": { - "safe-buffer": "5.1.2" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/content-disposition/node_modules/safe-buffer": { - "version": "5.1.2", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", - "dev": true - }, - "node_modules/content-type": { - "version": "1.0.4", - "integrity": "sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA==", - "dev": true, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/cookie": { - "version": "0.4.0", - "integrity": "sha512-+Hp8fLp57wnUSt0tY0tHEXh4voZRDnoIrZPqlo3DPiI4y9lwg/jqx+1Om94/W6ZaPDOUbnjOt/99w66zk+l1Xg==", - "dev": true, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/cookie-signature": { - "version": "1.0.6", - "integrity": "sha1-4wOogrNCzD7oylE6eZmXNNqzriw=", - "dev": true - }, - "node_modules/core-util-is": { - "version": "1.0.2", - "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=", - "dev": true - }, - "node_modules/cross-spawn": { - "version": "7.0.3", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", - "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", - "dev": true, - "dependencies": { - "path-key": "^3.1.0", - "shebang-command": "^2.0.0", - "which": "^2.0.1" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/debug": { - "version": "2.6.9", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "dev": true, - "dependencies": { - "ms": "2.0.0" - } - }, - "node_modules/deep-equal": { - "version": "1.1.1", - "integrity": "sha512-yd9c5AdiqVcR+JjcwUQb9DkhJc8ngNr0MahEBGvDiJw8puWab2yZlh+nkasOnZP+EGTAP6rRp2JzJhJZzvNF8g==", - "dev": true, - "dependencies": { - "is-arguments": "^1.0.4", - "is-date-object": "^1.0.1", - "is-regex": "^1.0.4", - "object-is": "^1.0.1", - "object-keys": "^1.1.1", - "regexp.prototype.flags": "^1.2.0" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/default-gateway": { - "version": "6.0.3", - "resolved": "https://registry.npmjs.org/default-gateway/-/default-gateway-6.0.3.tgz", - "integrity": "sha512-fwSOJsbbNzZ/CUFpqFBqYfYNLj1NbMPm8MMCIzHjC83iSJRBEGmDUxU+WP661BaBQImeC2yHwXtz+P/O9o+XEg==", - "dev": true, - "dependencies": { - "execa": "^5.0.0" - }, - "engines": { - "node": ">= 10" - } - }, - "node_modules/define-lazy-prop": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-2.0.0.tgz", - "integrity": "sha512-Ds09qNh8yw3khSjiJjiUInaGX9xlqZDY7JVryGxdxV7NPeuqQfplOpQ66yJFZut3jLa5zOwkXw1g9EI2uKh4Og==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/define-properties": { - "version": "1.1.3", - "integrity": "sha512-3MqfYKj2lLzdMSf8ZIZE/V+Zuy+BgD6f164e8K2w7dgnpKArBDerGYpM46IYYcjnkdPNMjPk9A6VFB8+3SKlXQ==", - "dev": true, - "dependencies": { - "object-keys": "^1.0.12" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/del": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/del/-/del-6.0.0.tgz", - "integrity": "sha512-1shh9DQ23L16oXSZKB2JxpL7iMy2E0S9d517ptA1P8iw0alkPtQcrKH7ru31rYtKwF499HkTu+DRzq3TCKDFRQ==", - "dev": true, - "dependencies": { - "globby": "^11.0.1", - "graceful-fs": "^4.2.4", - "is-glob": "^4.0.1", - "is-path-cwd": "^2.2.0", - "is-path-inside": "^3.0.2", - "p-map": "^4.0.0", - "rimraf": "^3.0.2", - "slash": "^3.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/depd": { - "version": "1.1.2", - "integrity": "sha1-m81S4UwJd2PnSbJ0xDRu0uVgtak=", - "dev": true, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/destroy": { - "version": "1.0.4", - "integrity": "sha1-l4hXRCxEdJ5CBmE+N5RiBYJqvYA=", - "dev": true - }, - "node_modules/detect-node": { - "version": "2.0.5", - "integrity": "sha512-qi86tE6hRcFHy8jI1m2VG+LaPUR1LhqDa5G8tVjuUXmOrpuAgqsA1pN0+ldgr3aKUH+QLI9hCY/OcRYisERejw==", - "dev": true - }, - "node_modules/dir-glob": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", - "integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==", - "dev": true, - "dependencies": { - "path-type": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/dns-equal": { - "version": "1.0.0", - "integrity": "sha1-s55/HabrCnW6nBcySzR1PEfgZU0=", - "dev": true - }, - "node_modules/dns-packet": { - "version": "1.3.4", - "resolved": "https://registry.npmjs.org/dns-packet/-/dns-packet-1.3.4.tgz", - "integrity": "sha512-BQ6F4vycLXBvdrJZ6S3gZewt6rcrks9KBgM9vrhW+knGRqc8uEdT7fuCwloc7nny5xNoMJ17HGH0R/6fpo8ECA==", - "dev": true, - "dependencies": { - "ip": "^1.1.0", - "safe-buffer": "^5.0.1" - } - }, - "node_modules/dns-txt": { - "version": "2.0.2", - "integrity": "sha1-uR2Ab10nGI5Ks+fRB9iBocxGQrY=", - "dev": true, - "dependencies": { - "buffer-indexof": "^1.0.0" - } - }, - "node_modules/ee-first": { - "version": "1.1.1", - "integrity": "sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0=", - "dev": true - }, - "node_modules/electron-to-chromium": { - "version": "1.3.904", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.3.904.tgz", - "integrity": "sha512-x5uZWXcVNYkTh4JubD7KSC1VMKz0vZwJUqVwY3ihsW0bst1BXDe494Uqbg3Y0fDGVjJqA8vEeGuvO5foyH2+qw==", - "dev": true - }, - "node_modules/encodeurl": { - "version": "1.0.2", - "integrity": "sha1-rT/0yG7C0CkyL1oCw6mmBslbP1k=", - "dev": true, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/end-of-stream": { - "version": "1.4.4", - "integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==", - "dependencies": { - "once": "^1.4.0" - } - }, - "node_modules/enhanced-resolve": { - "version": "5.8.3", - "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.8.3.tgz", - "integrity": "sha512-EGAbGvH7j7Xt2nc0E7D99La1OiEs8LnyimkRgwExpUMScN6O+3x9tIWs7PLQZVNx4YD+00skHXPXi1yQHpAmZA==", - "dev": true, - "dependencies": { - "graceful-fs": "^4.2.4", - "tapable": "^2.2.0" - }, - "engines": { - "node": ">=10.13.0" - } - }, - "node_modules/envinfo": { - "version": "7.8.1", - "resolved": "https://registry.npmjs.org/envinfo/-/envinfo-7.8.1.tgz", - "integrity": "sha512-/o+BXHmB7ocbHEAs6F2EnG0ogybVVUdkRunTT2glZU9XAaGmhqskrvKwqXuDfNjEO0LZKWdejEEpnq8aM0tOaw==", - "dev": true, - "bin": { - "envinfo": "dist/cli.js" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/es-module-lexer": { - "version": "0.9.3", - "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-0.9.3.tgz", - "integrity": "sha512-1HQ2M2sPtxwnvOvT1ZClHyQDiggdNjURWpY2we6aMKCQiUVxTmVs2UYPLIrD84sS+kMdUwfBSylbJPwNnBrnHQ==", - "dev": true - }, - "node_modules/escalade": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz", - "integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==", - "dev": true, - "engines": { - "node": ">=6" - } - }, - "node_modules/escape-html": { - "version": "1.0.3", - "integrity": "sha1-Aljq5NPQwJdN4cFpGI7wBR0dGYg=", - "dev": true - }, - "node_modules/eslint-scope": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz", - "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==", - "dev": true, - "dependencies": { - "esrecurse": "^4.3.0", - "estraverse": "^4.1.1" - }, - "engines": { - "node": ">=8.0.0" - } - }, - "node_modules/esrecurse": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", - "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", - "dev": true, - "dependencies": { - "estraverse": "^5.2.0" - }, - "engines": { - "node": ">=4.0" - } - }, - "node_modules/esrecurse/node_modules/estraverse": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", - "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", - "dev": true, - "engines": { - "node": ">=4.0" - } - }, - "node_modules/estraverse": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", - "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", - "dev": true, - "engines": { - "node": ">=4.0" - } - }, - "node_modules/etag": { - "version": "1.8.1", - "integrity": "sha1-Qa4u62XvpiJorr/qg6x9eSmbCIc=", - "dev": true, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/eventemitter3": { - "version": "4.0.7", - "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz", - "integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==", - "dev": true - }, - "node_modules/events": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", - "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==", - "dev": true, - "engines": { - "node": ">=0.8.x" - } - }, - "node_modules/execa": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz", - "integrity": "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==", - "dev": true, - "dependencies": { - "cross-spawn": "^7.0.3", - "get-stream": "^6.0.0", - "human-signals": "^2.1.0", - "is-stream": "^2.0.0", - "merge-stream": "^2.0.0", - "npm-run-path": "^4.0.1", - "onetime": "^5.1.2", - "signal-exit": "^3.0.3", - "strip-final-newline": "^2.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sindresorhus/execa?sponsor=1" - } - }, - "node_modules/express": { - "version": "4.17.1", - "integrity": "sha512-mHJ9O79RqluphRrcw2X/GTh3k9tVv8YcoyY4Kkh4WDMUYKRZUq0h1o0w2rrrxBqM7VoeUVqgb27xlEMXTnYt4g==", - "dev": true, - "dependencies": { - "accepts": "~1.3.7", - "array-flatten": "1.1.1", - "body-parser": "1.19.0", - "content-disposition": "0.5.3", - "content-type": "~1.0.4", - "cookie": "0.4.0", - "cookie-signature": "1.0.6", - "debug": "2.6.9", - "depd": "~1.1.2", - "encodeurl": "~1.0.2", - "escape-html": "~1.0.3", - "etag": "~1.8.1", - "finalhandler": "~1.1.2", - "fresh": "0.5.2", - "merge-descriptors": "1.0.1", - "methods": "~1.1.2", - "on-finished": "~2.3.0", - "parseurl": "~1.3.3", - "path-to-regexp": "0.1.7", - "proxy-addr": "~2.0.5", - "qs": "6.7.0", - "range-parser": "~1.2.1", - "safe-buffer": "5.1.2", - "send": "0.17.1", - "serve-static": "1.14.1", - "setprototypeof": "1.1.1", - "statuses": "~1.5.0", - "type-is": "~1.6.18", - "utils-merge": "1.0.1", - "vary": "~1.1.2" - }, - "engines": { - "node": ">= 0.10.0" - } - }, - "node_modules/express/node_modules/array-flatten": { - "version": "1.1.1", - "integrity": "sha1-ml9pkFGx5wczKPKgCJaLZOopVdI=", - "dev": true - }, - "node_modules/express/node_modules/safe-buffer": { - "version": "5.1.2", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", - "dev": true - }, - "node_modules/fast-deep-equal": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", - "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", - "dev": true - }, - "node_modules/fast-extend": { - "version": "1.0.2", - "integrity": "sha512-XXA9RmlPatkFKUzqVZAFth18R4Wo+Xug/S+C7YlYA3xrXwfPlW3dqNwOb4hvQo7wZJ2cNDYhrYuPzVOfHy5/uQ==" - }, - "node_modules/fast-glob": { - "version": "3.2.7", - "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.2.7.tgz", - "integrity": "sha512-rYGMRwip6lUMvYD3BTScMwT1HtAs2d71SMv66Vrxs0IekGZEjhM0pcMfjQPnknBt2zeCwQMEupiN02ZP4DiT1Q==", - "dev": true, - "dependencies": { - "@nodelib/fs.stat": "^2.0.2", - "@nodelib/fs.walk": "^1.2.3", - "glob-parent": "^5.1.2", - "merge2": "^1.3.0", - "micromatch": "^4.0.4" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/fast-json-stable-stringify": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", - "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", - "dev": true - }, - "node_modules/fastest-levenshtein": { - "version": "1.0.12", - "resolved": "https://registry.npmjs.org/fastest-levenshtein/-/fastest-levenshtein-1.0.12.tgz", - "integrity": "sha512-On2N+BpYJ15xIC974QNVuYGMOlEVt4s0EOI3wwMqOmK1fdDY+FN/zltPV8vosq4ad4c/gJ1KHScUn/6AWIgiow==", - "dev": true - }, - "node_modules/fastq": { - "version": "1.13.0", - "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.13.0.tgz", - "integrity": "sha512-YpkpUnK8od0o1hmeSc7UUs/eB/vIPWJYjKck2QKIzAf71Vm1AAQ3EbuZB3g2JIy+pg+ERD0vqI79KyZiB2e2Nw==", - "dev": true, - "dependencies": { - "reusify": "^1.0.4" - } - }, - "node_modules/faye-websocket": { - "version": "0.11.3", - "integrity": "sha512-D2y4bovYpzziGgbHYtGCMjlJM36vAl/y+xUyn1C+FVx8szd1E+86KwVw6XvYSzOP8iMpm1X0I4xJD+QtUb36OA==", - "dev": true, - "dependencies": { - "websocket-driver": ">=0.5.1" - }, - "engines": { - "node": ">=0.8.0" - } - }, - "node_modules/fill-range": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", - "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", - "dev": true, - "dependencies": { - "to-regex-range": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/finalhandler": { - "version": "1.1.2", - "integrity": "sha512-aAWcW57uxVNrQZqFXjITpW3sIUQmHGG3qSb9mUah9MgMC4NeWhNOlNjXEYq3HjRAvL6arUviZGGJsBg6z0zsWA==", - "dev": true, - "dependencies": { - "debug": "2.6.9", - "encodeurl": "~1.0.2", - "escape-html": "~1.0.3", - "on-finished": "~2.3.0", - "parseurl": "~1.3.3", - "statuses": "~1.5.0", - "unpipe": "~1.0.0" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/find-up": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", - "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", - "dev": true, - "dependencies": { - "locate-path": "^5.0.0", - "path-exists": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/follow-redirects": { - "version": "1.14.8", - "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.14.8.tgz", - "integrity": "sha512-1x0S9UVJHsQprFcEC/qnNzBLcIxsjAV905f/UkQxbclCsoTWlacCNOpQa/anodLl2uaEKFhfWOvM2Qg77+15zA==", - "dev": true, - "funding": [ - { - "type": "individual", - "url": "https://github.com/sponsors/RubenVerborgh" - } - ], - "engines": { - "node": ">=4.0" - }, - "peerDependenciesMeta": { - "debug": { - "optional": true - } - } - }, - "node_modules/forwarded": { - "version": "0.1.2", - "integrity": "sha1-mMI9qxF1ZXuMBXPozszZGw/xjIQ=", - "dev": true, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/fresh": { - "version": "0.5.2", - "integrity": "sha1-PYyt2Q2XZWn6g1qx+OSyOhBWBac=", - "dev": true, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/fs-constants": { - "version": "1.0.0", - "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==" - }, - "node_modules/fs-monkey": { - "version": "0.3.3", - "integrity": "sha512-FNUvuTAJ3CqCQb5ELn+qCbGR/Zllhf2HtwsdAtBi59s1WeCjKMT81fHcSu7dwIskqGVK+MmOrb7VOBlq3/SItw==" - }, - "node_modules/fs.realpath": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", - "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=", - "dev": true - }, - "node_modules/fsevents": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", - "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", - "dev": true, - "hasInstallScript": true, - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^8.16.0 || ^10.6.0 || >=11.0.0" - } - }, - "node_modules/function-bind": { - "version": "1.1.1", - "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==", - "dev": true - }, - "node_modules/get-intrinsic": { - "version": "1.1.1", - "integrity": "sha512-kWZrnVM42QCiEA2Ig1bG8zjoIMOgxWwYCEeNdwY6Tv/cOSeGpcoX4pXHfKUxNKVoArnrEr2e9srnAxxGIraS9Q==", - "dev": true, - "dependencies": { - "function-bind": "^1.1.1", - "has": "^1.0.3", - "has-symbols": "^1.0.1" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/get-stream": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", - "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==", - "dev": true, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/glob": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.0.tgz", - "integrity": "sha512-lmLf6gtyrPq8tTjSmrO94wBeQbFR3HbLHbuyD69wuyQkImp2hWqMGB47OX65FBkPffO641IP9jWa1z4ivqG26Q==", - "dev": true, - "dependencies": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.0.4", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - }, - "engines": { - "node": "*" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/glob-parent": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", - "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", - "dev": true, - "dependencies": { - "is-glob": "^4.0.1" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/glob-to-regexp": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz", - "integrity": "sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==", - "dev": true - }, - "node_modules/globby": { - "version": "11.0.4", - "resolved": "https://registry.npmjs.org/globby/-/globby-11.0.4.tgz", - "integrity": "sha512-9O4MVG9ioZJ08ffbcyVYyLOJLk5JQ688pJ4eMGLpdWLHq/Wr1D9BlriLQyL0E+jbkuePVZXYFj47QM/v093wHg==", - "dev": true, - "dependencies": { - "array-union": "^2.1.0", - "dir-glob": "^3.0.1", - "fast-glob": "^3.1.1", - "ignore": "^5.1.4", - "merge2": "^1.3.0", - "slash": "^3.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/graceful-fs": { - "version": "4.2.6", - "integrity": "sha512-nTnJ528pbqxYanhpDYsi4Rd8MAeaBA67+RZ10CM1m3bTAVFEDcd5AuA4a6W5YkGZ1iNXHzZz8T6TBKLeBuNriQ==", - "dev": true - }, - "node_modules/handle-thing": { - "version": "2.0.1", - "integrity": "sha512-9Qn4yBxelxoh2Ow62nP+Ka/kMnOXRi8BXnRaUwezLNhqelnN49xKz4F/dPP8OYLxLxq6JDtZb2i9XznUQbNPTg==", - "dev": true - }, - "node_modules/has": { - "version": "1.0.3", - "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", - "dev": true, - "dependencies": { - "function-bind": "^1.1.1" - }, - "engines": { - "node": ">= 0.4.0" - } - }, - "node_modules/has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/has-symbols": { - "version": "1.0.2", - "integrity": "sha512-chXa79rL/UC2KlX17jo3vRGz0azaWEx5tGqZg5pO3NUyEJVB17dMruQlzCCOfUvElghKcm5194+BCRvi2Rv/Gw==", - "dev": true, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/hpack.js": { - "version": "2.1.6", - "integrity": "sha1-h3dMCUnlE/QuhFdbPEVoH63ioLI=", - "dev": true, - "dependencies": { - "inherits": "^2.0.1", - "obuf": "^1.0.0", - "readable-stream": "^2.0.1", - "wbuf": "^1.1.0" - } - }, - "node_modules/hpack.js/node_modules/readable-stream": { - "version": "2.3.7", - "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==", - "dev": true, - "dependencies": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.3", - "isarray": "~1.0.0", - "process-nextick-args": "~2.0.0", - "safe-buffer": "~5.1.1", - "string_decoder": "~1.1.1", - "util-deprecate": "~1.0.1" - } - }, - "node_modules/hpack.js/node_modules/safe-buffer": { - "version": "5.1.2", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", - "dev": true - }, - "node_modules/hpack.js/node_modules/string_decoder": { - "version": "1.1.1", - "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", - "dev": true, - "dependencies": { - "safe-buffer": "~5.1.0" - } - }, - "node_modules/html-entities": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/html-entities/-/html-entities-2.3.2.tgz", - "integrity": "sha512-c3Ab/url5ksaT0WyleslpBEthOzWhrjQbg75y7XUsfSzi3Dgzt0l8w5e7DylRn15MTlMMD58dTfzddNS2kcAjQ==", - "dev": true - }, - "node_modules/http-deceiver": { - "version": "1.2.7", - "integrity": "sha1-+nFolEq5pRnTN8sL7HKE3D5yPYc=", - "dev": true - }, - "node_modules/http-errors": { - "version": "1.7.2", - "integrity": "sha512-uUQBt3H/cSIVfch6i1EuPNy/YsRSOUBXTVfZ+yR7Zjez3qjBz6i9+i4zjNaoqcoFVI4lQJ5plg63TvGfRSDCRg==", - "dev": true, - "dependencies": { - "depd": "~1.1.2", - "inherits": "2.0.3", - "setprototypeof": "1.1.1", - "statuses": ">= 1.5.0 < 2", - "toidentifier": "1.0.0" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/http-errors/node_modules/inherits": { - "version": "2.0.3", - "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=", - "dev": true - }, - "node_modules/http-parser-js": { - "version": "0.5.3", - "integrity": "sha512-t7hjvef/5HEK7RWTdUzVUhl8zkEu+LlaE0IYzdMuvbSDipxBRpOn4Uhw8ZyECEa808iVT8XCjzo6xmYt4CiLZg==", - "dev": true - }, - "node_modules/http-proxy": { - "version": "1.18.1", - "resolved": "https://registry.npmjs.org/http-proxy/-/http-proxy-1.18.1.tgz", - "integrity": "sha512-7mz/721AbnJwIVbnaSv1Cz3Am0ZLT/UBwkC92VlxhXv/k/BBQfM2fXElQNC27BVGr0uwUpplYPQM9LnaBMR5NQ==", - "dev": true, - "dependencies": { - "eventemitter3": "^4.0.0", - "follow-redirects": "^1.0.0", - "requires-port": "^1.0.0" - }, - "engines": { - "node": ">=8.0.0" - } - }, - "node_modules/http-proxy-middleware": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/http-proxy-middleware/-/http-proxy-middleware-2.0.1.tgz", - "integrity": "sha512-cfaXRVoZxSed/BmkA7SwBVNI9Kj7HFltaE5rqYOub5kWzWZ+gofV2koVN1j2rMW7pEfSSlCHGJ31xmuyFyfLOg==", - "dev": true, - "dependencies": { - "@types/http-proxy": "^1.17.5", - "http-proxy": "^1.18.1", - "is-glob": "^4.0.1", - "is-plain-obj": "^3.0.0", - "micromatch": "^4.0.2" - }, - "engines": { - "node": ">=12.0.0" - } - }, - "node_modules/human-signals": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz", - "integrity": "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==", - "dev": true, - "engines": { - "node": ">=10.17.0" - } - }, - "node_modules/iconv-lite": { - "version": "0.4.24", - "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", - "dev": true, - "dependencies": { - "safer-buffer": ">= 2.1.2 < 3" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/ieee754": { - "version": "1.2.1", - "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ] - }, - "node_modules/ignore": { - "version": "5.1.9", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.1.9.tgz", - "integrity": "sha512-2zeMQpbKz5dhZ9IwL0gbxSW5w0NK/MSAMtNuhgIHEPmaU3vPdKPL0UdvUCXs5SS4JAwsBxysK5sFMW8ocFiVjQ==", - "dev": true, - "engines": { - "node": ">= 4" - } - }, - "node_modules/import-local": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/import-local/-/import-local-3.0.3.tgz", - "integrity": "sha512-bE9iaUY3CXH8Cwfan/abDKAxe1KGT9kyGsBPqf6DMK/z0a2OzAsrukeYNgIH6cH5Xr452jb1TUL8rSfCLjZ9uA==", - "dev": true, - "dependencies": { - "pkg-dir": "^4.2.0", - "resolve-cwd": "^3.0.0" - }, - "bin": { - "import-local-fixture": "fixtures/cli.js" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/indent-string": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz", - "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/inflight": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", - "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", - "dev": true, - "dependencies": { - "once": "^1.3.0", - "wrappy": "1" - } - }, - "node_modules/inherits": { - "version": "2.0.4", - "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" - }, - "node_modules/internal-ip": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/internal-ip/-/internal-ip-6.2.0.tgz", - "integrity": "sha512-D8WGsR6yDt8uq7vDMu7mjcR+yRMm3dW8yufyChmszWRjcSHuxLBkR3GdS2HZAjodsaGuCvXeEJpueisXJULghg==", - "dev": true, - "dependencies": { - "default-gateway": "^6.0.0", - "ipaddr.js": "^1.9.1", - "is-ip": "^3.1.0", - "p-event": "^4.2.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sindresorhus/internal-ip?sponsor=1" - } - }, - "node_modules/interpret": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/interpret/-/interpret-2.2.0.tgz", - "integrity": "sha512-Ju0Bz/cEia55xDwUWEa8+olFpCiQoypjnQySseKtmjNrnps3P+xfpUmGr90T7yjlVJmOtybRvPXhKMbHr+fWnw==", - "dev": true, - "engines": { - "node": ">= 0.10" - } - }, - "node_modules/ip": { - "version": "1.1.5", - "integrity": "sha1-vd7XARQpCCjAoDnnLvJfWq7ENUo=", - "dev": true - }, - "node_modules/ip-regex": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ip-regex/-/ip-regex-4.3.0.tgz", - "integrity": "sha512-B9ZWJxHHOHUhUjCPrMpLD4xEq35bUTClHM1S6CBU5ixQnkZmwipwgc96vAd7AAGM9TGHvJR+Uss+/Ak6UphK+Q==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/ipaddr.js": { - "version": "1.9.1", - "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", - "dev": true, - "engines": { - "node": ">= 0.10" - } - }, - "node_modules/is-arguments": { - "version": "1.1.0", - "integrity": "sha512-1Ij4lOMPl/xB5kBDn7I+b2ttPMKa8szhEIrXDuXQD/oe3HJLTLhqhgGspwgyGd6MOywBUqVvYicF72lkgDnIHg==", - "dev": true, - "dependencies": { - "call-bind": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-binary-path": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", - "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", - "dev": true, - "dependencies": { - "binary-extensions": "^2.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/is-core-module": { - "version": "2.8.0", - "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.8.0.tgz", - "integrity": "sha512-vd15qHsaqrRL7dtH6QNuy0ndJmRDrS9HAM1CAiSifNUFv4x1a0CCVsj18hJ1mShxIG6T2i1sO78MkP56r0nYRw==", - "dev": true, - "dependencies": { - "has": "^1.0.3" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-date-object": { - "version": "1.0.2", - "integrity": "sha512-USlDT524woQ08aoZFzh3/Z6ch9Y/EWXEHQ/AaRN0SkKq4t2Jw2R2339tSXmwuVoY7LLlBCbOIlx2myP/L5zk0g==", - "dev": true, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-docker": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-2.2.1.tgz", - "integrity": "sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==", - "dev": true, - "bin": { - "is-docker": "cli.js" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/is-extglob": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", - "integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/is-glob": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", - "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", - "dev": true, - "dependencies": { - "is-extglob": "^2.1.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/is-ip": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/is-ip/-/is-ip-3.1.0.tgz", - "integrity": "sha512-35vd5necO7IitFPjd/YBeqwWnyDWbuLH9ZXQdMfDA8TEo7pv5X8yfrvVO3xbJbLUlERCMvf6X0hTUamQxCYJ9Q==", - "dev": true, - "dependencies": { - "ip-regex": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/is-number": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", - "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", - "dev": true, - "engines": { - "node": ">=0.12.0" - } - }, - "node_modules/is-path-cwd": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/is-path-cwd/-/is-path-cwd-2.2.0.tgz", - "integrity": "sha512-w942bTcih8fdJPJmQHFzkS76NEP8Kzzvmw92cXsazb8intwLqPibPPdXf4ANdKV3rYMuuQYGIWtvz9JilB3NFQ==", - "dev": true, - "engines": { - "node": ">=6" - } - }, - "node_modules/is-path-inside": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz", - "integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/is-plain-obj": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-3.0.0.tgz", - "integrity": "sha512-gwsOE28k+23GP1B6vFl1oVh/WOzmawBrKwo5Ev6wMKzPkaXaCDIQKzLnvsA42DRlbVTWorkgTKIviAKCWkfUwA==", - "dev": true, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/is-plain-object": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz", - "integrity": "sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==", - "dev": true, - "dependencies": { - "isobject": "^3.0.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/is-regex": { - "version": "1.1.2", - "integrity": "sha512-axvdhb5pdhEVThqJzYXwMlVuZwC+FF2DpcOhTS+y/8jVq4trxyPgfcwIxIKiyeuLlSQYKkmUaPQJ8ZE4yNKXDg==", - "dev": true, - "dependencies": { - "call-bind": "^1.0.2", - "has-symbols": "^1.0.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-stream": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", - "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", - "dev": true, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/is-wsl": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-2.2.0.tgz", - "integrity": "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==", - "dev": true, - "dependencies": { - "is-docker": "^2.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/isarray": { - "version": "1.0.0", - "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=", - "dev": true - }, - "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": "3.0.1", - "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", - "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/javascript-kit-swift": { - "resolved": "..", - "link": true - }, - "node_modules/jest-worker": { - "version": "27.3.1", - "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-27.3.1.tgz", - "integrity": "sha512-ks3WCzsiZaOPJl/oMsDjaf0TRiSv7ctNgs0FqRr2nARsovz6AWWy4oLElwcquGSz692DzgZQrCLScPNs5YlC4g==", - "dev": true, - "dependencies": { - "@types/node": "*", - "merge-stream": "^2.0.0", - "supports-color": "^8.0.0" - }, - "engines": { - "node": ">= 10.13.0" - } - }, - "node_modules/json-parse-better-errors": { - "version": "1.0.2", - "integrity": "sha512-mrqyZKfX5EhL7hvqcV6WG1yYjnjeuYDzDhhcAAUrq8Po85NBQBJP+ZDUT75qZQ98IkUoBqdkExkukOU7Ts2wrw==", - "dev": true - }, - "node_modules/json-schema-traverse": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", - "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", - "dev": true - }, - "node_modules/kind-of": { - "version": "6.0.3", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", - "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/loader-runner": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/loader-runner/-/loader-runner-4.2.0.tgz", - "integrity": "sha512-92+huvxMvYlMzMt0iIOukcwYBFpkYJdpl2xsZ7LrlayO7E8SOv+JJUEK17B/dJIHAOLMfh2dZZ/Y18WgmGtYNw==", - "dev": true, - "engines": { - "node": ">=6.11.5" - } - }, - "node_modules/locate-path": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", - "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", - "dev": true, - "dependencies": { - "p-locate": "^4.1.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/lodash": { - "version": "4.17.21", - "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", - "dev": true - }, - "node_modules/media-typer": { - "version": "0.3.0", - "integrity": "sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g=", - "dev": true, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/memfs": { - "version": "3.0.4", - "integrity": "sha512-OcZEzwX9E5AoY8SXjuAvw0DbIAYwUzV/I236I8Pqvrlv7sL/Y0E9aRCon05DhaV8pg1b32uxj76RgW0s5xjHBA==", - "dependencies": { - "fast-extend": "1.0.2", - "fs-monkey": "0.3.3" - } - }, - "node_modules/merge-descriptors": { - "version": "1.0.1", - "integrity": "sha1-sAqqVW3YtEVoFQ7J0blT8/kMu2E=", - "dev": true - }, - "node_modules/merge-stream": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", - "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", - "dev": true - }, - "node_modules/merge2": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", - "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", - "dev": true, - "engines": { - "node": ">= 8" - } - }, - "node_modules/methods": { - "version": "1.1.2", - "integrity": "sha1-VSmk1nZUE07cxSZmVoNbD4Ua/O4=", - "dev": true, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/micromatch": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.4.tgz", - "integrity": "sha512-pRmzw/XUcwXGpD9aI9q/0XOwLNygjETJ8y0ao0wdqprrzDa4YnxLcz7fQRZr8voh8V10kGhABbNcHVk5wHgWwg==", - "dev": true, - "dependencies": { - "braces": "^3.0.1", - "picomatch": "^2.2.3" - }, - "engines": { - "node": ">=8.6" - } - }, - "node_modules/mime": { - "version": "1.6.0", - "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", - "dev": true, - "bin": { - "mime": "cli.js" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/mime-db": { - "version": "1.51.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.51.0.tgz", - "integrity": "sha512-5y8A56jg7XVQx2mbv1lu49NR4dokRnhZYTtL+KGfaa27uq4pSTXkwQkFJl4pkRMyNFz/EtYDSkiiEHx3F7UN6g==", - "dev": true, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/mime-types": { - "version": "2.1.34", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.34.tgz", - "integrity": "sha512-6cP692WwGIs9XXdOO4++N+7qjqv0rqxxVvJ3VHPh/Sc9mVZcQP+ZGhkKiTvWMQRr2tbHkJP/Yn7Y0npb3ZBs4A==", - "dev": true, - "dependencies": { - "mime-db": "1.51.0" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/mimic-fn": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", - "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", - "dev": true, - "engines": { - "node": ">=6" - } - }, - "node_modules/minimalistic-assert": { - "version": "1.0.1", - "integrity": "sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==", - "dev": true - }, - "node_modules/minimatch": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", - "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", - "dev": true, - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, - "node_modules/minimist": { - "version": "1.2.6", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.6.tgz", - "integrity": "sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q==", - "dev": true - }, - "node_modules/mkdirp": { - "version": "0.5.5", - "integrity": "sha512-NKmAlESf6jMGym1++R0Ra7wvhV+wFW63FaSOFPwRahvea0gMUcGUhVeAg/0BC0wiv9ih5NYPB1Wn1UEI1/L+xQ==", - "dev": true, - "dependencies": { - "minimist": "^1.2.5" - }, - "bin": { - "mkdirp": "bin/cmd.js" - } - }, - "node_modules/ms": { - "version": "2.0.0", - "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", - "dev": true - }, - "node_modules/multicast-dns": { - "version": "6.2.3", - "integrity": "sha512-ji6J5enbMyGRHIAkAOu3WdV8nggqviKCEKtXcOqfphZZtQrmHKycfynJ2V7eVPUA4NhJ6V7Wf4TmGbTwKE9B6g==", - "dev": true, - "dependencies": { - "dns-packet": "^1.3.1", - "thunky": "^1.0.2" - }, - "bin": { - "multicast-dns": "cli.js" - } - }, - "node_modules/multicast-dns-service-types": { - "version": "1.1.0", - "integrity": "sha1-iZ8R2WhuXgXLkbNdXw5jt3PPyQE=", - "dev": true - }, - "node_modules/negotiator": { - "version": "0.6.2", - "integrity": "sha512-hZXc7K2e+PgeI1eDBe/10Ard4ekbfrrqG8Ep+8Jmf4JID2bNg7NvCPOZN+kfF574pFQI7mum2AUqDidoKqcTOw==", - "dev": true, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/neo-async": { - "version": "2.6.2", - "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz", - "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==", - "dev": true - }, - "node_modules/node-forge": { - "version": "0.10.0", - "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-0.10.0.tgz", - "integrity": "sha512-PPmu8eEeG9saEUvI97fm4OYxXVB6bFvyNTyiUOBichBpFG8A1Ljw3bY62+5oOjDEMHRnd0Y7HQ+x7uzxOzC6JA==", - "dev": true, - "engines": { - "node": ">= 6.0.0" - } - }, - "node_modules/node-releases": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.1.tgz", - "integrity": "sha512-CqyzN6z7Q6aMeF/ktcMVTzhAHCEpf8SOarwpzpf8pNBY2k5/oM34UHldUwp8VKI7uxct2HxSRdJjBaZeESzcxA==", - "dev": true - }, - "node_modules/normalize-path": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", - "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/npm-run-path": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz", - "integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==", - "dev": true, - "dependencies": { - "path-key": "^3.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/object-is": { - "version": "1.1.5", - "integrity": "sha512-3cyDsyHgtmi7I7DfSSI2LDp6SK2lwvtbg0p0R1e0RvTqF5ceGx+K2dfSjm1bKDMVCFEDAQvy+o8c6a7VujOddw==", - "dev": true, - "dependencies": { - "call-bind": "^1.0.2", - "define-properties": "^1.1.3" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/object-keys": { - "version": "1.1.1", - "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", - "dev": true, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/obuf": { - "version": "1.1.2", - "integrity": "sha512-PX1wu0AmAdPqOL1mWhqmlOd8kOIZQwGZw6rh7uby9fTc5lhaOWFLX3I6R1hrF9k3zUY40e6igsLGkDXK92LJNg==", - "dev": true - }, - "node_modules/on-finished": { - "version": "2.3.0", - "integrity": "sha1-IPEzZIGwg811M3mSoWlxqi2QaUc=", - "dev": true, - "dependencies": { - "ee-first": "1.1.1" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/on-headers": { - "version": "1.0.2", - "integrity": "sha512-pZAE+FJLoyITytdqK0U5s+FIpjN0JP3OzFi/u8Rx+EV5/W+JTWGXG8xFzevE7AjBfDqHv/8vL8qQsIhHnqRkrA==", - "dev": true, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/once": { - "version": "1.4.0", - "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", - "dependencies": { - "wrappy": "1" - } - }, - "node_modules/onetime": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", - "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", - "dev": true, - "dependencies": { - "mimic-fn": "^2.1.0" - }, - "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/open": { - "version": "8.4.0", - "resolved": "https://registry.npmjs.org/open/-/open-8.4.0.tgz", - "integrity": "sha512-XgFPPM+B28FtCCgSb9I+s9szOC1vZRSwgWsRUA5ylIxRTgKozqjOCrVOqGsYABPYK5qnfqClxZTFBa8PKt2v6Q==", - "dev": true, - "dependencies": { - "define-lazy-prop": "^2.0.0", - "is-docker": "^2.1.1", - "is-wsl": "^2.2.0" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/p-event": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/p-event/-/p-event-4.2.0.tgz", - "integrity": "sha512-KXatOjCRXXkSePPb1Nbi0p0m+gQAwdlbhi4wQKJPI1HsMQS9g+Sqp2o+QHziPr7eYJyOZet836KoHEVM1mwOrQ==", - "dev": true, - "dependencies": { - "p-timeout": "^3.1.0" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "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/p-limit": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", - "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", - "dev": true, - "dependencies": { - "p-try": "^2.0.0" - }, - "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/p-locate": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", - "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", - "dev": true, - "dependencies": { - "p-limit": "^2.2.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/p-map": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/p-map/-/p-map-4.0.0.tgz", - "integrity": "sha512-/bjOqmgETBYB5BoEeGVea8dmvHb2m9GLy1E9W43yeyfP6QQCZGFNa+XRceJEuDB6zqr+gKpIAmlLebMpykw/MQ==", - "dev": true, - "dependencies": { - "aggregate-error": "^3.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/p-retry": { - "version": "4.6.1", - "resolved": "https://registry.npmjs.org/p-retry/-/p-retry-4.6.1.tgz", - "integrity": "sha512-e2xXGNhZOZ0lfgR9kL34iGlU8N/KO0xZnQxVEwdeOvpqNDQfdnxIYizvWtK8RglUa3bGqI8g0R/BdfzLMxRkiA==", - "dev": true, - "dependencies": { - "@types/retry": "^0.12.0", - "retry": "^0.13.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/p-timeout": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/p-timeout/-/p-timeout-3.2.0.tgz", - "integrity": "sha512-rhIwUycgwwKcP9yTOOFK/AKsAopjjCakVqLHePO3CC6Mir1Z99xT+R63jZxAT5lFZLa2inS5h+ZS2GvR99/FBg==", - "dev": true, - "dependencies": { - "p-finally": "^1.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/p-try": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", - "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", - "dev": true, - "engines": { - "node": ">=6" - } - }, - "node_modules/pako": { - "version": "1.0.11", - "integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==" - }, - "node_modules/parseurl": { - "version": "1.3.3", - "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", - "dev": true, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/path-browserify": { - "version": "1.0.1", - "integrity": "sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==" - }, - "node_modules/path-exists": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", - "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/path-is-absolute": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", - "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/path-key": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", - "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "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 - }, - "node_modules/path-to-regexp": { - "version": "0.1.7", - "integrity": "sha1-32BBeABfUi8V60SQ5yR6G/qmf4w=", - "dev": true - }, - "node_modules/path-type": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", - "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/picocolors": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz", - "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==", - "dev": true - }, - "node_modules/picomatch": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.0.tgz", - "integrity": "sha512-lY1Q/PiJGC2zOv/z391WOTD+Z02bCgsFfvxoXXf6h7kv9o+WmsmzYqrAwY63sNgOxE4xEdq0WyUnXfKeBrSvYw==", - "dev": true, - "engines": { - "node": ">=8.6" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, - "node_modules/pkg-dir": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz", - "integrity": "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==", - "dev": true, - "dependencies": { - "find-up": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/portfinder": { - "version": "1.0.28", - "integrity": "sha512-Se+2isanIcEqf2XMHjyUKskczxbPH7dQnlMjXX6+dybayyHvAf/TCgyMRlzf/B6QDhAEFOGes0pzRo3by4AbMA==", - "dev": true, - "dependencies": { - "async": "^2.6.2", - "debug": "^3.1.1", - "mkdirp": "^0.5.5" - }, - "engines": { - "node": ">= 0.12.0" - } - }, - "node_modules/portfinder/node_modules/debug": { - "version": "3.2.7", - "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", - "dev": true, - "dependencies": { - "ms": "^2.1.1" - } - }, - "node_modules/portfinder/node_modules/ms": { - "version": "2.1.3", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "dev": true - }, - "node_modules/process-nextick-args": { - "version": "2.0.1", - "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", - "dev": true - }, - "node_modules/proxy-addr": { - "version": "2.0.6", - "integrity": "sha512-dh/frvCBVmSsDYzw6n926jv974gddhkFPfiN8hPOi30Wax25QZyZEGveluCgliBnqmuM+UJmBErbAUFIoDbjOw==", - "dev": true, - "dependencies": { - "forwarded": "~0.1.2", - "ipaddr.js": "1.9.1" - }, - "engines": { - "node": ">= 0.10" - } - }, - "node_modules/punycode": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz", - "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==", - "dev": true, - "engines": { - "node": ">=6" - } - }, - "node_modules/qs": { - "version": "6.7.0", - "integrity": "sha512-VCdBRNFTX1fyE7Nb6FYoURo/SPe62QCaAyzJvUjwRaIsc+NePBEniHlvxFmmX56+HZphIGtV0XeCirBtpDrTyQ==", - "dev": true, - "engines": { - "node": ">=0.6" - } - }, - "node_modules/querystring": { - "version": "0.2.0", - "integrity": "sha1-sgmEkgO7Jd+CDadW50cAWHhSFiA=", - "deprecated": "The querystring API is considered Legacy. new code should use the URLSearchParams API instead.", - "dev": true, - "engines": { - "node": ">=0.4.x" - } - }, - "node_modules/queue-microtask": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", - "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ] - }, - "node_modules/randombytes": { - "version": "2.1.0", - "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==", - "dependencies": { - "safe-buffer": "^5.1.0" - } - }, - "node_modules/randomfill": { - "version": "1.0.4", - "integrity": "sha512-87lcbR8+MhcWcUiQ+9e+Rwx8MyR2P7qnt15ynUlbm3TU/fjbgz4GsvfSUDTemtCCtVCqb4ZcEFlyPNTh9bBTLw==", - "dependencies": { - "randombytes": "^2.0.5", - "safe-buffer": "^5.1.0" - } - }, - "node_modules/range-parser": { - "version": "1.2.1", - "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", - "dev": true, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/raw-body": { - "version": "2.4.0", - "integrity": "sha512-4Oz8DUIwdvoa5qMJelxipzi/iJIi40O5cGV1wNYp5hvZP8ZN0T+jiNkL0QepXs+EsQ9XJ8ipEDoiH70ySUJP3Q==", - "dev": true, - "dependencies": { - "bytes": "3.1.0", - "http-errors": "1.7.2", - "iconv-lite": "0.4.24", - "unpipe": "1.0.0" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/raw-body/node_modules/bytes": { - "version": "3.1.0", - "integrity": "sha512-zauLjrfCG+xvoyaqLoV8bLVXXNGC4JqlxFCutSDWA6fJrTo2ZuvLYTqZ7aHBLZSMOopbzwv8f+wZcVzfVTI2Dg==", - "dev": true, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/readable-stream": { - "version": "3.6.0", - "integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==", - "dependencies": { - "inherits": "^2.0.3", - "string_decoder": "^1.1.1", - "util-deprecate": "^1.0.1" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/readdirp": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", - "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", - "dev": true, - "dependencies": { - "picomatch": "^2.2.1" - }, - "engines": { - "node": ">=8.10.0" - } - }, - "node_modules/rechoir": { - "version": "0.7.1", - "resolved": "https://registry.npmjs.org/rechoir/-/rechoir-0.7.1.tgz", - "integrity": "sha512-/njmZ8s1wVeR6pjTZ+0nCnv8SpZNRMT2D1RLOJQESlYFDBvwpTA4KWJpZ+sBJ4+vhjILRcK7JIFdGCdxEAAitg==", - "dev": true, - "dependencies": { - "resolve": "^1.9.0" - }, - "engines": { - "node": ">= 0.10" - } - }, - "node_modules/regexp.prototype.flags": { - "version": "1.3.1", - "integrity": "sha512-JiBdRBq91WlY7uRJ0ds7R+dU02i6LKi8r3BuQhNXn+kmeLN+EfHhfjqMRis1zJxnlu88hq/4dx0P2OP3APRTOA==", - "dev": true, - "dependencies": { - "call-bind": "^1.0.2", - "define-properties": "^1.1.3" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/require-from-string": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", - "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/requires-port": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz", - "integrity": "sha1-kl0mAdOaxIXgkc8NpcbmlNw9yv8=", - "dev": true - }, - "node_modules/resolve": { - "version": "1.20.0", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.20.0.tgz", - "integrity": "sha512-wENBPt4ySzg4ybFQW2TT1zMQucPK95HSh/nq2CFTZVOGut2+pQvSsgtda4d26YrYcr067wjbmzOG8byDPBX63A==", - "dev": true, - "dependencies": { - "is-core-module": "^2.2.0", - "path-parse": "^1.0.6" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/resolve-cwd": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/resolve-cwd/-/resolve-cwd-3.0.0.tgz", - "integrity": "sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg==", - "dev": true, - "dependencies": { - "resolve-from": "^5.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/resolve-from": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", - "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/retry": { - "version": "0.13.1", - "resolved": "https://registry.npmjs.org/retry/-/retry-0.13.1.tgz", - "integrity": "sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg==", - "dev": true, - "engines": { - "node": ">= 4" - } - }, - "node_modules/reusify": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", - "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==", - "dev": true, - "engines": { - "iojs": ">=1.0.0", - "node": ">=0.10.0" - } - }, - "node_modules/rimraf": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", - "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", - "dev": true, - "dependencies": { - "glob": "^7.1.3" - }, - "bin": { - "rimraf": "bin.js" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/run-parallel": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", - "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "dependencies": { - "queue-microtask": "^1.2.2" - } - }, - "node_modules/safe-buffer": { - "version": "5.2.1", - "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ] - }, - "node_modules/safer-buffer": { - "version": "2.1.2", - "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", - "dev": true - }, - "node_modules/schema-utils": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.1.1.tgz", - "integrity": "sha512-Y5PQxS4ITlC+EahLuXaY86TXfR7Dc5lw294alXOq86JAHCihAIZfqv8nNCWvaEJvaC51uN9hbLGeV0cFBdH+Fw==", - "dev": true, - "dependencies": { - "@types/json-schema": "^7.0.8", - "ajv": "^6.12.5", - "ajv-keywords": "^3.5.2" - }, - "engines": { - "node": ">= 10.13.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - } - }, - "node_modules/select-hose": { - "version": "2.0.0", - "integrity": "sha1-Yl2GWPhlr0Psliv8N2o3NZpJlMo=", - "dev": true - }, - "node_modules/selfsigned": { - "version": "1.10.11", - "resolved": "https://registry.npmjs.org/selfsigned/-/selfsigned-1.10.11.tgz", - "integrity": "sha512-aVmbPOfViZqOZPgRBT0+3u4yZFHpmnIghLMlAcb5/xhp5ZtB/RVnKhz5vl2M32CLXAqR4kha9zfhNg0Lf/sxKA==", - "dev": true, - "dependencies": { - "node-forge": "^0.10.0" - } - }, - "node_modules/send": { - "version": "0.17.1", - "integrity": "sha512-BsVKsiGcQMFwT8UxypobUKyv7irCNRHk1T0G680vk88yf6LBByGcZJOTJCrTP2xVN6yI+XjPJcNuE3V4fT9sAg==", - "dev": true, - "dependencies": { - "debug": "2.6.9", - "depd": "~1.1.2", - "destroy": "~1.0.4", - "encodeurl": "~1.0.2", - "escape-html": "~1.0.3", - "etag": "~1.8.1", - "fresh": "0.5.2", - "http-errors": "~1.7.2", - "mime": "1.6.0", - "ms": "2.1.1", - "on-finished": "~2.3.0", - "range-parser": "~1.2.1", - "statuses": "~1.5.0" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/send/node_modules/ms": { - "version": "2.1.1", - "integrity": "sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg==", - "dev": true - }, - "node_modules/serialize-javascript": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.0.tgz", - "integrity": "sha512-Qr3TosvguFt8ePWqsvRfrKyQXIiW+nGbYpy8XK24NQHE83caxWt+mIymTT19DGFbNWNLfEwsrkSmN64lVWB9ag==", - "dev": true, - "dependencies": { - "randombytes": "^2.1.0" - } - }, - "node_modules/serve-index": { - "version": "1.9.1", - "integrity": "sha1-03aNabHn2C5c4FD/9bRTvqEqkjk=", - "dev": true, - "dependencies": { - "accepts": "~1.3.4", - "batch": "0.6.1", - "debug": "2.6.9", - "escape-html": "~1.0.3", - "http-errors": "~1.6.2", - "mime-types": "~2.1.17", - "parseurl": "~1.3.2" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/serve-index/node_modules/http-errors": { - "version": "1.6.3", - "integrity": "sha1-i1VoC7S+KDoLW/TqLjhYC+HZMg0=", - "dev": true, - "dependencies": { - "depd": "~1.1.2", - "inherits": "2.0.3", - "setprototypeof": "1.1.0", - "statuses": ">= 1.4.0 < 2" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/serve-index/node_modules/inherits": { - "version": "2.0.3", - "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=", - "dev": true - }, - "node_modules/serve-index/node_modules/setprototypeof": { - "version": "1.1.0", - "integrity": "sha512-BvE/TwpZX4FXExxOxZyRGQQv651MSwmWKZGqvmPcRIjDqWub67kTKuIMx43cZZrS/cBBzwBcNDWoFxt2XEFIpQ==", - "dev": true - }, - "node_modules/serve-static": { - "version": "1.14.1", - "integrity": "sha512-JMrvUwE54emCYWlTI+hGrGv5I8dEwmco/00EvkzIIsR7MqrHonbD9pO2MOfFnpFntl7ecpZs+3mW+XbQZu9QCg==", - "dev": true, - "dependencies": { - "encodeurl": "~1.0.2", - "escape-html": "~1.0.3", - "parseurl": "~1.3.3", - "send": "0.17.1" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/setprototypeof": { - "version": "1.1.1", - "integrity": "sha512-JvdAWfbXeIGaZ9cILp38HntZSFSo3mWg6xGcJJsd+d4aRMOqauag1C63dJfDw7OaMYwEbHMOxEZ1lqVRYP2OAw==", - "dev": true - }, - "node_modules/shallow-clone": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/shallow-clone/-/shallow-clone-3.0.1.tgz", - "integrity": "sha512-/6KqX+GVUdqPuPPd2LxDDxzX6CAbjJehAAOKlNpqqUpAqPM6HeL8f+o3a+JsyGjn2lv0WY8UsTgUJjU9Ok55NA==", - "dev": true, - "dependencies": { - "kind-of": "^6.0.2" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/shebang-command": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", - "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", - "dev": true, - "dependencies": { - "shebang-regex": "^3.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/shebang-regex": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", - "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/signal-exit": { - "version": "3.0.6", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.6.tgz", - "integrity": "sha512-sDl4qMFpijcGw22U5w63KmD3cZJfBuFlVNbVMKje2keoKML7X2UzWbc4XrmEbDwg0NXJc3yv4/ox7b+JWb57kQ==", - "dev": true - }, - "node_modules/slash": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", - "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/sockjs": { - "version": "0.3.21", - "integrity": "sha512-DhbPFGpxjc6Z3I+uX07Id5ZO2XwYsWOrYjaSeieES78cq+JaJvVe5q/m1uvjIQhXinhIeCFRH6JgXe+mvVMyXw==", - "dev": true, - "dependencies": { - "faye-websocket": "^0.11.3", - "uuid": "^3.4.0", - "websocket-driver": "^0.7.4" - } - }, - "node_modules/source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/source-map-support": { - "version": "0.5.21", - "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", - "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", - "dev": true, - "dependencies": { - "buffer-from": "^1.0.0", - "source-map": "^0.6.0" - } - }, - "node_modules/spdy": { - "version": "4.0.2", - "integrity": "sha512-r46gZQZQV+Kl9oItvl1JZZqJKGr+oEkB08A6BzkiR7593/7IbtuncXHd2YoYeTsG4157ZssMu9KYvUHLcjcDoA==", - "dev": true, - "dependencies": { - "debug": "^4.1.0", - "handle-thing": "^2.0.0", - "http-deceiver": "^1.2.7", - "select-hose": "^2.0.0", - "spdy-transport": "^3.0.0" - }, - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/spdy-transport": { - "version": "3.0.0", - "integrity": "sha512-hsLVFE5SjA6TCisWeJXFKniGGOpBgMLmerfO2aCyCU5s7nJ/rpAepqmFifv/GCbSbueEeAJJnmSQ2rKC/g8Fcw==", - "dev": true, - "dependencies": { - "debug": "^4.1.0", - "detect-node": "^2.0.4", - "hpack.js": "^2.1.6", - "obuf": "^1.1.2", - "readable-stream": "^3.0.6", - "wbuf": "^1.7.3" - } - }, - "node_modules/spdy-transport/node_modules/debug": { - "version": "4.3.1", - "integrity": "sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ==", - "dev": true, - "dependencies": { - "ms": "2.1.2" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/spdy-transport/node_modules/ms": { - "version": "2.1.2", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", - "dev": true - }, - "node_modules/spdy/node_modules/debug": { - "version": "4.3.1", - "integrity": "sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ==", - "dev": true, - "dependencies": { - "ms": "2.1.2" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/spdy/node_modules/ms": { - "version": "2.1.2", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", - "dev": true - }, - "node_modules/statuses": { - "version": "1.5.0", - "integrity": "sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow=", - "dev": true, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/string_decoder": { - "version": "1.3.0", - "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", - "dependencies": { - "safe-buffer": "~5.2.0" - } - }, - "node_modules/strip-ansi": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.0.1.tgz", - "integrity": "sha512-cXNxvT8dFNRVfhVME3JAe98mkXDYN2O1l7jmcwMnOslDeESg1rF/OZMtK0nRAhiari1unG5cD4jG3rapUAkLbw==", - "dev": true, - "dependencies": { - "ansi-regex": "^6.0.1" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/strip-ansi?sponsor=1" - } - }, - "node_modules/strip-final-newline": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz", - "integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==", - "dev": true, - "engines": { - "node": ">=6" - } - }, - "node_modules/supports-color": { - "version": "8.1.1", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", - "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", - "dev": true, - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/supports-color?sponsor=1" - } - }, - "node_modules/tapable": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.2.1.tgz", - "integrity": "sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ==", - "dev": true, - "engines": { - "node": ">=6" - } - }, - "node_modules/tar-stream": { - "version": "2.2.0", - "integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==", - "dependencies": { - "bl": "^4.0.3", - "end-of-stream": "^1.4.1", - "fs-constants": "^1.0.0", - "inherits": "^2.0.3", - "readable-stream": "^3.1.1" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/terser": { - "version": "5.10.0", - "resolved": "https://registry.npmjs.org/terser/-/terser-5.10.0.tgz", - "integrity": "sha512-AMmF99DMfEDiRJfxfY5jj5wNH/bYO09cniSqhfoyxc8sFoYIgkJy86G04UoZU5VjlpnplVu0K6Tx6E9b5+DlHA==", - "dev": true, - "dependencies": { - "commander": "^2.20.0", - "source-map": "~0.7.2", - "source-map-support": "~0.5.20" - }, - "bin": { - "terser": "bin/terser" - }, - "engines": { - "node": ">=10" - }, - "peerDependencies": { - "acorn": "^8.5.0" - }, - "peerDependenciesMeta": { - "acorn": { - "optional": true - } - } - }, - "node_modules/terser-webpack-plugin": { - "version": "5.2.5", - "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.2.5.tgz", - "integrity": "sha512-3luOVHku5l0QBeYS8r4CdHYWEGMmIj3H1U64jgkdZzECcSOJAyJ9TjuqcQZvw1Y+4AOBN9SeYJPJmFn2cM4/2g==", - "dev": true, - "dependencies": { - "jest-worker": "^27.0.6", - "schema-utils": "^3.1.1", - "serialize-javascript": "^6.0.0", - "source-map": "^0.6.1", - "terser": "^5.7.2" - }, - "engines": { - "node": ">= 10.13.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - }, - "peerDependencies": { - "webpack": "^5.1.0" - }, - "peerDependenciesMeta": { - "@swc/core": { - "optional": true - }, - "esbuild": { - "optional": true - }, - "uglify-js": { - "optional": true - } - } - }, - "node_modules/terser/node_modules/source-map": { - "version": "0.7.3", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.3.tgz", - "integrity": "sha512-CkCj6giN3S+n9qrYiBTX5gystlENnRW5jZeNLHpe6aue+SrHcG5VYwujhW9s4dY31mEGsxBDrHR6oI69fTXsaQ==", - "dev": true, - "engines": { - "node": ">= 8" - } - }, - "node_modules/thunky": { - "version": "1.1.0", - "integrity": "sha512-eHY7nBftgThBqOyHGVN+l8gF0BucP09fMo0oO/Lb0w1OF80dJv+lDVpXG60WMQvkcxAkNybKsrEIE3ZtKGmPrA==", - "dev": true - }, - "node_modules/to-regex-range": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", - "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", - "dev": true, - "dependencies": { - "is-number": "^7.0.0" - }, - "engines": { - "node": ">=8.0" - } - }, - "node_modules/toidentifier": { - "version": "1.0.0", - "integrity": "sha512-yaOH/Pk/VEhBWWTlhI+qXxDFXlejDGcQipMlyxda9nthulaxLZUNcUqFxokp0vcYnvteJln5FNQDRrxj3YcbVw==", - "dev": true, - "engines": { - "node": ">=0.6" - } - }, - "node_modules/type-is": { - "version": "1.6.18", - "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", - "dev": true, - "dependencies": { - "media-typer": "0.3.0", - "mime-types": "~2.1.24" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/unpipe": { - "version": "1.0.0", - "integrity": "sha1-sr9O6FFKrmFltIF4KdIbLvSZBOw=", - "dev": true, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/uri-js": { - "version": "4.4.1", - "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", - "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", - "dev": true, - "dependencies": { - "punycode": "^2.1.0" - } - }, - "node_modules/url": { - "version": "0.11.0", - "integrity": "sha1-ODjpfPxgUh63PFJajlW/3Z4uKPE=", - "dev": true, - "dependencies": { - "punycode": "1.3.2", - "querystring": "0.2.0" - } - }, - "node_modules/url/node_modules/punycode": { - "version": "1.3.2", - "integrity": "sha1-llOgNvt8HuQjQvIyXM7v6jkmxI0=", - "dev": true - }, - "node_modules/util-deprecate": { - "version": "1.0.2", - "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=" - }, - "node_modules/utils-merge": { - "version": "1.0.1", - "integrity": "sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM=", - "dev": true, - "engines": { - "node": ">= 0.4.0" - } - }, - "node_modules/uuid": { - "version": "3.4.0", - "integrity": "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==", - "deprecated": "Please upgrade to version 7 or higher. Older versions may use Math.random() in certain circumstances, which is known to be problematic. See https://v8.dev/blog/math-random for details.", - "dev": true, - "bin": { - "uuid": "bin/uuid" - } - }, - "node_modules/vary": { - "version": "1.1.2", - "integrity": "sha1-IpnwLG3tMNSllhsLn3RSShj2NPw=", - "dev": true, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/watchpack": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.2.0.tgz", - "integrity": "sha512-up4YAn/XHgZHIxFBVCdlMiWDj6WaLKpwVeGQk2I5thdYxF/KmF0aaz6TfJZ/hfl1h/XlcDr7k1KH7ThDagpFaA==", - "dev": true, - "dependencies": { - "glob-to-regexp": "^0.4.1", - "graceful-fs": "^4.1.2" - }, - "engines": { - "node": ">=10.13.0" - } - }, - "node_modules/wbuf": { - "version": "1.7.3", - "integrity": "sha512-O84QOnr0icsbFGLS0O3bI5FswxzRr8/gHwWkDlQFskhSPryQXvrTMxjxGP4+iWYoauLoBvfDpkrOauZ+0iZpDA==", - "dev": true, - "dependencies": { - "minimalistic-assert": "^1.0.0" - } - }, - "node_modules/webpack": { - "version": "5.64.2", - "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.64.2.tgz", - "integrity": "sha512-4KGc0+Ozi0aS3EaLNRvEppfZUer+CaORKqL6OBjDLZOPf9YfN8leagFzwe6/PoBdHFxc/utKArl8LMC0Ivtmdg==", - "dev": true, - "dependencies": { - "@types/eslint-scope": "^3.7.0", - "@types/estree": "^0.0.50", - "@webassemblyjs/ast": "1.11.1", - "@webassemblyjs/wasm-edit": "1.11.1", - "@webassemblyjs/wasm-parser": "1.11.1", - "acorn": "^8.4.1", - "acorn-import-assertions": "^1.7.6", - "browserslist": "^4.14.5", - "chrome-trace-event": "^1.0.2", - "enhanced-resolve": "^5.8.3", - "es-module-lexer": "^0.9.0", - "eslint-scope": "5.1.1", - "events": "^3.2.0", - "glob-to-regexp": "^0.4.1", - "graceful-fs": "^4.2.4", - "json-parse-better-errors": "^1.0.2", - "loader-runner": "^4.2.0", - "mime-types": "^2.1.27", - "neo-async": "^2.6.2", - "schema-utils": "^3.1.0", - "tapable": "^2.1.1", - "terser-webpack-plugin": "^5.1.3", - "watchpack": "^2.2.0", - "webpack-sources": "^3.2.2" - }, - "bin": { - "webpack": "bin/webpack.js" - }, - "engines": { - "node": ">=10.13.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - }, - "peerDependenciesMeta": { - "webpack-cli": { - "optional": true - } - } - }, - "node_modules/webpack-cli": { - "version": "4.9.1", - "resolved": "https://registry.npmjs.org/webpack-cli/-/webpack-cli-4.9.1.tgz", - "integrity": "sha512-JYRFVuyFpzDxMDB+v/nanUdQYcZtqFPGzmlW4s+UkPMFhSpfRNmf1z4AwYcHJVdvEFAM7FFCQdNTpsBYhDLusQ==", - "dev": true, - "dependencies": { - "@discoveryjs/json-ext": "^0.5.0", - "@webpack-cli/configtest": "^1.1.0", - "@webpack-cli/info": "^1.4.0", - "@webpack-cli/serve": "^1.6.0", - "colorette": "^2.0.14", - "commander": "^7.0.0", - "execa": "^5.0.0", - "fastest-levenshtein": "^1.0.12", - "import-local": "^3.0.2", - "interpret": "^2.2.0", - "rechoir": "^0.7.0", - "webpack-merge": "^5.7.3" - }, - "bin": { - "webpack-cli": "bin/cli.js" - }, - "engines": { - "node": ">=10.13.0" - }, - "peerDependencies": { - "webpack": "4.x.x || 5.x.x" - }, - "peerDependenciesMeta": { - "@webpack-cli/generators": { - "optional": true - }, - "@webpack-cli/migrate": { - "optional": true - }, - "webpack-bundle-analyzer": { - "optional": true - }, - "webpack-dev-server": { - "optional": true - } - } - }, - "node_modules/webpack-cli/node_modules/commander": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/commander/-/commander-7.2.0.tgz", - "integrity": "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==", - "dev": true, - "engines": { - "node": ">= 10" - } - }, - "node_modules/webpack-dev-middleware": { - "version": "5.2.2", - "resolved": "https://registry.npmjs.org/webpack-dev-middleware/-/webpack-dev-middleware-5.2.2.tgz", - "integrity": "sha512-DjZyYrsHhkikAFNvSNKrpnziXukU1EChFAh9j4LAm6ndPLPW8cN0KhM7T+RAiOqsQ6ABfQ8hoKIs9IWMTjov+w==", - "dev": true, - "dependencies": { - "colorette": "^2.0.10", - "memfs": "^3.2.2", - "mime-types": "^2.1.31", - "range-parser": "^1.2.1", - "schema-utils": "^4.0.0" - }, - "engines": { - "node": ">= 12.13.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - }, - "peerDependencies": { - "webpack": "^4.0.0 || ^5.0.0" - } - }, - "node_modules/webpack-dev-middleware/node_modules/ajv": { - "version": "8.8.2", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.8.2.tgz", - "integrity": "sha512-x9VuX+R/jcFj1DHo/fCp99esgGDWiHENrKxaCENuCxpoMCmAt/COCGVDwA7kleEpEzJjDnvh3yGoOuLu0Dtllw==", - "dev": true, - "dependencies": { - "fast-deep-equal": "^3.1.1", - "json-schema-traverse": "^1.0.0", - "require-from-string": "^2.0.2", - "uri-js": "^4.2.2" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" - } - }, - "node_modules/webpack-dev-middleware/node_modules/ajv-keywords": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-5.1.0.tgz", - "integrity": "sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw==", - "dev": true, - "dependencies": { - "fast-deep-equal": "^3.1.3" - }, - "peerDependencies": { - "ajv": "^8.8.2" - } - }, - "node_modules/webpack-dev-middleware/node_modules/fs-monkey": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/fs-monkey/-/fs-monkey-1.0.3.tgz", - "integrity": "sha512-cybjIfiiE+pTWicSCLFHSrXZ6EilF30oh91FDP9S2B051prEa7QWfrVTQm10/dDpswBDXZugPa1Ogu8Yh+HV0Q==", - "dev": true - }, - "node_modules/webpack-dev-middleware/node_modules/json-schema-traverse": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", - "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", - "dev": true - }, - "node_modules/webpack-dev-middleware/node_modules/memfs": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/memfs/-/memfs-3.3.0.tgz", - "integrity": "sha512-BEE62uMfKOavX3iG7GYX43QJ+hAeeWnwIAuJ/R6q96jaMtiLzhsxHJC8B1L7fK7Pt/vXDRwb3SG/yBpNGDPqzg==", - "dev": true, - "dependencies": { - "fs-monkey": "1.0.3" - }, - "engines": { - "node": ">= 4.0.0" - } - }, - "node_modules/webpack-dev-middleware/node_modules/schema-utils": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.0.0.tgz", - "integrity": "sha512-1edyXKgh6XnJsJSQ8mKWXnN/BVaIbFMLpouRUrXgVq7WYne5kw3MW7UPhO44uRXQSIpTSXoJbmrR2X0w9kUTyg==", - "dev": true, - "dependencies": { - "@types/json-schema": "^7.0.9", - "ajv": "^8.8.0", - "ajv-formats": "^2.1.1", - "ajv-keywords": "^5.0.0" - }, - "engines": { - "node": ">= 12.13.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - } - }, - "node_modules/webpack-dev-server": { - "version": "4.5.0", - "resolved": "https://registry.npmjs.org/webpack-dev-server/-/webpack-dev-server-4.5.0.tgz", - "integrity": "sha512-Ss4WptsUjYa+3hPI4iYZYEc8FrtnfkaPrm5WTjk9ux5kiCS718836srs0ppKMHRaCHP5mQ6g4JZGcfDdGbCjpQ==", - "dev": true, - "dependencies": { - "ansi-html-community": "^0.0.8", - "bonjour": "^3.5.0", - "chokidar": "^3.5.2", - "colorette": "^2.0.10", - "compression": "^1.7.4", - "connect-history-api-fallback": "^1.6.0", - "del": "^6.0.0", - "express": "^4.17.1", - "graceful-fs": "^4.2.6", - "html-entities": "^2.3.2", - "http-proxy-middleware": "^2.0.0", - "internal-ip": "^6.2.0", - "ipaddr.js": "^2.0.1", - "open": "^8.0.9", - "p-retry": "^4.5.0", - "portfinder": "^1.0.28", - "schema-utils": "^3.1.0", - "selfsigned": "^1.10.11", - "serve-index": "^1.9.1", - "sockjs": "^0.3.21", - "spdy": "^4.0.2", - "strip-ansi": "^7.0.0", - "url": "^0.11.0", - "webpack-dev-middleware": "^5.2.1", - "ws": "^8.1.0" - }, - "bin": { - "webpack-dev-server": "bin/webpack-dev-server.js" - }, - "engines": { - "node": ">= 12.13.0" - }, - "peerDependencies": { - "webpack": "^4.37.0 || ^5.0.0" - }, - "peerDependenciesMeta": { - "webpack-cli": { - "optional": true - } - } - }, - "node_modules/webpack-dev-server/node_modules/ipaddr.js": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-2.0.1.tgz", - "integrity": "sha512-1qTgH9NG+IIJ4yfKs2e6Pp1bZg8wbDbKHT21HrLIeYBTRLgMYKnMTPAuI3Lcs61nfx5h1xlXnbJtH1kX5/d/ng==", - "dev": true, - "engines": { - "node": ">= 10" - } - }, - "node_modules/webpack-merge": { - "version": "5.8.0", - "resolved": "https://registry.npmjs.org/webpack-merge/-/webpack-merge-5.8.0.tgz", - "integrity": "sha512-/SaI7xY0831XwP6kzuwhKWVKDP9t1QY1h65lAFLbZqMPIuYcD9QAW4u9STIbU9kaJbPBB/geU/gLr1wDjOhQ+Q==", - "dev": true, - "dependencies": { - "clone-deep": "^4.0.1", - "wildcard": "^2.0.0" - }, - "engines": { - "node": ">=10.0.0" - } - }, - "node_modules/webpack-sources": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-3.2.2.tgz", - "integrity": "sha512-cp5qdmHnu5T8wRg2G3vZZHoJPN14aqQ89SyQ11NpGH5zEMDCclt49rzo+MaRazk7/UeILhAI+/sEtcM+7Fr0nw==", - "dev": true, - "engines": { - "node": ">=10.13.0" - } - }, - "node_modules/websocket-driver": { - "version": "0.7.4", - "integrity": "sha512-b17KeDIQVjvb0ssuSDF2cYXSg2iztliJ4B9WdsuB6J952qCPKmnVq4DyW5motImXHDC1cBT/1UezrJVsKw5zjg==", - "dev": true, - "dependencies": { - "http-parser-js": ">=0.5.1", - "safe-buffer": ">=5.1.0", - "websocket-extensions": ">=0.1.1" - }, - "engines": { - "node": ">=0.8.0" - } - }, - "node_modules/websocket-extensions": { - "version": "0.1.4", - "integrity": "sha512-OqedPIGOfsDlo31UNwYbCFMSaO9m9G/0faIHj5/dZFDMFqPTcx6UwqyOy3COEaEOg/9VsGIpdqn62W5KhoKSpg==", - "dev": true, - "engines": { - "node": ">=0.8.0" - } - }, - "node_modules/which": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", - "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", - "dev": true, - "dependencies": { - "isexe": "^2.0.0" - }, - "bin": { - "node-which": "bin/node-which" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/wildcard": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/wildcard/-/wildcard-2.0.0.tgz", - "integrity": "sha512-JcKqAHLPxcdb9KM49dufGXn2x3ssnfjbcaQdLlfZsL9rH9wgDQjUtDxbo8NE0F6SFvydeu1VhZe7hZuHsB2/pw==", - "dev": true - }, - "node_modules/wrappy": { - "version": "1.0.2", - "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=" - }, - "node_modules/ws": { - "version": "8.2.3", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.2.3.tgz", - "integrity": "sha512-wBuoj1BDpC6ZQ1B7DWQBYVLphPWkm8i9Y0/3YdHjHKHiohOJ1ws+3OccDWtH+PoC9DZD5WOTrJvNbWvjS6JWaA==", - "dev": true, - "engines": { - "node": ">=10.0.0" - }, - "peerDependencies": { - "bufferutil": "^4.0.1", - "utf-8-validate": "^5.0.2" - }, - "peerDependenciesMeta": { - "bufferutil": { - "optional": true - }, - "utf-8-validate": { - "optional": true - } - } - } - }, - "dependencies": { - "@discoveryjs/json-ext": { - "version": "0.5.5", - "resolved": "https://registry.npmjs.org/@discoveryjs/json-ext/-/json-ext-0.5.5.tgz", - "integrity": "sha512-6nFkfkmSeV/rqSaS4oWHgmpnYw194f6hmWF5is6b0J1naJZoiD0NTc9AiUwPHvWsowkjuHErCZT1wa0jg+BLIA==", - "dev": true - }, - "@nodelib/fs.scandir": { - "version": "2.1.5", - "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", - "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", - "dev": true, - "requires": { - "@nodelib/fs.stat": "2.0.5", - "run-parallel": "^1.1.9" - } - }, - "@nodelib/fs.stat": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", - "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", - "dev": true - }, - "@nodelib/fs.walk": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", - "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", - "dev": true, - "requires": { - "@nodelib/fs.scandir": "2.1.5", - "fastq": "^1.6.0" - } - }, - "@types/eslint": { - "version": "8.2.0", - "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-8.2.0.tgz", - "integrity": "sha512-74hbvsnc+7TEDa1z5YLSe4/q8hGYB3USNvCuzHUJrjPV6hXaq8IXcngCrHkuvFt0+8rFz7xYXrHgNayIX0UZvQ==", - "dev": true, - "requires": { - "@types/estree": "*", - "@types/json-schema": "*" - } - }, - "@types/eslint-scope": { - "version": "3.7.1", - "resolved": "https://registry.npmjs.org/@types/eslint-scope/-/eslint-scope-3.7.1.tgz", - "integrity": "sha512-SCFeogqiptms4Fg29WpOTk5nHIzfpKCemSN63ksBQYKTcXoJEmJagV+DhVmbapZzY4/5YaOV1nZwrsU79fFm1g==", - "dev": true, - "requires": { - "@types/eslint": "*", - "@types/estree": "*" - } - }, - "@types/estree": { - "version": "0.0.50", - "resolved": "https://registry.npmjs.org/@types/estree/-/estree-0.0.50.tgz", - "integrity": "sha512-C6N5s2ZFtuZRj54k2/zyRhNDjJwwcViAM3Nbm8zjBpbqAdZ00mr0CFxvSKeO8Y/e03WVFLpQMdHYVfUd6SB+Hw==", - "dev": true - }, - "@types/http-proxy": { - "version": "1.17.7", - "resolved": "https://registry.npmjs.org/@types/http-proxy/-/http-proxy-1.17.7.tgz", - "integrity": "sha512-9hdj6iXH64tHSLTY+Vt2eYOGzSogC+JQ2H7bdPWkuh7KXP5qLllWx++t+K9Wk556c3dkDdPws/SpMRi0sdCT1w==", - "dev": true, - "requires": { - "@types/node": "*" - } - }, - "@types/json-schema": { - "version": "7.0.9", - "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.9.tgz", - "integrity": "sha512-qcUXuemtEu+E5wZSJHNxUXeCZhAfXKQ41D+duX+VYPde7xyEVZci+/oXKJL13tnRs9lR2pr4fod59GT6/X1/yQ==", - "dev": true - }, - "@types/node": { - "version": "16.11.9", - "resolved": "https://registry.npmjs.org/@types/node/-/node-16.11.9.tgz", - "integrity": "sha512-MKmdASMf3LtPzwLyRrFjtFFZ48cMf8jmX5VRYrDQiJa8Ybu5VAmkqBWqKU8fdCwD8ysw4mQ9nrEHvzg6gunR7A==", - "dev": true - }, - "@types/retry": { - "version": "0.12.1", - "resolved": "https://registry.npmjs.org/@types/retry/-/retry-0.12.1.tgz", - "integrity": "sha512-xoDlM2S4ortawSWORYqsdU+2rxdh4LRW9ytc3zmT37RIKQh6IHyKwwtKhKis9ah8ol07DCkZxPt8BBvPjC6v4g==", - "dev": true - }, - "@wasmer/wasi": { - "version": "0.12.0", - "integrity": "sha512-FJhLZKAfLWm/yjQI7eCRHNbA8ezmb7LSpUYFkHruZXs2mXk2+DaQtSElEtOoNrVQ4vApTyVaAd5/b7uEu8w6wQ==", - "requires": { - "browser-process-hrtime": "^1.0.0", - "buffer-es6": "^4.9.3", - "path-browserify": "^1.0.0", - "randomfill": "^1.0.4" - } - }, - "@wasmer/wasmfs": { - "version": "0.12.0", - "integrity": "sha512-m1ftchyQ1DfSenm5XbbdGIpb6KJHH5z0gODo3IZr6lATkj4WXfX/UeBTZ0aG9YVShBp+kHLdUHvOkqjy6p/GWw==", - "requires": { - "memfs": "3.0.4", - "pako": "^1.0.11", - "tar-stream": "^2.1.0" - } - }, - "@webassemblyjs/ast": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.11.1.tgz", - "integrity": "sha512-ukBh14qFLjxTQNTXocdyksN5QdM28S1CxHt2rdskFyL+xFV7VremuBLVbmCePj+URalXBENx/9Lm7lnhihtCSw==", - "dev": true, - "requires": { - "@webassemblyjs/helper-numbers": "1.11.1", - "@webassemblyjs/helper-wasm-bytecode": "1.11.1" - } - }, - "@webassemblyjs/floating-point-hex-parser": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.11.1.tgz", - "integrity": "sha512-iGRfyc5Bq+NnNuX8b5hwBrRjzf0ocrJPI6GWFodBFzmFnyvrQ83SHKhmilCU/8Jv67i4GJZBMhEzltxzcNagtQ==", - "dev": true - }, - "@webassemblyjs/helper-api-error": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-api-error/-/helper-api-error-1.11.1.tgz", - "integrity": "sha512-RlhS8CBCXfRUR/cwo2ho9bkheSXG0+NwooXcc3PAILALf2QLdFyj7KGsKRbVc95hZnhnERon4kW/D3SZpp6Tcg==", - "dev": true - }, - "@webassemblyjs/helper-buffer": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-buffer/-/helper-buffer-1.11.1.tgz", - "integrity": "sha512-gwikF65aDNeeXa8JxXa2BAk+REjSyhrNC9ZwdT0f8jc4dQQeDQ7G4m0f2QCLPJiMTTO6wfDmRmj/pW0PsUvIcA==", - "dev": true - }, - "@webassemblyjs/helper-numbers": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-numbers/-/helper-numbers-1.11.1.tgz", - "integrity": "sha512-vDkbxiB8zfnPdNK9Rajcey5C0w+QJugEglN0of+kmO8l7lDb77AnlKYQF7aarZuCrv+l0UvqL+68gSDr3k9LPQ==", - "dev": true, - "requires": { - "@webassemblyjs/floating-point-hex-parser": "1.11.1", - "@webassemblyjs/helper-api-error": "1.11.1", - "@xtuc/long": "4.2.2" - } - }, - "@webassemblyjs/helper-wasm-bytecode": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.11.1.tgz", - "integrity": "sha512-PvpoOGiJwXeTrSf/qfudJhwlvDQxFgelbMqtq52WWiXC6Xgg1IREdngmPN3bs4RoO83PnL/nFrxucXj1+BX62Q==", - "dev": true - }, - "@webassemblyjs/helper-wasm-section": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.11.1.tgz", - "integrity": "sha512-10P9No29rYX1j7F3EVPX3JvGPQPae+AomuSTPiF9eBQeChHI6iqjMIwR9JmOJXwpnn/oVGDk7I5IlskuMwU/pg==", - "dev": true, - "requires": { - "@webassemblyjs/ast": "1.11.1", - "@webassemblyjs/helper-buffer": "1.11.1", - "@webassemblyjs/helper-wasm-bytecode": "1.11.1", - "@webassemblyjs/wasm-gen": "1.11.1" - } - }, - "@webassemblyjs/ieee754": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/ieee754/-/ieee754-1.11.1.tgz", - "integrity": "sha512-hJ87QIPtAMKbFq6CGTkZYJivEwZDbQUgYd3qKSadTNOhVY7p+gfP6Sr0lLRVTaG1JjFj+r3YchoqRYxNH3M0GQ==", - "dev": true, - "requires": { - "@xtuc/ieee754": "^1.2.0" - } - }, - "@webassemblyjs/leb128": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/leb128/-/leb128-1.11.1.tgz", - "integrity": "sha512-BJ2P0hNZ0u+Th1YZXJpzW6miwqQUGcIHT1G/sf72gLVD9DZ5AdYTqPNbHZh6K1M5VmKvFXwGSWZADz+qBWxeRw==", - "dev": true, - "requires": { - "@xtuc/long": "4.2.2" - } - }, - "@webassemblyjs/utf8": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/utf8/-/utf8-1.11.1.tgz", - "integrity": "sha512-9kqcxAEdMhiwQkHpkNiorZzqpGrodQQ2IGrHHxCy+Ozng0ofyMA0lTqiLkVs1uzTRejX+/O0EOT7KxqVPuXosQ==", - "dev": true - }, - "@webassemblyjs/wasm-edit": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-edit/-/wasm-edit-1.11.1.tgz", - "integrity": "sha512-g+RsupUC1aTHfR8CDgnsVRVZFJqdkFHpsHMfJuWQzWU3tvnLC07UqHICfP+4XyL2tnr1amvl1Sdp06TnYCmVkA==", - "dev": true, - "requires": { - "@webassemblyjs/ast": "1.11.1", - "@webassemblyjs/helper-buffer": "1.11.1", - "@webassemblyjs/helper-wasm-bytecode": "1.11.1", - "@webassemblyjs/helper-wasm-section": "1.11.1", - "@webassemblyjs/wasm-gen": "1.11.1", - "@webassemblyjs/wasm-opt": "1.11.1", - "@webassemblyjs/wasm-parser": "1.11.1", - "@webassemblyjs/wast-printer": "1.11.1" - } - }, - "@webassemblyjs/wasm-gen": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-gen/-/wasm-gen-1.11.1.tgz", - "integrity": "sha512-F7QqKXwwNlMmsulj6+O7r4mmtAlCWfO/0HdgOxSklZfQcDu0TpLiD1mRt/zF25Bk59FIjEuGAIyn5ei4yMfLhA==", - "dev": true, - "requires": { - "@webassemblyjs/ast": "1.11.1", - "@webassemblyjs/helper-wasm-bytecode": "1.11.1", - "@webassemblyjs/ieee754": "1.11.1", - "@webassemblyjs/leb128": "1.11.1", - "@webassemblyjs/utf8": "1.11.1" - } - }, - "@webassemblyjs/wasm-opt": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-opt/-/wasm-opt-1.11.1.tgz", - "integrity": "sha512-VqnkNqnZlU5EB64pp1l7hdm3hmQw7Vgqa0KF/KCNO9sIpI6Fk6brDEiX+iCOYrvMuBWDws0NkTOxYEb85XQHHw==", - "dev": true, - "requires": { - "@webassemblyjs/ast": "1.11.1", - "@webassemblyjs/helper-buffer": "1.11.1", - "@webassemblyjs/wasm-gen": "1.11.1", - "@webassemblyjs/wasm-parser": "1.11.1" - } - }, - "@webassemblyjs/wasm-parser": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-parser/-/wasm-parser-1.11.1.tgz", - "integrity": "sha512-rrBujw+dJu32gYB7/Lup6UhdkPx9S9SnobZzRVL7VcBH9Bt9bCBLEuX/YXOOtBsOZ4NQrRykKhffRWHvigQvOA==", - "dev": true, - "requires": { - "@webassemblyjs/ast": "1.11.1", - "@webassemblyjs/helper-api-error": "1.11.1", - "@webassemblyjs/helper-wasm-bytecode": "1.11.1", - "@webassemblyjs/ieee754": "1.11.1", - "@webassemblyjs/leb128": "1.11.1", - "@webassemblyjs/utf8": "1.11.1" - } - }, - "@webassemblyjs/wast-printer": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wast-printer/-/wast-printer-1.11.1.tgz", - "integrity": "sha512-IQboUWM4eKzWW+N/jij2sRatKMh99QEelo3Eb2q0qXkvPRISAj8Qxtmw5itwqK+TTkBuUIE45AxYPToqPtL5gg==", - "dev": true, - "requires": { - "@webassemblyjs/ast": "1.11.1", - "@xtuc/long": "4.2.2" - } - }, - "@webpack-cli/configtest": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@webpack-cli/configtest/-/configtest-1.1.0.tgz", - "integrity": "sha512-ttOkEkoalEHa7RaFYpM0ErK1xc4twg3Am9hfHhL7MVqlHebnkYd2wuI/ZqTDj0cVzZho6PdinY0phFZV3O0Mzg==", - "dev": true, - "requires": {} - }, - "@webpack-cli/info": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/@webpack-cli/info/-/info-1.4.0.tgz", - "integrity": "sha512-F6b+Man0rwE4n0409FyAJHStYA5OIZERxmnUfLVwv0mc0V1wLad3V7jqRlMkgKBeAq07jUvglacNaa6g9lOpuw==", - "dev": true, - "requires": { - "envinfo": "^7.7.3" - } - }, - "@webpack-cli/serve": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/@webpack-cli/serve/-/serve-1.6.0.tgz", - "integrity": "sha512-ZkVeqEmRpBV2GHvjjUZqEai2PpUbuq8Bqd//vEYsp63J8WyexI8ppCqVS3Zs0QADf6aWuPdU+0XsPI647PVlQA==", - "dev": true, - "requires": {} - }, - "@xtuc/ieee754": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/@xtuc/ieee754/-/ieee754-1.2.0.tgz", - "integrity": "sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA==", - "dev": true - }, - "@xtuc/long": { - "version": "4.2.2", - "resolved": "https://registry.npmjs.org/@xtuc/long/-/long-4.2.2.tgz", - "integrity": "sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==", - "dev": true - }, - "accepts": { - "version": "1.3.7", - "integrity": "sha512-Il80Qs2WjYlJIBNzNkK6KYqlVMTbZLXgHx2oT0pU/fjRHyEp+PEfEPY0R3WCwAGVOtauxh1hOxNgIf5bv7dQpA==", - "dev": true, - "requires": { - "mime-types": "~2.1.24", - "negotiator": "0.6.2" - } - }, - "acorn": { - "version": "8.6.0", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.6.0.tgz", - "integrity": "sha512-U1riIR+lBSNi3IbxtaHOIKdH8sLFv3NYfNv8sg7ZsNhcfl4HF2++BfqqrNAxoCLQW1iiylOj76ecnaUxz+z9yw==", - "dev": true - }, - "acorn-import-assertions": { - "version": "1.8.0", - "resolved": "https://registry.npmjs.org/acorn-import-assertions/-/acorn-import-assertions-1.8.0.tgz", - "integrity": "sha512-m7VZ3jwz4eK6A4Vtt8Ew1/mNbP24u0FhdyfA7fSvnJR6LMdfOYnmuIrrJAgrYfYJ10F/otaHTtrtrtmHdMNzEw==", - "dev": true, - "requires": {} - }, - "aggregate-error": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/aggregate-error/-/aggregate-error-3.1.0.tgz", - "integrity": "sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA==", - "dev": true, - "requires": { - "clean-stack": "^2.0.0", - "indent-string": "^4.0.0" - } - }, - "ajv": { - "version": "6.12.6", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", - "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", - "dev": true, - "requires": { - "fast-deep-equal": "^3.1.1", - "fast-json-stable-stringify": "^2.0.0", - "json-schema-traverse": "^0.4.1", - "uri-js": "^4.2.2" - } - }, - "ajv-formats": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-2.1.1.tgz", - "integrity": "sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==", - "dev": true, - "requires": { - "ajv": "^8.0.0" - }, - "dependencies": { - "ajv": { - "version": "8.8.2", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.8.2.tgz", - "integrity": "sha512-x9VuX+R/jcFj1DHo/fCp99esgGDWiHENrKxaCENuCxpoMCmAt/COCGVDwA7kleEpEzJjDnvh3yGoOuLu0Dtllw==", - "dev": true, - "requires": { - "fast-deep-equal": "^3.1.1", - "json-schema-traverse": "^1.0.0", - "require-from-string": "^2.0.2", - "uri-js": "^4.2.2" - } - }, - "json-schema-traverse": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", - "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", - "dev": true - } - } - }, - "ajv-keywords": { - "version": "3.5.2", - "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz", - "integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==", - "dev": true, - "requires": {} - }, - "ansi-html-community": { - "version": "0.0.8", - "resolved": "https://registry.npmjs.org/ansi-html-community/-/ansi-html-community-0.0.8.tgz", - "integrity": "sha512-1APHAyr3+PCamwNw3bXCPp4HFLONZt/yIH0sZp0/469KWNTEy+qN5jQ3GVX6DMZ1UXAi34yVwtTeaG/HpBuuzw==", - "dev": true - }, - "ansi-regex": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.0.1.tgz", - "integrity": "sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==", - "dev": true - }, - "anymatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.2.tgz", - "integrity": "sha512-P43ePfOAIupkguHUycrc4qJ9kz8ZiuOUijaETwX7THt0Y/GNK7v0aa8rY816xWjZ7rJdA5XdMcpVFTKMq+RvWg==", - "dev": true, - "requires": { - "normalize-path": "^3.0.0", - "picomatch": "^2.0.4" - } - }, - "array-flatten": { - "version": "2.1.2", - "integrity": "sha512-hNfzcOV8W4NdualtqBFPyVO+54DSJuZGY9qT4pRroB6S9e3iiido2ISIC5h9R2sPJ8H3FHCIiEnsv1lPXO3KtQ==", - "dev": true - }, - "array-union": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz", - "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==", - "dev": true - }, - "async": { - "version": "2.6.3", - "integrity": "sha512-zflvls11DCy+dQWzTW2dzuilv8Z5X/pjfmZOWba6TNIVDm+2UDaJmXSOXlasHKfNBs8oo3M0aT50fDEWfKZjXg==", - "dev": true, - "requires": { - "lodash": "^4.17.14" - } - }, - "balanced-match": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", - "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", - "dev": true - }, - "base64-js": { - "version": "1.5.1", - "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==" - }, - "batch": { - "version": "0.6.1", - "integrity": "sha1-3DQxT05nkxgJP8dgJyUl+UvyXBY=", - "dev": true - }, - "binary-extensions": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz", - "integrity": "sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==", - "dev": true - }, - "bl": { - "version": "4.1.0", - "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", - "requires": { - "buffer": "^5.5.0", - "inherits": "^2.0.4", - "readable-stream": "^3.4.0" - } - }, - "body-parser": { - "version": "1.19.0", - "integrity": "sha512-dhEPs72UPbDnAQJ9ZKMNTP6ptJaionhP5cBb541nXPlW60Jepo9RV/a4fX4XWW9CuFNK22krhrj1+rgzifNCsw==", - "dev": true, - "requires": { - "bytes": "3.1.0", - "content-type": "~1.0.4", - "debug": "2.6.9", - "depd": "~1.1.2", - "http-errors": "1.7.2", - "iconv-lite": "0.4.24", - "on-finished": "~2.3.0", - "qs": "6.7.0", - "raw-body": "2.4.0", - "type-is": "~1.6.17" - }, - "dependencies": { - "bytes": { - "version": "3.1.0", - "integrity": "sha512-zauLjrfCG+xvoyaqLoV8bLVXXNGC4JqlxFCutSDWA6fJrTo2ZuvLYTqZ7aHBLZSMOopbzwv8f+wZcVzfVTI2Dg==", - "dev": true - } - } - }, - "bonjour": { - "version": "3.5.0", - "integrity": "sha1-jokKGD2O6aI5OzhExpGkK897yfU=", - "dev": true, - "requires": { - "array-flatten": "^2.1.0", - "deep-equal": "^1.0.1", - "dns-equal": "^1.0.0", - "dns-txt": "^2.0.2", - "multicast-dns": "^6.0.1", - "multicast-dns-service-types": "^1.1.0" - } - }, - "brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", - "dev": true, - "requires": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "braces": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", - "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", - "dev": true, - "requires": { - "fill-range": "^7.0.1" - } - }, - "browser-process-hrtime": { - "version": "1.0.0", - "integrity": "sha512-9o5UecI3GhkpM6DrXr69PblIuWxPKk9Y0jHBRhdocZ2y7YECBFCsHm79Pr3OyR2AvjhDkabFJaDJMYRazHgsow==" - }, - "browserslist": { - "version": "4.18.1", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.18.1.tgz", - "integrity": "sha512-8ScCzdpPwR2wQh8IT82CA2VgDwjHyqMovPBZSNH54+tm4Jk2pCuv90gmAdH6J84OCRWi0b4gMe6O6XPXuJnjgQ==", - "dev": true, - "requires": { - "caniuse-lite": "^1.0.30001280", - "electron-to-chromium": "^1.3.896", - "escalade": "^3.1.1", - "node-releases": "^2.0.1", - "picocolors": "^1.0.0" - } - }, - "buffer": { - "version": "5.7.1", - "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", - "requires": { - "base64-js": "^1.3.1", - "ieee754": "^1.1.13" - } - }, - "buffer-es6": { - "version": "4.9.3", - "integrity": "sha1-8mNHuC33b9N+GLy1KIxJcM/VxAQ=" - }, - "buffer-from": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", - "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", - "dev": true - }, - "buffer-indexof": { - "version": "1.1.1", - "integrity": "sha512-4/rOEg86jivtPTeOUUT61jJO1Ya1TrR/OkqCSZDyq84WJh3LuuiphBYJN+fm5xufIk4XAFcEwte/8WzC8If/1g==", - "dev": true - }, - "bytes": { - "version": "3.0.0", - "integrity": "sha1-0ygVQE1olpn4Wk6k+odV3ROpYEg=", - "dev": true - }, - "call-bind": { - "version": "1.0.2", - "integrity": "sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==", - "dev": true, - "requires": { - "function-bind": "^1.1.1", - "get-intrinsic": "^1.0.2" - } - }, - "caniuse-lite": { - "version": "1.0.30001282", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001282.tgz", - "integrity": "sha512-YhF/hG6nqBEllymSIjLtR2iWDDnChvhnVJqp+vloyt2tEHFG1yBR+ac2B/rOw0qOK0m0lEXU2dv4E/sMk5P9Kg==", - "dev": true - }, - "chokidar": { - "version": "3.5.2", - "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.2.tgz", - "integrity": "sha512-ekGhOnNVPgT77r4K/U3GDhu+FQ2S8TnK/s2KbIGXi0SZWuwkZ2QNyfWdZW+TVfn84DpEP7rLeCt2UI6bJ8GwbQ==", - "dev": true, - "requires": { - "anymatch": "~3.1.2", - "braces": "~3.0.2", - "fsevents": "~2.3.2", - "glob-parent": "~5.1.2", - "is-binary-path": "~2.1.0", - "is-glob": "~4.0.1", - "normalize-path": "~3.0.0", - "readdirp": "~3.6.0" - } - }, - "chrome-trace-event": { - "version": "1.0.3", - "integrity": "sha512-p3KULyQg4S7NIHixdwbGX+nFHkoBiA4YQmyWtjb8XngSKV124nJmRysgAeujbUVb15vh+RvFUfCPqU7rXk+hZg==", - "dev": true - }, - "clean-stack": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/clean-stack/-/clean-stack-2.2.0.tgz", - "integrity": "sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A==", - "dev": true - }, - "clone-deep": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/clone-deep/-/clone-deep-4.0.1.tgz", - "integrity": "sha512-neHB9xuzh/wk0dIHweyAXv2aPGZIVk3pLMe+/RNzINf17fe0OG96QroktYAUm7SM1PBnzTabaLboqqxDyMU+SQ==", - "dev": true, - "requires": { - "is-plain-object": "^2.0.4", - "kind-of": "^6.0.2", - "shallow-clone": "^3.0.0" - } - }, - "colorette": { - "version": "2.0.16", - "resolved": "https://registry.npmjs.org/colorette/-/colorette-2.0.16.tgz", - "integrity": "sha512-hUewv7oMjCp+wkBv5Rm0v87eJhq4woh5rSR+42YSQJKecCqgIqNkZ6lAlQms/BwHPJA5NKMRlpxPRv0n8HQW6g==", - "dev": true - }, - "commander": { - "version": "2.20.3", - "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", - "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", - "dev": true - }, - "compressible": { - "version": "2.0.18", - "integrity": "sha512-AF3r7P5dWxL8MxyITRMlORQNaOA2IkAFaTr4k7BUumjPtRpGDTZpl0Pb1XCO6JeDCBdp126Cgs9sMxqSjgYyRg==", - "dev": true, - "requires": { - "mime-db": ">= 1.43.0 < 2" - } - }, - "compression": { - "version": "1.7.4", - "integrity": "sha512-jaSIDzP9pZVS4ZfQ+TzvtiWhdpFhE2RDHz8QJkpX9SIpLq88VueF5jJw6t+6CUQcAoA6t+x89MLrWAqpfDE8iQ==", - "dev": true, - "requires": { - "accepts": "~1.3.5", - "bytes": "3.0.0", - "compressible": "~2.0.16", - "debug": "2.6.9", - "on-headers": "~1.0.2", - "safe-buffer": "5.1.2", - "vary": "~1.1.2" - }, - "dependencies": { - "safe-buffer": { - "version": "5.1.2", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", - "dev": true - } - } - }, - "concat-map": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=", - "dev": true - }, - "connect-history-api-fallback": { - "version": "1.6.0", - "integrity": "sha512-e54B99q/OUoH64zYYRf3HBP5z24G38h5D3qXu23JGRoigpX5Ss4r9ZnDk3g0Z8uQC2x2lPaJ+UlWBc1ZWBWdLg==", - "dev": true - }, - "content-disposition": { - "version": "0.5.3", - "integrity": "sha512-ExO0774ikEObIAEV9kDo50o+79VCUdEB6n6lzKgGwupcVeRlhrj3qGAfwq8G6uBJjkqLrhT0qEYFcWng8z1z0g==", - "dev": true, - "requires": { - "safe-buffer": "5.1.2" - }, - "dependencies": { - "safe-buffer": { - "version": "5.1.2", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", - "dev": true - } - } - }, - "content-type": { - "version": "1.0.4", - "integrity": "sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA==", - "dev": true - }, - "cookie": { - "version": "0.4.0", - "integrity": "sha512-+Hp8fLp57wnUSt0tY0tHEXh4voZRDnoIrZPqlo3DPiI4y9lwg/jqx+1Om94/W6ZaPDOUbnjOt/99w66zk+l1Xg==", - "dev": true - }, - "cookie-signature": { - "version": "1.0.6", - "integrity": "sha1-4wOogrNCzD7oylE6eZmXNNqzriw=", - "dev": true - }, - "core-util-is": { - "version": "1.0.2", - "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=", - "dev": true - }, - "cross-spawn": { - "version": "7.0.3", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", - "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", - "dev": true, - "requires": { - "path-key": "^3.1.0", - "shebang-command": "^2.0.0", - "which": "^2.0.1" - } - }, - "debug": { - "version": "2.6.9", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "dev": true, - "requires": { - "ms": "2.0.0" - } - }, - "deep-equal": { - "version": "1.1.1", - "integrity": "sha512-yd9c5AdiqVcR+JjcwUQb9DkhJc8ngNr0MahEBGvDiJw8puWab2yZlh+nkasOnZP+EGTAP6rRp2JzJhJZzvNF8g==", - "dev": true, - "requires": { - "is-arguments": "^1.0.4", - "is-date-object": "^1.0.1", - "is-regex": "^1.0.4", - "object-is": "^1.0.1", - "object-keys": "^1.1.1", - "regexp.prototype.flags": "^1.2.0" - } - }, - "default-gateway": { - "version": "6.0.3", - "resolved": "https://registry.npmjs.org/default-gateway/-/default-gateway-6.0.3.tgz", - "integrity": "sha512-fwSOJsbbNzZ/CUFpqFBqYfYNLj1NbMPm8MMCIzHjC83iSJRBEGmDUxU+WP661BaBQImeC2yHwXtz+P/O9o+XEg==", - "dev": true, - "requires": { - "execa": "^5.0.0" - } - }, - "define-lazy-prop": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-2.0.0.tgz", - "integrity": "sha512-Ds09qNh8yw3khSjiJjiUInaGX9xlqZDY7JVryGxdxV7NPeuqQfplOpQ66yJFZut3jLa5zOwkXw1g9EI2uKh4Og==", - "dev": true - }, - "define-properties": { - "version": "1.1.3", - "integrity": "sha512-3MqfYKj2lLzdMSf8ZIZE/V+Zuy+BgD6f164e8K2w7dgnpKArBDerGYpM46IYYcjnkdPNMjPk9A6VFB8+3SKlXQ==", - "dev": true, - "requires": { - "object-keys": "^1.0.12" - } - }, - "del": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/del/-/del-6.0.0.tgz", - "integrity": "sha512-1shh9DQ23L16oXSZKB2JxpL7iMy2E0S9d517ptA1P8iw0alkPtQcrKH7ru31rYtKwF499HkTu+DRzq3TCKDFRQ==", - "dev": true, - "requires": { - "globby": "^11.0.1", - "graceful-fs": "^4.2.4", - "is-glob": "^4.0.1", - "is-path-cwd": "^2.2.0", - "is-path-inside": "^3.0.2", - "p-map": "^4.0.0", - "rimraf": "^3.0.2", - "slash": "^3.0.0" - } - }, - "depd": { - "version": "1.1.2", - "integrity": "sha1-m81S4UwJd2PnSbJ0xDRu0uVgtak=", - "dev": true - }, - "destroy": { - "version": "1.0.4", - "integrity": "sha1-l4hXRCxEdJ5CBmE+N5RiBYJqvYA=", - "dev": true - }, - "detect-node": { - "version": "2.0.5", - "integrity": "sha512-qi86tE6hRcFHy8jI1m2VG+LaPUR1LhqDa5G8tVjuUXmOrpuAgqsA1pN0+ldgr3aKUH+QLI9hCY/OcRYisERejw==", - "dev": true - }, - "dir-glob": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", - "integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==", - "dev": true, - "requires": { - "path-type": "^4.0.0" - } - }, - "dns-equal": { - "version": "1.0.0", - "integrity": "sha1-s55/HabrCnW6nBcySzR1PEfgZU0=", - "dev": true - }, - "dns-packet": { - "version": "1.3.4", - "resolved": "https://registry.npmjs.org/dns-packet/-/dns-packet-1.3.4.tgz", - "integrity": "sha512-BQ6F4vycLXBvdrJZ6S3gZewt6rcrks9KBgM9vrhW+knGRqc8uEdT7fuCwloc7nny5xNoMJ17HGH0R/6fpo8ECA==", - "dev": true, - "requires": { - "ip": "^1.1.0", - "safe-buffer": "^5.0.1" - } - }, - "dns-txt": { - "version": "2.0.2", - "integrity": "sha1-uR2Ab10nGI5Ks+fRB9iBocxGQrY=", - "dev": true, - "requires": { - "buffer-indexof": "^1.0.0" - } - }, - "ee-first": { - "version": "1.1.1", - "integrity": "sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0=", - "dev": true - }, - "electron-to-chromium": { - "version": "1.3.904", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.3.904.tgz", - "integrity": "sha512-x5uZWXcVNYkTh4JubD7KSC1VMKz0vZwJUqVwY3ihsW0bst1BXDe494Uqbg3Y0fDGVjJqA8vEeGuvO5foyH2+qw==", - "dev": true - }, - "encodeurl": { - "version": "1.0.2", - "integrity": "sha1-rT/0yG7C0CkyL1oCw6mmBslbP1k=", - "dev": true - }, - "end-of-stream": { - "version": "1.4.4", - "integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==", - "requires": { - "once": "^1.4.0" - } - }, - "enhanced-resolve": { - "version": "5.8.3", - "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.8.3.tgz", - "integrity": "sha512-EGAbGvH7j7Xt2nc0E7D99La1OiEs8LnyimkRgwExpUMScN6O+3x9tIWs7PLQZVNx4YD+00skHXPXi1yQHpAmZA==", - "dev": true, - "requires": { - "graceful-fs": "^4.2.4", - "tapable": "^2.2.0" - } - }, - "envinfo": { - "version": "7.8.1", - "resolved": "https://registry.npmjs.org/envinfo/-/envinfo-7.8.1.tgz", - "integrity": "sha512-/o+BXHmB7ocbHEAs6F2EnG0ogybVVUdkRunTT2glZU9XAaGmhqskrvKwqXuDfNjEO0LZKWdejEEpnq8aM0tOaw==", - "dev": true - }, - "es-module-lexer": { - "version": "0.9.3", - "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-0.9.3.tgz", - "integrity": "sha512-1HQ2M2sPtxwnvOvT1ZClHyQDiggdNjURWpY2we6aMKCQiUVxTmVs2UYPLIrD84sS+kMdUwfBSylbJPwNnBrnHQ==", - "dev": true - }, - "escalade": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz", - "integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==", - "dev": true - }, - "escape-html": { - "version": "1.0.3", - "integrity": "sha1-Aljq5NPQwJdN4cFpGI7wBR0dGYg=", - "dev": true - }, - "eslint-scope": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz", - "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==", - "dev": true, - "requires": { - "esrecurse": "^4.3.0", - "estraverse": "^4.1.1" - } - }, - "esrecurse": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", - "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", - "dev": true, - "requires": { - "estraverse": "^5.2.0" - }, - "dependencies": { - "estraverse": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", - "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", - "dev": true - } - } - }, - "estraverse": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", - "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", - "dev": true - }, - "etag": { - "version": "1.8.1", - "integrity": "sha1-Qa4u62XvpiJorr/qg6x9eSmbCIc=", - "dev": true - }, - "eventemitter3": { - "version": "4.0.7", - "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz", - "integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==", - "dev": true - }, - "events": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", - "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==", - "dev": true - }, - "execa": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz", - "integrity": "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==", - "dev": true, - "requires": { - "cross-spawn": "^7.0.3", - "get-stream": "^6.0.0", - "human-signals": "^2.1.0", - "is-stream": "^2.0.0", - "merge-stream": "^2.0.0", - "npm-run-path": "^4.0.1", - "onetime": "^5.1.2", - "signal-exit": "^3.0.3", - "strip-final-newline": "^2.0.0" - } - }, - "express": { - "version": "4.17.1", - "integrity": "sha512-mHJ9O79RqluphRrcw2X/GTh3k9tVv8YcoyY4Kkh4WDMUYKRZUq0h1o0w2rrrxBqM7VoeUVqgb27xlEMXTnYt4g==", - "dev": true, - "requires": { - "accepts": "~1.3.7", - "array-flatten": "1.1.1", - "body-parser": "1.19.0", - "content-disposition": "0.5.3", - "content-type": "~1.0.4", - "cookie": "0.4.0", - "cookie-signature": "1.0.6", - "debug": "2.6.9", - "depd": "~1.1.2", - "encodeurl": "~1.0.2", - "escape-html": "~1.0.3", - "etag": "~1.8.1", - "finalhandler": "~1.1.2", - "fresh": "0.5.2", - "merge-descriptors": "1.0.1", - "methods": "~1.1.2", - "on-finished": "~2.3.0", - "parseurl": "~1.3.3", - "path-to-regexp": "0.1.7", - "proxy-addr": "~2.0.5", - "qs": "6.7.0", - "range-parser": "~1.2.1", - "safe-buffer": "5.1.2", - "send": "0.17.1", - "serve-static": "1.14.1", - "setprototypeof": "1.1.1", - "statuses": "~1.5.0", - "type-is": "~1.6.18", - "utils-merge": "1.0.1", - "vary": "~1.1.2" - }, - "dependencies": { - "array-flatten": { - "version": "1.1.1", - "integrity": "sha1-ml9pkFGx5wczKPKgCJaLZOopVdI=", - "dev": true - }, - "safe-buffer": { - "version": "5.1.2", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", - "dev": true - } - } - }, - "fast-deep-equal": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", - "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", - "dev": true - }, - "fast-extend": { - "version": "1.0.2", - "integrity": "sha512-XXA9RmlPatkFKUzqVZAFth18R4Wo+Xug/S+C7YlYA3xrXwfPlW3dqNwOb4hvQo7wZJ2cNDYhrYuPzVOfHy5/uQ==" - }, - "fast-glob": { - "version": "3.2.7", - "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.2.7.tgz", - "integrity": "sha512-rYGMRwip6lUMvYD3BTScMwT1HtAs2d71SMv66Vrxs0IekGZEjhM0pcMfjQPnknBt2zeCwQMEupiN02ZP4DiT1Q==", - "dev": true, - "requires": { - "@nodelib/fs.stat": "^2.0.2", - "@nodelib/fs.walk": "^1.2.3", - "glob-parent": "^5.1.2", - "merge2": "^1.3.0", - "micromatch": "^4.0.4" - } - }, - "fast-json-stable-stringify": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", - "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", - "dev": true - }, - "fastest-levenshtein": { - "version": "1.0.12", - "resolved": "https://registry.npmjs.org/fastest-levenshtein/-/fastest-levenshtein-1.0.12.tgz", - "integrity": "sha512-On2N+BpYJ15xIC974QNVuYGMOlEVt4s0EOI3wwMqOmK1fdDY+FN/zltPV8vosq4ad4c/gJ1KHScUn/6AWIgiow==", - "dev": true - }, - "fastq": { - "version": "1.13.0", - "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.13.0.tgz", - "integrity": "sha512-YpkpUnK8od0o1hmeSc7UUs/eB/vIPWJYjKck2QKIzAf71Vm1AAQ3EbuZB3g2JIy+pg+ERD0vqI79KyZiB2e2Nw==", - "dev": true, - "requires": { - "reusify": "^1.0.4" - } - }, - "faye-websocket": { - "version": "0.11.3", - "integrity": "sha512-D2y4bovYpzziGgbHYtGCMjlJM36vAl/y+xUyn1C+FVx8szd1E+86KwVw6XvYSzOP8iMpm1X0I4xJD+QtUb36OA==", - "dev": true, - "requires": { - "websocket-driver": ">=0.5.1" - } - }, - "fill-range": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", - "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", - "dev": true, - "requires": { - "to-regex-range": "^5.0.1" - } - }, - "finalhandler": { - "version": "1.1.2", - "integrity": "sha512-aAWcW57uxVNrQZqFXjITpW3sIUQmHGG3qSb9mUah9MgMC4NeWhNOlNjXEYq3HjRAvL6arUviZGGJsBg6z0zsWA==", - "dev": true, - "requires": { - "debug": "2.6.9", - "encodeurl": "~1.0.2", - "escape-html": "~1.0.3", - "on-finished": "~2.3.0", - "parseurl": "~1.3.3", - "statuses": "~1.5.0", - "unpipe": "~1.0.0" - } - }, - "find-up": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", - "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", - "dev": true, - "requires": { - "locate-path": "^5.0.0", - "path-exists": "^4.0.0" - } - }, - "follow-redirects": { - "version": "1.14.8", - "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.14.8.tgz", - "integrity": "sha512-1x0S9UVJHsQprFcEC/qnNzBLcIxsjAV905f/UkQxbclCsoTWlacCNOpQa/anodLl2uaEKFhfWOvM2Qg77+15zA==", - "dev": true - }, - "forwarded": { - "version": "0.1.2", - "integrity": "sha1-mMI9qxF1ZXuMBXPozszZGw/xjIQ=", - "dev": true - }, - "fresh": { - "version": "0.5.2", - "integrity": "sha1-PYyt2Q2XZWn6g1qx+OSyOhBWBac=", - "dev": true - }, - "fs-constants": { - "version": "1.0.0", - "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==" - }, - "fs-monkey": { - "version": "0.3.3", - "integrity": "sha512-FNUvuTAJ3CqCQb5ELn+qCbGR/Zllhf2HtwsdAtBi59s1WeCjKMT81fHcSu7dwIskqGVK+MmOrb7VOBlq3/SItw==" - }, - "fs.realpath": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", - "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=", - "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", - "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==", - "dev": true - }, - "get-intrinsic": { - "version": "1.1.1", - "integrity": "sha512-kWZrnVM42QCiEA2Ig1bG8zjoIMOgxWwYCEeNdwY6Tv/cOSeGpcoX4pXHfKUxNKVoArnrEr2e9srnAxxGIraS9Q==", - "dev": true, - "requires": { - "function-bind": "^1.1.1", - "has": "^1.0.3", - "has-symbols": "^1.0.1" - } - }, - "get-stream": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", - "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==", - "dev": true - }, - "glob": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.0.tgz", - "integrity": "sha512-lmLf6gtyrPq8tTjSmrO94wBeQbFR3HbLHbuyD69wuyQkImp2hWqMGB47OX65FBkPffO641IP9jWa1z4ivqG26Q==", - "dev": true, - "requires": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.0.4", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - } - }, - "glob-parent": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", - "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", - "dev": true, - "requires": { - "is-glob": "^4.0.1" - } - }, - "glob-to-regexp": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz", - "integrity": "sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==", - "dev": true - }, - "globby": { - "version": "11.0.4", - "resolved": "https://registry.npmjs.org/globby/-/globby-11.0.4.tgz", - "integrity": "sha512-9O4MVG9ioZJ08ffbcyVYyLOJLk5JQ688pJ4eMGLpdWLHq/Wr1D9BlriLQyL0E+jbkuePVZXYFj47QM/v093wHg==", - "dev": true, - "requires": { - "array-union": "^2.1.0", - "dir-glob": "^3.0.1", - "fast-glob": "^3.1.1", - "ignore": "^5.1.4", - "merge2": "^1.3.0", - "slash": "^3.0.0" - } - }, - "graceful-fs": { - "version": "4.2.6", - "integrity": "sha512-nTnJ528pbqxYanhpDYsi4Rd8MAeaBA67+RZ10CM1m3bTAVFEDcd5AuA4a6W5YkGZ1iNXHzZz8T6TBKLeBuNriQ==", - "dev": true - }, - "handle-thing": { - "version": "2.0.1", - "integrity": "sha512-9Qn4yBxelxoh2Ow62nP+Ka/kMnOXRi8BXnRaUwezLNhqelnN49xKz4F/dPP8OYLxLxq6JDtZb2i9XznUQbNPTg==", - "dev": true - }, - "has": { - "version": "1.0.3", - "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", - "dev": true, - "requires": { - "function-bind": "^1.1.1" - } - }, - "has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true - }, - "has-symbols": { - "version": "1.0.2", - "integrity": "sha512-chXa79rL/UC2KlX17jo3vRGz0azaWEx5tGqZg5pO3NUyEJVB17dMruQlzCCOfUvElghKcm5194+BCRvi2Rv/Gw==", - "dev": true - }, - "hpack.js": { - "version": "2.1.6", - "integrity": "sha1-h3dMCUnlE/QuhFdbPEVoH63ioLI=", - "dev": true, - "requires": { - "inherits": "^2.0.1", - "obuf": "^1.0.0", - "readable-stream": "^2.0.1", - "wbuf": "^1.1.0" - }, - "dependencies": { - "readable-stream": { - "version": "2.3.7", - "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==", - "dev": true, - "requires": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.3", - "isarray": "~1.0.0", - "process-nextick-args": "~2.0.0", - "safe-buffer": "~5.1.1", - "string_decoder": "~1.1.1", - "util-deprecate": "~1.0.1" - } - }, - "safe-buffer": { - "version": "5.1.2", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", - "dev": true - }, - "string_decoder": { - "version": "1.1.1", - "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", - "dev": true, - "requires": { - "safe-buffer": "~5.1.0" - } - } - } - }, - "html-entities": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/html-entities/-/html-entities-2.3.2.tgz", - "integrity": "sha512-c3Ab/url5ksaT0WyleslpBEthOzWhrjQbg75y7XUsfSzi3Dgzt0l8w5e7DylRn15MTlMMD58dTfzddNS2kcAjQ==", - "dev": true - }, - "http-deceiver": { - "version": "1.2.7", - "integrity": "sha1-+nFolEq5pRnTN8sL7HKE3D5yPYc=", - "dev": true - }, - "http-errors": { - "version": "1.7.2", - "integrity": "sha512-uUQBt3H/cSIVfch6i1EuPNy/YsRSOUBXTVfZ+yR7Zjez3qjBz6i9+i4zjNaoqcoFVI4lQJ5plg63TvGfRSDCRg==", - "dev": true, - "requires": { - "depd": "~1.1.2", - "inherits": "2.0.3", - "setprototypeof": "1.1.1", - "statuses": ">= 1.5.0 < 2", - "toidentifier": "1.0.0" - }, - "dependencies": { - "inherits": { - "version": "2.0.3", - "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=", - "dev": true - } - } - }, - "http-parser-js": { - "version": "0.5.3", - "integrity": "sha512-t7hjvef/5HEK7RWTdUzVUhl8zkEu+LlaE0IYzdMuvbSDipxBRpOn4Uhw8ZyECEa808iVT8XCjzo6xmYt4CiLZg==", - "dev": true - }, - "http-proxy": { - "version": "1.18.1", - "resolved": "https://registry.npmjs.org/http-proxy/-/http-proxy-1.18.1.tgz", - "integrity": "sha512-7mz/721AbnJwIVbnaSv1Cz3Am0ZLT/UBwkC92VlxhXv/k/BBQfM2fXElQNC27BVGr0uwUpplYPQM9LnaBMR5NQ==", - "dev": true, - "requires": { - "eventemitter3": "^4.0.0", - "follow-redirects": "^1.0.0", - "requires-port": "^1.0.0" - } - }, - "http-proxy-middleware": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/http-proxy-middleware/-/http-proxy-middleware-2.0.1.tgz", - "integrity": "sha512-cfaXRVoZxSed/BmkA7SwBVNI9Kj7HFltaE5rqYOub5kWzWZ+gofV2koVN1j2rMW7pEfSSlCHGJ31xmuyFyfLOg==", - "dev": true, - "requires": { - "@types/http-proxy": "^1.17.5", - "http-proxy": "^1.18.1", - "is-glob": "^4.0.1", - "is-plain-obj": "^3.0.0", - "micromatch": "^4.0.2" - } - }, - "human-signals": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz", - "integrity": "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==", - "dev": true - }, - "iconv-lite": { - "version": "0.4.24", - "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", - "dev": true, - "requires": { - "safer-buffer": ">= 2.1.2 < 3" - } - }, - "ieee754": { - "version": "1.2.1", - "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==" - }, - "ignore": { - "version": "5.1.9", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.1.9.tgz", - "integrity": "sha512-2zeMQpbKz5dhZ9IwL0gbxSW5w0NK/MSAMtNuhgIHEPmaU3vPdKPL0UdvUCXs5SS4JAwsBxysK5sFMW8ocFiVjQ==", - "dev": true - }, - "import-local": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/import-local/-/import-local-3.0.3.tgz", - "integrity": "sha512-bE9iaUY3CXH8Cwfan/abDKAxe1KGT9kyGsBPqf6DMK/z0a2OzAsrukeYNgIH6cH5Xr452jb1TUL8rSfCLjZ9uA==", - "dev": true, - "requires": { - "pkg-dir": "^4.2.0", - "resolve-cwd": "^3.0.0" - } - }, - "indent-string": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz", - "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==", - "dev": true - }, - "inflight": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", - "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", - "dev": true, - "requires": { - "once": "^1.3.0", - "wrappy": "1" - } - }, - "inherits": { - "version": "2.0.4", - "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" - }, - "internal-ip": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/internal-ip/-/internal-ip-6.2.0.tgz", - "integrity": "sha512-D8WGsR6yDt8uq7vDMu7mjcR+yRMm3dW8yufyChmszWRjcSHuxLBkR3GdS2HZAjodsaGuCvXeEJpueisXJULghg==", - "dev": true, - "requires": { - "default-gateway": "^6.0.0", - "ipaddr.js": "^1.9.1", - "is-ip": "^3.1.0", - "p-event": "^4.2.0" - } - }, - "interpret": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/interpret/-/interpret-2.2.0.tgz", - "integrity": "sha512-Ju0Bz/cEia55xDwUWEa8+olFpCiQoypjnQySseKtmjNrnps3P+xfpUmGr90T7yjlVJmOtybRvPXhKMbHr+fWnw==", - "dev": true - }, - "ip": { - "version": "1.1.5", - "integrity": "sha1-vd7XARQpCCjAoDnnLvJfWq7ENUo=", - "dev": true - }, - "ip-regex": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ip-regex/-/ip-regex-4.3.0.tgz", - "integrity": "sha512-B9ZWJxHHOHUhUjCPrMpLD4xEq35bUTClHM1S6CBU5ixQnkZmwipwgc96vAd7AAGM9TGHvJR+Uss+/Ak6UphK+Q==", - "dev": true - }, - "ipaddr.js": { - "version": "1.9.1", - "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", - "dev": true - }, - "is-arguments": { - "version": "1.1.0", - "integrity": "sha512-1Ij4lOMPl/xB5kBDn7I+b2ttPMKa8szhEIrXDuXQD/oe3HJLTLhqhgGspwgyGd6MOywBUqVvYicF72lkgDnIHg==", - "dev": true, - "requires": { - "call-bind": "^1.0.0" - } - }, - "is-binary-path": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", - "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", - "dev": true, - "requires": { - "binary-extensions": "^2.0.0" - } - }, - "is-core-module": { - "version": "2.8.0", - "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.8.0.tgz", - "integrity": "sha512-vd15qHsaqrRL7dtH6QNuy0ndJmRDrS9HAM1CAiSifNUFv4x1a0CCVsj18hJ1mShxIG6T2i1sO78MkP56r0nYRw==", - "dev": true, - "requires": { - "has": "^1.0.3" - } - }, - "is-date-object": { - "version": "1.0.2", - "integrity": "sha512-USlDT524woQ08aoZFzh3/Z6ch9Y/EWXEHQ/AaRN0SkKq4t2Jw2R2339tSXmwuVoY7LLlBCbOIlx2myP/L5zk0g==", - "dev": true - }, - "is-docker": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-2.2.1.tgz", - "integrity": "sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==", - "dev": true - }, - "is-extglob": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", - "integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=", - "dev": true - }, - "is-glob": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", - "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", - "dev": true, - "requires": { - "is-extglob": "^2.1.1" - } - }, - "is-ip": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/is-ip/-/is-ip-3.1.0.tgz", - "integrity": "sha512-35vd5necO7IitFPjd/YBeqwWnyDWbuLH9ZXQdMfDA8TEo7pv5X8yfrvVO3xbJbLUlERCMvf6X0hTUamQxCYJ9Q==", - "dev": true, - "requires": { - "ip-regex": "^4.0.0" - } - }, - "is-number": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", - "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", - "dev": true - }, - "is-path-cwd": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/is-path-cwd/-/is-path-cwd-2.2.0.tgz", - "integrity": "sha512-w942bTcih8fdJPJmQHFzkS76NEP8Kzzvmw92cXsazb8intwLqPibPPdXf4ANdKV3rYMuuQYGIWtvz9JilB3NFQ==", - "dev": true - }, - "is-path-inside": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz", - "integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==", - "dev": true - }, - "is-plain-obj": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-3.0.0.tgz", - "integrity": "sha512-gwsOE28k+23GP1B6vFl1oVh/WOzmawBrKwo5Ev6wMKzPkaXaCDIQKzLnvsA42DRlbVTWorkgTKIviAKCWkfUwA==", - "dev": true - }, - "is-plain-object": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz", - "integrity": "sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==", - "dev": true, - "requires": { - "isobject": "^3.0.1" - } - }, - "is-regex": { - "version": "1.1.2", - "integrity": "sha512-axvdhb5pdhEVThqJzYXwMlVuZwC+FF2DpcOhTS+y/8jVq4trxyPgfcwIxIKiyeuLlSQYKkmUaPQJ8ZE4yNKXDg==", - "dev": true, - "requires": { - "call-bind": "^1.0.2", - "has-symbols": "^1.0.1" - } - }, - "is-stream": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", - "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", - "dev": true - }, - "is-wsl": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-2.2.0.tgz", - "integrity": "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==", - "dev": true, - "requires": { - "is-docker": "^2.0.0" - } - }, - "isarray": { - "version": "1.0.0", - "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=", - "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": "3.0.1", - "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", - "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=", - "dev": true - }, - "javascript-kit-swift": { - "version": "file:..", - "requires": { - "@rollup/plugin-typescript": "^8.3.0", - "prettier": "2.5.1", - "rollup": "^2.63.0", - "tslib": "^2.3.1", - "typescript": "^4.5.5" - }, - "dependencies": { - "prettier": { - "version": "2.1.2", - "integrity": "sha512-16c7K+x4qVlJg9rEbXl7HEGmQyZlG4R9AgP+oHKRMsMsuk8s+ATStlf1NpDqyBI1HpVyfjLOeMhH2LvuNvV5Vg==", - "dev": true - }, - "typescript": { - "version": "4.2.4", - "integrity": "sha512-V+evlYHZnQkaz8TRBuxTA92yZBPotr5H+WhQ7bD3hZUndx5tGOa1fuCgeSjxAzM1RiN5IzvadIXTVefuuwZCRg==", - "dev": true - } - } - }, - "jest-worker": { - "version": "27.3.1", - "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-27.3.1.tgz", - "integrity": "sha512-ks3WCzsiZaOPJl/oMsDjaf0TRiSv7ctNgs0FqRr2nARsovz6AWWy4oLElwcquGSz692DzgZQrCLScPNs5YlC4g==", - "dev": true, - "requires": { - "@types/node": "*", - "merge-stream": "^2.0.0", - "supports-color": "^8.0.0" - } - }, - "json-parse-better-errors": { - "version": "1.0.2", - "integrity": "sha512-mrqyZKfX5EhL7hvqcV6WG1yYjnjeuYDzDhhcAAUrq8Po85NBQBJP+ZDUT75qZQ98IkUoBqdkExkukOU7Ts2wrw==", - "dev": true - }, - "json-schema-traverse": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", - "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", - "dev": true - }, - "kind-of": { - "version": "6.0.3", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", - "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==", - "dev": true - }, - "loader-runner": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/loader-runner/-/loader-runner-4.2.0.tgz", - "integrity": "sha512-92+huvxMvYlMzMt0iIOukcwYBFpkYJdpl2xsZ7LrlayO7E8SOv+JJUEK17B/dJIHAOLMfh2dZZ/Y18WgmGtYNw==", - "dev": true - }, - "locate-path": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", - "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", - "dev": true, - "requires": { - "p-locate": "^4.1.0" - } - }, - "lodash": { - "version": "4.17.21", - "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", - "dev": true - }, - "media-typer": { - "version": "0.3.0", - "integrity": "sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g=", - "dev": true - }, - "memfs": { - "version": "3.0.4", - "integrity": "sha512-OcZEzwX9E5AoY8SXjuAvw0DbIAYwUzV/I236I8Pqvrlv7sL/Y0E9aRCon05DhaV8pg1b32uxj76RgW0s5xjHBA==", - "requires": { - "fast-extend": "1.0.2", - "fs-monkey": "0.3.3" - } - }, - "merge-descriptors": { - "version": "1.0.1", - "integrity": "sha1-sAqqVW3YtEVoFQ7J0blT8/kMu2E=", - "dev": true - }, - "merge-stream": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", - "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", - "dev": true - }, - "merge2": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", - "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", - "dev": true - }, - "methods": { - "version": "1.1.2", - "integrity": "sha1-VSmk1nZUE07cxSZmVoNbD4Ua/O4=", - "dev": true - }, - "micromatch": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.4.tgz", - "integrity": "sha512-pRmzw/XUcwXGpD9aI9q/0XOwLNygjETJ8y0ao0wdqprrzDa4YnxLcz7fQRZr8voh8V10kGhABbNcHVk5wHgWwg==", - "dev": true, - "requires": { - "braces": "^3.0.1", - "picomatch": "^2.2.3" - } - }, - "mime": { - "version": "1.6.0", - "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", - "dev": true - }, - "mime-db": { - "version": "1.51.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.51.0.tgz", - "integrity": "sha512-5y8A56jg7XVQx2mbv1lu49NR4dokRnhZYTtL+KGfaa27uq4pSTXkwQkFJl4pkRMyNFz/EtYDSkiiEHx3F7UN6g==", - "dev": true - }, - "mime-types": { - "version": "2.1.34", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.34.tgz", - "integrity": "sha512-6cP692WwGIs9XXdOO4++N+7qjqv0rqxxVvJ3VHPh/Sc9mVZcQP+ZGhkKiTvWMQRr2tbHkJP/Yn7Y0npb3ZBs4A==", - "dev": true, - "requires": { - "mime-db": "1.51.0" - } - }, - "mimic-fn": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", - "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", - "dev": true - }, - "minimalistic-assert": { - "version": "1.0.1", - "integrity": "sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==", - "dev": true - }, - "minimatch": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", - "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", - "dev": true, - "requires": { - "brace-expansion": "^1.1.7" - } - }, - "minimist": { - "version": "1.2.6", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.6.tgz", - "integrity": "sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q==", - "dev": true - }, - "mkdirp": { - "version": "0.5.5", - "integrity": "sha512-NKmAlESf6jMGym1++R0Ra7wvhV+wFW63FaSOFPwRahvea0gMUcGUhVeAg/0BC0wiv9ih5NYPB1Wn1UEI1/L+xQ==", - "dev": true, - "requires": { - "minimist": "^1.2.5" - } - }, - "ms": { - "version": "2.0.0", - "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", - "dev": true - }, - "multicast-dns": { - "version": "6.2.3", - "integrity": "sha512-ji6J5enbMyGRHIAkAOu3WdV8nggqviKCEKtXcOqfphZZtQrmHKycfynJ2V7eVPUA4NhJ6V7Wf4TmGbTwKE9B6g==", - "dev": true, - "requires": { - "dns-packet": "^1.3.1", - "thunky": "^1.0.2" - } - }, - "multicast-dns-service-types": { - "version": "1.1.0", - "integrity": "sha1-iZ8R2WhuXgXLkbNdXw5jt3PPyQE=", - "dev": true - }, - "negotiator": { - "version": "0.6.2", - "integrity": "sha512-hZXc7K2e+PgeI1eDBe/10Ard4ekbfrrqG8Ep+8Jmf4JID2bNg7NvCPOZN+kfF574pFQI7mum2AUqDidoKqcTOw==", - "dev": true - }, - "neo-async": { - "version": "2.6.2", - "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz", - "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==", - "dev": true - }, - "node-forge": { - "version": "0.10.0", - "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-0.10.0.tgz", - "integrity": "sha512-PPmu8eEeG9saEUvI97fm4OYxXVB6bFvyNTyiUOBichBpFG8A1Ljw3bY62+5oOjDEMHRnd0Y7HQ+x7uzxOzC6JA==", - "dev": true - }, - "node-releases": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.1.tgz", - "integrity": "sha512-CqyzN6z7Q6aMeF/ktcMVTzhAHCEpf8SOarwpzpf8pNBY2k5/oM34UHldUwp8VKI7uxct2HxSRdJjBaZeESzcxA==", - "dev": true - }, - "normalize-path": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", - "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", - "dev": true - }, - "npm-run-path": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz", - "integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==", - "dev": true, - "requires": { - "path-key": "^3.0.0" - } - }, - "object-is": { - "version": "1.1.5", - "integrity": "sha512-3cyDsyHgtmi7I7DfSSI2LDp6SK2lwvtbg0p0R1e0RvTqF5ceGx+K2dfSjm1bKDMVCFEDAQvy+o8c6a7VujOddw==", - "dev": true, - "requires": { - "call-bind": "^1.0.2", - "define-properties": "^1.1.3" - } - }, - "object-keys": { - "version": "1.1.1", - "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", - "dev": true - }, - "obuf": { - "version": "1.1.2", - "integrity": "sha512-PX1wu0AmAdPqOL1mWhqmlOd8kOIZQwGZw6rh7uby9fTc5lhaOWFLX3I6R1hrF9k3zUY40e6igsLGkDXK92LJNg==", - "dev": true - }, - "on-finished": { - "version": "2.3.0", - "integrity": "sha1-IPEzZIGwg811M3mSoWlxqi2QaUc=", - "dev": true, - "requires": { - "ee-first": "1.1.1" - } - }, - "on-headers": { - "version": "1.0.2", - "integrity": "sha512-pZAE+FJLoyITytdqK0U5s+FIpjN0JP3OzFi/u8Rx+EV5/W+JTWGXG8xFzevE7AjBfDqHv/8vL8qQsIhHnqRkrA==", - "dev": true - }, - "once": { - "version": "1.4.0", - "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", - "requires": { - "wrappy": "1" - } - }, - "onetime": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", - "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", - "dev": true, - "requires": { - "mimic-fn": "^2.1.0" - } - }, - "open": { - "version": "8.4.0", - "resolved": "https://registry.npmjs.org/open/-/open-8.4.0.tgz", - "integrity": "sha512-XgFPPM+B28FtCCgSb9I+s9szOC1vZRSwgWsRUA5ylIxRTgKozqjOCrVOqGsYABPYK5qnfqClxZTFBa8PKt2v6Q==", - "dev": true, - "requires": { - "define-lazy-prop": "^2.0.0", - "is-docker": "^2.1.1", - "is-wsl": "^2.2.0" - } - }, - "p-event": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/p-event/-/p-event-4.2.0.tgz", - "integrity": "sha512-KXatOjCRXXkSePPb1Nbi0p0m+gQAwdlbhi4wQKJPI1HsMQS9g+Sqp2o+QHziPr7eYJyOZet836KoHEVM1mwOrQ==", - "dev": true, - "requires": { - "p-timeout": "^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 - }, - "p-limit": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", - "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", - "dev": true, - "requires": { - "p-try": "^2.0.0" - } - }, - "p-locate": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", - "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", - "dev": true, - "requires": { - "p-limit": "^2.2.0" - } - }, - "p-map": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/p-map/-/p-map-4.0.0.tgz", - "integrity": "sha512-/bjOqmgETBYB5BoEeGVea8dmvHb2m9GLy1E9W43yeyfP6QQCZGFNa+XRceJEuDB6zqr+gKpIAmlLebMpykw/MQ==", - "dev": true, - "requires": { - "aggregate-error": "^3.0.0" - } - }, - "p-retry": { - "version": "4.6.1", - "resolved": "https://registry.npmjs.org/p-retry/-/p-retry-4.6.1.tgz", - "integrity": "sha512-e2xXGNhZOZ0lfgR9kL34iGlU8N/KO0xZnQxVEwdeOvpqNDQfdnxIYizvWtK8RglUa3bGqI8g0R/BdfzLMxRkiA==", - "dev": true, - "requires": { - "@types/retry": "^0.12.0", - "retry": "^0.13.1" - } - }, - "p-timeout": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/p-timeout/-/p-timeout-3.2.0.tgz", - "integrity": "sha512-rhIwUycgwwKcP9yTOOFK/AKsAopjjCakVqLHePO3CC6Mir1Z99xT+R63jZxAT5lFZLa2inS5h+ZS2GvR99/FBg==", - "dev": true, - "requires": { - "p-finally": "^1.0.0" - } - }, - "p-try": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", - "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", - "dev": true - }, - "pako": { - "version": "1.0.11", - "integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==" - }, - "parseurl": { - "version": "1.3.3", - "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", - "dev": true - }, - "path-browserify": { - "version": "1.0.1", - "integrity": "sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==" - }, - "path-exists": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", - "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", - "dev": true - }, - "path-is-absolute": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", - "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=", - "dev": true - }, - "path-key": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", - "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", - "dev": true - }, - "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 - }, - "path-to-regexp": { - "version": "0.1.7", - "integrity": "sha1-32BBeABfUi8V60SQ5yR6G/qmf4w=", - "dev": true - }, - "path-type": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", - "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", - "dev": true - }, - "picocolors": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz", - "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==", - "dev": true - }, - "picomatch": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.0.tgz", - "integrity": "sha512-lY1Q/PiJGC2zOv/z391WOTD+Z02bCgsFfvxoXXf6h7kv9o+WmsmzYqrAwY63sNgOxE4xEdq0WyUnXfKeBrSvYw==", - "dev": true - }, - "pkg-dir": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz", - "integrity": "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==", - "dev": true, - "requires": { - "find-up": "^4.0.0" - } - }, - "portfinder": { - "version": "1.0.28", - "integrity": "sha512-Se+2isanIcEqf2XMHjyUKskczxbPH7dQnlMjXX6+dybayyHvAf/TCgyMRlzf/B6QDhAEFOGes0pzRo3by4AbMA==", - "dev": true, - "requires": { - "async": "^2.6.2", - "debug": "^3.1.1", - "mkdirp": "^0.5.5" - }, - "dependencies": { - "debug": { - "version": "3.2.7", - "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", - "dev": true, - "requires": { - "ms": "^2.1.1" - } - }, - "ms": { - "version": "2.1.3", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "dev": true - } - } - }, - "process-nextick-args": { - "version": "2.0.1", - "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", - "dev": true - }, - "proxy-addr": { - "version": "2.0.6", - "integrity": "sha512-dh/frvCBVmSsDYzw6n926jv974gddhkFPfiN8hPOi30Wax25QZyZEGveluCgliBnqmuM+UJmBErbAUFIoDbjOw==", - "dev": true, - "requires": { - "forwarded": "~0.1.2", - "ipaddr.js": "1.9.1" - } - }, - "punycode": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz", - "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==", - "dev": true - }, - "qs": { - "version": "6.7.0", - "integrity": "sha512-VCdBRNFTX1fyE7Nb6FYoURo/SPe62QCaAyzJvUjwRaIsc+NePBEniHlvxFmmX56+HZphIGtV0XeCirBtpDrTyQ==", - "dev": true - }, - "querystring": { - "version": "0.2.0", - "integrity": "sha1-sgmEkgO7Jd+CDadW50cAWHhSFiA=", - "dev": true - }, - "queue-microtask": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", - "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", - "dev": true - }, - "randombytes": { - "version": "2.1.0", - "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==", - "requires": { - "safe-buffer": "^5.1.0" - } - }, - "randomfill": { - "version": "1.0.4", - "integrity": "sha512-87lcbR8+MhcWcUiQ+9e+Rwx8MyR2P7qnt15ynUlbm3TU/fjbgz4GsvfSUDTemtCCtVCqb4ZcEFlyPNTh9bBTLw==", - "requires": { - "randombytes": "^2.0.5", - "safe-buffer": "^5.1.0" - } - }, - "range-parser": { - "version": "1.2.1", - "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", - "dev": true - }, - "raw-body": { - "version": "2.4.0", - "integrity": "sha512-4Oz8DUIwdvoa5qMJelxipzi/iJIi40O5cGV1wNYp5hvZP8ZN0T+jiNkL0QepXs+EsQ9XJ8ipEDoiH70ySUJP3Q==", - "dev": true, - "requires": { - "bytes": "3.1.0", - "http-errors": "1.7.2", - "iconv-lite": "0.4.24", - "unpipe": "1.0.0" - }, - "dependencies": { - "bytes": { - "version": "3.1.0", - "integrity": "sha512-zauLjrfCG+xvoyaqLoV8bLVXXNGC4JqlxFCutSDWA6fJrTo2ZuvLYTqZ7aHBLZSMOopbzwv8f+wZcVzfVTI2Dg==", - "dev": true - } - } - }, - "readable-stream": { - "version": "3.6.0", - "integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==", - "requires": { - "inherits": "^2.0.3", - "string_decoder": "^1.1.1", - "util-deprecate": "^1.0.1" - } - }, - "readdirp": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", - "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", - "dev": true, - "requires": { - "picomatch": "^2.2.1" - } - }, - "rechoir": { - "version": "0.7.1", - "resolved": "https://registry.npmjs.org/rechoir/-/rechoir-0.7.1.tgz", - "integrity": "sha512-/njmZ8s1wVeR6pjTZ+0nCnv8SpZNRMT2D1RLOJQESlYFDBvwpTA4KWJpZ+sBJ4+vhjILRcK7JIFdGCdxEAAitg==", - "dev": true, - "requires": { - "resolve": "^1.9.0" - } - }, - "regexp.prototype.flags": { - "version": "1.3.1", - "integrity": "sha512-JiBdRBq91WlY7uRJ0ds7R+dU02i6LKi8r3BuQhNXn+kmeLN+EfHhfjqMRis1zJxnlu88hq/4dx0P2OP3APRTOA==", - "dev": true, - "requires": { - "call-bind": "^1.0.2", - "define-properties": "^1.1.3" - } - }, - "require-from-string": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", - "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", - "dev": true - }, - "requires-port": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz", - "integrity": "sha1-kl0mAdOaxIXgkc8NpcbmlNw9yv8=", - "dev": true - }, - "resolve": { - "version": "1.20.0", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.20.0.tgz", - "integrity": "sha512-wENBPt4ySzg4ybFQW2TT1zMQucPK95HSh/nq2CFTZVOGut2+pQvSsgtda4d26YrYcr067wjbmzOG8byDPBX63A==", - "dev": true, - "requires": { - "is-core-module": "^2.2.0", - "path-parse": "^1.0.6" - } - }, - "resolve-cwd": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/resolve-cwd/-/resolve-cwd-3.0.0.tgz", - "integrity": "sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg==", - "dev": true, - "requires": { - "resolve-from": "^5.0.0" - } - }, - "resolve-from": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", - "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", - "dev": true - }, - "retry": { - "version": "0.13.1", - "resolved": "https://registry.npmjs.org/retry/-/retry-0.13.1.tgz", - "integrity": "sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg==", - "dev": true - }, - "reusify": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", - "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==", - "dev": true - }, - "rimraf": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", - "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", - "dev": true, - "requires": { - "glob": "^7.1.3" - } - }, - "run-parallel": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", - "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", - "dev": true, - "requires": { - "queue-microtask": "^1.2.2" - } - }, - "safe-buffer": { - "version": "5.2.1", - "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==" - }, - "safer-buffer": { - "version": "2.1.2", - "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", - "dev": true - }, - "schema-utils": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.1.1.tgz", - "integrity": "sha512-Y5PQxS4ITlC+EahLuXaY86TXfR7Dc5lw294alXOq86JAHCihAIZfqv8nNCWvaEJvaC51uN9hbLGeV0cFBdH+Fw==", - "dev": true, - "requires": { - "@types/json-schema": "^7.0.8", - "ajv": "^6.12.5", - "ajv-keywords": "^3.5.2" - } - }, - "select-hose": { - "version": "2.0.0", - "integrity": "sha1-Yl2GWPhlr0Psliv8N2o3NZpJlMo=", - "dev": true - }, - "selfsigned": { - "version": "1.10.11", - "resolved": "https://registry.npmjs.org/selfsigned/-/selfsigned-1.10.11.tgz", - "integrity": "sha512-aVmbPOfViZqOZPgRBT0+3u4yZFHpmnIghLMlAcb5/xhp5ZtB/RVnKhz5vl2M32CLXAqR4kha9zfhNg0Lf/sxKA==", - "dev": true, - "requires": { - "node-forge": "^0.10.0" - } - }, - "send": { - "version": "0.17.1", - "integrity": "sha512-BsVKsiGcQMFwT8UxypobUKyv7irCNRHk1T0G680vk88yf6LBByGcZJOTJCrTP2xVN6yI+XjPJcNuE3V4fT9sAg==", - "dev": true, - "requires": { - "debug": "2.6.9", - "depd": "~1.1.2", - "destroy": "~1.0.4", - "encodeurl": "~1.0.2", - "escape-html": "~1.0.3", - "etag": "~1.8.1", - "fresh": "0.5.2", - "http-errors": "~1.7.2", - "mime": "1.6.0", - "ms": "2.1.1", - "on-finished": "~2.3.0", - "range-parser": "~1.2.1", - "statuses": "~1.5.0" - }, - "dependencies": { - "ms": { - "version": "2.1.1", - "integrity": "sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg==", - "dev": true - } - } - }, - "serialize-javascript": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.0.tgz", - "integrity": "sha512-Qr3TosvguFt8ePWqsvRfrKyQXIiW+nGbYpy8XK24NQHE83caxWt+mIymTT19DGFbNWNLfEwsrkSmN64lVWB9ag==", - "dev": true, - "requires": { - "randombytes": "^2.1.0" - } - }, - "serve-index": { - "version": "1.9.1", - "integrity": "sha1-03aNabHn2C5c4FD/9bRTvqEqkjk=", - "dev": true, - "requires": { - "accepts": "~1.3.4", - "batch": "0.6.1", - "debug": "2.6.9", - "escape-html": "~1.0.3", - "http-errors": "~1.6.2", - "mime-types": "~2.1.17", - "parseurl": "~1.3.2" - }, - "dependencies": { - "http-errors": { - "version": "1.6.3", - "integrity": "sha1-i1VoC7S+KDoLW/TqLjhYC+HZMg0=", - "dev": true, - "requires": { - "depd": "~1.1.2", - "inherits": "2.0.3", - "setprototypeof": "1.1.0", - "statuses": ">= 1.4.0 < 2" - } - }, - "inherits": { - "version": "2.0.3", - "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=", - "dev": true - }, - "setprototypeof": { - "version": "1.1.0", - "integrity": "sha512-BvE/TwpZX4FXExxOxZyRGQQv651MSwmWKZGqvmPcRIjDqWub67kTKuIMx43cZZrS/cBBzwBcNDWoFxt2XEFIpQ==", - "dev": true - } - } - }, - "serve-static": { - "version": "1.14.1", - "integrity": "sha512-JMrvUwE54emCYWlTI+hGrGv5I8dEwmco/00EvkzIIsR7MqrHonbD9pO2MOfFnpFntl7ecpZs+3mW+XbQZu9QCg==", - "dev": true, - "requires": { - "encodeurl": "~1.0.2", - "escape-html": "~1.0.3", - "parseurl": "~1.3.3", - "send": "0.17.1" - } - }, - "setprototypeof": { - "version": "1.1.1", - "integrity": "sha512-JvdAWfbXeIGaZ9cILp38HntZSFSo3mWg6xGcJJsd+d4aRMOqauag1C63dJfDw7OaMYwEbHMOxEZ1lqVRYP2OAw==", - "dev": true - }, - "shallow-clone": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/shallow-clone/-/shallow-clone-3.0.1.tgz", - "integrity": "sha512-/6KqX+GVUdqPuPPd2LxDDxzX6CAbjJehAAOKlNpqqUpAqPM6HeL8f+o3a+JsyGjn2lv0WY8UsTgUJjU9Ok55NA==", - "dev": true, - "requires": { - "kind-of": "^6.0.2" - } - }, - "shebang-command": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", - "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", - "dev": true, - "requires": { - "shebang-regex": "^3.0.0" - } - }, - "shebang-regex": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", - "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", - "dev": true - }, - "signal-exit": { - "version": "3.0.6", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.6.tgz", - "integrity": "sha512-sDl4qMFpijcGw22U5w63KmD3cZJfBuFlVNbVMKje2keoKML7X2UzWbc4XrmEbDwg0NXJc3yv4/ox7b+JWb57kQ==", - "dev": true - }, - "slash": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", - "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", - "dev": true - }, - "sockjs": { - "version": "0.3.21", - "integrity": "sha512-DhbPFGpxjc6Z3I+uX07Id5ZO2XwYsWOrYjaSeieES78cq+JaJvVe5q/m1uvjIQhXinhIeCFRH6JgXe+mvVMyXw==", - "dev": true, - "requires": { - "faye-websocket": "^0.11.3", - "uuid": "^3.4.0", - "websocket-driver": "^0.7.4" - } - }, - "source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "dev": true - }, - "source-map-support": { - "version": "0.5.21", - "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", - "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", - "dev": true, - "requires": { - "buffer-from": "^1.0.0", - "source-map": "^0.6.0" - } - }, - "spdy": { - "version": "4.0.2", - "integrity": "sha512-r46gZQZQV+Kl9oItvl1JZZqJKGr+oEkB08A6BzkiR7593/7IbtuncXHd2YoYeTsG4157ZssMu9KYvUHLcjcDoA==", - "dev": true, - "requires": { - "debug": "^4.1.0", - "handle-thing": "^2.0.0", - "http-deceiver": "^1.2.7", - "select-hose": "^2.0.0", - "spdy-transport": "^3.0.0" - }, - "dependencies": { - "debug": { - "version": "4.3.1", - "integrity": "sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ==", - "dev": true, - "requires": { - "ms": "2.1.2" - } - }, - "ms": { - "version": "2.1.2", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", - "dev": true - } - } - }, - "spdy-transport": { - "version": "3.0.0", - "integrity": "sha512-hsLVFE5SjA6TCisWeJXFKniGGOpBgMLmerfO2aCyCU5s7nJ/rpAepqmFifv/GCbSbueEeAJJnmSQ2rKC/g8Fcw==", - "dev": true, - "requires": { - "debug": "^4.1.0", - "detect-node": "^2.0.4", - "hpack.js": "^2.1.6", - "obuf": "^1.1.2", - "readable-stream": "^3.0.6", - "wbuf": "^1.7.3" - }, - "dependencies": { - "debug": { - "version": "4.3.1", - "integrity": "sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ==", - "dev": true, - "requires": { - "ms": "2.1.2" - } - }, - "ms": { - "version": "2.1.2", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", - "dev": true - } - } - }, - "statuses": { - "version": "1.5.0", - "integrity": "sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow=", - "dev": true - }, - "string_decoder": { - "version": "1.3.0", - "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", - "requires": { - "safe-buffer": "~5.2.0" - } - }, - "strip-ansi": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.0.1.tgz", - "integrity": "sha512-cXNxvT8dFNRVfhVME3JAe98mkXDYN2O1l7jmcwMnOslDeESg1rF/OZMtK0nRAhiari1unG5cD4jG3rapUAkLbw==", - "dev": true, - "requires": { - "ansi-regex": "^6.0.1" - } - }, - "strip-final-newline": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz", - "integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==", - "dev": true - }, - "supports-color": { - "version": "8.1.1", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", - "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", - "dev": true, - "requires": { - "has-flag": "^4.0.0" - } - }, - "tapable": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.2.1.tgz", - "integrity": "sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ==", - "dev": true - }, - "tar-stream": { - "version": "2.2.0", - "integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==", - "requires": { - "bl": "^4.0.3", - "end-of-stream": "^1.4.1", - "fs-constants": "^1.0.0", - "inherits": "^2.0.3", - "readable-stream": "^3.1.1" - } - }, - "terser": { - "version": "5.10.0", - "resolved": "https://registry.npmjs.org/terser/-/terser-5.10.0.tgz", - "integrity": "sha512-AMmF99DMfEDiRJfxfY5jj5wNH/bYO09cniSqhfoyxc8sFoYIgkJy86G04UoZU5VjlpnplVu0K6Tx6E9b5+DlHA==", - "dev": true, - "requires": { - "commander": "^2.20.0", - "source-map": "~0.7.2", - "source-map-support": "~0.5.20" - }, - "dependencies": { - "source-map": { - "version": "0.7.3", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.3.tgz", - "integrity": "sha512-CkCj6giN3S+n9qrYiBTX5gystlENnRW5jZeNLHpe6aue+SrHcG5VYwujhW9s4dY31mEGsxBDrHR6oI69fTXsaQ==", - "dev": true - } - } - }, - "terser-webpack-plugin": { - "version": "5.2.5", - "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.2.5.tgz", - "integrity": "sha512-3luOVHku5l0QBeYS8r4CdHYWEGMmIj3H1U64jgkdZzECcSOJAyJ9TjuqcQZvw1Y+4AOBN9SeYJPJmFn2cM4/2g==", - "dev": true, - "requires": { - "jest-worker": "^27.0.6", - "schema-utils": "^3.1.1", - "serialize-javascript": "^6.0.0", - "source-map": "^0.6.1", - "terser": "^5.7.2" - } - }, - "thunky": { - "version": "1.1.0", - "integrity": "sha512-eHY7nBftgThBqOyHGVN+l8gF0BucP09fMo0oO/Lb0w1OF80dJv+lDVpXG60WMQvkcxAkNybKsrEIE3ZtKGmPrA==", - "dev": true - }, - "to-regex-range": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", - "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", - "dev": true, - "requires": { - "is-number": "^7.0.0" - } - }, - "toidentifier": { - "version": "1.0.0", - "integrity": "sha512-yaOH/Pk/VEhBWWTlhI+qXxDFXlejDGcQipMlyxda9nthulaxLZUNcUqFxokp0vcYnvteJln5FNQDRrxj3YcbVw==", - "dev": true - }, - "type-is": { - "version": "1.6.18", - "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", - "dev": true, - "requires": { - "media-typer": "0.3.0", - "mime-types": "~2.1.24" - } - }, - "unpipe": { - "version": "1.0.0", - "integrity": "sha1-sr9O6FFKrmFltIF4KdIbLvSZBOw=", - "dev": true - }, - "uri-js": { - "version": "4.4.1", - "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", - "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", - "dev": true, - "requires": { - "punycode": "^2.1.0" - } - }, - "url": { - "version": "0.11.0", - "integrity": "sha1-ODjpfPxgUh63PFJajlW/3Z4uKPE=", - "dev": true, - "requires": { - "punycode": "1.3.2", - "querystring": "0.2.0" - }, - "dependencies": { - "punycode": { - "version": "1.3.2", - "integrity": "sha1-llOgNvt8HuQjQvIyXM7v6jkmxI0=", - "dev": true - } - } - }, - "util-deprecate": { - "version": "1.0.2", - "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=" - }, - "utils-merge": { - "version": "1.0.1", - "integrity": "sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM=", - "dev": true - }, - "uuid": { - "version": "3.4.0", - "integrity": "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==", - "dev": true - }, - "vary": { - "version": "1.1.2", - "integrity": "sha1-IpnwLG3tMNSllhsLn3RSShj2NPw=", - "dev": true - }, - "watchpack": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.2.0.tgz", - "integrity": "sha512-up4YAn/XHgZHIxFBVCdlMiWDj6WaLKpwVeGQk2I5thdYxF/KmF0aaz6TfJZ/hfl1h/XlcDr7k1KH7ThDagpFaA==", - "dev": true, - "requires": { - "glob-to-regexp": "^0.4.1", - "graceful-fs": "^4.1.2" - } - }, - "wbuf": { - "version": "1.7.3", - "integrity": "sha512-O84QOnr0icsbFGLS0O3bI5FswxzRr8/gHwWkDlQFskhSPryQXvrTMxjxGP4+iWYoauLoBvfDpkrOauZ+0iZpDA==", - "dev": true, - "requires": { - "minimalistic-assert": "^1.0.0" - } - }, - "webpack": { - "version": "5.64.2", - "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.64.2.tgz", - "integrity": "sha512-4KGc0+Ozi0aS3EaLNRvEppfZUer+CaORKqL6OBjDLZOPf9YfN8leagFzwe6/PoBdHFxc/utKArl8LMC0Ivtmdg==", - "dev": true, - "requires": { - "@types/eslint-scope": "^3.7.0", - "@types/estree": "^0.0.50", - "@webassemblyjs/ast": "1.11.1", - "@webassemblyjs/wasm-edit": "1.11.1", - "@webassemblyjs/wasm-parser": "1.11.1", - "acorn": "^8.4.1", - "acorn-import-assertions": "^1.7.6", - "browserslist": "^4.14.5", - "chrome-trace-event": "^1.0.2", - "enhanced-resolve": "^5.8.3", - "es-module-lexer": "^0.9.0", - "eslint-scope": "5.1.1", - "events": "^3.2.0", - "glob-to-regexp": "^0.4.1", - "graceful-fs": "^4.2.4", - "json-parse-better-errors": "^1.0.2", - "loader-runner": "^4.2.0", - "mime-types": "^2.1.27", - "neo-async": "^2.6.2", - "schema-utils": "^3.1.0", - "tapable": "^2.1.1", - "terser-webpack-plugin": "^5.1.3", - "watchpack": "^2.2.0", - "webpack-sources": "^3.2.2" - } - }, - "webpack-cli": { - "version": "4.9.1", - "resolved": "https://registry.npmjs.org/webpack-cli/-/webpack-cli-4.9.1.tgz", - "integrity": "sha512-JYRFVuyFpzDxMDB+v/nanUdQYcZtqFPGzmlW4s+UkPMFhSpfRNmf1z4AwYcHJVdvEFAM7FFCQdNTpsBYhDLusQ==", - "dev": true, - "requires": { - "@discoveryjs/json-ext": "^0.5.0", - "@webpack-cli/configtest": "^1.1.0", - "@webpack-cli/info": "^1.4.0", - "@webpack-cli/serve": "^1.6.0", - "colorette": "^2.0.14", - "commander": "^7.0.0", - "execa": "^5.0.0", - "fastest-levenshtein": "^1.0.12", - "import-local": "^3.0.2", - "interpret": "^2.2.0", - "rechoir": "^0.7.0", - "webpack-merge": "^5.7.3" - }, - "dependencies": { - "commander": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/commander/-/commander-7.2.0.tgz", - "integrity": "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==", - "dev": true - } - } - }, - "webpack-dev-middleware": { - "version": "5.2.2", - "resolved": "https://registry.npmjs.org/webpack-dev-middleware/-/webpack-dev-middleware-5.2.2.tgz", - "integrity": "sha512-DjZyYrsHhkikAFNvSNKrpnziXukU1EChFAh9j4LAm6ndPLPW8cN0KhM7T+RAiOqsQ6ABfQ8hoKIs9IWMTjov+w==", - "dev": true, - "requires": { - "colorette": "^2.0.10", - "memfs": "^3.2.2", - "mime-types": "^2.1.31", - "range-parser": "^1.2.1", - "schema-utils": "^4.0.0" - }, - "dependencies": { - "ajv": { - "version": "8.8.2", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.8.2.tgz", - "integrity": "sha512-x9VuX+R/jcFj1DHo/fCp99esgGDWiHENrKxaCENuCxpoMCmAt/COCGVDwA7kleEpEzJjDnvh3yGoOuLu0Dtllw==", - "dev": true, - "requires": { - "fast-deep-equal": "^3.1.1", - "json-schema-traverse": "^1.0.0", - "require-from-string": "^2.0.2", - "uri-js": "^4.2.2" - } - }, - "ajv-keywords": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-5.1.0.tgz", - "integrity": "sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw==", - "dev": true, - "requires": { - "fast-deep-equal": "^3.1.3" - } - }, - "fs-monkey": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/fs-monkey/-/fs-monkey-1.0.3.tgz", - "integrity": "sha512-cybjIfiiE+pTWicSCLFHSrXZ6EilF30oh91FDP9S2B051prEa7QWfrVTQm10/dDpswBDXZugPa1Ogu8Yh+HV0Q==", - "dev": true - }, - "json-schema-traverse": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", - "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", - "dev": true - }, - "memfs": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/memfs/-/memfs-3.3.0.tgz", - "integrity": "sha512-BEE62uMfKOavX3iG7GYX43QJ+hAeeWnwIAuJ/R6q96jaMtiLzhsxHJC8B1L7fK7Pt/vXDRwb3SG/yBpNGDPqzg==", - "dev": true, - "requires": { - "fs-monkey": "1.0.3" - } - }, - "schema-utils": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.0.0.tgz", - "integrity": "sha512-1edyXKgh6XnJsJSQ8mKWXnN/BVaIbFMLpouRUrXgVq7WYne5kw3MW7UPhO44uRXQSIpTSXoJbmrR2X0w9kUTyg==", - "dev": true, - "requires": { - "@types/json-schema": "^7.0.9", - "ajv": "^8.8.0", - "ajv-formats": "^2.1.1", - "ajv-keywords": "^5.0.0" - } - } - } - }, - "webpack-dev-server": { - "version": "4.5.0", - "resolved": "https://registry.npmjs.org/webpack-dev-server/-/webpack-dev-server-4.5.0.tgz", - "integrity": "sha512-Ss4WptsUjYa+3hPI4iYZYEc8FrtnfkaPrm5WTjk9ux5kiCS718836srs0ppKMHRaCHP5mQ6g4JZGcfDdGbCjpQ==", - "dev": true, - "requires": { - "ansi-html-community": "^0.0.8", - "bonjour": "^3.5.0", - "chokidar": "^3.5.2", - "colorette": "^2.0.10", - "compression": "^1.7.4", - "connect-history-api-fallback": "^1.6.0", - "del": "^6.0.0", - "express": "^4.17.1", - "graceful-fs": "^4.2.6", - "html-entities": "^2.3.2", - "http-proxy-middleware": "^2.0.0", - "internal-ip": "^6.2.0", - "ipaddr.js": "^2.0.1", - "open": "^8.0.9", - "p-retry": "^4.5.0", - "portfinder": "^1.0.28", - "schema-utils": "^3.1.0", - "selfsigned": "^1.10.11", - "serve-index": "^1.9.1", - "sockjs": "^0.3.21", - "spdy": "^4.0.2", - "strip-ansi": "^7.0.0", - "url": "^0.11.0", - "webpack-dev-middleware": "^5.2.1", - "ws": "^8.1.0" - }, - "dependencies": { - "ipaddr.js": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-2.0.1.tgz", - "integrity": "sha512-1qTgH9NG+IIJ4yfKs2e6Pp1bZg8wbDbKHT21HrLIeYBTRLgMYKnMTPAuI3Lcs61nfx5h1xlXnbJtH1kX5/d/ng==", - "dev": true - } - } - }, - "webpack-merge": { - "version": "5.8.0", - "resolved": "https://registry.npmjs.org/webpack-merge/-/webpack-merge-5.8.0.tgz", - "integrity": "sha512-/SaI7xY0831XwP6kzuwhKWVKDP9t1QY1h65lAFLbZqMPIuYcD9QAW4u9STIbU9kaJbPBB/geU/gLr1wDjOhQ+Q==", - "dev": true, - "requires": { - "clone-deep": "^4.0.1", - "wildcard": "^2.0.0" - } - }, - "webpack-sources": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-3.2.2.tgz", - "integrity": "sha512-cp5qdmHnu5T8wRg2G3vZZHoJPN14aqQ89SyQ11NpGH5zEMDCclt49rzo+MaRazk7/UeILhAI+/sEtcM+7Fr0nw==", - "dev": true - }, - "websocket-driver": { - "version": "0.7.4", - "integrity": "sha512-b17KeDIQVjvb0ssuSDF2cYXSg2iztliJ4B9WdsuB6J952qCPKmnVq4DyW5motImXHDC1cBT/1UezrJVsKw5zjg==", - "dev": true, - "requires": { - "http-parser-js": ">=0.5.1", - "safe-buffer": ">=5.1.0", - "websocket-extensions": ">=0.1.1" - } - }, - "websocket-extensions": { - "version": "0.1.4", - "integrity": "sha512-OqedPIGOfsDlo31UNwYbCFMSaO9m9G/0faIHj5/dZFDMFqPTcx6UwqyOy3COEaEOg/9VsGIpdqn62W5KhoKSpg==", - "dev": true - }, - "which": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", - "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", - "dev": true, - "requires": { - "isexe": "^2.0.0" - } - }, - "wildcard": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/wildcard/-/wildcard-2.0.0.tgz", - "integrity": "sha512-JcKqAHLPxcdb9KM49dufGXn2x3ssnfjbcaQdLlfZsL9rH9wgDQjUtDxbo8NE0F6SFvydeu1VhZe7hZuHsB2/pw==", - "dev": true - }, - "wrappy": { - "version": "1.0.2", - "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=" - }, - "ws": { - "version": "8.2.3", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.2.3.tgz", - "integrity": "sha512-wBuoj1BDpC6ZQ1B7DWQBYVLphPWkm8i9Y0/3YdHjHKHiohOJ1ws+3OccDWtH+PoC9DZD5WOTrJvNbWvjS6JWaA==", - "dev": true, - "requires": {} - } - } -} diff --git a/Example/package.json b/Example/package.json deleted file mode 100644 index f7f9a49c5..000000000 --- a/Example/package.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "name": "javascript-kit-example", - "version": "1.0.0", - "description": "An example use of JavaScriptKit", - "private": true, - "dependencies": { - "@wasmer/wasi": "^0.12.0", - "@wasmer/wasmfs": "^0.12.0", - "javascript-kit-swift": "file:.." - }, - "devDependencies": { - "webpack": "^5.64.2", - "webpack-cli": "^4.9.1", - "webpack-dev-server": "^4.5.0" - }, - "scripts": { - "build": "webpack", - "start": "webpack-dev-server" - }, - "author": "swiftwasm", - "license": "MIT" -} diff --git a/Example/public/index.html b/Example/public/index.html deleted file mode 100644 index b5c67fcf9..000000000 --- a/Example/public/index.html +++ /dev/null @@ -1,9 +0,0 @@ - - - - Getting Started - - - - - diff --git a/Example/src/index.js b/Example/src/index.js deleted file mode 100644 index f26a76c64..000000000 --- a/Example/src/index.js +++ /dev/null @@ -1,54 +0,0 @@ -import { SwiftRuntime } from "javascript-kit-swift"; -import { WASI } from "@wasmer/wasi"; -import { WasmFs } from "@wasmer/wasmfs"; - -const swift = new SwiftRuntime(); -// Instantiate a new WASI Instance -const wasmFs = new WasmFs(); - -// Output stdout and stderr to console -const originalWriteSync = wasmFs.fs.writeSync; -wasmFs.fs.writeSync = (fd, buffer, offset, length, position) => { - const text = new TextDecoder("utf-8").decode(buffer); - - // Filter out standalone "\n" added by every `print`, `console.log` - // always adds its own "\n" on top. - if (text !== "\n") { - switch (fd) { - case 1: - console.log(text); - break; - case 2: - console.error(text); - break; - } - } - return originalWriteSync(fd, buffer, offset, length, position); -}; - -let wasi = new WASI({ - args: [], - env: {}, - bindings: { - ...WASI.defaultBindings, - fs: wasmFs.fs, - }, -}); - -const startWasiTask = async () => { - // Fetch our Wasm File - const response = await fetch("JavaScriptKitExample.wasm"); - const responseArrayBuffer = await response.arrayBuffer(); - - // Instantiate the WebAssembly file - const wasm_bytes = new Uint8Array(responseArrayBuffer).buffer; - let { instance } = await WebAssembly.instantiate(wasm_bytes, { - wasi_snapshot_preview1: wasi.wasiImport, - javascript_kit: swift.importObjects(), - }); - - swift.setInstance(instance); - // Start the WebAssembly WASI instance! - wasi.start(instance); -}; -startWasiTask(); diff --git a/Example/webpack.config.js b/Example/webpack.config.js deleted file mode 100644 index 5f64741f7..000000000 --- a/Example/webpack.config.js +++ /dev/null @@ -1,23 +0,0 @@ -const path = require("path"); -const outputPath = path.resolve(__dirname, "dist"); - -module.exports = { - entry: "./src/index.js", - mode: "development", - output: { - filename: "main.js", - path: outputPath, - }, - devServer: { - static: [ - { - directory: path.join(__dirname, "public"), - watch: true, - }, - { - directory: path.join(__dirname, "dist"), - watch: true, - }, - ], - }, -}; diff --git a/Examples/ActorOnWebWorker/Package.swift b/Examples/ActorOnWebWorker/Package.swift new file mode 100644 index 000000000..82e87dfdc --- /dev/null +++ b/Examples/ActorOnWebWorker/Package.swift @@ -0,0 +1,20 @@ +// swift-tools-version: 6.0 + +import PackageDescription + +let package = Package( + name: "Example", + platforms: [.macOS("15"), .iOS("18"), .watchOS("11"), .tvOS("18"), .visionOS("2")], + dependencies: [ + .package(path: "../../") + ], + targets: [ + .executableTarget( + name: "MyApp", + dependencies: [ + .product(name: "JavaScriptKit", package: "JavaScriptKit"), + .product(name: "JavaScriptEventLoop", package: "JavaScriptKit"), + ] + ) + ] +) diff --git a/Examples/ActorOnWebWorker/README.md b/Examples/ActorOnWebWorker/README.md new file mode 100644 index 000000000..c0c849962 --- /dev/null +++ b/Examples/ActorOnWebWorker/README.md @@ -0,0 +1,21 @@ +# WebWorker + Actor example + +Install Development Snapshot toolchain `DEVELOPMENT-SNAPSHOT-2024-07-08-a` or later from [swift.org/install](https://www.swift.org/install/) and run the following commands: + +```sh +$ ( + set -eo pipefail; \ + V="$(swiftc --version | head -n1)"; \ + TAG="$(curl -sL "https://raw.githubusercontent.com/swiftwasm/swift-sdk-index/refs/heads/main/v1/tag-by-version.json" | jq -e -r --arg v "$V" '.[$v] | .[-1]')"; \ + curl -sL "https://raw.githubusercontent.com/swiftwasm/swift-sdk-index/refs/heads/main/v1/builds/$TAG.json" | \ + jq -r '.["swift-sdks"]["wasm32-unknown-wasip1-threads"] | "swift sdk install \"\(.url)\" --checksum \"\(.checksum)\""' | sh -x +) +$ export SWIFT_SDK_ID=$( + V="$(swiftc --version | head -n1)"; \ + TAG="$(curl -sL "https://raw.githubusercontent.com/swiftwasm/swift-sdk-index/refs/heads/main/v1/tag-by-version.json" | jq -e -r --arg v "$V" '.[$v] | .[-1]')"; \ + curl -sL "https://raw.githubusercontent.com/swiftwasm/swift-sdk-index/refs/heads/main/v1/builds/$TAG.json" | \ + jq -r '.["swift-sdks"]["wasm32-unknown-wasip1-threads"]["id"]' +) +$ ./build.sh +$ npx serve +``` diff --git a/Examples/ActorOnWebWorker/Sources/MyApp.swift b/Examples/ActorOnWebWorker/Sources/MyApp.swift new file mode 100644 index 000000000..357956a7e --- /dev/null +++ b/Examples/ActorOnWebWorker/Sources/MyApp.swift @@ -0,0 +1,294 @@ +import JavaScriptEventLoop +import JavaScriptKit + +// Simple full-text search service +actor SearchService { + struct Error: Swift.Error, CustomStringConvertible { + let message: String + + var description: String { + return self.message + } + } + + let serialExecutor: OwnedExecutor + + // Simple in-memory index: word -> positions + var index: [String: [Int]] = [:] + var originalContent: String = "" + lazy var console: JSValue = { + JSObject.global.console + }() + + nonisolated var unownedExecutor: UnownedSerialExecutor { + return self.serialExecutor.unownedExecutor + } + + init(serialExecutor: OwnedExecutor) { + self.serialExecutor = serialExecutor + } + + // Utility function for fetch + func fetch(_ url: String) -> JSPromise { + let jsFetch = JSObject.global.fetch.function! + return JSPromise(jsFetch(url).object!)! + } + + func fetchAndIndex(url: String) async throws { + let response = try await fetch(url).value() + if response.status != 200 { + throw Error(message: "Failed to fetch content") + } + let text = try await JSPromise(response.text().object!)!.value() + let content = text.string! + index(content) + } + + func index(_ contents: String) { + self.originalContent = contents + self.index = [:] + + // Simple tokenization and indexing + var position = 0 + let words = contents.lowercased().split(whereSeparator: { !$0.isLetter && !$0.isNumber }) + + for word in words { + let wordStr = String(word) + if wordStr.count > 1 { // Skip single-character words + if index[wordStr] == nil { + index[wordStr] = [] + } + index[wordStr]?.append(position) + } + position += 1 + } + + _ = console.log("Indexing complete with", index.count, "unique words") + } + + func search(_ query: String) -> [SearchResult] { + let queryWords = query.lowercased().split(whereSeparator: { !$0.isLetter && !$0.isNumber }) + + if queryWords.isEmpty { + return [] + } + + var results: [SearchResult] = [] + + // Start with the positions of the first query word + guard let firstWord = queryWords.first, + let firstWordPositions = index[String(firstWord)] + else { + return [] + } + + for position in firstWordPositions { + // Extract context around this position + let words = originalContent.lowercased().split(whereSeparator: { + !$0.isLetter && !$0.isNumber + }) + var contextWords: [String] = [] + + // Get words for context (5 words before, 10 words after) + let contextStart = max(0, position - 5) + let contextEnd = min(position + 10, words.count - 1) + + if contextStart <= contextEnd && contextStart < words.count { + for i in contextStart...contextEnd { + if i < words.count { + contextWords.append(String(words[i])) + } + } + } + + let context = contextWords.joined(separator: " ") + results.append(SearchResult(position: position, context: context)) + } + + return results + } +} + +struct SearchResult { + let position: Int + let context: String +} + +@MainActor +final class App { + private let document = JSObject.global.document + private let alert = JSObject.global.alert.function! + + // UI elements + 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 + + init(service: SearchService) { + self.service = service + container = document.getElementById("container") + urlInput = document.getElementById("urlInput") + indexButton = document.getElementById("indexButton") + searchInput = document.getElementById("searchInput") + searchButton = document.getElementById("searchButton") + statusElement = document.getElementById("status") + resultsElement = document.getElementById("results") + setupEventHandlers() + } + + 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 + } + ) + } + + private func performIndex() { + let url = urlInput.value.string! + + if url.isEmpty { + alert("Please enter a URL") + return + } + + updateStatus("Downloading and indexing content...") + + Task { [weak self] in + guard let self else { return } + do { + try await self.service.fetchAndIndex(url: url) + await MainActor.run { + self.updateStatus("Indexing complete!") + } + } catch { + await MainActor.run { + self.updateStatus("Error: \(error)") + } + } + } + } + + private func performSearch() { + let query = searchInput.value.string! + + if query.isEmpty { + alert("Please enter a search query") + return + } + + updateStatus("Searching...") + + Task { [weak self] in + guard let self else { return } + let searchResults = await self.service.search(query) + await MainActor.run { + self.displaySearchResults(searchResults) + } + } + } + + private func updateStatus(_ message: String) { + statusElement.innerText = .string(message) + } + + private func displaySearchResults(_ results: [SearchResult]) { + statusElement.innerText = .string("Search complete! Found \(results.count) results.") + resultsElement.innerHTML = .string("") + + if results.isEmpty { + 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() { + 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)" + ) + _ = 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 { + 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 +} +#endif diff --git a/Examples/ActorOnWebWorker/build.sh b/Examples/ActorOnWebWorker/build.sh new file mode 100755 index 000000000..c82a10c32 --- /dev/null +++ b/Examples/ActorOnWebWorker/build.sh @@ -0,0 +1,3 @@ +swift package --swift-sdk "${SWIFT_SDK_ID:-wasm32-unknown-wasip1-threads}" -c release \ + plugin --allow-writing-to-package-directory \ + js --use-cdn --output ./Bundle diff --git a/Examples/ActorOnWebWorker/index.html b/Examples/ActorOnWebWorker/index.html new file mode 100644 index 000000000..4a16f16a0 --- /dev/null +++ b/Examples/ActorOnWebWorker/index.html @@ -0,0 +1,31 @@ + + + + + WebWorker + Actor example + + + + +

Full-text Search with Actor on Web Worker

+ +
+ + +
+
+ + +

Ready

+
+
+ + + diff --git a/Examples/ActorOnWebWorker/serve.json b/Examples/ActorOnWebWorker/serve.json new file mode 100644 index 000000000..537a16904 --- /dev/null +++ b/Examples/ActorOnWebWorker/serve.json @@ -0,0 +1,14 @@ +{ + "headers": [{ + "source": "**/*", + "headers": [ + { + "key": "Cross-Origin-Embedder-Policy", + "value": "require-corp" + }, { + "key": "Cross-Origin-Opener-Policy", + "value": "same-origin" + } + ] + }] +} diff --git a/Example/JavaScriptKitExample/Package.swift b/Examples/Basic/Package.swift similarity index 52% rename from Example/JavaScriptKitExample/Package.swift rename to Examples/Basic/Package.swift index ecee23bad..6c729741c 100644 --- a/Example/JavaScriptKitExample/Package.swift +++ b/Examples/Basic/Package.swift @@ -1,22 +1,21 @@ -// swift-tools-version:5.2 +// swift-tools-version:6.0 import PackageDescription let package = Package( - name: "JavaScriptKitExample", - products: [ - .executable( - name: "JavaScriptKitExample", targets: ["JavaScriptKitExample"] - ), + name: "Basic", + platforms: [ + .macOS(.v14) ], dependencies: [.package(name: "JavaScriptKit", path: "../../")], targets: [ - .target( - name: "JavaScriptKitExample", + .executableTarget( + name: "Basic", dependencies: [ "JavaScriptKit", - .product(name: "JavaScriptEventLoop", package: "JavaScriptKit") + .product(name: "JavaScriptEventLoop", package: "JavaScriptKit"), ] ) - ] + ], + swiftLanguageModes: [.v5] ) diff --git a/Examples/Basic/README.md b/Examples/Basic/README.md new file mode 100644 index 000000000..a09d6a924 --- /dev/null +++ b/Examples/Basic/README.md @@ -0,0 +1,9 @@ +# Basic example + +Install Development Snapshot toolchain `DEVELOPMENT-SNAPSHOT-2024-07-08-a` from [swift.org/install](https://www.swift.org/install/) and run the following commands: + +```sh +$ swift sdk install https://github.com/swiftwasm/swift/releases/download/swift-wasm-DEVELOPMENT-SNAPSHOT-2024-07-09-a/swift-wasm-DEVELOPMENT-SNAPSHOT-2024-07-09-a-wasm32-unknown-wasi.artifactbundle.zip +$ ./build.sh +$ npx serve +``` diff --git a/Examples/Basic/Sources/main.swift b/Examples/Basic/Sources/main.swift new file mode 100644 index 000000000..7ea9231e1 --- /dev/null +++ b/Examples/Basic/Sources/main.swift @@ -0,0 +1,52 @@ +import JavaScriptEventLoop +import JavaScriptKit + +let alert = JSObject.global.alert.function! +let document = JSObject.global.document + +let divElement = document.createElement("div") +divElement.innerText = "Hello, world" +_ = document.body.appendChild(divElement) + +let buttonElement = document.createElement("button") +buttonElement.innerText = "Alert demo" +buttonElement.onclick = .object( + JSClosure { _ in + alert("Swift is running on browser!") + return .undefined + } +) + +_ = document.body.appendChild(buttonElement) + +private let jsFetch = JSObject.global.fetch.function! +func fetch(_ url: String) -> JSPromise { + JSPromise(jsFetch(url).object!)! +} + +JavaScriptEventLoop.installGlobalExecutor() + +struct Response: Decodable { + let uuid: String +} + +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) + } + } + + return .undefined + } +) + +_ = document.body.appendChild(asyncButtonElement) diff --git a/Examples/Basic/build.sh b/Examples/Basic/build.sh new file mode 100755 index 000000000..826e90f81 --- /dev/null +++ b/Examples/Basic/build.sh @@ -0,0 +1,3 @@ +#!/bin/bash +set -ex +swift package --swift-sdk "${SWIFT_SDK_ID:-wasm32-unknown-wasi}" -c "${1:-debug}" js --use-cdn diff --git a/Examples/Basic/index.html b/Examples/Basic/index.html new file mode 100644 index 000000000..93868214d --- /dev/null +++ b/Examples/Basic/index.html @@ -0,0 +1,15 @@ + + + + + Getting Started + + + + + + + diff --git a/Examples/Embedded/Package.swift b/Examples/Embedded/Package.swift new file mode 100644 index 000000000..5ae19adc6 --- /dev/null +++ b/Examples/Embedded/Package.swift @@ -0,0 +1,37 @@ +// swift-tools-version:6.0 + +import PackageDescription + +let package = Package( + name: "Embedded", + dependencies: [ + .package(name: "JavaScriptKit", path: "../../"), + .package(url: "https://github.com/swiftwasm/swift-dlmalloc", branch: "0.1.0"), + ], + targets: [ + .executableTarget( + name: "EmbeddedApp", + dependencies: [ + "JavaScriptKit", + .product(name: "dlmalloc", package: "swift-dlmalloc"), + ], + cSettings: [.unsafeFlags(["-fdeclspec"])], + swiftSettings: [ + .enableExperimentalFeature("Embedded"), + .enableExperimentalFeature("Extern"), + .unsafeFlags([ + "-Xfrontend", "-gnone", + "-Xfrontend", "-disable-stack-protector", + ]), + ], + linkerSettings: [ + .unsafeFlags([ + "-Xclang-linker", "-nostdlib", + "-Xlinker", "--no-entry", + "-Xlinker", "--export-if-defined=__main_argc_argv", + ]) + ] + ) + ], + swiftLanguageModes: [.v5] +) diff --git a/Examples/Embedded/README.md b/Examples/Embedded/README.md new file mode 100644 index 000000000..e99d659ff --- /dev/null +++ b/Examples/Embedded/README.md @@ -0,0 +1,8 @@ +# Embedded example + +Requires a recent DEVELOPMENT-SNAPSHOT toolchain. (tested with swift-6.1-DEVELOPMENT-SNAPSHOT-2025-02-21-a) + +```sh +$ ./build.sh +$ npx serve +``` diff --git a/Examples/Embedded/Sources/EmbeddedApp/_thingsThatShouldNotBeNeeded.swift b/Examples/Embedded/Sources/EmbeddedApp/_thingsThatShouldNotBeNeeded.swift new file mode 100644 index 000000000..8f45ccee9 --- /dev/null +++ b/Examples/Embedded/Sources/EmbeddedApp/_thingsThatShouldNotBeNeeded.swift @@ -0,0 +1,36 @@ +import JavaScriptKit + +// NOTE: it seems the embedded tree shaker gets rid of these exports if they are not used somewhere +func _i_need_to_be_here_for_wasm_exports_to_work() { + _ = _swjs_library_features + _ = _swjs_call_host_function + _ = _swjs_free_host_function +} + +// TODO: why do I need this? and surely this is not ideal... figure this out, or at least have this come from a C lib +@_cdecl("strlen") +func strlen(_ s: UnsafePointer) -> Int { + var p = s + while p.pointee != 0 { + p += 1 + } + return p - s +} + +enum LCG { + static var x: UInt8 = 0 + static let a: UInt8 = 0x05 + static let c: UInt8 = 0x0b + + static func next() -> UInt8 { + x = a &* x &+ c + return x + } +} + +@_cdecl("arc4random_buf") +public func arc4random_buf(_ buffer: UnsafeMutableRawPointer, _ size: Int) { + for i in 0..( + 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/Embedded/build.sh b/Examples/Embedded/build.sh new file mode 100755 index 000000000..f807cdbf5 --- /dev/null +++ b/Examples/Embedded/build.sh @@ -0,0 +1,5 @@ +#!/bin/bash +package_dir="$(cd "$(dirname "$0")" && pwd)" +JAVASCRIPTKIT_EXPERIMENTAL_EMBEDDED_WASM=true \ + swift package --package-path "$package_dir" \ + -c release --triple wasm32-unknown-none-wasm js diff --git a/Examples/Embedded/index.html b/Examples/Embedded/index.html new file mode 100644 index 000000000..93868214d --- /dev/null +++ b/Examples/Embedded/index.html @@ -0,0 +1,15 @@ + + + + + Getting Started + + + + + + + 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
new file mode 100644
index 000000000..f55b8400a
--- /dev/null
+++ b/Examples/Multithreading/Package.resolved
@@ -0,0 +1,23 @@
+{
+  "originHash" : "072d03a6e24e01bd372682a6090adb80cf29dea39421e065de6ff8853de704c9",
+  "pins" : [
+    {
+      "identity" : "chibi-ray",
+      "kind" : "remoteSourceControl",
+      "location" : "https://github.com/kateinoigakukun/chibi-ray",
+      "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
new file mode 100644
index 000000000..4d1ebde70
--- /dev/null
+++ b/Examples/Multithreading/Package.swift
@@ -0,0 +1,25 @@
+// swift-tools-version: 5.10
+
+import PackageDescription
+
+let package = Package(
+    name: "Example",
+    platforms: [.macOS("15"), .iOS("18"), .watchOS("11"), .tvOS("18"), .visionOS("2")],
+    dependencies: [
+        .package(path: "../../"),
+        .package(
+            url: "https://github.com/kateinoigakukun/chibi-ray",
+            revision: "c8cab621a3338dd2f8e817d3785362409d3b8cf1"
+        ),
+    ],
+    targets: [
+        .executableTarget(
+            name: "MyApp",
+            dependencies: [
+                .product(name: "JavaScriptKit", package: "JavaScriptKit"),
+                .product(name: "JavaScriptEventLoop", package: "JavaScriptKit"),
+                .product(name: "ChibiRay", package: "chibi-ray"),
+            ]
+        )
+    ]
+)
diff --git a/Examples/Multithreading/README.md b/Examples/Multithreading/README.md
new file mode 100644
index 000000000..346f8cc8b
--- /dev/null
+++ b/Examples/Multithreading/README.md
@@ -0,0 +1,21 @@
+# Multithreading example
+
+Install Development Snapshot toolchain `DEVELOPMENT-SNAPSHOT-2024-07-08-a` or later from [swift.org/install](https://www.swift.org/install/) and run the following commands:
+
+```sh
+$ (
+  set -eo pipefail; \
+  V="$(swiftc --version | head -n1)"; \
+  TAG="$(curl -sL "https://raw.githubusercontent.com/swiftwasm/swift-sdk-index/refs/heads/main/v1/tag-by-version.json" | jq -e -r --arg v "$V" '.[$v] | .[-1]')"; \
+  curl -sL "https://raw.githubusercontent.com/swiftwasm/swift-sdk-index/refs/heads/main/v1/builds/$TAG.json" | \
+  jq -r '.["swift-sdks"]["wasm32-unknown-wasip1-threads"] | "swift sdk install \"\(.url)\" --checksum \"\(.checksum)\""' | sh -x
+)
+$ export SWIFT_SDK_ID=$(
+  V="$(swiftc --version | head -n1)"; \
+  TAG="$(curl -sL "https://raw.githubusercontent.com/swiftwasm/swift-sdk-index/refs/heads/main/v1/tag-by-version.json" | jq -e -r --arg v "$V" '.[$v] | .[-1]')"; \
+  curl -sL "https://raw.githubusercontent.com/swiftwasm/swift-sdk-index/refs/heads/main/v1/builds/$TAG.json" | \
+  jq -r '.["swift-sdks"]["wasm32-unknown-wasip1-threads"]["id"]'
+)
+$ ./build.sh
+$ npx serve
+```
diff --git a/Examples/Multithreading/Sources/MyApp/Scene.swift b/Examples/Multithreading/Sources/MyApp/Scene.swift
new file mode 100644
index 000000000..bddde1715
--- /dev/null
+++ b/Examples/Multithreading/Sources/MyApp/Scene.swift
@@ -0,0 +1,93 @@
+import ChibiRay
+
+func createDemoScene(size: Int) -> Scene {
+    return Scene(
+        width: size,
+        height: size,
+        fov: 90,
+        elements: [
+            .sphere(
+                Sphere(
+                    center: Point(x: 1.0, y: -1.0, z: -7.0),
+                    radius: 1.0,
+                    material: Material(
+                        color: Color(red: 0.8, green: 0.2, blue: 0.4),
+                        albedo: 0.6,
+                        surface: .reflective(reflectivity: 0.9)
+                    )
+                )
+            ),
+            .sphere(
+                Sphere(
+                    center: Point(x: -2.0, y: 1.0, z: -10.0),
+                    radius: 3.0,
+                    material: Material(
+                        color: Color(red: 1.0, green: 0.4, blue: 0.4),
+                        albedo: 0.7,
+                        surface: .diffuse
+                    )
+                )
+            ),
+            .sphere(
+                Sphere(
+                    center: Point(x: 3.0, y: 2.0, z: -5.0),
+                    radius: 2.0,
+                    material: Material(
+                        color: Color(red: 0.4, green: 0.4, blue: 0.8),
+                        albedo: 0.5,
+                        surface: .refractive(index: 2, transparency: 0.9)
+                    )
+                )
+            ),
+            .plane(
+                Plane(
+                    origin: Point(x: 0.0, y: -2.0, z: -5.0),
+                    normal: Vector3(x: 0.0, y: -1.0, z: 0.0),
+                    material: Material(
+                        color: Color(red: 1.0, green: 1.0, blue: 1.0),
+                        albedo: 0.18,
+                        surface: .reflective(reflectivity: 0.5)
+                    )
+                )
+            ),
+            .plane(
+                Plane(
+                    origin: Point(x: 0.0, y: 0.0, z: -20.0),
+                    normal: Vector3(x: 0.0, y: 0.0, z: -1.0),
+                    material: Material(
+                        color: Color(red: 0.2, green: 0.3, blue: 1.0),
+                        albedo: 0.38,
+                        surface: .diffuse
+                    )
+                )
+            ),
+        ],
+        lights: [
+            .spherical(
+                SphericalLight(
+                    position: Point(x: 5.0, y: 10.0, z: -3.0),
+                    color: Color(red: 1.0, green: 1.0, blue: 1.0),
+                    intensity: 16000
+                )
+            ),
+            .spherical(
+                SphericalLight(
+                    position: Point(x: -3.0, y: 3.0, z: -5.0),
+                    color: Color(red: 0.3, green: 0.3, blue: 1.0),
+                    intensity: 1000
+                )
+            ),
+            .directional(
+                DirectionalLight(
+                    direction: Vector3(x: 0.0, y: -1.0, z: -1.0),
+                    color: Color(red: 0.8, green: 0.8, blue: 0.8),
+                    intensity: 0.2
+                )
+            ),
+        ],
+        shadowBias: 1e-13,
+        maxRecursionDepth: 10
+    )
+}
+
+extension Scene: @retroactive @unchecked Sendable {}
diff --git a/Examples/Multithreading/Sources/MyApp/main.swift b/Examples/Multithreading/Sources/MyApp/main.swift
new file mode 100644
index 000000000..9a1e09bb4
--- /dev/null
+++ b/Examples/Multithreading/Sources/MyApp/main.swift
@@ -0,0 +1,178 @@
+import ChibiRay
+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..
+
+    subscript(x: Int, y: Int) -> Color {
+        get {
+            return buffer[y * width + x]
+        }
+        nonmutating set {
+            buffer[y * width + x] = newValue
+        }
+    }
+}
+
+struct Work: Sendable {
+    let scene: Scene
+    let imageView: ImageView
+    let yRange: CountableRange
+
+    init(scene: Scene, imageView: ImageView, yRange: CountableRange) {
+        self.scene = scene
+        self.imageView = imageView
+        self.yRange = yRange
+    }
+    func run() {
+        for y in yRange {
+            for x in 0...allocate(capacity: scene.width * scene.height)
+    // Initialize the buffer with black color
+    imageBuffer.initialize(repeating: .black)
+    let imageView = ImageView(width: scene.width, height: scene.height, buffer: imageBuffer)
+
+    let clock = ContinuousClock()
+    let start = clock.now
+
+    func updateRenderTime() {
+        let renderSceneDuration = clock.now - start
+        renderTimeElement.textContent = .string("Render time: \(renderSceneDuration)")
+    }
+
+    var checkTimer: JSValue?
+    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
+        for i in 0..) -> 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/Multithreading/build.sh b/Examples/Multithreading/build.sh
new file mode 100755
index 000000000..c82a10c32
--- /dev/null
+++ b/Examples/Multithreading/build.sh
@@ -0,0 +1,3 @@
+swift package --swift-sdk "${SWIFT_SDK_ID:-wasm32-unknown-wasip1-threads}" -c release \
+    plugin --allow-writing-to-package-directory \
+    js --use-cdn --output ./Bundle
diff --git a/Examples/Multithreading/index.html b/Examples/Multithreading/index.html
new file mode 100644
index 000000000..20696d83a
--- /dev/null
+++ b/Examples/Multithreading/index.html
@@ -0,0 +1,57 @@
+
+
+
+  
+  Threading Example
+  
+  
+
+
+
+  
+  

Threading Example

+

+

+ + +
+
+ + +
+
+ + + +
+

+

+

🧵
+

+

+ + + + diff --git a/Examples/Multithreading/serve.json b/Examples/Multithreading/serve.json new file mode 100644 index 000000000..537a16904 --- /dev/null +++ b/Examples/Multithreading/serve.json @@ -0,0 +1,14 @@ +{ + "headers": [{ + "source": "**/*", + "headers": [ + { + "key": "Cross-Origin-Embedder-Policy", + "value": "require-corp" + }, { + "key": "Cross-Origin-Opener-Policy", + "value": "same-origin" + } + ] + }] +} diff --git a/Examples/OffscrenCanvas/Package.swift b/Examples/OffscrenCanvas/Package.swift new file mode 100644 index 000000000..ca6d7357f --- /dev/null +++ b/Examples/OffscrenCanvas/Package.swift @@ -0,0 +1,20 @@ +// swift-tools-version: 5.10 + +import PackageDescription + +let package = Package( + name: "Example", + platforms: [.macOS("15"), .iOS("18"), .watchOS("11"), .tvOS("18"), .visionOS("2")], + dependencies: [ + .package(path: "../../") + ], + targets: [ + .executableTarget( + name: "MyApp", + dependencies: [ + .product(name: "JavaScriptKit", package: "JavaScriptKit"), + .product(name: "JavaScriptEventLoop", package: "JavaScriptKit"), + ] + ) + ] +) diff --git a/Examples/OffscrenCanvas/README.md b/Examples/OffscrenCanvas/README.md new file mode 100644 index 000000000..395b0c295 --- /dev/null +++ b/Examples/OffscrenCanvas/README.md @@ -0,0 +1,21 @@ +# OffscreenCanvas example + +Install Development Snapshot toolchain `DEVELOPMENT-SNAPSHOT-2024-07-08-a` or later from [swift.org/install](https://www.swift.org/install/) and run the following commands: + +```sh +$ ( + set -eo pipefail; \ + V="$(swiftc --version | head -n1)"; \ + TAG="$(curl -sL "https://raw.githubusercontent.com/swiftwasm/swift-sdk-index/refs/heads/main/v1/tag-by-version.json" | jq -e -r --arg v "$V" '.[$v] | .[-1]')"; \ + curl -sL "https://raw.githubusercontent.com/swiftwasm/swift-sdk-index/refs/heads/main/v1/builds/$TAG.json" | \ + jq -r '.["swift-sdks"]["wasm32-unknown-wasip1-threads"] | "swift sdk install \"\(.url)\" --checksum \"\(.checksum)\""' | sh -x +) +$ export SWIFT_SDK_ID=$( + V="$(swiftc --version | head -n1)"; \ + TAG="$(curl -sL "https://raw.githubusercontent.com/swiftwasm/swift-sdk-index/refs/heads/main/v1/tag-by-version.json" | jq -e -r --arg v "$V" '.[$v] | .[-1]')"; \ + curl -sL "https://raw.githubusercontent.com/swiftwasm/swift-sdk-index/refs/heads/main/v1/builds/$TAG.json" | \ + jq -r '.["swift-sdks"]["wasm32-unknown-wasip1-threads"]["id"]' +) +$ ./build.sh +$ npx serve +``` diff --git a/Examples/OffscrenCanvas/Sources/MyApp/main.swift b/Examples/OffscrenCanvas/Sources/MyApp/main.swift new file mode 100644 index 000000000..a2a6e2aac --- /dev/null +++ b/Examples/OffscrenCanvas/Sources/MyApp/main.swift @@ -0,0 +1,142 @@ +import JavaScriptEventLoop +import JavaScriptKit + +JavaScriptEventLoop.installGlobalExecutor() +WebWorkerTaskExecutor.installGlobalExecutor() + +protocol CanvasRenderer { + func render(canvas: JSObject, size: Int) async throws +} + +struct BackgroundRenderer: CanvasRenderer { + func render(canvas: JSObject, size: Int) async throws { + let executor = try await WebWorkerTaskExecutor(numberOfThreads: 1) + let transfer = JSSending.transfer(canvas) + let renderingTask = Task(executorPreference: executor) { + let canvas = try await transfer.receive() + try await renderAnimation(canvas: canvas, size: size) + } + await withTaskCancellationHandler { + try? await renderingTask.value + } onCancel: { + renderingTask.cancel() + } + executor.terminate() + } +} + +struct MainThreadRenderer: CanvasRenderer { + func render(canvas: JSObject, size: Int) async throws { + try await renderAnimation(canvas: canvas, size: size) + } +} + +// FPS Counter for CSS animation +func startFPSMonitor() { + let fpsCounterElement = JSObject.global.document.getElementById("fps-counter").object! + + var lastTime = JSObject.global.performance.now().number! + var frames = 0 + + // Create a frame counter function + func countFrame() { + frames += 1 + let currentTime = JSObject.global.performance.now().number! + let elapsed = currentTime - lastTime + + if elapsed >= 1000 { + let fps = Int(Double(frames) * 1000 / elapsed) + fpsCounterElement.textContent = .string("FPS: \(fps)") + frames = 0 + lastTime = currentTime + } + + // Request next frame + _ = JSObject.global.requestAnimationFrame!( + JSClosure { _ in + countFrame() + return .undefined + } + ) + } + + // Start counting + countFrame() +} + +@MainActor +func onClick(renderer: CanvasRenderer) async throws { + let document = JSObject.global.document + + let canvasContainerElement = document.getElementById("canvas-container").object! + + // Remove all child elements from the canvas container + for i in 0..? = nil + + // Start the FPS monitor for CSS animations + startFPSMonitor() + + _ = renderButtonElement.addEventListener!( + "click", + JSClosure { _ in + renderingTask?.cancel() + renderingTask = Task { + let selectedValue = rendererSelectElement.value.string! + let renderer: CanvasRenderer = + selectedValue == "main" ? MainThreadRenderer() : BackgroundRenderer() + try await onClick(renderer: renderer) + } + return JSValue.undefined + } + ) + + _ = cancelButtonElement.addEventListener!( + "click", + JSClosure { _ in + renderingTask?.cancel() + return JSValue.undefined + } + ) +} + +Task { + try await main() +} + +#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 +} +#endif diff --git a/Examples/OffscrenCanvas/Sources/MyApp/render.swift b/Examples/OffscrenCanvas/Sources/MyApp/render.swift new file mode 100644 index 000000000..6a9d057a9 --- /dev/null +++ b/Examples/OffscrenCanvas/Sources/MyApp/render.swift @@ -0,0 +1,179 @@ +import Foundation +import JavaScriptKit + +func sleepOnThread(milliseconds: Int, isolation: isolated (any Actor)? = #isolation) async { + // Use the JavaScript setTimeout function to avoid hopping back to the main thread + await withCheckedContinuation(isolation: isolation) { continuation in + _ = JSObject.global.setTimeout!( + JSOneshotClosure { _ in + continuation.resume() + return JSValue.undefined + }, + milliseconds + ) + } +} + +func renderAnimation( + canvas: JSObject, + size: Int, + isolation: isolated (any Actor)? = #isolation +) + async throws +{ + let ctx = canvas.getContext!("2d").object! + + // Animation state variables + var time: Double = 0 + + // Create a large number of particles + let particleCount = 5000 + var particles: [[Double]] = [] + + // Initialize particles with random positions and velocities + for _ in 0.. Double(size) { + particles[i][2] *= -0.8 + } + if particles[i][1] < 0 || particles[i][1] > Double(size) { + particles[i][3] *= -0.8 + } + + // Calculate opacity based on lifespan + let opacity = particles[i][6] / particles[i][7] + + // Get coordinates and properties + let x = particles[i][0] + let y = particles[i][1] + let size = particles[i][4] + let hue = (particles[i][5] + time * 10).truncatingRemainder(dividingBy: 360) + + // Draw particle + _ = ctx.beginPath!() + ctx.fillStyle = .string("hsla(\(hue), 100%, 60%, \(opacity))") + _ = ctx.arc!(x, y, size, 0, 2 * Double.pi) + _ = ctx.fill!() + + // Connect nearby particles with lines (only check some to save CPU) + if i % 20 == 0 { + for j in (i + 1).. + + + + OffscreenCanvas Example + + + + + +

OffscreenCanvas Example

+

+

+ + + +
+

+ +

CSS Animation (Main Thread Performance Indicator)

+
+
+
+
+
+
+
+ +
FPS: 0
+ +
+ + + diff --git a/Examples/OffscrenCanvas/serve.json b/Examples/OffscrenCanvas/serve.json new file mode 100644 index 000000000..537a16904 --- /dev/null +++ b/Examples/OffscrenCanvas/serve.json @@ -0,0 +1,14 @@ +{ + "headers": [{ + "source": "**/*", + "headers": [ + { + "key": "Cross-Origin-Embedder-Policy", + "value": "require-corp" + }, { + "key": "Cross-Origin-Opener-Policy", + "value": "same-origin" + } + ] + }] +} diff --git a/Examples/Testing/Package.swift b/Examples/Testing/Package.swift new file mode 100644 index 000000000..2a81bfa7a --- /dev/null +++ b/Examples/Testing/Package.swift @@ -0,0 +1,30 @@ +// swift-tools-version: 6.0 + +import PackageDescription + +let package = Package( + name: "Counter", + products: [ + .library( + name: "Counter", + targets: ["Counter"] + ) + ], + dependencies: [.package(name: "JavaScriptKit", path: "../../")], + targets: [ + .target( + 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"), + ] + ), + ] +) diff --git a/Examples/Testing/README.md b/Examples/Testing/README.md new file mode 100644 index 000000000..2f28357a6 --- /dev/null +++ b/Examples/Testing/README.md @@ -0,0 +1,33 @@ +# Testing example + +This example demonstrates how to write and run tests for Swift code compiled to WebAssembly using JavaScriptKit. + +## Running Tests + +To run the tests, use the following command: + +```console +swift package --disable-sandbox --swift-sdk wasm32-unknown-wasi js test +``` + +## Code Coverage + +To generate and view code coverage reports: + +1. Run tests with code coverage enabled: + +```console +swift package --disable-sandbox --swift-sdk wasm32-unknown-wasi js test --enable-code-coverage +``` + +2. Generate HTML coverage report: + +```console +llvm-cov show -instr-profile=.build/plugins/PackageToJS/outputs/PackageTests/default.profdata --format=html .build/plugins/PackageToJS/outputs/PackageTests/main.wasm -o .build/coverage/html Sources +``` + +3. Serve and view the coverage report: + +```console +npx serve .build/coverage/html +``` diff --git a/Examples/Testing/Sources/Counter/Counter.swift b/Examples/Testing/Sources/Counter/Counter.swift new file mode 100644 index 000000000..61e0a7a3b --- /dev/null +++ b/Examples/Testing/Sources/Counter/Counter.swift @@ -0,0 +1,7 @@ +public struct Counter { + public private(set) var count = 0 + + public mutating func increment() { + count += 1 + } +} diff --git a/Examples/Testing/Tests/CounterTests/CounterTests.swift b/Examples/Testing/Tests/CounterTests/CounterTests.swift new file mode 100644 index 000000000..ec92cd14c --- /dev/null +++ b/Examples/Testing/Tests/CounterTests/CounterTests.swift @@ -0,0 +1,36 @@ +import XCTest + +@testable import Counter + +#if canImport(Testing) +import Testing + +@Test func increment() async throws { + var counter = Counter() + counter.increment() + #expect(counter.count == 1) +} + +@Test func incrementTwice() async throws { + var counter = Counter() + counter.increment() + counter.increment() + #expect(counter.count == 2) +} + +#endif + +class CounterTests: XCTestCase { + func testIncrement() async { + var counter = Counter() + counter.increment() + XCTAssertEqual(counter.count, 1) + } + + func testIncrementTwice() async { + var counter = Counter() + counter.increment() + counter.increment() + XCTAssertEqual(counter.count, 2) + } +} diff --git a/IntegrationTests/Makefile b/IntegrationTests/Makefile deleted file mode 100644 index 6e1a4dd05..000000000 --- a/IntegrationTests/Makefile +++ /dev/null @@ -1,42 +0,0 @@ -CONFIGURATION ?= debug -SWIFT_BUILD_FLAGS ?= - -FORCE: -TestSuites/.build/$(CONFIGURATION)/%.wasm: FORCE - swift build --package-path TestSuites \ - --product $(basename $(notdir $@)) \ - --triple wasm32-unknown-wasi \ - --configuration $(CONFIGURATION) \ - $(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: - node bin/benchmark-tests.js - -.PHONY: benchmark -benchmark: benchmark_setup run_benchmark - -.PHONY: primary_test -primary_test: build_rt dist/PrimaryTests.wasm - node bin/primary-tests.js - -.PHONY: concurrency_test -concurrency_test: build_rt dist/ConcurrencyTests.wasm - node bin/concurrency-tests.js - -.PHONY: test -test: concurrency_test primary_test 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 0344d0499..000000000 --- a/IntegrationTests/TestSuites/Package.swift +++ /dev/null @@ -1,39 +0,0 @@ -// swift-tools-version:5.2 - -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: "PrimaryTests", targets: ["PrimaryTests"] - ), - .executable( - name: "ConcurrencyTests", targets: ["ConcurrencyTests"] - ), - .executable( - name: "BenchmarkTests", targets: ["BenchmarkTests"] - ), - ], - dependencies: [.package(name: "JavaScriptKit", path: "../../")], - targets: [ - .target(name: "CHelpers"), - .target(name: "PrimaryTests", dependencies: [ - "JavaScriptKit", - "CHelpers", - ]), - .target( - name: "ConcurrencyTests", - dependencies: [ - .product(name: "JavaScriptEventLoop", package: "JavaScriptKit"), - ] - ), - .target(name: "BenchmarkTests", dependencies: ["JavaScriptKit"]), - ] -) diff --git a/IntegrationTests/TestSuites/Sources/BenchmarkTests/Benchmark.swift b/IntegrationTests/TestSuites/Sources/BenchmarkTests/Benchmark.swift deleted file mode 100644 index 3cebb9d07..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 () -> Void) { - let jsBody = JSClosure { arguments -> JSValue in - let iteration = Int(arguments[0].number!) - for _ in 0 ..< iteration { body() } - 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 df11f6f14..000000000 --- a/IntegrationTests/TestSuites/Sources/BenchmarkTests/main.swift +++ /dev/null @@ -1,32 +0,0 @@ -import JavaScriptKit - -let serialization = Benchmark("Serialization") - -let swiftInt: Double = 42 -serialization.testSuite("Swift Int to JavaScript") { - let jsNumber = JSValue.number(swiftInt) - let object = JSObject.global - for i in 0 ..< 100 { - object["numberValue\(i)"] = jsNumber - } -} - -let swiftString = "Hello, world" -serialization.testSuite("Swift String to JavaScript") { - let jsString = JSValue.string(swiftString) - let object = JSObject.global - for i in 0 ..< 100 { - object["stringValue\(i)"] = jsString - } -} - -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") { - for _ in 0 ..< 100 { - _ = 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 c5505a5a4..000000000 --- a/IntegrationTests/TestSuites/Sources/CHelpers/include/helpers.h +++ /dev/null @@ -1,5 +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); - 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/TestSuites/Sources/ConcurrencyTests/UnitTestUtils.swift b/IntegrationTests/TestSuites/Sources/ConcurrencyTests/UnitTestUtils.swift deleted file mode 100644 index 1557764a2..000000000 --- a/IntegrationTests/TestSuites/Sources/ConcurrencyTests/UnitTestUtils.swift +++ /dev/null @@ -1,127 +0,0 @@ -import JavaScriptKit - -#if compiler(>=5.5) -var printTestNames = false -// Uncomment the next line to print the name of each test suite before running it. -// This will make it easier to debug any errors that occur on the JS side. -//printTestNames = true - -func test(_ name: String, testBlock: () throws -> Void) throws { - if printTestNames { print(name) } - do { - try testBlock() - } catch { - print("Error in \(name)") - print(error) - throw error - } -} - -func asyncTest(_ name: String, testBlock: () async throws -> Void) async throws -> Void { - if printTestNames { print(name) } - do { - try await testBlock() - } catch { - print("Error in \(name)") - print(error) - throw error - } -} - -struct MessageError: Error { - let message: String - let file: StaticString - let line: UInt - let column: UInt - init(_ message: String, file: StaticString, line: UInt, column: UInt) { - self.message = message - self.file = file - self.line = line - self.column = column - } -} - -func expectEqual( - _ lhs: T, _ rhs: T, - file: StaticString = #file, line: UInt = #line, column: UInt = #column -) throws { - if lhs != rhs { - throw MessageError("Expect to be equal \"\(lhs)\" and \"\(rhs)\"", file: file, line: line, column: column) - } -} - -func expectCast( - _ value: T, to type: U.Type = U.self, - file: StaticString = #file, line: UInt = #line, column: UInt = #column -) throws -> U { - guard let value = value as? U else { - throw MessageError("Expect \"\(value)\" to be \(U.self)", file: file, line: line, column: column) - } - return value -} - -func expectObject(_ value: JSValue, file: StaticString = #file, line: UInt = #line, column: UInt = #column) throws -> JSObject { - switch value { - case let .object(ref): return ref - default: - throw MessageError("Type of \(value) should be \"object\"", file: file, line: line, column: column) - } -} - -func expectArray(_ value: JSValue, file: StaticString = #file, line: UInt = #line, column: UInt = #column) throws -> JSArray { - guard let array = value.array else { - throw MessageError("Type of \(value) should be \"object\"", file: file, line: line, column: column) - } - return array -} - -func expectFunction(_ value: JSValue, file: StaticString = #file, line: UInt = #line, column: UInt = #column) throws -> JSFunction { - switch value { - case let .function(ref): return ref - default: - throw MessageError("Type of \(value) should be \"function\"", file: file, line: line, column: column) - } -} - -func expectBoolean(_ value: JSValue, file: StaticString = #file, line: UInt = #line, column: UInt = #column) throws -> Bool { - switch value { - case let .boolean(bool): return bool - default: - throw MessageError("Type of \(value) should be \"boolean\"", file: file, line: line, column: column) - } -} - -func expectNumber(_ value: JSValue, file: StaticString = #file, line: UInt = #line, column: UInt = #column) throws -> Double { - switch value { - case let .number(number): return number - default: - throw MessageError("Type of \(value) should be \"number\"", file: file, line: line, column: column) - } -} - -func expectString(_ value: JSValue, file: StaticString = #file, line: UInt = #line, column: UInt = #column) throws -> String { - switch value { - case let .string(string): return String(string) - default: - throw MessageError("Type of \(value) should be \"string\"", file: file, line: line, column: column) - } -} - -func expectAsyncThrow(_ body: @autoclosure () async throws -> T, file: StaticString = #file, line: UInt = #line, column: UInt = #column) async throws -> Error { - do { - _ = try await body() - } catch { - return error - } - throw MessageError("Expect to throw an exception", file: file, line: line, column: column) -} - -func expectNotNil(_ value: T?, file: StaticString = #file, line: UInt = #line, column: UInt = #column) throws { - switch value { - case .some: return - case .none: - throw MessageError("Expect a non-nil value", file: file, line: line, column: column) - } -} - -#endif diff --git a/IntegrationTests/TestSuites/Sources/ConcurrencyTests/main.swift b/IntegrationTests/TestSuites/Sources/ConcurrencyTests/main.swift deleted file mode 100644 index 5447032fe..000000000 --- a/IntegrationTests/TestSuites/Sources/ConcurrencyTests/main.swift +++ /dev/null @@ -1,146 +0,0 @@ -import JavaScriptEventLoop -import JavaScriptKit -#if canImport(WASILibc) -import WASILibc -#elseif canImport(Darwin) -import Darwin -#endif - -#if compiler(>=5.5) - -func entrypoint() async throws { - struct E: Error, Equatable { - let value: Int - } - - try await asyncTest("Task.init value") { - let handle = Task { 1 } - try expectEqual(await handle.value, 1) - } - - try await asyncTest("Task.init throws") { - let handle = Task { - throw E(value: 2) - } - let error = try await expectAsyncThrow(await handle.value) - let e = try expectCast(error, to: E.self) - try expectEqual(e, E(value: 2)) - } - - try await asyncTest("await resolved Promise") { - let p = JSPromise(resolver: { resolve in - resolve(.success(1)) - }) - try await expectEqual(p.value, 1) - } - - try await asyncTest("await rejected Promise") { - let p = JSPromise(resolver: { resolve in - resolve(.failure(.number(3))) - }) - let error = try await expectAsyncThrow(await p.value) - let jsValue = try expectCast(error, to: JSValue.self) - try expectEqual(jsValue, 3) - } - - try await asyncTest("Continuation") { - let value = await withUnsafeContinuation { cont in - cont.resume(returning: 1) - } - try expectEqual(value, 1) - - let error = try await expectAsyncThrow( - try await withUnsafeThrowingContinuation { (cont: UnsafeContinuation) in - cont.resume(throwing: E(value: 2)) - } - ) - let e = try expectCast(error, to: E.self) - try expectEqual(e.value, 2) - } - - try await asyncTest("Task.sleep(_:)") { - let start = time(nil) - try await Task.sleep(nanoseconds: 2_000_000_000) - let diff = difftime(time(nil), start); - try expectEqual(diff >= 2, true) - } - - try await asyncTest("Job reordering based on priority") { - class Context: @unchecked Sendable { - var completed: [String] = [] - } - let context = Context() - - // When no priority, they should be ordered by the enqueued order - let t1 = Task(priority: nil) { - context.completed.append("t1") - } - let t2 = Task(priority: nil) { - context.completed.append("t2") - } - - _ = await (t1.value, t2.value) - try expectEqual(context.completed, ["t1", "t2"]) - - context.completed = [] - // When high priority is enqueued after a low one, they should be re-ordered - let t3 = Task(priority: .low) { - context.completed.append("t3") - } - let t4 = Task(priority: .high) { - context.completed.append("t4") - } - let t5 = Task(priority: .low) { - context.completed.append("t5") - } - - _ = await (t3.value, t4.value, t5.value) - try expectEqual(context.completed, ["t4", "t3", "t5"]) - } - // FIXME(katei): Somehow it doesn't work due to a mysterious unreachable inst - // at the end of thunk. - // This issue is not only on JS host environment, but also on standalone coop executor. - try await asyncTest("Task.sleep(nanoseconds:)") { - try await Task.sleep(nanoseconds: 1_000_000_000) - } -} - - -// Note: Please define `USE_SWIFT_TOOLS_VERSION_NEWER_THAN_5_5` if the swift-tools-version is newer -// than 5.5 to avoid the linking issue. -#if USE_SWIFT_TOOLS_VERSION_NEWER_THAN_5_5 -// Workaround: The latest SwiftPM rename main entry point name of executable target -// to avoid conflicting "main" with test target since `swift-tools-version >= 5.5`. -// The main symbol is renamed to "{{module_name}}_main" and it's renamed again to be -// "main" when linking the executable target. The former renaming is done by Swift compiler, -// and the latter is done by linker, so SwiftPM passes some special linker flags for each platform. -// But SwiftPM assumes that wasm-ld supports it by returning an empty array instead of nil even though -// wasm-ld doesn't support it yet. -// ref: https://github.com/apple/swift-package-manager/blob/1be68e811d0d814ba7abbb8effee45f1e8e6ec0d/Sources/Build/BuildPlan.swift#L117-L126 -// So define an explicit "main" by @_cdecl -@_cdecl("main") -func main(argc: Int32, argv: Int32) -> Int32 { - JavaScriptEventLoop.installGlobalExecutor() - Task { - do { - try await entrypoint() - } catch { - print(error) - } - } - return 0 -} -#else -JavaScriptEventLoop.installGlobalExecutor() -Task { - do { - try await entrypoint() - } catch { - print(error) - } -} - -#endif - - -#endif diff --git a/IntegrationTests/TestSuites/Sources/PrimaryTests/UnitTestUtils.swift b/IntegrationTests/TestSuites/Sources/PrimaryTests/UnitTestUtils.swift deleted file mode 100644 index 87ab72bc4..000000000 --- a/IntegrationTests/TestSuites/Sources/PrimaryTests/UnitTestUtils.swift +++ /dev/null @@ -1,141 +0,0 @@ -import JavaScriptKit - -var printTestNames = false -// Uncomment the next line to print the name of each test suite before running it. -// This will make it easier to debug any errors that occur on the JS side. -//printTestNames = true - -func test(_ name: String, testBlock: () throws -> Void) throws { - if printTestNames { print(name) } - do { - try testBlock() - } catch { - print("Error in \(name)") - print(error) - throw error - } -} - -struct MessageError: Error { - let message: String - let file: StaticString - let line: UInt - let column: UInt - init(_ message: String, file: StaticString, line: UInt, column: UInt) { - self.message = message - self.file = file - self.line = line - self.column = column - } -} - -func expectEqual( - _ lhs: T, _ rhs: T, - file: StaticString = #file, line: UInt = #line, column: UInt = #column -) throws { - if lhs != rhs { - throw MessageError("Expect to be equal \"\(lhs)\" and \"\(rhs)\"", file: file, line: line, column: column) - } -} - -func expectNotEqual( - _ lhs: T, _ rhs: T, - file: StaticString = #file, line: UInt = #line, column: UInt = #column -) throws { - if lhs == rhs { - throw MessageError("Expect to not be equal \"\(lhs)\" and \"\(rhs)\"", file: file, line: line, column: column) - } -} - -func expectObject(_ value: JSValue, file: StaticString = #file, line: UInt = #line, column: UInt = #column) throws -> JSObject { - switch value { - case let .object(ref): return ref - default: - throw MessageError("Type of \(value) should be \"object\"", file: file, line: line, column: column) - } -} - -func expectArray(_ value: JSValue, file: StaticString = #file, line: UInt = #line, column: UInt = #column) throws -> JSArray { - guard let array = value.array else { - throw MessageError("Type of \(value) should be \"object\"", file: file, line: line, column: column) - } - return array -} - -func expectFunction(_ value: JSValue, file: StaticString = #file, line: UInt = #line, column: UInt = #column) throws -> JSFunction { - switch value { - case let .function(ref): return ref - default: - throw MessageError("Type of \(value) should be \"function\"", file: file, line: line, column: column) - } -} - -func expectBoolean(_ value: JSValue, file: StaticString = #file, line: UInt = #line, column: UInt = #column) throws -> Bool { - switch value { - case let .boolean(bool): return bool - default: - throw MessageError("Type of \(value) should be \"boolean\"", file: file, line: line, column: column) - } -} - -func expectNumber(_ value: JSValue, file: StaticString = #file, line: UInt = #line, column: UInt = #column) throws -> Double { - switch value { - case let .number(number): return number - default: - throw MessageError("Type of \(value) should be \"number\"", file: file, line: line, column: column) - } -} - -func expectString(_ value: JSValue, file: StaticString = #file, line: UInt = #line, column: UInt = #column) throws -> String { - switch value { - case let .string(string): return String(string) - default: - throw MessageError("Type of \(value) should be \"string\"", file: file, line: line, column: column) - } -} - -func expectThrow(_ body: @autoclosure () throws -> T, file: StaticString = #file, line: UInt = #line, column: UInt = #column) throws -> Error { - do { - _ = try body() - } catch { - return error - } - throw MessageError("Expect to throw an exception", file: file, line: line, column: column) -} - -func expectNotNil(_ value: T?, file: StaticString = #file, line: UInt = #line, column: UInt = #column) throws { - switch value { - case .some: return - case .none: - throw MessageError("Expect a non-nil value", file: file, line: line, column: column) - } -} - -class Expectation { - private(set) var isFulfilled: Bool = false - private let label: String - private let expectedFulfillmentCount: Int - private var fulfillmentCount: Int = 0 - - init(label: String, expectedFulfillmentCount: Int = 1) { - self.label = label - self.expectedFulfillmentCount = expectedFulfillmentCount - } - - func fulfill() { - assert(!isFulfilled, "Too many fulfillment (label: \(label)): expectedFulfillmentCount is \(expectedFulfillmentCount)") - fulfillmentCount += 1 - if fulfillmentCount == expectedFulfillmentCount { - isFulfilled = true - } - } - - static func wait(_ expectations: [Expectation]) { - var timer: JSTimer! - timer = JSTimer(millisecondsDelay: 5.0, isRepeating: true) { - guard expectations.allSatisfy(\.isFulfilled) else { return } - assert(timer != nil) - timer = nil - } - } -} diff --git a/IntegrationTests/TestSuites/Sources/PrimaryTests/main.swift b/IntegrationTests/TestSuites/Sources/PrimaryTests/main.swift deleted file mode 100644 index e876b60e0..000000000 --- a/IntegrationTests/TestSuites/Sources/PrimaryTests/main.swift +++ /dev/null @@ -1,768 +0,0 @@ -import JavaScriptKit -import CHelpers - -try test("Literal Conversion") { - let global = JSObject.global - let inputs: [JSValue] = [ - .boolean(true), - .boolean(false), - .string("foobar"), - .string("👨‍👩‍👧‍👧 Family Emoji"), - .number(0), - .number(Double(Int32.max)), - .number(Double(Int32.min)), - .number(Double.infinity), - .number(Double.nan), - .null, - .undefined, - ] - for (index, input) in inputs.enumerated() { - let prop = JSString("prop_\(index)") - setJSValue(this: global, name: prop, value: input) - let got = getJSValue(this: global, name: prop) - switch (got, input) { - case let (.number(lhs), .number(rhs)): - // Compare bitPattern because nan == nan is always false - try expectEqual(lhs.bitPattern, rhs.bitPattern) - default: - try expectEqual(got, input) - } - } -} - -try test("Object Conversion") { - // Notes: globalObject1 is defined in JavaScript environment - // - // ```js - // global.globalObject1 = { - // "prop_1": { - // "nested_prop": 1, - // }, - // "prop_2": 2, - // "prop_3": true, - // "prop_4": [ - // 3, 4, "str_elm_1", 5, - // ], - // ... - // } - // ``` - // - - let globalObject1 = getJSValue(this: .global, name: "globalObject1") - let globalObject1Ref = try expectObject(globalObject1) - let prop_1 = getJSValue(this: globalObject1Ref, name: "prop_1") - let prop_1Ref = try expectObject(prop_1) - let nested_prop = getJSValue(this: prop_1Ref, name: "nested_prop") - try expectEqual(nested_prop, .number(1)) - let prop_2 = getJSValue(this: globalObject1Ref, name: "prop_2") - try expectEqual(prop_2, .number(2)) - let prop_3 = getJSValue(this: globalObject1Ref, name: "prop_3") - try expectEqual(prop_3, .boolean(true)) - let prop_4 = getJSValue(this: globalObject1Ref, name: "prop_4") - let prop_4Array = try expectObject(prop_4) - let expectedProp_4: [JSValue] = [ - .number(3), .number(4), .string("str_elm_1"), .null, .undefined, .number(5), - ] - for (index, expectedElement) in expectedProp_4.enumerated() { - let actualElement = getJSValue(this: prop_4Array, index: Int32(index)) - try expectEqual(actualElement, expectedElement) - } - - try expectEqual(getJSValue(this: globalObject1Ref, name: "undefined_prop"), .undefined) -} - -try test("Value Construction") { - let globalObject1 = getJSValue(this: .global, name: "globalObject1") - let globalObject1Ref = try expectObject(globalObject1) - let prop_2 = getJSValue(this: globalObject1Ref, name: "prop_2") - try expectEqual(Int.construct(from: prop_2), 2) - let prop_3 = getJSValue(this: globalObject1Ref, name: "prop_3") - try expectEqual(Bool.construct(from: prop_3), true) - let prop_7 = getJSValue(this: globalObject1Ref, name: "prop_7") - try expectEqual(Double.construct(from: prop_7), 3.14) - try expectEqual(Float.construct(from: prop_7), 3.14) -} - -try test("Array Iterator") { - let globalObject1 = getJSValue(this: .global, name: "globalObject1") - let globalObject1Ref = try expectObject(globalObject1) - let prop_4 = getJSValue(this: globalObject1Ref, name: "prop_4") - let array1 = try expectArray(prop_4) - let expectedProp_4: [JSValue] = [ - .number(3), .number(4), .string("str_elm_1"), .null, .undefined, .number(5), - ] - try expectEqual(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 expectArray(prop_8) - let expectedProp_8: [JSValue] = [0, 2, 3, 6] - try expectEqual(Array(array2), expectedProp_8) -} - -try test("Array RandomAccessCollection") { - let globalObject1 = getJSValue(this: .global, name: "globalObject1") - let globalObject1Ref = try expectObject(globalObject1) - let prop_4 = getJSValue(this: globalObject1Ref, name: "prop_4") - let array1 = try expectArray(prop_4) - let expectedProp_4: [JSValue] = [ - .number(3), .number(4), .string("str_elm_1"), .null, .undefined, .number(5), - ] - try expectEqual([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 expectArray(prop_8) - let expectedProp_8: [JSValue] = [ - 0, .undefined, 2, 3, .undefined, .undefined, 6 - ] - try expectEqual([array2[0], array2[1], array2[2], array2[3], array2[4], array2[5], array2[6]], expectedProp_8) -} - -try test("Value Decoder") { - struct GlobalObject1: Codable { - struct Prop1: Codable { - let nested_prop: Int - } - - let prop_1: Prop1 - let prop_2: Int - let prop_3: Bool - let prop_7: Float - } - let decoder = JSValueDecoder() - let rawGlobalObject1 = getJSValue(this: .global, name: "globalObject1") - let globalObject1 = try decoder.decode(GlobalObject1.self, from: rawGlobalObject1) - try expectEqual(globalObject1.prop_1.nested_prop, 1) - try expectEqual(globalObject1.prop_2, 2) - try expectEqual(globalObject1.prop_3, true) - try expectEqual(globalObject1.prop_7, 3.14) -} - -try test("Function Call") { - // Notes: globalObject1 is defined in JavaScript environment - // - // ```js - // global.globalObject1 = { - // ... - // "prop_5": { - // "func1": function () { return }, - // "func2": function () { return 1 }, - // "func3": function (n) { return n * 2 }, - // "func4": function (a, b, c) { return a + b + c }, - // "func5": function (x) { return "Hello, " + x }, - // "func6": function (c, a, b) { - // if (c) { return a } else { return b } - // }, - // } - // ... - // } - // ``` - // - - // Notes: If the size of `RawJSValue` is updated, these test suites will fail. - let globalObject1 = getJSValue(this: .global, name: "globalObject1") - let globalObject1Ref = try expectObject(globalObject1) - let prop_5 = getJSValue(this: globalObject1Ref, name: "prop_5") - let prop_5Ref = try expectObject(prop_5) - - let func1 = try expectFunction(getJSValue(this: prop_5Ref, name: "func1")) - try expectEqual(func1(), .undefined) - let func2 = try expectFunction(getJSValue(this: prop_5Ref, name: "func2")) - try expectEqual(func2(), .number(1)) - let func3 = try expectFunction(getJSValue(this: prop_5Ref, name: "func3")) - try expectEqual(func3(2), .number(4)) - let func4 = try expectFunction(getJSValue(this: prop_5Ref, name: "func4")) - try expectEqual(func4(2, 3, 4), .number(9)) - try expectEqual(func4(2, 3, 4, 5), .number(9)) - let func5 = try expectFunction(getJSValue(this: prop_5Ref, name: "func5")) - try expectEqual(func5("World!"), .string("Hello, World!")) - let func6 = try expectFunction(getJSValue(this: prop_5Ref, name: "func6")) - try expectEqual(func6(true, 1, 2), .number(1)) - try expectEqual(func6(false, 1, 2), .number(2)) - try expectEqual(func6(true, "OK", 2), .string("OK")) -} - -let evalClosure = JSObject.global.globalObject1.eval_closure.function! - -try test("Closure Lifetime") { - func expectCrashByCall(ofClosure c: JSClosureProtocol) throws { - print("======= BEGIN OF EXPECTED FATAL ERROR =====") - _ = try expectThrow(try evalClosure.throws(c)) - print("======= END OF EXPECTED FATAL ERROR =======") - } - - do { - let c1 = JSClosure { arguments in - return arguments[0] - } - try expectEqual(evalClosure(c1, JSValue.number(1.0)), .number(1.0)) -#if JAVASCRIPTKIT_WITHOUT_WEAKREFS - c1.release() -#endif - } - - do { - let c1 = JSClosure { _ in .undefined } -#if JAVASCRIPTKIT_WITHOUT_WEAKREFS - c1.release() -#endif - } - - do { - let array = JSObject.global.Array.function!.new() - let c1 = JSClosure { _ in .number(3) } - _ = array.push!(c1) - try expectEqual(array[0].function!().number, 3.0) -#if JAVASCRIPTKIT_WITHOUT_WEAKREFS - c1.release() -#endif - } - -// do { -// let weakRef = { () -> JSObject in -// let c1 = JSClosure { _ in .undefined } -// return JSObject.global.WeakRef.function!.new(c1) -// }() -// -// // unsure if this will actually work since GC may not run immediately -// try expectEqual(weakRef.deref!(), .undefined) -// } - -#if JAVASCRIPTKIT_WITHOUT_WEAKREFS - do { - let c1 = JSOneshotClosure { _ in - return .boolean(true) - } - try expectEqual(evalClosure(c1), .boolean(true)) - // second call will cause `fatalError` that can be caught as a JavaScript exception - try expectCrashByCall(ofClosure: c1) - // OneshotClosure won't call fatalError even if it's deallocated before `release` - } -#endif -} - -try test("Host Function Registration") { - // ```js - // global.globalObject1 = { - // ... - // "prop_6": { - // "call_host_1": function() { - // return global.globalObject1.prop_6.host_func_1() - // } - // } - // } - // ``` - let globalObject1 = getJSValue(this: .global, name: "globalObject1") - let globalObject1Ref = try expectObject(globalObject1) - let prop_6 = getJSValue(this: globalObject1Ref, name: "prop_6") - let prop_6Ref = try expectObject(prop_6) - - 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 expectFunction(call_host_1) - try expectEqual(call_host_1Func(), .number(1)) - try expectEqual(isHostFunc1Called, true) - -#if JAVASCRIPTKIT_WITHOUT_WEAKREFS - hostFunc1.release() -#endif - - let hostFunc2 = JSClosure { (arguments) -> JSValue in - do { - let input = try expectNumber(arguments[0]) - return .number(input * 2) - } catch { - return .string(String(describing: error)) - } - } - - try expectEqual(evalClosure(hostFunc2, 3), .number(6)) - _ = try expectString(evalClosure(hostFunc2, true)) - -#if JAVASCRIPTKIT_WITHOUT_WEAKREFS - hostFunc2.release() -#endif -} - -try test("New Object Construction") { - // ```js - // global.Animal = function(name, age, isCat) { - // this.name = name - // this.age = age - // this.bark = () => { - // return isCat ? "nyan" : "wan" - // } - // } - // ``` - let objectConstructor = try expectFunction(getJSValue(this: .global, name: "Animal")) - let cat1 = objectConstructor.new("Tama", 3, true) - try expectEqual(getJSValue(this: cat1, name: "name"), .string("Tama")) - try expectEqual(getJSValue(this: cat1, name: "age"), .number(3)) - try expectEqual(cat1.isInstanceOf(objectConstructor), true) - try expectEqual(cat1.isInstanceOf(try expectFunction(getJSValue(this: .global, name: "Array"))), false) - let cat1Bark = try expectFunction(getJSValue(this: cat1, name: "bark")) - try expectEqual(cat1Bark(), .string("nyan")) - - let dog1 = objectConstructor.new("Pochi", 3, false) - let dog1Bark = try expectFunction(getJSValue(this: dog1, name: "bark")) - try expectEqual(dog1Bark(), .string("wan")) -} - -try test("Call Function With This") { - // ```js - // global.Animal = function(name, age, isCat) { - // this.name = name - // this.age = age - // this.bark = () => { - // return isCat ? "nyan" : "wan" - // } - // this.isCat = isCat - // this.getIsCat = function() { - // return this.isCat - // } - // } - // ``` - let objectConstructor = try expectFunction(getJSValue(this: .global, name: "Animal")) - let cat1 = objectConstructor.new("Tama", 3, true) - let cat1Value = JSValue.object(cat1) - let getIsCat = try expectFunction(getJSValue(this: cat1, name: "getIsCat")) - let setName = try expectFunction(getJSValue(this: cat1, name: "setName")) - - // Direct call without this - try expectEqual(getIsCat(), .undefined) - - // Call with this - let gotIsCat = getIsCat(this: cat1) - try expectEqual(gotIsCat, .boolean(true)) - try expectEqual(cat1.getIsCat!(), .boolean(true)) - try expectEqual(cat1Value.getIsCat(), .boolean(true)) - - // Call with this and argument - setName(this: cat1, JSValue.string("Shiro")) - try expectEqual(getJSValue(this: cat1, name: "name"), .string("Shiro")) - _ = cat1.setName!("Tora") - try expectEqual(getJSValue(this: cat1, name: "name"), .string("Tora")) - _ = cat1Value.setName("Chibi") - try expectEqual(getJSValue(this: cat1, name: "name"), .string("Chibi")) -} - -try test("Object Conversion") { - let array1 = [1, 2, 3] - let jsArray1 = array1.jsValue().object! - try expectEqual(jsArray1.length, .number(3)) - try expectEqual(jsArray1[0], .number(1)) - try expectEqual(jsArray1[1], .number(2)) - try expectEqual(jsArray1[2], .number(3)) - - let array2: [ConvertibleToJSValue] = [1, "str", false] - let jsArray2 = array2.jsValue().object! - try expectEqual(jsArray2.length, .number(3)) - try expectEqual(jsArray2[0], .number(1)) - try expectEqual(jsArray2[1], .string("str")) - try expectEqual(jsArray2[2], .boolean(false)) - _ = jsArray2.push!(5) - try expectEqual(jsArray2.length, .number(4)) - _ = jsArray2.push!(jsArray1) - - try expectEqual(jsArray2[4], .object(jsArray1)) - - let dict1: [String: ConvertibleToJSValue] = [ - "prop1": 1, - "prop2": "foo", - ] - let jsDict1 = dict1.jsValue().object! - try expectEqual(jsDict1.prop1, .number(1)) - try expectEqual(jsDict1.prop2, .string("foo")) -} - -try test("ObjectRef Lifetime") { - // ```js - // global.globalObject1 = { - // "prop_1": { - // "nested_prop": 1, - // }, - // "prop_2": 2, - // "prop_3": true, - // "prop_4": [ - // 3, 4, "str_elm_1", 5, - // ], - // ... - // } - // ``` - - let identity = JSClosure { $0[0] } - let ref1 = getJSValue(this: .global, name: "globalObject1").object! - let ref2 = evalClosure(identity, ref1).object! - try expectEqual(ref1.prop_2, .number(2)) - try expectEqual(ref2.prop_2, .number(2)) - -#if JAVASCRIPTKIT_WITHOUT_WEAKREFS - identity.release() -#endif -} - -#if JAVASCRIPTKIT_WITHOUT_WEAKREFS -func closureScope() -> ObjectIdentifier { - let closure = JSClosure { _ in .undefined } - let result = ObjectIdentifier(closure) - closure.release() - return result -} - -try test("Closure Identifiers") { - let oid1 = closureScope() - let oid2 = closureScope() - try expectEqual(oid1, oid2) -} -#endif - -func checkArray(_ array: [T]) throws where T: TypedArrayElement & Equatable { - try expectEqual(toString(JSTypedArray(array).jsValue().object!), jsStringify(array)) - try checkArrayUnsafeBytes(array) -} - -func toString(_ object: T) -> String { - return object.toString!().string! -} - -func jsStringify(_ array: [Any]) -> String { - array.map({ String(describing: $0) }).joined(separator: ",") -} - -func checkArrayUnsafeBytes(_ array: [T]) throws where T: TypedArrayElement & Equatable { - let copyOfArray: [T] = JSTypedArray(array).withUnsafeBytes { buffer in - Array(buffer) - } - try expectEqual(copyOfArray, array) -} - -try test("TypedArray") { - let numbers = [UInt8](0 ... 255) - let typedArray = JSTypedArray(numbers) - try expectEqual(typedArray[12], 12) - try expectEqual(numbers.count, typedArray.lengthInBytes) - - let numbersSet = Set(0 ... 255) - let typedArrayFromSet = JSTypedArray(numbersSet) - try expectEqual(typedArrayFromSet.jsObject.length, 256) - try expectEqual(typedArrayFromSet.lengthInBytes, 256 * MemoryLayout.size) - - try checkArray([0, .max, 127, 1] as [UInt8]) - try checkArray([0, 1, .max, .min, -1] as [Int8]) - - try checkArray([0, .max, 255, 1] as [UInt16]) - try checkArray([0, 1, .max, .min, -1] as [Int16]) - - try checkArray([0, .max, 255, 1] as [UInt32]) - try checkArray([0, 1, .max, .min, -1] as [Int32]) - - try checkArray([0, .max, 255, 1] as [UInt]) - try checkArray([0, 1, .max, .min, -1] as [Int]) - - let float32Array: [Float32] = [0, 1, .pi, .greatestFiniteMagnitude, .infinity, .leastNonzeroMagnitude, .leastNormalMagnitude, 42] - let jsFloat32Array = JSTypedArray(float32Array) - for (i, num) in float32Array.enumerated() { - try expectEqual(num, jsFloat32Array[i]) - } - - let float64Array: [Float64] = [0, 1, .pi, .greatestFiniteMagnitude, .infinity, .leastNonzeroMagnitude, .leastNormalMagnitude, 42] - let jsFloat64Array = JSTypedArray(float64Array) - for (i, num) in float64Array.enumerated() { - try expectEqual(num, jsFloat64Array[i]) - } -} - -try test("TypedArray_Mutation") { - let array = JSTypedArray(length: 100) - for i in 0..<100 { - array[i] = i - } - for i in 0..<100 { - try expectEqual(i, array[i]) - } - try expectEqual(toString(array.jsValue().object!), jsStringify(Array(0..<100))) -} - -try test("Date") { - let date1Milliseconds = JSDate.now() - let date1 = JSDate(millisecondsSinceEpoch: date1Milliseconds) - let date2 = JSDate(millisecondsSinceEpoch: date1.valueOf()) - - try expectEqual(date1.valueOf(), date2.valueOf()) - try expectEqual(date1.fullYear, date2.fullYear) - try expectEqual(date1.month, date2.month) - try expectEqual(date1.date, date2.date) - try expectEqual(date1.day, date2.day) - try expectEqual(date1.hours, date2.hours) - try expectEqual(date1.minutes, date2.minutes) - try expectEqual(date1.seconds, date2.seconds) - try expectEqual(date1.milliseconds, date2.milliseconds) - try expectEqual(date1.utcFullYear, date2.utcFullYear) - try expectEqual(date1.utcMonth, date2.utcMonth) - try expectEqual(date1.utcDate, date2.utcDate) - try expectEqual(date1.utcDay, date2.utcDay) - try expectEqual(date1.utcHours, date2.utcHours) - try expectEqual(date1.utcMinutes, date2.utcMinutes) - try expectEqual(date1.utcSeconds, date2.utcSeconds) - try expectEqual(date1.utcMilliseconds, date2.utcMilliseconds) - try expectEqual(date1, date2) - - let date3 = JSDate(millisecondsSinceEpoch: 0) - try expectEqual(date3.valueOf(), 0) - try expectEqual(date3.utcFullYear, 1970) - try expectEqual(date3.utcMonth, 0) - try expectEqual(date3.utcDate, 1) - // the epoch date was on Friday - try expectEqual(date3.utcDay, 4) - try expectEqual(date3.utcHours, 0) - try expectEqual(date3.utcMinutes, 0) - try expectEqual(date3.utcSeconds, 0) - try expectEqual(date3.utcMilliseconds, 0) - try expectEqual(date3.toISOString(), "1970-01-01T00:00:00.000Z") - - try expectEqual(date3 < date1, true) -} - -// make the timers global to prevent early deallocation -var timeouts: [JSTimer] = [] -var interval: JSTimer? - -try test("Timer") { - let start = JSDate().valueOf() - let timeoutMilliseconds = 5.0 - var timeout: JSTimer! - timeout = JSTimer(millisecondsDelay: timeoutMilliseconds, isRepeating: false) { - // verify that at least `timeoutMilliseconds` passed since the `timeout` timer started - try! expectEqual(start + timeoutMilliseconds <= JSDate().valueOf(), true) - } - timeouts += [timeout] - - timeout = JSTimer(millisecondsDelay: timeoutMilliseconds, isRepeating: false) { - fatalError("timer should be cancelled") - } - timeout = nil - - var count = 0.0 - let maxCount = 5.0 - interval = JSTimer(millisecondsDelay: 5, isRepeating: true) { - // ensure that JSTimer is living - try! expectNotNil(interval) - // verify that at least `timeoutMilliseconds * count` passed since the `timeout` - // timer started - try! expectEqual(start + timeoutMilliseconds * count <= JSDate().valueOf(), true) - - guard count < maxCount else { - // stop the timer after `maxCount` reached - interval = nil - return - } - - count += 1 - } -} - -var timer: JSTimer? -var expectations: [Expectation] = [] - -try test("Promise") { - - let p1 = JSPromise.resolve(JSValue.null) - let exp1 = Expectation(label: "Promise.then testcase", expectedFulfillmentCount: 4) - p1.then { value in - try! expectEqual(value, .null) - exp1.fulfill() - return JSValue.number(1.0) - } - .then { value in - try! expectEqual(value, .number(1.0)) - exp1.fulfill() - return JSPromise.resolve(JSValue.boolean(true)) - } - .then { value in - try! expectEqual(value, .boolean(true)) - exp1.fulfill() - return JSValue.undefined - } - .catch { err -> JSValue in - print(err.object!.stack.string!) - fatalError("Not fired due to no throw") - } - .finally { exp1.fulfill() } - - let exp2 = Expectation(label: "Promise.catch testcase", expectedFulfillmentCount: 4) - let p2 = JSPromise.reject(JSValue.boolean(false)) - p2.then { _ -> JSValue in - fatalError("Not fired due to no success") - } - .catch { reason in - try! expectEqual(reason, .boolean(false)) - exp2.fulfill() - return JSValue.boolean(true) - } - .then { value in - try! expectEqual(value, .boolean(true)) - exp2.fulfill() - return JSPromise.reject(JSValue.number(2.0)) - } - .catch { reason in - try! expectEqual(reason, .number(2.0)) - exp2.fulfill() - return JSValue.undefined - } - .finally { exp2.fulfill() } - - - let start = JSDate().valueOf() - let timeoutMilliseconds = 5.0 - let exp3 = Expectation(label: "Promise and Timer testcae", expectedFulfillmentCount: 2) - - let p3 = JSPromise { resolve in - timer = JSTimer(millisecondsDelay: timeoutMilliseconds) { - exp3.fulfill() - resolve(.success(.undefined)) - } - } - - p3.then { _ in - // verify that at least `timeoutMilliseconds` passed since the timer started - try! expectEqual(start + timeoutMilliseconds <= JSDate().valueOf(), true) - exp3.fulfill() - return JSValue.undefined - } - - let exp4 = Expectation(label: "Promise lifetime") - // Ensure that users don't need to manage JSPromise lifetime - JSPromise.resolve(JSValue.boolean(true)).then { _ in - exp4.fulfill() - return JSValue.undefined - } - expectations += [exp1, exp2, exp3, exp4] -} - -try test("Error") { - let message = "test error" - let expectedDescription = "Error: test error" - let error = JSError(message: message) - try expectEqual(error.name, "Error") - try expectEqual(error.message, message) - try expectEqual(error.description, expectedDescription) - try expectEqual(error.stack?.isEmpty, false) - try expectEqual(JSError(from: .string("error"))?.description, nil) - try expectEqual(JSError(from: .object(error.jsObject))?.description, expectedDescription) -} - -try test("JSValue accessor") { - var globalObject1 = JSObject.global.globalObject1 - try expectEqual(globalObject1.prop_1.nested_prop, .number(1)) - try expectEqual(globalObject1.object!.prop_1.object!.nested_prop, .number(1)) - - try expectEqual(globalObject1.prop_4[0], .number(3)) - try expectEqual(globalObject1.prop_4[1], .number(4)) - - globalObject1.prop_1.nested_prop = "bar" - try expectEqual(globalObject1.prop_1.nested_prop, .string("bar")) - - /* TODO: Fix https://github.com/swiftwasm/JavaScriptKit/issues/132 and un-comment this test - `nested` should not be set again to `target.nested` by `target.nested.prop = .number(1)` - - let observableObj = globalObject1.observable_obj.object! - observableObj.set_called = .boolean(false) - observableObj.target.nested.prop = .number(1) - try expectEqual(observableObj.set_called, .boolean(false)) - - */ -} - -try test("Exception") { - // ```js - // global.globalObject1 = { - // ... - // prop_9: { - // func1: function () { - // throw new Error(); - // }, - // func2: function () { - // throw "String Error"; - // }, - // func3: function () { - // throw 3.0 - // }, - // }, - // ... - // } - // ``` - // - - let globalObject1 = JSObject.global.globalObject1 - let prop_9: JSValue = globalObject1.prop_9 - - // MARK: Throwing method calls - let error1 = try expectThrow(try prop_9.object!.throwing.func1!()) - try expectEqual(error1 is JSValue, true) - let errorObject = JSError(from: error1 as! JSValue) - try expectNotNil(errorObject) - - let error2 = try expectThrow(try prop_9.object!.throwing.func2!()) - try expectEqual(error2 is JSValue, true) - let errorString = try expectString(error2 as! JSValue) - try expectEqual(errorString, "String Error") - - let error3 = try expectThrow(try prop_9.object!.throwing.func3!()) - try expectEqual(error3 is JSValue, true) - let errorNumber = try expectNumber(error3 as! JSValue) - try expectEqual(errorNumber, 3.0) - - // MARK: Simple function calls - let error4 = try expectThrow(try prop_9.func1.function!.throws()) - try expectEqual(error4 is JSValue, true) - let errorObject2 = JSError(from: error4 as! JSValue) - try expectNotNil(errorObject2) - - // MARK: Throwing constructor call - let Animal = JSObject.global.Animal.function! - _ = try Animal.throws.new("Tama", 3, true) - let ageError = try expectThrow(try Animal.throws.new("Tama", -3, true)) - try expectEqual(ageError is JSValue, true) - let errorObject3 = JSError(from: ageError as! JSValue) - try expectNotNil(errorObject3) -} - -/// If WebAssembly.Memory is not accessed correctly (i.e. creating a new view each time), -/// this test will fail with `TypeError: Cannot perform Construct on a detached ArrayBuffer`, -/// since asking to grow memory will detach the backing ArrayBuffer. -/// See https://github.com/swiftwasm/JavaScriptKit/pull/153 -try test("Grow Memory") { - let string = "Hello" - let jsString = JSValue.string(string) - growMemory(1) - try expectEqual(string, jsString.description) -} - -try test("Hashable Conformance") { - let globalObject1 = JSObject.global.console.object! - let globalObject2 = JSObject.global.console.object! - try expectEqual(globalObject1.hashValue, globalObject2.hashValue) - // These are 2 different objects in Swift referencing the same object in JavaScript - try expectNotEqual(ObjectIdentifier(globalObject1), ObjectIdentifier(globalObject2)) - - let sameObjectSet: Set = [globalObject1, globalObject2] - try expectEqual(sameObjectSet.count, 1) - - let objectConstructor = JSObject.global.Object.function! - let obj = objectConstructor.new() - obj.a = 1.jsValue() - let firstHash = obj.hashValue - obj.b = 2.jsValue() - let secondHash = obj.hashValue - try expectEqual(firstHash, secondHash) -} - -Expectation.wait(expectations) diff --git a/IntegrationTests/bin/benchmark-tests.js b/IntegrationTests/bin/benchmark-tests.js deleted file mode 100644 index daf5a5e1b..000000000 --- a/IntegrationTests/bin/benchmark-tests.js +++ /dev/null @@ -1,44 +0,0 @@ -const { startWasiTask } = require("../lib"); -const { performance } = require("perf_hooks"); - -global.benchmarkRunner = function (name, body) { - console.log(`Running '${name}' ...`); - const startTime = performance.now(); - body(5000); - const endTime = performance.now(); - console.log("done " + (endTime - startTime) + " ms"); -}; - -class JSBenchmark { - constructor(title) { - this.title = title; - } - testSuite(name, body) { - benchmarkRunner(`${this.title}/${name}`, (iteration) => { - for (let idx = 0; idx < iteration; idx++) { - body(); - } - }); - } -} - -const serialization = new JSBenchmark("Serialization"); -serialization.testSuite("Write JavaScript number directly", () => { - const jsNumber = 42; - const object = global; - for (let idx = 0; idx < 100; idx++) { - object["numberValue" + idx] = jsNumber; - } -}); - -serialization.testSuite("Write JavaScript string directly", () => { - const jsString = "Hello, world"; - const object = global; - for (let idx = 0; idx < 100; idx++) { - object["stringValue" + idx] = jsString; - } -}); - -startWasiTask("./dist/BenchmarkTests.wasm").catch((err) => { - console.log(err); -}); diff --git a/IntegrationTests/bin/concurrency-tests.js b/IntegrationTests/bin/concurrency-tests.js deleted file mode 100644 index 2d705761f..000000000 --- a/IntegrationTests/bin/concurrency-tests.js +++ /dev/null @@ -1,6 +0,0 @@ -const { startWasiTask } = require("../lib"); - -startWasiTask("./dist/ConcurrencyTests.wasm").catch((err) => { - console.log(err); - process.exit(1); -}); diff --git a/IntegrationTests/bin/primary-tests.js b/IntegrationTests/bin/primary-tests.js deleted file mode 100644 index 79c62776a..000000000 --- a/IntegrationTests/bin/primary-tests.js +++ /dev/null @@ -1,90 +0,0 @@ -global.globalObject1 = { - prop_1: { - nested_prop: 1, - }, - prop_2: 2, - prop_3: true, - prop_4: [3, 4, "str_elm_1", null, undefined, 5], - prop_5: { - func1: function () { - return; - }, - func2: function () { - return 1; - }, - func3: function (n) { - return n * 2; - }, - func4: function (a, b, c) { - return a + b + c; - }, - func5: function (x) { - return "Hello, " + x; - }, - func6: function (c, a, b) { - if (c) { - return a; - } else { - return b; - } - }, - }, - prop_6: { - call_host_1: () => { - return global.globalObject1.prop_6.host_func_1(); - }, - }, - prop_7: 3.14, - prop_8: [0, , 2, 3, , , 6], - prop_9: { - func1: function () { - throw new Error(); - }, - func2: function () { - throw "String Error"; - }, - func3: function () { - throw 3.0 - }, - }, - eval_closure: function (fn) { - return fn(arguments[1]) - }, - observable_obj: { - set_called: false, - target: new Proxy({ - nested: {} - }, { - set(target, key, value) { - global.globalObject1.observable_obj.set_called = true; - target[key] = value; - return true; - } - }) - }, -}; - -global.Animal = function (name, age, isCat) { - if (age < 0) { - throw new Error("Invalid age " + age); - } - this.name = name; - this.age = age; - this.bark = () => { - return isCat ? "nyan" : "wan"; - }; - this.isCat = isCat; - this.getIsCat = function () { - return this.isCat; - }; - this.setName = function (name) { - this.name = name; - } -}; - -const { startWasiTask } = require("../lib"); - -startWasiTask("./dist/PrimaryTests.wasm").catch((err) => { - console.log(err); - process.exit(1); -}); diff --git a/IntegrationTests/lib.js b/IntegrationTests/lib.js deleted file mode 100644 index ee0a6a33f..000000000 --- a/IntegrationTests/lib.js +++ /dev/null @@ -1,50 +0,0 @@ -const SwiftRuntime = require("javascript-kit-swift").SwiftRuntime; -const WASI = require("@wasmer/wasi").WASI; -const WasmFs = require("@wasmer/wasmfs").WasmFs; - -const promisify = require("util").promisify; -const fs = require("fs"); -const readFile = promisify(fs.readFile); - -const startWasiTask = async (wasmPath) => { - // Instantiate a new WASI Instance - const wasmFs = new WasmFs(); - // Output stdout and stderr to console - const originalWriteSync = wasmFs.fs.writeSync; - wasmFs.fs.writeSync = (fd, buffer, offset, length, position) => { - const text = new TextDecoder("utf-8").decode(buffer); - switch (fd) { - case 1: - console.log(text); - break; - case 2: - console.error(text); - break; - } - return originalWriteSync(fd, buffer, offset, length, position); - }; - let wasi = new WASI({ - args: [], - env: {}, - bindings: { - ...WASI.defaultBindings, - fs: wasmFs.fs, - }, - }); - - const swift = new SwiftRuntime(); - // Fetch our Wasm File - const wasmBinary = await readFile(wasmPath); - - // Instantiate the WebAssembly file - let { instance } = await WebAssembly.instantiate(wasmBinary, { - wasi_snapshot_preview1: wasi.wasiImport, - javascript_kit: swift.importObjects(), - }); - - swift.setInstance(instance); - // Start the WebAssembly WASI instance! - wasi.start(instance); -}; - -module.exports = { startWasiTask }; diff --git a/IntegrationTests/package-lock.json b/IntegrationTests/package-lock.json deleted file mode 100644 index f2e53f99f..000000000 --- a/IntegrationTests/package-lock.json +++ /dev/null @@ -1,395 +0,0 @@ -{ - "name": "IntegrationTests", - "lockfileVersion": 2, - "requires": true, - "packages": { - "": { - "dependencies": { - "@wasmer/wasi": "^0.12.0", - "@wasmer/wasmfs": "^0.12.0", - "javascript-kit-swift": "file:.." - } - }, - "..": { - "name": "javascript-kit-swift", - "version": "0.12.0", - "license": "MIT", - "devDependencies": { - "prettier": "2.1.2", - "typescript": "^4.4.2" - } - }, - "../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/@wasmer/wasi": { - "version": "0.12.0", - "integrity": "sha512-FJhLZKAfLWm/yjQI7eCRHNbA8ezmb7LSpUYFkHruZXs2mXk2+DaQtSElEtOoNrVQ4vApTyVaAd5/b7uEu8w6wQ==", - "dependencies": { - "browser-process-hrtime": "^1.0.0", - "buffer-es6": "^4.9.3", - "path-browserify": "^1.0.0", - "randomfill": "^1.0.4" - } - }, - "node_modules/@wasmer/wasmfs": { - "version": "0.12.0", - "integrity": "sha512-m1ftchyQ1DfSenm5XbbdGIpb6KJHH5z0gODo3IZr6lATkj4WXfX/UeBTZ0aG9YVShBp+kHLdUHvOkqjy6p/GWw==", - "dependencies": { - "memfs": "3.0.4", - "pako": "^1.0.11", - "tar-stream": "^2.1.0" - } - }, - "node_modules/base64-js": { - "version": "1.3.1", - "integrity": "sha512-mLQ4i2QO1ytvGWFWmcngKO//JXAQueZvwEKtjgQFM4jIK0kU+ytMfplL8j+n5mspOfjHwoAg+9yhb7BwAHm36g==" - }, - "node_modules/bl": { - "version": "4.0.3", - "integrity": "sha512-fs4G6/Hu4/EE+F75J8DuN/0IpQqNjAdC7aEQv7Qt8MHGUH7Ckv2MwTEEeN9QehD0pfIDkMI1bkHYkKy7xHyKIg==", - "dependencies": { - "buffer": "^5.5.0", - "inherits": "^2.0.4", - "readable-stream": "^3.4.0" - } - }, - "node_modules/browser-process-hrtime": { - "version": "1.0.0", - "integrity": "sha512-9o5UecI3GhkpM6DrXr69PblIuWxPKk9Y0jHBRhdocZ2y7YECBFCsHm79Pr3OyR2AvjhDkabFJaDJMYRazHgsow==" - }, - "node_modules/buffer": { - "version": "5.6.0", - "integrity": "sha512-/gDYp/UtU0eA1ys8bOs9J6a+E/KWIY+DZ+Q2WESNUA0jFRsJOc0SNUO6xJ5SGA1xueg3NL65W6s+NY5l9cunuw==", - "dependencies": { - "base64-js": "^1.0.2", - "ieee754": "^1.1.4" - } - }, - "node_modules/buffer-es6": { - "version": "4.9.3", - "integrity": "sha1-8mNHuC33b9N+GLy1KIxJcM/VxAQ=" - }, - "node_modules/end-of-stream": { - "version": "1.4.4", - "integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==", - "dependencies": { - "once": "^1.4.0" - } - }, - "node_modules/fast-extend": { - "version": "1.0.2", - "integrity": "sha512-XXA9RmlPatkFKUzqVZAFth18R4Wo+Xug/S+C7YlYA3xrXwfPlW3dqNwOb4hvQo7wZJ2cNDYhrYuPzVOfHy5/uQ==" - }, - "node_modules/fs-constants": { - "version": "1.0.0", - "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==" - }, - "node_modules/fs-monkey": { - "version": "0.3.3", - "integrity": "sha512-FNUvuTAJ3CqCQb5ELn+qCbGR/Zllhf2HtwsdAtBi59s1WeCjKMT81fHcSu7dwIskqGVK+MmOrb7VOBlq3/SItw==" - }, - "node_modules/ieee754": { - "version": "1.1.13", - "integrity": "sha512-4vf7I2LYV/HaWerSo3XmlMkp5eZ83i+/CDluXi/IGTs/O1sejBNhTtnxzmRZfvOUqj7lZjqHkeTvpgSFDlWZTg==" - }, - "node_modules/inherits": { - "version": "2.0.4", - "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" - }, - "node_modules/javascript-kit-swift": { - "resolved": "..", - "link": true - }, - "node_modules/memfs": { - "version": "3.0.4", - "integrity": "sha512-OcZEzwX9E5AoY8SXjuAvw0DbIAYwUzV/I236I8Pqvrlv7sL/Y0E9aRCon05DhaV8pg1b32uxj76RgW0s5xjHBA==", - "dependencies": { - "fast-extend": "1.0.2", - "fs-monkey": "0.3.3" - } - }, - "node_modules/once": { - "version": "1.4.0", - "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", - "dependencies": { - "wrappy": "1" - } - }, - "node_modules/pako": { - "version": "1.0.11", - "integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==" - }, - "node_modules/path-browserify": { - "version": "1.0.1", - "integrity": "sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==" - }, - "node_modules/randombytes": { - "version": "2.1.0", - "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==", - "dependencies": { - "safe-buffer": "^5.1.0" - } - }, - "node_modules/randomfill": { - "version": "1.0.4", - "integrity": "sha512-87lcbR8+MhcWcUiQ+9e+Rwx8MyR2P7qnt15ynUlbm3TU/fjbgz4GsvfSUDTemtCCtVCqb4ZcEFlyPNTh9bBTLw==", - "dependencies": { - "randombytes": "^2.0.5", - "safe-buffer": "^5.1.0" - } - }, - "node_modules/readable-stream": { - "version": "3.6.0", - "integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==", - "dependencies": { - "inherits": "^2.0.3", - "string_decoder": "^1.1.1", - "util-deprecate": "^1.0.1" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/safe-buffer": { - "version": "5.2.1", - "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ] - }, - "node_modules/string_decoder": { - "version": "1.3.0", - "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", - "dependencies": { - "safe-buffer": "~5.2.0" - } - }, - "node_modules/tar-stream": { - "version": "2.1.4", - "integrity": "sha512-o3pS2zlG4gxr67GmFYBLlq+dM8gyRGUOvsrHclSkvtVtQbjV0s/+ZE8OpICbaj8clrX3tjeHngYGP7rweaBnuw==", - "dependencies": { - "bl": "^4.0.3", - "end-of-stream": "^1.4.1", - "fs-constants": "^1.0.0", - "inherits": "^2.0.3", - "readable-stream": "^3.1.1" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/util-deprecate": { - "version": "1.0.2", - "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=" - }, - "node_modules/wrappy": { - "version": "1.0.2", - "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=" - } - }, - "dependencies": { - "@wasmer/wasi": { - "version": "0.12.0", - "integrity": "sha512-FJhLZKAfLWm/yjQI7eCRHNbA8ezmb7LSpUYFkHruZXs2mXk2+DaQtSElEtOoNrVQ4vApTyVaAd5/b7uEu8w6wQ==", - "requires": { - "browser-process-hrtime": "^1.0.0", - "buffer-es6": "^4.9.3", - "path-browserify": "^1.0.0", - "randomfill": "^1.0.4" - } - }, - "@wasmer/wasmfs": { - "version": "0.12.0", - "integrity": "sha512-m1ftchyQ1DfSenm5XbbdGIpb6KJHH5z0gODo3IZr6lATkj4WXfX/UeBTZ0aG9YVShBp+kHLdUHvOkqjy6p/GWw==", - "requires": { - "memfs": "3.0.4", - "pako": "^1.0.11", - "tar-stream": "^2.1.0" - } - }, - "base64-js": { - "version": "1.3.1", - "integrity": "sha512-mLQ4i2QO1ytvGWFWmcngKO//JXAQueZvwEKtjgQFM4jIK0kU+ytMfplL8j+n5mspOfjHwoAg+9yhb7BwAHm36g==" - }, - "bl": { - "version": "4.0.3", - "integrity": "sha512-fs4G6/Hu4/EE+F75J8DuN/0IpQqNjAdC7aEQv7Qt8MHGUH7Ckv2MwTEEeN9QehD0pfIDkMI1bkHYkKy7xHyKIg==", - "requires": { - "buffer": "^5.5.0", - "inherits": "^2.0.4", - "readable-stream": "^3.4.0" - } - }, - "browser-process-hrtime": { - "version": "1.0.0", - "integrity": "sha512-9o5UecI3GhkpM6DrXr69PblIuWxPKk9Y0jHBRhdocZ2y7YECBFCsHm79Pr3OyR2AvjhDkabFJaDJMYRazHgsow==" - }, - "buffer": { - "version": "5.6.0", - "integrity": "sha512-/gDYp/UtU0eA1ys8bOs9J6a+E/KWIY+DZ+Q2WESNUA0jFRsJOc0SNUO6xJ5SGA1xueg3NL65W6s+NY5l9cunuw==", - "requires": { - "base64-js": "^1.0.2", - "ieee754": "^1.1.4" - } - }, - "buffer-es6": { - "version": "4.9.3", - "integrity": "sha1-8mNHuC33b9N+GLy1KIxJcM/VxAQ=" - }, - "end-of-stream": { - "version": "1.4.4", - "integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==", - "requires": { - "once": "^1.4.0" - } - }, - "fast-extend": { - "version": "1.0.2", - "integrity": "sha512-XXA9RmlPatkFKUzqVZAFth18R4Wo+Xug/S+C7YlYA3xrXwfPlW3dqNwOb4hvQo7wZJ2cNDYhrYuPzVOfHy5/uQ==" - }, - "fs-constants": { - "version": "1.0.0", - "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==" - }, - "fs-monkey": { - "version": "0.3.3", - "integrity": "sha512-FNUvuTAJ3CqCQb5ELn+qCbGR/Zllhf2HtwsdAtBi59s1WeCjKMT81fHcSu7dwIskqGVK+MmOrb7VOBlq3/SItw==" - }, - "ieee754": { - "version": "1.1.13", - "integrity": "sha512-4vf7I2LYV/HaWerSo3XmlMkp5eZ83i+/CDluXi/IGTs/O1sejBNhTtnxzmRZfvOUqj7lZjqHkeTvpgSFDlWZTg==" - }, - "inherits": { - "version": "2.0.4", - "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" - }, - "javascript-kit-swift": { - "version": "file:..", - "requires": { - "prettier": "2.1.2", - "typescript": "^4.4.2" - }, - "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 - } - } - }, - "memfs": { - "version": "3.0.4", - "integrity": "sha512-OcZEzwX9E5AoY8SXjuAvw0DbIAYwUzV/I236I8Pqvrlv7sL/Y0E9aRCon05DhaV8pg1b32uxj76RgW0s5xjHBA==", - "requires": { - "fast-extend": "1.0.2", - "fs-monkey": "0.3.3" - } - }, - "once": { - "version": "1.4.0", - "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", - "requires": { - "wrappy": "1" - } - }, - "pako": { - "version": "1.0.11", - "integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==" - }, - "path-browserify": { - "version": "1.0.1", - "integrity": "sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==" - }, - "randombytes": { - "version": "2.1.0", - "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==", - "requires": { - "safe-buffer": "^5.1.0" - } - }, - "randomfill": { - "version": "1.0.4", - "integrity": "sha512-87lcbR8+MhcWcUiQ+9e+Rwx8MyR2P7qnt15ynUlbm3TU/fjbgz4GsvfSUDTemtCCtVCqb4ZcEFlyPNTh9bBTLw==", - "requires": { - "randombytes": "^2.0.5", - "safe-buffer": "^5.1.0" - } - }, - "readable-stream": { - "version": "3.6.0", - "integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==", - "requires": { - "inherits": "^2.0.3", - "string_decoder": "^1.1.1", - "util-deprecate": "^1.0.1" - } - }, - "safe-buffer": { - "version": "5.2.1", - "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==" - }, - "string_decoder": { - "version": "1.3.0", - "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", - "requires": { - "safe-buffer": "~5.2.0" - } - }, - "tar-stream": { - "version": "2.1.4", - "integrity": "sha512-o3pS2zlG4gxr67GmFYBLlq+dM8gyRGUOvsrHclSkvtVtQbjV0s/+ZE8OpICbaj8clrX3tjeHngYGP7rweaBnuw==", - "requires": { - "bl": "^4.0.3", - "end-of-stream": "^1.4.1", - "fs-constants": "^1.0.0", - "inherits": "^2.0.3", - "readable-stream": "^3.1.1" - } - }, - "util-deprecate": { - "version": "1.0.2", - "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=" - }, - "wrappy": { - "version": "1.0.2", - "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=" - } - } -} diff --git a/IntegrationTests/package.json b/IntegrationTests/package.json deleted file mode 100644 index 678ecec49..000000000 --- a/IntegrationTests/package.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "private": true, - "dependencies": { - "@wasmer/wasi": "^0.12.0", - "@wasmer/wasmfs": "^0.12.0", - "javascript-kit-swift": "file:.." - } -} diff --git a/Makefile b/Makefile index b48af9e2b..e2aef5f8d 100644 --- a/Makefile +++ b/Makefile @@ -1,31 +1,23 @@ -MAKEFILE_DIR := $(dir $(lastword $(MAKEFILE_LIST))) +SWIFT_SDK_ID ?= wasm32-unknown-wasi .PHONY: bootstrap bootstrap: - ./scripts/install-toolchain.sh npm ci + npx playwright install -.PHONY: build -build: - swift build --triple wasm32-unknown-wasi - npm run build - -.PHONY: test -test: - cd IntegrationTests && \ - CONFIGURATION=debug make test && \ - CONFIGURATION=debug SWIFT_BUILD_FLAGS="-Xswiftc -DJAVASCRIPTKIT_WITHOUT_WEAKREFS" make test && \ - CONFIGURATION=release make test && \ - CONFIGURATION=release SWIFT_BUILD_FLAGS="-Xswiftc -DJAVASCRIPTKIT_WITHOUT_WEAKREFS" make test - -.PHONY: benchmark_setup -benchmark_setup: - cd IntegrationTests && make benchmark_setup +.PHONY: unittest +unittest: + @echo Running unit tests + env JAVASCRIPTKIT_EXPERIMENTAL_BRIDGEJS=1 swift package --swift-sdk "$(SWIFT_SDK_ID)" \ + --disable-sandbox \ + -Xlinker --stack-first \ + -Xlinker --global-base=524288 \ + -Xlinker -z \ + -Xlinker stack-size=524288 \ + js test --prelude ./Tests/prelude.mjs -.PHONY: run_benchmark -run_benchmark: - cd IntegrationTests && make -s 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.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 8a75bed39..3657bfa99 100644 --- a/Package.swift +++ b/Package.swift @@ -1,23 +1,150 @@ -// swift-tools-version:5.2 +// 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 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 + ? [ + .unsafeFlags(["-fdeclspec"]) + ] : nil, + swiftSettings: shouldBuildForEmbedded + ? [ + .enableExperimentalFeature("Embedded"), + .enableExperimentalFeature("Extern"), + .unsafeFlags(["-Xfrontend", "-emit-empty-object-file"]), + ] : nil ), .target(name: "_CJavaScriptKit"), + .testTarget( + name: "JavaScriptKitTests", + dependencies: ["JavaScriptKit"], + swiftSettings: [ + .enableExperimentalFeature("Extern") + ] + ), + + .target( + name: "JavaScriptBigIntSupport", + dependencies: ["_CJavaScriptBigIntSupport", "JavaScriptKit"], + swiftSettings: shouldBuildForEmbedded + ? [ + .enableExperimentalFeature("Embedded"), + .unsafeFlags(["-Xfrontend", "-emit-empty-object-file"]), + ] : [] + ), + .target(name: "_CJavaScriptBigIntSupport", dependencies: ["_CJavaScriptKit"]), + .testTarget( + name: "JavaScriptBigIntSupportTests", + dependencies: ["JavaScriptBigIntSupport", "JavaScriptKit"] + ), + .target( name: "JavaScriptEventLoop", - dependencies: ["JavaScriptKit", "_CJavaScriptEventLoop"] + dependencies: ["JavaScriptKit", "_CJavaScriptEventLoop"], + swiftSettings: shouldBuildForEmbedded + ? [ + .enableExperimentalFeature("Embedded"), + .unsafeFlags(["-Xfrontend", "-emit-empty-object-file"]), + ] : [] ), .target(name: "_CJavaScriptEventLoop"), + .testTarget( + name: "JavaScriptEventLoopTests", + dependencies: [ + "JavaScriptEventLoop", + "JavaScriptKit", + "JavaScriptEventLoopTestSupport", + ], + swiftSettings: [ + .enableExperimentalFeature("Extern") + ] + ), + .target( + name: "JavaScriptEventLoopTestSupport", + dependencies: [ + "_CJavaScriptEventLoopTestSupport", + "JavaScriptEventLoop", + ] + ), + .target(name: "_CJavaScriptEventLoopTestSupport"), + .testTarget( + name: "JavaScriptEventLoopTestSupportTests", + dependencies: [ + "JavaScriptKit", + "JavaScriptEventLoopTestSupport", + ] + ), + .plugin( + name: "PackageToJS", + capability: .command( + intent: .custom(verb: "js", description: "Convert a Swift package to a JavaScript package") + ), + 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..bef43bbca --- /dev/null +++ b/Plugins/BridgeJS/Sources/BridgeJSTool/ExportSwift.swift @@ -0,0 +1,599 @@ +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 + 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/Package.swift b/Plugins/PackageToJS/Package.swift new file mode 100644 index 000000000..57ccf3cf9 --- /dev/null +++ b/Plugins/PackageToJS/Package.swift @@ -0,0 +1,16 @@ +// swift-tools-version: 6.0 + +import PackageDescription + +let package = Package( + name: "PackageToJS", + platforms: [.macOS(.v13)], + targets: [ + .target(name: "PackageToJS"), + .testTarget( + name: "PackageToJSTests", + dependencies: ["PackageToJS"], + exclude: ["__Snapshots__"] + ), + ] +) diff --git a/Plugins/PackageToJS/README.md b/Plugins/PackageToJS/README.md new file mode 100644 index 000000000..8b1821187 --- /dev/null +++ b/Plugins/PackageToJS/README.md @@ -0,0 +1,45 @@ +# PackageToJS + +A Swift Package Manager plugin that facilitates building and packaging Swift WebAssembly applications for JavaScript environments. + +## Overview + +PackageToJS is a command plugin for Swift Package Manager that simplifies the process of compiling Swift code to WebAssembly and generating the necessary JavaScript bindings. It's an essential tool for SwiftWasm projects, especially those using JavaScriptKit to interact with JavaScript from Swift. + +## Features + +- Build WebAssembly file and generate JavaScript wrappers +- Test driver for Swift Testing and XCTest +- Generated JS files can be consumed by JS bundler tools like Vite + +## Requirements + +- Swift 6.0 or later +- A compatible WebAssembly SDK + +## Relationship with Carton + +PackageToJS is intended to replace Carton by providing a more integrated solution for building and packaging Swift WebAssembly applications. Unlike Carton, which offers a development server and hot-reloading, PackageToJS focuses solely on compilation and JavaScript wrapper generation. + +## Internal Architecture + +PackageToJS consists of several components: +- `PackageToJSPlugin.swift`: Main entry point for the Swift Package Manager plugin (Note that this file is not included when running unit tests for the plugin) +- `PackageToJS.swift`: Core functionality for building and packaging +- `MiniMake.swift`: Build system utilities +- `ParseWasm.swift`: WebAssembly binary parsing +- `Preprocess.swift`: Preprocessor for `./Templates` files + +## Internal Testing + +To run the unit tests for the `PackageToJS` plugin, use the following command: + +```bash +swift test --package-path ./Plugins/PackageToJS +``` + +Please define the following environment variables when you want to run E2E tests: + +- `SWIFT_SDK_ID`: Specifies the Swift SDK identifier to use +- `SWIFT_PATH`: Specifies the `bin` path to the Swift toolchain to use + 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 new file mode 100644 index 000000000..0004af6c0 --- /dev/null +++ b/Plugins/PackageToJS/Sources/MiniMake.swift @@ -0,0 +1,339 @@ +import Foundation + +/// A minimal build system +/// +/// This build system is a traditional mtime-based incremental build system. +struct MiniMake { + /// Attributes of a task + enum TaskAttribute: String, Codable { + /// Task is phony, meaning it must be built even if its inputs are up to date + case phony + /// Don't print anything when building this task + case silent + } + + /// Information about a task enough to capture build + /// graph changes + struct TaskInfo: Encodable { + /// Input tasks not yet built + let wants: [TaskKey] + /// Set of file paths that must be built before this task + let inputs: [BuildPath] + /// Output file path + let output: BuildPath + /// Attributes of the task + let attributes: [TaskAttribute] + /// Salt for the task, used to differentiate between otherwise identical tasks + var salt: Data? + } + + /// A task to build + struct Task { + let info: TaskInfo + /// Input tasks (files and phony tasks) not yet built + let wants: Set + /// Attributes of the task + let attributes: Set + /// Key of the task + let key: TaskKey + /// Build operation + let build: (_ task: Task, _ scope: VariableScope) throws -> Void + /// Whether the task is done + var isDone: Bool + + var inputs: [BuildPath] { self.info.inputs } + var output: BuildPath { self.info.output } + } + + /// A task key + struct TaskKey: Encodable, Hashable, Comparable, CustomStringConvertible { + let id: String + var description: String { self.id } + + fileprivate init(id: String) { + self.id = id + } + + func encode(to encoder: any Encoder) throws { + var container = encoder.singleValueContainer() + try container.encode(self.id) + } + + static func < (lhs: TaskKey, rhs: TaskKey) -> Bool { lhs.id < rhs.id } + } + + struct VariableScope { + let variables: [String: String] + + func resolve(path: BuildPath) -> URL { + var components = [String]() + for component in path.components { + switch component { + case .prefix(let variable): + guard let value = variables[variable] else { + fatalError("Build path variable \"\(variable)\" not defined!") + } + components.append(value) + case .constant(let path): + components.append(path) + } + } + guard let first = components.first else { + fatalError("Build path is empty") + } + var url = URL(fileURLWithPath: first) + for component in components.dropFirst() { + url = url.appending(path: component) + } + return url + } + } + + /// All tasks in the build system + private var tasks: [TaskKey: Task] + /// Whether to explain why tasks are built + private var shouldExplain: Bool + /// Prints progress of the build + private var printProgress: ProgressPrinter.PrintProgress + + init( + explain: Bool = false, + printProgress: @escaping ProgressPrinter.PrintProgress + ) { + self.tasks = [:] + self.shouldExplain = explain + self.printProgress = printProgress + } + + /// Adds a task to the build system + mutating func addTask( + 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) + let saltData = try! salt.map { + let encoder = JSONEncoder() + encoder.outputFormatting = .sortedKeys + return try encoder.encode($0) + } + let info = TaskInfo( + 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 + ) + return taskKey + } + + /// Computes a stable fingerprint of the build graph + /// + /// This fingerprint must be stable across builds and must change + /// if the build graph changes in any way. + func computeFingerprint(root: TaskKey, prettyPrint: Bool = false) throws -> Data { + let encoder = JSONEncoder() + encoder.outputFormatting = .sortedKeys + if prettyPrint { + encoder.outputFormatting.insert(.prettyPrinted) + } + let tasks = self.tasks.sorted { $0.key < $1.key }.map { $0.value.info } + return try encoder.encode(tasks) + } + + private func explain(_ message: @autoclosure () -> String) { + if self.shouldExplain { + print(message()) + } + } + + private func violated(_ message: @autoclosure () -> String) { + print(message()) + } + + /// Prints progress of the build + struct ProgressPrinter { + struct Context { + let subject: Task + let total: Int + let built: Int + let scope: VariableScope + } + typealias PrintProgress = (_ context: Context, _ message: String) -> Void + + /// Total number of tasks to build + let total: Int + /// Number of tasks built so far + var built: Int + /// Prints progress of the build + var printProgress: PrintProgress + + init(total: Int, printProgress: @escaping PrintProgress) { + self.total = total + self.built = 0 + self.printProgress = printProgress + } + + private static var green: String { "\u{001B}[32m" } + private static var yellow: String { "\u{001B}[33m" } + private static var reset: String { "\u{001B}[0m" } + + mutating func started(_ task: Task, scope: VariableScope) { + self.print(task, scope, "\(Self.green)building\(Self.reset)") + } + + mutating func skipped(_ task: Task, scope: VariableScope) { + self.print(task, scope, "\(Self.yellow)skipped\(Self.reset)") + } + + private mutating func print(_ task: Task, _ scope: VariableScope, _ message: @autoclosure () -> String) { + guard !task.attributes.contains(.silent) else { return } + self.printProgress(Context(subject: task, total: self.total, built: self.built, scope: scope), message()) + self.built += 1 + } + } + + /// Computes the total number of tasks to build used for progress display + private func computeTotalTasksForDisplay(task: Task) -> Int { + var visited = Set() + func visit(task: Task) -> Int { + guard !visited.contains(task.key) else { return 0 } + visited.insert(task.key) + var total = task.attributes.contains(.silent) ? 0 : 1 + for want in task.wants { + total += visit(task: self.tasks[want]!) + } + return total + } + return visit(task: task) + } + + /// Cleans all outputs of all tasks + func cleanEverything(scope: VariableScope) { + for task in self.tasks.values { + try? FileManager.default.removeItem(at: scope.resolve(path: task.output)) + } + } + + /// Starts building + func build(output: TaskKey, scope: VariableScope) throws { + /// Returns true if any of the task's inputs have a modification date later than the task's output + func shouldBuild(task: Task) -> Bool { + if task.attributes.contains(.phony) { + return true + } + let outputURL = scope.resolve(path: task.output) + if !FileManager.default.fileExists(atPath: outputURL.path) { + explain("Task \(task.output) should be built because it doesn't exist") + return true + } + let outputMtime = try? outputURL.resourceValues(forKeys: [.contentModificationDateKey]) + .contentModificationDate + return task.inputs.contains { input in + let inputURL = scope.resolve(path: input) + // Ignore directory modification times + var isDirectory: ObjCBool = false + let fileExists = FileManager.default.fileExists( + atPath: inputURL.path, + isDirectory: &isDirectory + ) + if fileExists && isDirectory.boolValue { + return false + } + + let inputMtime = try? inputURL.resourceValues(forKeys: [.contentModificationDateKey] + ).contentModificationDate + let shouldBuild = + outputMtime == nil || inputMtime == nil || outputMtime! < inputMtime! + if shouldBuild { + explain( + "Task \(task.output) should be re-built because \(input) is newer: \(outputMtime?.timeIntervalSince1970 ?? 0) < \(inputMtime?.timeIntervalSince1970 ?? 0)" + ) + } + return shouldBuild + } + } + var progressPrinter = ProgressPrinter( + total: self.computeTotalTasksForDisplay(task: self.tasks[output]!), + printProgress: self.printProgress + ) + // Make a copy of the tasks so we can mutate the state + var tasks = self.tasks + + func runTask(taskKey: TaskKey) throws { + guard var task = tasks[taskKey] else { + violated("Task \(taskKey) not found") + return + } + guard !task.isDone else { return } + + // Build dependencies first + for want in task.wants.sorted() { + try runTask(taskKey: want) + } + + if shouldBuild(task: task) { + progressPrinter.started(task, scope: scope) + try task.build(task, scope) + } else { + progressPrinter.skipped(task, scope: scope) + } + task.isDone = true + tasks[taskKey] = task + } + try runTask(taskKey: output) + } +} + +struct BuildPath: Encodable, Hashable, CustomStringConvertible { + enum Component: Hashable, CustomStringConvertible { + case prefix(variable: String) + case constant(String) + + var description: String { + switch self { + case .prefix(let variable): return "$\(variable)" + case .constant(let path): return path + } + } + } + fileprivate let components: [Component] + + var description: String { self.components.map(\.description).joined(separator: "/") } + + init(phony: String) { + self.components = [.constant(phony)] + } + + init(prefix: String, _ tail: String...) { + self.components = [.prefix(variable: prefix)] + tail.map(Component.constant) + } + + init(absolute: String) { + self.components = [.constant(absolute)] + } + + private init(components: [Component]) { + self.components = components + } + + func appending(path: String) -> BuildPath { + return BuildPath(components: self.components + [.constant(path)]) + } + + func encode(to encoder: any Encoder) throws { + var container = encoder.singleValueContainer() + try container.encode(self.description) + } +} diff --git a/Plugins/PackageToJS/Sources/PackageToJS.swift b/Plugins/PackageToJS/Sources/PackageToJS.swift new file mode 100644 index 000000000..2b8b4458a --- /dev/null +++ b/Plugins/PackageToJS/Sources/PackageToJS.swift @@ -0,0 +1,779 @@ +import Foundation + +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) + var explain: Bool = false + /// Whether to print verbose output + var verbose: Bool = false + /// Whether to use CDN for dependency packages (default: false) + var useCDN: Bool = false + /// Whether to enable code coverage collection (default: false) + var enableCodeCoverage: Bool = false + } + + enum DebugInfoFormat: String, CaseIterable { + /// No debug info + case none + /// The all DWARF sections and "name" section + case dwarf + /// Only "name" section + case name + } + + struct BuildOptions { + /// Product to build (default: executable target if there's only one) + var product: String? + /// Whether to apply wasm-opt optimizations in release mode (default: true) + var noOptimize: Bool + /// The format of debug info to keep in the final wasm file (default: none) + var debugInfoFormat: DebugInfoFormat + /// The options for packaging + var packageOptions: PackageOptions + } + + struct TestOptions { + /// Whether to only build tests, don't run them + var buildOnly: Bool + /// Lists all tests + var listTests: Bool + /// The filter to apply to the tests + var filter: [String] + /// The prelude script to use for the tests + var prelude: String? + /// The environment to use for the tests + var environment: String? + /// Whether to run tests in the browser with inspector enabled + var inspect: Bool + /// The extra arguments to pass to node + var extraNodeArguments: [String] + /// The options for packaging + var packageOptions: PackageOptions + } + + static func deriveBuildConfiguration(wasmProductArtifact: URL) -> (configuration: String, triple: String) { + // e.g. path/to/.build/wasm32-unknown-wasi/debug/Basic.wasm -> ("debug", "wasm32-unknown-wasi") + + // First, resolve symlink to get the actual path as SwiftPM 6.0 and earlier returns unresolved + // symlink path for product artifact. + let wasmProductArtifact = wasmProductArtifact.resolvingSymlinksInPath() + let buildConfiguration = wasmProductArtifact.deletingLastPathComponent().lastPathComponent + let triple = wasmProductArtifact.deletingLastPathComponent().deletingLastPathComponent().lastPathComponent + return (buildConfiguration, triple) + } + + static func runTest(testRunner: URL, currentDirectoryURL: URL, outputDir: URL, testOptions: TestOptions) throws { + var testJsArguments: [String] = [] + var testLibraryArguments: [String] = [] + if testOptions.listTests { + testLibraryArguments.append("--list-tests") + } + if let prelude = testOptions.prelude { + let preludeURL = URL( + fileURLWithPath: prelude, + relativeTo: URL(fileURLWithPath: FileManager.default.currentDirectoryPath) + ) + testJsArguments.append("--prelude") + testJsArguments.append(preludeURL.path) + } + if let environment = testOptions.environment { + testJsArguments.append("--environment") + testJsArguments.append(environment) + } + if testOptions.inspect { + testJsArguments.append("--inspect") + } + + let xctestCoverageFile = outputDir.appending(path: "XCTest.profraw") + do { + var extraArguments = testJsArguments + if testOptions.packageOptions.enableCodeCoverage { + extraArguments.append("--coverage-file") + extraArguments.append(xctestCoverageFile.path) + } + extraArguments.append("--") + extraArguments.append(contentsOf: testLibraryArguments) + extraArguments.append(contentsOf: testOptions.filter) + + try PackageToJS.runSingleTestingLibrary( + testRunner: testRunner, + currentDirectoryURL: currentDirectoryURL, + extraArguments: extraArguments, + testParser: testOptions.packageOptions.verbose + ? nil : FancyTestsParser(write: { print($0, terminator: "") }), + testOptions: testOptions + ) + } + let swiftTestingCoverageFile = outputDir.appending(path: "SwiftTesting.profraw") + do { + var extraArguments = testJsArguments + if testOptions.packageOptions.enableCodeCoverage { + extraArguments.append("--coverage-file") + extraArguments.append(swiftTestingCoverageFile.path) + } + extraArguments.append("--") + extraArguments.append("--testing-library") + extraArguments.append("swift-testing") + extraArguments.append(contentsOf: testLibraryArguments) + for filter in testOptions.filter { + extraArguments.append("--filter") + extraArguments.append(filter) + } + + try PackageToJS.runSingleTestingLibrary( + testRunner: testRunner, + currentDirectoryURL: currentDirectoryURL, + extraArguments: extraArguments, + testOptions: testOptions + ) + } + + if testOptions.packageOptions.enableCodeCoverage { + let profrawFiles = [xctestCoverageFile.path, swiftTestingCoverageFile.path].filter { + FileManager.default.fileExists(atPath: $0) + } + do { + try PackageToJS.postProcessCoverageFiles(outputDir: outputDir, profrawFiles: profrawFiles) + } catch { + print("Warning: Failed to merge coverage files: \(error)") + } + } + } + + static func runSingleTestingLibrary( + testRunner: URL, + currentDirectoryURL: URL, + extraArguments: [String], + testParser: FancyTestsParser? = nil, + testOptions: TestOptions + ) throws { + let node = try which("node") + var arguments = ["--experimental-wasi-unstable-preview1"] + arguments.append(contentsOf: testOptions.extraNodeArguments) + arguments.append(testRunner.path) + arguments.append(contentsOf: extraArguments) + + print("Running test...") + logCommandExecution(node.path, arguments) + + let task = Process() + task.executableURL = node + task.arguments = arguments + + var finalize: () -> Void = {} + if let testParser = testParser { + let stdoutBuffer = LineBuffer { line in + testParser.onLine(line) + } + let stdoutPipe = Pipe() + stdoutPipe.fileHandleForReading.readabilityHandler = { handle in + stdoutBuffer.append(handle.availableData) + } + task.standardOutput = stdoutPipe + finalize = { + if let data = try? stdoutPipe.fileHandleForReading.readToEnd() { + stdoutBuffer.append(data) + } + stdoutBuffer.flush() + testParser.finalize() + } + } + + task.currentDirectoryURL = currentDirectoryURL + try task.forwardTerminationSignals { + try task.run() + task.waitUntilExit() + } + finalize() + // swift-testing returns EX_UNAVAILABLE (which is 69 in wasi-libc) for "no tests found" + guard [0, 69].contains(task.terminationStatus) else { + throw PackageToJSError("Test failed with status \(task.terminationStatus)") + } + } + + static func postProcessCoverageFiles(outputDir: URL, profrawFiles: [String]) throws { + let mergedCoverageFile = outputDir.appending(path: "default.profdata") + do { + // Merge the coverage files by llvm-profdata + let arguments = ["merge", "-sparse", "-output", mergedCoverageFile.path] + profrawFiles + let llvmProfdata = try which("llvm-profdata") + logCommandExecution(llvmProfdata.path, arguments) + try runCommand(llvmProfdata, arguments) + print("Saved profile data to \(mergedCoverageFile.path)") + } + } + + class LineBuffer: @unchecked Sendable { + let lock = NSLock() + var buffer = "" + let handler: (String) -> Void + + init(handler: @escaping (String) -> Void) { + self.handler = handler + } + + func append(_ data: Data) { + let string = String(data: data, encoding: .utf8) ?? "" + append(string) + } + + func append(_ data: String) { + lock.lock() + defer { lock.unlock() } + buffer.append(data) + let lines = buffer.split(separator: "\n", omittingEmptySubsequences: false) + for line in lines.dropLast() { + handler(String(line)) + } + buffer = String(lines.last ?? "") + } + + func flush() { + lock.lock() + defer { lock.unlock() } + handler(buffer) + buffer = "" + } + } +} + +struct PackageToJSError: Swift.Error, CustomStringConvertible { + let description: String + + init(_ message: String) { + self.description = "Error: " + message + } +} + +protocol PackagingSystem { + func createDirectory(atPath: String) throws + func syncFile(from: String, to: String) throws + func writeFile(atPath: String, content: Data) throws + + func wasmOpt(_ arguments: [String], input: String, output: String) throws + func npmInstall(packageDir: String) throws +} + +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 + ) + } + + func syncFile(from: String, to: String) throws { + if FileManager.default.fileExists(atPath: to) { + try FileManager.default.removeItem(atPath: to) + } + try FileManager.default.copyItem(atPath: from, toPath: to) + try FileManager.default.setAttributes( + [.modificationDate: Date()], + ofItemAtPath: to + ) + } + + func writeFile(atPath: String, content: Data) throws { + do { + try content.write(to: URL(fileURLWithPath: atPath)) + } catch { + throw PackageToJSError("Failed to write file \(atPath): \(error)") + } + } +} + +final class DefaultPackagingSystem: PackagingSystem { + + private let printWarning: (String) -> Void + + init(printWarning: @escaping (String) -> Void) { + self.printWarning = printWarning + } + + func npmInstall(packageDir: String) throws { + try runCommand(try which("npm"), ["-C", packageDir, "install"]) + } + + lazy var warnMissingWasmOpt: () = { + self.printWarning("Warning: wasm-opt is not installed, optimizations will not be applied") + }() + + func wasmOpt(_ arguments: [String], input: String, output: String) throws { + guard let wasmOpt = try? which("wasm-opt") else { + _ = warnMissingWasmOpt + try FileManager.default.copyItem(atPath: input, toPath: output) + return + } + try runCommand(wasmOpt, arguments + ["-o", output, input]) + } +} + +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 PackageToJSError("Executable \(executable) not found in PATH") +} + +private func runCommand(_ command: URL, _ arguments: [String]) throws { + let task = Process() + task.executableURL = command + task.arguments = arguments + task.currentDirectoryURL = URL(fileURLWithPath: FileManager.default.currentDirectoryPath) + try task.run() + task.waitUntilExit() + guard task.terminationStatus == 0 else { + throw PackageToJSError("Command failed with status \(task.terminationStatus)") + } +} + +/// Plans the build for packaging. +struct PackagingPlanner { + /// The options for packaging + let options: PackageToJS.PackageOptions + /// The package ID of the package that this plugin is running on + let packageId: String + /// The directory of the package that contains this plugin + 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 + let intermediatesDir: BuildPath + /// The filename of the .wasm file + let wasmFilename: String + /// The path to the .wasm product artifact + let wasmProductArtifact: BuildPath + /// The build configuration + let configuration: String + /// The target triple + let triple: String + /// The system interface to use + let system: any PackagingSystem + + init( + options: PackageToJS.PackageOptions, + packageId: String, + intermediatesDir: BuildPath, + selfPackageDir: BuildPath, + exportedSkeletons: [BuildPath], + importedSkeletons: [BuildPath], + outputDir: BuildPath, + wasmProductArtifact: BuildPath, + wasmFilename: String, + configuration: String, + triple: String, + selfPath: BuildPath = BuildPath(absolute: #filePath), + system: any PackagingSystem + ) { + self.options = options + self.packageId = packageId + self.selfPackageDir = selfPackageDir + self.exportedSkeletons = exportedSkeletons + self.importedSkeletons = importedSkeletons + self.outputDir = outputDir + self.intermediatesDir = intermediatesDir + self.wasmFilename = wasmFilename + self.selfPath = selfPath + self.wasmProductArtifact = wasmProductArtifact + self.configuration = configuration + self.triple = triple + self.system = system + } + + // MARK: - Build plans + + /// Construct the build plan and return the root task key + func planBuild( + make: inout MiniMake, + buildOptions: PackageToJS.BuildOptions + ) throws -> MiniMake.TaskKey { + let (allTasks, _, _, _) = try planBuildInternal( + make: &make, + noOptimize: buildOptions.noOptimize, + debugInfoFormat: buildOptions.debugInfoFormat + ) + return make.addTask( + inputTasks: allTasks, + output: BuildPath(phony: "all"), + attributes: [.phony, .silent] + ) + } + + private func planBuildInternal( + make: inout MiniMake, + noOptimize: Bool, + debugInfoFormat: PackageToJS.DebugInfoFormat + ) throws -> ( + allTasks: [MiniMake.TaskKey], + outputDirTask: MiniMake.TaskKey, + intermediatesDirTask: MiniMake.TaskKey, + packageJsonTask: MiniMake.TaskKey + ) { + // Prepare output directory + let outputDirTask = make.addTask( + inputFiles: [selfPath], + output: outputDir, + attributes: [.silent] + ) { + try system.createDirectory(atPath: $1.resolve(path: $0.output).path) + } + + var packageInputs: [MiniMake.TaskKey] = [] + + // Guess the build configuration from the parent directory name of .wasm file + let wasm: MiniMake.TaskKey + + let shouldOptimize: Bool + if self.configuration == "debug" { + shouldOptimize = false + } else { + shouldOptimize = !noOptimize + } + + let intermediatesDirTask = make.addTask( + inputFiles: [selfPath], + output: intermediatesDir, + attributes: [.silent] + ) { + try system.createDirectory(atPath: $1.resolve(path: $0.output).path) + } + + let finalWasmPath = outputDir.appending(path: wasmFilename) + + if shouldOptimize { + let wasmOptInputFile: BuildPath + let wasmOptInputTask: MiniMake.TaskKey? + switch debugInfoFormat { + case .dwarf: + // Keep the original wasm file + wasmOptInputFile = wasmProductArtifact + wasmOptInputTask = nil + case .name, .none: + // Optimize the wasm in release mode + 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], + 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 + ) + } + } + // Then, run wasm-opt with all optimizations + wasm = make.addTask( + 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 + ) + } + } else { + // Copy the wasm product artifact + wasm = make.addTask( + inputFiles: [selfPath, wasmProductArtifact], + inputTasks: [outputDirTask], + output: finalWasmPath + ) { + 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], + output: wasmImportsPath + ) { + let metadata = try parseImports( + moduleBytes: try Data(contentsOf: URL(fileURLWithPath: $1.resolve(path: finalWasmPath).path)) + ) + let jsonEncoder = JSONEncoder() + jsonEncoder.outputFormatting = .prettyPrinted + let jsonData = try jsonEncoder.encode(metadata) + try system.writeFile(atPath: $1.resolve(path: $0.output).path, content: jsonData) + } + + packageInputs.append(wasmImportsTask) + + let platformsDir = outputDir.appending(path: "platforms") + let platformsDirTask = make.addTask( + 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: [] + ) + 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"), + ("Plugins/PackageToJS/Templates/index.d.ts", "index.d.ts"), + ("Plugins/PackageToJS/Templates/instantiate.js", "instantiate.js"), + ("Plugins/PackageToJS/Templates/instantiate.d.ts", "instantiate.d.ts"), + ("Plugins/PackageToJS/Templates/platforms/browser.js", "platforms/browser.js"), + ("Plugins/PackageToJS/Templates/platforms/browser.d.ts", "platforms/browser.d.ts"), + ("Plugins/PackageToJS/Templates/platforms/browser.worker.js", "platforms/browser.worker.js"), + ("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 + ) + ) + } + return (packageInputs, outputDirTask, intermediatesDirTask, packageJsonTask) + } + + /// Construct the test build plan and return the root task key + func planTestBuild( + make: inout MiniMake + ) throws -> (rootTask: MiniMake.TaskKey, binDir: BuildPath) { + var (allTasks, outputDirTask, intermediatesDirTask, packageJsonTask) = try planBuildInternal( + 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()) + } + ) + + let binDir = outputDir.appending(path: "bin") + let binDirTask = make.addTask( + inputFiles: [selfPath], + inputTasks: [outputDirTask], + output: binDir + ) { + try system.createDirectory(atPath: $1.resolve(path: $0.output).path) + } + allTasks.append(binDirTask) + + // Copy the template files + for (file, output) in [ + ("Plugins/PackageToJS/Templates/test.js", "test.js"), + ("Plugins/PackageToJS/Templates/test.d.ts", "test.d.ts"), + ("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] + ) + ) + } + let rootTask = make.addTask( + inputTasks: allTasks, + output: BuildPath(phony: "all"), + attributes: [.phony, .silent] + ) + return (rootTask, binDir) + } + + private func planCopyTemplateFile( + make: inout MiniMake, + file: String, + output: String, + outputDirTask: MiniMake.TaskKey, + inputFiles: [BuildPath], + inputTasks: [MiniMake.TaskKey], + wasmImportsPath: BuildPath? = nil + ) -> MiniMake.TaskKey { + + struct Salt: Encodable { + let conditions: [String: Bool] + let substitutions: [String: String] + } + + let inputPath = selfPackageDir.appending(path: file) + let conditions: [String: Bool] = [ + "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, + "PACKAGE_TO_JS_PACKAGE_NAME": options.packageName ?? packageId.lowercased(), + ] + let salt = Salt(conditions: conditions, substitutions: constantSubstitutions) + + return make.addTask( + 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 memoryImport = importEntries.first { + $0.module == "env" && $0.name == "memory" + } + if case .memory(let type) = memoryImport?.kind { + substitutions["PACKAGE_TO_JS_MEMORY_INITIAL"] = type.minimum.description + substitutions["PACKAGE_TO_JS_MEMORY_MAXIMUM"] = (type.maximum ?? type.minimum).description + substitutions["PACKAGE_TO_JS_MEMORY_SHARED"] = type.shared.description + } + } + + let inputPath = $1.resolve(path: inputPath) + var content = try String(contentsOf: inputPath, encoding: .utf8) + let options = PreprocessOptions(conditions: conditions, substitutions: substitutions) + content = try preprocess(source: content, file: inputPath.path, options: options) + try system.writeFile(atPath: $1.resolve(path: $0.output).path, content: Data(content.utf8)) + } + } +} + +// MARK: - Utilities + +func logCommandExecution(_ command: String, _ arguments: [String]) { + var fullArguments = [command] + fullArguments.append(contentsOf: arguments) + print("$ \(fullArguments.map { "\"\($0)\"" }.joined(separator: " "))") +} + +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/PackageToJS/Sources/PackageToJSPlugin.swift b/Plugins/PackageToJS/Sources/PackageToJSPlugin.swift new file mode 100644 index 000000000..e7f74e974 --- /dev/null +++ b/Plugins/PackageToJS/Sources/PackageToJSPlugin.swift @@ -0,0 +1,760 @@ +#if canImport(PackagePlugin) +// Import minimal Foundation APIs to speed up overload resolution +@preconcurrency import struct Foundation.URL +@preconcurrency import struct Foundation.Data +@preconcurrency import class Foundation.Process +@preconcurrency import class Foundation.ProcessInfo +@preconcurrency import class Foundation.FileManager +@preconcurrency import struct Foundation.CocoaError +@preconcurrency import func Foundation.fputs +@preconcurrency import func Foundation.exit +@preconcurrency import var Foundation.stderr +import PackagePlugin + +/// The main entry point for the PackageToJS plugin. +@main +struct PackageToJSPlugin: CommandPlugin { + static let friendlyBuildDiagnostics: + [@Sendable (_ build: PackageManager.BuildResult, _ arguments: [String]) -> String?] = [ + ( + // 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") + else { return nil } + let didYouMean = + [ + "swift", "package", "--swift-sdk", "wasm32-unknown-wasi", "js", + ] + arguments + return """ + Please pass `--swift-sdk` to "swift package". + + Did you mean this? + \(didYouMean.joined(separator: " ")) + """ + }), + ( + // In case selected Swift SDK version is not compatible with the Swift compiler version + { build, arguments in + let regex = + #/module compiled with Swift (?\d+\.\d+(?:\.\d+)?) cannot be imported by the Swift (?\d+\.\d+(?:\.\d+)?) compiler/# + guard let match = build.logText.firstMatch(of: regex) else { return nil } + let swiftSDKVersion = match.swiftSDKVersion + let compilerVersion = match.compilerVersion + return """ + Swift versions mismatch: + - Swift SDK version: \(swiftSDKVersion) + - Swift compiler version: \(compilerVersion) + + Please ensure you are using matching versions of the Swift SDK and Swift compiler. + + 1. Use 'swift --version' to check your Swift compiler version + 2. Use 'swift sdk list' to check available Swift SDKs + 3. Select a matching SDK version with --swift-sdk option + """ + }), + ( + // 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 { + return nil + } + return """ + The selected toolchain might be an Xcode toolchain, which doesn't support WebAssembly target. + + Please use a swift.org Open Source toolchain with WebAssembly support. + See https://book.swiftwasm.org/getting-started/setup.html for more information. + """ + }), + ] + + private func emitHintMessage(_ message: String) { + printStderr("\n" + "\u{001B}[1m\u{001B}[97mHint:\u{001B}[0m " + message) + } + + private func reportBuildFailure( + _ build: PackageManager.BuildResult, + _ arguments: [String] + ) { + for diagnostic in Self.friendlyBuildDiagnostics { + if let message = diagnostic(build, arguments) { + emitHintMessage(message) + return + } + } + } + + func performCommand(context: PluginContext, arguments: [String]) throws { + do { + if arguments.first == "test" { + return try performTestCommand(context: context, arguments: Array(arguments.dropFirst())) + } + + return try performBuildCommand(context: context, arguments: arguments) + } catch let error as CocoaError where error.code == .fileWriteNoPermission { + guard let filePath = error.filePath else { throw error } + + let packageDir = context.package.directoryURL + printStderr("\n\u{001B}[1m\u{001B}[91merror:\u{001B}[0m \(error.localizedDescription)") + + 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 + emitHintMessage( + """ + Please pass `--allow-writing-to-package-directory` to "swift package". + + Did you mean this? + \(didYouMean.joined(separator: " ")) + """ + ) + } 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 + emitHintMessage( + """ + Please pass `--allow-writing-to-directory ` to "swift package". + + Did you mean this? + \(didYouMean.joined(separator: " ")) + """ + ) + } + exit(1) + } + } + + static let JAVASCRIPTKIT_PACKAGE_ID: Package.ID = "javascriptkit" + + func performBuildCommand(context: PluginContext, arguments: [String]) throws { + if arguments.contains(where: { ["-h", "--help"].contains($0) }) { + printStderr(PackageToJS.BuildOptions.help()) + return + } + + var extractor = ArgumentExtractor(arguments) + let buildOptions = PackageToJS.BuildOptions.parse(from: &extractor) + + if extractor.remainingArguments.count > 0 { + printStderr( + "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, + 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 { + URL(fileURLWithPath: outputPath) + } else { + context.pluginWorkDirectoryURL.appending(path: "Package") + } + var make = MiniMake( + explain: buildOptions.packageOptions.explain, + printProgress: self.printProgress + ) + let planner = PackagingPlanner( + 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 + ) + cleanIfBuildGraphChanged(root: rootTask, make: make, context: context) + print("Packaging...") + let scope = MiniMake.VariableScope(variables: [:]) + try make.build(output: rootTask, scope: scope) + print("Packaging finished") + } + + func performTestCommand(context: PluginContext, arguments: [String]) throws { + if arguments.contains(where: { ["-h", "--help"].contains($0) }) { + printStderr(PackageToJS.TestOptions.help()) + return + } + + var extractor = ArgumentExtractor(arguments) + let testOptions = PackageToJS.TestOptions.parse(from: &extractor) + + if extractor.remainingArguments.count > 0 { + printStderr( + "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, + selfPackage: selfPackage, + context: context, + options: testOptions.packageOptions + ) + guard build.succeeded else { + reportBuildFailure(build, arguments) + 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. + // This doesn't work when `--scratch-path` is used but + // we don't have a way to guess the correct path. (we can find + // the path by building a dummy executable product but it's + // not worth the overhead) + var productArtifact: URL? + for fileExtension in ["wasm", "xctest"] { + let packageDir = context.package.directoryURL + let path = packageDir.appending(path: ".build/debug/\(productName).\(fileExtension)").path + if FileManager.default.fileExists(atPath: path) { + productArtifact = URL(fileURLWithPath: path) + break + } + } + guard let productArtifact = productArtifact else { + throw PackageToJSError( + "Failed to find '\(productName).wasm' or '\(productName).xctest'" + ) + } + let outputDir = + if let outputPath = testOptions.packageOptions.outputPath { + URL(fileURLWithPath: outputPath) + } else { + context.pluginWorkDirectoryURL.appending(path: "PackageTests") + } + var make = MiniMake( + explain: testOptions.packageOptions.explain, + printProgress: self.printProgress + ) + let planner = PackagingPlanner( + 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. + wasmFilename: productArtifact.lastPathComponent.hasSuffix(".wasm") + ? productArtifact.lastPathComponent + : productArtifact.lastPathComponent + ".wasm" + ) + let (rootTask, binDir) = try planner.planTestBuild( + make: &make + ) + cleanIfBuildGraphChanged(root: rootTask, make: make, context: context) + print("Packaging tests...") + let scope = MiniMake.VariableScope(variables: [:]) + try make.build(output: rootTask, scope: scope) + print("Packaging tests finished") + + if !testOptions.buildOnly { + let testRunner = scope.resolve(path: binDir.appending(path: "test.js")) + try PackageToJS.runTest( + testRunner: testRunner, + currentDirectoryURL: context.pluginWorkDirectoryURL, + outputDir: outputDir, + testOptions: testOptions + ) + } + } + + 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: buildConfiguration, + logging: options.verbose ? .verbose : .concise + ) + parameters.echoLogs = true + parameters.otherSwiftcFlags = ["-color-diagnostics"] + 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. + parameters.otherSwiftcFlags += [ + "-static-stdlib", "-Xclang-linker", "-mexec-model=reactor", + ] + parameters.otherLinkerFlags += [ + "--export-if-defined=__main_argc_argv" + ] + + // Enable code coverage options if requested + if options.enableCodeCoverage { + parameters.otherSwiftcFlags += ["-profile-coverage-mapping", "-profile-generate"] + parameters.otherCFlags += ["-fprofile-instr-generate", "-fcoverage-mapping"] + } + } + 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 + /// configurations, which leads to placing the .wasm file in a different + /// path. + private func cleanIfBuildGraphChanged( + root: MiniMake.TaskKey, + make: MiniMake, + context: PluginContext + ) { + let buildFingerprint = context.pluginWorkDirectoryURL.appending(path: "minimake.json") + let lastBuildFingerprint = try? Data(contentsOf: buildFingerprint) + let currentBuildFingerprint = try? make.computeFingerprint(root: root) + if lastBuildFingerprint != currentBuildFingerprint { + printStderr("Build graph changed, cleaning...") + make.cleanEverything(scope: MiniMake.VariableScope(variables: [:])) + } + try? currentBuildFingerprint?.write(to: buildFingerprint) + } + + 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 + "/") + ? String(outputPath.dropFirst(buildCwd.count + 1)) : outputPath + printStderr("[\(context.built + 1)/\(context.total)] \(displayName): \(message)") + } +} + +private func printStderr(_ message: String) { + fputs(message + "\n", stderr) +} + +// MARK: - Options parsing + +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, + 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 { + static func parse(from extractor: inout ArgumentExtractor) -> PackageToJS.BuildOptions { + let product = extractor.extractOption(named: "product").last + let noOptimize = extractor.extractFlag(named: "no-optimize") + let rawDebugInfoFormat = extractor.extractOption(named: "debug-info-format").last + 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: ", "))" + ) + } + debugInfoFormat = format + } + let packageOptions = PackageToJS.PackageOptions.parse(from: &extractor) + return PackageToJS.BuildOptions( + product: product, + noOptimize: noOptimize != 0, + debugInfoFormat: debugInfoFormat, + packageOptions: packageOptions + ) + } + + static func help() -> String { + return """ + OVERVIEW: Builds a JavaScript module from a Swift package. + + USAGE: swift package --swift-sdk [SwiftPM options] js [options] [subcommand] + + OPTIONS: + --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 + + EXAMPLES: + $ swift package --swift-sdk wasm32-unknown-wasi js + # 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 js -c release + + # Run tests + $ swift package --swift-sdk wasm32-unknown-wasi js test + """ + } +} + +extension PackageToJS.TestOptions { + static func parse(from extractor: inout ArgumentExtractor) -> PackageToJS.TestOptions { + let buildOnly = extractor.extractFlag(named: "build-only") + let listTests = extractor.extractFlag(named: "list-tests") + let filter = extractor.extractOption(named: "filter") + let prelude = extractor.extractOption(named: "prelude").last + let environment = extractor.extractOption(named: "environment").last + let inspect = extractor.extractFlag(named: "inspect") + 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, + extraNodeArguments: extraNodeArguments, + packageOptions: packageOptions + ) + + if !options.buildOnly, !options.packageOptions.useCDN { + options.packageOptions.useCDN = true + } + + return options + } + + static func help() -> String { + return """ + OVERVIEW: Builds and runs tests + + 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 + -Xnode Extra arguments to pass to Node.js + \(PackageToJS.PackageOptions.optionsHelp()) + + EXAMPLES: + $ swift package --swift-sdk wasm32-unknown-wasi js test + $ swift package --swift-sdk wasm32-unknown-wasi js test --environment browser + # Just build tests, don't run them + $ swift package --swift-sdk wasm32-unknown-wasi js test --build-only + $ node .build/plugins/PackageToJS/outputs/PackageTests/bin/test.js + """ + } +} + +// 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) + } + } 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 + } +} + +/// Derive default product from the package +/// - Returns: The name of the product to build +/// - Throws: `PackageToJSError` if there's no executable product or if there's more than one +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" + ) + } + guard executableProducts.count == 1 else { + throw PackageToJSError( + "Failed to disambiguate the product. Pass one of \(executableProducts.map(\.name).joined(separator: ", ")) to the --product option" + ) + + } + return executableProducts[0].name +} + +extension PackageManager.BuildResult { + /// Find `.wasm` executable artifact + internal func findWasmArtifact(for product: String) throws -> URL { + let executables = self.builtArtifacts.filter { + ($0.kind == .executable) && ($0.url.lastPathComponent == "\(product).wasm") + } + guard !executables.isEmpty else { + throw PackageToJSError( + "Failed to find '\(product).wasm' from executable artifacts of product '\(product)'" + ) + } + guard executables.count == 1, let executable = executables.first else { + throw PackageToJSError( + "Failed to disambiguate executable product artifacts from \(executables.map(\.url.path).joined(separator: ", "))" + ) + } + return executable.url + } +} + +private func findPackageInDependencies(package: Package, id: Package.ID) -> Package? { + var visited: Set = [] + func visit(package: Package) -> Package? { + if visited.contains(package.id) { return nil } + visited.insert(package.id) + if package.id == id { return package } + for dependency in package.dependencies { + if let found = visit(package: dependency.package) { + return found + } + } + return nil + } + 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 + ) { + let outputBaseName = outputDir.lastPathComponent + let (configuration, triple) = PackageToJS.deriveBuildConfiguration(wasmProductArtifact: wasmProductArtifact) + let system = DefaultPackagingSystem(printWarning: printStderr) + self.init( + options: options, + packageId: context.package.id, + 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, + configuration: configuration, + triple: triple, + system: system + ) + } +} + +#endif diff --git a/Plugins/PackageToJS/Sources/ParseWasm.swift b/Plugins/PackageToJS/Sources/ParseWasm.swift new file mode 100644 index 000000000..4372b32c5 --- /dev/null +++ b/Plugins/PackageToJS/Sources/ParseWasm.swift @@ -0,0 +1,312 @@ +import struct Foundation.Data + +/// Represents the type of value in WebAssembly +enum ValueType { + case i32 + case i64 + case f32 + case f64 + case funcref + case externref + case v128 +} + +/// Represents a function type in WebAssembly +struct FunctionType { + let parameters: [ValueType] + let results: [ValueType] +} + +/// Represents a table type in WebAssembly +struct TableType { + let element: ElementType + let minimum: UInt32 + let maximum: UInt32? + + enum ElementType: String { + case funcref + case externref + } +} + +/// Represents a memory type in WebAssembly +struct MemoryType: Codable { + let minimum: UInt32 + let maximum: UInt32? + let shared: Bool + let index: IndexType + + enum IndexType: String, Codable { + case i32 + case i64 + } +} + +/// Represents a global type in WebAssembly +struct GlobalType { + let value: ValueType + let mutable: Bool +} + +/// Represents an import entry in WebAssembly +struct ImportEntry: Codable { + let module: String + let name: String + let kind: ImportKind + + enum ImportKind: Codable { + case function + case table + case memory(type: MemoryType) + case global + } +} + +/// Parse state for WebAssembly parsing +private class ParseState { + private let moduleBytes: Data + private var offset: Int + + init(moduleBytes: Data) { + self.moduleBytes = moduleBytes + self.offset = 0 + } + + func hasMoreBytes() -> Bool { + return offset < moduleBytes.count + } + + func readByte() throws -> UInt8 { + guard offset < moduleBytes.count else { + throw ParseError.unexpectedEndOfData + } + let byte = moduleBytes[offset] + offset += 1 + return byte + } + + func skipBytes(_ count: Int) throws { + guard offset + count <= moduleBytes.count else { + throw ParseError.unexpectedEndOfData + } + offset += count + } + + /// Read an unsigned LEB128 integer + func readUnsignedLEB128() throws -> UInt32 { + var result: UInt32 = 0 + var shift: UInt32 = 0 + var byte: UInt8 + + repeat { + byte = try readByte() + result |= UInt32(byte & 0x7F) << shift + shift += 7 + if shift > 32 { + throw ParseError.integerOverflow + } + } while (byte & 0x80) != 0 + + return result + } + + func readName() throws -> String { + let nameLength = try readUnsignedLEB128() + guard offset + Int(nameLength) <= moduleBytes.count else { + throw ParseError.unexpectedEndOfData + } + + let nameBytes = moduleBytes[offset..<(offset + Int(nameLength))] + guard let name = String(bytes: nameBytes, encoding: .utf8) else { + throw ParseError.invalidUTF8 + } + + offset += Int(nameLength) + return name + } + + func assertBytes(_ expected: [UInt8]) throws { + let baseOffset = offset + let expectedLength = expected.count + + guard baseOffset + expectedLength <= moduleBytes.count else { + throw ParseError.unexpectedEndOfData + } + + for i in 0.. [ImportEntry] { + let parseState = ParseState(moduleBytes: moduleBytes) + try parseMagicNumber(parseState) + try parseVersion(parseState) + + var types: [FunctionType] = [] + var imports: [ImportEntry] = [] + + while parseState.hasMoreBytes() { + let sectionId = try parseState.readByte() + let sectionSize = try parseState.readUnsignedLEB128() + + switch sectionId { + case 1: // Type section + let typeCount = try parseState.readUnsignedLEB128() + for _ in 0.. TableType { + let elementType = try parseState.readByte() + + let element: TableType.ElementType + switch elementType { + case 0x70: + element = .funcref + case 0x6F: + element = .externref + default: + throw ParseError.unknownTableElementType(elementType) + } + + let limits = try parseLimits(parseState) + return TableType(element: element, minimum: limits.minimum, maximum: limits.maximum) +} + +private func parseLimits(_ parseState: ParseState) throws -> MemoryType { + let flags = try parseState.readByte() + let minimum = try parseState.readUnsignedLEB128() + let hasMaximum = (flags & 1) != 0 + let shared = (flags & 2) != 0 + let isMemory64 = (flags & 4) != 0 + let index: MemoryType.IndexType = isMemory64 ? .i64 : .i32 + + if hasMaximum { + let maximum = try parseState.readUnsignedLEB128() + return MemoryType(minimum: minimum, maximum: maximum, shared: shared, index: index) + } else { + return MemoryType(minimum: minimum, maximum: nil, shared: shared, index: index) + } +} + +private func parseGlobalType(_ parseState: ParseState) throws -> GlobalType { + let value = try parseValueType(parseState) + let mutable = try parseState.readByte() == 1 + return GlobalType(value: value, mutable: mutable) +} + +private func parseValueType(_ parseState: ParseState) throws -> ValueType { + let type = try parseState.readByte() + switch type { + case 0x7F: + return .i32 + case 0x7E: + return .i64 + case 0x7D: + return .f32 + case 0x7C: + return .f64 + case 0x70: + return .funcref + case 0x6F: + return .externref + case 0x7B: + return .v128 + default: + throw ParseError.unknownValueType(type) + } +} + +private func parseFunctionType(_ parseState: ParseState) throws -> FunctionType { + let form = try parseState.readByte() + if form != 0x60 { + throw ParseError.invalidFunctionTypeForm(form) + } + + var parameters: [ValueType] = [] + let parameterCount = try parseState.readUnsignedLEB128() + for _ in 0.. */` +/// - `/* #else */` +/// - `/* #endif */` +/// - `@@` +/// - `import.meta.` +/// +/// The condition is a boolean expression that can use the variables +/// defined in the `options`. Variable names must be `[a-zA-Z0-9_]+`. +/// Contents between `if-else-endif` blocks will be included or excluded +/// based on the condition like C's `#if` directive. +/// +/// `@@` and `import.meta.` will be substituted with +/// the value of the variable. +/// +/// The preprocessor will return the preprocessed source code. +func preprocess(source: String, file: String? = nil, options: PreprocessOptions) throws -> String { + let preprocessor = Preprocessor(source: source, file: file, options: options) + let tokens = try preprocessor.tokenize() + let parsed = try preprocessor.parse(tokens: tokens) + return try preprocessor.preprocess(parsed: parsed) +} + +struct PreprocessOptions { + /// The conditions to evaluate in the source code + var conditions: [String: Bool] = [:] + /// The variables to substitute in the source code + var substitutions: [String: String] = [:] +} + +private struct Preprocessor { + enum Token: Equatable { + case `if`(condition: String) + case `else` + case `endif` + case block(String) + } + + struct TokenInfo { + let token: Token + let position: String.Index + } + + struct PreprocessorError: Error, CustomStringConvertible { + let file: String? + let message: String + let source: String + let line: Int + let column: Int + + init(file: String?, message: String, source: String, line: Int, column: Int) { + self.file = file + self.message = message + self.source = source + self.line = line + self.column = column + } + + init(file: String?, message: String, source: String, index: String.Index) { + let (line, column) = Self.computeLineAndColumn(from: index, in: source) + self.init(file: file, message: message, source: source, line: line, column: column) + } + + /// Get the 1-indexed line and column + 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[.. 0 { + description += formatLine(number: line - 1, content: lines[lineIndex - 1], width: lineNumberWidth) + } + description += formatLine(number: line, content: lines[lineIndex], width: lineNumberWidth) + description += formatPointer(column: column, width: lineNumberWidth) + if lineIndex + 1 < lines.count { + description += formatLine(number: line + 1, content: lines[lineIndex + 1], width: lineNumberWidth) + } + + return description + } + + private func formatLine(number: Int, content: String.SubSequence, width: Int) -> String { + return "\(number)".padding(toLength: width, withPad: " ", startingAt: 0) + " | \(content)\n" + } + + private func formatPointer(column: Int, width: Int) -> String { + let padding = String(repeating: " ", count: width) + " | " + String(repeating: " ", count: column - 1) + return padding + "^\n" + } + } + + let source: String + let file: String? + let options: PreprocessOptions + + init(source: String, file: String?, options: PreprocessOptions) { + self.source = source + self.file = file + self.options = options + } + + func unexpectedTokenError(expected: Token?, token: Token, at index: String.Index) -> PreprocessorError { + let message = expected.map { "Expected \($0) but got \(token)" } ?? "Unexpected token \(token)" + return PreprocessorError( + file: file, + message: message, + source: source, + index: index + ) + } + + 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 + ) + } + + func unexpectedDirectiveError(at index: String.Index) -> PreprocessorError { + return PreprocessorError( + file: file, + 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 + ) + } + + func undefinedVariableError(name: String, at index: String.Index) -> PreprocessorError { + return PreprocessorError( + file: file, + message: "Undefined variable \(name)", + source: source, + index: index + ) + } + + func tokenize() throws -> [TokenInfo] { + var cursor = source.startIndex + var tokens: [TokenInfo] = [] + + var bufferStart = cursor + + func consume(_ count: Int = 1) { + cursor = source.index(cursor, offsetBy: count) + } + + func takeIdentifier() throws -> String { + var identifier = "" + var char = try peek() + while ["a"..."z", "A"..."Z", "0"..."9"].contains(where: { $0.contains(char) }) + || char == "_" + { + identifier.append(char) + consume() + char = try peek() + } + return identifier + } + + func expect(_ expected: Character) throws { + guard try peek() == expected else { + throw unexpectedCharacterError(expected: expected, character: try peek(), at: cursor) + } + consume() + } + + func expect(_ expected: String) throws { + guard + let endIndex = source.index( + cursor, + offsetBy: expected.count, + limitedBy: source.endIndex + ) + else { + throw eofError(at: cursor) + } + guard source[cursor.. Character { + guard cursor < source.endIndex else { + throw eofError(at: cursor) + } + return source[cursor] + } + + func peek2() throws -> (Character, Character) { + guard cursor < source.endIndex, source.index(after: cursor) < source.endIndex else { + throw eofError(at: cursor) + } + let char1 = source[cursor] + let char2 = source[source.index(after: cursor)] + return (char1, char2) + } + + func addToken(_ token: Token, at position: String.Index) { + tokens.append(.init(token: token, position: position)) + } + + func flushBufferToken() { + guard bufferStart < cursor else { return } + addToken(.block(String(source[bufferStart.. Token] = [ + "if": { + try expect(" ") + let condition = try takeIdentifier() + return .if(condition: condition) + }, + "else": { + return .else + }, + "endif": { + return .endif + }, + ] + var token: Token? + for (keyword, factory) in directives { + guard directiveSource.hasPrefix(keyword) else { + continue + } + consume(keyword.count) + token = try factory() + try expect(" */") + break + } + guard let token = token else { + throw unexpectedDirectiveError(at: directiveStart) + } + // Skip a trailing newline + if (try? peek()) == "\n" { + consume() + } + addToken(token, at: directiveStart) + bufferStart = cursor + } + flushBufferToken() + return tokens + } + + enum ParseResult { + case block(String) + indirect case `if`( + condition: String, + then: [ParseResult], + else: [ParseResult], + position: String.Index + ) + } + + func parse(tokens: [TokenInfo]) throws -> [ParseResult] { + var cursor = tokens.startIndex + + func consume() { + cursor = tokens.index(after: cursor) + } + + func parse() throws -> ParseResult { + switch tokens[cursor].token { + case .block(let content): + consume() + return .block(content) + case .if(let condition): + let ifPosition = tokens[cursor].position + consume() + var then: [ParseResult] = [] + var `else`: [ParseResult] = [] + while cursor < tokens.endIndex && tokens[cursor].token != .else + && tokens[cursor].token != .endif + { + then.append(try parse()) + } + if case .else = tokens[cursor].token { + consume() + while cursor < tokens.endIndex && tokens[cursor].token != .endif { + `else`.append(try parse()) + } + } + guard case .endif = tokens[cursor].token else { + throw unexpectedTokenError( + 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 + ) + } + } + var results: [ParseResult] = [] + while cursor < tokens.endIndex { + results.append(try parse()) + } + return results + } + + func preprocess(parsed: [ParseResult]) throws -> String { + var result = "" + + func appendBlock(content: String) { + // Apply substitutions + var substitutedContent = content + for (key, value) in options.substitutions { + substitutedContent = substitutedContent.replacingOccurrences( + of: "@" + key + "@", + with: value + ) + substitutedContent = substitutedContent.replacingOccurrences( + of: "import.meta." + key, + with: value + ) + } + result.append(substitutedContent) + } + + func evaluate(parsed: ParseResult) throws { + switch parsed { + case .block(let content): + appendBlock(content: content) + case .if(let condition, let then, let `else`, let position): + guard let condition = options.conditions[condition] else { + throw undefinedVariableError(name: condition, at: position) + } + let blocks = condition ? then : `else` + for block in blocks { + try evaluate(parsed: block) + } + } + } + for parsed in parsed { + try evaluate(parsed: parsed) + } + return result + } +} diff --git a/Plugins/PackageToJS/Sources/TestsParser.swift b/Plugins/PackageToJS/Sources/TestsParser.swift new file mode 100644 index 000000000..72afd6b07 --- /dev/null +++ b/Plugins/PackageToJS/Sources/TestsParser.swift @@ -0,0 +1,255 @@ +/// The original implementation of this file is from Carton. +/// https://github.com/swiftwasm/carton/blob/1.1.3/Sources/carton-frontend-slim/TestRunners/TestsParser.swift + +import Foundation +import RegexBuilder + +extension String.StringInterpolation { + /// Display `value` with the specified ANSI-escaped `color` values, then apply the reset. + fileprivate mutating func appendInterpolation(_ value: T, color: String...) { + appendInterpolation("\(color.map { "\u{001B}\($0)" }.joined())\(value)\u{001B}[0m") + } +} + +class FancyTestsParser { + let write: (String) -> Void + + init(write: @escaping (String) -> Void) { + self.write = write + } + + private enum Status { + case passed, failed, skipped + case unknown(String.SubSequence?) + + var isNegative: Bool { + switch self { + case .failed, .unknown(nil): return true + default: return false + } + } + + init(rawValue: String.SubSequence) { + switch rawValue { + case "passed": self = .passed + case "failed": self = .failed + case "skipped": self = .skipped + default: self = .unknown(rawValue) + } + } + } + + private struct Suite { + let name: String.SubSequence + var status: Status = .unknown(nil) + + var statusLabel: String { + switch status { + case .passed: return "\(" PASSED ", color: "[1m", "[97m", "[42m")" + case .failed: return "\(" FAILED ", color: "[1m", "[97m", "[101m")" + case .skipped: return "\(" SKIPPED ", color: "[1m", "[97m", "[97m")" + case .unknown(let status): + return "\(" \(status ?? "UNKNOWN") ", color: "[1m", "[97m", "[101m")" + } + } + + var cases: [Case] + + struct Case { + let name: String.SubSequence + var statusMark: String { + switch status { + case .passed: return "\("\u{2714}", color: "[92m")" + case .failed: return "\("\u{2718}", color: "[91m")" + case .skipped: return "\("\u{279C}", color: "[97m")" + case .unknown: return "\("?", color: "[97m")" + } + } + var status: Status = .unknown(nil) + var duration: String.SubSequence? + } + } + + private var suites = [Suite]() + + private let swiftIdentifier = #/[_\p{L}\p{Nl}][_\p{L}\p{Nl}\p{Mn}\p{Nd}\p{Pc}]*/# + private let timestamp = #/\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}\.\d{3}/# + private lazy var suiteStarted = Regex { + "Test Suite '" + Capture { + OneOrMore(CharacterClass.anyOf("'").inverted) + } + "' started at " + Capture { self.timestamp } + } + private lazy var suiteStatus = Regex { + "Test Suite '" + Capture { OneOrMore(CharacterClass.anyOf("'").inverted) } + "' " + Capture { + ChoiceOf { + "failed" + "passed" + } + } + " at " + Capture { self.timestamp } + } + private lazy var testCaseStarted = Regex { + "Test Case '" + Capture { self.swiftIdentifier } + "." + Capture { self.swiftIdentifier } + "' started" + } + private lazy var testCaseStatus = Regex { + "Test Case '" + Capture { self.swiftIdentifier } + "." + Capture { self.swiftIdentifier } + "' " + Capture { + ChoiceOf { + "failed" + "passed" + "skipped" + } + } + " (" + Capture { + OneOrMore(.digit) + "." + OneOrMore(.digit) + } + " seconds)" + } + + private let testSummary = + #/Executed \d+ (test|tests), with (?:\d+ (?:test|tests) skipped and )?\d+ (failure|failures) \((?\d+) unexpected\) in (?\d+\.\d+) \(\d+\.\d+\) seconds/# + + func onLine(_ line: String) { + if let match = line.firstMatch( + of: suiteStarted + ) { + let (_, suite, _) = match.output + suites.append(.init(name: suite, cases: [])) + } else if let match = line.firstMatch( + of: suiteStatus + ) { + let (_, suite, status, _) = match.output + if let suiteIdx = suites.firstIndex(where: { $0.name == suite }) { + suites[suiteIdx].status = Status(rawValue: status) + flushSingleSuite(suites[suiteIdx]) + } + } else if let match = line.firstMatch( + of: testCaseStarted + ) { + let (_, suite, testCase) = match.output + if let suiteIdx = suites.firstIndex(where: { $0.name == suite }) { + suites[suiteIdx].cases.append( + .init(name: testCase, duration: nil) + ) + } + } else if let match = line.firstMatch( + of: testCaseStatus + ) { + let (_, suite, testCase, status, duration) = match.output + if let suiteIdx = suites.firstIndex(where: { $0.name == suite }) { + if let caseIdx = suites[suiteIdx].cases.firstIndex(where: { + $0.name == testCase + }) { + suites[suiteIdx].cases[caseIdx].status = Status(rawValue: status) + suites[suiteIdx].cases[caseIdx].duration = duration + } + } + } else if line.firstMatch(of: testSummary) != nil { + // do nothing + } else { + if !line.isEmpty { + write(line + "\n") + } + } + } + + private func flushSingleSuite(_ suite: Suite) { + write(suite.statusLabel + " " + suite.name + "\n") + for testCase in suite.cases { + write(" " + testCase.statusMark + " " + testCase.name) + if let duration = testCase.duration { + write(" \("(\(duration)s)", color: "[90m")") + } + write("\n") + } + } + + func finalize() { + write("\n") + + func formatCategory( + label: String, + statuses: [Status] + ) -> String { + var passed = 0 + var skipped = 0 + var failed = 0 + var unknown = 0 + for status in statuses { + switch status { + case .passed: passed += 1 + case .skipped: skipped += 1 + case .failed: failed += 1 + case .unknown: unknown += 1 + } + } + var result = "\(label) " + if passed > 0 { + result += "\u{001B}[32m\(passed) passed\u{001B}[0m, " + } + if skipped > 0 { + result += "\u{001B}[97m\(skipped) skipped\u{001B}[0m, " + } + if failed > 0 { + result += "\u{001B}[31m\(failed) failed\u{001B}[0m, " + } + if unknown > 0 { + result += "\u{001B}[31m\(unknown) unknown\u{001B}[0m, " + } + result += "\u{001B}[0m\(statuses.count) total\n" + return result + } + + let suitesWithCases = suites.filter { $0.cases.count > 0 } + write( + formatCategory( + label: "Test Suites:", + statuses: suitesWithCases.map(\.status) + ) + ) + let allCaseStatuses = suitesWithCases.flatMap { + $0.cases.map { $0.status } + } + write( + formatCategory( + label: "Tests: ", + statuses: allCaseStatuses + ) + ) + + if suites.contains(where: { $0.name == "All tests" }) { + write("\("Ran all test suites.", color: "[90m")\n") // gray + } + + if suites.contains(where: { $0.status.isNegative }) { + write("\n\("Failed test cases:", color: "[31m")\n") + for suite in suites.filter({ $0.status.isNegative }) { + for testCase in suite.cases.filter({ $0.status.isNegative }) { + write(" \(testCase.statusMark) \(suite.name).\(testCase.name)\n") + } + } + + write( + "\n\("Some tests failed. Use --verbose for raw test output.", color: "[33m")\n" + ) + } + } +} diff --git a/Plugins/PackageToJS/Templates/bin/test.js b/Plugins/PackageToJS/Templates/bin/test.js new file mode 100644 index 000000000..f888b9d1c --- /dev/null +++ b/Plugins/PackageToJS/Templates/bin/test.js @@ -0,0 +1,94 @@ +import * as nodePlatform from "../platforms/node.js" +import { instantiate } from "../instantiate.js" +import { testBrowser } from "../test.js" +import { parseArgs } from "node:util" +import path from "node:path" +import { writeFileSync } from "node:fs" + +function splitArgs(args) { + // Split arguments into two parts by "--" + const part1 = [] + const part2 = [] + let index = 0 + while (index < args.length) { + if (args[index] === "--") { + index++ + break + } + part1.push(args[index]) + index++ + } + while (index < args.length) { + part2.push(args[index]) + index++ + } + return [part1, part2] +} + +const [testJsArgs, testFrameworkArgs] = splitArgs(process.argv.slice(2)) +const args = parseArgs({ + args: testJsArgs, + options: { + prelude: { type: "string" }, + environment: { type: "string" }, + inspect: { type: "boolean" }, + "coverage-file": { type: "string" }, + }, +}) + +const harnesses = { + node: async ({ preludeScript }) => { + try { + 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); + } + }, + /* #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 }) + } + } + await instantiate(options) + } catch (e) { + if (e instanceof WebAssembly.CompileError) { + // Check Node.js major version + 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.`) + } + } + throw e + } + }, + browser: async ({ preludeScript }) => { + process.exit(await testBrowser({ preludeScript, inspect: args.values.inspect, args: testFrameworkArgs })); + } +} + +const harness = harnesses[args.values.environment ?? "node"] +if (!harness) { + console.error(`Invalid environment: ${args.values.environment}`) + process.exit(1) +} + +const options = {} +if (args.values.prelude) { + options.preludeScript = path.resolve(process.cwd(), args.values.prelude) +} + +await harness(options) diff --git a/Plugins/PackageToJS/Templates/index.d.ts b/Plugins/PackageToJS/Templates/index.d.ts new file mode 100644 index 000000000..77d68efd9 --- /dev/null +++ b/Plugins/PackageToJS/Templates/index.d.ts @@ -0,0 +1,27 @@ +import type { Exports, Imports, ModuleSource } from './instantiate.js' + +export type Options = { + /** + * The WebAssembly module to instantiate + * + * 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 */ +} + +/** + * Instantiate and initialize the module + * + * This is a convenience function for browser environments. + * If you need a more flexible API, see `instantiate`. + */ +export declare function init(options?: Options): Promise<{ + instance: WebAssembly.Instance, + exports: Exports +}> diff --git a/Plugins/PackageToJS/Templates/index.js b/Plugins/PackageToJS/Templates/index.js new file mode 100644 index 000000000..76721511a --- /dev/null +++ b/Plugins/PackageToJS/Templates/index.js @@ -0,0 +1,28 @@ +// @ts-check +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) { + /** @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 */ + }) + return await instantiate(instantiateOptions); +} diff --git a/Plugins/PackageToJS/Templates/instantiate.d.ts b/Plugins/PackageToJS/Templates/instantiate.d.ts new file mode 100644 index 000000000..11837aba8 --- /dev/null +++ b/Plugins/PackageToJS/Templates/instantiate.d.ts @@ -0,0 +1,120 @@ +import type { /* #if USE_SHARED_MEMORY */SwiftRuntimeThreadChannel, /* #endif */SwiftRuntime } from "./runtime.js"; + +/* #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 + */ +export declare const MODULE_PATH: string; + +/* #if USE_SHARED_MEMORY */ +/** + * The type of the WebAssembly memory imported by the module + */ +export declare const MEMORY_TYPE: { + initial: number, + maximum: number, + shared: boolean +} +/* #endif */ +export interface WASI { + /** + * The WASI Preview 1 import object + */ + wasiImport: WebAssembly.ModuleImports + /** + * Initialize the WASI reactor instance + * + * @param instance - The instance of the WebAssembly module + */ + initialize(instance: WebAssembly.Instance): void + /** + * Set a new instance of the WebAssembly module to the WASI context + * Typically used when instantiating a WebAssembly module for a thread + * + * @param instance - The instance of the WebAssembly module + */ + setInstance(instance: WebAssembly.Instance): void + /** + * Extract a file from the WASI filesystem + * + * @param path - The path to the file to extract + * @returns The data of the file if it was extracted, undefined otherwise + */ + extractFile?(path: string): Uint8Array | undefined +} + +export type ModuleSource = WebAssembly.Module | ArrayBufferView | ArrayBuffer | Response | PromiseLike + +/** + * 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: Imports, +/* #endif */ +/* #if IS_WASI */ + /** + * The WASI implementation to use + */ + wasi: WASI, +/* #endif */ +/* #if USE_SHARED_MEMORY */ + /** + * The WebAssembly memory to use (must be 'shared') + */ + memory: WebAssembly.Memory + /** + * The thread channel is a set of functions that are used to communicate + * between the main thread and the worker thread. + */ + threadChannel: SwiftRuntimeThreadChannel & { + spawnThread: (module: WebAssembly.Module, memory: WebAssembly.Memory, startArg: any) => number; + } +/* #endif */ + /** + * Add imports to the WebAssembly import object + * @param imports - The imports to add + */ + addToCoreImports?: ( + imports: WebAssembly.Imports, + getInstance: () => WebAssembly.Instance | null, + getExports: () => Exports | null, + ) => void +} + +/** + * Instantiate the given WebAssembly module + */ +export declare function instantiate(options: InstantiateOptions): Promise<{ + instance: WebAssembly.Instance, + swift: SwiftRuntime, + exports: Exports +}> + +/** + * Instantiate the given WebAssembly module for a thread + */ +export declare function instantiateForThread(tid: number, startArg: number, options: InstantiateOptions): Promise<{ + instance: WebAssembly.Instance, + swift: SwiftRuntime, + exports: Exports +}> diff --git a/Plugins/PackageToJS/Templates/instantiate.js b/Plugins/PackageToJS/Templates/instantiate.js new file mode 100644 index 000000000..08351e67e --- /dev/null +++ b/Plugins/PackageToJS/Templates/instantiate.js @@ -0,0 +1,130 @@ +// @ts-check +import { SwiftRuntime } from "./runtime.js" + +export const MODULE_PATH = "@PACKAGE_TO_JS_MODULE_PATH@"; +/* #if USE_SHARED_MEMORY */ +export const MEMORY_TYPE = { + // @ts-ignore + initial: import.meta.PACKAGE_TO_JS_MEMORY_INITIAL, + // @ts-ignore + maximum: import.meta.PACKAGE_TO_JS_MEMORY_MAXIMUM, + // @ts-ignore + shared: import.meta.PACKAGE_TO_JS_MEMORY_SHARED, +} +/* #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, 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 +) { + const result = await _instantiate(options); +/* #if IS_WASI */ + options.wasi.initialize(result.instance); +/* #endif */ + result.swift.main(); + return result; +} + +/** @type {import('./instantiate.d').instantiateForThread} */ +export async function instantiateForThread( + tid, startArg, options +) { + const result = await _instantiate(options); +/* #if IS_WASI */ + options.wasi.setInstance(result.instance); +/* #endif */ + result.swift.startThread(tid, startArg) + return result; +} + +/** @type {import('./instantiate.d').instantiate} */ +async function _instantiate( + options +) { + const _WebAssembly = options.WebAssembly || WebAssembly; + const moduleSource = options.module; +/* #if IS_WASI */ + const { wasi } = options; +/* #endif */ + 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 = { + javascript_kit: swift.wasmImports, +/* #if IS_WASI */ + wasi_snapshot_preview1: wasi.wasiImport, +/* #if USE_SHARED_MEMORY */ + env: { + memory: options.memory, + }, + wasi: { + "thread-spawn": (startArg) => { + return options.threadChannel.spawnThread(module, options.memory, startArg); + } + } +/* #endif */ +/* #endif */ + }; + instantiator.addImports(importObject); + options.addToCoreImports?.(importObject, () => instance, () => exports); + + let module; + let instance; + let exports; + if (moduleSource instanceof _WebAssembly.Module) { + module = moduleSource; + 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); + module = result.module; + instance = result.instance; + } else { + const moduleBytes = await (await moduleSource).arrayBuffer(); + 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); + } + + swift.setInstance(instance); + instantiator.setInstance(instance); + exports = instantiator.createExports(instance); + + return { + instance, + swift, + exports, + } +} diff --git a/Plugins/PackageToJS/Templates/package.json b/Plugins/PackageToJS/Templates/package.json new file mode 100644 index 000000000..79562784a --- /dev/null +++ b/Plugins/PackageToJS/Templates/package.json @@ -0,0 +1,16 @@ +{ + "name": "@PACKAGE_TO_JS_PACKAGE_NAME@", + "version": "0.0.0", + "type": "module", + "private": true, + "exports": { + ".": "./index.js", + "./wasm": "./@PACKAGE_TO_JS_MODULE_PATH@" + }, + "dependencies": { + "@bjorn3/browser_wasi_shim": "0.3.0" + }, + "devDependencies": { + "playwright": "^1.51.0" + } +} diff --git a/Plugins/PackageToJS/Templates/platforms/browser.d.ts b/Plugins/PackageToJS/Templates/platforms/browser.d.ts new file mode 100644 index 000000000..b851c2283 --- /dev/null +++ b/Plugins/PackageToJS/Templates/platforms/browser.d.ts @@ -0,0 +1,18 @@ +import type { InstantiateOptions, ModuleSource/* #if HAS_IMPORTS */, Imports/* #endif */ } from "../instantiate.js" + +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 */ +}): Promise + +export function createDefaultWorkerFactory(preludeScript?: string): (module: WebAssembly.Module, memory: WebAssembly.Memory, startArg: any) => Worker diff --git a/Plugins/PackageToJS/Templates/platforms/browser.js b/Plugins/PackageToJS/Templates/platforms/browser.js new file mode 100644 index 000000000..9afd5c94a --- /dev/null +++ b/Plugins/PackageToJS/Templates/platforms/browser.js @@ -0,0 +1,140 @@ +// @ts-check +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 */ + +/* #if USE_SHARED_MEMORY */ +export async function defaultBrowserThreadSetup() { + const threadChannel = { + spawnThread: () => { + throw new Error("Cannot spawn a new thread from a worker thread") + }, + postMessageToMainThread: (message, transfer) => { + // @ts-ignore + self.postMessage(message, transfer); + }, + listenMessageFromMainThread: (listener) => { + // @ts-ignore + self.onmessage = (event) => listener(event.data); + } + } + +/* #if IS_WASI */ + const wasi = new WASI(/* args */[MODULE_PATH], /* env */[], /* fd */[ + new OpenFile(new File([])), // stdin + ConsoleStdout.lineBuffered((stdout) => { + console.log(stdout); + }), + ConsoleStdout.lineBuffered((stderr) => { + console.error(stderr); + }), + new PreopenDirectory("/", new Map()), + ], { debug: false }) +/* #endif */ + return { +/* #if IS_WASI */ + wasi: Object.assign(wasi, { + setInstance(instance) { + wasi.inst = instance; + } + }), +/* #endif */ + threadChannel, + } +} + +/** @type {import('./browser.d.ts').createDefaultWorkerFactory} */ +export function createDefaultWorkerFactory(preludeScript) { + return (tid, startArg, module, memory) => { + const worker = new Worker(new URL("./browser.worker.js", import.meta.url), { + type: "module", + }); + worker.addEventListener("messageerror", (error) => { + console.error(`Worker thread ${tid} error:`, error); + throw error; + }); + worker.postMessage({ module, memory, tid, startArg, preludeScript }); + return worker; + } +} + +class DefaultBrowserThreadRegistry { + workers = new Map(); + nextTid = 1; + + constructor(createWorker) { + this.createWorker = createWorker; + } + + spawnThread(module, memory, startArg) { + const tid = this.nextTid++; + this.workers.set(tid, this.createWorker(tid, startArg, module, memory)); + return tid; + } + + listenMessageFromWorkerThread(tid, listener) { + const worker = this.workers.get(tid); + worker?.addEventListener("message", (event) => { + listener(event.data); + }); + } + + postMessageToWorkerThread(tid, message, transfer) { + const worker = this.workers.get(tid); + worker?.postMessage(message, transfer); + } + + terminateWorkerThread(tid) { + const worker = this.workers.get(tid); + worker.terminate(); + this.workers.delete(tid); + } +} +/* #endif */ + +/** @type {import('./browser.d.ts').defaultBrowserSetup} */ +export async function defaultBrowserSetup(options) { +/* #if IS_WASI */ + const args = options.args ?? [] + const onStdoutLine = options.onStdoutLine ?? ((line) => console.log(line)) + const onStderrLine = options.onStderrLine ?? ((line) => console.error(line)) + const wasi = new WASI(/* args */[MODULE_PATH, ...args], /* env */[], /* fd */[ + new OpenFile(new File([])), // stdin + ConsoleStdout.lineBuffered((stdout) => { + onStdoutLine(stdout); + }), + ConsoleStdout.lineBuffered((stderr) => { + onStderrLine(stderr); + }), + new PreopenDirectory("/", new Map()), + ], { debug: false }) +/* #endif */ +/* #if USE_SHARED_MEMORY */ + const memory = new WebAssembly.Memory(MEMORY_TYPE); + const threadChannel = new DefaultBrowserThreadRegistry(options.spawnWorker) +/* #endif */ + + return { + module: options.module, +/* #if HAS_IMPORTS */ + imports: options.imports, +/* #endif */ +/* #if IS_WASI */ + wasi: Object.assign(wasi, { + setInstance(instance) { + wasi.inst = instance; + } + }), +/* #endif */ +/* #if USE_SHARED_MEMORY */ + memory, threadChannel, +/* #endif */ + } +} diff --git a/Plugins/PackageToJS/Templates/platforms/browser.worker.js b/Plugins/PackageToJS/Templates/platforms/browser.worker.js new file mode 100644 index 000000000..42fe6a2fa --- /dev/null +++ b/Plugins/PackageToJS/Templates/platforms/browser.worker.js @@ -0,0 +1,18 @@ +import { instantiateForThread } from "../instantiate.js" +import { defaultBrowserThreadSetup } from "./browser.js" + +self.onmessage = async (event) => { + const { module, memory, tid, startArg, preludeScript } = event.data; + let options = await defaultBrowserThreadSetup(); + if (preludeScript) { + const prelude = await import(preludeScript); + if (prelude.setupOptions) { + options = prelude.setupOptions(options, { isMainThread: false }) + } + } + await instantiateForThread(tid, startArg, { + ...options, + module, memory, + imports: {}, + }) +} diff --git a/Plugins/PackageToJS/Templates/platforms/node.d.ts b/Plugins/PackageToJS/Templates/platforms/node.d.ts new file mode 100644 index 000000000..9d80205fc --- /dev/null +++ b/Plugins/PackageToJS/Templates/platforms/node.d.ts @@ -0,0 +1,14 @@ +import type { InstantiateOptions } from "../instantiate.js" +import type { Worker } from "node:worker_threads" + +export function defaultNodeSetup(options: { +/* #if IS_WASI */ + args?: string[], +/* #endif */ + onExit?: (code: number) => void, +/* #if USE_SHARED_MEMORY */ + spawnWorker: (module: WebAssembly.Module, memory: WebAssembly.Memory, startArg: any) => Worker, +/* #endif */ +}): Promise + +export function createDefaultWorkerFactory(preludeScript: string): (module: WebAssembly.Module, memory: WebAssembly.Memory, startArg: any) => Worker diff --git a/Plugins/PackageToJS/Templates/platforms/node.js b/Plugins/PackageToJS/Templates/platforms/node.js new file mode 100644 index 000000000..c45bdf354 --- /dev/null +++ b/Plugins/PackageToJS/Templates/platforms/node.js @@ -0,0 +1,198 @@ +// @ts-check +import { fileURLToPath } from "node:url"; +import { Worker, parentPort } from "node:worker_threads"; +import { MODULE_PATH /* #if USE_SHARED_MEMORY */, MEMORY_TYPE /* #endif */} from "../instantiate.js" +/* #if IS_WASI */ +import { WASI, File, OpenFile, ConsoleStdout, PreopenDirectory, Directory, Inode } from '@bjorn3/browser_wasi_shim'; +/* #endif */ + +/* #if USE_SHARED_MEMORY */ +export async function defaultNodeThreadSetup() { + const threadChannel = { + spawnThread: () => { + throw new Error("Cannot spawn a new thread from a worker thread") + }, + postMessageToMainThread: (message, transfer) => { + // @ts-ignore + parentPort.postMessage(message, transfer); + }, + listenMessageFromMainThread: (listener) => { + // @ts-ignore + parentPort.on("message", listener) + } + } + + const wasi = new WASI(/* args */[MODULE_PATH], /* env */[], /* fd */[ + new OpenFile(new File([])), // stdin + ConsoleStdout.lineBuffered((stdout) => { + console.log(stdout); + }), + ConsoleStdout.lineBuffered((stderr) => { + console.error(stderr); + }), + new PreopenDirectory("/", new Map()), + ], { debug: false }) + + return { + wasi: Object.assign(wasi, { + setInstance(instance) { + wasi.inst = instance; + } + }), + threadChannel, + } +} + +export function createDefaultWorkerFactory(preludeScript) { + return (tid, startArg, module, memory) => { + const selfFilePath = new URL(import.meta.url).pathname; + const instantiatePath = fileURLToPath(new URL("../instantiate.js", import.meta.url)); + const worker = new Worker(` + const { parentPort } = require('node:worker_threads'); + + Error.stackTraceLimit = 100; + parentPort.once("message", async (event) => { + const { instantiatePath, selfFilePath, module, memory, tid, startArg, preludeScript } = event; + const { defaultNodeThreadSetup } = await import(selfFilePath); + const { instantiateForThread } = await import(instantiatePath); + let options = await defaultNodeThreadSetup(); + if (preludeScript) { + const prelude = await import(preludeScript); + if (prelude.setupOptions) { + options = prelude.setupOptions(options, { isMainThread: false }) + } + } + await instantiateForThread(tid, startArg, { + ...options, + module, memory, + imports: {}, + }) + }) + `, + { eval: true } + ) + worker.on("error", (error) => { + console.error(`Worker thread ${tid} error:`, error); + throw error; + }); + worker.postMessage({ instantiatePath, selfFilePath, module, memory, tid, startArg, preludeScript }); + return worker; + } +} + +class DefaultNodeThreadRegistry { + workers = new Map(); + nextTid = 1; + + constructor(createWorker) { + this.createWorker = createWorker; + } + + spawnThread(module, memory, startArg) { + const tid = this.nextTid++; + this.workers.set(tid, this.createWorker(tid, startArg, module, memory)); + return tid; + } + + listenMessageFromWorkerThread(tid, listener) { + const worker = this.workers.get(tid); + worker.on("message", listener); + } + + postMessageToWorkerThread(tid, message, transfer) { + const worker = this.workers.get(tid); + worker.postMessage(message, transfer); + } + + terminateWorkerThread(tid) { + const worker = this.workers.get(tid); + worker.terminate(); + this.workers.delete(tid); + } +} +/* #endif */ + +/** @type {import('./node.d.ts').defaultNodeSetup} */ +export async function defaultNodeSetup(options) { + const path = await import("node:path"); + const { fileURLToPath } = await import("node:url"); + const { readFile } = await import("node:fs/promises") + + const args = options.args ?? process.argv.slice(2) + const rootFs = new Map(); + const wasi = new WASI(/* args */[MODULE_PATH, ...args], /* env */[], /* fd */[ + new OpenFile(new File([])), // stdin + ConsoleStdout.lineBuffered((stdout) => { + console.log(stdout); + }), + ConsoleStdout.lineBuffered((stderr) => { + console.error(stderr); + }), + new PreopenDirectory("/", rootFs), + ], { debug: false }) + const pkgDir = path.dirname(path.dirname(fileURLToPath(import.meta.url))) + const module = await WebAssembly.compile(await readFile(path.join(pkgDir, MODULE_PATH))) +/* #if USE_SHARED_MEMORY */ + const memory = new WebAssembly.Memory(MEMORY_TYPE); + const threadChannel = new DefaultNodeThreadRegistry(options.spawnWorker) +/* #endif */ + + return { + module, + imports: {}, +/* #if IS_WASI */ + wasi: Object.assign(wasi, { + setInstance(instance) { + wasi.inst = instance; + }, + /** + * @param {string} path + * @returns {Uint8Array | undefined} + */ + extractFile(path) { + /** + * @param {Map} parent + * @param {string[]} components + * @param {number} index + * @returns {Inode | undefined} + */ + const getFile = (parent, components, index) => { + const name = components[index]; + const entry = parent.get(name); + if (entry === undefined) { + return undefined; + } + if (index === components.length - 1) { + return entry; + } + if (entry instanceof Directory) { + return getFile(entry.contents, components, index + 1); + } + throw new Error(`Expected directory at ${components.slice(0, index).join("/")}`); + } + + const components = path.split("/"); + const file = getFile(rootFs, components, 0); + if (file === undefined) { + return undefined; + } + if (file instanceof File) { + return file.data; + } + return undefined; + } + }), + addToCoreImports(importObject) { + importObject["wasi_snapshot_preview1"]["proc_exit"] = (code) => { + if (options.onExit) { + options.onExit(code); + } + process.exit(code); + } + }, +/* #endif */ +/* #if USE_SHARED_MEMORY */ + memory, threadChannel, +/* #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.browser.html b/Plugins/PackageToJS/Templates/test.browser.html new file mode 100644 index 000000000..35a37c943 --- /dev/null +++ b/Plugins/PackageToJS/Templates/test.browser.html @@ -0,0 +1,33 @@ + + + + + + + + diff --git a/Plugins/PackageToJS/Templates/test.d.ts b/Plugins/PackageToJS/Templates/test.d.ts new file mode 100644 index 000000000..2968f6dd9 --- /dev/null +++ b/Plugins/PackageToJS/Templates/test.d.ts @@ -0,0 +1,12 @@ +import type { InstantiateOptions, instantiate } from "./instantiate"; + +export function testBrowser( + options: { + preludeScript?: string, + args?: string[], + } +): Promise + +export function testBrowserInPage( + options: InstantiateOptions +): ReturnType diff --git a/Plugins/PackageToJS/Templates/test.js b/Plugins/PackageToJS/Templates/test.js new file mode 100644 index 000000000..8c4432492 --- /dev/null +++ b/Plugins/PackageToJS/Templates/test.js @@ -0,0 +1,188 @@ +/** @type {import('./test.d.ts').testBrowser} */ +export async function testBrowser( + options = {}, +) { + const { fileURLToPath } = await import("node:url"); + const path = await import("node:path"); + const fs = await import("node:fs/promises"); + const os = await import("node:os"); + const { existsSync } = await import("node:fs"); + const selfUrl = fileURLToPath(import.meta.url); + const webRoot = path.dirname(selfUrl); + + const http = await import("node:http"); + const defaultContentTypes = { + ".html": "text/html", + ".js": "text/javascript", + ".mjs": "text/javascript", + ".wasm": "application/wasm", + }; + const preludeScriptPath = "/prelude.js" + const server = http.createServer(async (req, res) => { + const url = new URL(req.url, `http://${req.headers.host}`); + const pathname = url.pathname; + const filePath = path.join(webRoot, pathname); + + res.setHeader("Cross-Origin-Embedder-Policy", "require-corp"); + res.setHeader("Cross-Origin-Opener-Policy", "same-origin"); + + if (existsSync(filePath) && (await fs.stat(filePath)).isFile()) { + const data = await fs.readFile(filePath); + const ext = pathname.slice(pathname.lastIndexOf(".")); + const contentType = options.contentTypes?.(pathname) || defaultContentTypes[ext] || "text/plain"; + res.writeHead(200, { "Content-Type": contentType }); + res.end(data); + } else if (pathname === "/process-info.json") { + res.writeHead(200, { "Content-Type": "application/json" }); + const info = { + env: process.env, + args: options.args, + }; + if (options.preludeScript) { + info.preludeScript = preludeScriptPath; + } + res.end(JSON.stringify(info)); + } else if (pathname === preludeScriptPath) { + res.writeHead(200, { "Content-Type": "text/javascript" }); + res.end(await fs.readFile(options.preludeScript, "utf-8")); + } else { + res.writeHead(404); + res.end(); + } + }); + + async function tryListen(port) { + try { + await new Promise((resolve) => { + server.listen({ host: "localhost", port }, () => resolve()); + server.once("error", (error) => { + if (error.code === "EADDRINUSE") { + resolve(null); + } else { + throw error; + } + }); + }); + return server.address(); + } catch { + return null; + } + } + + // Try to listen on port 3000, if it's already in use, try a random available port + let address = await tryListen(3000); + if (!address) { + address = await tryListen(0); + } + + if (options.inspect) { + console.log("Serving test page at http://localhost:" + address.port + "/test.browser.html"); + console.log("Inspect mode: Press Ctrl+C to exit"); + await new Promise((resolve) => process.on("SIGINT", resolve)); + process.exit(128 + os.constants.signals.SIGINT); + } + + const playwright = await (async () => { + try { + // @ts-ignore + return await import("playwright") + } catch { + // Playwright is not available in the current environment + console.error(`Playwright is not available in the current environment. +Please run the following command to install it: + + $ npm install playwright && npx playwright install chromium + `); + process.exit(1); + } + })(); + const browser = await playwright.chromium.launch(); + const context = await browser.newContext(); + const page = await context.newPage(); + + + // Forward console messages in the page to the Node.js console + page.on("console", (message) => { + console.log(message.text()); + }); + + const onExit = new Promise((resolve) => { + page.exposeFunction("exitTest", resolve); + }); + await page.goto(`http://localhost:${address.port}/test.browser.html`); + const exitCode = await onExit; + await browser.close(); + return exitCode; +} + +/** @type {import('./test.d.ts').testBrowserInPage} */ +export async function testBrowserInPage(options, processInfo) { + const exitTest = (code) => { + const fn = window.exitTest; + if (fn) { fn(code); } + } + + const handleError = (error) => { + console.error(error); + exitTest(1); + }; + + // There are 6 cases to exit test + // 1. Successfully finished XCTest with `exit(0)` synchronously + // 2. Unsuccessfully finished XCTest with `exit(non - zero)` synchronously + // 3. Successfully finished XCTest with `exit(0)` asynchronously + // 4. Unsuccessfully finished XCTest with `exit(non - zero)` asynchronously + // 5. Crash by throwing JS exception synchronously + // 6. Crash by throwing JS exception asynchronously + + class ExitError extends Error { + constructor(code) { + super(`Process exited with code ${code}`); + this.code = code; + } + } + const handleExitOrError = (error) => { + if (error instanceof ExitError) { + exitTest(error.code); + } else { + handleError(error) // something wrong happens during test + } + } + + // Handle asynchronous exits (case 3, 4, 6) + window.addEventListener("unhandledrejection", event => { + event.preventDefault(); + const error = event.reason; + handleExitOrError(error); + }); + + const { instantiate } = await import("./instantiate.js"); + let setupOptions = (options, _) => { return options }; + if (processInfo.preludeScript) { + const prelude = await import(processInfo.preludeScript); + if (prelude.setupOptions) { + setupOptions = prelude.setupOptions; + } + } + + options = await setupOptions(options, { isMainThread: true }); + + try { + // Instantiate the WebAssembly file + return await instantiate({ + ...options, + addToCoreImports: (imports) => { + options.addToCoreImports?.(imports); + imports["wasi_snapshot_preview1"]["proc_exit"] = (code) => { + exitTest(code); + throw new ExitError(code); + }; + }, + }); + // When JavaScriptEventLoop executor is still running, + // reachable here without catch (case 3, 4, 6) + } catch (error) { + // Handle synchronous exits (case 1, 2, 5) + handleExitOrError(error); + } +} 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 new file mode 100644 index 000000000..7c41cf3bf --- /dev/null +++ b/Plugins/PackageToJS/Tests/ExampleTests.swift @@ -0,0 +1,271 @@ +import Foundation +import Testing + +@testable import PackageToJS + +extension Trait where Self == ConditionTrait { + static var requireSwiftSDK: ConditionTrait { + .enabled( + if: ProcessInfo.processInfo.environment["SWIFT_SDK_ID"] != nil + && ProcessInfo.processInfo.environment["SWIFT_PATH"] != nil, + "Requires SWIFT_SDK_ID and SWIFT_PATH environment variables" + ) + } + + static func requireSwiftSDK(triple: String) -> ConditionTrait { + .enabled( + if: ProcessInfo.processInfo.environment["SWIFT_SDK_ID"] != nil + && ProcessInfo.processInfo.environment["SWIFT_PATH"] != nil + && ProcessInfo.processInfo.environment["SWIFT_SDK_ID"]!.hasSuffix(triple), + "Requires SWIFT_SDK_ID and SWIFT_PATH environment variables" + ) + } + + static var requireEmbeddedSwift: ConditionTrait { + // Check if $SWIFT_PATH/../lib/swift/embedded/wasm32-unknown-none-wasm/ exists + return .enabled( + if: { + guard let swiftPath = ProcessInfo.processInfo.environment["SWIFT_PATH"] else { + return false + } + let embeddedPath = URL(fileURLWithPath: swiftPath).deletingLastPathComponent() + .appending(path: "lib/swift/embedded/wasm32-unknown-none-wasm") + return FileManager.default.fileExists(atPath: embeddedPath.path) + }(), + "Requires embedded Swift SDK under $SWIFT_PATH/../lib/swift/embedded" + ) + } +} + +@Suite struct ExampleTests { + static func getSwiftSDKID() -> String? { + ProcessInfo.processInfo.environment["SWIFT_SDK_ID"] + } + + static func getSwiftPath() -> String? { + ProcessInfo.processInfo.environment["SWIFT_PATH"] + } + + static let repoPath = URL(fileURLWithPath: #filePath) + .deletingLastPathComponent() + .deletingLastPathComponent() + .deletingLastPathComponent() + .deletingLastPathComponent() + + static func copyRepository(to destination: URL) throws { + try FileManager.default.createDirectory( + atPath: destination.path, + withIntermediateDirectories: true, + attributes: nil + ) + let ignore = [ + ".git", + ".vscode", + ".build", + "node_modules", + ] + + let enumerator = FileManager.default.enumerator(atPath: repoPath.path)! + while let file = enumerator.nextObject() as? String { + let sourcePath = repoPath.appending(path: file) + let destinationPath = destination.appending(path: file) + if ignore.contains(where: { file.hasSuffix($0) }) { + 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 + ) + enumerator.skipDescendants() + continue + } + + // Skip directories + var isDirectory: ObjCBool = false + if FileManager.default.fileExists(atPath: sourcePath.path, isDirectory: &isDirectory) { + if isDirectory.boolValue { + continue + } + } + + do { + try FileManager.default.createDirectory( + at: destinationPath.deletingLastPathComponent(), + withIntermediateDirectories: true, + attributes: nil + ) + try FileManager.default.copyItem(at: sourcePath, to: destinationPath) + } catch { + print("Failed to copy \(sourcePath) to \(destinationPath): \(error)") + throw error + } + } + } + + typealias RunSwift = (_ args: [String], _ env: [String: String]) throws -> Void + + func withPackage(at path: String, body: (URL, _ 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 + let process = Process() + process.executableURL = URL( + fileURLWithPath: "swift", + relativeTo: URL( + fileURLWithPath: try #require(Self.getSwiftPath()) + ) + ) + process.arguments = args + process.currentDirectoryURL = destination.appending(path: path) + process.environment = ProcessInfo.processInfo.environment.merging(env) { _, new in + new + } + let stdoutPath = tempDir.appending(path: "stdout.txt") + let stderrPath = tempDir.appending(path: "stderr.txt") + _ = FileManager.default.createFile(atPath: stdoutPath.path, contents: nil) + _ = FileManager.default.createFile(atPath: stderrPath.path, contents: nil) + process.standardOutput = try FileHandle(forWritingTo: stdoutPath) + process.standardError = try FileHandle(forWritingTo: stderrPath) + + try process.run() + process.waitUntilExit() + if process.terminationStatus != 0 { + retain = true + } + try #require( + process.terminationStatus == 0, + """ + Swift package should build successfully, check \(destination.appending(path: path).path) for details + stdout: \(stdoutPath.path) + stderr: \(stderrPath.path) + + \((try? String(contentsOf: stdoutPath, encoding: .utf8)) ?? "<>") + \((try? String(contentsOf: stderrPath, encoding: .utf8)) ?? "<>") + """ + ) + } + } + } + + @Test(.requireSwiftSDK) + func basic() throws { + let swiftSDKID = try #require(Self.getSwiftSDKID()) + 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"], + [:] + ) + } + } + + @Test(.requireSwiftSDK) + func testing() throws { + let swiftSDKID = try #require(Self.getSwiftSDKID()) + try withPackage(at: "Examples/Testing") { packageDir, runSwift in + 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!'); + """ + 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)"], + [:] + ) + 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 runSwift(["package", "--swift-sdk", swiftSDKID, "js", "test", "--environment", "browser"], [:]) + } + } + + #if compiler(>=6.1) + @Test(.requireSwiftSDK) + 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 + ] + ) + do { + 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() + } + } + } + #endif + + @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 runSwift(["package", "--swift-sdk", swiftSDKID, "js"], [:]) + } + } + + @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 runSwift(["package", "--swift-sdk", swiftSDKID, "js"], [:]) + } + } + + @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 runSwift(["package", "--swift-sdk", swiftSDKID, "js"], [:]) + } + } + + @Test(.requireEmbeddedSwift) func embedded() throws { + try withPackage(at: "Examples/Embedded") { packageDir, runSwift in + try runSwift( + ["package", "--triple", "wasm32-unknown-none-wasm", "js", "-c", "release"], + [ + "JAVASCRIPTKIT_EXPERIMENTAL_EMBEDDED_WASM": "true" + ] + ) + } + } +} diff --git a/Plugins/PackageToJS/Tests/MiniMakeTests.swift b/Plugins/PackageToJS/Tests/MiniMakeTests.swift new file mode 100644 index 000000000..c0bba29c7 --- /dev/null +++ b/Plugins/PackageToJS/Tests/MiniMakeTests.swift @@ -0,0 +1,257 @@ +import Foundation +import Testing + +@testable import PackageToJS + +@Suite struct MiniMakeTests { + // Test basic task management functionality + @Test func basicTaskManagement() throws { + try withTemporaryDirectory { tempDir, _ in + var make = MiniMake(printProgress: { _, _ in }) + let outDir = BuildPath(prefix: "OUTPUT") + + let task = make.addTask(output: outDir.appending(path: "output.txt")) { + 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 + ]) + ) + let content = try String(contentsOfFile: tempDir.appendingPathComponent("output.txt").path, encoding: .utf8) + #expect(content == "Hello") + } + } + + // Test that task dependencies are handled correctly + @Test func taskDependencies() throws { + try withTemporaryDirectory { tempDir, _ in + var make = MiniMake(printProgress: { _, _ in }) + let prefix = BuildPath(prefix: "PREFIX") + let scope = MiniMake.VariableScope(variables: [ + "PREFIX": tempDir.path + ]) + let input = prefix.appending(path: "input.txt") + let intermediate = prefix.appending(path: "intermediate.txt") + let output = prefix.appending(path: "output.txt") + + try "Input".write(toFile: scope.resolve(path: input).path, atomically: true, encoding: .utf8) + + 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 + ) + } + + let finalTask = make.addTask( + 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 + ) + } + + try make.build(output: finalTask, scope: scope) + let content = try String(contentsOfFile: scope.resolve(path: output).path, encoding: .utf8) + #expect(content == "Input processed final") + } + } + + // Test that phony tasks are always rebuilt + @Test func phonyTask() throws { + try withTemporaryDirectory { tempDir, _ in + var make = MiniMake(printProgress: { _, _ in }) + let phonyName = "phony.txt" + let outputPath = BuildPath(prefix: "OUTPUT").appending(path: phonyName) + try "Hello".write(toFile: tempDir.appendingPathComponent(phonyName).path, atomically: true, encoding: .utf8) + var buildCount = 0 + + 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 + ) + } + + let scope = MiniMake.VariableScope(variables: [ + "OUTPUT": tempDir.path + ]) + try make.build(output: task, scope: scope) + try make.build(output: task, scope: scope) + + #expect(buildCount == 2, "Phony task should always rebuild") + } + } + + // Test that the same build graph produces stable fingerprints + @Test func fingerprintStability() throws { + var make1 = MiniMake(printProgress: { _, _ in }) + var make2 = MiniMake(printProgress: { _, _ in }) + + let output1 = BuildPath(prefix: "OUTPUT") + + let task1 = make1.addTask(output: output1) { _, _ in } + let task2 = make2.addTask(output: output1) { _, _ in } + + let fingerprint1 = try make1.computeFingerprint(root: task1) + let fingerprint2 = try make2.computeFingerprint(root: task2) + + #expect(fingerprint1 == fingerprint2, "Same build graph should have same fingerprint") + } + + // Test that rebuilds are controlled by timestamps + @Test func timestampBasedRebuild() throws { + try withTemporaryDirectory { tempDir, _ in + var make = MiniMake(printProgress: { _, _ in }) + let prefix = BuildPath(prefix: "PREFIX") + let scope = MiniMake.VariableScope(variables: [ + "PREFIX": tempDir.path + ]) + let input = prefix.appending(path: "input.txt") + let output = prefix.appending(path: "output.txt") + var buildCount = 0 + + try "Initial".write(toFile: scope.resolve(path: input).path, atomically: true, encoding: .utf8) + + let task = make.addTask(inputFiles: [input], output: output) { task, scope in + buildCount += 1 + let content = try String(contentsOfFile: scope.resolve(path: task.inputs[0]).path, encoding: .utf8) + try content.write(toFile: scope.resolve(path: task.output).path, atomically: true, encoding: .utf8) + } + + // First build + try make.build(output: task, scope: scope) + #expect(buildCount == 1, "First build should occur") + + // Second build without changes + try make.build(output: task, scope: scope) + #expect(buildCount == 1, "No rebuild should occur if input is not modified") + + // Modify input and rebuild + try "Modified".write(toFile: scope.resolve(path: input).path, atomically: true, encoding: .utf8) + try make.build(output: task, scope: scope) + #expect(buildCount == 2, "Should rebuild when input is modified") + } + } + + // Test that silent tasks execute without output + @Test func silentTask() throws { + try withTemporaryDirectory { tempDir, _ in + var messages: [(String, Int, Int, String)] = [] + var make = MiniMake( + printProgress: { ctx, message in + messages.append((ctx.subject.output.description, ctx.total, ctx.built, message)) + } + ) + let prefix = BuildPath(prefix: "PREFIX") + let scope = MiniMake.VariableScope(variables: [ + "PREFIX": tempDir.path + ]) + let silentOutputPath = prefix.appending(path: "silent.txt") + let silentTask = make.addTask(output: silentOutputPath, attributes: [.silent]) { task, scope in + try "Silent".write(toFile: scope.resolve(path: task.output).path, atomically: true, encoding: .utf8) + } + let finalOutputPath = prefix.appending(path: "output.txt") + let task = make.addTask( + 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" + ) + 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")) + } + } + + // Test that error cases are handled appropriately + @Test func errorWhileBuilding() throws { + struct BuildError: Error {} + try withTemporaryDirectory { tempDir, _ in + var make = MiniMake(printProgress: { _, _ in }) + let prefix = BuildPath(prefix: "PREFIX") + let scope = MiniMake.VariableScope(variables: [ + "PREFIX": tempDir.path + ]) + let output = prefix.appending(path: "error.txt") + + let task = make.addTask(output: output) { task, scope in + throw BuildError() + } + + #expect(throws: BuildError.self) { + try make.build(output: task, scope: scope) + } + } + } + + // Test that cleanup functionality works correctly + @Test func cleanup() throws { + try withTemporaryDirectory { tempDir, _ in + var make = MiniMake(printProgress: { _, _ in }) + let prefix = BuildPath(prefix: "PREFIX") + let scope = MiniMake.VariableScope(variables: [ + "PREFIX": tempDir.path + ]) + let outputs = [ + prefix.appending(path: "clean1.txt"), + prefix.appending(path: "clean2.txt"), + ] + + // 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 + ) + } + } + + for task in tasks { + try make.build(output: task, scope: scope) + } + + // Verify files exist + for output in outputs { + #expect( + FileManager.default.fileExists(atPath: scope.resolve(path: output).path), + "Output file should exist before cleanup" + ) + } + + // Clean everything + make.cleanEverything(scope: scope) + + // Verify files are removed + for output in outputs { + #expect( + !FileManager.default.fileExists(atPath: scope.resolve(path: output).path), + "Output file should not exist after cleanup" + ) + } + } + } +} diff --git a/Plugins/PackageToJS/Tests/PackagingPlannerTests.swift b/Plugins/PackageToJS/Tests/PackagingPlannerTests.swift new file mode 100644 index 000000000..03fc4c9cc --- /dev/null +++ b/Plugins/PackageToJS/Tests/PackagingPlannerTests.swift @@ -0,0 +1,115 @@ +import Foundation +import Testing + +@testable import PackageToJS + +@Suite struct PackagingPlannerTests { + struct BuildSnapshot: Codable, Equatable { + let npmInstalls: [String] + } + class TestPackagingSystem: PackagingSystem { + var npmInstallCalls: [String] = [] + func npmInstall(packageDir: String) throws { + npmInstallCalls.append(packageDir) + } + + func wasmOpt(_ arguments: [String], input: String, output: String) throws { + try FileManager.default.copyItem( + at: URL(fileURLWithPath: input), + to: URL(fileURLWithPath: output) + ) + } + } + + func snapshotBuildPlan( + filePath: String = #filePath, + function: String = #function, + sourceLocation: SourceLocation = #_sourceLocation, + variant: String? = nil, + body: (inout MiniMake) throws -> MiniMake.TaskKey + ) throws { + var make = MiniMake(explain: false, printProgress: { _, _ in }) + 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 + ) + } + + typealias DebugInfoFormat = PackageToJS.DebugInfoFormat + + @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_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 { + let options = PackageToJS.PackageOptions() + let system = TestPackagingSystem() + let planner = PackagingPlanner( + options: options, + 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", + configuration: configuration, + triple: "wasm32-unknown-wasi", + selfPath: BuildPath(prefix: "PLANNER_SOURCE_PATH"), + system: system + ) + try snapshotBuildPlan(variant: variant) { make in + try planner.planBuild( + make: &make, + buildOptions: PackageToJS.BuildOptions( + product: "test", + noOptimize: noOptimize, + debugInfoFormat: debugInfoFormat, + packageOptions: options + ) + ) + } + } + + @Test func planTestBuild() throws { + let options = PackageToJS.PackageOptions() + let system = TestPackagingSystem() + let planner = PackagingPlanner( + options: options, + 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", + configuration: "debug", + triple: "wasm32-unknown-wasi", + selfPath: BuildPath(prefix: "PLANNER_SOURCE_PATH"), + system: system + ) + 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 new file mode 100644 index 000000000..6e7e4a1b9 --- /dev/null +++ b/Plugins/PackageToJS/Tests/PreprocessTests.swift @@ -0,0 +1,140 @@ +import Testing + +@testable import PackageToJS + +@Suite struct PreprocessTests { + @Test func thenBlock() throws { + let source = """ + /* #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") + } + + @Test func elseBlock() throws { + let source = """ + /* #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") + } + + @Test func onelineIf() throws { + let source = """ + /* #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\");") + } + + @Test func undefinedVariable() throws { + let source = """ + /* #if FOO */ + /* #endif */ + """ + let options = PreprocessOptions(conditions: [:]) + #expect(throws: Error.self) { + try preprocess(source: source, options: options) + } + } + + @Test func substitution() throws { + let source = "@FOO@" + let options = PreprocessOptions(substitutions: ["FOO": "BAR"]) + let result = try preprocess(source: source, options: options) + #expect(result == "BAR") + } + + @Test func missingEndOfDirective() throws { + let source = """ + /* #if FOO + """ + #expect(throws: Error.self) { + try preprocess(source: source, options: PreprocessOptions()) + } + } + + @Test(arguments: [ + (foo: true, bar: true, expected: "console.log(\"FOO\");\nconsole.log(\"FOO & BAR\");\n"), + (foo: true, bar: false, expected: "console.log(\"FOO\");\nconsole.log(\"FOO & !BAR\");\n"), + (foo: false, bar: true, expected: "console.log(\"!FOO\");\n"), + (foo: false, bar: false, expected: "console.log(\"!FOO\");\n"), + ]) + 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 */ + """ + let options = PreprocessOptions(conditions: ["FOO": foo, "BAR": bar]) + let result = try preprocess(source: source, options: options) + #expect(result == expected) + } + + @Test func multipleSubstitutions() throws { + let source = """ + const name = "@NAME@"; + const version = "@VERSION@"; + """ + let options = PreprocessOptions(substitutions: [ + "NAME": "MyApp", + "VERSION": "1.0.0", + ]) + let result = try preprocess(source: source, options: options) + #expect( + result == """ + const name = "MyApp"; + const version = "1.0.0"; + """ + ) + } + + @Test func invalidVariableName() throws { + let source = """ + /* #if invalid-name */ + console.log("error"); + /* #endif */ + """ + #expect(throws: Error.self) { + try preprocess(source: source, options: PreprocessOptions()) + } + } + + @Test func emptyBlocks() throws { + let source = """ + /* #if FOO */ + /* #else */ + /* #endif */ + """ + let options = PreprocessOptions(conditions: ["FOO": true]) + let result = try preprocess(source: source, options: options) + #expect(result == "") + } + + @Test func ignoreNonDirectiveComments() throws { + let source = """ + /* 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 new file mode 100644 index 000000000..e900954ff --- /dev/null +++ b/Plugins/PackageToJS/Tests/SnapshotTesting.swift @@ -0,0 +1,37 @@ +import Foundation +import Testing + +func assertSnapshot( + 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 snapshotFileName: String = + "\(function[.. Comment { + "Snapshot mismatch: \(actualFilePath) \(snapshotPath.path)" + } + if !ok { + try input.write(to: URL(fileURLWithPath: actualFilePath)) + } + #expect(ok, buildComment(), sourceLocation: sourceLocation) + } else { + try input.write(to: snapshotPath) + #expect(Bool(false), "Snapshot created at \(snapshotPath.path)", sourceLocation: sourceLocation) + } +} diff --git a/Plugins/PackageToJS/Tests/TemplatesTests.swift b/Plugins/PackageToJS/Tests/TemplatesTests.swift new file mode 100644 index 000000000..e885eb087 --- /dev/null +++ b/Plugins/PackageToJS/Tests/TemplatesTests.swift @@ -0,0 +1,20 @@ +import Testing +import Foundation +@testable import PackageToJS + +@Suite struct TemplatesTests { + static let templatesPath = URL(fileURLWithPath: #filePath) + .deletingLastPathComponent() + .deletingLastPathComponent() + .appendingPathComponent("Templates") + + /// `npx tsc -p Templates/tsconfig.json` + @Test func tscCheck() throws { + let tsc = Process() + tsc.executableURL = try which("npx") + tsc.arguments = ["tsc", "-p", Self.templatesPath.appending(path: "tsconfig.json").path] + try tsc.run() + tsc.waitUntilExit() + #expect(tsc.terminationStatus == 0) + } +} diff --git a/Plugins/PackageToJS/Tests/TemporaryDirectory.swift b/Plugins/PackageToJS/Tests/TemporaryDirectory.swift new file mode 100644 index 000000000..199380fac --- /dev/null +++ b/Plugins/PackageToJS/Tests/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/PackageToJS/Tests/TestsParserTests.swift b/Plugins/PackageToJS/Tests/TestsParserTests.swift new file mode 100644 index 000000000..7b1972d12 --- /dev/null +++ b/Plugins/PackageToJS/Tests/TestsParserTests.swift @@ -0,0 +1,134 @@ +import Foundation +import Testing + +@testable import PackageToJS + +@Suite struct TestsParserTests { + func assertFancyFormatSnapshot( + _ input: String, + filePath: String = #filePath, + function: String = #function, + sourceLocation: SourceLocation = #_sourceLocation + ) throws { + var output = "" + let parser = FancyTestsParser(write: { output += $0 }) + let lines = input.split(separator: "\n", omittingEmptySubsequences: false) + + for line in lines { + parser.onLine(String(line)) + } + parser.finalize() + try assertSnapshot( + filePath: filePath, + function: function, + sourceLocation: sourceLocation, + input: Data(output.utf8), + fileExtension: "txt" + ) + } + + @Test func testAllPassed() throws { + try assertFancyFormatSnapshot( + """ + Test Suite 'All tests' started at 2025-03-16 08:10:01.946 + Test Suite '/.xctest' started at 2025-03-16 08:10:01.967 + Test Suite 'CounterTests' started at 2025-03-16 08:10:01.967 + Test Case 'CounterTests.testIncrement' started at 2025-03-16 08:10:01.967 + Test Case 'CounterTests.testIncrement' passed (0.002 seconds) + Test Case 'CounterTests.testIncrementTwice' started at 2025-03-16 08:10:01.969 + Test Case 'CounterTests.testIncrementTwice' passed (0.001 seconds) + Test Suite 'CounterTests' passed at 2025-03-16 08:10:01.970 + Executed 2 tests, with 0 failures (0 unexpected) in 0.003 (0.003) seconds + Test Suite '/.xctest' passed at 2025-03-16 08:10:01.970 + Executed 2 tests, with 0 failures (0 unexpected) in 0.003 (0.003) seconds + Test Suite 'All tests' passed at 2025-03-16 08:10:01.970 + Executed 2 tests, with 0 failures (0 unexpected) in 0.003 (0.003) seconds + """ + ) + } + + @Test func testThrowFailure() throws { + try assertFancyFormatSnapshot( + """ + Test Suite 'All tests' started at 2025-03-16 08:40:27.267 + Test Suite '/.xctest' started at 2025-03-16 08:40:27.287 + Test Suite 'CounterTests' started at 2025-03-16 08:40:27.287 + Test Case 'CounterTests.testThrowFailure' started at 2025-03-16 08:40:27.287 + :0: error: CounterTests.testThrowFailure : threw error "TestError()" + Test Case 'CounterTests.testThrowFailure' failed (0.002 seconds) + Test Suite 'CounterTests' failed at 2025-03-16 08:40:27.290 + Executed 1 test, with 1 failure (1 unexpected) in 0.002 (0.002) seconds + Test Suite '/.xctest' failed at 2025-03-16 08:40:27.290 + Executed 1 test, with 1 failure (1 unexpected) in 0.002 (0.002) seconds + Test Suite 'All tests' failed at 2025-03-16 08:40:27.290 + Executed 1 test, with 1 failure (1 unexpected) in 0.002 (0.002) seconds + """ + ) + } + + @Test func testAssertFailure() throws { + try assertFancyFormatSnapshot( + """ + Test Suite 'All tests' started at 2025-03-16 08:43:32.415 + Test Suite '/.xctest' started at 2025-03-16 08:43:32.465 + Test Suite 'CounterTests' started at 2025-03-16 08:43:32.465 + Test Case 'CounterTests.testAssertailure' started at 2025-03-16 08:43:32.465 + /tmp/Tests/CounterTests/CounterTests.swift:27: error: CounterTests.testAssertailure : XCTAssertEqual failed: ("1") is not equal to ("2") - + Test Case 'CounterTests.testAssertailure' failed (0.001 seconds) + Test Suite 'CounterTests' failed at 2025-03-16 08:43:32.467 + Executed 1 test, with 1 failure (0 unexpected) in 0.001 (0.001) seconds + Test Suite '/.xctest' failed at 2025-03-16 08:43:32.467 + Executed 1 test, with 1 failure (0 unexpected) in 0.001 (0.001) seconds + Test Suite 'All tests' failed at 2025-03-16 08:43:32.468 + Executed 1 test, with 1 failure (0 unexpected) in 0.001 (0.001) seconds + """ + ) + } + + @Test func testSkipped() throws { + try assertFancyFormatSnapshot( + """ + Test Suite 'All tests' started at 2025-03-16 09:56:50.924 + Test Suite '/.xctest' started at 2025-03-16 09:56:50.945 + Test Suite 'CounterTests' started at 2025-03-16 09:56:50.945 + Test Case 'CounterTests.testIncrement' started at 2025-03-16 09:56:50.946 + /tmp/Tests/CounterTests/CounterTests.swift:25: CounterTests.testIncrement : Test skipped - Skip it + Test Case 'CounterTests.testIncrement' skipped (0.006 seconds) + Test Case 'CounterTests.testIncrementTwice' started at 2025-03-16 09:56:50.953 + Test Case 'CounterTests.testIncrementTwice' passed (0.0 seconds) + Test Suite 'CounterTests' passed at 2025-03-16 09:56:50.953 + Executed 2 tests, with 1 test skipped and 0 failures (0 unexpected) in 0.006 (0.006) seconds + Test Suite '/.xctest' passed at 2025-03-16 09:56:50.954 + Executed 2 tests, with 1 test skipped and 0 failures (0 unexpected) in 0.006 (0.006) seconds + Test Suite 'All tests' passed at 2025-03-16 09:56:50.954 + Executed 2 tests, with 1 test skipped and 0 failures (0 unexpected) in 0.006 (0.006) seconds + """ + ) + } + + @Test func testCrash() throws { + try assertFancyFormatSnapshot( + """ + Test Suite 'All tests' started at 2025-03-16 09:37:07.882 + Test Suite '/.xctest' started at 2025-03-16 09:37:07.903 + Test Suite 'CounterTests' started at 2025-03-16 09:37:07.903 + Test Case 'CounterTests.testIncrement' started at 2025-03-16 09:37:07.903 + CounterTests/CounterTests.swift:26: Fatal error: Crash + wasm://wasm/CounterPackageTests.xctest-0ef3150a:1 + + + RuntimeError: unreachable + at CounterPackageTests.xctest.$ss17_assertionFailure__4file4line5flagss5NeverOs12StaticStringV_SSAHSus6UInt32VtF (wasm://wasm/CounterPackageTests.xctest-0ef3150a:wasm-function[5087]:0x1475da) + at CounterPackageTests.xctest.$s12CounterTestsAAC13testIncrementyyYaKFTY1_ (wasm://wasm/CounterPackageTests.xctest-0ef3150a:wasm-function[1448]:0x9a33b) + at CounterPackageTests.xctest.swift::runJobInEstablishedExecutorContext(swift::Job*) (wasm://wasm/CounterPackageTests.xctest-0ef3150a:wasm-function[29848]:0x58cb39) + at CounterPackageTests.xctest.swift_job_run (wasm://wasm/CounterPackageTests.xctest-0ef3150a:wasm-function[29863]:0x58d720) + at CounterPackageTests.xctest.$sScJ16runSynchronously2onySce_tF (wasm://wasm/CounterPackageTests.xctest-0ef3150a:wasm-function[1571]:0x9fe5a) + at CounterPackageTests.xctest.$s19JavaScriptEventLoopAAC10runAllJobsyyF (wasm://wasm/CounterPackageTests.xctest-0ef3150a:wasm-function[1675]:0xa32c4) + at CounterPackageTests.xctest.$s19JavaScriptEventLoopAAC14insertJobQueue3jobyScJ_tFyycfU0_ (wasm://wasm/CounterPackageTests.xctest-0ef3150a:wasm-function[1674]:0xa30b7) + at CounterPackageTests.xctest.$s19JavaScriptEventLoopAAC14insertJobQueue3jobyScJ_tFyycfU0_TA (wasm://wasm/CounterPackageTests.xctest-0ef3150a:wasm-function[1666]:0xa2c6b) + at CounterPackageTests.xctest.$s19JavaScriptEventLoopAAC6create33_F9DB15AFB1FFBEDBFE9D13500E01F3F2LLAByFZyyyccfU0_0aB3Kit20ConvertibleToJSValue_pAE0Q0OcfU_ (wasm://wasm/CounterPackageTests.xctest-0ef3150a:wasm-function[1541]:0x9de13) + at CounterPackageTests.xctest.$s19JavaScriptEventLoopAAC6create33_F9DB15AFB1FFBEDBFE9D13500E01F3F2LLAByFZyyyccfU0_0aB3Kit20ConvertibleToJSValue_pAE0Q0OcfU_TA (wasm://wasm/CounterPackageTests.xctest-0ef3150a:wasm-function[1540]:0x9dd8d) + """ + ) + } +} diff --git a/Plugins/PackageToJS/Tests/__Snapshots__/PackagingPlannerTests/planBuild_debug.json b/Plugins/PackageToJS/Tests/__Snapshots__/PackagingPlannerTests/planBuild_debug.json new file mode 100644 index 000000000..13768da75 --- /dev/null +++ b/Plugins/PackageToJS/Tests/__Snapshots__/PackagingPlannerTests/planBuild_debug.json @@ -0,0 +1,293 @@ +[ + { + "attributes" : [ + "silent" + ], + "inputs" : [ + "$PLANNER_SOURCE_PATH" + ], + "output" : "$INTERMEDIATES", + "wants" : [ + + ] + }, + { + "attributes" : [ + + ], + "inputs" : [ + "$PLANNER_SOURCE_PATH", + "$OUTPUT\/main.wasm" + ], + "output" : "$INTERMEDIATES\/wasm-imports.json", + "wants" : [ + "$OUTPUT", + "$INTERMEDIATES", + "$OUTPUT\/main.wasm" + ] + }, + { + "attributes" : [ + "silent" + ], + "inputs" : [ + "$PLANNER_SOURCE_PATH" + ], + "output" : "$OUTPUT", + "wants" : [ + + ] + }, + { + "attributes" : [ + + ], + "inputs" : [ + "$PLANNER_SOURCE_PATH", + "$SELF_PACKAGE\/Plugins\/PackageToJS\/Templates\/index.d.ts", + "$INTERMEDIATES\/wasm-imports.json" + ], + "output" : "$OUTPUT\/index.d.ts", + "salt" : "eyJjb25kaXRpb25zIjp7IkhBU19CUklER0UiOmZhbHNlLCJIQVNfSU1QT1JUUyI6ZmFsc2UsIklTX1dBU0kiOnRydWUsIlVTRV9TSEFSRURfTUVNT1JZIjpmYWxzZSwiVVNFX1dBU0lfQ0ROIjpmYWxzZX0sInN1YnN0aXR1dGlvbnMiOnsiUEFDS0FHRV9UT19KU19NT0RVTEVfUEFUSCI6Im1haW4ud2FzbSIsIlBBQ0tBR0VfVE9fSlNfUEFDS0FHRV9OQU1FIjoidGVzdCJ9fQ==", + "wants" : [ + "$OUTPUT", + "$OUTPUT\/platforms", + "$INTERMEDIATES\/wasm-imports.json" + ] + }, + { + "attributes" : [ + + ], + "inputs" : [ + "$PLANNER_SOURCE_PATH", + "$SELF_PACKAGE\/Plugins\/PackageToJS\/Templates\/index.js", + "$INTERMEDIATES\/wasm-imports.json" + ], + "output" : "$OUTPUT\/index.js", + "salt" : "eyJjb25kaXRpb25zIjp7IkhBU19CUklER0UiOmZhbHNlLCJIQVNfSU1QT1JUUyI6ZmFsc2UsIklTX1dBU0kiOnRydWUsIlVTRV9TSEFSRURfTUVNT1JZIjpmYWxzZSwiVVNFX1dBU0lfQ0ROIjpmYWxzZX0sInN1YnN0aXR1dGlvbnMiOnsiUEFDS0FHRV9UT19KU19NT0RVTEVfUEFUSCI6Im1haW4ud2FzbSIsIlBBQ0tBR0VfVE9fSlNfUEFDS0FHRV9OQU1FIjoidGVzdCJ9fQ==", + "wants" : [ + "$OUTPUT", + "$OUTPUT\/platforms", + "$INTERMEDIATES\/wasm-imports.json" + ] + }, + { + "attributes" : [ + + ], + "inputs" : [ + "$PLANNER_SOURCE_PATH", + "$SELF_PACKAGE\/Plugins\/PackageToJS\/Templates\/instantiate.d.ts", + "$INTERMEDIATES\/wasm-imports.json" + ], + "output" : "$OUTPUT\/instantiate.d.ts", + "salt" : "eyJjb25kaXRpb25zIjp7IkhBU19CUklER0UiOmZhbHNlLCJIQVNfSU1QT1JUUyI6ZmFsc2UsIklTX1dBU0kiOnRydWUsIlVTRV9TSEFSRURfTUVNT1JZIjpmYWxzZSwiVVNFX1dBU0lfQ0ROIjpmYWxzZX0sInN1YnN0aXR1dGlvbnMiOnsiUEFDS0FHRV9UT19KU19NT0RVTEVfUEFUSCI6Im1haW4ud2FzbSIsIlBBQ0tBR0VfVE9fSlNfUEFDS0FHRV9OQU1FIjoidGVzdCJ9fQ==", + "wants" : [ + "$OUTPUT", + "$OUTPUT\/platforms", + "$INTERMEDIATES\/wasm-imports.json" + ] + }, + { + "attributes" : [ + + ], + "inputs" : [ + "$PLANNER_SOURCE_PATH", + "$SELF_PACKAGE\/Plugins\/PackageToJS\/Templates\/instantiate.js", + "$INTERMEDIATES\/wasm-imports.json" + ], + "output" : "$OUTPUT\/instantiate.js", + "salt" : "eyJjb25kaXRpb25zIjp7IkhBU19CUklER0UiOmZhbHNlLCJIQVNfSU1QT1JUUyI6ZmFsc2UsIklTX1dBU0kiOnRydWUsIlVTRV9TSEFSRURfTUVNT1JZIjpmYWxzZSwiVVNFX1dBU0lfQ0ROIjpmYWxzZX0sInN1YnN0aXR1dGlvbnMiOnsiUEFDS0FHRV9UT19KU19NT0RVTEVfUEFUSCI6Im1haW4ud2FzbSIsIlBBQ0tBR0VfVE9fSlNfUEFDS0FHRV9OQU1FIjoidGVzdCJ9fQ==", + "wants" : [ + "$OUTPUT", + "$OUTPUT\/platforms", + "$INTERMEDIATES\/wasm-imports.json" + ] + }, + { + "attributes" : [ + + ], + "inputs" : [ + "$PLANNER_SOURCE_PATH", + "$WASM_PRODUCT_ARTIFACT" + ], + "output" : "$OUTPUT\/main.wasm", + "wants" : [ + "$OUTPUT" + ] + }, + { + "attributes" : [ + + ], + "inputs" : [ + "$PLANNER_SOURCE_PATH", + "$SELF_PACKAGE\/Plugins\/PackageToJS\/Templates\/package.json" + ], + "output" : "$OUTPUT\/package.json", + "salt" : "eyJjb25kaXRpb25zIjp7IkhBU19CUklER0UiOmZhbHNlLCJIQVNfSU1QT1JUUyI6ZmFsc2UsIklTX1dBU0kiOnRydWUsIlVTRV9TSEFSRURfTUVNT1JZIjpmYWxzZSwiVVNFX1dBU0lfQ0ROIjpmYWxzZX0sInN1YnN0aXR1dGlvbnMiOnsiUEFDS0FHRV9UT19KU19NT0RVTEVfUEFUSCI6Im1haW4ud2FzbSIsIlBBQ0tBR0VfVE9fSlNfUEFDS0FHRV9OQU1FIjoidGVzdCJ9fQ==", + "wants" : [ + "$OUTPUT" + ] + }, + { + "attributes" : [ + "silent" + ], + "inputs" : [ + "$PLANNER_SOURCE_PATH" + ], + "output" : "$OUTPUT\/platforms", + "wants" : [ + + ] + }, + { + "attributes" : [ + + ], + "inputs" : [ + "$PLANNER_SOURCE_PATH", + "$SELF_PACKAGE\/Plugins\/PackageToJS\/Templates\/platforms\/browser.d.ts", + "$INTERMEDIATES\/wasm-imports.json" + ], + "output" : "$OUTPUT\/platforms\/browser.d.ts", + "salt" : "eyJjb25kaXRpb25zIjp7IkhBU19CUklER0UiOmZhbHNlLCJIQVNfSU1QT1JUUyI6ZmFsc2UsIklTX1dBU0kiOnRydWUsIlVTRV9TSEFSRURfTUVNT1JZIjpmYWxzZSwiVVNFX1dBU0lfQ0ROIjpmYWxzZX0sInN1YnN0aXR1dGlvbnMiOnsiUEFDS0FHRV9UT19KU19NT0RVTEVfUEFUSCI6Im1haW4ud2FzbSIsIlBBQ0tBR0VfVE9fSlNfUEFDS0FHRV9OQU1FIjoidGVzdCJ9fQ==", + "wants" : [ + "$OUTPUT", + "$OUTPUT\/platforms", + "$INTERMEDIATES\/wasm-imports.json" + ] + }, + { + "attributes" : [ + + ], + "inputs" : [ + "$PLANNER_SOURCE_PATH", + "$SELF_PACKAGE\/Plugins\/PackageToJS\/Templates\/platforms\/browser.js", + "$INTERMEDIATES\/wasm-imports.json" + ], + "output" : "$OUTPUT\/platforms\/browser.js", + "salt" : "eyJjb25kaXRpb25zIjp7IkhBU19CUklER0UiOmZhbHNlLCJIQVNfSU1QT1JUUyI6ZmFsc2UsIklTX1dBU0kiOnRydWUsIlVTRV9TSEFSRURfTUVNT1JZIjpmYWxzZSwiVVNFX1dBU0lfQ0ROIjpmYWxzZX0sInN1YnN0aXR1dGlvbnMiOnsiUEFDS0FHRV9UT19KU19NT0RVTEVfUEFUSCI6Im1haW4ud2FzbSIsIlBBQ0tBR0VfVE9fSlNfUEFDS0FHRV9OQU1FIjoidGVzdCJ9fQ==", + "wants" : [ + "$OUTPUT", + "$OUTPUT\/platforms", + "$INTERMEDIATES\/wasm-imports.json" + ] + }, + { + "attributes" : [ + + ], + "inputs" : [ + "$PLANNER_SOURCE_PATH", + "$SELF_PACKAGE\/Plugins\/PackageToJS\/Templates\/platforms\/browser.worker.js", + "$INTERMEDIATES\/wasm-imports.json" + ], + "output" : "$OUTPUT\/platforms\/browser.worker.js", + "salt" : "eyJjb25kaXRpb25zIjp7IkhBU19CUklER0UiOmZhbHNlLCJIQVNfSU1QT1JUUyI6ZmFsc2UsIklTX1dBU0kiOnRydWUsIlVTRV9TSEFSRURfTUVNT1JZIjpmYWxzZSwiVVNFX1dBU0lfQ0ROIjpmYWxzZX0sInN1YnN0aXR1dGlvbnMiOnsiUEFDS0FHRV9UT19KU19NT0RVTEVfUEFUSCI6Im1haW4ud2FzbSIsIlBBQ0tBR0VfVE9fSlNfUEFDS0FHRV9OQU1FIjoidGVzdCJ9fQ==", + "wants" : [ + "$OUTPUT", + "$OUTPUT\/platforms", + "$INTERMEDIATES\/wasm-imports.json" + ] + }, + { + "attributes" : [ + + ], + "inputs" : [ + "$PLANNER_SOURCE_PATH", + "$SELF_PACKAGE\/Plugins\/PackageToJS\/Templates\/platforms\/node.d.ts", + "$INTERMEDIATES\/wasm-imports.json" + ], + "output" : "$OUTPUT\/platforms\/node.d.ts", + "salt" : "eyJjb25kaXRpb25zIjp7IkhBU19CUklER0UiOmZhbHNlLCJIQVNfSU1QT1JUUyI6ZmFsc2UsIklTX1dBU0kiOnRydWUsIlVTRV9TSEFSRURfTUVNT1JZIjpmYWxzZSwiVVNFX1dBU0lfQ0ROIjpmYWxzZX0sInN1YnN0aXR1dGlvbnMiOnsiUEFDS0FHRV9UT19KU19NT0RVTEVfUEFUSCI6Im1haW4ud2FzbSIsIlBBQ0tBR0VfVE9fSlNfUEFDS0FHRV9OQU1FIjoidGVzdCJ9fQ==", + "wants" : [ + "$OUTPUT", + "$OUTPUT\/platforms", + "$INTERMEDIATES\/wasm-imports.json" + ] + }, + { + "attributes" : [ + + ], + "inputs" : [ + "$PLANNER_SOURCE_PATH", + "$SELF_PACKAGE\/Plugins\/PackageToJS\/Templates\/platforms\/node.js", + "$INTERMEDIATES\/wasm-imports.json" + ], + "output" : "$OUTPUT\/platforms\/node.js", + "salt" : "eyJjb25kaXRpb25zIjp7IkhBU19CUklER0UiOmZhbHNlLCJIQVNfSU1QT1JUUyI6ZmFsc2UsIklTX1dBU0kiOnRydWUsIlVTRV9TSEFSRURfTUVNT1JZIjpmYWxzZSwiVVNFX1dBU0lfQ0ROIjpmYWxzZX0sInN1YnN0aXR1dGlvbnMiOnsiUEFDS0FHRV9UT19KU19NT0RVTEVfUEFUSCI6Im1haW4ud2FzbSIsIlBBQ0tBR0VfVE9fSlNfUEFDS0FHRV9OQU1FIjoidGVzdCJ9fQ==", + "wants" : [ + "$OUTPUT", + "$OUTPUT\/platforms", + "$INTERMEDIATES\/wasm-imports.json" + ] + }, + { + "attributes" : [ + + ], + "inputs" : [ + "$PLANNER_SOURCE_PATH", + "$SELF_PACKAGE\/Sources\/JavaScriptKit\/Runtime\/index.d.ts", + "$INTERMEDIATES\/wasm-imports.json" + ], + "output" : "$OUTPUT\/runtime.d.ts", + "salt" : "eyJjb25kaXRpb25zIjp7IkhBU19CUklER0UiOmZhbHNlLCJIQVNfSU1QT1JUUyI6ZmFsc2UsIklTX1dBU0kiOnRydWUsIlVTRV9TSEFSRURfTUVNT1JZIjpmYWxzZSwiVVNFX1dBU0lfQ0ROIjpmYWxzZX0sInN1YnN0aXR1dGlvbnMiOnsiUEFDS0FHRV9UT19KU19NT0RVTEVfUEFUSCI6Im1haW4ud2FzbSIsIlBBQ0tBR0VfVE9fSlNfUEFDS0FHRV9OQU1FIjoidGVzdCJ9fQ==", + "wants" : [ + "$OUTPUT", + "$OUTPUT\/platforms", + "$INTERMEDIATES\/wasm-imports.json" + ] + }, + { + "attributes" : [ + + ], + "inputs" : [ + "$PLANNER_SOURCE_PATH", + "$SELF_PACKAGE\/Sources\/JavaScriptKit\/Runtime\/index.mjs", + "$INTERMEDIATES\/wasm-imports.json" + ], + "output" : "$OUTPUT\/runtime.js", + "salt" : "eyJjb25kaXRpb25zIjp7IkhBU19CUklER0UiOmZhbHNlLCJIQVNfSU1QT1JUUyI6ZmFsc2UsIklTX1dBU0kiOnRydWUsIlVTRV9TSEFSRURfTUVNT1JZIjpmYWxzZSwiVVNFX1dBU0lfQ0ROIjpmYWxzZX0sInN1YnN0aXR1dGlvbnMiOnsiUEFDS0FHRV9UT19KU19NT0RVTEVfUEFUSCI6Im1haW4ud2FzbSIsIlBBQ0tBR0VfVE9fSlNfUEFDS0FHRV9OQU1FIjoidGVzdCJ9fQ==", + "wants" : [ + "$OUTPUT", + "$OUTPUT\/platforms", + "$INTERMEDIATES\/wasm-imports.json" + ] + }, + { + "attributes" : [ + "phony", + "silent" + ], + "inputs" : [ + + ], + "output" : "all", + "wants" : [ + "$OUTPUT\/main.wasm", + "$INTERMEDIATES\/wasm-imports.json", + "$OUTPUT\/package.json", + "$OUTPUT\/index.js", + "$OUTPUT\/index.d.ts", + "$OUTPUT\/instantiate.js", + "$OUTPUT\/instantiate.d.ts", + "$OUTPUT\/platforms\/browser.js", + "$OUTPUT\/platforms\/browser.d.ts", + "$OUTPUT\/platforms\/browser.worker.js", + "$OUTPUT\/platforms\/node.js", + "$OUTPUT\/platforms\/node.d.ts", + "$OUTPUT\/runtime.js", + "$OUTPUT\/runtime.d.ts" + ] + } +] \ No newline at end of file diff --git a/Plugins/PackageToJS/Tests/__Snapshots__/PackagingPlannerTests/planBuild_release.json b/Plugins/PackageToJS/Tests/__Snapshots__/PackagingPlannerTests/planBuild_release.json new file mode 100644 index 000000000..ccfbc35cc --- /dev/null +++ b/Plugins/PackageToJS/Tests/__Snapshots__/PackagingPlannerTests/planBuild_release.json @@ -0,0 +1,308 @@ +[ + { + "attributes" : [ + "silent" + ], + "inputs" : [ + "$PLANNER_SOURCE_PATH" + ], + "output" : "$INTERMEDIATES", + "wants" : [ + + ] + }, + { + "attributes" : [ + + ], + "inputs" : [ + "$PLANNER_SOURCE_PATH", + "$WASM_PRODUCT_ARTIFACT" + ], + "output" : "$INTERMEDIATES\/main.wasm.no-dwarf", + "wants" : [ + "$OUTPUT", + "$INTERMEDIATES" + ] + }, + { + "attributes" : [ + + ], + "inputs" : [ + "$PLANNER_SOURCE_PATH", + "$OUTPUT\/main.wasm" + ], + "output" : "$INTERMEDIATES\/wasm-imports.json", + "wants" : [ + "$OUTPUT", + "$INTERMEDIATES", + "$OUTPUT\/main.wasm" + ] + }, + { + "attributes" : [ + "silent" + ], + "inputs" : [ + "$PLANNER_SOURCE_PATH" + ], + "output" : "$OUTPUT", + "wants" : [ + + ] + }, + { + "attributes" : [ + + ], + "inputs" : [ + "$PLANNER_SOURCE_PATH", + "$SELF_PACKAGE\/Plugins\/PackageToJS\/Templates\/index.d.ts", + "$INTERMEDIATES\/wasm-imports.json" + ], + "output" : "$OUTPUT\/index.d.ts", + "salt" : "eyJjb25kaXRpb25zIjp7IkhBU19CUklER0UiOmZhbHNlLCJIQVNfSU1QT1JUUyI6ZmFsc2UsIklTX1dBU0kiOnRydWUsIlVTRV9TSEFSRURfTUVNT1JZIjpmYWxzZSwiVVNFX1dBU0lfQ0ROIjpmYWxzZX0sInN1YnN0aXR1dGlvbnMiOnsiUEFDS0FHRV9UT19KU19NT0RVTEVfUEFUSCI6Im1haW4ud2FzbSIsIlBBQ0tBR0VfVE9fSlNfUEFDS0FHRV9OQU1FIjoidGVzdCJ9fQ==", + "wants" : [ + "$OUTPUT", + "$OUTPUT\/platforms", + "$INTERMEDIATES\/wasm-imports.json" + ] + }, + { + "attributes" : [ + + ], + "inputs" : [ + "$PLANNER_SOURCE_PATH", + "$SELF_PACKAGE\/Plugins\/PackageToJS\/Templates\/index.js", + "$INTERMEDIATES\/wasm-imports.json" + ], + "output" : "$OUTPUT\/index.js", + "salt" : "eyJjb25kaXRpb25zIjp7IkhBU19CUklER0UiOmZhbHNlLCJIQVNfSU1QT1JUUyI6ZmFsc2UsIklTX1dBU0kiOnRydWUsIlVTRV9TSEFSRURfTUVNT1JZIjpmYWxzZSwiVVNFX1dBU0lfQ0ROIjpmYWxzZX0sInN1YnN0aXR1dGlvbnMiOnsiUEFDS0FHRV9UT19KU19NT0RVTEVfUEFUSCI6Im1haW4ud2FzbSIsIlBBQ0tBR0VfVE9fSlNfUEFDS0FHRV9OQU1FIjoidGVzdCJ9fQ==", + "wants" : [ + "$OUTPUT", + "$OUTPUT\/platforms", + "$INTERMEDIATES\/wasm-imports.json" + ] + }, + { + "attributes" : [ + + ], + "inputs" : [ + "$PLANNER_SOURCE_PATH", + "$SELF_PACKAGE\/Plugins\/PackageToJS\/Templates\/instantiate.d.ts", + "$INTERMEDIATES\/wasm-imports.json" + ], + "output" : "$OUTPUT\/instantiate.d.ts", + "salt" : "eyJjb25kaXRpb25zIjp7IkhBU19CUklER0UiOmZhbHNlLCJIQVNfSU1QT1JUUyI6ZmFsc2UsIklTX1dBU0kiOnRydWUsIlVTRV9TSEFSRURfTUVNT1JZIjpmYWxzZSwiVVNFX1dBU0lfQ0ROIjpmYWxzZX0sInN1YnN0aXR1dGlvbnMiOnsiUEFDS0FHRV9UT19KU19NT0RVTEVfUEFUSCI6Im1haW4ud2FzbSIsIlBBQ0tBR0VfVE9fSlNfUEFDS0FHRV9OQU1FIjoidGVzdCJ9fQ==", + "wants" : [ + "$OUTPUT", + "$OUTPUT\/platforms", + "$INTERMEDIATES\/wasm-imports.json" + ] + }, + { + "attributes" : [ + + ], + "inputs" : [ + "$PLANNER_SOURCE_PATH", + "$SELF_PACKAGE\/Plugins\/PackageToJS\/Templates\/instantiate.js", + "$INTERMEDIATES\/wasm-imports.json" + ], + "output" : "$OUTPUT\/instantiate.js", + "salt" : "eyJjb25kaXRpb25zIjp7IkhBU19CUklER0UiOmZhbHNlLCJIQVNfSU1QT1JUUyI6ZmFsc2UsIklTX1dBU0kiOnRydWUsIlVTRV9TSEFSRURfTUVNT1JZIjpmYWxzZSwiVVNFX1dBU0lfQ0ROIjpmYWxzZX0sInN1YnN0aXR1dGlvbnMiOnsiUEFDS0FHRV9UT19KU19NT0RVTEVfUEFUSCI6Im1haW4ud2FzbSIsIlBBQ0tBR0VfVE9fSlNfUEFDS0FHRV9OQU1FIjoidGVzdCJ9fQ==", + "wants" : [ + "$OUTPUT", + "$OUTPUT\/platforms", + "$INTERMEDIATES\/wasm-imports.json" + ] + }, + { + "attributes" : [ + + ], + "inputs" : [ + "$PLANNER_SOURCE_PATH", + "$INTERMEDIATES\/main.wasm.no-dwarf" + ], + "output" : "$OUTPUT\/main.wasm", + "wants" : [ + "$OUTPUT", + "$INTERMEDIATES\/main.wasm.no-dwarf" + ] + }, + { + "attributes" : [ + + ], + "inputs" : [ + "$PLANNER_SOURCE_PATH", + "$SELF_PACKAGE\/Plugins\/PackageToJS\/Templates\/package.json" + ], + "output" : "$OUTPUT\/package.json", + "salt" : "eyJjb25kaXRpb25zIjp7IkhBU19CUklER0UiOmZhbHNlLCJIQVNfSU1QT1JUUyI6ZmFsc2UsIklTX1dBU0kiOnRydWUsIlVTRV9TSEFSRURfTUVNT1JZIjpmYWxzZSwiVVNFX1dBU0lfQ0ROIjpmYWxzZX0sInN1YnN0aXR1dGlvbnMiOnsiUEFDS0FHRV9UT19KU19NT0RVTEVfUEFUSCI6Im1haW4ud2FzbSIsIlBBQ0tBR0VfVE9fSlNfUEFDS0FHRV9OQU1FIjoidGVzdCJ9fQ==", + "wants" : [ + "$OUTPUT" + ] + }, + { + "attributes" : [ + "silent" + ], + "inputs" : [ + "$PLANNER_SOURCE_PATH" + ], + "output" : "$OUTPUT\/platforms", + "wants" : [ + + ] + }, + { + "attributes" : [ + + ], + "inputs" : [ + "$PLANNER_SOURCE_PATH", + "$SELF_PACKAGE\/Plugins\/PackageToJS\/Templates\/platforms\/browser.d.ts", + "$INTERMEDIATES\/wasm-imports.json" + ], + "output" : "$OUTPUT\/platforms\/browser.d.ts", + "salt" : "eyJjb25kaXRpb25zIjp7IkhBU19CUklER0UiOmZhbHNlLCJIQVNfSU1QT1JUUyI6ZmFsc2UsIklTX1dBU0kiOnRydWUsIlVTRV9TSEFSRURfTUVNT1JZIjpmYWxzZSwiVVNFX1dBU0lfQ0ROIjpmYWxzZX0sInN1YnN0aXR1dGlvbnMiOnsiUEFDS0FHRV9UT19KU19NT0RVTEVfUEFUSCI6Im1haW4ud2FzbSIsIlBBQ0tBR0VfVE9fSlNfUEFDS0FHRV9OQU1FIjoidGVzdCJ9fQ==", + "wants" : [ + "$OUTPUT", + "$OUTPUT\/platforms", + "$INTERMEDIATES\/wasm-imports.json" + ] + }, + { + "attributes" : [ + + ], + "inputs" : [ + "$PLANNER_SOURCE_PATH", + "$SELF_PACKAGE\/Plugins\/PackageToJS\/Templates\/platforms\/browser.js", + "$INTERMEDIATES\/wasm-imports.json" + ], + "output" : "$OUTPUT\/platforms\/browser.js", + "salt" : "eyJjb25kaXRpb25zIjp7IkhBU19CUklER0UiOmZhbHNlLCJIQVNfSU1QT1JUUyI6ZmFsc2UsIklTX1dBU0kiOnRydWUsIlVTRV9TSEFSRURfTUVNT1JZIjpmYWxzZSwiVVNFX1dBU0lfQ0ROIjpmYWxzZX0sInN1YnN0aXR1dGlvbnMiOnsiUEFDS0FHRV9UT19KU19NT0RVTEVfUEFUSCI6Im1haW4ud2FzbSIsIlBBQ0tBR0VfVE9fSlNfUEFDS0FHRV9OQU1FIjoidGVzdCJ9fQ==", + "wants" : [ + "$OUTPUT", + "$OUTPUT\/platforms", + "$INTERMEDIATES\/wasm-imports.json" + ] + }, + { + "attributes" : [ + + ], + "inputs" : [ + "$PLANNER_SOURCE_PATH", + "$SELF_PACKAGE\/Plugins\/PackageToJS\/Templates\/platforms\/browser.worker.js", + "$INTERMEDIATES\/wasm-imports.json" + ], + "output" : "$OUTPUT\/platforms\/browser.worker.js", + "salt" : "eyJjb25kaXRpb25zIjp7IkhBU19CUklER0UiOmZhbHNlLCJIQVNfSU1QT1JUUyI6ZmFsc2UsIklTX1dBU0kiOnRydWUsIlVTRV9TSEFSRURfTUVNT1JZIjpmYWxzZSwiVVNFX1dBU0lfQ0ROIjpmYWxzZX0sInN1YnN0aXR1dGlvbnMiOnsiUEFDS0FHRV9UT19KU19NT0RVTEVfUEFUSCI6Im1haW4ud2FzbSIsIlBBQ0tBR0VfVE9fSlNfUEFDS0FHRV9OQU1FIjoidGVzdCJ9fQ==", + "wants" : [ + "$OUTPUT", + "$OUTPUT\/platforms", + "$INTERMEDIATES\/wasm-imports.json" + ] + }, + { + "attributes" : [ + + ], + "inputs" : [ + "$PLANNER_SOURCE_PATH", + "$SELF_PACKAGE\/Plugins\/PackageToJS\/Templates\/platforms\/node.d.ts", + "$INTERMEDIATES\/wasm-imports.json" + ], + "output" : "$OUTPUT\/platforms\/node.d.ts", + "salt" : "eyJjb25kaXRpb25zIjp7IkhBU19CUklER0UiOmZhbHNlLCJIQVNfSU1QT1JUUyI6ZmFsc2UsIklTX1dBU0kiOnRydWUsIlVTRV9TSEFSRURfTUVNT1JZIjpmYWxzZSwiVVNFX1dBU0lfQ0ROIjpmYWxzZX0sInN1YnN0aXR1dGlvbnMiOnsiUEFDS0FHRV9UT19KU19NT0RVTEVfUEFUSCI6Im1haW4ud2FzbSIsIlBBQ0tBR0VfVE9fSlNfUEFDS0FHRV9OQU1FIjoidGVzdCJ9fQ==", + "wants" : [ + "$OUTPUT", + "$OUTPUT\/platforms", + "$INTERMEDIATES\/wasm-imports.json" + ] + }, + { + "attributes" : [ + + ], + "inputs" : [ + "$PLANNER_SOURCE_PATH", + "$SELF_PACKAGE\/Plugins\/PackageToJS\/Templates\/platforms\/node.js", + "$INTERMEDIATES\/wasm-imports.json" + ], + "output" : "$OUTPUT\/platforms\/node.js", + "salt" : "eyJjb25kaXRpb25zIjp7IkhBU19CUklER0UiOmZhbHNlLCJIQVNfSU1QT1JUUyI6ZmFsc2UsIklTX1dBU0kiOnRydWUsIlVTRV9TSEFSRURfTUVNT1JZIjpmYWxzZSwiVVNFX1dBU0lfQ0ROIjpmYWxzZX0sInN1YnN0aXR1dGlvbnMiOnsiUEFDS0FHRV9UT19KU19NT0RVTEVfUEFUSCI6Im1haW4ud2FzbSIsIlBBQ0tBR0VfVE9fSlNfUEFDS0FHRV9OQU1FIjoidGVzdCJ9fQ==", + "wants" : [ + "$OUTPUT", + "$OUTPUT\/platforms", + "$INTERMEDIATES\/wasm-imports.json" + ] + }, + { + "attributes" : [ + + ], + "inputs" : [ + "$PLANNER_SOURCE_PATH", + "$SELF_PACKAGE\/Sources\/JavaScriptKit\/Runtime\/index.d.ts", + "$INTERMEDIATES\/wasm-imports.json" + ], + "output" : "$OUTPUT\/runtime.d.ts", + "salt" : "eyJjb25kaXRpb25zIjp7IkhBU19CUklER0UiOmZhbHNlLCJIQVNfSU1QT1JUUyI6ZmFsc2UsIklTX1dBU0kiOnRydWUsIlVTRV9TSEFSRURfTUVNT1JZIjpmYWxzZSwiVVNFX1dBU0lfQ0ROIjpmYWxzZX0sInN1YnN0aXR1dGlvbnMiOnsiUEFDS0FHRV9UT19KU19NT0RVTEVfUEFUSCI6Im1haW4ud2FzbSIsIlBBQ0tBR0VfVE9fSlNfUEFDS0FHRV9OQU1FIjoidGVzdCJ9fQ==", + "wants" : [ + "$OUTPUT", + "$OUTPUT\/platforms", + "$INTERMEDIATES\/wasm-imports.json" + ] + }, + { + "attributes" : [ + + ], + "inputs" : [ + "$PLANNER_SOURCE_PATH", + "$SELF_PACKAGE\/Sources\/JavaScriptKit\/Runtime\/index.mjs", + "$INTERMEDIATES\/wasm-imports.json" + ], + "output" : "$OUTPUT\/runtime.js", + "salt" : "eyJjb25kaXRpb25zIjp7IkhBU19CUklER0UiOmZhbHNlLCJIQVNfSU1QT1JUUyI6ZmFsc2UsIklTX1dBU0kiOnRydWUsIlVTRV9TSEFSRURfTUVNT1JZIjpmYWxzZSwiVVNFX1dBU0lfQ0ROIjpmYWxzZX0sInN1YnN0aXR1dGlvbnMiOnsiUEFDS0FHRV9UT19KU19NT0RVTEVfUEFUSCI6Im1haW4ud2FzbSIsIlBBQ0tBR0VfVE9fSlNfUEFDS0FHRV9OQU1FIjoidGVzdCJ9fQ==", + "wants" : [ + "$OUTPUT", + "$OUTPUT\/platforms", + "$INTERMEDIATES\/wasm-imports.json" + ] + }, + { + "attributes" : [ + "phony", + "silent" + ], + "inputs" : [ + + ], + "output" : "all", + "wants" : [ + "$OUTPUT\/main.wasm", + "$INTERMEDIATES\/wasm-imports.json", + "$OUTPUT\/package.json", + "$OUTPUT\/index.js", + "$OUTPUT\/index.d.ts", + "$OUTPUT\/instantiate.js", + "$OUTPUT\/instantiate.d.ts", + "$OUTPUT\/platforms\/browser.js", + "$OUTPUT\/platforms\/browser.d.ts", + "$OUTPUT\/platforms\/browser.worker.js", + "$OUTPUT\/platforms\/node.js", + "$OUTPUT\/platforms\/node.d.ts", + "$OUTPUT\/runtime.js", + "$OUTPUT\/runtime.d.ts" + ] + } +] \ No newline at end of file diff --git a/Plugins/PackageToJS/Tests/__Snapshots__/PackagingPlannerTests/planBuild_release_dwarf.json b/Plugins/PackageToJS/Tests/__Snapshots__/PackagingPlannerTests/planBuild_release_dwarf.json new file mode 100644 index 000000000..13768da75 --- /dev/null +++ b/Plugins/PackageToJS/Tests/__Snapshots__/PackagingPlannerTests/planBuild_release_dwarf.json @@ -0,0 +1,293 @@ +[ + { + "attributes" : [ + "silent" + ], + "inputs" : [ + "$PLANNER_SOURCE_PATH" + ], + "output" : "$INTERMEDIATES", + "wants" : [ + + ] + }, + { + "attributes" : [ + + ], + "inputs" : [ + "$PLANNER_SOURCE_PATH", + "$OUTPUT\/main.wasm" + ], + "output" : "$INTERMEDIATES\/wasm-imports.json", + "wants" : [ + "$OUTPUT", + "$INTERMEDIATES", + "$OUTPUT\/main.wasm" + ] + }, + { + "attributes" : [ + "silent" + ], + "inputs" : [ + "$PLANNER_SOURCE_PATH" + ], + "output" : "$OUTPUT", + "wants" : [ + + ] + }, + { + "attributes" : [ + + ], + "inputs" : [ + "$PLANNER_SOURCE_PATH", + "$SELF_PACKAGE\/Plugins\/PackageToJS\/Templates\/index.d.ts", + "$INTERMEDIATES\/wasm-imports.json" + ], + "output" : "$OUTPUT\/index.d.ts", + "salt" : "eyJjb25kaXRpb25zIjp7IkhBU19CUklER0UiOmZhbHNlLCJIQVNfSU1QT1JUUyI6ZmFsc2UsIklTX1dBU0kiOnRydWUsIlVTRV9TSEFSRURfTUVNT1JZIjpmYWxzZSwiVVNFX1dBU0lfQ0ROIjpmYWxzZX0sInN1YnN0aXR1dGlvbnMiOnsiUEFDS0FHRV9UT19KU19NT0RVTEVfUEFUSCI6Im1haW4ud2FzbSIsIlBBQ0tBR0VfVE9fSlNfUEFDS0FHRV9OQU1FIjoidGVzdCJ9fQ==", + "wants" : [ + "$OUTPUT", + "$OUTPUT\/platforms", + "$INTERMEDIATES\/wasm-imports.json" + ] + }, + { + "attributes" : [ + + ], + "inputs" : [ + "$PLANNER_SOURCE_PATH", + "$SELF_PACKAGE\/Plugins\/PackageToJS\/Templates\/index.js", + "$INTERMEDIATES\/wasm-imports.json" + ], + "output" : "$OUTPUT\/index.js", + "salt" : "eyJjb25kaXRpb25zIjp7IkhBU19CUklER0UiOmZhbHNlLCJIQVNfSU1QT1JUUyI6ZmFsc2UsIklTX1dBU0kiOnRydWUsIlVTRV9TSEFSRURfTUVNT1JZIjpmYWxzZSwiVVNFX1dBU0lfQ0ROIjpmYWxzZX0sInN1YnN0aXR1dGlvbnMiOnsiUEFDS0FHRV9UT19KU19NT0RVTEVfUEFUSCI6Im1haW4ud2FzbSIsIlBBQ0tBR0VfVE9fSlNfUEFDS0FHRV9OQU1FIjoidGVzdCJ9fQ==", + "wants" : [ + "$OUTPUT", + "$OUTPUT\/platforms", + "$INTERMEDIATES\/wasm-imports.json" + ] + }, + { + "attributes" : [ + + ], + "inputs" : [ + "$PLANNER_SOURCE_PATH", + "$SELF_PACKAGE\/Plugins\/PackageToJS\/Templates\/instantiate.d.ts", + "$INTERMEDIATES\/wasm-imports.json" + ], + "output" : "$OUTPUT\/instantiate.d.ts", + "salt" : "eyJjb25kaXRpb25zIjp7IkhBU19CUklER0UiOmZhbHNlLCJIQVNfSU1QT1JUUyI6ZmFsc2UsIklTX1dBU0kiOnRydWUsIlVTRV9TSEFSRURfTUVNT1JZIjpmYWxzZSwiVVNFX1dBU0lfQ0ROIjpmYWxzZX0sInN1YnN0aXR1dGlvbnMiOnsiUEFDS0FHRV9UT19KU19NT0RVTEVfUEFUSCI6Im1haW4ud2FzbSIsIlBBQ0tBR0VfVE9fSlNfUEFDS0FHRV9OQU1FIjoidGVzdCJ9fQ==", + "wants" : [ + "$OUTPUT", + "$OUTPUT\/platforms", + "$INTERMEDIATES\/wasm-imports.json" + ] + }, + { + "attributes" : [ + + ], + "inputs" : [ + "$PLANNER_SOURCE_PATH", + "$SELF_PACKAGE\/Plugins\/PackageToJS\/Templates\/instantiate.js", + "$INTERMEDIATES\/wasm-imports.json" + ], + "output" : "$OUTPUT\/instantiate.js", + "salt" : "eyJjb25kaXRpb25zIjp7IkhBU19CUklER0UiOmZhbHNlLCJIQVNfSU1QT1JUUyI6ZmFsc2UsIklTX1dBU0kiOnRydWUsIlVTRV9TSEFSRURfTUVNT1JZIjpmYWxzZSwiVVNFX1dBU0lfQ0ROIjpmYWxzZX0sInN1YnN0aXR1dGlvbnMiOnsiUEFDS0FHRV9UT19KU19NT0RVTEVfUEFUSCI6Im1haW4ud2FzbSIsIlBBQ0tBR0VfVE9fSlNfUEFDS0FHRV9OQU1FIjoidGVzdCJ9fQ==", + "wants" : [ + "$OUTPUT", + "$OUTPUT\/platforms", + "$INTERMEDIATES\/wasm-imports.json" + ] + }, + { + "attributes" : [ + + ], + "inputs" : [ + "$PLANNER_SOURCE_PATH", + "$WASM_PRODUCT_ARTIFACT" + ], + "output" : "$OUTPUT\/main.wasm", + "wants" : [ + "$OUTPUT" + ] + }, + { + "attributes" : [ + + ], + "inputs" : [ + "$PLANNER_SOURCE_PATH", + "$SELF_PACKAGE\/Plugins\/PackageToJS\/Templates\/package.json" + ], + "output" : "$OUTPUT\/package.json", + "salt" : "eyJjb25kaXRpb25zIjp7IkhBU19CUklER0UiOmZhbHNlLCJIQVNfSU1QT1JUUyI6ZmFsc2UsIklTX1dBU0kiOnRydWUsIlVTRV9TSEFSRURfTUVNT1JZIjpmYWxzZSwiVVNFX1dBU0lfQ0ROIjpmYWxzZX0sInN1YnN0aXR1dGlvbnMiOnsiUEFDS0FHRV9UT19KU19NT0RVTEVfUEFUSCI6Im1haW4ud2FzbSIsIlBBQ0tBR0VfVE9fSlNfUEFDS0FHRV9OQU1FIjoidGVzdCJ9fQ==", + "wants" : [ + "$OUTPUT" + ] + }, + { + "attributes" : [ + "silent" + ], + "inputs" : [ + "$PLANNER_SOURCE_PATH" + ], + "output" : "$OUTPUT\/platforms", + "wants" : [ + + ] + }, + { + "attributes" : [ + + ], + "inputs" : [ + "$PLANNER_SOURCE_PATH", + "$SELF_PACKAGE\/Plugins\/PackageToJS\/Templates\/platforms\/browser.d.ts", + "$INTERMEDIATES\/wasm-imports.json" + ], + "output" : "$OUTPUT\/platforms\/browser.d.ts", + "salt" : "eyJjb25kaXRpb25zIjp7IkhBU19CUklER0UiOmZhbHNlLCJIQVNfSU1QT1JUUyI6ZmFsc2UsIklTX1dBU0kiOnRydWUsIlVTRV9TSEFSRURfTUVNT1JZIjpmYWxzZSwiVVNFX1dBU0lfQ0ROIjpmYWxzZX0sInN1YnN0aXR1dGlvbnMiOnsiUEFDS0FHRV9UT19KU19NT0RVTEVfUEFUSCI6Im1haW4ud2FzbSIsIlBBQ0tBR0VfVE9fSlNfUEFDS0FHRV9OQU1FIjoidGVzdCJ9fQ==", + "wants" : [ + "$OUTPUT", + "$OUTPUT\/platforms", + "$INTERMEDIATES\/wasm-imports.json" + ] + }, + { + "attributes" : [ + + ], + "inputs" : [ + "$PLANNER_SOURCE_PATH", + "$SELF_PACKAGE\/Plugins\/PackageToJS\/Templates\/platforms\/browser.js", + "$INTERMEDIATES\/wasm-imports.json" + ], + "output" : "$OUTPUT\/platforms\/browser.js", + "salt" : "eyJjb25kaXRpb25zIjp7IkhBU19CUklER0UiOmZhbHNlLCJIQVNfSU1QT1JUUyI6ZmFsc2UsIklTX1dBU0kiOnRydWUsIlVTRV9TSEFSRURfTUVNT1JZIjpmYWxzZSwiVVNFX1dBU0lfQ0ROIjpmYWxzZX0sInN1YnN0aXR1dGlvbnMiOnsiUEFDS0FHRV9UT19KU19NT0RVTEVfUEFUSCI6Im1haW4ud2FzbSIsIlBBQ0tBR0VfVE9fSlNfUEFDS0FHRV9OQU1FIjoidGVzdCJ9fQ==", + "wants" : [ + "$OUTPUT", + "$OUTPUT\/platforms", + "$INTERMEDIATES\/wasm-imports.json" + ] + }, + { + "attributes" : [ + + ], + "inputs" : [ + "$PLANNER_SOURCE_PATH", + "$SELF_PACKAGE\/Plugins\/PackageToJS\/Templates\/platforms\/browser.worker.js", + "$INTERMEDIATES\/wasm-imports.json" + ], + "output" : "$OUTPUT\/platforms\/browser.worker.js", + "salt" : "eyJjb25kaXRpb25zIjp7IkhBU19CUklER0UiOmZhbHNlLCJIQVNfSU1QT1JUUyI6ZmFsc2UsIklTX1dBU0kiOnRydWUsIlVTRV9TSEFSRURfTUVNT1JZIjpmYWxzZSwiVVNFX1dBU0lfQ0ROIjpmYWxzZX0sInN1YnN0aXR1dGlvbnMiOnsiUEFDS0FHRV9UT19KU19NT0RVTEVfUEFUSCI6Im1haW4ud2FzbSIsIlBBQ0tBR0VfVE9fSlNfUEFDS0FHRV9OQU1FIjoidGVzdCJ9fQ==", + "wants" : [ + "$OUTPUT", + "$OUTPUT\/platforms", + "$INTERMEDIATES\/wasm-imports.json" + ] + }, + { + "attributes" : [ + + ], + "inputs" : [ + "$PLANNER_SOURCE_PATH", + "$SELF_PACKAGE\/Plugins\/PackageToJS\/Templates\/platforms\/node.d.ts", + "$INTERMEDIATES\/wasm-imports.json" + ], + "output" : "$OUTPUT\/platforms\/node.d.ts", + "salt" : "eyJjb25kaXRpb25zIjp7IkhBU19CUklER0UiOmZhbHNlLCJIQVNfSU1QT1JUUyI6ZmFsc2UsIklTX1dBU0kiOnRydWUsIlVTRV9TSEFSRURfTUVNT1JZIjpmYWxzZSwiVVNFX1dBU0lfQ0ROIjpmYWxzZX0sInN1YnN0aXR1dGlvbnMiOnsiUEFDS0FHRV9UT19KU19NT0RVTEVfUEFUSCI6Im1haW4ud2FzbSIsIlBBQ0tBR0VfVE9fSlNfUEFDS0FHRV9OQU1FIjoidGVzdCJ9fQ==", + "wants" : [ + "$OUTPUT", + "$OUTPUT\/platforms", + "$INTERMEDIATES\/wasm-imports.json" + ] + }, + { + "attributes" : [ + + ], + "inputs" : [ + "$PLANNER_SOURCE_PATH", + "$SELF_PACKAGE\/Plugins\/PackageToJS\/Templates\/platforms\/node.js", + "$INTERMEDIATES\/wasm-imports.json" + ], + "output" : "$OUTPUT\/platforms\/node.js", + "salt" : "eyJjb25kaXRpb25zIjp7IkhBU19CUklER0UiOmZhbHNlLCJIQVNfSU1QT1JUUyI6ZmFsc2UsIklTX1dBU0kiOnRydWUsIlVTRV9TSEFSRURfTUVNT1JZIjpmYWxzZSwiVVNFX1dBU0lfQ0ROIjpmYWxzZX0sInN1YnN0aXR1dGlvbnMiOnsiUEFDS0FHRV9UT19KU19NT0RVTEVfUEFUSCI6Im1haW4ud2FzbSIsIlBBQ0tBR0VfVE9fSlNfUEFDS0FHRV9OQU1FIjoidGVzdCJ9fQ==", + "wants" : [ + "$OUTPUT", + "$OUTPUT\/platforms", + "$INTERMEDIATES\/wasm-imports.json" + ] + }, + { + "attributes" : [ + + ], + "inputs" : [ + "$PLANNER_SOURCE_PATH", + "$SELF_PACKAGE\/Sources\/JavaScriptKit\/Runtime\/index.d.ts", + "$INTERMEDIATES\/wasm-imports.json" + ], + "output" : "$OUTPUT\/runtime.d.ts", + "salt" : "eyJjb25kaXRpb25zIjp7IkhBU19CUklER0UiOmZhbHNlLCJIQVNfSU1QT1JUUyI6ZmFsc2UsIklTX1dBU0kiOnRydWUsIlVTRV9TSEFSRURfTUVNT1JZIjpmYWxzZSwiVVNFX1dBU0lfQ0ROIjpmYWxzZX0sInN1YnN0aXR1dGlvbnMiOnsiUEFDS0FHRV9UT19KU19NT0RVTEVfUEFUSCI6Im1haW4ud2FzbSIsIlBBQ0tBR0VfVE9fSlNfUEFDS0FHRV9OQU1FIjoidGVzdCJ9fQ==", + "wants" : [ + "$OUTPUT", + "$OUTPUT\/platforms", + "$INTERMEDIATES\/wasm-imports.json" + ] + }, + { + "attributes" : [ + + ], + "inputs" : [ + "$PLANNER_SOURCE_PATH", + "$SELF_PACKAGE\/Sources\/JavaScriptKit\/Runtime\/index.mjs", + "$INTERMEDIATES\/wasm-imports.json" + ], + "output" : "$OUTPUT\/runtime.js", + "salt" : "eyJjb25kaXRpb25zIjp7IkhBU19CUklER0UiOmZhbHNlLCJIQVNfSU1QT1JUUyI6ZmFsc2UsIklTX1dBU0kiOnRydWUsIlVTRV9TSEFSRURfTUVNT1JZIjpmYWxzZSwiVVNFX1dBU0lfQ0ROIjpmYWxzZX0sInN1YnN0aXR1dGlvbnMiOnsiUEFDS0FHRV9UT19KU19NT0RVTEVfUEFUSCI6Im1haW4ud2FzbSIsIlBBQ0tBR0VfVE9fSlNfUEFDS0FHRV9OQU1FIjoidGVzdCJ9fQ==", + "wants" : [ + "$OUTPUT", + "$OUTPUT\/platforms", + "$INTERMEDIATES\/wasm-imports.json" + ] + }, + { + "attributes" : [ + "phony", + "silent" + ], + "inputs" : [ + + ], + "output" : "all", + "wants" : [ + "$OUTPUT\/main.wasm", + "$INTERMEDIATES\/wasm-imports.json", + "$OUTPUT\/package.json", + "$OUTPUT\/index.js", + "$OUTPUT\/index.d.ts", + "$OUTPUT\/instantiate.js", + "$OUTPUT\/instantiate.d.ts", + "$OUTPUT\/platforms\/browser.js", + "$OUTPUT\/platforms\/browser.d.ts", + "$OUTPUT\/platforms\/browser.worker.js", + "$OUTPUT\/platforms\/node.js", + "$OUTPUT\/platforms\/node.d.ts", + "$OUTPUT\/runtime.js", + "$OUTPUT\/runtime.d.ts" + ] + } +] \ No newline at end of file diff --git a/Plugins/PackageToJS/Tests/__Snapshots__/PackagingPlannerTests/planBuild_release_name.json b/Plugins/PackageToJS/Tests/__Snapshots__/PackagingPlannerTests/planBuild_release_name.json new file mode 100644 index 000000000..ccfbc35cc --- /dev/null +++ b/Plugins/PackageToJS/Tests/__Snapshots__/PackagingPlannerTests/planBuild_release_name.json @@ -0,0 +1,308 @@ +[ + { + "attributes" : [ + "silent" + ], + "inputs" : [ + "$PLANNER_SOURCE_PATH" + ], + "output" : "$INTERMEDIATES", + "wants" : [ + + ] + }, + { + "attributes" : [ + + ], + "inputs" : [ + "$PLANNER_SOURCE_PATH", + "$WASM_PRODUCT_ARTIFACT" + ], + "output" : "$INTERMEDIATES\/main.wasm.no-dwarf", + "wants" : [ + "$OUTPUT", + "$INTERMEDIATES" + ] + }, + { + "attributes" : [ + + ], + "inputs" : [ + "$PLANNER_SOURCE_PATH", + "$OUTPUT\/main.wasm" + ], + "output" : "$INTERMEDIATES\/wasm-imports.json", + "wants" : [ + "$OUTPUT", + "$INTERMEDIATES", + "$OUTPUT\/main.wasm" + ] + }, + { + "attributes" : [ + "silent" + ], + "inputs" : [ + "$PLANNER_SOURCE_PATH" + ], + "output" : "$OUTPUT", + "wants" : [ + + ] + }, + { + "attributes" : [ + + ], + "inputs" : [ + "$PLANNER_SOURCE_PATH", + "$SELF_PACKAGE\/Plugins\/PackageToJS\/Templates\/index.d.ts", + "$INTERMEDIATES\/wasm-imports.json" + ], + "output" : "$OUTPUT\/index.d.ts", + "salt" : "eyJjb25kaXRpb25zIjp7IkhBU19CUklER0UiOmZhbHNlLCJIQVNfSU1QT1JUUyI6ZmFsc2UsIklTX1dBU0kiOnRydWUsIlVTRV9TSEFSRURfTUVNT1JZIjpmYWxzZSwiVVNFX1dBU0lfQ0ROIjpmYWxzZX0sInN1YnN0aXR1dGlvbnMiOnsiUEFDS0FHRV9UT19KU19NT0RVTEVfUEFUSCI6Im1haW4ud2FzbSIsIlBBQ0tBR0VfVE9fSlNfUEFDS0FHRV9OQU1FIjoidGVzdCJ9fQ==", + "wants" : [ + "$OUTPUT", + "$OUTPUT\/platforms", + "$INTERMEDIATES\/wasm-imports.json" + ] + }, + { + "attributes" : [ + + ], + "inputs" : [ + "$PLANNER_SOURCE_PATH", + "$SELF_PACKAGE\/Plugins\/PackageToJS\/Templates\/index.js", + "$INTERMEDIATES\/wasm-imports.json" + ], + "output" : "$OUTPUT\/index.js", + "salt" : "eyJjb25kaXRpb25zIjp7IkhBU19CUklER0UiOmZhbHNlLCJIQVNfSU1QT1JUUyI6ZmFsc2UsIklTX1dBU0kiOnRydWUsIlVTRV9TSEFSRURfTUVNT1JZIjpmYWxzZSwiVVNFX1dBU0lfQ0ROIjpmYWxzZX0sInN1YnN0aXR1dGlvbnMiOnsiUEFDS0FHRV9UT19KU19NT0RVTEVfUEFUSCI6Im1haW4ud2FzbSIsIlBBQ0tBR0VfVE9fSlNfUEFDS0FHRV9OQU1FIjoidGVzdCJ9fQ==", + "wants" : [ + "$OUTPUT", + "$OUTPUT\/platforms", + "$INTERMEDIATES\/wasm-imports.json" + ] + }, + { + "attributes" : [ + + ], + "inputs" : [ + "$PLANNER_SOURCE_PATH", + "$SELF_PACKAGE\/Plugins\/PackageToJS\/Templates\/instantiate.d.ts", + "$INTERMEDIATES\/wasm-imports.json" + ], + "output" : "$OUTPUT\/instantiate.d.ts", + "salt" : "eyJjb25kaXRpb25zIjp7IkhBU19CUklER0UiOmZhbHNlLCJIQVNfSU1QT1JUUyI6ZmFsc2UsIklTX1dBU0kiOnRydWUsIlVTRV9TSEFSRURfTUVNT1JZIjpmYWxzZSwiVVNFX1dBU0lfQ0ROIjpmYWxzZX0sInN1YnN0aXR1dGlvbnMiOnsiUEFDS0FHRV9UT19KU19NT0RVTEVfUEFUSCI6Im1haW4ud2FzbSIsIlBBQ0tBR0VfVE9fSlNfUEFDS0FHRV9OQU1FIjoidGVzdCJ9fQ==", + "wants" : [ + "$OUTPUT", + "$OUTPUT\/platforms", + "$INTERMEDIATES\/wasm-imports.json" + ] + }, + { + "attributes" : [ + + ], + "inputs" : [ + "$PLANNER_SOURCE_PATH", + "$SELF_PACKAGE\/Plugins\/PackageToJS\/Templates\/instantiate.js", + "$INTERMEDIATES\/wasm-imports.json" + ], + "output" : "$OUTPUT\/instantiate.js", + "salt" : "eyJjb25kaXRpb25zIjp7IkhBU19CUklER0UiOmZhbHNlLCJIQVNfSU1QT1JUUyI6ZmFsc2UsIklTX1dBU0kiOnRydWUsIlVTRV9TSEFSRURfTUVNT1JZIjpmYWxzZSwiVVNFX1dBU0lfQ0ROIjpmYWxzZX0sInN1YnN0aXR1dGlvbnMiOnsiUEFDS0FHRV9UT19KU19NT0RVTEVfUEFUSCI6Im1haW4ud2FzbSIsIlBBQ0tBR0VfVE9fSlNfUEFDS0FHRV9OQU1FIjoidGVzdCJ9fQ==", + "wants" : [ + "$OUTPUT", + "$OUTPUT\/platforms", + "$INTERMEDIATES\/wasm-imports.json" + ] + }, + { + "attributes" : [ + + ], + "inputs" : [ + "$PLANNER_SOURCE_PATH", + "$INTERMEDIATES\/main.wasm.no-dwarf" + ], + "output" : "$OUTPUT\/main.wasm", + "wants" : [ + "$OUTPUT", + "$INTERMEDIATES\/main.wasm.no-dwarf" + ] + }, + { + "attributes" : [ + + ], + "inputs" : [ + "$PLANNER_SOURCE_PATH", + "$SELF_PACKAGE\/Plugins\/PackageToJS\/Templates\/package.json" + ], + "output" : "$OUTPUT\/package.json", + "salt" : "eyJjb25kaXRpb25zIjp7IkhBU19CUklER0UiOmZhbHNlLCJIQVNfSU1QT1JUUyI6ZmFsc2UsIklTX1dBU0kiOnRydWUsIlVTRV9TSEFSRURfTUVNT1JZIjpmYWxzZSwiVVNFX1dBU0lfQ0ROIjpmYWxzZX0sInN1YnN0aXR1dGlvbnMiOnsiUEFDS0FHRV9UT19KU19NT0RVTEVfUEFUSCI6Im1haW4ud2FzbSIsIlBBQ0tBR0VfVE9fSlNfUEFDS0FHRV9OQU1FIjoidGVzdCJ9fQ==", + "wants" : [ + "$OUTPUT" + ] + }, + { + "attributes" : [ + "silent" + ], + "inputs" : [ + "$PLANNER_SOURCE_PATH" + ], + "output" : "$OUTPUT\/platforms", + "wants" : [ + + ] + }, + { + "attributes" : [ + + ], + "inputs" : [ + "$PLANNER_SOURCE_PATH", + "$SELF_PACKAGE\/Plugins\/PackageToJS\/Templates\/platforms\/browser.d.ts", + "$INTERMEDIATES\/wasm-imports.json" + ], + "output" : "$OUTPUT\/platforms\/browser.d.ts", + "salt" : "eyJjb25kaXRpb25zIjp7IkhBU19CUklER0UiOmZhbHNlLCJIQVNfSU1QT1JUUyI6ZmFsc2UsIklTX1dBU0kiOnRydWUsIlVTRV9TSEFSRURfTUVNT1JZIjpmYWxzZSwiVVNFX1dBU0lfQ0ROIjpmYWxzZX0sInN1YnN0aXR1dGlvbnMiOnsiUEFDS0FHRV9UT19KU19NT0RVTEVfUEFUSCI6Im1haW4ud2FzbSIsIlBBQ0tBR0VfVE9fSlNfUEFDS0FHRV9OQU1FIjoidGVzdCJ9fQ==", + "wants" : [ + "$OUTPUT", + "$OUTPUT\/platforms", + "$INTERMEDIATES\/wasm-imports.json" + ] + }, + { + "attributes" : [ + + ], + "inputs" : [ + "$PLANNER_SOURCE_PATH", + "$SELF_PACKAGE\/Plugins\/PackageToJS\/Templates\/platforms\/browser.js", + "$INTERMEDIATES\/wasm-imports.json" + ], + "output" : "$OUTPUT\/platforms\/browser.js", + "salt" : "eyJjb25kaXRpb25zIjp7IkhBU19CUklER0UiOmZhbHNlLCJIQVNfSU1QT1JUUyI6ZmFsc2UsIklTX1dBU0kiOnRydWUsIlVTRV9TSEFSRURfTUVNT1JZIjpmYWxzZSwiVVNFX1dBU0lfQ0ROIjpmYWxzZX0sInN1YnN0aXR1dGlvbnMiOnsiUEFDS0FHRV9UT19KU19NT0RVTEVfUEFUSCI6Im1haW4ud2FzbSIsIlBBQ0tBR0VfVE9fSlNfUEFDS0FHRV9OQU1FIjoidGVzdCJ9fQ==", + "wants" : [ + "$OUTPUT", + "$OUTPUT\/platforms", + "$INTERMEDIATES\/wasm-imports.json" + ] + }, + { + "attributes" : [ + + ], + "inputs" : [ + "$PLANNER_SOURCE_PATH", + "$SELF_PACKAGE\/Plugins\/PackageToJS\/Templates\/platforms\/browser.worker.js", + "$INTERMEDIATES\/wasm-imports.json" + ], + "output" : "$OUTPUT\/platforms\/browser.worker.js", + "salt" : "eyJjb25kaXRpb25zIjp7IkhBU19CUklER0UiOmZhbHNlLCJIQVNfSU1QT1JUUyI6ZmFsc2UsIklTX1dBU0kiOnRydWUsIlVTRV9TSEFSRURfTUVNT1JZIjpmYWxzZSwiVVNFX1dBU0lfQ0ROIjpmYWxzZX0sInN1YnN0aXR1dGlvbnMiOnsiUEFDS0FHRV9UT19KU19NT0RVTEVfUEFUSCI6Im1haW4ud2FzbSIsIlBBQ0tBR0VfVE9fSlNfUEFDS0FHRV9OQU1FIjoidGVzdCJ9fQ==", + "wants" : [ + "$OUTPUT", + "$OUTPUT\/platforms", + "$INTERMEDIATES\/wasm-imports.json" + ] + }, + { + "attributes" : [ + + ], + "inputs" : [ + "$PLANNER_SOURCE_PATH", + "$SELF_PACKAGE\/Plugins\/PackageToJS\/Templates\/platforms\/node.d.ts", + "$INTERMEDIATES\/wasm-imports.json" + ], + "output" : "$OUTPUT\/platforms\/node.d.ts", + "salt" : "eyJjb25kaXRpb25zIjp7IkhBU19CUklER0UiOmZhbHNlLCJIQVNfSU1QT1JUUyI6ZmFsc2UsIklTX1dBU0kiOnRydWUsIlVTRV9TSEFSRURfTUVNT1JZIjpmYWxzZSwiVVNFX1dBU0lfQ0ROIjpmYWxzZX0sInN1YnN0aXR1dGlvbnMiOnsiUEFDS0FHRV9UT19KU19NT0RVTEVfUEFUSCI6Im1haW4ud2FzbSIsIlBBQ0tBR0VfVE9fSlNfUEFDS0FHRV9OQU1FIjoidGVzdCJ9fQ==", + "wants" : [ + "$OUTPUT", + "$OUTPUT\/platforms", + "$INTERMEDIATES\/wasm-imports.json" + ] + }, + { + "attributes" : [ + + ], + "inputs" : [ + "$PLANNER_SOURCE_PATH", + "$SELF_PACKAGE\/Plugins\/PackageToJS\/Templates\/platforms\/node.js", + "$INTERMEDIATES\/wasm-imports.json" + ], + "output" : "$OUTPUT\/platforms\/node.js", + "salt" : "eyJjb25kaXRpb25zIjp7IkhBU19CUklER0UiOmZhbHNlLCJIQVNfSU1QT1JUUyI6ZmFsc2UsIklTX1dBU0kiOnRydWUsIlVTRV9TSEFSRURfTUVNT1JZIjpmYWxzZSwiVVNFX1dBU0lfQ0ROIjpmYWxzZX0sInN1YnN0aXR1dGlvbnMiOnsiUEFDS0FHRV9UT19KU19NT0RVTEVfUEFUSCI6Im1haW4ud2FzbSIsIlBBQ0tBR0VfVE9fSlNfUEFDS0FHRV9OQU1FIjoidGVzdCJ9fQ==", + "wants" : [ + "$OUTPUT", + "$OUTPUT\/platforms", + "$INTERMEDIATES\/wasm-imports.json" + ] + }, + { + "attributes" : [ + + ], + "inputs" : [ + "$PLANNER_SOURCE_PATH", + "$SELF_PACKAGE\/Sources\/JavaScriptKit\/Runtime\/index.d.ts", + "$INTERMEDIATES\/wasm-imports.json" + ], + "output" : "$OUTPUT\/runtime.d.ts", + "salt" : "eyJjb25kaXRpb25zIjp7IkhBU19CUklER0UiOmZhbHNlLCJIQVNfSU1QT1JUUyI6ZmFsc2UsIklTX1dBU0kiOnRydWUsIlVTRV9TSEFSRURfTUVNT1JZIjpmYWxzZSwiVVNFX1dBU0lfQ0ROIjpmYWxzZX0sInN1YnN0aXR1dGlvbnMiOnsiUEFDS0FHRV9UT19KU19NT0RVTEVfUEFUSCI6Im1haW4ud2FzbSIsIlBBQ0tBR0VfVE9fSlNfUEFDS0FHRV9OQU1FIjoidGVzdCJ9fQ==", + "wants" : [ + "$OUTPUT", + "$OUTPUT\/platforms", + "$INTERMEDIATES\/wasm-imports.json" + ] + }, + { + "attributes" : [ + + ], + "inputs" : [ + "$PLANNER_SOURCE_PATH", + "$SELF_PACKAGE\/Sources\/JavaScriptKit\/Runtime\/index.mjs", + "$INTERMEDIATES\/wasm-imports.json" + ], + "output" : "$OUTPUT\/runtime.js", + "salt" : "eyJjb25kaXRpb25zIjp7IkhBU19CUklER0UiOmZhbHNlLCJIQVNfSU1QT1JUUyI6ZmFsc2UsIklTX1dBU0kiOnRydWUsIlVTRV9TSEFSRURfTUVNT1JZIjpmYWxzZSwiVVNFX1dBU0lfQ0ROIjpmYWxzZX0sInN1YnN0aXR1dGlvbnMiOnsiUEFDS0FHRV9UT19KU19NT0RVTEVfUEFUSCI6Im1haW4ud2FzbSIsIlBBQ0tBR0VfVE9fSlNfUEFDS0FHRV9OQU1FIjoidGVzdCJ9fQ==", + "wants" : [ + "$OUTPUT", + "$OUTPUT\/platforms", + "$INTERMEDIATES\/wasm-imports.json" + ] + }, + { + "attributes" : [ + "phony", + "silent" + ], + "inputs" : [ + + ], + "output" : "all", + "wants" : [ + "$OUTPUT\/main.wasm", + "$INTERMEDIATES\/wasm-imports.json", + "$OUTPUT\/package.json", + "$OUTPUT\/index.js", + "$OUTPUT\/index.d.ts", + "$OUTPUT\/instantiate.js", + "$OUTPUT\/instantiate.d.ts", + "$OUTPUT\/platforms\/browser.js", + "$OUTPUT\/platforms\/browser.d.ts", + "$OUTPUT\/platforms\/browser.worker.js", + "$OUTPUT\/platforms\/node.js", + "$OUTPUT\/platforms\/node.d.ts", + "$OUTPUT\/runtime.js", + "$OUTPUT\/runtime.d.ts" + ] + } +] \ No newline at end of file diff --git a/Plugins/PackageToJS/Tests/__Snapshots__/PackagingPlannerTests/planBuild_release_no_optimize.json b/Plugins/PackageToJS/Tests/__Snapshots__/PackagingPlannerTests/planBuild_release_no_optimize.json new file mode 100644 index 000000000..13768da75 --- /dev/null +++ b/Plugins/PackageToJS/Tests/__Snapshots__/PackagingPlannerTests/planBuild_release_no_optimize.json @@ -0,0 +1,293 @@ +[ + { + "attributes" : [ + "silent" + ], + "inputs" : [ + "$PLANNER_SOURCE_PATH" + ], + "output" : "$INTERMEDIATES", + "wants" : [ + + ] + }, + { + "attributes" : [ + + ], + "inputs" : [ + "$PLANNER_SOURCE_PATH", + "$OUTPUT\/main.wasm" + ], + "output" : "$INTERMEDIATES\/wasm-imports.json", + "wants" : [ + "$OUTPUT", + "$INTERMEDIATES", + "$OUTPUT\/main.wasm" + ] + }, + { + "attributes" : [ + "silent" + ], + "inputs" : [ + "$PLANNER_SOURCE_PATH" + ], + "output" : "$OUTPUT", + "wants" : [ + + ] + }, + { + "attributes" : [ + + ], + "inputs" : [ + "$PLANNER_SOURCE_PATH", + "$SELF_PACKAGE\/Plugins\/PackageToJS\/Templates\/index.d.ts", + "$INTERMEDIATES\/wasm-imports.json" + ], + "output" : "$OUTPUT\/index.d.ts", + "salt" : "eyJjb25kaXRpb25zIjp7IkhBU19CUklER0UiOmZhbHNlLCJIQVNfSU1QT1JUUyI6ZmFsc2UsIklTX1dBU0kiOnRydWUsIlVTRV9TSEFSRURfTUVNT1JZIjpmYWxzZSwiVVNFX1dBU0lfQ0ROIjpmYWxzZX0sInN1YnN0aXR1dGlvbnMiOnsiUEFDS0FHRV9UT19KU19NT0RVTEVfUEFUSCI6Im1haW4ud2FzbSIsIlBBQ0tBR0VfVE9fSlNfUEFDS0FHRV9OQU1FIjoidGVzdCJ9fQ==", + "wants" : [ + "$OUTPUT", + "$OUTPUT\/platforms", + "$INTERMEDIATES\/wasm-imports.json" + ] + }, + { + "attributes" : [ + + ], + "inputs" : [ + "$PLANNER_SOURCE_PATH", + "$SELF_PACKAGE\/Plugins\/PackageToJS\/Templates\/index.js", + "$INTERMEDIATES\/wasm-imports.json" + ], + "output" : "$OUTPUT\/index.js", + "salt" : "eyJjb25kaXRpb25zIjp7IkhBU19CUklER0UiOmZhbHNlLCJIQVNfSU1QT1JUUyI6ZmFsc2UsIklTX1dBU0kiOnRydWUsIlVTRV9TSEFSRURfTUVNT1JZIjpmYWxzZSwiVVNFX1dBU0lfQ0ROIjpmYWxzZX0sInN1YnN0aXR1dGlvbnMiOnsiUEFDS0FHRV9UT19KU19NT0RVTEVfUEFUSCI6Im1haW4ud2FzbSIsIlBBQ0tBR0VfVE9fSlNfUEFDS0FHRV9OQU1FIjoidGVzdCJ9fQ==", + "wants" : [ + "$OUTPUT", + "$OUTPUT\/platforms", + "$INTERMEDIATES\/wasm-imports.json" + ] + }, + { + "attributes" : [ + + ], + "inputs" : [ + "$PLANNER_SOURCE_PATH", + "$SELF_PACKAGE\/Plugins\/PackageToJS\/Templates\/instantiate.d.ts", + "$INTERMEDIATES\/wasm-imports.json" + ], + "output" : "$OUTPUT\/instantiate.d.ts", + "salt" : "eyJjb25kaXRpb25zIjp7IkhBU19CUklER0UiOmZhbHNlLCJIQVNfSU1QT1JUUyI6ZmFsc2UsIklTX1dBU0kiOnRydWUsIlVTRV9TSEFSRURfTUVNT1JZIjpmYWxzZSwiVVNFX1dBU0lfQ0ROIjpmYWxzZX0sInN1YnN0aXR1dGlvbnMiOnsiUEFDS0FHRV9UT19KU19NT0RVTEVfUEFUSCI6Im1haW4ud2FzbSIsIlBBQ0tBR0VfVE9fSlNfUEFDS0FHRV9OQU1FIjoidGVzdCJ9fQ==", + "wants" : [ + "$OUTPUT", + "$OUTPUT\/platforms", + "$INTERMEDIATES\/wasm-imports.json" + ] + }, + { + "attributes" : [ + + ], + "inputs" : [ + "$PLANNER_SOURCE_PATH", + "$SELF_PACKAGE\/Plugins\/PackageToJS\/Templates\/instantiate.js", + "$INTERMEDIATES\/wasm-imports.json" + ], + "output" : "$OUTPUT\/instantiate.js", + "salt" : "eyJjb25kaXRpb25zIjp7IkhBU19CUklER0UiOmZhbHNlLCJIQVNfSU1QT1JUUyI6ZmFsc2UsIklTX1dBU0kiOnRydWUsIlVTRV9TSEFSRURfTUVNT1JZIjpmYWxzZSwiVVNFX1dBU0lfQ0ROIjpmYWxzZX0sInN1YnN0aXR1dGlvbnMiOnsiUEFDS0FHRV9UT19KU19NT0RVTEVfUEFUSCI6Im1haW4ud2FzbSIsIlBBQ0tBR0VfVE9fSlNfUEFDS0FHRV9OQU1FIjoidGVzdCJ9fQ==", + "wants" : [ + "$OUTPUT", + "$OUTPUT\/platforms", + "$INTERMEDIATES\/wasm-imports.json" + ] + }, + { + "attributes" : [ + + ], + "inputs" : [ + "$PLANNER_SOURCE_PATH", + "$WASM_PRODUCT_ARTIFACT" + ], + "output" : "$OUTPUT\/main.wasm", + "wants" : [ + "$OUTPUT" + ] + }, + { + "attributes" : [ + + ], + "inputs" : [ + "$PLANNER_SOURCE_PATH", + "$SELF_PACKAGE\/Plugins\/PackageToJS\/Templates\/package.json" + ], + "output" : "$OUTPUT\/package.json", + "salt" : "eyJjb25kaXRpb25zIjp7IkhBU19CUklER0UiOmZhbHNlLCJIQVNfSU1QT1JUUyI6ZmFsc2UsIklTX1dBU0kiOnRydWUsIlVTRV9TSEFSRURfTUVNT1JZIjpmYWxzZSwiVVNFX1dBU0lfQ0ROIjpmYWxzZX0sInN1YnN0aXR1dGlvbnMiOnsiUEFDS0FHRV9UT19KU19NT0RVTEVfUEFUSCI6Im1haW4ud2FzbSIsIlBBQ0tBR0VfVE9fSlNfUEFDS0FHRV9OQU1FIjoidGVzdCJ9fQ==", + "wants" : [ + "$OUTPUT" + ] + }, + { + "attributes" : [ + "silent" + ], + "inputs" : [ + "$PLANNER_SOURCE_PATH" + ], + "output" : "$OUTPUT\/platforms", + "wants" : [ + + ] + }, + { + "attributes" : [ + + ], + "inputs" : [ + "$PLANNER_SOURCE_PATH", + "$SELF_PACKAGE\/Plugins\/PackageToJS\/Templates\/platforms\/browser.d.ts", + "$INTERMEDIATES\/wasm-imports.json" + ], + "output" : "$OUTPUT\/platforms\/browser.d.ts", + "salt" : "eyJjb25kaXRpb25zIjp7IkhBU19CUklER0UiOmZhbHNlLCJIQVNfSU1QT1JUUyI6ZmFsc2UsIklTX1dBU0kiOnRydWUsIlVTRV9TSEFSRURfTUVNT1JZIjpmYWxzZSwiVVNFX1dBU0lfQ0ROIjpmYWxzZX0sInN1YnN0aXR1dGlvbnMiOnsiUEFDS0FHRV9UT19KU19NT0RVTEVfUEFUSCI6Im1haW4ud2FzbSIsIlBBQ0tBR0VfVE9fSlNfUEFDS0FHRV9OQU1FIjoidGVzdCJ9fQ==", + "wants" : [ + "$OUTPUT", + "$OUTPUT\/platforms", + "$INTERMEDIATES\/wasm-imports.json" + ] + }, + { + "attributes" : [ + + ], + "inputs" : [ + "$PLANNER_SOURCE_PATH", + "$SELF_PACKAGE\/Plugins\/PackageToJS\/Templates\/platforms\/browser.js", + "$INTERMEDIATES\/wasm-imports.json" + ], + "output" : "$OUTPUT\/platforms\/browser.js", + "salt" : "eyJjb25kaXRpb25zIjp7IkhBU19CUklER0UiOmZhbHNlLCJIQVNfSU1QT1JUUyI6ZmFsc2UsIklTX1dBU0kiOnRydWUsIlVTRV9TSEFSRURfTUVNT1JZIjpmYWxzZSwiVVNFX1dBU0lfQ0ROIjpmYWxzZX0sInN1YnN0aXR1dGlvbnMiOnsiUEFDS0FHRV9UT19KU19NT0RVTEVfUEFUSCI6Im1haW4ud2FzbSIsIlBBQ0tBR0VfVE9fSlNfUEFDS0FHRV9OQU1FIjoidGVzdCJ9fQ==", + "wants" : [ + "$OUTPUT", + "$OUTPUT\/platforms", + "$INTERMEDIATES\/wasm-imports.json" + ] + }, + { + "attributes" : [ + + ], + "inputs" : [ + "$PLANNER_SOURCE_PATH", + "$SELF_PACKAGE\/Plugins\/PackageToJS\/Templates\/platforms\/browser.worker.js", + "$INTERMEDIATES\/wasm-imports.json" + ], + "output" : "$OUTPUT\/platforms\/browser.worker.js", + "salt" : "eyJjb25kaXRpb25zIjp7IkhBU19CUklER0UiOmZhbHNlLCJIQVNfSU1QT1JUUyI6ZmFsc2UsIklTX1dBU0kiOnRydWUsIlVTRV9TSEFSRURfTUVNT1JZIjpmYWxzZSwiVVNFX1dBU0lfQ0ROIjpmYWxzZX0sInN1YnN0aXR1dGlvbnMiOnsiUEFDS0FHRV9UT19KU19NT0RVTEVfUEFUSCI6Im1haW4ud2FzbSIsIlBBQ0tBR0VfVE9fSlNfUEFDS0FHRV9OQU1FIjoidGVzdCJ9fQ==", + "wants" : [ + "$OUTPUT", + "$OUTPUT\/platforms", + "$INTERMEDIATES\/wasm-imports.json" + ] + }, + { + "attributes" : [ + + ], + "inputs" : [ + "$PLANNER_SOURCE_PATH", + "$SELF_PACKAGE\/Plugins\/PackageToJS\/Templates\/platforms\/node.d.ts", + "$INTERMEDIATES\/wasm-imports.json" + ], + "output" : "$OUTPUT\/platforms\/node.d.ts", + "salt" : "eyJjb25kaXRpb25zIjp7IkhBU19CUklER0UiOmZhbHNlLCJIQVNfSU1QT1JUUyI6ZmFsc2UsIklTX1dBU0kiOnRydWUsIlVTRV9TSEFSRURfTUVNT1JZIjpmYWxzZSwiVVNFX1dBU0lfQ0ROIjpmYWxzZX0sInN1YnN0aXR1dGlvbnMiOnsiUEFDS0FHRV9UT19KU19NT0RVTEVfUEFUSCI6Im1haW4ud2FzbSIsIlBBQ0tBR0VfVE9fSlNfUEFDS0FHRV9OQU1FIjoidGVzdCJ9fQ==", + "wants" : [ + "$OUTPUT", + "$OUTPUT\/platforms", + "$INTERMEDIATES\/wasm-imports.json" + ] + }, + { + "attributes" : [ + + ], + "inputs" : [ + "$PLANNER_SOURCE_PATH", + "$SELF_PACKAGE\/Plugins\/PackageToJS\/Templates\/platforms\/node.js", + "$INTERMEDIATES\/wasm-imports.json" + ], + "output" : "$OUTPUT\/platforms\/node.js", + "salt" : "eyJjb25kaXRpb25zIjp7IkhBU19CUklER0UiOmZhbHNlLCJIQVNfSU1QT1JUUyI6ZmFsc2UsIklTX1dBU0kiOnRydWUsIlVTRV9TSEFSRURfTUVNT1JZIjpmYWxzZSwiVVNFX1dBU0lfQ0ROIjpmYWxzZX0sInN1YnN0aXR1dGlvbnMiOnsiUEFDS0FHRV9UT19KU19NT0RVTEVfUEFUSCI6Im1haW4ud2FzbSIsIlBBQ0tBR0VfVE9fSlNfUEFDS0FHRV9OQU1FIjoidGVzdCJ9fQ==", + "wants" : [ + "$OUTPUT", + "$OUTPUT\/platforms", + "$INTERMEDIATES\/wasm-imports.json" + ] + }, + { + "attributes" : [ + + ], + "inputs" : [ + "$PLANNER_SOURCE_PATH", + "$SELF_PACKAGE\/Sources\/JavaScriptKit\/Runtime\/index.d.ts", + "$INTERMEDIATES\/wasm-imports.json" + ], + "output" : "$OUTPUT\/runtime.d.ts", + "salt" : "eyJjb25kaXRpb25zIjp7IkhBU19CUklER0UiOmZhbHNlLCJIQVNfSU1QT1JUUyI6ZmFsc2UsIklTX1dBU0kiOnRydWUsIlVTRV9TSEFSRURfTUVNT1JZIjpmYWxzZSwiVVNFX1dBU0lfQ0ROIjpmYWxzZX0sInN1YnN0aXR1dGlvbnMiOnsiUEFDS0FHRV9UT19KU19NT0RVTEVfUEFUSCI6Im1haW4ud2FzbSIsIlBBQ0tBR0VfVE9fSlNfUEFDS0FHRV9OQU1FIjoidGVzdCJ9fQ==", + "wants" : [ + "$OUTPUT", + "$OUTPUT\/platforms", + "$INTERMEDIATES\/wasm-imports.json" + ] + }, + { + "attributes" : [ + + ], + "inputs" : [ + "$PLANNER_SOURCE_PATH", + "$SELF_PACKAGE\/Sources\/JavaScriptKit\/Runtime\/index.mjs", + "$INTERMEDIATES\/wasm-imports.json" + ], + "output" : "$OUTPUT\/runtime.js", + "salt" : "eyJjb25kaXRpb25zIjp7IkhBU19CUklER0UiOmZhbHNlLCJIQVNfSU1QT1JUUyI6ZmFsc2UsIklTX1dBU0kiOnRydWUsIlVTRV9TSEFSRURfTUVNT1JZIjpmYWxzZSwiVVNFX1dBU0lfQ0ROIjpmYWxzZX0sInN1YnN0aXR1dGlvbnMiOnsiUEFDS0FHRV9UT19KU19NT0RVTEVfUEFUSCI6Im1haW4ud2FzbSIsIlBBQ0tBR0VfVE9fSlNfUEFDS0FHRV9OQU1FIjoidGVzdCJ9fQ==", + "wants" : [ + "$OUTPUT", + "$OUTPUT\/platforms", + "$INTERMEDIATES\/wasm-imports.json" + ] + }, + { + "attributes" : [ + "phony", + "silent" + ], + "inputs" : [ + + ], + "output" : "all", + "wants" : [ + "$OUTPUT\/main.wasm", + "$INTERMEDIATES\/wasm-imports.json", + "$OUTPUT\/package.json", + "$OUTPUT\/index.js", + "$OUTPUT\/index.d.ts", + "$OUTPUT\/instantiate.js", + "$OUTPUT\/instantiate.d.ts", + "$OUTPUT\/platforms\/browser.js", + "$OUTPUT\/platforms\/browser.d.ts", + "$OUTPUT\/platforms\/browser.worker.js", + "$OUTPUT\/platforms\/node.js", + "$OUTPUT\/platforms\/node.d.ts", + "$OUTPUT\/runtime.js", + "$OUTPUT\/runtime.d.ts" + ] + } +] \ No newline at end of file diff --git a/Plugins/PackageToJS/Tests/__Snapshots__/PackagingPlannerTests/planTestBuild.json b/Plugins/PackageToJS/Tests/__Snapshots__/PackagingPlannerTests/planTestBuild.json new file mode 100644 index 000000000..89425dc83 --- /dev/null +++ b/Plugins/PackageToJS/Tests/__Snapshots__/PackagingPlannerTests/planTestBuild.json @@ -0,0 +1,385 @@ +[ + { + "attributes" : [ + "silent" + ], + "inputs" : [ + "$PLANNER_SOURCE_PATH" + ], + "output" : "$INTERMEDIATES", + "wants" : [ + + ] + }, + { + "attributes" : [ + + ], + "inputs" : [ + "$PLANNER_SOURCE_PATH", + "$OUTPUT\/package.json" + ], + "output" : "$INTERMEDIATES\/npm-install.stamp", + "wants" : [ + "$INTERMEDIATES", + "$OUTPUT\/package.json" + ] + }, + { + "attributes" : [ + + ], + "inputs" : [ + "$PLANNER_SOURCE_PATH", + "$OUTPUT\/main.wasm" + ], + "output" : "$INTERMEDIATES\/wasm-imports.json", + "wants" : [ + "$OUTPUT", + "$INTERMEDIATES", + "$OUTPUT\/main.wasm" + ] + }, + { + "attributes" : [ + "silent" + ], + "inputs" : [ + "$PLANNER_SOURCE_PATH" + ], + "output" : "$OUTPUT", + "wants" : [ + + ] + }, + { + "attributes" : [ + + ], + "inputs" : [ + "$PLANNER_SOURCE_PATH" + ], + "output" : "$OUTPUT\/bin", + "wants" : [ + "$OUTPUT" + ] + }, + { + "attributes" : [ + + ], + "inputs" : [ + "$PLANNER_SOURCE_PATH", + "$SELF_PACKAGE\/Plugins\/PackageToJS\/Templates\/bin\/test.js" + ], + "output" : "$OUTPUT\/bin\/test.js", + "salt" : "eyJjb25kaXRpb25zIjp7IkhBU19CUklER0UiOmZhbHNlLCJIQVNfSU1QT1JUUyI6ZmFsc2UsIklTX1dBU0kiOnRydWUsIlVTRV9TSEFSRURfTUVNT1JZIjpmYWxzZSwiVVNFX1dBU0lfQ0ROIjpmYWxzZX0sInN1YnN0aXR1dGlvbnMiOnsiUEFDS0FHRV9UT19KU19NT0RVTEVfUEFUSCI6Im1haW4ud2FzbSIsIlBBQ0tBR0VfVE9fSlNfUEFDS0FHRV9OQU1FIjoidGVzdCJ9fQ==", + "wants" : [ + "$OUTPUT", + "$OUTPUT\/bin" + ] + }, + { + "attributes" : [ + + ], + "inputs" : [ + "$PLANNER_SOURCE_PATH", + "$SELF_PACKAGE\/Plugins\/PackageToJS\/Templates\/index.d.ts", + "$INTERMEDIATES\/wasm-imports.json" + ], + "output" : "$OUTPUT\/index.d.ts", + "salt" : "eyJjb25kaXRpb25zIjp7IkhBU19CUklER0UiOmZhbHNlLCJIQVNfSU1QT1JUUyI6ZmFsc2UsIklTX1dBU0kiOnRydWUsIlVTRV9TSEFSRURfTUVNT1JZIjpmYWxzZSwiVVNFX1dBU0lfQ0ROIjpmYWxzZX0sInN1YnN0aXR1dGlvbnMiOnsiUEFDS0FHRV9UT19KU19NT0RVTEVfUEFUSCI6Im1haW4ud2FzbSIsIlBBQ0tBR0VfVE9fSlNfUEFDS0FHRV9OQU1FIjoidGVzdCJ9fQ==", + "wants" : [ + "$OUTPUT", + "$OUTPUT\/platforms", + "$INTERMEDIATES\/wasm-imports.json" + ] + }, + { + "attributes" : [ + + ], + "inputs" : [ + "$PLANNER_SOURCE_PATH", + "$SELF_PACKAGE\/Plugins\/PackageToJS\/Templates\/index.js", + "$INTERMEDIATES\/wasm-imports.json" + ], + "output" : "$OUTPUT\/index.js", + "salt" : "eyJjb25kaXRpb25zIjp7IkhBU19CUklER0UiOmZhbHNlLCJIQVNfSU1QT1JUUyI6ZmFsc2UsIklTX1dBU0kiOnRydWUsIlVTRV9TSEFSRURfTUVNT1JZIjpmYWxzZSwiVVNFX1dBU0lfQ0ROIjpmYWxzZX0sInN1YnN0aXR1dGlvbnMiOnsiUEFDS0FHRV9UT19KU19NT0RVTEVfUEFUSCI6Im1haW4ud2FzbSIsIlBBQ0tBR0VfVE9fSlNfUEFDS0FHRV9OQU1FIjoidGVzdCJ9fQ==", + "wants" : [ + "$OUTPUT", + "$OUTPUT\/platforms", + "$INTERMEDIATES\/wasm-imports.json" + ] + }, + { + "attributes" : [ + + ], + "inputs" : [ + "$PLANNER_SOURCE_PATH", + "$SELF_PACKAGE\/Plugins\/PackageToJS\/Templates\/instantiate.d.ts", + "$INTERMEDIATES\/wasm-imports.json" + ], + "output" : "$OUTPUT\/instantiate.d.ts", + "salt" : "eyJjb25kaXRpb25zIjp7IkhBU19CUklER0UiOmZhbHNlLCJIQVNfSU1QT1JUUyI6ZmFsc2UsIklTX1dBU0kiOnRydWUsIlVTRV9TSEFSRURfTUVNT1JZIjpmYWxzZSwiVVNFX1dBU0lfQ0ROIjpmYWxzZX0sInN1YnN0aXR1dGlvbnMiOnsiUEFDS0FHRV9UT19KU19NT0RVTEVfUEFUSCI6Im1haW4ud2FzbSIsIlBBQ0tBR0VfVE9fSlNfUEFDS0FHRV9OQU1FIjoidGVzdCJ9fQ==", + "wants" : [ + "$OUTPUT", + "$OUTPUT\/platforms", + "$INTERMEDIATES\/wasm-imports.json" + ] + }, + { + "attributes" : [ + + ], + "inputs" : [ + "$PLANNER_SOURCE_PATH", + "$SELF_PACKAGE\/Plugins\/PackageToJS\/Templates\/instantiate.js", + "$INTERMEDIATES\/wasm-imports.json" + ], + "output" : "$OUTPUT\/instantiate.js", + "salt" : "eyJjb25kaXRpb25zIjp7IkhBU19CUklER0UiOmZhbHNlLCJIQVNfSU1QT1JUUyI6ZmFsc2UsIklTX1dBU0kiOnRydWUsIlVTRV9TSEFSRURfTUVNT1JZIjpmYWxzZSwiVVNFX1dBU0lfQ0ROIjpmYWxzZX0sInN1YnN0aXR1dGlvbnMiOnsiUEFDS0FHRV9UT19KU19NT0RVTEVfUEFUSCI6Im1haW4ud2FzbSIsIlBBQ0tBR0VfVE9fSlNfUEFDS0FHRV9OQU1FIjoidGVzdCJ9fQ==", + "wants" : [ + "$OUTPUT", + "$OUTPUT\/platforms", + "$INTERMEDIATES\/wasm-imports.json" + ] + }, + { + "attributes" : [ + + ], + "inputs" : [ + "$PLANNER_SOURCE_PATH", + "$WASM_PRODUCT_ARTIFACT" + ], + "output" : "$OUTPUT\/main.wasm", + "wants" : [ + "$OUTPUT" + ] + }, + { + "attributes" : [ + + ], + "inputs" : [ + "$PLANNER_SOURCE_PATH", + "$SELF_PACKAGE\/Plugins\/PackageToJS\/Templates\/package.json" + ], + "output" : "$OUTPUT\/package.json", + "salt" : "eyJjb25kaXRpb25zIjp7IkhBU19CUklER0UiOmZhbHNlLCJIQVNfSU1QT1JUUyI6ZmFsc2UsIklTX1dBU0kiOnRydWUsIlVTRV9TSEFSRURfTUVNT1JZIjpmYWxzZSwiVVNFX1dBU0lfQ0ROIjpmYWxzZX0sInN1YnN0aXR1dGlvbnMiOnsiUEFDS0FHRV9UT19KU19NT0RVTEVfUEFUSCI6Im1haW4ud2FzbSIsIlBBQ0tBR0VfVE9fSlNfUEFDS0FHRV9OQU1FIjoidGVzdCJ9fQ==", + "wants" : [ + "$OUTPUT" + ] + }, + { + "attributes" : [ + "silent" + ], + "inputs" : [ + "$PLANNER_SOURCE_PATH" + ], + "output" : "$OUTPUT\/platforms", + "wants" : [ + + ] + }, + { + "attributes" : [ + + ], + "inputs" : [ + "$PLANNER_SOURCE_PATH", + "$SELF_PACKAGE\/Plugins\/PackageToJS\/Templates\/platforms\/browser.d.ts", + "$INTERMEDIATES\/wasm-imports.json" + ], + "output" : "$OUTPUT\/platforms\/browser.d.ts", + "salt" : "eyJjb25kaXRpb25zIjp7IkhBU19CUklER0UiOmZhbHNlLCJIQVNfSU1QT1JUUyI6ZmFsc2UsIklTX1dBU0kiOnRydWUsIlVTRV9TSEFSRURfTUVNT1JZIjpmYWxzZSwiVVNFX1dBU0lfQ0ROIjpmYWxzZX0sInN1YnN0aXR1dGlvbnMiOnsiUEFDS0FHRV9UT19KU19NT0RVTEVfUEFUSCI6Im1haW4ud2FzbSIsIlBBQ0tBR0VfVE9fSlNfUEFDS0FHRV9OQU1FIjoidGVzdCJ9fQ==", + "wants" : [ + "$OUTPUT", + "$OUTPUT\/platforms", + "$INTERMEDIATES\/wasm-imports.json" + ] + }, + { + "attributes" : [ + + ], + "inputs" : [ + "$PLANNER_SOURCE_PATH", + "$SELF_PACKAGE\/Plugins\/PackageToJS\/Templates\/platforms\/browser.js", + "$INTERMEDIATES\/wasm-imports.json" + ], + "output" : "$OUTPUT\/platforms\/browser.js", + "salt" : "eyJjb25kaXRpb25zIjp7IkhBU19CUklER0UiOmZhbHNlLCJIQVNfSU1QT1JUUyI6ZmFsc2UsIklTX1dBU0kiOnRydWUsIlVTRV9TSEFSRURfTUVNT1JZIjpmYWxzZSwiVVNFX1dBU0lfQ0ROIjpmYWxzZX0sInN1YnN0aXR1dGlvbnMiOnsiUEFDS0FHRV9UT19KU19NT0RVTEVfUEFUSCI6Im1haW4ud2FzbSIsIlBBQ0tBR0VfVE9fSlNfUEFDS0FHRV9OQU1FIjoidGVzdCJ9fQ==", + "wants" : [ + "$OUTPUT", + "$OUTPUT\/platforms", + "$INTERMEDIATES\/wasm-imports.json" + ] + }, + { + "attributes" : [ + + ], + "inputs" : [ + "$PLANNER_SOURCE_PATH", + "$SELF_PACKAGE\/Plugins\/PackageToJS\/Templates\/platforms\/browser.worker.js", + "$INTERMEDIATES\/wasm-imports.json" + ], + "output" : "$OUTPUT\/platforms\/browser.worker.js", + "salt" : "eyJjb25kaXRpb25zIjp7IkhBU19CUklER0UiOmZhbHNlLCJIQVNfSU1QT1JUUyI6ZmFsc2UsIklTX1dBU0kiOnRydWUsIlVTRV9TSEFSRURfTUVNT1JZIjpmYWxzZSwiVVNFX1dBU0lfQ0ROIjpmYWxzZX0sInN1YnN0aXR1dGlvbnMiOnsiUEFDS0FHRV9UT19KU19NT0RVTEVfUEFUSCI6Im1haW4ud2FzbSIsIlBBQ0tBR0VfVE9fSlNfUEFDS0FHRV9OQU1FIjoidGVzdCJ9fQ==", + "wants" : [ + "$OUTPUT", + "$OUTPUT\/platforms", + "$INTERMEDIATES\/wasm-imports.json" + ] + }, + { + "attributes" : [ + + ], + "inputs" : [ + "$PLANNER_SOURCE_PATH", + "$SELF_PACKAGE\/Plugins\/PackageToJS\/Templates\/platforms\/node.d.ts", + "$INTERMEDIATES\/wasm-imports.json" + ], + "output" : "$OUTPUT\/platforms\/node.d.ts", + "salt" : "eyJjb25kaXRpb25zIjp7IkhBU19CUklER0UiOmZhbHNlLCJIQVNfSU1QT1JUUyI6ZmFsc2UsIklTX1dBU0kiOnRydWUsIlVTRV9TSEFSRURfTUVNT1JZIjpmYWxzZSwiVVNFX1dBU0lfQ0ROIjpmYWxzZX0sInN1YnN0aXR1dGlvbnMiOnsiUEFDS0FHRV9UT19KU19NT0RVTEVfUEFUSCI6Im1haW4ud2FzbSIsIlBBQ0tBR0VfVE9fSlNfUEFDS0FHRV9OQU1FIjoidGVzdCJ9fQ==", + "wants" : [ + "$OUTPUT", + "$OUTPUT\/platforms", + "$INTERMEDIATES\/wasm-imports.json" + ] + }, + { + "attributes" : [ + + ], + "inputs" : [ + "$PLANNER_SOURCE_PATH", + "$SELF_PACKAGE\/Plugins\/PackageToJS\/Templates\/platforms\/node.js", + "$INTERMEDIATES\/wasm-imports.json" + ], + "output" : "$OUTPUT\/platforms\/node.js", + "salt" : "eyJjb25kaXRpb25zIjp7IkhBU19CUklER0UiOmZhbHNlLCJIQVNfSU1QT1JUUyI6ZmFsc2UsIklTX1dBU0kiOnRydWUsIlVTRV9TSEFSRURfTUVNT1JZIjpmYWxzZSwiVVNFX1dBU0lfQ0ROIjpmYWxzZX0sInN1YnN0aXR1dGlvbnMiOnsiUEFDS0FHRV9UT19KU19NT0RVTEVfUEFUSCI6Im1haW4ud2FzbSIsIlBBQ0tBR0VfVE9fSlNfUEFDS0FHRV9OQU1FIjoidGVzdCJ9fQ==", + "wants" : [ + "$OUTPUT", + "$OUTPUT\/platforms", + "$INTERMEDIATES\/wasm-imports.json" + ] + }, + { + "attributes" : [ + + ], + "inputs" : [ + "$PLANNER_SOURCE_PATH", + "$SELF_PACKAGE\/Sources\/JavaScriptKit\/Runtime\/index.d.ts", + "$INTERMEDIATES\/wasm-imports.json" + ], + "output" : "$OUTPUT\/runtime.d.ts", + "salt" : "eyJjb25kaXRpb25zIjp7IkhBU19CUklER0UiOmZhbHNlLCJIQVNfSU1QT1JUUyI6ZmFsc2UsIklTX1dBU0kiOnRydWUsIlVTRV9TSEFSRURfTUVNT1JZIjpmYWxzZSwiVVNFX1dBU0lfQ0ROIjpmYWxzZX0sInN1YnN0aXR1dGlvbnMiOnsiUEFDS0FHRV9UT19KU19NT0RVTEVfUEFUSCI6Im1haW4ud2FzbSIsIlBBQ0tBR0VfVE9fSlNfUEFDS0FHRV9OQU1FIjoidGVzdCJ9fQ==", + "wants" : [ + "$OUTPUT", + "$OUTPUT\/platforms", + "$INTERMEDIATES\/wasm-imports.json" + ] + }, + { + "attributes" : [ + + ], + "inputs" : [ + "$PLANNER_SOURCE_PATH", + "$SELF_PACKAGE\/Sources\/JavaScriptKit\/Runtime\/index.mjs", + "$INTERMEDIATES\/wasm-imports.json" + ], + "output" : "$OUTPUT\/runtime.js", + "salt" : "eyJjb25kaXRpb25zIjp7IkhBU19CUklER0UiOmZhbHNlLCJIQVNfSU1QT1JUUyI6ZmFsc2UsIklTX1dBU0kiOnRydWUsIlVTRV9TSEFSRURfTUVNT1JZIjpmYWxzZSwiVVNFX1dBU0lfQ0ROIjpmYWxzZX0sInN1YnN0aXR1dGlvbnMiOnsiUEFDS0FHRV9UT19KU19NT0RVTEVfUEFUSCI6Im1haW4ud2FzbSIsIlBBQ0tBR0VfVE9fSlNfUEFDS0FHRV9OQU1FIjoidGVzdCJ9fQ==", + "wants" : [ + "$OUTPUT", + "$OUTPUT\/platforms", + "$INTERMEDIATES\/wasm-imports.json" + ] + }, + { + "attributes" : [ + + ], + "inputs" : [ + "$PLANNER_SOURCE_PATH", + "$SELF_PACKAGE\/Plugins\/PackageToJS\/Templates\/test.browser.html" + ], + "output" : "$OUTPUT\/test.browser.html", + "salt" : "eyJjb25kaXRpb25zIjp7IkhBU19CUklER0UiOmZhbHNlLCJIQVNfSU1QT1JUUyI6ZmFsc2UsIklTX1dBU0kiOnRydWUsIlVTRV9TSEFSRURfTUVNT1JZIjpmYWxzZSwiVVNFX1dBU0lfQ0ROIjpmYWxzZX0sInN1YnN0aXR1dGlvbnMiOnsiUEFDS0FHRV9UT19KU19NT0RVTEVfUEFUSCI6Im1haW4ud2FzbSIsIlBBQ0tBR0VfVE9fSlNfUEFDS0FHRV9OQU1FIjoidGVzdCJ9fQ==", + "wants" : [ + "$OUTPUT", + "$OUTPUT\/bin" + ] + }, + { + "attributes" : [ + + ], + "inputs" : [ + "$PLANNER_SOURCE_PATH", + "$SELF_PACKAGE\/Plugins\/PackageToJS\/Templates\/test.d.ts" + ], + "output" : "$OUTPUT\/test.d.ts", + "salt" : "eyJjb25kaXRpb25zIjp7IkhBU19CUklER0UiOmZhbHNlLCJIQVNfSU1QT1JUUyI6ZmFsc2UsIklTX1dBU0kiOnRydWUsIlVTRV9TSEFSRURfTUVNT1JZIjpmYWxzZSwiVVNFX1dBU0lfQ0ROIjpmYWxzZX0sInN1YnN0aXR1dGlvbnMiOnsiUEFDS0FHRV9UT19KU19NT0RVTEVfUEFUSCI6Im1haW4ud2FzbSIsIlBBQ0tBR0VfVE9fSlNfUEFDS0FHRV9OQU1FIjoidGVzdCJ9fQ==", + "wants" : [ + "$OUTPUT", + "$OUTPUT\/bin" + ] + }, + { + "attributes" : [ + + ], + "inputs" : [ + "$PLANNER_SOURCE_PATH", + "$SELF_PACKAGE\/Plugins\/PackageToJS\/Templates\/test.js" + ], + "output" : "$OUTPUT\/test.js", + "salt" : "eyJjb25kaXRpb25zIjp7IkhBU19CUklER0UiOmZhbHNlLCJIQVNfSU1QT1JUUyI6ZmFsc2UsIklTX1dBU0kiOnRydWUsIlVTRV9TSEFSRURfTUVNT1JZIjpmYWxzZSwiVVNFX1dBU0lfQ0ROIjpmYWxzZX0sInN1YnN0aXR1dGlvbnMiOnsiUEFDS0FHRV9UT19KU19NT0RVTEVfUEFUSCI6Im1haW4ud2FzbSIsIlBBQ0tBR0VfVE9fSlNfUEFDS0FHRV9OQU1FIjoidGVzdCJ9fQ==", + "wants" : [ + "$OUTPUT", + "$OUTPUT\/bin" + ] + }, + { + "attributes" : [ + "phony", + "silent" + ], + "inputs" : [ + + ], + "output" : "all", + "wants" : [ + "$OUTPUT\/main.wasm", + "$INTERMEDIATES\/wasm-imports.json", + "$OUTPUT\/package.json", + "$OUTPUT\/index.js", + "$OUTPUT\/index.d.ts", + "$OUTPUT\/instantiate.js", + "$OUTPUT\/instantiate.d.ts", + "$OUTPUT\/platforms\/browser.js", + "$OUTPUT\/platforms\/browser.d.ts", + "$OUTPUT\/platforms\/browser.worker.js", + "$OUTPUT\/platforms\/node.js", + "$OUTPUT\/platforms\/node.d.ts", + "$OUTPUT\/runtime.js", + "$OUTPUT\/runtime.d.ts", + "$INTERMEDIATES\/npm-install.stamp", + "$OUTPUT\/bin", + "$OUTPUT\/test.js", + "$OUTPUT\/test.d.ts", + "$OUTPUT\/test.browser.html", + "$OUTPUT\/bin\/test.js" + ] + } +] \ No newline at end of file diff --git a/Plugins/PackageToJS/Tests/__Snapshots__/TestsParserTests/testAllPassed.txt b/Plugins/PackageToJS/Tests/__Snapshots__/TestsParserTests/testAllPassed.txt new file mode 100644 index 000000000..16683bfb0 --- /dev/null +++ b/Plugins/PackageToJS/Tests/__Snapshots__/TestsParserTests/testAllPassed.txt @@ -0,0 +1,9 @@ + PASSED  CounterTests + ✔ testIncrement (0.002s) + ✔ testIncrementTwice (0.001s) + PASSED  /.xctest + PASSED  All tests + +Test Suites: 1 passed, 1 total +Tests: 2 passed, 2 total +Ran all test suites. diff --git a/Plugins/PackageToJS/Tests/__Snapshots__/TestsParserTests/testAssertFailure.txt b/Plugins/PackageToJS/Tests/__Snapshots__/TestsParserTests/testAssertFailure.txt new file mode 100644 index 000000000..323cd70a7 --- /dev/null +++ b/Plugins/PackageToJS/Tests/__Snapshots__/TestsParserTests/testAssertFailure.txt @@ -0,0 +1,14 @@ +/tmp/Tests/CounterTests/CounterTests.swift:27: error: CounterTests.testAssertailure : XCTAssertEqual failed: ("1") is not equal to ("2") - + FAILED  CounterTests + ✘ testAssertailure (0.001s) + FAILED  /.xctest + FAILED  All tests + +Test Suites: 1 failed, 1 total +Tests: 1 failed, 1 total +Ran all test suites. + +Failed test cases: + ✘ CounterTests.testAssertailure + +Some tests failed. Use --verbose for raw test output. diff --git a/Plugins/PackageToJS/Tests/__Snapshots__/TestsParserTests/testCrash.txt b/Plugins/PackageToJS/Tests/__Snapshots__/TestsParserTests/testCrash.txt new file mode 100644 index 000000000..02977cc1d --- /dev/null +++ b/Plugins/PackageToJS/Tests/__Snapshots__/TestsParserTests/testCrash.txt @@ -0,0 +1,22 @@ +CounterTests/CounterTests.swift:26: Fatal error: Crash +wasm://wasm/CounterPackageTests.xctest-0ef3150a:1 +RuntimeError: unreachable + at CounterPackageTests.xctest.$ss17_assertionFailure__4file4line5flagss5NeverOs12StaticStringV_SSAHSus6UInt32VtF (wasm://wasm/CounterPackageTests.xctest-0ef3150a:wasm-function[5087]:0x1475da) + at CounterPackageTests.xctest.$s12CounterTestsAAC13testIncrementyyYaKFTY1_ (wasm://wasm/CounterPackageTests.xctest-0ef3150a:wasm-function[1448]:0x9a33b) + at CounterPackageTests.xctest.swift::runJobInEstablishedExecutorContext(swift::Job*) (wasm://wasm/CounterPackageTests.xctest-0ef3150a:wasm-function[29848]:0x58cb39) + at CounterPackageTests.xctest.swift_job_run (wasm://wasm/CounterPackageTests.xctest-0ef3150a:wasm-function[29863]:0x58d720) + at CounterPackageTests.xctest.$sScJ16runSynchronously2onySce_tF (wasm://wasm/CounterPackageTests.xctest-0ef3150a:wasm-function[1571]:0x9fe5a) + at CounterPackageTests.xctest.$s19JavaScriptEventLoopAAC10runAllJobsyyF (wasm://wasm/CounterPackageTests.xctest-0ef3150a:wasm-function[1675]:0xa32c4) + at CounterPackageTests.xctest.$s19JavaScriptEventLoopAAC14insertJobQueue3jobyScJ_tFyycfU0_ (wasm://wasm/CounterPackageTests.xctest-0ef3150a:wasm-function[1674]:0xa30b7) + at CounterPackageTests.xctest.$s19JavaScriptEventLoopAAC14insertJobQueue3jobyScJ_tFyycfU0_TA (wasm://wasm/CounterPackageTests.xctest-0ef3150a:wasm-function[1666]:0xa2c6b) + at CounterPackageTests.xctest.$s19JavaScriptEventLoopAAC6create33_F9DB15AFB1FFBEDBFE9D13500E01F3F2LLAByFZyyyccfU0_0aB3Kit20ConvertibleToJSValue_pAE0Q0OcfU_ (wasm://wasm/CounterPackageTests.xctest-0ef3150a:wasm-function[1541]:0x9de13) + at CounterPackageTests.xctest.$s19JavaScriptEventLoopAAC6create33_F9DB15AFB1FFBEDBFE9D13500E01F3F2LLAByFZyyyccfU0_0aB3Kit20ConvertibleToJSValue_pAE0Q0OcfU_TA (wasm://wasm/CounterPackageTests.xctest-0ef3150a:wasm-function[1540]:0x9dd8d) + +Test Suites: 1 unknown, 1 total +Tests: 1 unknown, 1 total +Ran all test suites. + +Failed test cases: + ? CounterTests.testIncrement + +Some tests failed. Use --verbose for raw test output. diff --git a/Plugins/PackageToJS/Tests/__Snapshots__/TestsParserTests/testSkipped.txt b/Plugins/PackageToJS/Tests/__Snapshots__/TestsParserTests/testSkipped.txt new file mode 100644 index 000000000..9873deff6 --- /dev/null +++ b/Plugins/PackageToJS/Tests/__Snapshots__/TestsParserTests/testSkipped.txt @@ -0,0 +1,10 @@ +/tmp/Tests/CounterTests/CounterTests.swift:25: CounterTests.testIncrement : Test skipped - Skip it + PASSED  CounterTests + ➜ testIncrement (0.006s) + ✔ testIncrementTwice (0.0s) + PASSED  /.xctest + PASSED  All tests + +Test Suites: 1 passed, 1 total +Tests: 1 passed, 1 skipped, 2 total +Ran all test suites. diff --git a/Plugins/PackageToJS/Tests/__Snapshots__/TestsParserTests/testThrowFailure.txt b/Plugins/PackageToJS/Tests/__Snapshots__/TestsParserTests/testThrowFailure.txt new file mode 100644 index 000000000..9c2e3613c --- /dev/null +++ b/Plugins/PackageToJS/Tests/__Snapshots__/TestsParserTests/testThrowFailure.txt @@ -0,0 +1,14 @@ +:0: error: CounterTests.testThrowFailure : threw error "TestError()" + FAILED  CounterTests + ✘ testThrowFailure (0.002s) + FAILED  /.xctest + FAILED  All tests + +Test Suites: 1 failed, 1 total +Tests: 1 failed, 1 total +Ran all test suites. + +Failed test cases: + ✘ CounterTests.testThrowFailure + +Some tests failed. Use --verbose for raw test output. diff --git a/README.md b/README.md index 46dc963a9..2f41b7a31 100644 --- a/README.md +++ b/README.md @@ -1,238 +1,55 @@ # JavaScriptKit -![Run unit tests](https://github.com/swiftwasm/JavaScriptKit/workflows/Run%20unit%20tests/badge.svg?branch=main) +[![Run unit tests](https://github.com/swiftwasm/JavaScriptKit/actions/workflows/test.yml/badge.svg)](https://github.com/swiftwasm/JavaScriptKit/actions/workflows/test.yml) +[![](https://img.shields.io/badge/docc-read_documentation-blue)](https://swiftpackageindex.com/swiftwasm/JavaScriptKit/documentation) Swift framework to interact with JavaScript through WebAssembly. -## Getting started +## Quick Start -This JavaScript code +Check out the [Hello World](https://swiftpackageindex.com/swiftwasm/JavaScriptKit/tutorials/javascriptkit/hello-world) tutorial for a step-by-step guide to getting started. -```javascript -const alert = window.alert; -const document = window.document; +## Overview -const divElement = document.createElement("div"); -divElement.innerText = "Hello, world"; -const body = document.body; -body.appendChild(divElement); +JavaScriptKit provides a seamless way to interact with JavaScript from Swift code when compiled to WebAssembly. It allows Swift developers to: -const pet = { - age: 3, - owner: { - name: "Mike", - }, -}; - -alert("JavaScript is running on browser!"); -``` - -Can be written in Swift using JavaScriptKit - -```swift -import JavaScriptKit - -let document = JSObject.global.document - -var divElement = document.createElement("div") -divElement.innerText = "Hello, world" -_ = document.body.appendChild(divElement) - -struct Owner: Codable { - let name: String -} - -struct Pet: Codable { - let age: Int - let owner: Owner -} - -let jsPet = JSObject.global.pet -let swiftPet: Pet = try JSValueDecoder().decode(from: jsPet) - -_ = JSObject.global.alert!("Swift is running in the browser!") -``` - -### `async`/`await` - -Starting with SwiftWasm 5.5 you can use `async`/`await` with `JSPromise` objects. This requires -a few additional steps though (you can skip these steps if your app depends on -[Tokamak](https://tokamak.dev)): - -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() -``` - -Then you can `await` on the `value` property of `JSPromise` instances, like in the example below: +- Access JavaScript objects and functions +- Create closures that can be called from JavaScript +- Convert between Swift and JavaScript data types +- Use JavaScript promises with Swift's `async/await` +- Work with multi-threading ```swift import JavaScriptKit -import JavaScriptEventLoop -let alert = JSObject.global.alert.function! +// Access global JavaScript objects let document = JSObject.global.document -private let jsFetch = JSObject.global.fetch.function! -func fetch(_ url: String) -> JSPromise { - JSPromise(jsFetch(url).object!)! -} - -JavaScriptEventLoop.installGlobalExecutor() - -struct Response: Decodable { - let uuid: String -} - -var 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) - } - } +// Create and manipulate DOM elements +var div = document.createElement("div") +div.innerText = "Hello from Swift!" +_ = document.body.appendChild(div) +// Handle events with Swift closures +var button = document.createElement("button") +button.innerText = "Click me" +button.onclick = .object(JSClosure { _ in + JSObject.global.alert!("Button clicked!") return .undefined }) - -_ = document.body.appendChild(asyncButtonElement) +_ = document.body.appendChild(button) ``` -## Requirements - -### For developers - -- macOS 11 and Xcode 13.2 or later versions, which support Swift Concurrency back-deployment. -To use earlier versions of Xcode on macOS 11 you'll have to -add `.unsafeFlags(["-Xfrontend", "-disable-availability-checking"])` in `Package.swift` manifest of -your package that depends on JavaScriptKit. You can also use Xcode 13.0 and 13.1 on macOS Monterey, -since this OS does not need back-deployment. -- [Swift 5.5 or later](https://swift.org/download/) and Ubuntu 18.04 if you'd like to use Linux. - Other Linux distributions are currently not supported. - -### For users of apps depending on JavaScriptKit - -Any recent browser that [supports WebAssembly](https://caniuse.com/#feat=wasm) and required -JavaScript features should work, which currently includes: +Check out the [examples](https://github.com/swiftwasm/JavaScriptKit/tree/main/Examples) for more detailed usage. -- Edge 84+ -- Firefox 79+ -- Chrome 84+ -- Desktop Safari 14.1+ -- Mobile Safari 14.8+ +## Contributing -If you need to support older browser versions, you'll have to build with -`JAVASCRIPTKIT_WITHOUT_WEAKREFS` flag, passing `-Xswiftc -DJAVASCRIPTKIT_WITHOUT_WEAKREFS` flags -when compiling. This should lower browser requirements to these versions: +Contributions are welcome! See [CONTRIBUTING.md](CONTRIBUTING.md) for details on how to contribute to the project. -- Edge 16+ -- Firefox 61+ -- Chrome 66+ -- (Mobile) Safari 12+ +## Sponsoring -Not all of these versions are tested on regular basis though, compatibility reports are very welcome! +[Become a gold or platinum sponsor](https://github.com/sponsors/swiftwasm/) and contact maintainers to add your logo on our README on Github with a link to your site. -## Usage in a browser application - -The easiest way to get started with JavaScriptKit in your browser app is with [the `carton` -bundler](https://carton.dev). - -As a part of these steps -you'll install `carton` via [Homebrew](https://brew.sh/) on macOS (you can also use the -[`ghcr.io/swiftwasm/carton`](https://github.com/orgs/swiftwasm/packages/container/package/carton) -Docker image if you prefer to run the build steps on Linux). Assuming you already have Homebrew -installed, you can create a new app that uses JavaScriptKit by following these steps: - -1. Install `carton`: - -``` -brew install swiftwasm/tap/carton -``` - -If you had `carton` installed before this, make sure you have version 0.6.1 or greater: - -``` -carton --version -``` - -2. Create a directory for your project and make it current: - -``` -mkdir SwiftWasmApp && cd SwiftWasmApp -``` - -3. Initialize the project from a template with `carton`: - -``` -carton init --template basic -``` - -4. Build the project and start the development server, `carton dev` can be kept running - during development: - -``` -carton dev -``` - -5. Open [http://127.0.0.1:8080/](http://127.0.0.1:8080/) in your browser and a developer console - within it. You'll see `Hello, world!` output in the console. You can edit the app source code in - your favorite editor and save it, `carton` will immediately rebuild the app and reload all - browser tabs that have the app open. - -You can also build your project with webpack.js and a manually installed SwiftWasm toolchain. Please -see the following sections and the [Example](https://github.com/swiftwasm/JavaScriptKit/tree/main/Example) -directory for more information in this more advanced use case. - -## Manual toolchain installation - -This library only supports [`swiftwasm/swift`](https://github.com/swiftwasm/swift) toolchain distribution. -The toolchain can be installed via [`swiftenv`](https://github.com/kylef/swiftenv), in -the same way as the official Swift nightly toolchain. - -You have to install the toolchain manually when working on the source code of JavaScriptKit itself, -especially if you change anything in the JavaScript runtime parts. This is because the runtime parts are -embedded in `carton` and currently can't be replaced dynamically with the JavaScript code you've -updated locally. - -Just pass a toolchain archive URL for [the latest SwiftWasm 5.5 -release](https://github.com/swiftwasm/swift/releases) appropriate for your platform: - -```sh -$ swiftenv install https://github.com/swiftwasm/swift/releases/download/swift-wasm-5.5.0-RELEASE/swift-wasm-5.5.0-RELEASE-macos_x86_64.pkg -``` - -You can also use the `install-toolchain.sh` helper script that uses a hardcoded toolchain snapshot: - -```sh -$ ./scripts/install-toolchain.sh -$ swift --version -Swift version 5.5 (swiftlang-5.5.0) -Target: arm64-apple-darwin20.6.0 -``` + + + diff --git a/Runtime/rollup.config.js b/Runtime/rollup.config.js deleted file mode 100644 index cf9b49b57..000000000 --- a/Runtime/rollup.config.js +++ /dev/null @@ -1,20 +0,0 @@ -import typescript from "@rollup/plugin-typescript"; - -/** @type {import('rollup').RollupOptions} */ -const config = { - input: "src/index.ts", - output: [ - { - file: "lib/index.mjs", - format: "esm", - }, - { - dir: "lib", - format: "umd", - name: "JavaScriptKit", - }, - ], - plugins: [typescript()], -}; - -export default config; diff --git a/Runtime/rollup.config.mjs b/Runtime/rollup.config.mjs new file mode 100644 index 000000000..b29609fe1 --- /dev/null +++ b/Runtime/rollup.config.mjs @@ -0,0 +1,26 @@ +import typescript from "@rollup/plugin-typescript"; +import dts from "rollup-plugin-dts"; + +/** @type {import('rollup').RollupOptions} */ +const config = [ + { + input: "src/index.ts", + output: [ + { + file: "lib/index.mjs", + format: "esm", + }, + ], + plugins: [typescript()], + }, + { + input: "src/index.ts", + output: { + file: "lib/index.d.ts", + format: "esm", + }, + plugins: [dts()], + }, +]; + +export default config; diff --git a/Runtime/src/closure-heap.ts b/Runtime/src/closure-heap.ts index 8d2c5600c..269934390 100644 --- a/Runtime/src/closure-heap.ts +++ b/Runtime/src/closure-heap.ts @@ -1,4 +1,4 @@ -import { ExportedFunctions } from "./types"; +import { ExportedFunctions } from "./types.js"; /// Memory lifetime of closures in Swift are managed by Swift side export class SwiftClosureDeallocator { diff --git a/Runtime/src/index.ts b/Runtime/src/index.ts index baf9ffd17..05c2964f4 100644 --- a/Runtime/src/index.ts +++ b/Runtime/src/index.ts @@ -1,41 +1,120 @@ -import { SwiftClosureDeallocator } from "./closure-heap"; +import { SwiftClosureDeallocator } from "./closure-heap.js"; import { LibraryFeatures, ExportedFunctions, ref, pointer, TypedArray, - ImportedFunctions, -} from "./types"; -import * as JSValue from "./js-value"; -import { Memory } from "./memory"; + MAIN_THREAD_TID, +} from "./types.js"; +import * as JSValue from "./js-value.js"; +import { Memory } from "./memory.js"; +import { deserializeError, MainToWorkerMessage, MessageBroker, ResponseMessage, ITCInterface, serializeError, SwiftRuntimeThreadChannel, WorkerToMainMessage } from "./itc.js"; +import { decodeObjectRefs } from "./js-value.js"; +export { SwiftRuntimeThreadChannel }; + +export 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; +}; export class SwiftRuntime { private _instance: WebAssembly.Instance | null; private _memory: Memory | null; private _closureDeallocator: SwiftClosureDeallocator | null; - private version: number = 705; + private options: SwiftRuntimeOptions; + private version: number = 708; private textDecoder = new TextDecoder("utf-8"); private textEncoder = new TextEncoder(); // Only support utf-8 + /** The thread ID of the current thread. */ + private tid: number | null; + + UnsafeEventLoopYield = UnsafeEventLoopYield; - constructor() { + constructor(options?: SwiftRuntimeOptions) { this._instance = null; this._memory = null; this._closureDeallocator = null; + this.tid = null; + this.options = options || {}; } setInstance(instance: WebAssembly.Instance) { this._instance = instance; + if (typeof (this.exports as any)._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. ${this.exports.swjs_library_version()} != ${ + `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: number, startArg: number) { + 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; + } + } + private get instance() { if (!this._instance) throw new Error("WebAssembly instance is not set yet"); @@ -67,32 +146,36 @@ export class SwiftRuntime { return this._closureDeallocator; } - private callHostFunction(host_func_id: number, args: any[]) { + private callHostFunction( + host_func_id: number, + line: number, + file: string, + args: any[] + ) { 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; - JSValue.write( - argument, - base, - base + 4, - base + 8, - false, - this.memory - ); + JSValue.write(argument, base, base + 4, base + 8, false, memory); } let output: any; // This ref is released by the swjs_call_host_function implementation - const callback_func_ref = this.memory.retain((result: any) => { + const callback_func_ref = memory.retain((result: any) => { output = result; }); - this.exports.swjs_call_host_function( + 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; } @@ -100,255 +183,572 @@ export class SwiftRuntime { /** @deprecated Use `wasmImports` instead */ importObjects = () => this.wasmImports; - readonly wasmImports: ImportedFunctions = { - swjs_set_prop: ( - ref: ref, - name: ref, - kind: JSValue.Kind, - payload1: number, - payload2: number - ) => { - const obj = this.memory.getObject(ref); - Reflect.set( - obj, - this.memory.getObject(name), - JSValue.decode(kind, payload1, payload2, this.memory) - ); - }, - - swjs_get_prop: ( - ref: ref, - name: ref, - kind_ptr: pointer, - payload1_ptr: pointer, - payload2_ptr: pointer - ) => { - const obj = this.memory.getObject(ref); - const result = Reflect.get(obj, this.memory.getObject(name)); - JSValue.write( - result, - kind_ptr, - payload1_ptr, - payload2_ptr, - false, - this.memory - ); - }, - - swjs_set_subscript: ( - ref: ref, - index: number, - kind: JSValue.Kind, - payload1: number, - payload2: number - ) => { - const obj = this.memory.getObject(ref); - Reflect.set( - obj, - index, - JSValue.decode(kind, payload1, payload2, this.memory) - ); - }, - - swjs_get_subscript: ( - ref: ref, - index: number, - kind_ptr: pointer, - payload1_ptr: pointer, - payload2_ptr: pointer - ) => { - const obj = this.memory.getObject(ref); - const result = Reflect.get(obj, index); - JSValue.write( - result, - kind_ptr, - payload1_ptr, - payload2_ptr, - false, - this.memory - ); - }, - - swjs_encode_string: (ref: ref, bytes_ptr_result: pointer) => { - const bytes = this.textEncoder.encode(this.memory.getObject(ref)); - const bytes_ptr = this.memory.retain(bytes); - this.memory.writeUint32(bytes_ptr_result, bytes_ptr); - return bytes.length; - }, - - swjs_decode_string: (bytes_ptr: pointer, length: number) => { - const bytes = this.memory.bytes().subarray( - bytes_ptr, - bytes_ptr + length - ); - const string = this.textDecoder.decode(bytes); - return this.memory.retain(string); - }, - - swjs_load_string: (ref: ref, buffer: pointer) => { - const bytes = this.memory.getObject(ref); - this.memory.writeBytes(buffer, bytes); - }, - - swjs_call_function: ( - ref: ref, - argv: pointer, - argc: number, - kind_ptr: pointer, - payload1_ptr: pointer, - payload2_ptr: pointer - ) => { - const func = this.memory.getObject(ref); - let result: any; - try { - result = Reflect.apply( - func, - undefined, - JSValue.decodeArray(argv, argc, this.memory) + get wasmImports(): WebAssembly.ModuleImports { + let broker: MessageBroker | null = null; + const getMessageBroker = (threadChannel: SwiftRuntimeThreadChannel) => { + if (broker) return broker; + const itcInterface = new ITCInterface(this.memory); + const newBroker = new MessageBroker(this.tid ?? -1, threadChannel, { + onRequest: (message) => { + let returnValue: ResponseMessage["data"]["response"]; + 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: 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: ref, + name: ref, + kind: JSValue.Kind, + payload1: number, + payload2: number + ) => { + const memory = this.memory; + const obj = memory.getObject(ref); + const key = memory.getObject(name); + const value = JSValue.decode(kind, payload1, payload2, memory); + obj[key] = value; + }, + swjs_get_prop: ( + ref: ref, + name: ref, + payload1_ptr: pointer, + payload2_ptr: pointer + ) => { + const memory = this.memory; + const obj = memory.getObject(ref); + const key = memory.getObject(name); + const result = obj[key]; + return JSValue.writeAndReturnKindBits( + result, + payload1_ptr, + payload2_ptr, + false, + memory ); - } catch (error) { - JSValue.write( - error, - kind_ptr, + }, + + swjs_set_subscript: ( + ref: ref, + index: number, + kind: JSValue.Kind, + payload1: number, + payload2: number + ) => { + const memory = this.memory; + const obj = memory.getObject(ref); + const value = JSValue.decode(kind, payload1, payload2, memory); + obj[index] = value; + }, + swjs_get_subscript: ( + ref: ref, + index: number, + payload1_ptr: pointer, + payload2_ptr: pointer + ) => { + const obj = this.memory.getObject(ref); + const result = obj[index]; + return JSValue.writeAndReturnKindBits( + result, payload1_ptr, payload2_ptr, - true, + false, this.memory ); - return; - } - JSValue.write( - result, - kind_ptr, - payload1_ptr, - payload2_ptr, - false, - this.memory - ); - }, - - swjs_call_function_with_this: ( - obj_ref: ref, - func_ref: ref, - argv: pointer, - argc: number, - kind_ptr: pointer, - payload1_ptr: pointer, - payload2_ptr: pointer - ) => { - const obj = this.memory.getObject(obj_ref); - const func = this.memory.getObject(func_ref); - let result: any; - try { - result = Reflect.apply( - func, - obj, - JSValue.decodeArray(argv, argc, this.memory) + }, + + swjs_encode_string: (ref: ref, bytes_ptr_result: pointer) => { + 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: pointer, length: number) => { + 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: pointer, length: number) => { + 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: ref, buffer: pointer) => { + const memory = this.memory; + const bytes = memory.getObject(ref); + memory.writeBytes(buffer, bytes); + }, + + swjs_call_function: ( + ref: ref, + argv: pointer, + argc: number, + payload1_ptr: pointer, + payload2_ptr: pointer + ) => { + const memory = this.memory; + const func = memory.getObject(ref); + let result = undefined; + try { + const args = JSValue.decodeArray(argv, argc, memory); + result = func(...args); + } catch (error) { + return JSValue.writeAndReturnKindBits( + error, + payload1_ptr, + payload2_ptr, + true, + this.memory + ); + } + return JSValue.writeAndReturnKindBits( + result, + payload1_ptr, + payload2_ptr, + false, + this.memory ); - } catch (error) { - JSValue.write( - error, - kind_ptr, + }, + swjs_call_function_no_catch: ( + ref: ref, + argv: pointer, + argc: number, + payload1_ptr: pointer, + payload2_ptr: pointer + ) => { + const memory = this.memory; + const func = memory.getObject(ref); + const args = JSValue.decodeArray(argv, argc, memory); + const result = func(...args); + return JSValue.writeAndReturnKindBits( + result, payload1_ptr, payload2_ptr, - true, + false, this.memory ); - return; - } - JSValue.write( - result, - kind_ptr, - payload1_ptr, - payload2_ptr, - false, - this.memory - ); - }, - swjs_call_new: (ref: ref, argv: pointer, argc: number) => { - const constructor = this.memory.getObject(ref); - const instance = Reflect.construct( - constructor, - JSValue.decodeArray(argv, argc, this.memory) - ); - return this.memory.retain(instance); - }, - - swjs_call_throwing_new: ( - ref: ref, - argv: pointer, - argc: number, - exception_kind_ptr: pointer, - exception_payload1_ptr: pointer, - exception_payload2_ptr: pointer - ) => { - const constructor = this.memory.getObject(ref); - let result: any; - try { - result = Reflect.construct( - constructor, - JSValue.decodeArray(argv, argc, this.memory) + }, + + swjs_call_function_with_this: ( + obj_ref: ref, + func_ref: ref, + argv: pointer, + argc: number, + payload1_ptr: pointer, + payload2_ptr: pointer + ) => { + const memory = this.memory; + const obj = memory.getObject(obj_ref); + const func = memory.getObject(func_ref); + let result: any; + try { + const args = JSValue.decodeArray(argv, argc, memory); + result = func.apply(obj, args); + } catch (error) { + return JSValue.writeAndReturnKindBits( + error, + payload1_ptr, + payload2_ptr, + true, + this.memory + ); + } + return JSValue.writeAndReturnKindBits( + result, + payload1_ptr, + payload2_ptr, + false, + this.memory + ); + }, + swjs_call_function_with_this_no_catch: ( + obj_ref: ref, + func_ref: ref, + argv: pointer, + argc: number, + payload1_ptr: pointer, + payload2_ptr: pointer + ) => { + const memory = this.memory; + const obj = memory.getObject(obj_ref); + const func = memory.getObject(func_ref); + let result = undefined; + const args = JSValue.decodeArray(argv, argc, memory); + result = func.apply(obj, args); + return JSValue.writeAndReturnKindBits( + result, + payload1_ptr, + payload2_ptr, + false, + this.memory ); - } catch (error) { + }, + + swjs_call_new: (ref: ref, argv: pointer, argc: number) => { + const memory = this.memory; + const constructor = memory.getObject(ref); + const args = JSValue.decodeArray(argv, argc, memory); + const instance = new constructor(...args); + return this.memory.retain(instance); + }, + swjs_call_throwing_new: ( + ref: ref, + argv: pointer, + argc: number, + exception_kind_ptr: pointer, + exception_payload1_ptr: pointer, + exception_payload2_ptr: pointer + ) => { + let memory = this.memory; + const constructor = memory.getObject(ref); + let result: any; + try { + const args = JSValue.decodeArray(argv, argc, memory); + result = new constructor(...args); + } catch (error) { + JSValue.write( + error, + exception_kind_ptr, + exception_payload1_ptr, + exception_payload2_ptr, + true, + this.memory + ); + return -1; + } + memory = this.memory; JSValue.write( - error, + null, exception_kind_ptr, exception_payload1_ptr, exception_payload2_ptr, - true, - this.memory + false, + memory ); - return -1; - } - JSValue.write( - null, - exception_kind_ptr, - exception_payload1_ptr, - exception_payload2_ptr, - false, - this.memory + return memory.retain(result); + }, + + swjs_instanceof: (obj_ref: ref, constructor_ref: 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: 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, + file: ref + ) => { + const fileString = this.memory.getObject(file) as string; + const func = (...args: any[]) => + this.callHostFunction(host_func_id, line, fileString, args); + const func_ref = this.memory.retain(func); + this.closureDeallocator?.track(func, func_ref); + return func_ref; + }, + + swjs_create_typed_array: ( + constructor_ref: ref, + elementsPtr: pointer, + length: number + ) => { + const ArrayType: TypedArray = + 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: ref, buffer: pointer) => { + const memory = this.memory; + const typedArray = memory.getObject(ref); + const bytes = new Uint8Array(typedArray.buffer); + memory.writeBytes(buffer, bytes); + }, + + swjs_release: (ref: ref) => { + this.memory.release(ref); + }, + + swjs_release_remote: (tid: number, ref: ref) => { + 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: this.tid ?? MAIN_THREAD_TID, + targetTid: tid, + context: 0, + request: { + method: "release", + parameters: [ref], + } + } + }) + }, + + swjs_i64_to_bigint: (value: bigint, signed: number) => { + return this.memory.retain( + signed ? value : BigInt.asUintN(64, value) + ); + }, + swjs_bigint_to_i64: (ref: ref, signed: number) => { + 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: number, upper: number, signed: number) => { + 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: number) => { + 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: never = message; + throw new Error(`Unknown message type: ${unknownMessage}`); + } + }); + }, + swjs_wake_up_worker_thread: (tid: number) => { + this.postMessageToWorkerThread(tid, { type: "wake" }); + }, + swjs_listen_message_from_worker_thread: (tid: number) => { + 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: never = message; + throw new Error(`Unknown message type: ${unknownMessage}`); + } + }, + ); + }, + swjs_terminate_worker_thread: (tid: number) => { + const threadChannel = this.options.threadChannel; + if (threadChannel && "terminateWorkerThread" in threadChannel) { + threadChannel.terminateWorkerThread?.(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: ref, + transferring_objects: pointer, + transferring_objects_count: number, + object_source_tid: number, + sending_context: pointer, + ) => { + 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: this.tid ?? 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: pointer, + sending_objects_count: number, + transferring_objects: pointer, + transferring_objects_count: number, + object_source_tid: number, + sending_context: pointer, + ) => { + 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: this.tid ?? MAIN_THREAD_TID, + targetTid: object_source_tid, + context: sending_context, + request: { + method: "sendObjects", + parameters: [sendingObjects, transferringObjects, sending_context], + } + } + }) + }, + }; + } + + private postMessageToMainThread(message: WorkerToMainMessage, transfer: any[] = []) { + 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." ); - return this.memory.retain(result); - }, - - swjs_instanceof: (obj_ref: ref, constructor_ref: ref) => { - const obj = this.memory.getObject(obj_ref); - const constructor = this.memory.getObject(constructor_ref); - return obj instanceof constructor; - }, - - swjs_create_function: (host_func_id: number) => { - const func = (...args: any[]) => - this.callHostFunction(host_func_id, args); - const func_ref = this.memory.retain(func); - this.closureDeallocator?.track(func, func_ref); - return func_ref; - }, - - swjs_create_typed_array: ( - constructor_ref: ref, - elementsPtr: pointer, - length: number - ) => { - const ArrayType: TypedArray = - this.memory.getObject(constructor_ref); - const array = new ArrayType( - this.memory.rawMemory.buffer, - elementsPtr, - length + } + threadChannel.postMessageToMainThread(message, transfer); + } + + private postMessageToWorkerThread(tid: number, message: MainToWorkerMessage, transfer: any[] = []) { + 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." ); - // Call `.slice()` to copy the memory - return this.memory.retain(array.slice()); - }, - - swjs_load_typed_array: (ref: ref, buffer: pointer) => { - const typedArray = this.memory.getObject(ref); - const bytes = new Uint8Array(typedArray.buffer); - this.memory.writeBytes(buffer, bytes); - }, - - swjs_release: (ref: ref) => { - this.memory.release(ref); - }, - }; + } + 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 {} diff --git a/Runtime/src/itc.ts b/Runtime/src/itc.ts new file mode 100644 index 000000000..e2c93622a --- /dev/null +++ b/Runtime/src/itc.ts @@ -0,0 +1,247 @@ +// This file defines the interface for the inter-thread communication. +import type { ref, pointer } from "./types.js"; +import { Memory } from "./memory.js"; + +/** + * 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); + * }; + * } + * } + * }); + * ``` + */ +export 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; + }; + + +export class ITCInterface { + constructor(private memory: Memory) {} + + send(sendingObject: ref, transferringObjects: ref[], sendingContext: pointer): { object: any, sendingContext: pointer, transfer: Transferable[] } { + const object = this.memory.getObject(sendingObject); + const transfer = transferringObjects.map(ref => this.memory.getObject(ref)); + return { object, sendingContext, transfer }; + } + + sendObjects(sendingObjects: ref[], transferringObjects: ref[], sendingContext: pointer): { object: any[], sendingContext: pointer, transfer: Transferable[] } { + 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: ref): { object: undefined, transfer: Transferable[] } { + this.memory.release(objectRef); + return { object: undefined, transfer: [] }; + } +} + +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]; + +export 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 } + +export 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; + }; + } +} + +export type MainToWorkerMessage = { + type: "wake"; +} | RequestMessage | ResponseMessage; + +export type WorkerToMainMessage = { + type: "job"; + data: number; +} | RequestMessage | ResponseMessage; + + +export class MessageBroker { + constructor( + private selfTid: number, + private threadChannel: SwiftRuntimeThreadChannel, + private handlers: { + onRequest: (message: RequestMessage) => void, + onResponse: (message: ResponseMessage) => void, + } + ) { + } + + request(message: RequestMessage) { + 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: ResponseMessage) { + 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: RequestMessage) { + 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: ResponseMessage) { + 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"); + } + } +} + +export function serializeError(error: unknown): SerializedError { + if (error instanceof Error) { + return { isError: true, value: { message: error.message, name: error.name, stack: error.stack } }; + } + return { isError: false, value: error }; +} + +export function deserializeError(error: SerializedError): unknown { + if (error.isError) { + return Object.assign(new Error(error.value.message), error.value); + } + return error.value; +} diff --git a/Runtime/src/js-value.ts b/Runtime/src/js-value.ts index 61f7b486a..dcc378f61 100644 --- a/Runtime/src/js-value.ts +++ b/Runtime/src/js-value.ts @@ -1,8 +1,7 @@ -import { Memory } from "./memory"; -import { pointer } from "./types"; +import { Memory } from "./memory.js"; +import { assertNever, JavaScriptValueKindAndFlags, pointer, ref } from "./types.js"; -export enum Kind { - Invalid = -1, +export const enum Kind { Boolean = 0, String = 1, Number = 2, @@ -10,6 +9,8 @@ export enum Kind { Null = 4, Undefined = 5, Function = 6, + Symbol = 7, + BigInt = 8, } export const decode = ( @@ -32,6 +33,8 @@ export const decode = ( case Kind.String: case Kind.Object: case Kind.Function: + case Kind.Symbol: + case Kind.BigInt: return memory.getObject(payload1); case Kind.Null: @@ -41,24 +44,36 @@ export const decode = ( return undefined; default: - throw new Error(`JSValue Type kind "${kind}" is not supported`); + assertNever(kind, `JSValue Type kind "${kind}" is not supported`); } }; // Note: // `decodeValues` assumes that the size of RawJSValue is 16. export const decodeArray = (ptr: pointer, length: number, memory: 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 = memory.readUint32(base); - const payload1 = memory.readUint32(base + 4); - const payload2 = memory.readFloat64(base + 8); + 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. export const write = ( value: any, kind_ptr: pointer, @@ -67,42 +82,71 @@ export const write = ( is_exception: boolean, memory: Memory ) => { + const kind = writeAndReturnKindBits( + value, + payload1_ptr, + payload2_ptr, + is_exception, + memory + ); + memory.writeUint32(kind_ptr, kind); +}; + +export const writeAndReturnKindBits = ( + value: any, + payload1_ptr: pointer, + payload2_ptr: pointer, + is_exception: boolean, + memory: Memory +): JavaScriptValueKindAndFlags => { const exceptionBit = (is_exception ? 1 : 0) << 31; if (value === null) { - memory.writeUint32(kind_ptr, exceptionBit | Kind.Null); - return; + return exceptionBit | Kind.Null; } - switch (typeof value) { + + const writeRef = (kind: Kind) => { + memory.writeUint32(payload1_ptr, memory.retain(value)); + return exceptionBit | kind; + }; + + const type = typeof value; + switch (type) { case "boolean": { - memory.writeUint32(kind_ptr, exceptionBit | Kind.Boolean); memory.writeUint32(payload1_ptr, value ? 1 : 0); - break; + return exceptionBit | Kind.Boolean; } case "number": { - memory.writeUint32(kind_ptr, exceptionBit | Kind.Number); memory.writeFloat64(payload2_ptr, value); - break; + return exceptionBit | Kind.Number; } case "string": { - memory.writeUint32(kind_ptr, exceptionBit | Kind.String); - memory.writeUint32(payload1_ptr, memory.retain(value)); - break; + return writeRef(Kind.String); } case "undefined": { - memory.writeUint32(kind_ptr, exceptionBit | Kind.Undefined); - break; + return exceptionBit | Kind.Undefined; } case "object": { - memory.writeUint32(kind_ptr, exceptionBit | Kind.Object); - memory.writeUint32(payload1_ptr, memory.retain(value)); - break; + return writeRef(Kind.Object); } case "function": { - memory.writeUint32(kind_ptr, exceptionBit | Kind.Function); - memory.writeUint32(payload1_ptr, memory.retain(value)); - break; + return writeRef(Kind.Function); + } + case "symbol": { + return writeRef(Kind.Symbol); + } + case "bigint": { + return writeRef(Kind.BigInt); } default: - throw new Error(`Type "${typeof value}" is not supported yet`); + assertNever(type, `Type "${type}" is not supported yet`); } + throw new Error("Unreachable"); }; + +export function decodeObjectRefs(ptr: pointer, length: number, memory: Memory): ref[] { + const result: ref[] = new Array(length); + for (let i = 0; i < length; i++) { + result[i] = memory.readUint32(ptr + 4 * i); + } + return result; +} diff --git a/Runtime/src/memory.ts b/Runtime/src/memory.ts index 3c010a5f5..d8334516d 100644 --- a/Runtime/src/memory.ts +++ b/Runtime/src/memory.ts @@ -1,5 +1,5 @@ -import { SwiftRuntimeHeap } from "./object-heap"; -import { pointer } from "./types"; +import { SwiftRuntimeHeap } from "./object-heap.js"; +import { pointer } from "./types.js"; export class Memory { readonly rawMemory: WebAssembly.Memory; @@ -17,13 +17,20 @@ export class Memory { bytes = () => new Uint8Array(this.rawMemory.buffer); dataView = () => new DataView(this.rawMemory.buffer); - writeBytes = (ptr: pointer, bytes: Uint8Array) => this.bytes().set(bytes, ptr); + writeBytes = (ptr: pointer, bytes: Uint8Array) => + this.bytes().set(bytes, ptr); readUint32 = (ptr: pointer) => this.dataView().getUint32(ptr, true); + readUint64 = (ptr: pointer) => this.dataView().getBigUint64(ptr, true); + readInt64 = (ptr: pointer) => this.dataView().getBigInt64(ptr, true); readFloat64 = (ptr: pointer) => this.dataView().getFloat64(ptr, true); writeUint32 = (ptr: pointer, value: number) => this.dataView().setUint32(ptr, value, true); + writeUint64 = (ptr: pointer, value: bigint) => + this.dataView().setBigUint64(ptr, value, true); + writeInt64 = (ptr: pointer, value: bigint) => + this.dataView().setBigInt64(ptr, value, true); writeFloat64 = (ptr: pointer, value: number) => this.dataView().setFloat64(ptr, value, true); } diff --git a/Runtime/src/object-heap.ts b/Runtime/src/object-heap.ts index 61f1925fe..d59f5101e 100644 --- a/Runtime/src/object-heap.ts +++ b/Runtime/src/object-heap.ts @@ -1,5 +1,5 @@ -import { globalVariable } from "./find-global"; -import { ref } from "./types"; +import { globalVariable } from "./find-global.js"; +import { ref } from "./types.js"; type SwiftRuntimeHeapEntry = { id: number; @@ -22,33 +22,25 @@ export class SwiftRuntimeHeap { } retain(value: any) { - const isObject = typeof value == "object"; const entry = this._heapEntryByValue.get(value); - if (isObject && entry) { + if (entry) { entry.rc++; return entry.id; } const id = this._heapNextKey++; this._heapValueById.set(id, value); - if (isObject) { - this._heapEntryByValue.set(value, { id: id, rc: 1 }); - } + this._heapEntryByValue.set(value, { id: id, rc: 1 }); return id; } release(ref: ref) { const value = this._heapValueById.get(ref); - const isObject = typeof value == "object"; - if (isObject) { - const entry = this._heapEntryByValue.get(value)!; - entry.rc--; - if (entry.rc != 0) return; - - this._heapEntryByValue.delete(value); - this._heapValueById.delete(ref); - } else { - this._heapValueById.delete(ref); - } + const entry = this._heapEntryByValue.get(value)!; + entry.rc--; + if (entry.rc != 0) return; + + this._heapEntryByValue.delete(value); + this._heapValueById.delete(ref); } referenceHeap(ref: ref) { diff --git a/Runtime/src/types.ts b/Runtime/src/types.ts index 4f613cc6a..a8872f80d 100644 --- a/Runtime/src/types.ts +++ b/Runtime/src/types.ts @@ -1,7 +1,8 @@ -import * as JSValue from "./js-value"; - export type ref = number; export type pointer = number; +export type bool = number; +export type JavaScriptValueKind = number; +export type JavaScriptValueKindAndFlags = number; export interface ExportedFunctions { swjs_library_version(): number; @@ -13,81 +14,17 @@ export interface ExportedFunctions { argv: pointer, argc: number, callback_func_ref: ref - ): void; + ): bool; swjs_free_host_function(host_func_id: 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, - kind_ptr: pointer, - payload1_ptr: pointer, - payload2_ptr: pointer - ): void; - swjs_set_subscript( - ref: number, - index: number, - kind: JSValue.Kind, - payload1: number, - payload2: number - ): void; - swjs_get_subscript( - ref: number, - index: number, - kind_ptr: pointer, - payload1_ptr: pointer, - payload2_ptr: pointer - ): void; - 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, - kind_ptr: pointer, - payload1_ptr: pointer, - payload2_ptr: pointer - ): void; - swjs_call_function_with_this( - obj_ref: ref, - func_ref: ref, - argv: pointer, - argc: number, - kind_ptr: pointer, - payload1_ptr: pointer, - payload2_ptr: pointer - ): void; - 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): 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_enqueue_main_job_from_worker(unowned_job: number): void; + swjs_wake_worker_thread(): void; + swjs_receive_response(object: ref, transferring: pointer): void; + swjs_receive_error(error: ref, context: number): void; } -export enum LibraryFeatures { +export const enum LibraryFeatures { WeakRefs = 1 << 0, } @@ -98,8 +35,13 @@ export type TypedArray = | Uint16ArrayConstructor | Int32ArrayConstructor | Uint32ArrayConstructor - // BigInt is not yet supported, see https://github.com/swiftwasm/JavaScriptKit/issues/56 - // | BigInt64ArrayConstructor - // | BigUint64ArrayConstructor + | BigInt64ArrayConstructor + | BigUint64ArrayConstructor | Float32ArrayConstructor | Float64ArrayConstructor; + +export function assertNever(x: never, message: string) { + throw new Error(message); +} + +export const MAIN_THREAD_TID = -1; diff --git a/Runtime/tsconfig.json b/Runtime/tsconfig.json index 8fa8e650d..bd4f8a1a1 100644 --- a/Runtime/tsconfig.json +++ b/Runtime/tsconfig.json @@ -1,14 +1,13 @@ { "compilerOptions": { - "declaration": true, - "declarationDir": "lib", + "declaration": false, "importHelpers": true, "module": "esnext", "noEmit": true, "rootDir": "src", "strict": true, "target": "es2017", - "lib": ["es2017", "DOM", "ESNext.WeakRef"], + "lib": ["es2020", "DOM", "ESNext.WeakRef"], "skipLibCheck": true }, "include": ["src/**/*"], diff --git a/Sources/JavaScriptBigIntSupport/Int64+I64.swift b/Sources/JavaScriptBigIntSupport/Int64+I64.swift new file mode 100644 index 000000000..e361e72e9 --- /dev/null +++ b/Sources/JavaScriptBigIntSupport/Int64+I64.swift @@ -0,0 +1,13 @@ +import JavaScriptKit + +extension UInt64: JavaScriptKit.ConvertibleToJSValue, JavaScriptKit.TypedArrayElement { + public static var typedArrayClass: JSFunction { JSObject.global.BigUint64Array.function! } + + public var jsValue: JSValue { .bigInt(JSBigInt(unsigned: self)) } +} + +extension Int64: JavaScriptKit.ConvertibleToJSValue, JavaScriptKit.TypedArrayElement { + public static var typedArrayClass: JSFunction { JSObject.global.BigInt64Array.function! } + + public var jsValue: JSValue { .bigInt(JSBigInt(self)) } +} diff --git a/Sources/JavaScriptBigIntSupport/JSBigInt+I64.swift b/Sources/JavaScriptBigIntSupport/JSBigInt+I64.swift new file mode 100644 index 000000000..0fe1561d0 --- /dev/null +++ b/Sources/JavaScriptBigIntSupport/JSBigInt+I64.swift @@ -0,0 +1,20 @@ +@_spi(JSObject_id) import JavaScriptKit +import _CJavaScriptBigIntSupport + +extension JSBigInt: JavaScriptKit.JSBigIntExtended { + public var int64Value: Int64 { + swjs_bigint_to_i64(id, true) + } + + public var uInt64Value: UInt64 { + UInt64(bitPattern: swjs_bigint_to_i64(id, false)) + } + + public convenience init(_ value: Int64) { + self.init(id: swjs_i64_to_bigint(value, true)) + } + + public convenience init(unsigned value: UInt64) { + self.init(id: swjs_i64_to_bigint(Int64(bitPattern: value), false)) + } +} diff --git a/Sources/JavaScriptEventLoop/JSSending.swift b/Sources/JavaScriptEventLoop/JSSending.swift new file mode 100644 index 000000000..3408b232f --- /dev/null +++ b/Sources/JavaScriptEventLoop/JSSending.swift @@ -0,0 +1,403 @@ +import _Concurrency +@_spi(JSObject_id) import JavaScriptKit +import _CJavaScriptKit + +#if canImport(Synchronization) +import Synchronization +#endif + +/// A temporary object intended to send a JavaScript object from one thread to another. +/// +/// `JSSending` provides a way to safely transfer or clone JavaScript objects between threads +/// in a multi-threaded WebAssembly environment. +/// +/// There are two primary ways to use `JSSending`: +/// 1. Transfer an object (`JSSending.transfer`) - The original object becomes unusable +/// 2. Clone an object (`JSSending.init`) - Creates a copy, original remains usable +/// +/// To receive a sent object on the destination thread, call the `receive()` method. +/// +/// - Note: `JSSending` is `Sendable` and can be safely shared across thread boundaries. +/// +/// ## Example +/// +/// ```swift +/// // Transfer an object to another thread +/// let buffer = JSObject.global.Uint8Array.function!.new(100).buffer.object! +/// let transferring = JSSending.transfer(buffer) +/// +/// // Receive the object on a worker thread +/// let executor = try await WebWorkerTaskExecutor(numberOfThreads: 1) +/// Task(executorPreference: executor) { +/// let receivedBuffer = try await transferring.receive() +/// // Use the received buffer +/// } +/// +/// // Clone an object for use in another thread +/// let object = JSObject.global.Object.function!.new() +/// object["test"] = "Hello, World!" +/// let cloning = JSSending(object) +/// +/// Task(executorPreference: executor) { +/// let receivedObject = try await cloning.receive() +/// // Use the received object +/// } +/// ``` +@available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) +public struct JSSending: @unchecked Sendable { + // HACK: We need to make this Storage "class" instead of "struct" to avoid using + // outlined value operations in parameter-packed contexts, which leads to a + // compiler crash. https://github.com/swiftlang/swift/pull/79201 + fileprivate class Storage { + /// The original object that is sent. + /// + /// Retain it here to prevent it from being released before the sending is complete. + let sourceObject: JSObject + /// A function that constructs an object from a JavaScript object reference. + let construct: (_ object: JSObject) -> T + /// The JavaScript object reference of the original object. + let idInSource: JavaScriptObjectRef + /// The TID of the thread that owns the original object. + let sourceTid: Int32 + /// Whether the object should be "transferred" or "cloned". + let transferring: Bool + + init( + sourceObject: JSObject, + construct: @escaping (_ object: JSObject) -> T, + idInSource: JavaScriptObjectRef, + sourceTid: Int32, + transferring: Bool + ) { + self.sourceObject = sourceObject + self.construct = construct + self.idInSource = idInSource + self.sourceTid = sourceTid + self.transferring = transferring + } + } + + private let storage: Storage + + fileprivate init( + sourceObject: T, + construct: @escaping (_ object: JSObject) -> T, + deconstruct: @escaping (_ object: T) -> JSObject, + getSourceTid: @escaping (_ object: T) -> Int32, + transferring: Bool + ) { + let object = deconstruct(sourceObject) + self.storage = Storage( + sourceObject: object, + construct: construct, + idInSource: object.id, + sourceTid: getSourceTid(sourceObject), + transferring: transferring + ) + } +} + +@available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) +extension JSSending where T == JSObject { + private init(_ object: JSObject, transferring: Bool) { + self.init( + sourceObject: object, + construct: { $0 }, + deconstruct: { $0 }, + getSourceTid: { + #if compiler(>=6.1) && _runtime(_multithreaded) + return $0.ownerTid + #else + _ = $0 + // On single-threaded runtime, source and destination threads are always the main thread (TID = -1). + return -1 + #endif + }, + transferring: transferring + ) + } + + /// Transfers a `JSObject` to another thread. + /// + /// The original `JSObject` is ["transferred"](https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API/Transferable_objects) + /// to the receiving thread, which means its ownership is completely moved. After transferring, + /// the original object becomes neutered (unusable) in the source thread. + /// + /// This is more efficient than cloning for large objects like `ArrayBuffer` because no copying + /// is involved, but the original object can no longer be accessed. + /// + /// Only objects that implement the JavaScript [Transferable](https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API/Transferable_objects) + /// interface can be transferred. Common transferable objects include: + /// - `ArrayBuffer` + /// - `MessagePort` + /// - `ImageBitmap` + /// - `OffscreenCanvas` + /// + /// ## Example + /// + /// ```swift + /// let buffer = JSObject.global.Uint8Array.function!.new(100).buffer.object! + /// let transferring = JSSending.transfer(buffer) + /// + /// // After transfer, the original buffer is neutered + /// // buffer.byteLength.number! will be 0 + /// ``` + /// + /// - Precondition: The thread calling this method should have the ownership of the `JSObject`. + /// - Postcondition: The original `JSObject` is no longer owned by the thread, further access to it + /// on the thread that called this method is invalid and will result in undefined behavior. + /// + /// - Parameter object: The `JSObject` to be transferred. + /// - Returns: A `JSSending` instance that can be shared across threads. + @available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) + public static func transfer(_ object: JSObject) -> JSSending { + JSSending(object, transferring: true) + } + + /// Clones a `JSObject` to another thread. + /// + /// Creates a copy of the object that can be sent to another thread. The original object + /// remains usable in the source thread. This is safer than transferring when you need + /// to continue using the original object, but has higher memory overhead since it creates + /// a complete copy. + /// + /// Most JavaScript objects can be cloned, but some complex objects including closures may + /// not be clonable. + /// + /// ## Example + /// + /// ```swift + /// let object = JSObject.global.Object.function!.new() + /// object["test"] = "Hello, World!" + /// let cloning = JSSending(object) + /// + /// // Original object is still valid and usable + /// // object["test"].string! is still "Hello, World!" + /// ``` + /// + /// - Precondition: The thread calling this method should have the ownership of the `JSObject`. + /// - Parameter object: The `JSObject` to be cloned. + /// - Returns: A `JSSending` instance that can be shared across threads. + @available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) + public init(_ object: JSObject) { + self.init(object, transferring: false) + } +} + +@available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) +extension JSSending { + + /// Receives a sent `JSObject` from a thread. + /// + /// This method completes the transfer or clone operation, making the object available + /// in the receiving thread. It must be called on the destination thread where you want + /// to use the object. + /// + /// - Important: This method should be called only once for each `JSSending` instance. + /// Attempting to receive the same object multiple times will result in an error. + /// + /// ## Example - Transferring + /// + /// ```swift + /// let canvas = JSObject.global.document.createElement("canvas").object! + /// let transferring = JSSending.transfer(canvas.transferControlToOffscreen().object!) + /// + /// let executor = try await WebWorkerTaskExecutor(numberOfThreads: 1) + /// Task(executorPreference: executor) { + /// let canvas = try await transferring.receive() + /// // Use the canvas in the worker thread + /// } + /// ``` + /// + /// ## Example - Cloning + /// + /// ```swift + /// let data = JSObject.global.Object.function!.new() + /// data["value"] = 42 + /// let cloning = JSSending(data) + /// + /// let executor = try await WebWorkerTaskExecutor(numberOfThreads: 1) + /// Task(executorPreference: executor) { + /// let data = try await cloning.receive() + /// print(data["value"].number!) // 42 + /// } + /// ``` + /// + /// - Parameter isolation: The actor isolation context for this call, used in Swift concurrency. + /// - 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 { + #if compiler(>=6.1) && _runtime(_multithreaded) + let idInDestination = try await withCheckedThrowingContinuation { continuation in + let context = _JSSendingContext(continuation: continuation) + let idInSource = self.storage.idInSource + let transferring = self.storage.transferring ? [idInSource] : [] + swjs_request_sending_object( + idInSource, + transferring, + Int32(transferring.count), + self.storage.sourceTid, + Unmanaged.passRetained(context).toOpaque() + ) + } + return storage.construct(JSObject(id: idInDestination)) + #else + return storage.construct(storage.sourceObject) + #endif + } + + // 6.0 and below can't compile the following without a compiler crash. + #if compiler(>=6.1) + /// Receives multiple `JSSending` instances from a thread in a single operation. + /// + /// This method is more efficient than receiving multiple objects individually, as it + /// batches the receive operations. It's especially useful when transferring or cloning + /// multiple related objects that need to be received together. + /// + /// - Important: All objects being received must come from the same source thread. + /// + /// ## Example + /// + /// ```swift + /// // Create and transfer multiple objects + /// let buffer1 = Uint8Array.new(10).buffer.object! + /// let buffer2 = Uint8Array.new(20).buffer.object! + /// let transferring1 = JSSending.transfer(buffer1) + /// let transferring2 = JSSending.transfer(buffer2) + /// + /// // Receive both objects in a single operation + /// let executor = try await WebWorkerTaskExecutor(numberOfThreads: 1) + /// Task(executorPreference: executor) { + /// let (receivedBuffer1, receivedBuffer2) = try await JSSending.receive(transferring1, transferring2) + /// // Use both buffers in the worker thread + /// } + /// ``` + /// + /// - Parameters: + /// - sendings: The `JSSending` instances to receive. + /// - isolation: The actor isolation context for this call, used in Swift concurrency. + /// - Returns: A tuple containing the received objects. + /// - Throws: `JSSendingError` if any sending operation fails, or `JSException` if a JavaScript error occurs. + @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 + ) async throws -> (repeat each U) where T == (repeat each U) { + #if compiler(>=6.1) && _runtime(_multithreaded) + var sendingObjects: [JavaScriptObjectRef] = [] + var transferringObjects: [JavaScriptObjectRef] = [] + var sourceTid: Int32? + for object in repeat each sendings { + sendingObjects.append(object.storage.idInSource) + if object.storage.transferring { + transferringObjects.append(object.storage.idInSource) + } + if sourceTid == nil { + sourceTid = object.storage.sourceTid + } else { + guard sourceTid == object.storage.sourceTid else { + throw JSSendingError("All objects sent at once must be from the same thread") + } + } + } + let objects = try await withCheckedThrowingContinuation { continuation in + let context = _JSSendingContext(continuation: continuation) + sendingObjects.withUnsafeBufferPointer { sendingObjects in + transferringObjects.withUnsafeBufferPointer { transferringObjects in + swjs_request_sending_objects( + sendingObjects.baseAddress!, + Int32(sendingObjects.count), + transferringObjects.baseAddress!, + Int32(transferringObjects.count), + sourceTid!, + Unmanaged.passRetained(context).toOpaque() + ) + } + } + } + guard let objectsArray = JSArray(JSObject(id: objects)) else { + fatalError("Non-array object received!?") + } + var index = 0 + func extract(_ sending: JSSending) -> R { + let result = objectsArray[index] + index += 1 + return sending.storage.construct(result.object!) + } + return (repeat extract(each sendings)) + #else + return try await (repeat (each sendings).receive()) + #endif + } + #endif // compiler(>=6.1) +} + +@available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) +private final class _JSSendingContext: Sendable { + let continuation: CheckedContinuation + + init(continuation: CheckedContinuation) { + self.continuation = continuation + } +} + +/// Error type representing failures during JavaScript object sending operations. +/// +/// This error is thrown when a problem occurs during object transfer or cloning +/// between threads, such as attempting to send objects from different threads +/// in a batch operation or other sending-related failures. +public struct JSSendingError: Error, CustomStringConvertible { + /// A description of the error that occurred. + public let description: String + + init(_ message: String) { + self.description = message + } +} + +/// A function that should be called when an object source thread sends an object to a +/// destination thread. +/// +/// - 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") +#endif +@available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) +func _swjs_receive_response(_ object: JavaScriptObjectRef, _ contextPtr: UnsafeRawPointer?) { + #if compiler(>=6.1) && _runtime(_multithreaded) + guard let contextPtr = contextPtr else { return } + let context = Unmanaged<_JSSendingContext>.fromOpaque(contextPtr).takeRetainedValue() + context.continuation.resume(returning: object) + #endif +} + +/// A function that should be called when an object source thread sends an error to a +/// destination thread. +/// +/// - 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") +#endif +@available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) +func _swjs_receive_error(_ error: JavaScriptObjectRef, _ contextPtr: UnsafeRawPointer?) { + #if compiler(>=6.1) && _runtime(_multithreaded) + guard let contextPtr = contextPtr else { return } + let context = Unmanaged<_JSSendingContext>.fromOpaque(contextPtr).takeRetainedValue() + context.continuation.resume(throwing: JSException(JSObject(id: error).jsValue)) + #endif +} diff --git a/Sources/JavaScriptEventLoop/JavaScriptEventLoop.swift b/Sources/JavaScriptEventLoop/JavaScriptEventLoop.swift index b68889c42..8948723d4 100644 --- a/Sources/JavaScriptEventLoop/JavaScriptEventLoop.swift +++ b/Sources/JavaScriptEventLoop/JavaScriptEventLoop.swift @@ -1,33 +1,84 @@ import JavaScriptKit +import _Concurrency import _CJavaScriptEventLoop +import _CJavaScriptKit // NOTE: `@available` annotations are semantically wrong, but they make it easier to develop applications targeting WebAssembly in Xcode. #if compiler(>=5.5) -@available(macOS 12.0, iOS 15.0, watchOS 8.0, tvOS 15.0, *) +/// 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 { /// A function that queues a given closure as a microtask into JavaScript event loop. /// See also: https://developer.mozilla.org/en-US/docs/Web/API/HTML_DOM_API/Microtask_guide - let queueMicrotask: @Sendable (@escaping () -> Void) -> Void + public var queueMicrotask: (@escaping () -> Void) -> Void /// A function that invokes a given closure after a specified number of milliseconds. - let setTimeout: @Sendable (UInt64, @escaping () -> Void) -> Void + public var setTimeout: (Double, @escaping () -> Void) -> Void /// A mutable state to manage internal job queue /// Note that this should be guarded atomically when supporting multi-threaded environment. var queueState = QueueState() private init( - queueTask: @Sendable @escaping (@escaping () -> Void) -> Void, - setTimeout: @Sendable @escaping (UInt64, @escaping () -> Void) -> Void + queueTask: @escaping (@escaping () -> Void) -> Void, + setTimeout: @escaping (Double, @escaping () -> Void) -> Void ) { self.queueMicrotask = queueTask self.setTimeout = setTimeout } - /// A singleton instance of the Executor - public static let shared: JavaScriptEventLoop = { + /// A per-thread singleton instance of the Executor + public static var shared: JavaScriptEventLoop { + return _shared + } + + #if compiler(>=6.1) && _runtime(_multithreaded) + // In multi-threaded environment, we have an event loop executor per + // thread (per Web Worker). A job enqueued in one thread should be + // executed in the same thread under this global executor. + private static var _shared: JavaScriptEventLoop { + if let tls = swjs_thread_local_event_loop { + let eventLoop = Unmanaged.fromOpaque(tls).takeUnretainedValue() + return eventLoop + } + let eventLoop = create() + swjs_thread_local_event_loop = Unmanaged.passRetained(eventLoop).toOpaque() + return eventLoop + } + #else + private static let _shared: JavaScriptEventLoop = create() + #endif + + private static func create() -> JavaScriptEventLoop { let promise = JSPromise(resolver: { resolver -> Void in resolver(.success(.undefined)) }) @@ -42,74 +93,218 @@ 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 - }() + } - private static var didInstallGlobalExecutor = false + @MainActor private 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 the custom-executor will be introduced officially. - /// See also: https://github.com/rjmccall/swift-evolution/blob/custom-executors/proposals/0000-custom-executors.md#the-default-global-concurrent-executor + /// This installation step will be unnecessary after custom executor are + /// 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() + } + } + + @MainActor private static func installGlobalExecutorIsolated() { guard !didInstallGlobalExecutor else { return } - typealias swift_task_enqueueGlobal_hook_Fn = @convention(thin) (UnownedJob, swift_task_enqueueGlobal_original) -> Void + #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.enqueue(job) + JavaScriptEventLoop.shared.unsafeEnqueue(job) } - swift_task_enqueueGlobal_hook = unsafeBitCast(swift_task_enqueueGlobal_hook_impl, to: UnsafeMutableRawPointer?.self) + 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 + 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) + 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 + 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.enqueue(job) + JavaScriptEventLoop.shared.unsafeEnqueue(job) } - swift_task_enqueueMainExecutor_hook = unsafeBitCast(swift_task_enqueueMainExecutor_hook_impl, to: UnsafeMutableRawPointer?.self) - + 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(milliseconds, { - job._runSynchronously(on: self.asUnownedSerialExecutor()) - }) + setTimeout( + Double(milliseconds), + { + #if compiler(>=5.9) + job.runSynchronously(on: self.asUnownedSerialExecutor()) + #else + job._runSynchronously(on: self.asUnownedSerialExecutor()) + #endif + } + ) } - public func enqueue(_ job: UnownedJob) { + private func unsafeEnqueue(_ job: UnownedJob) { insertJobQueue(job: job) } + #if compiler(>=5.9) + @available(macOS 14.0, iOS 17.0, watchOS 10.0, tvOS 17.0, *) + public func enqueue(_ job: consuming ExecutorJob) { + // NOTE: Converting a `ExecutorJob` to an ``UnownedJob`` and invoking + // ``UnownedJob/runSynchronously(_:)` on it multiple times is undefined behavior. + unsafeEnqueue(UnownedJob(job)) + } + #else + public func enqueue(_ job: UnownedJob) { + unsafeEnqueue(job) + } + #endif + public func asUnownedSerialExecutor() -> UnownedSerialExecutor { return UnownedSerialExecutor(ordinary: self) } } -@available(macOS 12.0, iOS 15.0, watchOS 8.0, tvOS 15.0, *) -public extension JSPromise { +#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, *) +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: Swift.Result.success($0)) + return JSValue.undefined + }, + failure: { + 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. + 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: Swift.Result.success($0)) + return JSValue.undefined + }, + failure: { + 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. + public var result: JSPromise.Result { + get async { + await withUnsafeContinuation { [self] continuation in self.then( success: { - continuation.resume(returning: $0) + continuation.resume(returning: .success($0)) return JSValue.undefined }, failure: { - continuation.resume(throwing: $0) + continuation.resume(returning: .failure($0)) return JSValue.undefined } ) diff --git a/Sources/JavaScriptEventLoop/JobQueue.swift b/Sources/JavaScriptEventLoop/JobQueue.swift index 56090d120..a0f2c4bbb 100644 --- a/Sources/JavaScriptEventLoop/JobQueue.swift +++ b/Sources/JavaScriptEventLoop/JobQueue.swift @@ -2,17 +2,18 @@ // 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) -@available(macOS 12.0, iOS 15.0, watchOS 8.0, tvOS 15.0, *) +@available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) struct QueueState: Sendable { fileprivate var headJob: UnownedJob? = nil fileprivate var isSpinning: Bool = false } -@available(macOS 12.0, iOS 15.0, watchOS 8.0, tvOS 15.0, *) +@available(macOS 14.0, iOS 17.0, watchOS 10.0, tvOS 17.0, *) extension JavaScriptEventLoop { func insertJobQueue(job newJob: UnownedJob) { @@ -43,7 +44,11 @@ extension JavaScriptEventLoop { assert(queueState.isSpinning) while let job = self.claimNextFromQueue() { + #if compiler(>=5.9) + job.runSynchronously(on: self.asUnownedSerialExecutor()) + #else job._runSynchronously(on: self.asUnownedSerialExecutor()) + #endif } queueState.isSpinning = false @@ -58,19 +63,19 @@ extension JavaScriptEventLoop { } } -@available(macOS 12.0, iOS 15.0, watchOS 8.0, tvOS 15.0, *) -fileprivate extension UnownedJob { +@available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) +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 @@ -79,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 new file mode 100644 index 000000000..d42c5adda --- /dev/null +++ b/Sources/JavaScriptEventLoop/WebWorkerDedicatedExecutor.swift @@ -0,0 +1,65 @@ +#if !hasFeature(Embedded) +import JavaScriptKit +import _CJavaScriptEventLoop +import _Concurrency + +#if canImport(Synchronization) +import Synchronization +#endif +#if canImport(wasi_pthread) +import wasi_pthread +import WASILibc +#endif + +/// A serial executor that runs on a dedicated web worker thread. +/// +/// This executor is useful for running actors on a dedicated web worker thread. +/// +/// ## Usage +/// +/// ```swift +/// actor MyActor { +/// let executor: WebWorkerDedicatedExecutor +/// nonisolated var unownedExecutor: UnownedSerialExecutor { +/// self.executor.asUnownedSerialExecutor() +/// } +/// init(executor: WebWorkerDedicatedExecutor) { +/// self.executor = executor +/// } +/// } +/// +/// let executor = try await WebWorkerDedicatedExecutor() +/// let actor = MyActor(executor: executor) +/// ``` +/// +/// - SeeAlso: ``WebWorkerTaskExecutor`` +@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) +public final class WebWorkerDedicatedExecutor: SerialExecutor { + + private let underlying: WebWorkerTaskExecutor + + /// - Parameters: + /// - 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(timeout: Duration = .seconds(3), checkInterval: Duration = .microseconds(5)) async throws { + let underlying = try await WebWorkerTaskExecutor( + numberOfThreads: 1, + timeout: timeout, + checkInterval: checkInterval + ) + self.underlying = underlying + } + + /// Terminates the worker thread. + public func terminate() { + self.underlying.terminate() + } + + // MARK: - SerialExecutor conformance + + public func enqueue(_ job: consuming ExecutorJob) { + self.underlying.enqueue(job) + } +} +#endif diff --git a/Sources/JavaScriptEventLoop/WebWorkerTaskExecutor.swift b/Sources/JavaScriptEventLoop/WebWorkerTaskExecutor.swift new file mode 100644 index 000000000..651e7be2a --- /dev/null +++ b/Sources/JavaScriptEventLoop/WebWorkerTaskExecutor.swift @@ -0,0 +1,669 @@ +#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 +#endif +#if canImport(wasi_pthread) +import wasi_pthread +import WASILibc +#endif + +// MARK: - Web Worker Task Executor + +/// A task executor that runs tasks on Web Worker threads. +/// +/// The `WebWorkerTaskExecutor` provides a way to execute Swift tasks in parallel across multiple +/// Web Worker threads, enabling true multi-threaded execution in WebAssembly environments. +/// This allows CPU-intensive tasks to be offloaded from the main thread, keeping the user +/// interface responsive. +/// +/// ## Multithreading Model +/// +/// 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 +/// explicitly transfer or clone objects: +/// +/// ```swift +/// // Create and transfer an object to a worker thread +/// let buffer = JSObject.global.ArrayBuffer.function!.new(1024).object! +/// let transferring = JSSending.transfer(buffer) +/// +/// let task = Task(executorPreference: executor) { +/// // Receive the transferred buffer in the worker +/// let workerBuffer = try await transferring.receive() +/// // Use the buffer in the worker thread +/// } +/// ``` +/// +/// ## Prerequisites +/// +/// This task executor is designed to work with [wasi-threads](https://github.com/WebAssembly/wasi-threads) +/// but it requires the following single extension: +/// The wasi-threads implementation should listen to the `message` event +/// from spawned Web Workers, and forward the message to the main thread +/// by calling `_swjs_enqueue_main_job_from_worker`. +/// +/// ## Basic Usage +/// +/// ```swift +/// // Create an executor with 4 worker threads +/// let executor = try await WebWorkerTaskExecutor(numberOfThreads: 4) +/// defer { executor.terminate() } +/// +/// // Execute a task on a worker thread +/// let task = Task(executorPreference: executor) { +/// // This runs on a worker thread +/// return performHeavyComputation() +/// } +/// let result = await task.value +/// +/// // Run a block on a worker thread +/// await withTaskExecutorPreference(executor) { +/// // This entire block runs on a worker thread +/// performHeavyComputation() +/// } +/// +/// // Execute multiple tasks in parallel +/// await withTaskGroup(of: Int.self) { group in +/// for i in 0..<10 { +/// group.addTask(executorPreference: executor) { +/// // Each task runs on a worker thread +/// return fibonacci(i) +/// } +/// } +/// +/// for await result in group { +/// // Process results as they complete +/// } +/// } +/// ``` +/// +/// ## Known limitations +/// +/// Currently, the Cooperative Global Executor of Swift runtime has a bug around +/// main executor detection. The issue leads to ignoring the `@MainActor` +/// attribute, which is supposed to run tasks on the main thread, when this web +/// worker executor is preferred. +/// +/// ```swift +/// func run(executor: WebWorkerTaskExecutor) async { +/// await withTaskExecutorPreference(executor) { +/// // This block runs on the Web Worker thread. +/// await MainActor.run { +/// // This block should run on the main thread, but it runs on +/// // the Web Worker thread. +/// } +/// } +/// // 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 +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 + /// The worker instance in Swift world lives as long as the + /// `WebWorkerTaskExecutor` instance that spawned it lives. Thus, the worker + /// instance may outlive the underlying Web Worker thread. + fileprivate final class Worker: Sendable { + + /// The state of the worker. + /// + /// State transition: + /// + /// +---------+ +------------+ + /// +----->| Idle |--[terminate]-->| Terminated | + /// | +---+-----+ +------------+ + /// | | + /// | [enqueue] + /// | | + /// [no more job] | + /// | v + /// | +---------+ + /// +------| Running | + /// +---------+ + /// + enum State: UInt32, AtomicRepresentable { + /// The worker is idle and waiting for a new job. + case idle = 0 + /// The worker is processing a job. + case running = 1 + /// The worker is terminated. + case terminated = 2 + } + let state: Atomic = Atomic(.idle) + /// TODO: Rewrite it to use real queue :-) + let jobQueue: Mutex<[UnownedJob]> = Mutex([]) + /// The TaskExecutor that spawned this worker. + /// This variable must be set only once when the worker is started. + nonisolated(unsafe) weak var parentTaskExecutor: WebWorkerTaskExecutor.Executor? + /// The thread ID of this worker. + let tid: Atomic = Atomic(0) + + /// A trace statistics + struct TraceStats: CustomStringConvertible { + var enqueuedJobs: Int = 0 + var dequeuedJobs: Int = 0 + var processedJobs: Int = 0 + + var description: String { + "TraceStats(E: \(enqueuedJobs), D: \(dequeuedJobs), P: \(processedJobs))" + } + } + #if JAVASCRIPTKIT_STATS + private let traceStats = Mutex(TraceStats()) + private func statsIncrement(_ keyPath: WritableKeyPath) { + traceStats.withLock { stats in + stats[keyPath: keyPath] += 1 + } + } + #else + private func statsIncrement(_ keyPath: WritableKeyPath) {} + #endif + + /// The worker bound to the current thread. + /// Returns `nil` if the current thread is not a worker thread. + static var currentThread: Worker? { + guard let ptr = swjs_thread_local_task_executor_worker else { + return nil + } + return Unmanaged.fromOpaque(ptr).takeUnretainedValue() + } + + init() {} + + /// Enqueue a job to the worker. + func enqueue(_ job: UnownedJob) { + statsIncrement(\.enqueuedJobs) + var locked: Bool + repeat { + let result: Void? = jobQueue.withLockIfAvailable { queue in + queue.append(job) + // Wake up the worker to process a job. + switch state.exchange(.running, ordering: .sequentiallyConsistent) { + case .idle: + if Self.currentThread === self { + // 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 + } + ) + } else { + let tid = self.tid.load(ordering: .sequentiallyConsistent) + swjs_wake_up_worker_thread(tid) + } + case .running: + // The worker is already running, no need to wake up. + break + case .terminated: + // Will not wake up the worker because it's already terminated. + break + } + } + locked = result != nil + } while !locked + } + + func scheduleNextRun() { + _ = JSObject.global.queueMicrotask!( + JSOneshotClosure { _ in + self.run() + return JSValue.undefined + } + ) + } + + /// Run the worker + /// + /// NOTE: This function must be called from the worker thread. + /// It will return when the worker is terminated. + func start(executor: WebWorkerTaskExecutor.Executor) { + // Get the thread ID of the current worker thread from the JS side. + // NOTE: Unfortunately even though `pthread_self` internally holds the thread ID, + // there is no public API to get it because it's a part of implementation details + // of wasi-libc. So we need to get it from the JS side. + let tid = swjs_get_worker_thread_id() + // Set the thread-local variable to the current worker. + // `self` outlives the worker thread because `Executor` retains the worker. + // Thus it's safe to store the reference without extra retain. + swjs_thread_local_task_executor_worker = Unmanaged.passUnretained(self).toOpaque() + // Start listening events from the main thread. + // This must be called after setting the swjs_thread_local_task_executor_worker + // because the event listener enqueues jobs to the TLS worker. + swjs_listen_message_from_main_thread() + // Set the parent executor. + parentTaskExecutor = executor + // Store the thread ID to the worker. This notifies the main thread that the worker is started. + self.tid.store(tid, ordering: .sequentiallyConsistent) + trace("Worker.start tid=\(tid)") + } + + /// 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() { + trace("Worker.run") + guard let executor = parentTaskExecutor else { + preconditionFailure("The worker must be started with a parent executor.") + } + do { + // Assert the state at the beginning of the run. + let state = state.load(ordering: .sequentiallyConsistent) + assert( + state == .running || state == .terminated, + "Invalid state: not running (tid=\(self.tid.load(ordering: .sequentiallyConsistent)), \(state))" + ) + } + while true { + // Pop a job from the queue. + let job = jobQueue.withLock { queue -> UnownedJob? in + if let job = queue.first { + 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 + ) + + switch (exchanged, original) { + case (true, _): + trace("Worker.run exited \(original) -> idle") + return nil // Regular case + case (false, .idle): + 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. + } + } + guard let job else { return } + statsIncrement(\.dequeuedJobs) + job.runSynchronously( + on: executor.asUnownedTaskExecutor() + ) + statsIncrement(\.processedJobs) + // The job is done. Continue to the next job. + } + } + + /// Terminate the worker. + func terminate() { + trace("Worker.terminate tid=\(tid.load(ordering: .sequentiallyConsistent))") + state.store(.terminated, ordering: .sequentiallyConsistent) + let tid = self.tid.load(ordering: .sequentiallyConsistent) + guard tid != 0 else { + // The worker is not started yet. + return + } + swjs_terminate_worker_thread(tid) + } + } + + fileprivate final class Executor: TaskExecutor { + let numberOfThreads: Int + let workers: [Worker] + let roundRobinIndex: Mutex = Mutex(0) + + init(numberOfThreads: Int) { + self.numberOfThreads = numberOfThreads + var workers = [Worker]() + for _ in 0..=6.1) && _runtime(_multithreaded) + class Context: @unchecked Sendable { + let executor: WebWorkerTaskExecutor.Executor + let worker: Worker + init(executor: WebWorkerTaskExecutor.Executor, worker: Worker) { + self.executor = executor + self.worker = worker + } + } + 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 unmanagedContext = Unmanaged.passRetained(context) + contexts.append(unmanagedContext) + let ptr = unmanagedContext.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!).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. + let clock = ContinuousClock() + let workerInitStarted = clock.now + for worker in workers { + var tid: pid_t + repeat { + if workerInitStarted.duration(to: .now) > timeout { + throw SpawnError( + reason: "Worker thread initialization timeout exceeded (\(timeout))" + ) + } + tid = worker.tid.load(ordering: .sequentiallyConsistent) + try await clock.sleep(for: checkInterval) + } while tid == 0 + swjs_listen_message_from_worker_thread(tid) + } + #else + fatalError("Unsupported platform") + #endif + } + + func terminate() { + for worker in workers { + worker.terminate() + } + } + + func enqueue(_ job: UnownedJob) { + precondition(!workers.isEmpty, "No worker threads are available") + + // If the current thread is a worker thread, enqueue the job to the current worker. + if let worker = Worker.currentThread { + worker.enqueue(job) + return + } + // Otherwise (main thread), enqueue the job to the worker with round-robin scheduling. + // TODO: Use a more sophisticated scheduling algorithm with priority. + roundRobinIndex.withLock { index in + let worker = workers[index] + worker.enqueue(job) + index = (index + 1) % numberOfThreads + } + } + } + + private let executor: Executor + + /// Creates a new Web Worker task executor with the specified number of worker threads. + /// + /// This initializer creates a pool of Web Worker threads that can execute Swift tasks + /// in parallel. The initialization is asynchronous because it waits for all worker + /// threads to be properly initialized before returning. + /// + /// The number of threads should typically match the number of available CPU cores + /// for CPU-bound workloads. For I/O-bound workloads, you might benefit from more + /// threads than CPU cores. + /// + /// ## Example + /// + /// ```swift + /// // Create an executor with 4 worker threads + /// let executor = try await WebWorkerTaskExecutor(numberOfThreads: 4) + /// + /// // Always terminate the executor when you're done with it + /// defer { executor.terminate() } + /// + /// // Use the executor... + /// ``` + /// + /// - Parameters: + /// - numberOfThreads: The number of Web Worker threads to spawn. + /// - 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 { + 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 + /// be ignored and may never complete. + /// + /// It's recommended to use a `defer` statement immediately after creating the executor + /// to ensure it's properly terminated when it goes out of scope. + /// + /// ## Example + /// + /// ```swift + /// do { + /// let executor = try await WebWorkerTaskExecutor(numberOfThreads: 4) + /// defer { executor.terminate() } + /// + /// // Use the executor... + /// } + /// // Executor is automatically terminated when exiting the scope + /// ``` + /// + /// - Important: This method must be called after all tasks that prefer this executor are done. + /// Otherwise, the tasks may stuck forever. + public func terminate() { + executor.terminate() + } + + /// Returns the number of worker threads managed by this executor. + /// + /// This property reflects the value provided during initialization and doesn't change + /// during the lifetime of the executor. + /// + /// ## Example + /// + /// ```swift + /// let executor = try await WebWorkerTaskExecutor(numberOfThreads: 4) + /// print("Executor is running with \(executor.numberOfThreads) threads") + /// // Prints: "Executor is running with 4 threads" + /// ``` + public var numberOfThreads: Int { + executor.numberOfThreads + } + + // MARK: TaskExecutor conformance + + /// Enqueues a job to be executed by one of the worker threads. + /// + /// This method is part of the `TaskExecutor` protocol and is called by the Swift + /// Concurrency runtime. You typically don't need to call this method directly. + /// + /// - Parameter job: The job to enqueue. + public func enqueue(_ job: UnownedJob) { + Self.traceStatsIncrement(\.enqueueExecutor) + executor.enqueue(job) + } + + // MARK: Statistics + + /// Executor global statistics + internal struct ExecutorStats: CustomStringConvertible { + var sendJobToMainThread: Int = 0 + var receiveJobFromWorkerThread: Int = 0 + var enqueueGlobal: Int = 0 + var enqueueExecutor: Int = 0 + + var description: String { + "ExecutorStats(sendWtoM: \(sendJobToMainThread), recvWfromM: \(receiveJobFromWorkerThread)), enqueueGlobal: \(enqueueGlobal), enqueueExecutor: \(enqueueExecutor)" + } + } + #if JAVASCRIPTKIT_STATS + private static let stats = Mutex(ExecutorStats()) + fileprivate static func traceStatsIncrement(_ keyPath: WritableKeyPath) { + stats.withLock { stats in + stats[keyPath: keyPath] += 1 + } + } + internal func dumpStats() { + Self.stats.withLock { stats in + print("WebWorkerTaskExecutor stats: \(stats)") + } + } + #else + fileprivate static func traceStatsIncrement(_ keyPath: WritableKeyPath) {} + 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 + } +} + +/// 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+ +@_expose(wasm, "swjs_enqueue_main_job_from_worker") +#endif +func _swjs_enqueue_main_job_from_worker(_ job: UnownedJob) { + WebWorkerTaskExecutor.traceStatsIncrement(\.receiveJobFromWorkerThread) + JavaScriptEventLoop.shared.enqueue(ExecutorJob(job)) +} + +/// 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+ +@_expose(wasm, "swjs_wake_worker_thread") +#endif +func _swjs_wake_worker_thread() { + WebWorkerTaskExecutor.Worker.currentThread!.run() +} + +private func trace(_ message: String) { + #if JAVASCRIPTKIT_TRACE + JSObject.global.process.stdout.write("[trace tid=\(swjs_get_worker_thread_id())] \(message)\n") + #endif +} + +#endif // compiler(>=6.0) diff --git a/Sources/JavaScriptEventLoopTestSupport/JavaScriptEventLoopTestSupport.swift b/Sources/JavaScriptEventLoopTestSupport/JavaScriptEventLoopTestSupport.swift new file mode 100644 index 000000000..0582fe8c4 --- /dev/null +++ b/Sources/JavaScriptEventLoopTestSupport/JavaScriptEventLoopTestSupport.swift @@ -0,0 +1,38 @@ +/// If you need to execute Swift async functions that can be resumed by JS +/// event loop in your XCTest suites, please add `JavaScriptEventLoopTestSupport` +/// to your test target dependencies. +/// +/// ```diff +/// .testTarget( +/// name: "MyAppTests", +/// dependencies: [ +/// "MyApp", +/// + "JavaScriptEventLoopTestSupport", +/// ] +/// ) +/// ``` +/// +/// Linking this module automatically activates JS event loop based global +/// executor by calling `JavaScriptEventLoop.installGlobalExecutor()` + +import JavaScriptEventLoop + +// This module just expose 'JavaScriptEventLoop.installGlobalExecutor' to C ABI +// See _CJavaScriptEventLoopTestSupport.c for why this is needed + +#if compiler(>=5.5) + +@available(macOS 14.0, iOS 17.0, watchOS 10.0, tvOS 17.0, *) +@_cdecl("swift_javascriptkit_activate_js_executor_impl") +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 + } +} + +#endif diff --git a/Sources/JavaScriptKit/BasicObjects/JSArray.swift b/Sources/JavaScriptKit/BasicObjects/JSArray.swift index 2452c17e7..fad602465 100644 --- a/Sources/JavaScriptKit/BasicObjects/JSArray.swift +++ b/Sources/JavaScriptKit/BasicObjects/JSArray.swift @@ -1,10 +1,12 @@ -/// A wrapper around [the JavaScript Array class](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Array) +/// A wrapper around [the JavaScript `Array` +/// 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 JSArray: JSBridgedClass { - public static let constructor = JSObject.global.Array.function! + public static var constructor: JSFunction? { _constructor.wrappedValue } + private static let _constructor = LazyThreadLocal(initialize: { JSObject.global.Array.function }) static func isArray(_ object: JSObject) -> Bool { - constructor.isArray!(object).boolean! + constructor!.isArray!(object).boolean! } public let jsObject: JSObject @@ -35,6 +37,8 @@ extension JSArray: RandomAccessCollection { Iterator(jsObject: jsObject) } + /// Iterator type for `JSArray`, conforming to `IteratorProtocol` from the standard library, which allows + /// easy iteration over elements of `JSArray` instances. public class Iterator: IteratorProtocol { private let jsObject: JSObject private var index = 0 @@ -88,9 +92,9 @@ extension JSArray: RandomAccessCollection { } } -private let alwaysTrue = JSClosure { _ in .boolean(true) } +private let alwaysTrue = LazyThreadLocal(initialize: { JSClosure { _ in .boolean(true) } }) private func getObjectValuesLength(_ object: JSObject) -> Int { - let values = object.filter!(alwaysTrue).object! + let values = object.filter!(alwaysTrue.wrappedValue).object! return Int(values.length.number!) } diff --git a/Sources/JavaScriptKit/BasicObjects/JSDate.swift b/Sources/JavaScriptKit/BasicObjects/JSDate.swift index 3f38f5aba..9157796bc 100644 --- a/Sources/JavaScriptKit/BasicObjects/JSDate.swift +++ b/Sources/JavaScriptKit/BasicObjects/JSDate.swift @@ -1,32 +1,32 @@ -/** 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 let constructor = JSObject.global.Date.function! + public static var constructor: JSFunction? { _constructor.wrappedValue } + private static let _constructor = LazyThreadLocal(initialize: { JSObject.global.Date.function }) /// The underlying JavaScript `Date` object. public let jsObject: JSObject /** Creates a new instance of the JavaScript `Date` class with a given amount of milliseconds - that passed since midnight 01 January 1970 UTC. - */ + that passed since midnight 01 January 1970 UTC. + */ public init(millisecondsSinceEpoch: Double? = nil) { if let milliseconds = millisecondsSinceEpoch { - jsObject = Self.constructor.new(milliseconds) + jsObject = Self.constructor!.new(milliseconds) } else { - jsObject = Self.constructor.new() + jsObject = Self.constructor!.new() } } - /** According to the standard, `monthIndex` is zero-indexed, where `11` is December. `day` - represents a day of the month starting at `1`. - */ + /** According to the standard, `monthIndex` is zero-indexed, where `11` is December. `day` + represents a day of the month starting at `1`. + */ public init( year: Int, monthIndex: Int, @@ -36,7 +36,7 @@ public final class JSDate: JSBridgedClass { seconds: Int = 0, milliseconds: Int = 0 ) { - jsObject = Self.constructor.new(year, monthIndex, day, hours, minutes, seconds, milliseconds) + jsObject = Self.constructor!.new(year, monthIndex, day, hours, minutes, seconds, milliseconds) } public init(unsafelyWrapping jsObject: JSObject) { @@ -198,7 +198,7 @@ public final class JSDate: JSBridgedClass { Int(jsObject.getTimezoneOffset!().number!) } - /// Returns a string conforming to ISO 8601 that contains date and time, e.g. + /// Returns a string conforming to ISO 8601 that contains date and time, e.g. /// `"2020-09-15T08:56:54.811Z"`. public func toISOString() -> String { jsObject.toISOString!().string! @@ -214,25 +214,25 @@ public final class JSDate: JSBridgedClass { jsObject.toLocaleTimeString!().string! } - /** Returns a string formatted according to - [rfc7231](https://tools.ietf.org/html/rfc7231#section-7.1.1.1) and modified according to - [ecma-262](https://www.ecma-international.org/ecma-262/10.0/index.html#sec-date.prototype.toutcstring), - e.g. `Tue, 15 Sep 2020 09:04:40 GMT`. - */ + /** Returns a string formatted according to + [rfc7231](https://tools.ietf.org/html/rfc7231#section-7.1.1.1) and modified according to + [ecma-262](https://www.ecma-international.org/ecma-262/10.0/index.html#sec-date.prototype.toutcstring), + e.g. `Tue, 15 Sep 2020 09:04:40 GMT`. + */ public func toUTCString() -> String { jsObject.toUTCString!().string! } - /** Number of milliseconds since midnight 01 January 1970 UTC to the present moment ignoring - leap seconds. - */ + /** Number of milliseconds since midnight 01 January 1970 UTC to the present moment ignoring + leap seconds. + */ public static func now() -> Double { - constructor.now!().number! + constructor!.now!().number! } - /** Number of milliseconds since midnight 01 January 1970 UTC to the given date ignoring leap - seconds. - */ + /** Number of milliseconds since midnight 01 January 1970 UTC to the given date ignoring leap + seconds. + */ public func valueOf() -> Double { jsObject.valueOf!().number! } diff --git a/Sources/JavaScriptKit/BasicObjects/JSError.swift b/Sources/JavaScriptKit/BasicObjects/JSError.swift index 305f1d9d5..38accb97b 100644 --- a/Sources/JavaScriptKit/BasicObjects/JSError.swift +++ b/Sources/JavaScriptKit/BasicObjects/JSError.swift @@ -1,17 +1,17 @@ -/** 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: Error, JSBridgedClass { +/// 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 let constructor = JSObject.global.Error.function! + public static var constructor: JSFunction? { _constructor.wrappedValue } + private static let _constructor = LazyThreadLocal(initialize: { JSObject.global.Error.function }) /// The underlying JavaScript `Error` object. public let jsObject: JSObject /// Creates a new instance of the JavaScript `Error` class with a given message. public init(message: String) { - jsObject = Self.constructor.new([message]) + jsObject = Self.constructor!.new([message]) } public init(unsafelyWrapping jsObject: JSObject) { @@ -34,7 +34,7 @@ public final class JSError: Error, JSBridgedClass { } /// Creates a new `JSValue` from this `JSError` instance. - public func jsValue() -> JSValue { + public var jsValue: JSValue { .object(jsObject) } } diff --git a/Sources/JavaScriptKit/BasicObjects/JSPromise.swift b/Sources/JavaScriptKit/BasicObjects/JSPromise.swift index 5b0d47dd4..7502bb5f1 100644 --- a/Sources/JavaScriptKit/BasicObjects/JSPromise.swift +++ b/Sources/JavaScriptKit/BasicObjects/JSPromise.swift @@ -1,14 +1,4 @@ -/** A wrapper around [the JavaScript `Promise` class](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Promise) -that exposes its functions in a type-safe and Swifty way. The `JSPromise` API is generic over both -`Success` and `Failure` types, which improves compatibility with other statically-typed APIs such -as Combine. If you don't know the exact type of your `Success` value, you should use `JSValue`, e.g. -`JSPromise`. In the rare case, where you can't guarantee that the error thrown -is of actual JavaScript `Error` type, you should use `JSPromise`. - -This doesn't 100% match the JavaScript API, as `then` overload with two callbacks is not available. -It's impossible to unify success and failure types from both callbacks in a single returned promise -without type erasure. You should chain `then` and `catch` in those cases to avoid type erasure. -*/ +/// A wrapper around [the JavaScript `Promise` class](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Promise) public final class JSPromise: JSBridgedClass { /// The underlying JavaScript `Promise` object. public let jsObject: JSObject @@ -18,35 +8,45 @@ public final class JSPromise: JSBridgedClass { .object(jsObject) } - public static var constructor: JSFunction { + public static var constructor: JSFunction? { JSObject.global.Promise.function! } /// This private initializer assumes that the passed object is a JavaScript `Promise` public init(unsafelyWrapping object: JSObject) { - self.jsObject = object + jsObject = object } - /** Creates a new `JSPromise` instance from a given JavaScript `Promise` object. If `jsObject` - is not an instance of JavaScript `Promise`, this initializer will return `nil`. - */ + /// Creates a new `JSPromise` instance from a given JavaScript `Promise` object. If `jsObject` + /// is not an instance of JavaScript `Promise`, this initializer will return `nil`. public convenience init?(_ jsObject: JSObject) { self.init(from: jsObject) } - /** Creates a new `JSPromise` instance from a given JavaScript `Promise` object. If `value` - is not an object and is not an instance of JavaScript `Promise`, this function will - return `nil`. - */ + /// Creates a new `JSPromise` instance from a given JavaScript `Promise` object. If `value` + /// 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 } - return Self.init(jsObject) + guard case .object(let jsObject) = value else { return nil } + return Self(jsObject) } - /** Creates a new `JSPromise` instance from a given `resolver` closure. `resolver` takes - two closure that your code should call to either resolve or reject this `JSPromise` instance. - */ - public convenience init(resolver: @escaping (@escaping (Result) -> ()) -> ()) { + /// The result of a promise. + public enum Result: Equatable { + /// The promise resolved with a value. + case success(JSValue) + /// The promise rejected with a value. + case failure(JSValue) + } + + /// Creates a new `JSPromise` instance from a given `resolver` closure. + /// The closure is passed a completion handler. Passing a successful + /// `Result` to the completion handler will cause the promise to resolve + /// with the corresponding value; passing a failure `Result` will cause the + /// promise to reject with the corresponding value. + /// Calling the completion handler more than once will have no effect + /// (per the JavaScript specification). + public convenience init(resolver: @escaping (@escaping (Result) -> Void) -> Void) { let closure = JSOneshotClosure { arguments in // The arguments are always coming from the `Promise` constructor, so we should be // safe to assume their type here @@ -55,68 +55,121 @@ 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) } } return .undefined } - self.init(unsafelyWrapping: Self.constructor.new(closure)) + self.init(unsafelyWrapping: Self.constructor!.new(closure)) } + #if !hasFeature(Embedded) public static func resolve(_ value: ConvertibleToJSValue) -> JSPromise { - self.init(unsafelyWrapping: Self.constructor.resolve!(value).object!) + self.init(unsafelyWrapping: Self.constructor!.resolve!(value).object!) } public static func reject(_ reason: ConvertibleToJSValue) -> JSPromise { - self.init(unsafelyWrapping: Self.constructor.reject!(reason).object!) + self.init(unsafelyWrapping: Self.constructor!.reject!(reason).object!) + } + #else + public static func resolve(_ value: some ConvertibleToJSValue) -> JSPromise { + self.init(unsafelyWrapping: constructor!.resolve!(value).object!) } - /** Schedules the `success` closure to be invoked on sucessful completion of `self`. - */ + public static func reject(_ reason: some ConvertibleToJSValue) -> JSPromise { + self.init(unsafelyWrapping: constructor!.reject!(reason).object!) + } + #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 { let closure = JSOneshotClosure { - return success($0[0]).jsValue() + success($0[0]).jsValue } return JSPromise(unsafelyWrapping: jsObject.then!(closure).object!) } - /** Schedules the `success` closure to be invoked on sucessful completion of `self`. - */ + #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: @escaping (JSValue) -> ConvertibleToJSValue, - failure: @escaping (JSValue) -> ConvertibleToJSValue) -> JSPromise { + 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!) + } + #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 + ) -> JSPromise { let successClosure = JSOneshotClosure { - return success($0[0]).jsValue() + success($0[0]).jsValue } let failureClosure = JSOneshotClosure { - return failure($0[0]).jsValue() + failure($0[0]).jsValue } return JSPromise(unsafelyWrapping: jsObject.then!(successClosure, failureClosure).object!) } - /** Schedules the `failure` closure to be invoked on rejected completion of `self`. - */ + #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 `catch`(failure: @escaping (JSValue) -> ConvertibleToJSValue) -> JSPromise { + 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!) + } + #endif + + /// Schedules the `failure` closure to be invoked on rejected completion of `self`. + @discardableResult + public func `catch`(failure: @escaping (sending JSValue) -> ConvertibleToJSValue) -> JSPromise { let closure = JSOneshotClosure { - return failure($0[0]).jsValue() + 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!) } + #endif - /** Schedules the `failure` closure to be invoked on either successful or rejected completion of - `self`. - */ + /// Schedules the `failure` closure to be invoked on either successful or rejected + /// completion of `self`. @discardableResult - public func finally(successOrFailure: @escaping () -> ()) -> JSPromise { + public func finally(successOrFailure: @escaping () -> Void) -> JSPromise { let closure = JSOneshotClosure { _ in successOrFailure() return .undefined } return .init(unsafelyWrapping: jsObject.finally!(closure).object!) } + #endif } diff --git a/Sources/JavaScriptKit/BasicObjects/JSTimer.swift b/Sources/JavaScriptKit/BasicObjects/JSTimer.swift index 228b7e83d..3655a185c 100644 --- a/Sources/JavaScriptKit/BasicObjects/JSTimer.swift +++ b/Sources/JavaScriptKit/BasicObjects/JSTimer.swift @@ -1,20 +1,44 @@ -/** 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) + case repeating(JSClosure) + + var jsValue: JSValue { + switch self { + case .oneshot(let closure): return closure.jsValue + case .repeating(let closure): return closure.jsValue + } + } + + func release() { + switch self { + case .oneshot(let closure): + closure.release() + case .repeating(let closure): + #if JAVASCRIPTKIT_WITHOUT_WEAKREFS + closure.release() + #else + break // no-op + #endif + } + } + } + /// Indicates whether this timer instance calls its callback repeatedly at a given delay. public let isRepeating: Bool - private let closure: JSClosureProtocol + private let closure: ClosureStorage /** Node.js and browser APIs are slightly different. `setTimeout`/`setInterval` return an object in Node.js, while browsers return a number. Fortunately, clearTimeout and clearInterval take @@ -33,23 +57,27 @@ 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 = JSClosure { _ in - callback() - return .undefined - } + closure = .repeating( + JSClosure { _ in + callback() + return .undefined + } + ) } else { - closure = JSOneshotClosure { _ in - callback() - return .undefined - } + closure = .oneshot( + JSOneshotClosure { _ in + callback() + return .undefined + } + ) } self.isRepeating = isRepeating if isRepeating { - value = global.setInterval.function!(closure, millisecondsDelay) + value = global.setInterval.function!(arguments: [closure.jsValue, millisecondsDelay.jsValue]) } else { - value = global.setTimeout.function!(closure, millisecondsDelay) + value = global.setTimeout.function!(arguments: [closure.jsValue, millisecondsDelay.jsValue]) } } diff --git a/Sources/JavaScriptKit/BasicObjects/JSTypedArray.swift b/Sources/JavaScriptKit/BasicObjects/JSTypedArray.swift index e073d7c29..47919b17d 100644 --- a/Sources/JavaScriptKit/BasicObjects/JSTypedArray.swift +++ b/Sources/JavaScriptKit/BasicObjects/JSTypedArray.swift @@ -1,19 +1,21 @@ // // Created by Manuel Burghard. Licensed unter MIT. // - 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 } } -/// A wrapper around all JavaScript [TypedArray](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/TypedArray) classes that exposes their properties in a type-safe way. -/// FIXME: [BigInt-based TypedArrays are currently not supported](https://github.com/swiftwasm/JavaScriptKit/issues/56). -public class JSTypedArray: JSBridgedClass, ExpressibleByArrayLiteral where Element: TypedArrayElement { - public static var constructor: JSFunction { Element.typedArrayClass } +/// 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 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 { @@ -21,7 +23,7 @@ public class JSTypedArray: JSBridgedClass, ExpressibleByArrayLiteral wh return Element.construct(from: jsObject[index])! } set { - self.jsObject[index] = newValue.jsValue() + jsObject[index] = newValue.jsValue } } @@ -30,24 +32,25 @@ public class JSTypedArray: JSBridgedClass, ExpressibleByArrayLiteral wh /// /// - Parameter length: The number of elements that will be allocated. public init(length: Int) { - jsObject = Element.typedArrayClass.new(length) + jsObject = Self.constructor!.new(length) } - required public init(unsafelyWrapping jsObject: JSObject) { + public required init(unsafelyWrapping jsObject: JSObject) { self.jsObject = jsObject } - required public convenience init(arrayLiteral elements: Element...) { + public required convenience init(arrayLiteral elements: Element...) { self.init(elements) } + /// Initialize a new instance of TypedArray in JavaScript environment with given elements. /// /// - 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 - _create_typed_array(Element.typedArrayClass.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`. @@ -55,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. @@ -62,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. /// @@ -76,74 +99,106 @@ 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 = malloc(bytesLength)! - defer { free(rawBuffer) } - _load_typed_array(jsObject.id, rawBuffer.assumingMemoryBound(to: UInt8.self)) - let length = lengthInBytes / MemoryLayout.size - let boundPtr = rawBuffer.bindMemory(to: Element.self, capacity: length) - let bufferPtr = UnsafeBufferPointer(start: boundPtr, 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 } -} -// MARK: - Int and UInt support + #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 buffer = UnsafeMutableBufferPointer.allocate(capacity: length) + defer { buffer.deallocate() } + copyMemory(to: buffer) + let result = try await body(UnsafeBufferPointer(buffer)) + return result + } + #endif -// 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 = - 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 = - 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 { - public static var typedArrayClass = JSObject.global.Int8Array.function! + public static var typedArrayClass: JSFunction { JSObject.global.Int8Array.function! } } + extension UInt8: TypedArrayElement { - public static var typedArrayClass = JSObject.global.Uint8Array.function! + public static var typedArrayClass: JSFunction { JSObject.global.Uint8Array.function! } } -// TODO: Support Uint8ClampedArray? extension Int16: TypedArrayElement { - public static var typedArrayClass = JSObject.global.Int16Array.function! + public static var typedArrayClass: JSFunction { JSObject.global.Int16Array.function! } } + extension UInt16: TypedArrayElement { - public static var typedArrayClass = JSObject.global.Uint16Array.function! + public static var typedArrayClass: JSFunction { JSObject.global.Uint16Array.function! } } extension Int32: TypedArrayElement { - public static var typedArrayClass = JSObject.global.Int32Array.function! + public static var typedArrayClass: JSFunction { JSObject.global.Int32Array.function! } } + extension UInt32: TypedArrayElement { - public static var typedArrayClass = JSObject.global.Uint32Array.function! + public static var typedArrayClass: JSFunction { JSObject.global.Uint32Array.function! } } -// FIXME: Support passing BigInts across the bridge -// See https://github.com/swiftwasm/JavaScriptKit/issues/56 -//extension Int64: TypedArrayElement { -// public static var typedArrayClass = JSObject.global.BigInt64Array.function! -//} -//extension UInt64: TypedArrayElement { -// public static var typedArrayClass = JSObject.global.BigUint64Array.function! -//} - extension Float32: TypedArrayElement { - public static var typedArrayClass = JSObject.global.Float32Array.function! + public static var typedArrayClass: JSFunction { JSObject.global.Float32Array.function! } } + extension Float64: TypedArrayElement { - public static var typedArrayClass = JSObject.global.Float64Array.function! + public static var typedArrayClass: JSFunction { JSObject.global.Float64Array.function! } +} + +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 063597a9e..d4a5921cd 100644 --- a/Sources/JavaScriptKit/ConstructibleFromJSValue.swift +++ b/Sources/JavaScriptKit/ConstructibleFromJSValue.swift @@ -20,80 +20,112 @@ extension String: ConstructibleFromJSValue { } } -extension Double: ConstructibleFromJSValue { - public static func construct(from value: JSValue) -> Double? { - return value.number - } -} - -extension Float: ConstructibleFromJSValue { - public static func construct(from value: JSValue) -> Float? { - return value.number.map(Float.init) - } -} - -extension Int: ConstructibleFromJSValue { - public static func construct(from value: JSValue) -> Self? { - value.number.map(Self.init) - } -} - -extension Int8: ConstructibleFromJSValue { - public static func construct(from value: JSValue) -> Self? { - value.number.map(Self.init) +extension JSString: ConstructibleFromJSValue { + public static func construct(from value: JSValue) -> JSString? { + value.jsString } } -extension Int16: ConstructibleFromJSValue { +extension BinaryFloatingPoint where Self: ConstructibleFromJSValue { public static func construct(from value: JSValue) -> Self? { value.number.map(Self.init) } } +extension Double: ConstructibleFromJSValue {} +extension Float: ConstructibleFromJSValue {} -extension Int32: ConstructibleFromJSValue { - public static func construct(from value: JSValue) -> Self? { - value.number.map(Self.init) +extension SignedInteger where Self: ConstructibleFromJSValue { + /// Construct an instance of `SignedInteger` from the given `JSBigIntExtended`. + /// + /// If the value is too large to fit in the `Self` type, `nil` is returned. + /// + /// - Parameter bigInt: The `JSBigIntExtended` to decode + public init?(exactly bigInt: some JSBigIntExtended) { + self.init(exactly: bigInt.int64Value) } -} -extension Int64: ConstructibleFromJSValue { - public static func construct(from value: JSValue) -> Self? { - value.number.map(Self.init) + /// Construct an instance of `SignedInteger` from the given `JSBigIntExtended`. + /// + /// Crash if the value is too large to fit in the `Self` type. + /// + /// - Parameter bigInt: The `JSBigIntExtended` to decode + public init(_ bigInt: some JSBigIntExtended) { + self.init(bigInt.int64Value) } -} -extension UInt: ConstructibleFromJSValue { + /// Construct an instance of `SignedInteger` from the given `JSValue`. + /// + /// Returns `nil` if one of the following conditions is met: + /// - The value is not a number or a bigint. + /// - The value is a number that does not fit or cannot be represented + /// in the `Self` type (e.g. NaN, Infinity). + /// - The value is a bigint that does not fit in the `Self` type. + /// + /// If the value is a number, it is rounded towards zero before conversion. + /// + /// - Parameter value: The `JSValue` to decode public static func construct(from value: JSValue) -> Self? { - value.number.map(Self.init) + if let number = value.number { + return Self(exactly: number.rounded(.towardZero)) + } + #if !hasFeature(Embedded) + if let bigInt = value.bigInt as? JSBigIntExtended { + return Self(exactly: bigInt) + } + #endif + return nil } } +extension Int: ConstructibleFromJSValue {} +extension Int8: ConstructibleFromJSValue {} +extension Int16: ConstructibleFromJSValue {} +extension Int32: ConstructibleFromJSValue {} +extension Int64: ConstructibleFromJSValue {} -extension UInt8: ConstructibleFromJSValue { - public static func construct(from value: JSValue) -> Self? { - value.number.map(Self.init) - } -} +extension UnsignedInteger where Self: ConstructibleFromJSValue { -extension UInt16: ConstructibleFromJSValue { - public static func construct(from value: JSValue) -> Self? { - value.number.map(Self.init) + /// Construct an instance of `UnsignedInteger` from the given `JSBigIntExtended`. + /// + /// Returns `nil` if the value is negative or too large to fit in the `Self` type. + /// + /// - Parameter bigInt: The `JSBigIntExtended` to decode + public init?(exactly bigInt: some JSBigIntExtended) { + self.init(exactly: bigInt.uInt64Value) } -} -extension UInt32: ConstructibleFromJSValue { - public static func construct(from value: JSValue) -> Self? { - value.number.map(Self.init) + /// Construct an instance of `UnsignedInteger` from the given `JSBigIntExtended`. + /// + /// Crash if the value is negative or too large to fit in the `Self` type. + /// + /// - Parameter bigInt: The `JSBigIntExtended` to decode + public init(_ bigInt: some JSBigIntExtended) { + self.init(bigInt.uInt64Value) } -} -extension UInt64: ConstructibleFromJSValue { + /// Construct an instance of `UnsignedInteger` from the given `JSValue`. + /// + /// Returns `nil` if one of the following conditions is met: + /// - The value is not a number or a bigint. + /// - The value is a number that does not fit or cannot be represented + /// in the `Self` type (e.g. NaN, Infinity). + /// - The value is a bigint that does not fit in the `Self` type. + /// - The value is negative. + /// + /// - Parameter value: The `JSValue` to decode public static func construct(from value: JSValue) -> Self? { - value.number.map(Self.init) - } -} - -extension JSString: ConstructibleFromJSValue { - public static func construct(from value: JSValue) -> JSString? { - value.jsString + if let number = value.number { + return Self(exactly: number.rounded(.towardZero)) + } + #if !hasFeature(Embedded) + if let bigInt = value.bigInt as? JSBigIntExtended { + return Self(exactly: bigInt) + } + #endif + return nil } } +extension UInt: ConstructibleFromJSValue {} +extension UInt8: ConstructibleFromJSValue {} +extension UInt16: ConstructibleFromJSValue {} +extension UInt32: ConstructibleFromJSValue {} +extension UInt64: ConstructibleFromJSValue {} diff --git a/Sources/JavaScriptKit/ConvertibleToJSValue.swift b/Sources/JavaScriptKit/ConvertibleToJSValue.swift index 7917c32cb..805ee74d5 100644 --- a/Sources/JavaScriptKit/ConvertibleToJSValue.swift +++ b/Sources/JavaScriptKit/ConvertibleToJSValue.swift @@ -3,7 +3,12 @@ import _CJavaScriptKit /// Objects that can be converted to a JavaScript value, preferably in a lossless manner. public protocol ConvertibleToJSValue { /// Create a JSValue that represents this object - func jsValue() -> JSValue + var jsValue: JSValue { get } +} + +extension ConvertibleToJSValue { + @available(*, deprecated, message: "Use the .jsValue property instead") + public func jsValue() -> JSValue { jsValue } } public typealias JSValueCompatible = ConvertibleToJSValue & ConstructibleFromJSValue @@ -13,67 +18,65 @@ extension JSValue: JSValueCompatible { return value } - public func jsValue() -> JSValue { self } + public var jsValue: JSValue { self } } extension Bool: ConvertibleToJSValue { - public func jsValue() -> JSValue { .boolean(self) } + public var jsValue: JSValue { .boolean(self) } } extension Int: ConvertibleToJSValue { - public func jsValue() -> JSValue { .number(Double(self)) } + public var jsValue: JSValue { + assert(Self.bitWidth == 32) + return .number(Double(self)) + } } extension UInt: ConvertibleToJSValue { - public func jsValue() -> JSValue { .number(Double(self)) } + public var jsValue: JSValue { + assert(Self.bitWidth == 32) + return .number(Double(self)) + } } extension Float: ConvertibleToJSValue { - public func jsValue() -> JSValue { .number(Double(self)) } + public var jsValue: JSValue { .number(Double(self)) } } extension Double: ConvertibleToJSValue { - public func jsValue() -> JSValue { .number(self) } + public var jsValue: JSValue { .number(self) } } extension String: ConvertibleToJSValue { - public func jsValue() -> JSValue { .string(JSString(self)) } + public var jsValue: JSValue { .string(JSString(self)) } } extension UInt8: ConvertibleToJSValue { - public func jsValue() -> JSValue { .number(Double(self)) } + public var jsValue: JSValue { .number(Double(self)) } } extension UInt16: ConvertibleToJSValue { - public func jsValue() -> JSValue { .number(Double(self)) } + public var jsValue: JSValue { .number(Double(self)) } } extension UInt32: ConvertibleToJSValue { - public func jsValue() -> JSValue { .number(Double(self)) } -} - -extension UInt64: ConvertibleToJSValue { - public func jsValue() -> JSValue { .number(Double(self)) } + public var jsValue: JSValue { .number(Double(self)) } } extension Int8: ConvertibleToJSValue { - public func jsValue() -> JSValue { .number(Double(self)) } + public var jsValue: JSValue { .number(Double(self)) } } extension Int16: ConvertibleToJSValue { - public func jsValue() -> JSValue { .number(Double(self)) } + public var jsValue: JSValue { .number(Double(self)) } } extension Int32: ConvertibleToJSValue { - public func jsValue() -> JSValue { .number(Double(self)) } -} - -extension Int64: ConvertibleToJSValue { - public func jsValue() -> JSValue { .number(Double(self)) } + public var jsValue: JSValue { .number(Double(self)) } } extension JSString: ConvertibleToJSValue { - public func jsValue() -> JSValue { .string(self) } + public var jsValue: JSValue { .string(self) } } extension JSObject: JSValueCompatible { @@ -81,20 +84,28 @@ extension JSObject: JSValueCompatible { // from `JSFunction` } -private let objectConstructor = JSObject.global.Object.function! -private let arrayConstructor = JSObject.global.Array.function! +private let _objectConstructor = LazyThreadLocal(initialize: { JSObject.global.Object.function! }) +private var objectConstructor: JSFunction { _objectConstructor.wrappedValue } +private let _arrayConstructor = LazyThreadLocal(initialize: { JSObject.global.Array.function! }) +private var arrayConstructor: JSFunction { _arrayConstructor.wrappedValue } -extension Dictionary where Value: ConvertibleToJSValue, Key == String { - public func jsValue() -> JSValue { - Swift.Dictionary.jsValue(self)() +#if !hasFeature(Embedded) +extension Dictionary where Value == ConvertibleToJSValue, Key == String { + public var jsValue: JSValue { + let object = objectConstructor.new() + for (key, value) in self { + object[key] = value.jsValue + } + return .object(object) } } +#endif -extension Dictionary: ConvertibleToJSValue where Value == ConvertibleToJSValue, Key == String { - public func jsValue() -> JSValue { +extension Dictionary: ConvertibleToJSValue where Value: ConvertibleToJSValue, Key == String { + public var jsValue: JSValue { let object = objectConstructor.new() for (key, value) in self { - object[key] = value.jsValue() + object[key] = value.jsValue } return .object(object) } @@ -104,7 +115,7 @@ extension Dictionary: ConstructibleFromJSValue where Value: ConstructibleFromJSV public static func construct(from value: JSValue) -> Self? { guard let objectRef = value.object, - let keys: [String] = objectConstructor.keys!(objectRef.jsValue()).fromJSValue() + let keys: [String] = objectConstructor.keys!(objectRef.jsValue).fromJSValue() else { return nil } var entries = [(String, Value)]() @@ -123,37 +134,44 @@ extension Optional: ConstructibleFromJSValue where Wrapped: ConstructibleFromJSV public static func construct(from value: JSValue) -> Self? { switch value { case .null, .undefined: - return nil + return .some(nil) default: - return Wrapped.construct(from: value) + guard let wrapped = Wrapped.construct(from: value) else { return nil } + return .some(wrapped) } } } extension Optional: ConvertibleToJSValue where Wrapped: ConvertibleToJSValue { - public func jsValue() -> JSValue { + public var jsValue: JSValue { switch self { case .none: return .null - case let .some(wrapped): return wrapped.jsValue() + case .some(let wrapped): return wrapped.jsValue } } } -extension Array where Element: ConvertibleToJSValue { - public func jsValue() -> JSValue { - Array.jsValue(self)() +extension Array: ConvertibleToJSValue where Element: ConvertibleToJSValue { + public var jsValue: JSValue { + let array = arrayConstructor.new(count) + for (index, element) in enumerated() { + array[index] = element.jsValue + } + return .object(array) } } -extension Array: ConvertibleToJSValue where Element == ConvertibleToJSValue { - public func jsValue() -> JSValue { +#if !hasFeature(Embedded) +extension Array where Element == ConvertibleToJSValue { + public var jsValue: JSValue { let array = arrayConstructor.new(count) for (index, element) in enumerated() { - array[index] = element.jsValue() + array[index] = element.jsValue } return .object(array) } } +#endif extension Array: ConstructibleFromJSValue where Element: ConstructibleFromJSValue { public static func construct(from value: JSValue) -> [Element]? { @@ -166,7 +184,7 @@ extension Array: ConstructibleFromJSValue where Element: ConstructibleFromJSValu var array = [Element]() array.reserveCapacity(count) - for i in 0 ..< count { + for i in 0.. JSValue { + public var jsValue: JSValue { switch kind { - case .invalid: - fatalError() case .boolean: return .boolean(payload1 != 0) case .number: @@ -194,6 +210,10 @@ extension RawJSValue: ConvertibleToJSValue { return .undefined case .function: return .function(JSFunction(id: UInt32(payload1))) + case .symbol: + return .symbol(JSSymbol(id: UInt32(payload1))) + case .bigInt: + return .bigInt(JSBigInt(id: UInt32(payload1))) } } } @@ -204,16 +224,16 @@ extension JSValue { 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): + case .string(let string): return string.withRawJSValue(body) - case let .object(ref): + case .object(let ref): kind = .object payload1 = JavaScriptPayload1(ref.id) case .null: @@ -222,23 +242,34 @@ extension JSValue { case .undefined: kind = .undefined payload1 = 0 - case let .function(functionRef): + case .function(let functionRef): kind = .function payload1 = JavaScriptPayload1(functionRef.id) + case .symbol(let symbolRef): + kind = .symbol + payload1 = JavaScriptPayload1(symbolRef.id) + case .bigInt(let bigIntRef): + kind = .bigInt + payload1 = JavaScriptPayload1(bigIntRef.id) } let rawValue = RawJSValue(kind: kind, payload1: payload1, payload2: payload2) return body(rawValue) } } -extension Array where Element == ConvertibleToJSValue { +extension Array where Element: ConvertibleToJSValue { func withRawJSValues(_ body: ([RawJSValue]) -> T) -> T { - func _withRawJSValues( - _ values: [ConvertibleToJSValue], _ index: Int, - _ results: inout [RawJSValue], _ body: ([RawJSValue]) -> 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 + return values[index].jsValue.withRawJSValue { (rawValue) -> T in results.append(rawValue) return _withRawJSValues(values, index + 1, &results, body) } @@ -248,8 +279,26 @@ extension Array where Element == ConvertibleToJSValue { } } -extension Array where Element: ConvertibleToJSValue { +#if !hasFeature(Embedded) +extension Array where Element == ConvertibleToJSValue { func withRawJSValues(_ body: ([RawJSValue]) -> T) -> T { - [ConvertibleToJSValue].withRawJSValues(self)(body) + // 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) + } + } + 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/Articles/JavaScript-Environment-Requirements.md b/Sources/JavaScriptKit/Documentation.docc/Articles/JavaScript-Environment-Requirements.md new file mode 100644 index 000000000..6483e4ca6 --- /dev/null +++ b/Sources/JavaScriptKit/Documentation.docc/Articles/JavaScript-Environment-Requirements.md @@ -0,0 +1,48 @@ +# JavaScript Environment Requirements + +## Required JavaScript Features + +The JavaScript package produced by the JavaScriptKit packaging plugin requires the following JavaScript features: + +- [`FinalizationRegistry`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/FinalizationRegistry#browser_compatibility) +- [WebAssembly BigInt to i64 conversion in JS API](https://caniuse.com/wasm-bigint) + +## Browser Compatibility + +These JavaScript features are supported in the following browsers: + +- Chrome 85+ (August 2020) +- Firefox 79+ (July 2020) +- Desktop Safari 14.1+ (April 2021) +- Mobile Safari 14.5+ (April 2021) +- Edge 85+ (August 2020) +- Node.js 15.0+ (October 2020) + +Older browsers will not be able to run applications built with JavaScriptKit unless polyfills are provided. + +## Handling Missing Features + +### FinalizationRegistry + +When using JavaScriptKit in environments without `FinalizationRegistry` support, you can: + +1. Build with the opt-out flag: `-Xswiftc -DJAVASCRIPTKIT_WITHOUT_WEAKREFS` +2. Then manually manage memory by calling `release()` on all `JSClosure` instances: + +```swift +let closure = JSClosure { args in + // Your code here + return .undefined +} + +// Use the closure... + +// Then release it when done +#if JAVASCRIPTKIT_WITHOUT_WEAKREFS +closure.release() +#endif +``` + +### WebAssembly BigInt Support + +If you need to work with 64-bit integers in JavaScript, ensure your target environment supports WebAssembly BigInt conversions. For environments that don't support this feature, you'll need to avoid importing `JavaScriptBigIntSupport` diff --git a/Sources/JavaScriptKit/Documentation.docc/Documentation.md b/Sources/JavaScriptKit/Documentation.docc/Documentation.md new file mode 100644 index 000000000..ffc168431 --- /dev/null +++ b/Sources/JavaScriptKit/Documentation.docc/Documentation.md @@ -0,0 +1,64 @@ +# ``JavaScriptKit`` + +Swift framework to interact with JavaScript through WebAssembly. + +## Overview + +**JavaScriptKit** provides a seamless way to interact with JavaScript from Swift code when compiled to WebAssembly. + +## Quick Start + +Check out the tutorial for a step-by-step guide to getting started. + +### Key Features + +- Access JavaScript objects and functions +- Create closures that can be called from JavaScript +- Convert between Swift and JavaScript data types +- Use JavaScript promises with Swift's `async/await` +- Work with multi-threading + +### Example + +```swift +import JavaScriptKit + +// Access global JavaScript objects +let document = JSObject.global.document + +// Create and manipulate DOM elements +var div = document.createElement("div") +div.innerText = "Hello from Swift!" +_ = document.body.appendChild(div) + +// Handle events with Swift closures +var button = document.createElement("button") +button.innerText = "Click me" +button.onclick = .object(JSClosure { _ in + JSObject.global.alert!("Button clicked!") + return .undefined +}) +_ = document.body.appendChild(button) +``` + +Check out the [examples](https://github.com/swiftwasm/JavaScriptKit/tree/main/Examples) for more detailed usage. + +## Topics + +### Tutorials + +- + +### 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 new file mode 100644 index 000000000..c054e3a48 --- /dev/null +++ b/Sources/JavaScriptKit/Documentation.docc/Tutorials/Hello-World/Hello-World.tutorial @@ -0,0 +1,101 @@ +@Tutorial(time: 5) { + @Intro(title: "Quick Start: Hello World") { + This tutorial walks you through creating a simple web application using JavaScriptKit. You'll learn how to set up a Swift package, add JavaScriptKit as a dependency, write code to manipulate the DOM, and build and run your web application. + + JavaScriptKit allows you to interact with JavaScript APIs directly from Swift code when targeting WebAssembly, making it easy to build web applications using Swift. + } + + @Section(title: "Prerequisites") { + Visit the [installation guide](https://book.swiftwasm.org/getting-started/setup.html) to install the Swift SDK for WebAssembly before starting this tutorial. + This tutorial assumes you have the Swift SDK for WebAssembly installed. Please check your Swift installation. + + @Steps { + @Step { + Check your Swift toolchain version. If you see different + @Code(name: "Console", file: "hello-world-0-1-swift-version.txt") + } + @Step { + Select a Swift SDK for WebAssembly version that matches the version of the Swift toolchain you have installed. + + The following sections of this tutorial assume you have set the `SWIFT_SDK_ID` environment variable. + @Code(name: "Console", file: "hello-world-0-2-select-sdk.txt") + } + } + } + + @Section(title: "Set up your project") { + Let's start by creating a new Swift package and configuring it to use JavaScriptKit. + + @Steps { + @Step { + Create a new Swift package by running the following command in your terminal: + This creates a new Swift executable package named "Hello" with a basic folder structure. + + @Code(name: "Console", file: "hello-world-1-1-init-package.txt") + } + + @Step { + Add JavaScriptKit as a dependency using the Swift Package Manager: + This command adds the JavaScriptKit GitHub repository as a dependency to your package. + + @Code(name: "Console", file: "hello-world-1-2-add-dependency.txt") + } + + @Step { + Add JavaScriptKit as a target dependency: + This command adds JavaScriptKit as a target dependency to your package. + + @Code(name: "Console", file: "hello-world-1-3-add-target-dependency.txt") + } + } + } + + @Section(title: "Write your web application") { + Now let's write some Swift code that manipulates the DOM to create a simple web page. + + @Steps { + @Step { + Create or modify the main.swift file in your Sources/Hello directory: + This code creates a new div element, sets its text content to "Hello from Swift!", and appends it to the document body. + + @Code(name: "main.swift", file: "hello-world-2-1-main-swift.swift") + } + + @Step { + Create an index.html file in the root of your project to load your WebAssembly application: + This HTML file includes a script that loads and runs your compiled WebAssembly code. + + @Code(name: "index.html", file: "hello-world-2-2-index-html.html") + } + } + } + + @Section(title: "Build and run your application") { + Let's build your application and run it in a web browser. + + @Steps { + @Step { + Build your application with the Swift WebAssembly toolchain: + This command compiles your Swift code to WebAssembly and generates the necessary JavaScript bindings. + + @Code(name: "Console", file: "hello-world-3-1-build.txt") + } + + @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") + } + + @Step { + Open your application in a web browser: + Your browser should open and display a page with "Hello from Swift!" as text added by your Swift code. + + @Code(name: "Console", file: "hello-world-3-3-open.txt") { + @Image(alt: "Preview of the web application", source: "hello-world-3-3-app.png") + } + } + } + } +} diff --git a/Sources/JavaScriptKit/Documentation.docc/Tutorials/Hello-World/Resources/hello-world-0-1-swift-version.txt b/Sources/JavaScriptKit/Documentation.docc/Tutorials/Hello-World/Resources/hello-world-0-1-swift-version.txt new file mode 100644 index 000000000..5d5ad28df --- /dev/null +++ b/Sources/JavaScriptKit/Documentation.docc/Tutorials/Hello-World/Resources/hello-world-0-1-swift-version.txt @@ -0,0 +1,7 @@ +$ swift --version +Apple Swift version 6.0.3 (swift-6.0.3-RELEASE) +or +Swift version 6.0.3 (swift-6.0.3-RELEASE) + +$ swift sdk list +6.0.3-RELEASE-wasm32-unknown-wasi diff --git a/Sources/JavaScriptKit/Documentation.docc/Tutorials/Hello-World/Resources/hello-world-0-2-select-sdk.txt b/Sources/JavaScriptKit/Documentation.docc/Tutorials/Hello-World/Resources/hello-world-0-2-select-sdk.txt new file mode 100644 index 000000000..b5fc2c620 --- /dev/null +++ b/Sources/JavaScriptKit/Documentation.docc/Tutorials/Hello-World/Resources/hello-world-0-2-select-sdk.txt @@ -0,0 +1,9 @@ +$ swift --version +Apple Swift version 6.0.3 (swift-6.0.3-RELEASE) +or +Swift version 6.0.3 (swift-6.0.3-RELEASE) + +$ swift sdk list +6.0.3-RELEASE-wasm32-unknown-wasi + +$ export SWIFT_SDK_ID=6.0.3-RELEASE-wasm32-unknown-wasi diff --git a/Sources/JavaScriptKit/Documentation.docc/Tutorials/Hello-World/Resources/hello-world-1-1-init-package.txt b/Sources/JavaScriptKit/Documentation.docc/Tutorials/Hello-World/Resources/hello-world-1-1-init-package.txt new file mode 100644 index 000000000..938b88e01 --- /dev/null +++ b/Sources/JavaScriptKit/Documentation.docc/Tutorials/Hello-World/Resources/hello-world-1-1-init-package.txt @@ -0,0 +1,6 @@ +$ swift package init --name Hello --type executable +Creating executable package: Hello +Creating Package.swift +Creating .gitignore +Creating Sources/ +Creating Sources/main.swift diff --git a/Sources/JavaScriptKit/Documentation.docc/Tutorials/Hello-World/Resources/hello-world-1-2-add-dependency.txt b/Sources/JavaScriptKit/Documentation.docc/Tutorials/Hello-World/Resources/hello-world-1-2-add-dependency.txt new file mode 100644 index 000000000..358629d0c --- /dev/null +++ b/Sources/JavaScriptKit/Documentation.docc/Tutorials/Hello-World/Resources/hello-world-1-2-add-dependency.txt @@ -0,0 +1,9 @@ +$ swift package init --name Hello --type executable +Creating executable package: Hello +Creating Package.swift +Creating .gitignore +Creating Sources/ +Creating Sources/main.swift + +$ swift package add-dependency https://github.com/swiftwasm/JavaScriptKit.git --branch main +Updating package manifest at Package.swift... done. diff --git a/Sources/JavaScriptKit/Documentation.docc/Tutorials/Hello-World/Resources/hello-world-1-3-add-target-dependency.txt b/Sources/JavaScriptKit/Documentation.docc/Tutorials/Hello-World/Resources/hello-world-1-3-add-target-dependency.txt new file mode 100644 index 000000000..317690412 --- /dev/null +++ b/Sources/JavaScriptKit/Documentation.docc/Tutorials/Hello-World/Resources/hello-world-1-3-add-target-dependency.txt @@ -0,0 +1,12 @@ +$ swift package init --name Hello --type executable +Creating executable package: Hello +Creating Package.swift +Creating .gitignore +Creating Sources/ +Creating Sources/main.swift + +$ swift package add-dependency https://github.com/swiftwasm/JavaScriptKit.git --branch main +Updating package manifest at Package.swift... done. + +$ swift package add-target-dependency --package JavaScriptKit JavaScriptKit Hello +Updating package manifest at Package.swift... done. 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 new file mode 100644 index 000000000..a528e65b1 --- /dev/null +++ b/Sources/JavaScriptKit/Documentation.docc/Tutorials/Hello-World/Resources/hello-world-2-1-main-swift.swift @@ -0,0 +1,6 @@ +import JavaScriptKit + +let document = JSObject.global.document +var div = document.createElement("div") +div.innerText = "Hello from Swift!" +document.body.appendChild(div) diff --git a/Sources/JavaScriptKit/Documentation.docc/Tutorials/Hello-World/Resources/hello-world-2-2-index-html.html b/Sources/JavaScriptKit/Documentation.docc/Tutorials/Hello-World/Resources/hello-world-2-2-index-html.html new file mode 100644 index 000000000..c75dd927a --- /dev/null +++ b/Sources/JavaScriptKit/Documentation.docc/Tutorials/Hello-World/Resources/hello-world-2-2-index-html.html @@ -0,0 +1,14 @@ + + + + + Swift Web App + + + +

My Swift Web App

+ + diff --git a/Sources/JavaScriptKit/Documentation.docc/Tutorials/Hello-World/Resources/hello-world-3-1-build.txt b/Sources/JavaScriptKit/Documentation.docc/Tutorials/Hello-World/Resources/hello-world-3-1-build.txt new file mode 100644 index 000000000..9c0ef39c2 --- /dev/null +++ b/Sources/JavaScriptKit/Documentation.docc/Tutorials/Hello-World/Resources/hello-world-3-1-build.txt @@ -0,0 +1,6 @@ +$ swift package --swift-sdk $SWIFT_SDK_ID js --use-cdn +[37/37] Linking Hello.wasm +Build of product 'Hello' complete! (5.16s) +Packaging... +... +Packaging finished 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 new file mode 100644 index 000000000..ad560a635 --- /dev/null +++ b/Sources/JavaScriptKit/Documentation.docc/Tutorials/Hello-World/Resources/hello-world-3-2-server.txt @@ -0,0 +1,7 @@ +$ swift package --swift-sdk $SWIFT_SDK_ID js --use-cdn +[37/37] Linking Hello.wasm +Build of product 'Hello' complete! (5.16s) +Packaging... +... +Packaging finished +$ npx serve diff --git a/Sources/JavaScriptKit/Documentation.docc/Tutorials/Hello-World/Resources/hello-world-3-3-app.png b/Sources/JavaScriptKit/Documentation.docc/Tutorials/Hello-World/Resources/hello-world-3-3-app.png new file mode 100644 index 000000000..033cafbcd Binary files /dev/null and b/Sources/JavaScriptKit/Documentation.docc/Tutorials/Hello-World/Resources/hello-world-3-3-app.png differ 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 new file mode 100644 index 000000000..8abe30b7c --- /dev/null +++ b/Sources/JavaScriptKit/Documentation.docc/Tutorials/Hello-World/Resources/hello-world-3-3-open.txt @@ -0,0 +1,8 @@ +$ swift package --swift-sdk $SWIFT_SDK_ID js --use-cdn +[37/37] Linking Hello.wasm +Build of product 'Hello' complete! (5.16s) +Packaging... +... +Packaging finished +$ npx serve +$ open http://localhost:3000 diff --git a/Sources/JavaScriptKit/Documentation.docc/Tutorials/Resources/image.png b/Sources/JavaScriptKit/Documentation.docc/Tutorials/Resources/image.png new file mode 100644 index 000000000..5b24016a9 Binary files /dev/null and b/Sources/JavaScriptKit/Documentation.docc/Tutorials/Resources/image.png differ diff --git a/Sources/JavaScriptKit/Documentation.docc/Tutorials/Table-of-Contents.tutorial b/Sources/JavaScriptKit/Documentation.docc/Tutorials/Table-of-Contents.tutorial new file mode 100644 index 000000000..c2950be1e --- /dev/null +++ b/Sources/JavaScriptKit/Documentation.docc/Tutorials/Table-of-Contents.tutorial @@ -0,0 +1,9 @@ +@Tutorials(name: "JavaScriptKit") { + @Intro(title: "Working with JavaScriptKit") { + JavaScriptKit is a Swift package that allows you to interact with JavaScript APIs directly from Swift code when targeting WebAssembly. + } + @Chapter(name: "Hello World") { + @Image(source: "image.png") + @TutorialReference(tutorial: "doc:Hello-World") + } +} diff --git a/Sources/JavaScriptKit/Features.swift b/Sources/JavaScriptKit/Features.swift index e479003c5..313edab40 100644 --- a/Sources/JavaScriptKit/Features.swift +++ b/Sources/JavaScriptKit/Features.swift @@ -5,8 +5,14 @@ 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 } + +#if compiler(>=6.0) && hasFeature(Embedded) +// 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 diff --git a/Sources/JavaScriptKit/FundamentalObjects/JSBigInt.swift b/Sources/JavaScriptKit/FundamentalObjects/JSBigInt.swift new file mode 100644 index 000000000..3f0c2632b --- /dev/null +++ b/Sources/JavaScriptKit/FundamentalObjects/JSBigInt.swift @@ -0,0 +1,46 @@ +import _CJavaScriptKit + +private var constructor: JSFunction { JSObject.global.BigInt.function! } + +/// A wrapper around [the JavaScript `BigInt` +/// class](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Array) +/// that exposes its properties in a type-safe and Swifty way. +public final class JSBigInt: JSObject { + @_spi(JSObject_id) + 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 & 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 & 0xffff_ffff), UInt32(value >> 32), false)) + } + + override public var jsValue: JSValue { + .bigInt(self) + } + + public func clamped(bitSize: Int, signed: Bool) -> JSBigInt { + if signed { + return constructor.asIntN(bitSize, self).bigInt! + } else { + return constructor.asUintN(bitSize, self).bigInt! + } + } +} + +public protocol JSBigIntExtended: JSBigInt { + var int64Value: Int64 { get } + var uInt64Value: UInt64 { get } + + init(_ value: Int64) + init(unsigned value: UInt64) +} diff --git a/Sources/JavaScriptKit/FundamentalObjects/JSClosure.swift b/Sources/JavaScriptKit/FundamentalObjects/JSClosure.swift index f8c2632c9..fa713c3b9 100644 --- a/Sources/JavaScriptKit/FundamentalObjects/JSClosure.swift +++ b/Sources/JavaScriptKit/FundamentalObjects/JSClosure.swift @@ -1,6 +1,6 @@ import _CJavaScriptKit -/// JSClosureProtocol wraps Swift closure objects for use in JavaScript. Conforming types +/// `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 /// task to the user by requiring an explicit `release()` call. public protocol JSClosureProtocol: JSValueCompatible { @@ -10,30 +10,48 @@ public protocol JSClosureProtocol: JSValueCompatible { func release() } - -/// `JSOneshotClosure` is a JavaScript function that can be called only once. +/// `JSOneshotClosure` is a JavaScript function that can be called only once. This class can be used +/// for optimized memory management when compared to the common `JSClosure`. public class JSOneshotClosure: JSObject, JSClosureProtocol { private var hostFuncRef: JavaScriptHostFuncRef = 0 - public init(_ body: @escaping ([JSValue]) -> JSValue) { + public init(_ body: @escaping (sending [JSValue]) -> JSValue, file: String = #fileID, line: UInt32 = #line) { // 1. Fill `id` as zero at first to access `self` to get `ObjectIdentifier`. super.init(id: 0) // 2. Create a new JavaScript function which calls the given Swift function. - hostFuncRef = JavaScriptHostFuncRef(bitPattern: Int32(ObjectIdentifier(self).hashValue)) - id = _create_function(hostFuncRef) + hostFuncRef = JavaScriptHostFuncRef(bitPattern: ObjectIdentifier(self)) + _id = withExtendedLifetime(JSString(file)) { file in + swjs_create_function(hostFuncRef, line, file.asInternalJSRef()) + } // 3. Retain the given body in static storage by `funcRef`. - JSClosure.sharedClosures[hostFuncRef] = (self, { - defer { self.release() } - return body($0) - }) + JSClosure.sharedClosures.wrappedValue[hostFuncRef] = ( + self, + { + defer { self.release() } + return body($0) + } + ) + } + + @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) + @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 + { + JSOneshotClosure(makeAsyncClosure(body)) } + #endif /// Release this function resource. /// After calling `release`, calling this function from JavaScript will fail. public func release() { - JSClosure.sharedClosures[hostFuncRef] = nil + JSClosure.sharedClosures.wrappedValue[hostFuncRef] = nil } } @@ -42,20 +60,38 @@ public class JSOneshotClosure: JSObject, JSClosureProtocol { /// /// e.g. /// ```swift -/// let eventListenter = JSClosure { _ in +/// let eventListener = JSClosure { _ in /// ... /// return JSValue.undefined /// } /// -/// button.addEventListener!("click", JSValue.function(eventListenter)) +/// button.addEventListener!("click", JSValue.function(eventListener)) /// ... -/// button.removeEventListener!("click", JSValue.function(eventListenter)) +/// button.removeEventListener!("click", JSValue.function(eventListener)) /// ``` /// -public class JSClosure: JSObject, JSClosureProtocol { +public class JSClosure: JSFunction, JSClosureProtocol { + + class SharedJSClosure { + // Note: 6.0 compilers built with assertions enabled crash when calling + // `removeValue(forKey:)` on a dictionary with value type containing + // `sending`. Wrap the value type with a struct to avoid the crash. + struct Entry { + let item: (object: JSObject, body: (sending [JSValue]) -> JSValue) + } + private var storage: [JavaScriptHostFuncRef: Entry] = [:] + init() {} + + subscript(_ key: JavaScriptHostFuncRef) -> (object: JSObject, body: (sending [JSValue]) -> JSValue)? { + get { storage[key]?.item } + set { storage[key] = newValue.map { Entry(item: $0) } } + } + } // Note: Retain the closure object itself also to avoid funcRef conflicts - fileprivate static var sharedClosures: [JavaScriptHostFuncRef: (object: JSObject, body: ([JSValue]) -> JSValue)] = [:] + fileprivate static let sharedClosures = LazyThreadLocal { + SharedJSClosure() + } private var hostFuncRef: JavaScriptHostFuncRef = 0 @@ -63,26 +99,45 @@ public class JSClosure: JSObject, 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 }) } - public init(_ body: @escaping ([JSValue]) -> JSValue) { + public init(_ body: @escaping (sending [JSValue]) -> JSValue, file: String = #fileID, line: UInt32 = #line) { // 1. Fill `id` as zero at first to access `self` to get `ObjectIdentifier`. super.init(id: 0) // 2. Create a new JavaScript function which calls the given Swift function. - hostFuncRef = JavaScriptHostFuncRef(bitPattern: Int32(ObjectIdentifier(self).hashValue)) - id = _create_function(hostFuncRef) + hostFuncRef = JavaScriptHostFuncRef(bitPattern: ObjectIdentifier(self)) + _id = withExtendedLifetime(JSString(file)) { file in + swjs_create_function(hostFuncRef, line, file.asInternalJSRef()) + } // 3. Retain the given body in static storage by `funcRef`. - Self.sharedClosures[hostFuncRef] = (self, body) + Self.sharedClosures.wrappedValue[hostFuncRef] = (self, body) + } + + @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) + @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 { + JSClosure(makeAsyncClosure(body)) } + #endif #if JAVASCRIPTKIT_WITHOUT_WEAKREFS deinit { @@ -93,6 +148,38 @@ public class JSClosure: JSObject, JSClosureProtocol { #endif } +#if compiler(>=5.5) && !hasFeature(Embedded) +@available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) +private func makeAsyncClosure( + _ body: sending @escaping (sending [JSValue]) async throws -> JSValue +) -> ((sending [JSValue]) -> JSValue) { + { arguments in + JSPromise { resolver in + // NOTE: The context is fully transferred to the unstructured task + // isolation but the compiler can't prove it yet, so we need to + // use `@unchecked Sendable` to make it compile with the Swift 6 mode. + struct Context: @unchecked Sendable { + let resolver: (JSPromise.Result) -> Void + let arguments: [JSValue] + let body: (sending [JSValue]) async throws -> JSValue + } + let context = Context(resolver: resolver, arguments: arguments, body: body) + Task { + do { + 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)) + } + } + } + }.jsValue() + } +} +#endif // MARK: - `JSClosure` mechanism note // @@ -128,24 +215,27 @@ public class JSClosure: JSObject, JSClosureProtocol { // │ │ │ // └─────────────────────┴──────────────────────────┘ +/// Returns true if the host function has been already released, otherwise false. @_cdecl("_call_host_function_impl") func _call_host_function_impl( _ hostFuncRef: JavaScriptHostFuncRef, - _ argv: UnsafePointer, _ argc: Int32, + _ argv: UnsafePointer, + _ argc: Int32, _ callbackFuncRef: JavaScriptObjectRef -) { - guard let (_, hostFunc) = JSClosure.sharedClosures[hostFuncRef] else { - fatalError("The function was already released") +) -> Bool { + guard let (_, hostFunc) = JSClosure.sharedClosures.wrappedValue[hostFuncRef] else { + return true } - let arguments = UnsafeBufferPointer(start: argv, count: Int(argc)).map { - $0.jsValue() + var arguments: [JSValue] = [] + for i in 0..=6.0) && hasFeature(Embedded) +// 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 { + + _call_host_function_impl(hostFuncRef, argv, argc, callbackFuncRef) +} + +@_expose(wasm, "swjs_free_host_function") +public func _swjs_free_host_function(_ hostFuncRef: JavaScriptHostFuncRef) { + _free_host_function_impl(hostFuncRef) } #endif diff --git a/Sources/JavaScriptKit/FundamentalObjects/JSFunction.swift b/Sources/JavaScriptKit/FundamentalObjects/JSFunction.swift index d9d66ff94..172483612 100644 --- a/Sources/JavaScriptKit/FundamentalObjects/JSFunction.swift +++ b/Sources/JavaScriptKit/FundamentalObjects/JSFunction.swift @@ -11,23 +11,38 @@ import _CJavaScriptKit /// ``` /// public class JSFunction: JSObject { - + #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. /// - arguments: Arguments to be passed to this function. /// - Returns: The result of this call. @discardableResult - public func callAsFunction(this: JSObject? = nil, arguments: [ConvertibleToJSValue]) -> JSValue { - try! invokeJSFunction(self, arguments: arguments, this: this) + public func callAsFunction(this: JSObject, arguments: [ConvertibleToJSValue]) -> JSValue { + invokeNonThrowingJSFunction(arguments: arguments, this: this).jsValue + } + + /// Call this function with given `arguments`. + /// - Parameters: + /// - arguments: Arguments to be passed to this function. + /// - Returns: The result of this call. + @discardableResult + public func callAsFunction(arguments: [ConvertibleToJSValue]) -> JSValue { + invokeNonThrowingJSFunction(arguments: arguments).jsValue } /// A variadic arguments version of `callAsFunction`. @discardableResult - public func callAsFunction(this: JSObject? = nil, _ arguments: ConvertibleToJSValue...) -> JSValue { + public func callAsFunction(this: JSObject, _ arguments: ConvertibleToJSValue...) -> JSValue { self(this: this, arguments: arguments) } + /// A variadic arguments version of `callAsFunction`. + @discardableResult + public func callAsFunction(_ arguments: ConvertibleToJSValue...) -> JSValue { + self(arguments: arguments) + } + /// Instantiate an object from this function as a constructor. /// /// Guaranteed to return an object because either: @@ -41,11 +56,16 @@ public class JSFunction: JSObject { public func new(arguments: [ConvertibleToJSValue]) -> JSObject { arguments.withRawJSValues { rawValues in rawValues.withUnsafeBufferPointer { bufferPointer in - return JSObject(id: _call_new(self.id, bufferPointer.baseAddress!, Int32(bufferPointer.count))) + JSObject(id: swjs_call_new(self.id, bufferPointer.baseAddress!, Int32(bufferPointer.count))) } } } + /// A variadic arguments version of `new`. + public func new(_ arguments: ConvertibleToJSValue...) -> JSObject { + new(arguments: arguments) + } + /// A modifier to call this function as a throwing function /// /// @@ -64,10 +84,19 @@ public class JSFunction: JSObject { public var `throws`: JSThrowingFunction { JSThrowingFunction(self) } + #endif - /// A variadic arguments version of `new`. - public func new(_ arguments: ConvertibleToJSValue...) -> JSObject { - new(arguments: arguments) + @discardableResult + public func callAsFunction(arguments: [JSValue]) -> JSValue { + invokeNonThrowingJSFunction(arguments: arguments).jsValue + } + + public func new(arguments: [JSValue]) -> JSObject { + arguments.withRawJSValues { rawValues in + rawValues.withUnsafeBufferPointer { bufferPointer in + JSObject(id: swjs_call_new(self.id, bufferPointer.baseAddress!, Int32(bufferPointer.count))) + } + } } @available(*, unavailable, message: "Please use JSClosure instead") @@ -75,39 +104,333 @@ public class JSFunction: JSObject { fatalError("unavailable") } - public override class func construct(from value: JSValue) -> Self? { - return value.function as? Self + override public var jsValue: JSValue { + .function(self) } - override public func jsValue() -> JSValue { - .function(self) + final func invokeNonThrowingJSFunction(arguments: [JSValue]) -> RawJSValue { + arguments.withRawJSValues { invokeNonThrowingJSFunction(rawValues: $0) } } -} -internal func invokeJSFunction(_ jsFunc: JSFunction, arguments: [ConvertibleToJSValue], this: JSObject?) throws -> JSValue { - let (result, isException) = arguments.withRawJSValues { rawValues in - rawValues.withUnsafeBufferPointer { bufferPointer -> (JSValue, Bool) in + final func invokeNonThrowingJSFunction(arguments: [JSValue], this: JSObject) -> RawJSValue { + arguments.withRawJSValues { invokeNonThrowingJSFunction(rawValues: $0, this: this) } + } + + #if !hasFeature(Embedded) + final func invokeNonThrowingJSFunction(arguments: [ConvertibleToJSValue]) -> RawJSValue { + arguments.withRawJSValues { invokeNonThrowingJSFunction(rawValues: $0) } + } + + final func invokeNonThrowingJSFunction(arguments: [ConvertibleToJSValue], this: JSObject) -> RawJSValue { + arguments.withRawJSValues { invokeNonThrowingJSFunction(rawValues: $0, this: this) } + } + #endif + + final private func invokeNonThrowingJSFunction(rawValues: [RawJSValue]) -> RawJSValue { + rawValues.withUnsafeBufferPointer { [id] bufferPointer in let argv = bufferPointer.baseAddress let argc = bufferPointer.count - var kindAndFlags = JavaScriptValueKindAndFlags() var payload1 = JavaScriptPayload1() var payload2 = JavaScriptPayload2() - if let thisId = this?.id { - _call_function_with_this(thisId, - jsFunc.id, argv, Int32(argc), - &kindAndFlags, &payload1, &payload2) - } else { - _call_function( - jsFunc.id, argv, Int32(argc), - &kindAndFlags, &payload1, &payload2 - ) - } + let resultBitPattern = swjs_call_function_no_catch( + id, + argv, + Int32(argc), + &payload1, + &payload2 + ) + let kindAndFlags = JavaScriptValueKindAndFlags(bitPattern: resultBitPattern) + assert(!kindAndFlags.isException) + let result = RawJSValue(kind: kindAndFlags.kind, payload1: payload1, payload2: payload2) + return result + } + } + + final private func invokeNonThrowingJSFunction(rawValues: [RawJSValue], this: JSObject) -> RawJSValue { + rawValues.withUnsafeBufferPointer { [id] bufferPointer in + let argv = bufferPointer.baseAddress + 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 kindAndFlags = JavaScriptValueKindAndFlags(bitPattern: resultBitPattern) + #if !hasFeature(Embedded) + assert(!kindAndFlags.isException) + #endif let result = RawJSValue(kind: kindAndFlags.kind, payload1: payload1, payload2: payload2) - return (result.jsValue(), kindAndFlags.isException) + return result } } - if isException { - throw result +} + +#if hasFeature(Embedded) +// Overloads of `callAsFunction(ConvertibleToJSValue...) -> JSValue` +// for 0 through 7 arguments for Embedded Swift. +// +// These are required because the `ConvertibleToJSValue...` version is not +// available in Embedded Swift due to lack of support for existentials. +// +// Once Embedded Swift supports parameter packs/variadic generics, we can +// replace all variants with a single method each that takes a generic pack. +extension JSFunction { + + @discardableResult + public func callAsFunction(this: JSObject) -> JSValue { + invokeNonThrowingJSFunction(arguments: [], this: this).jsValue + } + + @discardableResult + public func callAsFunction(this: JSObject, _ arg0: some ConvertibleToJSValue) -> JSValue { + invokeNonThrowingJSFunction(arguments: [arg0.jsValue], this: this).jsValue + } + + @discardableResult + public func callAsFunction( + this: JSObject, + _ arg0: some ConvertibleToJSValue, + _ arg1: some ConvertibleToJSValue + ) -> JSValue { + invokeNonThrowingJSFunction(arguments: [arg0.jsValue, arg1.jsValue], this: this).jsValue + } + + @discardableResult + public func callAsFunction( + this: JSObject, + _ arg0: some ConvertibleToJSValue, + _ arg1: some ConvertibleToJSValue, + _ arg2: some ConvertibleToJSValue + ) -> JSValue { + invokeNonThrowingJSFunction(arguments: [arg0.jsValue, arg1.jsValue, arg2.jsValue], this: this).jsValue + } + + @discardableResult + 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 + } + + @discardableResult + public func callAsFunction( + this: JSObject, + _ 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], + this: this + ).jsValue + } + + @discardableResult + public func callAsFunction( + this: JSObject, + _ arg0: some ConvertibleToJSValue, + _ arg1: some ConvertibleToJSValue, + _ arg2: some ConvertibleToJSValue, + _ arg3: some ConvertibleToJSValue, + _ arg4: some ConvertibleToJSValue, + _ arg5: some ConvertibleToJSValue + ) -> JSValue { + invokeNonThrowingJSFunction( + arguments: [arg0.jsValue, arg1.jsValue, arg2.jsValue, arg3.jsValue, arg4.jsValue, arg5.jsValue], + this: this + ).jsValue + } + + @discardableResult + public func callAsFunction( + this: JSObject, + _ arg0: some ConvertibleToJSValue, + _ arg1: some ConvertibleToJSValue, + _ arg2: some ConvertibleToJSValue, + _ arg3: some ConvertibleToJSValue, + _ arg4: some ConvertibleToJSValue, + _ 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 + } + + @discardableResult + public func callAsFunction(this: JSObject, arguments: [JSValue]) -> JSValue { + invokeNonThrowingJSFunction(arguments: arguments, this: this).jsValue + } + + @discardableResult + public func callAsFunction() -> JSValue { + invokeNonThrowingJSFunction(arguments: []).jsValue + } + + @discardableResult + public func callAsFunction(_ arg0: some ConvertibleToJSValue) -> JSValue { + invokeNonThrowingJSFunction(arguments: [arg0.jsValue]).jsValue + } + + @discardableResult + public func callAsFunction( + _ arg0: some ConvertibleToJSValue, + _ arg1: some ConvertibleToJSValue + ) -> JSValue { + invokeNonThrowingJSFunction(arguments: [arg0.jsValue, arg1.jsValue]).jsValue + } + + @discardableResult + public func callAsFunction( + _ arg0: some ConvertibleToJSValue, + _ arg1: some ConvertibleToJSValue, + _ arg2: some ConvertibleToJSValue + ) -> JSValue { + invokeNonThrowingJSFunction(arguments: [arg0.jsValue, arg1.jsValue, arg2.jsValue]).jsValue + } + + @discardableResult + public func callAsFunction( + _ arg0: some ConvertibleToJSValue, + _ arg1: some ConvertibleToJSValue, + _ arg2: some ConvertibleToJSValue, + _ arg3: some ConvertibleToJSValue + ) -> JSValue { + invokeNonThrowingJSFunction(arguments: [arg0.jsValue, arg1.jsValue, arg2.jsValue, arg3.jsValue]).jsValue + } + + @discardableResult + 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 + } + + @discardableResult + public func callAsFunction( + _ arg0: some ConvertibleToJSValue, + _ arg1: some ConvertibleToJSValue, + _ arg2: some ConvertibleToJSValue, + _ arg3: some ConvertibleToJSValue, + _ arg4: some ConvertibleToJSValue, + _ arg5: some ConvertibleToJSValue + ) -> JSValue { + invokeNonThrowingJSFunction(arguments: [ + arg0.jsValue, arg1.jsValue, arg2.jsValue, arg3.jsValue, arg4.jsValue, arg5.jsValue, + ]).jsValue + } + + @discardableResult + public func callAsFunction( + _ arg0: some ConvertibleToJSValue, + _ arg1: some ConvertibleToJSValue, + _ arg2: some ConvertibleToJSValue, + _ arg3: some ConvertibleToJSValue, + _ arg4: some ConvertibleToJSValue, + _ arg5: some ConvertibleToJSValue, + _ arg6: some ConvertibleToJSValue + ) -> JSValue { + invokeNonThrowingJSFunction(arguments: [ + arg0.jsValue, arg1.jsValue, arg2.jsValue, arg3.jsValue, arg4.jsValue, arg5.jsValue, arg6.jsValue, + ]).jsValue + } + + public func new() -> JSObject { + new(arguments: []) + } + + public func new(_ arg0: some ConvertibleToJSValue) -> JSObject { + new(arguments: [arg0.jsValue]) + } + + public func new( + _ arg0: some ConvertibleToJSValue, + _ arg1: some ConvertibleToJSValue + ) -> JSObject { + new(arguments: [arg0.jsValue, arg1.jsValue]) + } + + public func new( + _ arg0: some ConvertibleToJSValue, + _ arg1: some ConvertibleToJSValue, + _ arg2: some ConvertibleToJSValue + ) -> JSObject { + new(arguments: [arg0.jsValue, arg1.jsValue, arg2.jsValue]) + } + + public func new( + _ arg0: some ConvertibleToJSValue, + _ arg1: some ConvertibleToJSValue, + _ arg2: some ConvertibleToJSValue, + _ arg3: some ConvertibleToJSValue + ) -> JSObject { + new(arguments: [arg0.jsValue, arg1.jsValue, arg2.jsValue, arg3.jsValue]) + } + + public func new( + _ arg0: some ConvertibleToJSValue, + _ arg1: some ConvertibleToJSValue, + _ arg2: some ConvertibleToJSValue, + _ arg3: some ConvertibleToJSValue, + _ arg4: some ConvertibleToJSValue + ) -> JSObject { + new(arguments: [arg0.jsValue, arg1.jsValue, arg2.jsValue, arg3.jsValue, arg4.jsValue]) + } + + public func new( + _ arg0: some ConvertibleToJSValue, + _ arg1: some ConvertibleToJSValue, + _ arg2: some ConvertibleToJSValue, + _ arg3: some ConvertibleToJSValue, + _ arg4: some ConvertibleToJSValue, + _ arg5: some ConvertibleToJSValue + ) -> JSObject { + new(arguments: [arg0.jsValue, arg1.jsValue, arg2.jsValue, arg3.jsValue, arg4.jsValue, arg5.jsValue]) + } + + public func new( + _ arg0: some ConvertibleToJSValue, + _ arg1: some ConvertibleToJSValue, + _ arg2: some ConvertibleToJSValue, + _ arg3: some ConvertibleToJSValue, + _ arg4: some ConvertibleToJSValue, + _ arg5: some ConvertibleToJSValue, + _ arg6: some ConvertibleToJSValue + ) -> JSObject { + new(arguments: [ + arg0.jsValue, arg1.jsValue, arg2.jsValue, arg3.jsValue, arg4.jsValue, arg5.jsValue, arg6.jsValue, + ]) + } +} +#endif + +internal struct JavaScriptValueKindAndFlags { + static var errorBit: UInt32 { 1 << 31 } + let kind: JavaScriptValueKind + let isException: Bool + + init(bitPattern: UInt32) { + self.kind = JavaScriptValueKind(rawValue: bitPattern & ~Self.errorBit)! + self.isException = (bitPattern & Self.errorBit) != 0 } - return result } diff --git a/Sources/JavaScriptKit/FundamentalObjects/JSObject.swift b/Sources/JavaScriptKit/FundamentalObjects/JSObject.swift index 3bafe60b5..12dbf9e02 100644 --- a/Sources/JavaScriptKit/FundamentalObjects/JSObject.swift +++ b/Sources/JavaScriptKit/FundamentalObjects/JSObject.swift @@ -15,12 +15,69 @@ 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 { - internal var id: JavaScriptObjectRef - init(id: JavaScriptObjectRef) { - self.id = id +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) + package let ownerTid: Int32 + #endif + + @_spi(JSObject_id) + @inlinable + public var id: JavaScriptObjectRef { _id } + + @_spi(JSObject_id) + public init(id: JavaScriptObjectRef) { + self._id = id + #if compiler(>=6.1) && _runtime(_multithreaded) + self.ownerTid = swjs_get_worker_thread_id_cached() + #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. + /// + /// - Parameter hint: A string to provide additional context for debugging. + /// + /// NOTE: Accessing a `JSObject` from a thread other than the thread it was created on + /// is a programmer error and will result in a runtime assertion failure because JavaScript + /// 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())" + ) + #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())" + ) + #endif } + #if !hasFeature(Embedded) /// Returns the `name` member method binding this object as `this` context. /// /// e.g. @@ -39,12 +96,31 @@ public class JSObject: Equatable { } } + /// Returns the `name` member method binding this object as `this` context. + /// + /// e.g. + /// ```swift + /// let document = JSObject.global.document.object! + /// let divElement = document.createElement!("div") + /// ``` + /// + /// - Parameter name: The name of this object's member to access. + /// - Returns: The `name` member method binding this object as `this` context. + @_disfavoredOverload + public subscript(_ name: JSString) -> ((ConvertibleToJSValue...) -> JSValue)? { + guard let function = self[name].function else { return nil } + return { (arguments: ConvertibleToJSValue...) in + function(this: self, arguments: arguments) + } + } + /// A convenience method of `subscript(_ name: String) -> ((ConvertibleToJSValue...) -> JSValue)?` /// to access the member through Dynamic Member Lookup. @_disfavoredOverload public subscript(dynamicMember name: String) -> ((ConvertibleToJSValue...) -> JSValue)? { self[name] } + #endif /// A convenience method of `subscript(_ name: String) -> JSValue` /// to access the member through Dynamic Member Lookup. @@ -57,26 +133,59 @@ public class JSObject: Equatable { /// - Parameter name: The name of this object's member to access. /// - Returns: The value of the `name` member of this object. public subscript(_ name: String) -> JSValue { - get { getJSValue(this: self, name: JSString(name)) } - set { setJSValue(this: self, name: JSString(name), value: newValue) } + get { + assertOnOwnerThread(hint: "reading '\(name)' property") + return getJSValue(this: self, name: JSString(name)) + } + set { + assertOnOwnerThread(hint: "writing '\(name)' property") + setJSValue(this: self, name: JSString(name), value: newValue) + } } /// Access the `name` member dynamically through JavaScript and Swift runtime bridge library. /// - Parameter name: The name of this object's member to access. /// - Returns: The value of the `name` member of this object. public subscript(_ name: JSString) -> JSValue { - get { getJSValue(this: self, name: name) } - set { setJSValue(this: self, name: name, value: newValue) } + get { + assertOnOwnerThread(hint: "reading '<>' property") + return getJSValue(this: self, name: name) + } + set { + assertOnOwnerThread(hint: "writing '<>' property") + setJSValue(this: self, name: name, value: newValue) + } } /// Access the `index` member dynamically through JavaScript and Swift runtime bridge library. /// - Parameter index: The index of this object's member to access. /// - Returns: The value of the `index` member of this object. public subscript(_ index: Int) -> JSValue { - get { getJSValue(this: self, index: Int32(index)) } - set { setJSValue(this: self, index: Int32(index), value: newValue) } + get { + assertOnOwnerThread(hint: "reading '\(index)' property") + return getJSValue(this: self, index: Int32(index)) + } + set { + assertOnOwnerThread(hint: "writing '\(index)' property") + setJSValue(this: self, index: Int32(index), value: newValue) + } } + /// Access the `symbol` member dynamically through JavaScript and Swift runtime bridge library. + /// - Parameter symbol: The name of this object's member to access. + /// - Returns: The value of the `name` member of this object. + public subscript(_ name: JSSymbol) -> JSValue { + get { + assertOnOwnerThread(hint: "reading '<>' property") + return getJSValue(this: self, symbol: name) + } + set { + assertOnOwnerThread(hint: "writing '<>' property") + setJSValue(this: self, symbol: name, value: newValue) + } + } + + #if !hasFeature(Embedded) /// A modifier to call methods as throwing methods capturing `this` /// /// @@ -94,24 +203,38 @@ public class JSObject: Equatable { /// let animal = JSObject.global.animal.object! /// try animal.throwing.validateAge!() /// ``` - public var `throwing`: JSThrowingObject { + public var throwing: JSThrowingObject { JSThrowingObject(self) } + #endif /// Return `true` if this value is an instance of the passed `constructor` function. /// - Parameter constructor: The constructor function to check. /// - Returns: The result of `instanceof` in the JavaScript environment. public func isInstanceOf(_ constructor: JSFunction) -> Bool { - _instanceof(id, constructor.id) + assertOnOwnerThread(hint: "calling 'isInstanceOf'") + return swjs_instanceof(id, constructor.id) } static let _JS_Predef_Value_Global: JavaScriptObjectRef = 0 /// A `JSObject` of the global scope object. /// This allows access to the global properties and global names by accessing the `JSObject` returned. - public static let global = JSObject(id: _JS_Predef_Value_Global) + public static var global: JSObject { return _global.wrappedValue } + private static let _global = LazyThreadLocal(initialize: { + JSObject(id: _JS_Predef_Value_Global) + }) - deinit { _release(id) } + deinit { + #if compiler(>=6.1) && _runtime(_multithreaded) + if ownerTid != swjs_get_worker_thread_id_cached() { + // If the object is not owned by the current thread + swjs_release_remote(ownerTid, id) + return + } + #endif + swjs_release(id) + } /// Returns a Boolean value indicating whether two values point to same objects. /// @@ -119,14 +242,30 @@ public class JSObject: Equatable { /// - lhs: A object to compare. /// - rhs: Another object to compare. public static func == (lhs: JSObject, rhs: JSObject) -> Bool { + assertSameOwnerThread(lhs: lhs, rhs: rhs, hint: "comparing two JSObjects for equality") return lhs.id == rhs.id } - public class func construct(from value: JSValue) -> Self? { - return value.object as? Self + public static func construct(from value: JSValue) -> Self? { + switch value { + case .boolean, + .string, + .number, + .null, + .undefined: + return nil + case .object(let object): + return object as? Self + case .function(let function): + return function as? Self + case .symbol(let symbol): + return symbol as? Self + case .bigInt(let bigInt): + return bigInt as? Self + } } - public func jsValue() -> JSValue { + public var jsValue: JSValue { .object(self) } } @@ -146,6 +285,7 @@ extension JSObject: Hashable { } } +#if !hasFeature(Embedded) /// A `JSObject` wrapper that enables throwing method calls capturing `this`. /// Exceptions produced by JavaScript functions will be thrown as `JSValue`. @dynamicMemberLookup @@ -173,3 +313,105 @@ public class JSThrowingObject { self[name] } } +#endif + +#if hasFeature(Embedded) +// Overloads of `JSObject.subscript(_ name: String) -> ((ConvertibleToJSValue...) -> JSValue)?` +// for 0 through 7 arguments for Embedded Swift. +// +// These are required because the `ConvertibleToJSValue...` subscript is not +// available in Embedded Swift due to lack of support for existentials. +// +// NOTE: Once Embedded Swift supports parameter packs/variadic generics, we can +// replace all of these with a single method that takes a generic pack. +extension JSObject { + @_disfavoredOverload + public subscript(dynamicMember name: String) -> (() -> JSValue)? { + self[name].function.map { function in + { function(this: self) } + } + } + + @_disfavoredOverload + public subscript(dynamicMember name: String) -> ((A0) -> JSValue)? { + self[name].function.map { function in + { function(this: self, $0) } + } + } + + @_disfavoredOverload + public subscript< + A0: ConvertibleToJSValue, + A1: ConvertibleToJSValue + >(dynamicMember name: String) -> ((A0, A1) -> JSValue)? { + self[name].function.map { function in + { function(this: self, $0, $1) } + } + } + + @_disfavoredOverload + public subscript< + A0: ConvertibleToJSValue, + A1: ConvertibleToJSValue, + A2: ConvertibleToJSValue + >(dynamicMember name: String) -> ((A0, A1, A2) -> JSValue)? { + self[name].function.map { function in + { function(this: self, $0, $1, $2) } + } + } + + @_disfavoredOverload + public subscript< + A0: ConvertibleToJSValue, + A1: ConvertibleToJSValue, + A2: ConvertibleToJSValue, + A3: ConvertibleToJSValue + >(dynamicMember name: String) -> ((A0, A1, A2, A3) -> JSValue)? { + self[name].function.map { function in + { function(this: self, $0, $1, $2, $3) } + } + } + + @_disfavoredOverload + public subscript< + A0: ConvertibleToJSValue, + A1: ConvertibleToJSValue, + A2: ConvertibleToJSValue, + A3: ConvertibleToJSValue, + A4: ConvertibleToJSValue + >(dynamicMember name: String) -> ((A0, A1, A2, A3, A4) -> JSValue)? { + self[name].function.map { function in + { function(this: self, $0, $1, $2, $3, $4) } + } + } + + @_disfavoredOverload + public subscript< + A0: ConvertibleToJSValue, + A1: ConvertibleToJSValue, + A2: ConvertibleToJSValue, + A3: ConvertibleToJSValue, + A4: ConvertibleToJSValue, + A5: ConvertibleToJSValue + >(dynamicMember name: String) -> ((A0, A1, A2, A3, A4, A5) -> JSValue)? { + self[name].function.map { function in + { function(this: self, $0, $1, $2, $3, $4, $5) } + } + } + + @_disfavoredOverload + public subscript< + A0: ConvertibleToJSValue, + A1: ConvertibleToJSValue, + A2: ConvertibleToJSValue, + A3: ConvertibleToJSValue, + A4: ConvertibleToJSValue, + A5: ConvertibleToJSValue, + A6: ConvertibleToJSValue + >(dynamicMember name: String) -> ((A0, A1, A2, A3, A4, A5, A6) -> JSValue)? { + self[name].function.map { function in + { function(this: self, $0, $1, $2, $3, $4, $5, $6) } + } + } +} +#endif diff --git a/Sources/JavaScriptKit/FundamentalObjects/JSString.swift b/Sources/JavaScriptKit/FundamentalObjects/JSString.swift index 5621793d0..f084ffc81 100644 --- a/Sources/JavaScriptKit/FundamentalObjects/JSString.swift +++ b/Sources/JavaScriptKit/FundamentalObjects/JSString.swift @@ -19,24 +19,24 @@ public struct JSString: LosslessStringConvertible, Equatable { /// The initializers of this type must initialize `jsRef` or `buffer`. /// And the uninitialized one will be lazily initialized class Guts { - var shouldDealocateRef: Bool = false + var shouldDeallocateRef: Bool = false lazy var jsRef: JavaScriptObjectRef = { - self.shouldDealocateRef = true + self.shouldDeallocateRef = true return buffer.withUTF8 { bufferPtr in - return _decode_string(bufferPtr.baseAddress!, Int32(bufferPtr.count)) + return swjs_decode_string(bufferPtr.baseAddress!, Int32(bufferPtr.count)) } }() lazy var buffer: String = { var bytesRef: JavaScriptObjectRef = 0 - let bytesLength = Int(_encode_string(jsRef, &bytesRef)) + let bytesLength = Int(swjs_encode_string(jsRef, &bytesRef)) // +1 for null terminator - let buffer = malloc(Int(bytesLength + 1))!.assumingMemoryBound(to: UInt8.self) + let buffer = UnsafeMutablePointer.allocate(capacity: bytesLength + 1) defer { - free(buffer) - _release(bytesRef) + buffer.deallocate() + swjs_release(bytesRef) } - _load_string(bytesRef, buffer) + swjs_load_string(bytesRef, buffer) buffer[bytesLength] = 0 return String(decodingCString: UnsafePointer(buffer), as: UTF8.self) }() @@ -47,12 +47,12 @@ public struct JSString: LosslessStringConvertible, Equatable { init(from jsRef: JavaScriptObjectRef) { self.jsRef = jsRef - self.shouldDealocateRef = true + self.shouldDeallocateRef = true } deinit { - guard shouldDealocateRef else { return } - _release(jsRef) + guard shouldDeallocateRef else { return } + swjs_release(jsRef) } } @@ -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,7 +91,6 @@ extension JSString: ExpressibleByStringLiteral { } } - // MARK: - Internal Helpers extension JSString { @@ -97,7 +100,9 @@ extension JSString { func withRawJSValue(_ body: (RawJSValue) -> T) -> T { let rawValue = RawJSValue( - kind: .string, payload1: guts.jsRef, payload2: 0 + kind: .string, + payload1: guts.jsRef, + payload2: 0 ) return body(rawValue) } diff --git a/Sources/JavaScriptKit/FundamentalObjects/JSSymbol.swift b/Sources/JavaScriptKit/FundamentalObjects/JSSymbol.swift new file mode 100644 index 000000000..a9461317b --- /dev/null +++ b/Sources/JavaScriptKit/FundamentalObjects/JSSymbol.swift @@ -0,0 +1,69 @@ +import _CJavaScriptKit + +private let _Symbol = LazyThreadLocal(initialize: { JSObject.global.Symbol.function! }) +private var Symbol: JSFunction { _Symbol.wrappedValue } + +/// A wrapper around [the JavaScript `Symbol` +/// class](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Symbol) +/// that exposes its properties in a type-safe and Swifty way. +public class JSSymbol: JSObject { + public var name: String? { self["description"].string } + + public init(_ description: JSString) { + // can’t do `self =` so we have to get the ID manually + let result = Symbol.invokeNonThrowingJSFunction(arguments: [description.jsValue]) + precondition(result.kind == .symbol) + super.init(id: UInt32(result.payload1)) + } + @_disfavoredOverload + public convenience init(_ description: String) { + self.init(JSString(description)) + } + + override init(id: JavaScriptObjectRef) { + 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! + } + + @_disfavoredOverload + public static func `for`(key: String) -> JSSymbol { + Symbol.for!(key).symbol! + } + + public static func key(for symbol: JSSymbol) -> JSString? { + Symbol.keyFor!(symbol).jsString + } + + @_disfavoredOverload + public static func key(for symbol: JSSymbol) -> String? { + Symbol.keyFor!(symbol).string + } + + override public var jsValue: JSValue { + .symbol(self) + } +} + +extension JSSymbol { + public static var asyncIterator: JSSymbol! { Symbol.asyncIterator.symbol } + public static var hasInstance: JSSymbol! { Symbol.hasInstance.symbol } + public static var isConcatSpreadable: JSSymbol! { Symbol.isConcatSpreadable.symbol } + public static var iterator: JSSymbol! { Symbol.iterator.symbol } + public static var match: JSSymbol! { Symbol.match.symbol } + public static var matchAll: JSSymbol! { Symbol.matchAll.symbol } + public static var replace: JSSymbol! { Symbol.replace.symbol } + public static var search: JSSymbol! { Symbol.search.symbol } + public static var species: JSSymbol! { Symbol.species.symbol } + public static var split: JSSymbol! { Symbol.split.symbol } + public static var toPrimitive: JSSymbol! { Symbol.toPrimitive.symbol } + public static var toStringTag: JSSymbol! { Symbol.toStringTag.symbol } + public static var unscopables: JSSymbol! { Symbol.unscopables.symbol } +} diff --git a/Sources/JavaScriptKit/FundamentalObjects/JSThrowingFunction.swift b/Sources/JavaScriptKit/FundamentalObjects/JSThrowingFunction.swift index 0fe96d318..aee17fd69 100644 --- a/Sources/JavaScriptKit/FundamentalObjects/JSThrowingFunction.swift +++ b/Sources/JavaScriptKit/FundamentalObjects/JSThrowingFunction.swift @@ -1,6 +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 { @@ -36,21 +36,30 @@ public class JSThrowingFunction { /// - Parameter arguments: Arguments to be passed to this constructor function. /// - Returns: A new instance of this constructor. public func new(arguments: [ConvertibleToJSValue]) throws -> JSObject { - try arguments.withRawJSValues { rawValues -> Result in + try arguments.withRawJSValues { rawValues -> Result in rawValues.withUnsafeBufferPointer { bufferPointer in let argv = bufferPointer.baseAddress let argc = bufferPointer.count - var exceptionKind = JavaScriptValueKindAndFlags() + var exceptionRawKind = JavaScriptRawValueKindAndFlags() var exceptionPayload1 = JavaScriptPayload1() var exceptionPayload2 = JavaScriptPayload2() - let resultObj = _call_throwing_new( - self.base.id, argv, Int32(argc), - &exceptionKind, &exceptionPayload1, &exceptionPayload2 + let resultObj = swjs_call_throwing_new( + 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) - return .failure(exception.jsValue()) + let exception = RawJSValue( + kind: exceptionKind.kind, + payload1: exceptionPayload1, + payload2: exceptionPayload2 + ) + return .failure(JSException(exception.jsValue)) } return .success(JSObject(id: resultObj)) } @@ -62,3 +71,47 @@ public class JSThrowingFunction { try new(arguments: arguments) } } + +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 + let argv = bufferPointer.baseAddress + let argc = bufferPointer.count + let kindAndFlags: JavaScriptValueKindAndFlags + var payload1 = JavaScriptPayload1() + var payload2 = JavaScriptPayload2() + if let thisId = this?.id { + let resultBitPattern = swjs_call_function_with_this( + thisId, + id, + argv, + Int32(argc), + &payload1, + &payload2 + ) + kindAndFlags = JavaScriptValueKindAndFlags(bitPattern: resultBitPattern) + } else { + let resultBitPattern = swjs_call_function( + id, + argv, + Int32(argc), + &payload1, + &payload2 + ) + kindAndFlags = JavaScriptValueKindAndFlags(bitPattern: resultBitPattern) + } + let result = RawJSValue(kind: kindAndFlags.kind, payload1: payload1, payload2: payload2) + return (result.jsValue, kindAndFlags.isException) + } + } + if isException { + throw JSException(result) + } + return result +} +#endif diff --git a/Sources/JavaScriptKit/JSBridgedType.swift b/Sources/JavaScriptKit/JSBridgedType.swift index acd1fa6ef..92739079e 100644 --- a/Sources/JavaScriptKit/JSBridgedType.swift +++ b/Sources/JavaScriptKit/JSBridgedType.swift @@ -1,27 +1,22 @@ /// Use this protocol when your type has no single JavaScript class. /// For example, a union type of multiple classes or primitive values. public protocol JSBridgedType: JSValueCompatible, CustomStringConvertible { - /// This is the value your class wraps. - var value: JSValue { get } - /// If your class is incompatible with the provided value, return `nil`. init?(from value: JSValue) } extension JSBridgedType { public static func construct(from value: JSValue) -> Self? { - return Self.init(from: value) + Self(from: value) } - public func jsValue() -> JSValue { value } - - public var description: String { value.description } + public var description: String { jsValue.description } } /// Conform to this protocol when your Swift class wraps a JavaScript class. public protocol JSBridgedClass: JSBridgedType { /// The constructor function for the JavaScript class - static var constructor: JSFunction { get } + static var constructor: JSFunction? { get } /// The JavaScript object wrapped by this instance. /// You may assume that `jsObject instanceof Self.constructor == true` @@ -33,14 +28,15 @@ public protocol JSBridgedClass: JSBridgedType { } extension JSBridgedClass { - public var value: JSValue { jsObject.jsValue() } + public var jsValue: JSValue { jsObject.jsValue } + public init?(from value: JSValue) { guard let object = value.object else { return nil } self.init(from: object) } public init?(from object: JSObject) { - guard object.isInstanceOf(Self.constructor) else { return nil } + 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 new file mode 100644 index 000000000..8783d808b --- /dev/null +++ b/Sources/JavaScriptKit/JSException.swift @@ -0,0 +1,34 @@ +/// `JSException` is a wrapper that handles exceptions thrown during JavaScript execution as Swift +/// `Error` objects. +/// When a JavaScript function throws an exception, it's wrapped as a `JSException` and propagated +/// through Swift's error handling mechanism. +/// +/// Example: +/// ```swift +/// do { +/// try jsFunction.throws() +/// } catch let error as JSException { +/// // Access the value thrown from JavaScript +/// let jsErrorValue = error.thrownValue +/// } +/// ``` +public struct JSException: Error, Equatable { + /// The value thrown from JavaScript. + /// This can be any JavaScript value (error object, string, number, etc.). + public var thrownValue: JSValue { + return _thrownValue + } + + /// The actual JavaScript value that was thrown. + /// + /// Marked as `nonisolated(unsafe)` to satisfy `Sendable` requirement + /// from `Error` protocol. + private nonisolated(unsafe) let _thrownValue: JSValue + + /// Initializes a new JSException instance with a value thrown from JavaScript. + /// + /// Only available within the package. + package init(_ thrownValue: JSValue) { + self._thrownValue = thrownValue + } +} diff --git a/Sources/JavaScriptKit/JSValue.swift b/Sources/JavaScriptKit/JSValue.swift index 34bb78232..b9f8dd4a7 100644 --- a/Sources/JavaScriptKit/JSValue.swift +++ b/Sources/JavaScriptKit/JSValue.swift @@ -10,12 +10,14 @@ public enum JSValue: Equatable { case null case undefined case function(JSFunction) + case symbol(JSSymbol) + case bigInt(JSBigInt) /// Returns the `Bool` value of this JS value if its type is boolean. /// 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 } } @@ -35,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 } } @@ -44,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 } } @@ -53,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 } } @@ -62,7 +64,25 @@ 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 + } + } + + /// Returns the `JSSymbol` of this JS value if its type is function. + /// If not, returns `nil`. + public var symbol: JSSymbol? { + switch self { + case .symbol(let symbol): return symbol + default: return nil + } + } + + /// Returns the `JSBigInt` of this JS value if its type is function. + /// If not, returns `nil`. + public var bigInt: JSBigInt? { + switch self { + case .bigInt(let bigInt): return bigInt default: return nil } } @@ -80,30 +100,37 @@ public enum JSValue: Equatable { } } +/// JSValue is intentionally not `Sendable` because accessing a JSValue living in a different +/// thread is invalid. Although there are some cases where Swift allows sending a non-Sendable +/// values to other isolation domains, not conforming `Sendable` is still useful to prevent +/// accidental misuse. +@available(*, unavailable) +extension JSValue: Sendable {} + 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. public subscript(dynamicMember name: String) -> ((ConvertibleToJSValue...) -> JSValue) { object![dynamicMember: name]! } + #endif /// An unsafe convenience method of `JSObject.subscript(_ index: Int) -> JSValue` /// - Precondition: `self` must be a JavaScript Object. 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. public subscript(_ index: Int) -> JSValue { get { object![index] } - set { object![index] = newValue } + nonmutating set { object![index] = newValue } } } -extension JSValue: Swift.Error {} - extension JSValue { public func fromJSValue() -> Type? where Type: ConstructibleFromJSValue { return Type.construct(from: self) @@ -111,7 +138,6 @@ extension JSValue { } extension JSValue { - public static func string(_ value: String) -> JSValue { .string(JSString(value)) } @@ -130,22 +156,27 @@ extension JSValue { /// into below code. /// /// ```swift - /// let eventListenter = JSClosure { _ in + /// let eventListener = JSClosure { _ in /// ... /// return JSValue.undefined /// } /// - /// button.addEventListener!("click", JSValue.function(eventListenter)) + /// button.addEventListener!("click", JSValue.function(eventListener)) /// ... - /// button.removeEventListener!("click", JSValue.function(eventListenter)) - /// eventListenter.release() + /// button.removeEventListener!("click", JSValue.function(eventListener)) + /// eventListener.release() /// ``` @available(*, deprecated, message: "Please create JSClosure directly and manage its lifetime manually.") 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.") + @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) } @@ -170,53 +201,83 @@ extension JSValue: ExpressibleByFloatLiteral { } extension JSValue: ExpressibleByNilLiteral { - public init(nilLiteral: ()) { + public init(nilLiteral _: ()) { self = .null } } public func getJSValue(this: JSObject, name: JSString) -> JSValue { var rawValue = RawJSValue() - _get_prop(this.id, name.asInternalJSRef(), - &rawValue.kind, - &rawValue.payload1, &rawValue.payload2) - return rawValue.jsValue() + let rawBitPattern = swjs_get_prop( + this.id, + name.asInternalJSRef(), + &rawValue.payload1, + &rawValue.payload2 + ) + rawValue.kind = unsafeBitCast(rawBitPattern, to: JavaScriptValueKind.self) + return rawValue.jsValue } public func setJSValue(this: JSObject, name: JSString, value: JSValue) { value.withRawJSValue { rawValue in - _set_prop(this.id, name.asInternalJSRef(), rawValue.kind, rawValue.payload1, rawValue.payload2) + swjs_set_prop(this.id, name.asInternalJSRef(), rawValue.kind, rawValue.payload1, rawValue.payload2) } } public func getJSValue(this: JSObject, index: Int32) -> JSValue { var rawValue = RawJSValue() - _get_subscript(this.id, index, - &rawValue.kind, - &rawValue.payload1, &rawValue.payload2) - return rawValue.jsValue() + let rawBitPattern = swjs_get_subscript( + this.id, + index, + &rawValue.payload1, + &rawValue.payload2 + ) + rawValue.kind = unsafeBitCast(rawBitPattern, to: JavaScriptValueKind.self) + return rawValue.jsValue } public func setJSValue(this: JSObject, index: Int32, value: JSValue) { value.withRawJSValue { rawValue in - _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 + ) + rawValue.kind = unsafeBitCast(rawBitPattern, to: JavaScriptValueKind.self) + return rawValue.jsValue +} + +public func setJSValue(this: JSObject, symbol: JSSymbol, value: JSValue) { + value.withRawJSValue { rawValue in + swjs_set_prop(this.id, symbol.id, rawValue.kind, rawValue.payload1, rawValue.payload2) } } 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. + /// 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. public func isInstanceOf(_ constructor: JSFunction) -> Bool { switch self { - case .boolean, .string, .number, .null, .undefined: + 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) } } @@ -224,19 +285,93 @@ extension JSValue { extension JSValue: CustomStringConvertible { public var description: String { - switch self { - case let .boolean(boolean): - return boolean.description - case .string(let string): - return string.description - case .number(let number): - return number.description - case .object(let object), .function(let object as JSObject): - return object.toString!().fromJSValue()! - case .null: - return "null" - case .undefined: - return "undefined" - } + // per https://tc39.es/ecma262/multipage/text-processing.html#sec-string-constructor-string-value + // this always returns a string + JSObject.global.String.function!(self).string! + } +} + +#if hasFeature(Embedded) +// Overloads of `JSValue.subscript(dynamicMember name: String) -> ((ConvertibleToJSValue...) -> JSValue)` +// for 0 through 7 arguments for Embedded Swift. +// +// These are required because the `ConvertibleToJSValue...` subscript is not +// available in Embedded Swift due to lack of support for existentials. +// +// Note: Once Embedded Swift supports parameter packs/variadic generics, we can +// replace all of these with a single method that takes a generic pack. +extension JSValue { + @_disfavoredOverload + public subscript(dynamicMember name: String) -> (() -> JSValue) { + object![dynamicMember: name]! + } + + @_disfavoredOverload + public subscript(dynamicMember name: String) -> ((A0) -> JSValue) { + object![dynamicMember: name]! + } + + @_disfavoredOverload + public subscript< + A0: ConvertibleToJSValue, + A1: ConvertibleToJSValue + >(dynamicMember name: String) -> ((A0, A1) -> JSValue) { + object![dynamicMember: name]! + } + + @_disfavoredOverload + public subscript< + A0: ConvertibleToJSValue, + A1: ConvertibleToJSValue, + A2: ConvertibleToJSValue + >(dynamicMember name: String) -> ((A0, A1, A2) -> JSValue) { + object![dynamicMember: name]! + } + + @_disfavoredOverload + public subscript< + A0: ConvertibleToJSValue, + A1: ConvertibleToJSValue, + A2: ConvertibleToJSValue, + A3: ConvertibleToJSValue + >(dynamicMember name: String) -> ((A0, A1, A2, A3) -> JSValue) { + object![dynamicMember: name]! + } + + @_disfavoredOverload + public subscript< + A0: ConvertibleToJSValue, + A1: ConvertibleToJSValue, + A2: ConvertibleToJSValue, + A3: ConvertibleToJSValue, + A4: ConvertibleToJSValue + >(dynamicMember name: String) -> ((A0, A1, A2, A3, A4) -> JSValue) { + object![dynamicMember: name]! + } + + @_disfavoredOverload + public subscript< + A0: ConvertibleToJSValue, + A1: ConvertibleToJSValue, + A2: ConvertibleToJSValue, + A3: ConvertibleToJSValue, + A4: ConvertibleToJSValue, + A5: ConvertibleToJSValue + >(dynamicMember name: String) -> ((A0, A1, A2, A3, A4, A5) -> JSValue) { + object![dynamicMember: name]! + } + + @_disfavoredOverload + public subscript< + A0: ConvertibleToJSValue, + A1: ConvertibleToJSValue, + A2: ConvertibleToJSValue, + A3: ConvertibleToJSValue, + A4: ConvertibleToJSValue, + A5: ConvertibleToJSValue, + A6: ConvertibleToJSValue + >(dynamicMember name: String) -> ((A0, A1, A2, A3, A4, A5, A6) -> JSValue) { + object![dynamicMember: name]! } } +#endif diff --git a/Sources/JavaScriptKit/JSValueDecoder.swift b/Sources/JavaScriptKit/JSValueDecoder.swift index c70dd8e27..08e29915c 100644 --- a/Sources/JavaScriptKit/JSValueDecoder.swift +++ b/Sources/JavaScriptKit/JSValueDecoder.swift @@ -1,3 +1,4 @@ +#if !hasFeature(Embedded) private struct _Decoder: Decoder { fileprivate let node: JSValue @@ -34,9 +35,8 @@ private struct _Decoder: Decoder { } private enum Object { - static let ref = JSObject.global.Object.object! static func keys(_ object: JSObject) -> [String] { - let keys = ref.keys!(object).array! + let keys = JSObject.constructor.keys!(object).array! return keys.map { $0.string! } } } @@ -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) } @@ -248,3 +252,4 @@ public class JSValueDecoder { return try T(from: decoder) } } +#endif 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.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 new file mode 100644 index 000000000..e92ca32ac --- /dev/null +++ b/Sources/JavaScriptKit/ThreadLocal.swift @@ -0,0 +1,129 @@ +#if arch(wasm32) +#if canImport(wasi_pthread) +import wasi_pthread +#endif +#elseif canImport(Darwin) +import Darwin +#elseif canImport(Glibc) +import Glibc +#else +#error("Unsupported platform") +#endif + +/// A property wrapper that provides thread-local storage for a value. +/// +/// 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) + /// The wrapped value stored in the thread-local storage. + /// The initial value is `nil` for each thread. + var wrappedValue: Value? { + get { + guard let pointer = pthread_getspecific(key) else { + return nil + } + return fromPointer(pointer) + } + set { + if let oldPointer = pthread_getspecific(key) { + release(oldPointer) + } + if let newValue = newValue { + let pointer = toPointer(newValue) + pthread_setspecific(key, pointer) + } + } + } + + private let key: pthread_key_t + private let toPointer: @Sendable (Value) -> UnsafeMutableRawPointer + private let fromPointer: @Sendable (UnsafeMutableRawPointer) -> Value + private let release: @Sendable (UnsafeMutableRawPointer) -> Void + + /// A constructor that requires `Value` to be `AnyObject` to be + /// able to store the value directly in the thread-local storage. + init() where Value: AnyObject { + var key = pthread_key_t() + pthread_key_create(&key, nil) + self.key = key + self.toPointer = { Unmanaged.passRetained($0).toOpaque() } + self.fromPointer = { Unmanaged.fromOpaque($0).takeUnretainedValue() } + self.release = { Unmanaged.fromOpaque($0).release() } + } + + private class Box { + let value: Value + init(_ value: Value) { + self.value = value + } + } + + /// A constructor that doesn't require `Value` to be `AnyObject` but + /// boxing the value in heap-allocated memory. + init(boxing _: Void) { + var key = pthread_key_t() + pthread_key_create(&key, nil) + self.key = key + self.toPointer = { + let box = Box($0) + let pointer = Unmanaged.passRetained(box).toOpaque() + return pointer + } + self.fromPointer = { + let box = Unmanaged.fromOpaque($0).takeUnretainedValue() + return box.value + } + self.release = { Unmanaged.fromOpaque($0).release() } + } + #else + // Fallback implementation for platforms that don't support pthread + private class SendableBox: @unchecked Sendable { + var value: Value? = nil + } + private let _storage = SendableBox() + var wrappedValue: Value? { + get { _storage.value } + set { _storage.value = newValue } + } + + init() where Value: AnyObject { + wrappedValue = nil + } + init(boxing _: Void) { + wrappedValue = nil + } + #endif + + deinit { + preconditionFailure("ThreadLocal can only be used as an immortal storage, cannot be deallocated") + } +} + +/// A property wrapper that lazily initializes a thread-local value +/// for each thread that accesses the value. +@propertyWrapper +final class LazyThreadLocal: Sendable { + private let storage: ThreadLocal + + var wrappedValue: Value { + if let value = storage.wrappedValue { + return value + } + let value = initialValue() + storage.wrappedValue = value + return value + } + + private let initialValue: @Sendable () -> Value + + init(initialize: @Sendable @escaping () -> Value) where Value: AnyObject { + self.storage = ThreadLocal() + self.initialValue = initialize + } + + init(initialize: @Sendable @escaping () -> Value) { + self.storage = ThreadLocal(boxing: ()) + self.initialValue = initialize + } +} diff --git a/Sources/JavaScriptKit/XcodeSupport.swift b/Sources/JavaScriptKit/XcodeSupport.swift deleted file mode 100644 index 5bb02e3a3..000000000 --- a/Sources/JavaScriptKit/XcodeSupport.swift +++ /dev/null @@ -1,91 +0,0 @@ -import _CJavaScriptKit - -/// Note: -/// Define all runtime functions stub which are imported from JavaScript environment. -/// SwiftPM doesn't support WebAssembly target yet, so we need to define them to -/// avoid link failure. -/// When running with JavaScript runtime library, they are ignored completely. -#if !arch(wasm32) - func _set_prop( - _: JavaScriptObjectRef, - _: JavaScriptObjectRef, - _: JavaScriptValueKind, - _: JavaScriptPayload1, - _: JavaScriptPayload2 - ) { fatalError() } - func _get_prop( - _: JavaScriptObjectRef, - _: JavaScriptObjectRef, - _: UnsafeMutablePointer!, - _: UnsafeMutablePointer!, - _: UnsafeMutablePointer! - ) { fatalError() } - func _set_subscript( - _: JavaScriptObjectRef, - _: Int32, - _: JavaScriptValueKind, - _: JavaScriptPayload1, - _: JavaScriptPayload2 - ) { fatalError() } - func _get_subscript( - _: JavaScriptObjectRef, - _: Int32, - _: UnsafeMutablePointer!, - _: UnsafeMutablePointer!, - _: UnsafeMutablePointer! - ) { fatalError() } - func _encode_string( - _: JavaScriptObjectRef, - _: UnsafeMutablePointer! - ) -> Int32 { fatalError() } - func _decode_string( - _: UnsafePointer!, - _: Int32 - ) -> JavaScriptObjectRef { fatalError() } - func _load_string( - _: JavaScriptObjectRef, - _: UnsafeMutablePointer! - ) { fatalError() } - func _call_function( - _: JavaScriptObjectRef, - _: UnsafePointer!, _: Int32, - _: UnsafeMutablePointer!, - _: UnsafeMutablePointer!, - _: UnsafeMutablePointer! - ) { fatalError() } - func _call_function_with_this( - _: JavaScriptObjectRef, - _: JavaScriptObjectRef, - _: UnsafePointer!, _: Int32, - _: UnsafeMutablePointer!, - _: UnsafeMutablePointer!, - _: UnsafeMutablePointer! - ) { fatalError() } - func _call_new( - _: JavaScriptObjectRef, - _: UnsafePointer!, _: Int32 - ) -> JavaScriptObjectRef { fatalError() } - func _call_throwing_new( - _: JavaScriptObjectRef, - _: UnsafePointer!, _: Int32, - _: UnsafeMutablePointer!, - _: UnsafeMutablePointer!, - _: UnsafeMutablePointer! - ) -> JavaScriptObjectRef { fatalError() } - func _instanceof( - _: JavaScriptObjectRef, - _: JavaScriptObjectRef - ) -> Bool { fatalError() } - func _create_function(_: JavaScriptHostFuncRef) -> JavaScriptObjectRef { fatalError() } - func _create_typed_array( - _: JavaScriptObjectRef, - _: UnsafePointer, - _: Int32 - ) -> JavaScriptObjectRef { fatalError() } - func _load_typed_array( - _: JavaScriptObjectRef, - _: UnsafeMutablePointer! - ) { fatalError() } - func _release(_: JavaScriptObjectRef) { fatalError() } - -#endif diff --git a/Sources/_CJavaScriptBigIntSupport/_CJavaScriptKit+I64.c b/Sources/_CJavaScriptBigIntSupport/_CJavaScriptKit+I64.c new file mode 100644 index 000000000..e6b1f4566 --- /dev/null +++ b/Sources/_CJavaScriptBigIntSupport/_CJavaScriptKit+I64.c @@ -0,0 +1 @@ +// empty file to appease build process diff --git a/Sources/_CJavaScriptBigIntSupport/include/_CJavaScriptKit+I64.h b/Sources/_CJavaScriptBigIntSupport/include/_CJavaScriptKit+I64.h new file mode 100644 index 000000000..69d25e47b --- /dev/null +++ b/Sources/_CJavaScriptBigIntSupport/include/_CJavaScriptKit+I64.h @@ -0,0 +1,29 @@ + +#ifndef _CJavaScriptBigIntSupport_h +#define _CJavaScriptBigIntSupport_h + +#include <_CJavaScriptKit.h> + +#if __wasm32__ +# define IMPORT_JS_FUNCTION(name, returns, args) \ +__attribute__((__import_module__("javascript_kit"), __import_name__(#name))) extern returns name args; +#else +# define IMPORT_JS_FUNCTION(name, returns, args) \ + static inline returns name args { \ + abort(); \ + } +#endif + +/// Converts the provided Int64 or UInt64 to a BigInt. +/// +/// @param value The value to convert. +/// @param is_signed Whether to treat the value as a signed integer or not. +IMPORT_JS_FUNCTION(swjs_i64_to_bigint, JavaScriptObjectRef, (const long long value, bool is_signed)) + +/// Converts the provided BigInt to an Int64 or UInt64. +/// +/// @param ref The target JavaScript object. +/// @param is_signed Whether to treat the return value as a signed integer or not. +IMPORT_JS_FUNCTION(swjs_bigint_to_i64, long long, (const JavaScriptObjectRef ref, bool is_signed)) + +#endif /* _CJavaScriptBigIntSupport_h */ diff --git a/Sources/_CJavaScriptBigIntSupport/include/module.modulemap b/Sources/_CJavaScriptBigIntSupport/include/module.modulemap new file mode 100644 index 000000000..944871cde --- /dev/null +++ b/Sources/_CJavaScriptBigIntSupport/include/module.modulemap @@ -0,0 +1,4 @@ +module _CJavaScriptBigIntSupport { + header "_CJavaScriptKit+I64.h" + export * +} diff --git a/Sources/_CJavaScriptEventLoop/_CJavaScriptEventLoop.c b/Sources/_CJavaScriptEventLoop/_CJavaScriptEventLoop.c index e69de29bb..ebb05e1db 100644 --- a/Sources/_CJavaScriptEventLoop/_CJavaScriptEventLoop.c +++ b/Sources/_CJavaScriptEventLoop/_CJavaScriptEventLoop.c @@ -0,0 +1,5 @@ +#include "_CJavaScriptEventLoop.h" + +_Thread_local void *swjs_thread_local_event_loop; + +_Thread_local void *swjs_thread_local_task_executor_worker; diff --git a/Sources/_CJavaScriptEventLoop/include/_CJavaScriptEventLoop.h b/Sources/_CJavaScriptEventLoop/include/_CJavaScriptEventLoop.h index 51c98afc6..0fa08c9e7 100644 --- a/Sources/_CJavaScriptEventLoop/include/_CJavaScriptEventLoop.h +++ b/Sources/_CJavaScriptEventLoop/include/_CJavaScriptEventLoop.h @@ -9,6 +9,8 @@ #define SWIFT_EXPORT_FROM(LIBRARY) __attribute__((__visibility__("default"))) +#define SWIFT_NONISOLATED_UNSAFE __attribute__((swift_attr("nonisolated(unsafe)"))) + /// A schedulable unit /// Note that this type layout is a part of public ABI, so we expect this field layout won't break in the future versions. /// Current implementation refers the `swift-5.5-RELEASE` implementation. @@ -27,20 +29,45 @@ typedef SWIFT_CC(swift) void (*swift_task_enqueueGlobal_original)( Job *_Nonnull job); SWIFT_EXPORT_FROM(swift_Concurrency) -void *_Nullable swift_task_enqueueGlobal_hook; +extern void *_Nullable swift_task_enqueueGlobal_hook SWIFT_NONISOLATED_UNSAFE; /// A hook to take over global enqueuing with delay. typedef SWIFT_CC(swift) void (*swift_task_enqueueGlobalWithDelay_original)( unsigned long long delay, Job *_Nonnull job); SWIFT_EXPORT_FROM(swift_Concurrency) -void *_Nullable swift_task_enqueueGlobalWithDelay_hook; +extern void *_Nullable swift_task_enqueueGlobalWithDelay_hook SWIFT_NONISOLATED_UNSAFE; -unsigned long long foo; +typedef SWIFT_CC(swift) void (*swift_task_enqueueGlobalWithDeadline_original)( + long long sec, + long long nsec, + long long tsec, + long long tnsec, + int clock, Job *_Nonnull job); +SWIFT_EXPORT_FROM(swift_Concurrency) +extern void *_Nullable swift_task_enqueueGlobalWithDeadline_hook SWIFT_NONISOLATED_UNSAFE; /// A hook to take over main executor enqueueing. typedef SWIFT_CC(swift) void (*swift_task_enqueueMainExecutor_original)( Job *_Nonnull job); SWIFT_EXPORT_FROM(swift_Concurrency) -void *_Nullable swift_task_enqueueMainExecutor_hook; +extern void *_Nullable swift_task_enqueueMainExecutor_hook SWIFT_NONISOLATED_UNSAFE; + +/// A hook to override the entrypoint to the main runloop used to drive the +/// concurrency runtime and drain the main queue. This function must not return. +/// Note: If the hook is wrapping the original function and the `compatOverride` +/// is passed in, the `original` function pointer must be passed into the +/// compatibility override function as the original function. +typedef SWIFT_CC(swift) void (*swift_task_asyncMainDrainQueue_original)(); +typedef SWIFT_CC(swift) void (*swift_task_asyncMainDrainQueue_override)( + swift_task_asyncMainDrainQueue_original _Nullable original); +SWIFT_EXPORT_FROM(swift_Concurrency) +extern void *_Nullable swift_task_asyncMainDrainQueue_hook SWIFT_NONISOLATED_UNSAFE; + + +/// MARK: - thread local storage + +extern _Thread_local void * _Nullable swjs_thread_local_event_loop SWIFT_NONISOLATED_UNSAFE; + +extern _Thread_local void * _Nullable swjs_thread_local_task_executor_worker SWIFT_NONISOLATED_UNSAFE; #endif diff --git a/Sources/_CJavaScriptEventLoopTestSupport/_CJavaScriptEventLoopTestSupport.c b/Sources/_CJavaScriptEventLoopTestSupport/_CJavaScriptEventLoopTestSupport.c new file mode 100644 index 000000000..7dfdbe2e8 --- /dev/null +++ b/Sources/_CJavaScriptEventLoopTestSupport/_CJavaScriptEventLoopTestSupport.c @@ -0,0 +1,19 @@ +// This 'ctor' function is called at startup time of this program. +// It's invoked by '_start' of command-line or '_initialize' of reactor. +// This ctor activate the event loop based global executor automatically +// before running the test cases. For general applications, applications +// have to activate the event loop manually on their responsibility. +// However, XCTest framework doesn't provide a way to run arbitrary code +// before running all of the test suites. So, we have to do it here. +// +// See also: https://github.com/WebAssembly/WASI/blob/main/legacy/application-abi.md#current-unstable-abi + +extern void swift_javascriptkit_activate_js_executor_impl(void); + +// priority 0~100 is reserved by wasi-libc +// https://github.com/WebAssembly/wasi-libc/blob/30094b6ed05f19cee102115215863d185f2db4f0/libc-bottom-half/sources/environ.c#L20 +__attribute__((constructor(/* priority */ 200))) +void swift_javascriptkit_activate_js_executor(void) { + swift_javascriptkit_activate_js_executor_impl(); +} + diff --git a/Sources/_CJavaScriptEventLoopTestSupport/include/dummy.h b/Sources/_CJavaScriptEventLoopTestSupport/include/dummy.h new file mode 100644 index 000000000..e69de29bb diff --git a/Sources/_CJavaScriptKit/_CJavaScriptKit.c b/Sources/_CJavaScriptKit/_CJavaScriptKit.c index 38329ff14..a32881804 100644 --- a/Sources/_CJavaScriptKit/_CJavaScriptKit.c +++ b/Sources/_CJavaScriptKit/_CJavaScriptKit.c @@ -1,17 +1,49 @@ #include "_CJavaScriptKit.h" -#include - #if __wasm32__ +# 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 + +# 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`. +__attribute__((export_name("swjs_library_version"))) +int swjs_library_version(void) { + return 708; +} -void _call_host_function_impl(const JavaScriptHostFuncRef host_func_ref, +__attribute__((export_name("swjs_prepare_host_function_call"))) +void *swjs_prepare_host_function_call(const int argc) { + return malloc(argc * sizeof(RawJSValue)); +} + +__attribute__((export_name("swjs_cleanup_host_function_call"))) +void swjs_cleanup_host_function_call(void *argv_buffer) { + free(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__ +bool _call_host_function_impl(const JavaScriptHostFuncRef host_func_ref, const RawJSValue *argv, const int argc, const JavaScriptObjectRef callback_func); __attribute__((export_name("swjs_call_host_function"))) -void swjs_call_host_function(const JavaScriptHostFuncRef host_func_ref, +bool swjs_call_host_function(const JavaScriptHostFuncRef host_func_ref, const RawJSValue *argv, const int argc, const JavaScriptObjectRef callback_func) { - _call_host_function_impl(host_func_ref, argv, argc, callback_func); + return _call_host_function_impl(host_func_ref, argv, argc, callback_func); } void _free_host_function_impl(const JavaScriptHostFuncRef host_func_ref); @@ -21,29 +53,19 @@ void swjs_free_host_function(const JavaScriptHostFuncRef host_func_ref) { _free_host_function_impl(host_func_ref); } -__attribute__((export_name("swjs_prepare_host_function_call"))) -void *swjs_prepare_host_function_call(const int argc) { - return malloc(argc * sizeof(RawJSValue)); -} - -__attribute__((export_name("swjs_cleanup_host_function_call"))) -void swjs_cleanup_host_function_call(void *argv_buffer) { - free(argv_buffer); -} - -/// 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`. -__attribute__((export_name("swjs_library_version"))) -int swjs_library_version(void) { - return 705; -} - int _library_features(void); __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; + if (tid == 0) { + tid = swjs_get_worker_thread_id(); + } + return tid; +} diff --git a/Sources/_CJavaScriptKit/include/_CJavaScriptKit.h b/Sources/_CJavaScriptKit/include/_CJavaScriptKit.h index 8979cee56..931b48f7a 100644 --- a/Sources/_CJavaScriptKit/include/_CJavaScriptKit.h +++ b/Sources/_CJavaScriptKit/include/_CJavaScriptKit.h @@ -1,19 +1,23 @@ #ifndef _CJavaScriptKit_h #define _CJavaScriptKit_h +#if __has_include("stdlib.h") #include +#else +#include +#endif #include +#include /// `JavaScriptObjectRef` represents JavaScript object reference that is referenced by Swift side. /// This value is an address of `SwiftRuntimeHeap`. typedef unsigned int JavaScriptObjectRef; /// `JavaScriptHostFuncRef` represents Swift closure that is referenced by JavaScript side. /// This value is produced by `JSClosure`. -typedef unsigned int JavaScriptHostFuncRef; +typedef uintptr_t JavaScriptHostFuncRef; /// `JavaScriptValueKind` represents the kind of JavaScript primitive value. typedef enum __attribute__((enum_extensibility(closed))) { - JavaScriptValueKindInvalid = -1, JavaScriptValueKindBoolean = 0, JavaScriptValueKindString = 1, JavaScriptValueKindNumber = 2, @@ -21,17 +25,16 @@ typedef enum __attribute__((enum_extensibility(closed))) { JavaScriptValueKindNull = 4, JavaScriptValueKindUndefined = 5, JavaScriptValueKindFunction = 6, + JavaScriptValueKindSymbol = 7, + JavaScriptValueKindBigInt = 8, } JavaScriptValueKind; -typedef struct { - JavaScriptValueKind kind: 31; - bool isException: 1; -} JavaScriptValueKindAndFlags; +typedef uint32_t JavaScriptRawValueKindAndFlags; typedef unsigned JavaScriptPayload1; typedef double JavaScriptPayload2; -/// `RawJSValue` is abstract representaion of JavaScript primitive value. +/// `RawJSValue` is abstract representation of JavaScript primitive value. /// /// For boolean value: /// payload1: 1 or 0 @@ -60,6 +63,10 @@ typedef double JavaScriptPayload2; /// payload1: the target `JavaScriptHostFuncRef` /// payload2: 0 /// +/// For symbol and bigint values: +/// payload1: `JavaScriptObjectRef` +/// payload2: 0 +/// typedef struct { JavaScriptValueKind kind; JavaScriptPayload1 payload1; @@ -67,139 +74,166 @@ typedef struct { } RawJSValue; #if __wasm32__ +# define IMPORT_JS_FUNCTION(name, returns, args) \ +__attribute__((__import_module__("javascript_kit"), __import_name__(#name))) extern returns name args; +#else +# define IMPORT_JS_FUNCTION(name, returns, args) \ + static inline returns name args { \ + abort(); \ + } +#endif -/// `_set_prop` sets a value of `_this` JavaScript object. +/// Sets a value of `_this` JavaScript object. /// /// @param _this The target JavaScript object to set the given value. /// @param prop A JavaScript string object to reference a member of `_this` object. /// @param kind A kind of JavaScript value to set the target object. /// @param payload1 The first payload of JavaScript value to set the target object. /// @param payload2 The second payload of JavaScript value to set the target object. -__attribute__((__import_module__("javascript_kit"), - __import_name__("swjs_set_prop"))) -extern void _set_prop(const JavaScriptObjectRef _this, - const JavaScriptObjectRef prop, - const JavaScriptValueKind kind, - const JavaScriptPayload1 payload1, - const JavaScriptPayload2 payload2); - -/// `_get_prop` gets a value of `_this` JavaScript object. +IMPORT_JS_FUNCTION(swjs_set_prop, void, (const JavaScriptObjectRef _this, + const JavaScriptObjectRef prop, + const JavaScriptValueKind kind, + const JavaScriptPayload1 payload1, + const JavaScriptPayload2 payload2)) + +/// Gets a value of `_this` JavaScript object. /// /// @param _this The target JavaScript object to get its member value. /// @param prop A JavaScript string object to reference a member of `_this` object. -/// @param kind A result pointer of JavaScript value kind to get. /// @param payload1 A result pointer of first payload of JavaScript value to set the target object. /// @param payload2 A result pointer of second payload of JavaScript value to set the target object. -__attribute__((__import_module__("javascript_kit"), - __import_name__("swjs_get_prop"))) -extern void _get_prop(const JavaScriptObjectRef _this, - const JavaScriptObjectRef prop, - JavaScriptValueKind *kind, - JavaScriptPayload1 *payload1, - JavaScriptPayload2 *payload2); - -/// `_set_subscript` sets a value of `_this` JavaScript object. +/// @return A `JavaScriptValueKind` bits represented as 32bit integer for the returned value. +IMPORT_JS_FUNCTION(swjs_get_prop, uint32_t, (const JavaScriptObjectRef _this, + const JavaScriptObjectRef prop, + JavaScriptPayload1 * _Nonnull payload1, + JavaScriptPayload2 * _Nonnull payload2)) + +/// Sets a value of `_this` JavaScript object. /// /// @param _this The target JavaScript object to set its member value. /// @param index A subscript index to set value. /// @param kind A kind of JavaScript value to set the target object. /// @param payload1 The first payload of JavaScript value to set the target object. /// @param payload2 The second payload of JavaScript value to set the target object. -__attribute__((__import_module__("javascript_kit"), - __import_name__("swjs_set_subscript"))) -extern void _set_subscript(const JavaScriptObjectRef _this, - const int index, - const JavaScriptValueKind kind, - const JavaScriptPayload1 payload1, - const JavaScriptPayload2 payload2); - -/// `_get_subscript` gets a value of `_this` JavaScript object. +IMPORT_JS_FUNCTION(swjs_set_subscript, void, (const JavaScriptObjectRef _this, + const int index, + const JavaScriptValueKind kind, + const JavaScriptPayload1 payload1, + const JavaScriptPayload2 payload2)) + +/// Gets a value of `_this` JavaScript object. /// /// @param _this The target JavaScript object to get its member value. /// @param index A subscript index to get value. -/// @param kind A result pointer of JavaScript value kind to get. /// @param payload1 A result pointer of first payload of JavaScript value to get the target object. /// @param payload2 A result pointer of second payload of JavaScript value to get the target object. -__attribute__((__import_module__("javascript_kit"), - __import_name__("swjs_get_subscript"))) -extern void _get_subscript(const JavaScriptObjectRef _this, - const int index, - JavaScriptValueKind *kind, - JavaScriptPayload1 *payload1, - JavaScriptPayload2 *payload2); - -/// `_encode_string` encodes the `str_obj` to bytes sequence and returns the length of bytes. +/// @return A `JavaScriptValueKind` bits represented as 32bit integer for the returned value. +/// get a value of `_this` JavaScript object. +IMPORT_JS_FUNCTION(swjs_get_subscript, uint32_t, (const JavaScriptObjectRef _this, + const int index, + JavaScriptPayload1 * _Nonnull payload1, + JavaScriptPayload2 * _Nonnull payload2)) + +/// Encodes the `str_obj` to bytes sequence and returns the length of bytes. /// /// @param str_obj A JavaScript string object ref to encode. /// @param bytes_result A result pointer of bytes sequence representation in JavaScript. /// This value will be used to load the actual bytes using `_load_string`. /// @result The length of bytes sequence. This value will be used to allocate Swift side string buffer to load the actual bytes. -__attribute__((__import_module__("javascript_kit"), - __import_name__("swjs_encode_string"))) -extern int _encode_string(const JavaScriptObjectRef str_obj, JavaScriptObjectRef *bytes_result); +IMPORT_JS_FUNCTION(swjs_encode_string, int, (const JavaScriptObjectRef str_obj, JavaScriptObjectRef * _Nonnull bytes_result)) -/// `_decode_string` decodes the given bytes sequence into JavaScript string object. +/// Decodes the given bytes sequence into JavaScript string object. /// /// @param bytes_ptr A `uint8_t` byte sequence to decode. /// @param length The length of `bytes_ptr`. /// @result The decoded JavaScript string object. -__attribute__((__import_module__("javascript_kit"), - __import_name__("swjs_decode_string"))) -extern JavaScriptObjectRef _decode_string(const unsigned char *bytes_ptr, const int length); +IMPORT_JS_FUNCTION(swjs_decode_string, JavaScriptObjectRef, (const unsigned char * _Nonnull bytes_ptr, const int length)) -/// `_load_string` loads the actual bytes sequence of `bytes` into `buffer` which is a Swift side memory address. +/// Loads the actual bytes sequence of `bytes` into `buffer` which is a Swift side memory address. /// /// @param bytes A bytes sequence representation in JavaScript to load. This value should be derived from `_encode_string`. /// @param buffer A Swift side string buffer to load the bytes. -__attribute__((__import_module__("javascript_kit"), - __import_name__("swjs_load_string"))) -extern void _load_string(const JavaScriptObjectRef bytes, unsigned char *buffer); +IMPORT_JS_FUNCTION(swjs_load_string, void, (const JavaScriptObjectRef bytes, unsigned char * _Nonnull buffer)) + +/// Converts the provided Int64 or UInt64 to a BigInt in slow path by splitting 64bit integer to two 32bit integers +/// to avoid depending on [JS-BigInt-integration](https://github.com/WebAssembly/JS-BigInt-integration) feature +/// +/// @param lower The lower 32bit of the value to convert. +/// @param upper The upper 32bit of the value to convert. +/// @param is_signed Whether to treat the value as a signed integer or not. +IMPORT_JS_FUNCTION(swjs_i64_to_bigint_slow, JavaScriptObjectRef, (unsigned int lower, unsigned int upper, bool is_signed)) -/// `_call_function` calls JavaScript function with given arguments list. +/// Calls JavaScript function with given arguments list. /// /// @param ref The target JavaScript function to call. /// @param argv A list of `RawJSValue` arguments to apply. /// @param argc The length of `argv``. -/// @param result_kind A result pointer of JavaScript value kind of returned result or thrown exception. /// @param result_payload1 A result pointer of first payload of JavaScript value of returned result or thrown exception. /// @param result_payload2 A result pointer of second payload of JavaScript value of returned result or thrown exception. -__attribute__((__import_module__("javascript_kit"), - __import_name__("swjs_call_function"))) -extern void _call_function(const JavaScriptObjectRef ref, const RawJSValue *argv, - const int argc, JavaScriptValueKindAndFlags *result_kind, - JavaScriptPayload1 *result_payload1, - JavaScriptPayload2 *result_payload2); - -/// `_call_function_with_this` calls JavaScript function with given arguments list and given `_this`. +/// @return A `JavaScriptValueKindAndFlags` bits represented as 32bit integer for the returned value. +IMPORT_JS_FUNCTION(swjs_call_function, uint32_t, (const JavaScriptObjectRef ref, + const RawJSValue * _Nullable argv, + const int argc, + JavaScriptPayload1 * _Nonnull result_payload1, + JavaScriptPayload2 * _Nonnull result_payload2)) + +/// Calls JavaScript function with given arguments list without capturing any exception +/// +/// @param ref The target JavaScript function to call. +/// @param argv A list of `RawJSValue` arguments to apply. +/// @param argc The length of `argv``. +/// @param result_payload1 A result pointer of first payload of JavaScript value of returned result or thrown exception. +/// @param result_payload2 A result pointer of second payload of JavaScript value of returned result or thrown exception. +/// @return A `JavaScriptValueKindAndFlags` bits represented as 32bit integer for the returned value. +IMPORT_JS_FUNCTION(swjs_call_function_no_catch, uint32_t, (const JavaScriptObjectRef ref, + const RawJSValue * _Nullable argv, + const int argc, + JavaScriptPayload1 * _Nonnull result_payload1, + JavaScriptPayload2 * _Nonnull result_payload2)) + +/// Calls JavaScript function with given arguments list and given `_this`. +/// +/// @param _this The value of `this` provided for the call to `func_ref`. +/// @param func_ref The target JavaScript function to call. +/// @param argv A list of `RawJSValue` arguments to apply. +/// @param argc The length of `argv``. +/// @param result_payload1 A result pointer of first payload of JavaScript value of returned result or thrown exception. +/// @param result_payload2 A result pointer of second payload of JavaScript value of returned result or thrown exception. +/// @return A `JavaScriptValueKindAndFlags` bits represented as 32bit integer for the returned value. +IMPORT_JS_FUNCTION(swjs_call_function_with_this, uint32_t, (const JavaScriptObjectRef _this, + const JavaScriptObjectRef func_ref, + const RawJSValue * _Nullable argv, + const int argc, + JavaScriptPayload1 * _Nonnull result_payload1, + JavaScriptPayload2 * _Nonnull result_payload2)) + +/// Calls JavaScript function with given arguments list and given `_this` without capturing any exception. /// /// @param _this The value of `this` provided for the call to `func_ref`. /// @param func_ref The target JavaScript function to call. /// @param argv A list of `RawJSValue` arguments to apply. /// @param argc The length of `argv``. -/// @param result_kind A result pointer of JavaScript value kind of returned result or thrown exception. /// @param result_payload1 A result pointer of first payload of JavaScript value of returned result or thrown exception. /// @param result_payload2 A result pointer of second payload of JavaScript value of returned result or thrown exception. -__attribute__((__import_module__("javascript_kit"), - __import_name__("swjs_call_function_with_this"))) -extern void _call_function_with_this(const JavaScriptObjectRef _this, - const JavaScriptObjectRef func_ref, - const RawJSValue *argv, const int argc, - JavaScriptValueKindAndFlags *result_kind, - JavaScriptPayload1 *result_payload1, - JavaScriptPayload2 *result_payload2); - -/// `_call_new` calls JavaScript object constructor with given arguments list. +/// @return A `JavaScriptValueKindAndFlags` bits represented as 32bit integer for the returned value. +IMPORT_JS_FUNCTION(swjs_call_function_with_this_no_catch, uint32_t, (const JavaScriptObjectRef _this, + const JavaScriptObjectRef func_ref, + const RawJSValue * _Nullable argv, + const int argc, + JavaScriptPayload1 * _Nonnull result_payload1, + JavaScriptPayload2 * _Nonnull result_payload2)) + +/// Calls JavaScript object constructor with given arguments list. /// /// @param ref The target JavaScript constructor to call. /// @param argv A list of `RawJSValue` arguments to apply. /// @param argc The length of `argv``. /// @returns A reference to the constructed object. -__attribute__((__import_module__("javascript_kit"), - __import_name__("swjs_call_new"))) -extern JavaScriptObjectRef _call_new(const JavaScriptObjectRef ref, - const RawJSValue *argv, const int argc); +IMPORT_JS_FUNCTION(swjs_call_new, JavaScriptObjectRef, (const JavaScriptObjectRef ref, + const RawJSValue * _Nullable argv, + const int argc)) -/// `_call_throwing_new` calls JavaScript object constructor with given arguments list. +/// Calls JavaScript object constructor with given arguments list. /// /// @param ref The target JavaScript constructor to call. /// @param argv A list of `RawJSValue` arguments to apply. @@ -208,60 +242,106 @@ extern JavaScriptObjectRef _call_new(const JavaScriptObjectRef ref, /// @param exception_payload1 A result pointer of first payload of JavaScript value of thrown exception. /// @param exception_payload2 A result pointer of second payload of JavaScript value of thrown exception. /// @returns A reference to the constructed object. -__attribute__((__import_module__("javascript_kit"), - __import_name__("swjs_call_throwing_new"))) -extern JavaScriptObjectRef _call_throwing_new(const JavaScriptObjectRef ref, - const RawJSValue *argv, const int argc, - JavaScriptValueKindAndFlags *exception_kind, - JavaScriptPayload1 *exception_payload1, - JavaScriptPayload2 *exception_payload2); - -/// `_instanceof` acts like JavaScript `instanceof` operator. +IMPORT_JS_FUNCTION(swjs_call_throwing_new, JavaScriptObjectRef, (const JavaScriptObjectRef ref, + const RawJSValue * _Nullable argv, + const int argc, + JavaScriptRawValueKindAndFlags * _Nonnull exception_kind, + JavaScriptPayload1 * _Nonnull exception_payload1, + JavaScriptPayload2 * _Nonnull exception_payload2)) + +/// Acts like JavaScript `instanceof` operator. /// /// @param obj The target object to check its prototype chain. /// @param constructor The `constructor` object to check against. /// @result Return `true` if `constructor` appears anywhere in the prototype chain of `obj`. Return `false` if not. -__attribute__((__import_module__("javascript_kit"), - __import_name__("swjs_instanceof"))) -extern bool _instanceof(const JavaScriptObjectRef obj, - const JavaScriptObjectRef constructor); +IMPORT_JS_FUNCTION(swjs_instanceof, bool, (const JavaScriptObjectRef obj, + const JavaScriptObjectRef constructor)) -/// `_create_function` creates a JavaScript thunk function that calls Swift side closure. +/// 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 /// /// @param host_func_id The target Swift side function called by the created thunk function. +/// @param line The line where the function is created. Will be used for diagnostics +/// @param file The file name where the function is created. Will be used for diagnostics /// @returns A reference to the newly-created JavaScript thunk function -__attribute__((__import_module__("javascript_kit"), - __import_name__("swjs_create_function"))) -extern JavaScriptObjectRef _create_function(const JavaScriptHostFuncRef host_func_id); +IMPORT_JS_FUNCTION(swjs_create_function, JavaScriptObjectRef, (const JavaScriptHostFuncRef host_func_id, + unsigned int line, + JavaScriptObjectRef file)) -/// Instantiate a new `TypedArray` object with given elements +/// Instantiates a new `TypedArray` object with given elements /// This is used to provide an efficient way to create `TypedArray`. /// /// @param constructor The `TypedArray` constructor. /// @param elements_ptr The elements pointer to initialize. They are assumed to be the same size of `constructor` elements size. /// @param length The length of `elements_ptr` /// @returns A reference to the constructed typed array -__attribute__((__import_module__("javascript_kit"), - __import_name__("swjs_create_typed_array"))) -extern JavaScriptObjectRef _create_typed_array(const JavaScriptObjectRef constructor, - const void *elements_ptr, const int length); +IMPORT_JS_FUNCTION(swjs_create_typed_array, JavaScriptObjectRef, (const JavaScriptObjectRef constructor, + const void * _Nullable elements_ptr, + const int length)) /// Copies the byte contents of a typed array into a Swift side memory buffer. /// /// @param ref A JavaScript typed array object. /// @param buffer A Swift side buffer into which to copy the bytes. -__attribute__((__import_module__("javascript_kit"), - __import_name__("swjs_load_typed_array"))) -extern void _load_typed_array(const JavaScriptObjectRef ref, unsigned char *buffer); +IMPORT_JS_FUNCTION(swjs_load_typed_array, void, (const JavaScriptObjectRef ref, unsigned char * _Nonnull buffer)) /// Decrements reference count of `ref` retained by `SwiftRuntimeHeap` in JavaScript side. /// /// @param ref The target JavaScript object. -__attribute__((__import_module__("javascript_kit"), - __import_name__("swjs_release"))) -extern void _release(const JavaScriptObjectRef ref); +IMPORT_JS_FUNCTION(swjs_release, void, (const JavaScriptObjectRef ref)) -#endif +/// Decrements reference count of `ref` retained by `SwiftRuntimeHeap` in `object_tid` thread. +/// +/// @param object_tid The TID of the thread that owns the target object. +/// @param ref The target JavaScript object. +IMPORT_JS_FUNCTION(swjs_release_remote, void, (int object_tid, const JavaScriptObjectRef ref)) + +/// Yields current program control by throwing `UnsafeEventLoopYield` JavaScript exception. +/// See note on `UnsafeEventLoopYield` for more details +/// +/// @note This function never returns +IMPORT_JS_FUNCTION(swjs_unsafe_event_loop_yield, void, (void)) + +IMPORT_JS_FUNCTION(swjs_send_job_to_main_thread, void, (uintptr_t job)) + +IMPORT_JS_FUNCTION(swjs_listen_message_from_main_thread, void, (void)) + +IMPORT_JS_FUNCTION(swjs_wake_up_worker_thread, void, (int tid)) + +IMPORT_JS_FUNCTION(swjs_listen_message_from_worker_thread, void, (int tid)) + +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)) + +int swjs_get_worker_thread_id_cached(void); + +/// Requests sending a JavaScript object to another worker thread. +/// +/// This must be called from the destination thread of the transfer. +IMPORT_JS_FUNCTION(swjs_request_sending_object, void, (JavaScriptObjectRef sending_object, + const JavaScriptObjectRef * _Nonnull transferring_objects, + int transferring_objects_count, + int object_source_tid, + void * _Nonnull sending_context)) + +IMPORT_JS_FUNCTION(swjs_request_sending_objects, void, (const JavaScriptObjectRef * _Nonnull sending_objects, + int sending_objects_count, + const JavaScriptObjectRef * _Nonnull transferring_objects, + int transferring_objects_count, + int object_source_tid, + void * _Nonnull sending_context)) #endif /* _CJavaScriptKit_h */ diff --git a/Tests/BridgeJSRuntimeTests/ExportAPITests.swift b/Tests/BridgeJSRuntimeTests/ExportAPITests.swift new file mode 100644 index 000000000..1473594e5 --- /dev/null +++ b/Tests/BridgeJSRuntimeTests/ExportAPITests.swift @@ -0,0 +1,61 @@ +import XCTest +import JavaScriptKit + +@_extern(wasm, module: "BridgeJSRuntimeTests", name: "runJsWorks") +@_extern(c) +func runJsWorks() -> Void + +@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..cc3c9df31 --- /dev/null +++ b/Tests/BridgeJSRuntimeTests/Generated/ExportSwift.swift @@ -0,0 +1,98 @@ +@_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_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..f60426a09 --- /dev/null +++ b/Tests/BridgeJSRuntimeTests/Generated/JavaScript/ExportSwift.json @@ -0,0 +1,206 @@ +{ + "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_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 new file mode 100644 index 000000000..73aacf1bb --- /dev/null +++ b/Tests/JavaScriptBigIntSupportTests/JavaScriptBigIntSupportTests.swift @@ -0,0 +1,50 @@ +import JavaScriptBigIntSupport +import JavaScriptKit +import XCTest + +class JavaScriptBigIntSupportTests: XCTestCase { + func testBigIntSupport() { + // Test signed values + func testSignedValue(_ value: Int64, file: StaticString = #filePath, line: UInt = #line) { + let bigInt = JSBigInt(value) + XCTAssertEqual(bigInt.description, value.description, file: file, line: line) + 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) + XCTAssertEqual(bigInt.description, value.description, file: file, line: line) + 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) + testUnsignedValue(1 << 63) + 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 new file mode 100644 index 000000000..d2b776304 --- /dev/null +++ b/Tests/JavaScriptEventLoopTestSupportTests/JavaScriptEventLoopTestSupportTests.swift @@ -0,0 +1,15 @@ +import JavaScriptKit +import XCTest + +final class JavaScriptEventLoopTestSupportTests: XCTestCase { + func testAwaitMicrotask() async { + let _: () = await withCheckedContinuation { cont in + JSObject.global.queueMicrotask.function!( + JSOneshotClosure { _ in + cont.resume(returning: ()) + return .undefined + } + ) + } + } +} diff --git a/Tests/JavaScriptEventLoopTests/JSPromiseTests.swift b/Tests/JavaScriptEventLoopTests/JSPromiseTests.swift new file mode 100644 index 000000000..962b04421 --- /dev/null +++ b/Tests/JavaScriptEventLoopTests/JSPromiseTests.swift @@ -0,0 +1,98 @@ +import XCTest + +@testable import JavaScriptKit + +final class JSPromiseTests: XCTestCase { + func testPromiseThen() async throws { + var p1 = JSPromise.resolve(JSValue.null) + await withCheckedContinuation { continuation in + p1 = p1.then { value in + XCTAssertEqual(value, .null) + continuation.resume() + return JSValue.number(1.0) + } + } + await withCheckedContinuation { continuation in + p1 = p1.then { value in + XCTAssertEqual(value, .number(1.0)) + continuation.resume() + return JSPromise.resolve(JSValue.boolean(true)) + } + } + await withCheckedContinuation { continuation in + p1 = p1.then { value in + XCTAssertEqual(value, .boolean(true)) + continuation.resume() + return JSValue.undefined + } + } + await withCheckedContinuation { continuation in + p1 = p1.catch { error in + XCTFail("Not fired due to no throw") + return JSValue.undefined + } + .finally { continuation.resume() } + } + } + + func testPromiseCatch() async throws { + var p2 = JSPromise.reject(JSValue.boolean(false)) + await withCheckedContinuation { continuation in + p2 = p2.catch { error in + XCTAssertEqual(error, .boolean(false)) + continuation.resume() + return JSValue.boolean(true) + } + } + await withCheckedContinuation { continuation in + p2 = p2.then { value in + XCTAssertEqual(value, .boolean(true)) + continuation.resume() + return JSPromise.reject(JSValue.number(2.0)) + } + } + await withCheckedContinuation { continuation in + p2 = p2.catch { error in + XCTAssertEqual(error, .number(2.0)) + continuation.resume() + return JSValue.undefined + } + } + await withCheckedContinuation { continuation in + p2 = p2.finally { continuation.resume() } + } + } + + func testPromiseAndTimer() async throws { + let start = JSDate().valueOf() + let timeoutMilliseconds = 5.0 + var timer: JSTimer? + + var p3: JSPromise? + await withCheckedContinuation { continuation in + p3 = JSPromise { resolve in + timer = JSTimer(millisecondsDelay: timeoutMilliseconds) { + continuation.resume() + resolve(.success(.undefined)) + } + } + } + + await withCheckedContinuation { continuation in + p3?.then { _ in + XCTAssertEqual(start + timeoutMilliseconds <= JSDate().valueOf(), true) + continuation.resume() + return JSValue.undefined + } + } + + // Ensure that users don't need to manage JSPromise lifetime + await withCheckedContinuation { continuation in + JSPromise.resolve(JSValue.boolean(true)).then { _ in + continuation.resume() + return JSValue.undefined + } + } + withExtendedLifetime(timer) {} + } +} diff --git a/Tests/JavaScriptEventLoopTests/JSTimerTests.swift b/Tests/JavaScriptEventLoopTests/JSTimerTests.swift new file mode 100644 index 000000000..1d3fec036 --- /dev/null +++ b/Tests/JavaScriptEventLoopTests/JSTimerTests.swift @@ -0,0 +1,55 @@ +import XCTest + +@testable import JavaScriptKit + +final class JSTimerTests: XCTestCase { + + func testOneshotTimerCancelled() { + let timeoutMilliseconds = 5.0 + var timeout: JSTimer! + timeout = JSTimer(millisecondsDelay: timeoutMilliseconds, isRepeating: false) { + XCTFail("timer should be cancelled") + } + _ = timeout + timeout = nil + } + + func testRepeatingTimerCancelled() async throws { + var count = 0.0 + let maxCount = 5.0 + var interval: JSTimer? + let start = JSDate().valueOf() + let timeoutMilliseconds = 5.0 + + await withCheckedContinuation { continuation in + interval = JSTimer(millisecondsDelay: 5, isRepeating: true) { + // ensure that JSTimer is living + XCTAssertNotNil(interval) + // verify that at least `timeoutMilliseconds * count` passed since the `timeout` + // timer started + XCTAssertTrue(start + timeoutMilliseconds * count <= JSDate().valueOf()) + + guard count < maxCount else { + // stop the timer after `maxCount` reached + interval = nil + continuation.resume() + return + } + + count += 1 + } + } + withExtendedLifetime(interval) {} + } + + func testTimer() async throws { + let timeoutMilliseconds = 5.0 + var timeout: JSTimer! + await withCheckedContinuation { continuation in + timeout = JSTimer(millisecondsDelay: timeoutMilliseconds, isRepeating: false) { + continuation.resume() + } + } + withExtendedLifetime(timeout) {} + } +} diff --git a/Tests/JavaScriptEventLoopTests/JavaScriptEventLoopTests.swift b/Tests/JavaScriptEventLoopTests/JavaScriptEventLoopTests.swift new file mode 100644 index 000000000..1da56e680 --- /dev/null +++ b/Tests/JavaScriptEventLoopTests/JavaScriptEventLoopTests.swift @@ -0,0 +1,262 @@ +import JavaScriptEventLoop +import JavaScriptKit +import XCTest + +final class JavaScriptEventLoopTests: XCTestCase { + // Helper utilities for testing + struct MessageError: Error { + let message: String + let file: StaticString + let line: UInt + let column: UInt + init(_ message: String, file: StaticString, line: UInt, column: UInt) { + self.message = message + self.file = file + self.line = line + self.column = column + } + } + + func expectAsyncThrow( + _ body: @autoclosure () async throws -> T, + file: StaticString = #file, + line: UInt = #line, + column: UInt = #column + ) async throws -> Error { + do { + _ = try await body() + } catch { + return error + } + throw MessageError("Expect to throw an exception", file: file, line: line, column: column) + } + + func performanceNow() -> Double { + return JSObject.global.performance.now().number! + } + + func measureTime(_ block: () async throws -> Void) async rethrows -> Double { + let start = performanceNow() + try await block() + return performanceNow() - start + } + + // Error type used in tests + struct E: Error, Equatable { + let value: Int + } + + // MARK: - Task Tests + + func testTaskInit() async throws { + // Test Task.init value + let handle = Task { 1 } + let value = await handle.value + XCTAssertEqual(value, 1) + } + + func testTaskInitThrows() async throws { + // Test Task.init throws + let throwingHandle = Task { + throw E(value: 2) + } + let error = try await expectAsyncThrow(await throwingHandle.value) + let e = try XCTUnwrap(error as? E) + XCTAssertEqual(e, E(value: 2)) + } + + func testTaskSleep() async throws { + // Test Task.sleep(_:) + let sleepDiff = try await measureTime { + try await Task.sleep(nanoseconds: 200_000_000) + } + XCTAssertGreaterThanOrEqual(sleepDiff, 150) + + // Test shorter sleep duration + let shortSleepDiff = try await measureTime { + try await Task.sleep(nanoseconds: 100_000_000) + } + XCTAssertGreaterThanOrEqual(shortSleepDiff, 50) + } + + func testTaskPriority() async throws { + // Test Job reordering based on priority + class Context: @unchecked Sendable { + var completed: [String] = [] + } + let context = Context() + + // When no priority, they should be ordered by the enqueued order + let t1 = Task(priority: nil) { + context.completed.append("t1") + } + let t2 = Task(priority: nil) { + context.completed.append("t2") + } + + _ = await (t1.value, t2.value) + XCTAssertEqual(context.completed, ["t1", "t2"]) + + context.completed = [] + // When high priority is enqueued after a low one, they should be re-ordered + let t3 = Task(priority: .low) { + context.completed.append("t3") + } + let t4 = Task(priority: .high) { + context.completed.append("t4") + } + let t5 = Task(priority: .low) { + context.completed.append("t5") + } + + _ = await (t3.value, t4.value, t5.value) + XCTAssertEqual(context.completed, ["t4", "t3", "t5"]) + } + + // MARK: - Promise Tests + + func testPromiseResolution() async throws { + // Test await resolved Promise + let p = JSPromise(resolver: { resolve in + resolve(.success(1)) + }) + let resolutionValue = try await p.value + XCTAssertEqual(resolutionValue, .number(1)) + let resolutionResult = await p.result + XCTAssertEqual(resolutionResult, .success(.number(1))) + } + + func testPromiseRejection() async throws { + // Test await rejected Promise + let rejectedPromise = JSPromise(resolver: { resolve in + resolve(.failure(.number(3))) + }) + let promiseError = try await expectAsyncThrow(try await rejectedPromise.value) + let jsValue = try XCTUnwrap(promiseError as? JSException).thrownValue + XCTAssertEqual(jsValue, .number(3)) + let rejectionResult = await rejectedPromise.result + XCTAssertEqual(rejectionResult, .failure(.number(3))) + } + + func testPromiseThen() async throws { + // Test Async JSPromise: then + let promise = JSPromise { resolve in + _ = JSObject.global.setTimeout!( + JSClosure { _ in + resolve(.success(JSValue.number(3))) + return .undefined + }.jsValue, + 100 + ) + } + let promise2 = promise.then { result in + try await Task.sleep(nanoseconds: 100_000_000) + return String(result.number!) + } + let thenDiff = try await measureTime { + let result = try await promise2.value + XCTAssertEqual(result, .string("3.0")) + } + XCTAssertGreaterThanOrEqual(thenDiff, 200) + } + + func testPromiseThenWithFailure() async throws { + // Test Async JSPromise: then(success:failure:) + let failingPromise = JSPromise { resolve in + _ = JSObject.global.setTimeout!( + JSClosure { _ in + resolve(.failure(JSError(message: "test").jsValue)) + return .undefined + }.jsValue, + 100 + ) + } + let failingPromise2 = failingPromise.then { _ in + throw MessageError("Should not be called", file: #file, line: #line, column: #column) + } failure: { err in + return err + } + let failingResult = try await failingPromise2.value + XCTAssertEqual(failingResult.object?.message, .string("test")) + } + + func testPromiseCatch() async throws { + // Test Async JSPromise: catch + let catchPromise = JSPromise { resolve in + _ = JSObject.global.setTimeout!( + JSClosure { _ in + resolve(.failure(JSError(message: "test").jsValue)) + return .undefined + }.jsValue, + 100 + ) + } + let catchPromise2 = catchPromise.catch { err in + try await Task.sleep(nanoseconds: 100_000_000) + return err + } + let catchDiff = try await measureTime { + let result = try await catchPromise2.value + XCTAssertEqual(result.object?.message, .string("test")) + } + XCTAssertGreaterThanOrEqual(catchDiff, 150) + } + + // MARK: - Continuation Tests + + func testContinuation() async throws { + // Test Continuation + let continuationValue = await withUnsafeContinuation { cont in + cont.resume(returning: 1) + } + XCTAssertEqual(continuationValue, 1) + + let continuationError = try await expectAsyncThrow( + try await withUnsafeThrowingContinuation { (cont: UnsafeContinuation) in + cont.resume(throwing: E(value: 2)) + } + ) + let errorValue = try XCTUnwrap(continuationError as? E) + XCTAssertEqual(errorValue.value, 2) + } + + // MARK: - JSClosure Tests + + func testAsyncJSClosure() async throws { + // Test Async JSClosure + let delayClosure = JSClosure.async { _ -> JSValue in + try await Task.sleep(nanoseconds: 200_000_000) + return JSValue.number(3) + } + let delayObject = JSObject.global.Object.function!.new() + delayObject.closure = delayClosure.jsValue + + let closureDiff = try await measureTime { + let promise = JSPromise(from: delayObject.closure!()) + XCTAssertNotNil(promise) + let result = try await promise!.value + XCTAssertEqual(result, .number(3)) + } + XCTAssertGreaterThanOrEqual(closureDiff, 150) + } + + // 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) + + // 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/WebWorkerDedicatedExecutorTests.swift b/Tests/JavaScriptEventLoopTests/WebWorkerDedicatedExecutorTests.swift new file mode 100644 index 000000000..b6c2bd8db --- /dev/null +++ b/Tests/JavaScriptEventLoopTests/WebWorkerDedicatedExecutorTests.swift @@ -0,0 +1,34 @@ +#if compiler(>=6.1) && _runtime(_multithreaded) +import XCTest +@testable import JavaScriptEventLoop + +final class WebWorkerDedicatedExecutorTests: XCTestCase { + actor MyActor { + let executor: WebWorkerDedicatedExecutor + nonisolated var unownedExecutor: UnownedSerialExecutor { + self.executor.asUnownedSerialExecutor() + } + + init(executor: WebWorkerDedicatedExecutor) { + self.executor = executor + XCTAssertTrue(isMainThread()) + } + + func onWorkerThread() async { + XCTAssertFalse(isMainThread()) + await Task.detached {}.value + // Should keep on the thread after back from the other isolation domain + XCTAssertFalse(isMainThread()) + } + } + + func testEnqueue() async throws { + let executor = try await WebWorkerDedicatedExecutor() + defer { executor.terminate() } + let actor = MyActor(executor: executor) + XCTAssertTrue(isMainThread()) + await actor.onWorkerThread() + XCTAssertTrue(isMainThread()) + } +} +#endif diff --git a/Tests/JavaScriptEventLoopTests/WebWorkerTaskExecutorTests.swift b/Tests/JavaScriptEventLoopTests/WebWorkerTaskExecutorTests.swift new file mode 100644 index 000000000..b9c42c02e --- /dev/null +++ b/Tests/JavaScriptEventLoopTests/WebWorkerTaskExecutorTests.swift @@ -0,0 +1,457 @@ +#if compiler(>=6.1) && _runtime(_multithreaded) +import XCTest +import _CJavaScriptKit // For swjs_get_worker_thread_id +@testable import JavaScriptKit +@testable import JavaScriptEventLoop + +@_extern(wasm, module: "JavaScriptEventLoopTestSupportTests", name: "isMainThread") +func isMainThread() -> Bool + +#if canImport(wasi_pthread) +import wasi_pthread +/// 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 + +final class WebWorkerTaskExecutorTests: XCTestCase { + func testTaskRunOnMainThread() async throws { + let executor = try await WebWorkerTaskExecutor(numberOfThreads: 1) + + XCTAssertTrue(isMainThread()) + + let task = Task(executorPreference: executor) { + return isMainThread() + } + let taskRunOnMainThread = await task.value + // The task should run on the worker thread + XCTAssertFalse(taskRunOnMainThread) + // After the task is done, back to the main thread + XCTAssertTrue(isMainThread()) + + executor.terminate() + } + + func testWithPreferenceBlock() async throws { + let executor = try await WebWorkerTaskExecutor(numberOfThreads: 1) + await withTaskExecutorPreference(executor) { + XCTAssertFalse(isMainThread()) + } + } + + func testAwaitInsideTask() async throws { + let executor = try await WebWorkerTaskExecutor(numberOfThreads: 1) + defer { executor.terminate() } + + let task = Task(executorPreference: executor) { + await Task.yield() + _ = try await JSPromise.resolve(1).value + return isMainThread() + } + let taskRunOnMainThread = try await task.value + XCTAssertFalse(taskRunOnMainThread) + } + + func testSleepInsideTask() async throws { + let executor = try await WebWorkerTaskExecutor(numberOfThreads: 1) + + let task = Task(executorPreference: executor) { + XCTAssertFalse(isMainThread()) + try await Task.sleep(nanoseconds: 10) + XCTAssertFalse(isMainThread()) + try await Task.sleep(nanoseconds: 100) + XCTAssertFalse(isMainThread()) + let clock = ContinuousClock() + try await clock.sleep(for: .milliseconds(10)) + return isMainThread() + } + let taskRunOnMainThread = try await task.value + XCTAssertFalse(taskRunOnMainThread) + + executor.terminate() + } + + func testMainActorRun() async throws { + let executor = try await WebWorkerTaskExecutor(numberOfThreads: 1) + + let task = Task(executorPreference: executor) { + await MainActor.run { + return isMainThread() + } + } + let taskRunOnMainThread = await task.value + // FIXME: The block passed to `MainActor.run` should run on the main thread + // XCTAssertTrue(taskRunOnMainThread) + XCTAssertFalse(taskRunOnMainThread) + // After the task is done, back to the main thread + XCTAssertTrue(isMainThread()) + + executor.terminate() + } + + func testTaskGroupRunOnSameThread() async throws { + let executor = try await WebWorkerTaskExecutor(numberOfThreads: 3) + + let mainTid = swjs_get_worker_thread_id() + await withTaskExecutorPreference(executor) { + let tid = swjs_get_worker_thread_id() + await withTaskGroup(of: Int32.self) { group in + group.addTask { + return swjs_get_worker_thread_id() + } + + group.addTask { + return swjs_get_worker_thread_id() + } + + for await id in group { + XCTAssertEqual(id, tid) + XCTAssertNotEqual(id, mainTid) + } + } + } + + executor.terminate() + } + + func testTaskGroupRunOnDifferentThreads() async throws { + let executor = try await WebWorkerTaskExecutor(numberOfThreads: 2) + + struct Item: Hashable { + let type: String + let tid: Int32 + let value: Int + init(_ type: String, _ tid: Int32, _ value: Int) { + self.type = type + self.tid = tid + self.value = value + } + } + + await withTaskGroup(of: Item.self) { group in + group.addTask { + let tid = swjs_get_worker_thread_id() + return Item("main", tid, 0) + } + + let numberOffloadedTasks = 10 + for i in 0..(boxing: ()) + } + let executor = try await WebWorkerTaskExecutor(numberOfThreads: 1) + XCTAssertNil(Check.value.wrappedValue) + Check.value.wrappedValue = 42 + XCTAssertEqual(Check.value.wrappedValue, 42) + + let task = Task(executorPreference: executor) { + XCTAssertNil(Check.value.wrappedValue) + Check.value.wrappedValue = 100 + XCTAssertEqual(Check.value.wrappedValue, 100) + return Check.value.wrappedValue + } + let result = await task.value + XCTAssertEqual(result, 100) + XCTAssertEqual(Check.value.wrappedValue, 42) + executor.terminate() + } + + func testLazyThreadLocalPerThreadInitialization() async throws { + struct Check { + nonisolated(unsafe) static var valueToInitialize = 42 + nonisolated(unsafe) static var countOfInitialization = 0 + static let value = LazyThreadLocal(initialize: { + countOfInitialization += 1 + return valueToInitialize + }) + } + let executor = try await WebWorkerTaskExecutor(numberOfThreads: 1) + XCTAssertEqual(Check.countOfInitialization, 0) + XCTAssertEqual(Check.value.wrappedValue, 42) + XCTAssertEqual(Check.countOfInitialization, 1) + + Check.valueToInitialize = 100 + + let task = Task(executorPreference: executor) { + XCTAssertEqual(Check.countOfInitialization, 1) + XCTAssertEqual(Check.value.wrappedValue, 100) + XCTAssertEqual(Check.countOfInitialization, 2) + return Check.value.wrappedValue + } + let result = await task.value + XCTAssertEqual(result, 100) + XCTAssertEqual(Check.countOfInitialization, 2) + executor.terminate() + } + + func testJSValueDecoderOnWorker() async throws { + struct DecodeMe: Codable { + struct Prop1: Codable { + let nested_prop: Int + } + + let prop_1: Prop1 + let prop_2: Int + let prop_3: Bool + let prop_7: Float + let prop_8: String + let prop_9: [String] + } + + 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"] + } + """ + let object = JSObject.global.JSON.parse(json) + let decoder = JSValueDecoder() + let result = try decoder.decode(DecodeMe.self, from: object) + XCTAssertEqual(result.prop_1.nested_prop, 42) + XCTAssertEqual(result.prop_2, 100) + XCTAssertEqual(result.prop_3, true) + XCTAssertEqual(result.prop_7, 3.14) + XCTAssertEqual(result.prop_8, "Hello, World!") + XCTAssertEqual(result.prop_9, ["a", "b", "c"]) + } + // Run the job on the main thread first to initialize the object cache + try decodeJob() + + let executor = try await WebWorkerTaskExecutor(numberOfThreads: 1) + defer { executor.terminate() } + let task = Task(executorPreference: executor) { + // Run the job on the worker thread to test the object cache + // is not shared with the main thread + try decodeJob() + } + try await task.value + } + + func testJSArrayCountOnWorker() async throws { + let executor = try await WebWorkerTaskExecutor(numberOfThreads: 1) + func check() { + let object = JSObject.global.Array.function!.new(1, 2, 3, 4, 5) + let array = JSArray(object)! + XCTAssertEqual(array.count, 5) + } + check() + let task = Task(executorPreference: executor) { + check() + } + await task.value + executor.terminate() + } + + func testSendingWithoutReceiving() async throws { + let object = JSObject.global.Object.function!.new() + _ = JSSending.transfer(object) + _ = JSSending(object) + } + + func testTransferMainToWorker() async throws { + let Uint8Array = JSObject.global.Uint8Array.function! + let buffer = Uint8Array.new(100).buffer.object! + let transferring = JSSending.transfer(buffer) + let executor = try await WebWorkerTaskExecutor(numberOfThreads: 1) + let task = Task(executorPreference: executor) { + let buffer = try await transferring.receive() + return buffer.byteLength.number! + } + let byteLength = try await task.value + XCTAssertEqual(byteLength, 100) + + // Transferred Uint8Array should have 0 byteLength + XCTAssertEqual(buffer.byteLength.number!, 0) + } + + func testTransferWorkerToMain() async throws { + let executor = try await WebWorkerTaskExecutor(numberOfThreads: 1) + let task = Task(executorPreference: executor) { + let Uint8Array = JSObject.global.Uint8Array.function! + let buffer = Uint8Array.new(100).buffer.object! + let transferring = JSSending.transfer(buffer) + return transferring + } + let transferring = await task.value + let buffer = try await transferring.receive() + XCTAssertEqual(buffer.byteLength.number!, 100) + } + + func testTransferNonTransferable() async throws { + let object = JSObject.global.Object.function!.new() + let transferring = JSSending.transfer(object) + let executor = try await WebWorkerTaskExecutor(numberOfThreads: 1) + let task = Task(executorPreference: executor) { + do { + _ = try await transferring.receive() + return nil + } catch let error as JSException { + return error.thrownValue.description + } + } + guard let jsErrorMessage = try await task.value else { + XCTFail("Should throw an error") + return + } + 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")) + // } + + func testCloneMultipleTimes() async throws { + let executor = try await WebWorkerTaskExecutor(numberOfThreads: 1) + let object = JSObject.global.Object.function!.new() + object["test"] = "Hello, World!" + + for _ in 0..<2 { + let cloning = JSSending(object) + let task = Task(executorPreference: executor) { + let object = try await cloning.receive() + return object["test"].string! + } + let result = try await task.value + XCTAssertEqual(result, "Hello, World!") + } + } + + func testTransferBetweenWorkers() async throws { + let executor1 = try await WebWorkerTaskExecutor(numberOfThreads: 1) + let executor2 = try await WebWorkerTaskExecutor(numberOfThreads: 1) + let task = Task(executorPreference: executor1) { + let Uint8Array = JSObject.global.Uint8Array.function! + let buffer = Uint8Array.new(100).buffer.object! + let transferring = JSSending.transfer(buffer) + return transferring + } + let transferring = await task.value + let task2 = Task(executorPreference: executor2) { + let buffer = try await transferring.receive() + return buffer.byteLength.number! + } + let byteLength = try await task2.value + XCTAssertEqual(byteLength, 100) + } + + func testTransferMultipleItems() async throws { + let executor = try await WebWorkerTaskExecutor(numberOfThreads: 1) + let Uint8Array = JSObject.global.Uint8Array.function! + let buffer1 = Uint8Array.new(10).buffer.object! + let buffer2 = Uint8Array.new(11).buffer.object! + let transferring1 = JSSending.transfer(buffer1) + let transferring2 = JSSending.transfer(buffer2) + let task = Task(executorPreference: executor) { + let (buffer1, buffer2) = try await JSSending.receive(transferring1, transferring2) + return (buffer1.byteLength.number!, buffer2.byteLength.number!) + } + let (byteLength1, byteLength2) = try await task.value + XCTAssertEqual(byteLength1, 10) + XCTAssertEqual(byteLength2, 11) + XCTAssertEqual(buffer1.byteLength.number!, 0) + XCTAssertEqual(buffer2.byteLength.number!, 0) + + // Mix transferring and cloning + let buffer3 = Uint8Array.new(12).buffer.object! + let buffer4 = Uint8Array.new(13).buffer.object! + let transferring3 = JSSending.transfer(buffer3) + let cloning4 = JSSending(buffer4) + let task2 = Task(executorPreference: executor) { + let (buffer3, buffer4) = try await JSSending.receive(transferring3, cloning4) + return (buffer3.byteLength.number!, buffer4.byteLength.number!) + } + let (byteLength3, byteLength4) = try await task2.value + XCTAssertEqual(byteLength3, 12) + XCTAssertEqual(byteLength4, 13) + XCTAssertEqual(buffer3.byteLength.number!, 0) + XCTAssertEqual(buffer4.byteLength.number!, 13) + } + + func testCloneObjectToWorker() async throws { + let executor = try await WebWorkerTaskExecutor(numberOfThreads: 1) + let object = JSObject.global.Object.function!.new() + object["test"] = "Hello, World!" + let cloning = JSSending(object) + let task = Task(executorPreference: executor) { + let object = try await cloning.receive() + return object["test"].string! + } + let result = try await task.value + XCTAssertEqual(result, "Hello, World!") + + // Further access to the original object is valid + XCTAssertEqual(object["test"].string!, "Hello, World!") + } + + // 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 new file mode 100644 index 000000000..a4649879e --- /dev/null +++ b/Tests/JavaScriptKitTests/JSTypedArrayTests.swift @@ -0,0 +1,126 @@ +import JavaScriptKit +import XCTest + +final class JSTypedArrayTests: XCTestCase { + func testEmptyArray() { + _ = JSTypedArray([]) + _ = JSTypedArray([]) + _ = JSTypedArray([Int8]()) + _ = JSTypedArray([UInt8]()) + _ = JSUInt8ClampedArray([UInt8]()) + _ = JSTypedArray([Int16]()) + _ = JSTypedArray([UInt16]()) + _ = JSTypedArray([Int32]()) + _ = JSTypedArray([UInt32]()) + _ = JSTypedArray([Float32]()) + _ = JSTypedArray([Float64]()) + } + + func testTypedArray() { + func checkArray(_ array: [T]) where T: TypedArrayElement & Equatable, T.Element == T { + XCTAssertEqual(toString(JSTypedArray(array).jsValue.object!), jsStringify(array)) + checkArrayUnsafeBytes(array) + } + + func toString(_ object: T) -> String { + return object.toString!().string! + } + + func jsStringify(_ array: [Any]) -> String { + array.map({ String(describing: $0) }).joined(separator: ",") + } + + 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) + XCTAssertEqual(typedArray[12], 12) + XCTAssertEqual(numbers.count, typedArray.lengthInBytes) + + let numbersSet = Set(0...255) + let typedArrayFromSet = JSTypedArray(numbersSet) + XCTAssertEqual(typedArrayFromSet.jsObject.length, 256) + XCTAssertEqual(typedArrayFromSet.lengthInBytes, 256 * MemoryLayout.size) + + checkArray([0, .max, 127, 1] as [UInt8]) + checkArray([0, 1, .max, .min, -1] as [Int8]) + + checkArray([0, .max, 255, 1] as [UInt16]) + checkArray([0, 1, .max, .min, -1] as [Int16]) + + checkArray([0, .max, 255, 1] as [UInt32]) + checkArray([0, 1, .max, .min, -1] as [Int32]) + + checkArray([0, .max, 255, 1] as [UInt]) + checkArray([0, 1, .max, .min, -1] as [Int]) + + let float32Array: [Float32] = [ + 0, 1, .pi, .greatestFiniteMagnitude, .infinity, .leastNonzeroMagnitude, + .leastNormalMagnitude, 42, + ] + let jsFloat32Array = JSTypedArray(float32Array) + for (i, num) in float32Array.enumerated() { + XCTAssertEqual(num, jsFloat32Array[i]) + } + + let float64Array: [Float64] = [ + 0, 1, .pi, .greatestFiniteMagnitude, .infinity, .leastNonzeroMagnitude, + .leastNormalMagnitude, 42, + ] + let jsFloat64Array = JSTypedArray(float64Array) + for (i, num) in float64Array.enumerated() { + XCTAssertEqual(num, jsFloat64Array[i]) + } + } + + func testTypedArrayMutation() { + let array = JSTypedArray(length: 100) + for i in 0..<100 { + array[i] = i + } + for i in 0..<100 { + XCTAssertEqual(i, array[i]) + } + + func toString(_ object: T) -> String { + return object.toString!().string! + } + + func jsStringify(_ array: [Any]) -> String { + array.map({ String(describing: $0) }).joined(separator: ",") + } + + 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 new file mode 100644 index 000000000..246df522a --- /dev/null +++ b/Tests/JavaScriptKitTests/JavaScriptKitTests.swift @@ -0,0 +1,675 @@ +import JavaScriptKit +import XCTest + +class JavaScriptKitTests: XCTestCase { + func testLiteralConversion() { + let global = JSObject.global + let inputs: [JSValue] = [ + .boolean(true), + .boolean(false), + .string("foobar"), + .string("👨‍👩‍👧‍👧 Family Emoji"), + .number(0), + .number(Double(Int32.max)), + .number(Double(Int32.min)), + .number(Double.infinity), + .number(Double.nan), + .null, + .undefined, + ] + for (index, input) in inputs.enumerated() { + let prop = JSString("prop_\(index)") + setJSValue(this: global, name: prop, value: input) + let got = getJSValue(this: global, name: prop) + switch (got, input) { + case (.number(let lhs), .number(let rhs)): + // Compare bitPattern because nan == nan is always false + XCTAssertEqual(lhs.bitPattern, rhs.bitPattern) + default: + XCTAssertEqual(got, input) + } + } + } + + func testObjectConversion() { + // Notes: globalObject1 is defined in JavaScript environment + // + // ```js + // global.globalObject1 = { + // "prop_1": { + // "nested_prop": 1, + // }, + // "prop_2": 2, + // "prop_3": true, + // "prop_4": [ + // 3, 4, "str_elm_1", 5, + // ], + // ... + // } + // ``` + + let globalObject1 = getJSValue(this: .global, name: "globalObject1") + let globalObject1Ref = try! XCTUnwrap(globalObject1.object) + let prop_1 = getJSValue(this: globalObject1Ref, name: "prop_1") + let prop_1Ref = try! XCTUnwrap(prop_1.object) + let nested_prop = getJSValue(this: prop_1Ref, name: "nested_prop") + XCTAssertEqual(nested_prop, .number(1)) + let prop_2 = getJSValue(this: globalObject1Ref, name: "prop_2") + XCTAssertEqual(prop_2, .number(2)) + let prop_3 = getJSValue(this: globalObject1Ref, name: "prop_3") + XCTAssertEqual(prop_3, .boolean(true)) + let prop_4 = getJSValue(this: globalObject1Ref, name: "prop_4") + let prop_4Array = try! XCTUnwrap(prop_4.object) + let expectedProp_4: [JSValue] = [ + .number(3), .number(4), .string("str_elm_1"), .null, .undefined, .number(5), + ] + for (index, expectedElement) in expectedProp_4.enumerated() { + 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) + let prop_2 = getJSValue(this: globalObject1Ref, name: "prop_2") + XCTAssertEqual(Int.construct(from: prop_2), 2) + let prop_3 = getJSValue(this: globalObject1Ref, name: "prop_3") + XCTAssertEqual(Bool.construct(from: prop_3), true) + 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), + ] { + XCTAssertNil(Int.construct(from: source)) + XCTAssertNil(Int8.construct(from: source)) + XCTAssertNil(Int16.construct(from: source)) + XCTAssertNil(Int32.construct(from: source)) + XCTAssertNil(Int64.construct(from: source)) + XCTAssertNil(UInt.construct(from: source)) + XCTAssertNil(UInt8.construct(from: source)) + XCTAssertNil(UInt16.construct(from: source)) + XCTAssertNil(UInt32.construct(from: source)) + XCTAssertNil(UInt64.construct(from: source)) + } + } + + func testArrayIterator() { + let globalObject1 = getJSValue(this: .global, name: "globalObject1") + let globalObject1Ref = try! XCTUnwrap(globalObject1.object) + let prop_4 = getJSValue(this: globalObject1Ref, name: "prop_4") + let array1 = try! XCTUnwrap(prop_4.array) + let expectedProp_4: [JSValue] = [ + .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) + let prop_4 = getJSValue(this: globalObject1Ref, name: "prop_4") + let array1 = try! XCTUnwrap(prop_4.array) + let expectedProp_4: [JSValue] = [ + .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, + ] + 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 + let prop_7: Float + } + let decoder = JSValueDecoder() + let rawGlobalObject1 = getJSValue(this: .global, name: "globalObject1") + let globalObject1 = try! decoder.decode(GlobalObject1.self, from: rawGlobalObject1) + XCTAssertEqual(globalObject1.prop_1.nested_prop, 1) + XCTAssertEqual(globalObject1.prop_2, 2) + XCTAssertEqual(globalObject1.prop_3, true) + XCTAssertEqual(globalObject1.prop_7, 3.14) + } + + func testFunctionCall() { + // Notes: globalObject1 is defined in JavaScript environment + // + // ```js + // global.globalObject1 = { + // ... + // "prop_5": { + // "func1": function () { return }, + // "func2": function () { return 1 }, + // "func3": function (n) { return n * 2 }, + // "func4": function (a, b, c) { return a + b + c }, + // "func5": function (x) { return "Hello, " + x }, + // "func6": function (c, a, b) { + // if (c) { return a } else { return b } + // }, + // } + // ... + // } + // ``` + + 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) + XCTAssertEqual(func2(), .number(1)) + let func3 = try! XCTUnwrap(getJSValue(this: prop_5Ref, name: "func3").function) + XCTAssertEqual(func3(2), .number(4)) + let func4 = try! XCTUnwrap(getJSValue(this: prop_5Ref, name: "func4").function) + XCTAssertEqual(func4(2, 3, 4), .number(9)) + XCTAssertEqual(func4(2, 3, 4, 5), .number(9)) + let func5 = try! XCTUnwrap(getJSValue(this: prop_5Ref, name: "func5").function) + XCTAssertEqual(func5("World!"), .string("Hello, World!")) + let func6 = try! XCTUnwrap(getJSValue(this: prop_5Ref, name: "func6").function) + XCTAssertEqual(func6(true, 1, 2), .number(1)) + 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 + c1.release() + #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 + c1.release() + #endif + } + + do { + let c1 = JSClosure { _ in .undefined } + XCTAssertEqual(c1(), .undefined) + } + + do { + let c1 = JSClosure { _ in .number(4) } + XCTAssertEqual(c1(), .number(4)) + } + } + + func testHostFunctionRegistration() { + // ```js + // global.globalObject1 = { + // ... + // "prop_6": { + // "call_host_1": function() { + // return global.globalObject1.prop_6.host_func_1() + // } + // } + // } + // ``` + let globalObject1 = getJSValue(this: .global, name: "globalObject1") + 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 + hostFunc1.release() + #endif + + let evalClosure = JSObject.global.globalObject1.eval_closure.function! + let hostFunc2 = JSClosure { (arguments) -> JSValue in + if let input = arguments[0].number { + return .number(input * 2) + } else { + return .string(String(describing: arguments[0])) + } + } + + XCTAssertEqual(evalClosure(hostFunc2, 3), .number(6)) + XCTAssertTrue(evalClosure(hostFunc2, true).string != nil) + + #if JAVASCRIPTKIT_WITHOUT_WEAKREFS + hostFunc2.release() + #endif + } + + func testNewObjectConstruction() { + // ```js + // global.Animal = function(name, age, isCat) { + // this.name = name + // this.age = age + // this.bark = () => { + // return isCat ? "nyan" : "wan" + // } + // } + // ``` + let objectConstructor = try! XCTUnwrap(getJSValue(this: .global, name: "Animal").function) + let cat1 = objectConstructor.new("Tama", 3, true) + XCTAssertEqual(getJSValue(this: cat1, name: "name"), .string("Tama")) + XCTAssertEqual(getJSValue(this: cat1, name: "age"), .number(3)) + XCTAssertEqual(cat1.isInstanceOf(objectConstructor), true) + 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: {}, + fn: () => {}, + sym: Symbol("s"), + bi: BigInt(3) + }; + ``` + */ + 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 + let object: JSValue = js.obj + 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) { + // this.name = name + // this.age = age + // this.bark = () => { + // return isCat ? "nyan" : "wan" + // } + // this.isCat = isCat + // this.getIsCat = function() { + // return this.isCat + // } + // } + // ``` + let objectConstructor = try! XCTUnwrap(getJSValue(this: .global, name: "Animal").function) + let cat1 = objectConstructor.new("Tama", 3, true) + 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")) + _ = cat1.setName!("Tora") + XCTAssertEqual(getJSValue(this: cat1, name: "name"), .string("Tora")) + _ = cat1Value.setName("Chibi") + XCTAssertEqual(getJSValue(this: cat1, name: "name"), .string("Chibi")) + } + + func testJSObjectConversion() { + let array1 = [1, 2, 3] + let jsArray1 = array1.jsValue.object! + XCTAssertEqual(jsArray1.length, .number(3)) + 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)) + XCTAssertEqual(jsArray2[0], .number(1)) + XCTAssertEqual(jsArray2[1], .string("str")) + XCTAssertEqual(jsArray2[2], .boolean(false)) + _ = 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, + ] + let jsDict1 = dict1.jsValue.object! + XCTAssertEqual(jsDict1.prop1, .number(1)) + XCTAssertEqual(jsDict1.prop2, .string("foo")) + } + + func testObjectRefLifetime() { + // ```js + // global.globalObject1 = { + // "prop_1": { + // "nested_prop": 1, + // }, + // "prop_2": 2, + // "prop_3": true, + // "prop_4": [ + // 3, 4, "str_elm_1", 5, + // ], + // ... + // } + // ``` + + 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 + identity.release() + #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) + XCTAssertEqual(date1.date, date2.date) + XCTAssertEqual(date1.day, date2.day) + XCTAssertEqual(date1.hours, date2.hours) + XCTAssertEqual(date1.minutes, date2.minutes) + XCTAssertEqual(date1.seconds, date2.seconds) + XCTAssertEqual(date1.milliseconds, date2.milliseconds) + XCTAssertEqual(date1.utcFullYear, date2.utcFullYear) + XCTAssertEqual(date1.utcMonth, date2.utcMonth) + XCTAssertEqual(date1.utcDate, date2.utcDate) + XCTAssertEqual(date1.utcDay, date2.utcDay) + XCTAssertEqual(date1.utcHours, date2.utcHours) + XCTAssertEqual(date1.utcMinutes, date2.utcMinutes) + 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) + XCTAssertEqual(date3.utcMonth, 0) + XCTAssertEqual(date3.utcDate, 1) + // the epoch date was on Friday + XCTAssertEqual(date3.utcDay, 4) + XCTAssertEqual(date3.utcHours, 0) + XCTAssertEqual(date3.utcMinutes, 0) + 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" + let error = JSError(message: message) + XCTAssertEqual(error.name, "Error") + XCTAssertEqual(error.message, message) + XCTAssertEqual(error.description, expectedDescription) + XCTAssertFalse(error.stack?.isEmpty ?? true) + XCTAssertNil(JSError(from: .string("error"))?.description) + XCTAssertEqual(JSError(from: .object(error.jsObject))?.description, expectedDescription) + } + + func testJSValueAccessor() { + 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 = { + // ... + // prop_9: { + // func1: function () { + // throw new Error(); + // }, + // func2: function () { + // throw "String Error"; + // }, + // func3: function () { + // throw 3.0 + // }, + // }, + // ... + // } + // ``` + // + 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)) + XCTAssertThrowsError(try Animal.throws.new("Tama", -3, true)) { error in + XCTAssertTrue(error is JSException) + let errorObject = JSError(from: (error as! JSException).thrownValue) + 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 + // Object.defineProperty(hasInstanceClass, Symbol.hasInstance, { value: () => true }) + let hasInstanceObject = JSObject.global.Object.function!.new() + hasInstanceObject.prop = JSClosure { _ in .undefined }.jsValue + let hasInstanceClass = hasInstanceObject.prop.function! + let propertyDescriptor = JSObject.global.Object.function!.new() + propertyDescriptor.value = JSClosure { _ in .boolean(true) }.jsValue + _ = JSObject.global.Object.function!.defineProperty!( + 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! + XCTAssertEqual(jsArray1.length, .number(3)) + 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)) + XCTAssertEqual(jsArray2[0], .number(1)) + XCTAssertEqual(jsArray2[1], .string("str")) + XCTAssertEqual(jsArray2[2], .boolean(false)) + _ = 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, + ] + let jsDict1 = dict1.jsValue.object! + XCTAssertEqual(jsDict1.prop1, .number(1)) + XCTAssertEqual(jsDict1.prop2, .string("foo")) + } + + func testGrowMemory() { + // If WebAssembly.Memory is not accessed correctly (i.e. creating a new view each time), + // this test will fail with `TypeError: Cannot perform Construct on a detached ArrayBuffer`, + // since asking to grow memory will detach the backing ArrayBuffer. + // See https://github.com/swiftwasm/JavaScriptKit/pull/153 + let string = "Hello" + let jsString = JSValue.string(string) + _ = growMemory(0, 1) + XCTAssertEqual(string, jsString.description) + } + + func testHashableConformance() { + let globalObject1 = JSObject.global.console.object! + let globalObject2 = JSObject.global.console.object! + XCTAssertEqual(globalObject1.hashValue, globalObject2.hashValue) + // These are 2 different objects in Swift referencing the same object in JavaScript + XCTAssertNotEqual(ObjectIdentifier(globalObject1), ObjectIdentifier(globalObject2)) + + let objectConstructor = JSObject.global.Object.function! + let obj = objectConstructor.new() + obj.a = 1.jsValue + let firstHash = obj.hashValue + obj.b = 2.jsValue + let secondHash = obj.hashValue + XCTAssertEqual(firstHash, secondHash) + } +} + +@_extern(c, "llvm.wasm.memory.grow.i32") +func growMemory(_ memory: Int32, _ pages: Int32) -> Int32 diff --git a/Tests/JavaScriptKitTests/ThreadLocalTests.swift b/Tests/JavaScriptKitTests/ThreadLocalTests.swift new file mode 100644 index 000000000..d1d736b8b --- /dev/null +++ b/Tests/JavaScriptKitTests/ThreadLocalTests.swift @@ -0,0 +1,47 @@ +import XCTest + +@testable import JavaScriptKit + +final class ThreadLocalTests: XCTestCase { + class MyHeapObject {} + struct MyStruct { + var object: MyHeapObject + } + + func testLeak() throws { + struct Check { + static let value = ThreadLocal() + static let value2 = ThreadLocal(boxing: ()) + } + weak var weakObject: MyHeapObject? + do { + let object = MyHeapObject() + weakObject = object + Check.value.wrappedValue = object + XCTAssertNotNil(Check.value.wrappedValue) + XCTAssertTrue(Check.value.wrappedValue === object) + Check.value.wrappedValue = nil + } + XCTAssertNil(weakObject) + + weak var weakObject2: MyHeapObject? + do { + let object = MyHeapObject() + weakObject2 = object + Check.value2.wrappedValue = MyStruct(object: object) + XCTAssertNotNil(Check.value2.wrappedValue) + XCTAssertTrue(Check.value2.wrappedValue!.object === object) + Check.value2.wrappedValue = nil + } + XCTAssertNil(weakObject2) + } + + func testLazyThreadLocal() throws { + struct Check { + static let value = LazyThreadLocal(initialize: { MyHeapObject() }) + } + let object1 = Check.value.wrappedValue + let object2 = Check.value.wrappedValue + XCTAssertTrue(object1 === object2) + } +} diff --git a/Tests/prelude.mjs b/Tests/prelude.mjs new file mode 100644 index 000000000..1e12d3755 --- /dev/null +++ b/Tests/prelude.mjs @@ -0,0 +1,174 @@ +/** @type {import('./../.build/plugins/PackageToJS/outputs/PackageTests/test.d.ts').Prelude["setupOptions"]} */ +export function setupOptions(options, context) { + Error.stackTraceLimit = 100; + setupTestGlobals(globalThis); + return { + ...options, + addToCoreImports(importObject, getInstance, getExports) { + options.addToCoreImports?.(importObject); + 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) { + 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: { + nested_prop: 1, + }, + prop_2: 2, + prop_3: true, + prop_4: [3, 4, "str_elm_1", null, undefined, 5], + prop_5: { + func1: function () { + return; + }, + func2: function () { + return 1; + }, + func3: function (n) { + return n * 2; + }, + func4: function (a, b, c) { + return a + b + c; + }, + func5: function (x) { + return "Hello, " + x; + }, + func6: function (c, a, b) { + if (c) { + return a; + } else { + return b; + } + }, + }, + prop_6: { + call_host_1: () => { + return global.globalObject1.prop_6.host_func_1(); + }, + }, + prop_7: 3.14, + prop_8: [0, , 2, 3, , , 6], + prop_9: { + func1: function () { + throw new Error(); + }, + func2: function () { + throw "String Error"; + }, + func3: function () { + throw 3.0; + }, + }, + eval_closure: function (fn) { + return fn(arguments[1]); + }, + observable_obj: { + set_called: false, + target: new Proxy( + { + nested: {}, + }, + { + set(target, key, value) { + global.globalObject1.observable_obj.set_called = true; + target[key] = value; + return true; + }, + } + ), + }, + }; + + global.Animal = function (name, age, isCat) { + if (age < 0) { + throw new Error("Invalid age " + age); + } + this.name = name; + this.age = age; + this.bark = () => { + return isCat ? "nyan" : "wan"; + }; + this.isCat = isCat; + this.getIsCat = function () { + return this.isCat; + }; + this.setName = function (name) { + this.name = name; + }; + }; + + global.callThrowingClosure = (c) => { + try { + c(); + } catch (error) { + return error; + } + }; + + global.objectDecodingTest = { + obj: {}, + fn: () => { }, + sym: Symbol("s"), + bi: BigInt(3) + }; +} 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 c0cbbbdb2..000000000 --- a/ci/perf-tester/package-lock.json +++ /dev/null @@ -1,868 +0,0 @@ -{ - "name": "perf-tester", - "lockfileVersion": 2, - "requires": true, - "packages": { - "": { - "devDependencies": { - "@actions/core": "^1.2.6", - "@actions/exec": "^1.0.3", - "@actions/github": "^2.0.1" - } - }, - "node_modules/@actions/core": { - "version": "1.2.6", - "resolved": "https://registry.npmjs.org/@actions/core/-/core-1.2.6.tgz", - "integrity": "sha512-ZQYitnqiyBc3D+k7LsgSBmMDVkOVidaagDG7j3fOym77jNunWRuYx7VSHa9GNfFZh+zh61xsCjRj4JxMZlDqTA==", - "dev": true - }, - "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/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/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/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.2.6", - "resolved": "https://registry.npmjs.org/@actions/core/-/core-1.2.6.tgz", - "integrity": "sha512-ZQYitnqiyBc3D+k7LsgSBmMDVkOVidaagDG7j3fOym77jNunWRuYx7VSHa9GNfFZh+zh61xsCjRj4JxMZlDqTA==", - "dev": true - }, - "@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/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 - }, - "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" - } - }, - "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 97718ab9a..000000000 --- a/ci/perf-tester/package.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "private": true, - "main": "src/index.js", - "devDependencies": { - "@actions/core": "^1.2.6", - "@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 f9495303d..000000000 --- a/ci/perf-tester/src/index.js +++ /dev/null @@ -1,256 +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 { - getInput, - runBenchmark, - averageBenchmarks, - toDiff, - diffTable, - toBool, -} = require("./utils.js"); - -const benchmarkParallel = 2; -const benchmarkSerial = 2; -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); -}; - -async function run(octokit, context, token) { - 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 targetted at ${pr.base.ref} (${pr.base.sha})` - ); - - const buildScript = getInput("build-script"); - startGroup(`[current] Build using '${buildScript}'`); - await exec(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 '${buildScript}'`); - await exec(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: parseInt( - getInput("minimum-change-threshold"), - 10 - ), - }); - - let outputRawMarkdown = false; - - const commentInfo = { - ...context.repo, - issue_number: pull_number, - }; - - const comment = { - ...commentInfo, - body: - markdownDiff + - '\n\nperformance-action', - }; - - if (toBool(getInput("use-check"))) { - if (token) { - const finish = await createCheck(octokit, context); - await finish({ - conclusion: "success", - output: { - title: `Compressed Size Action`, - summary: markdownDiff, - }, - }); - } else { - outputRawMarkdown = true; - } - } else { - 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" && - /[\s\n]*performance-action/.test(c.body) - ) { - 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!"); -} - -// create a check and return a function that updates (completes) it -async function createCheck(octokit, context) { - const check = await octokit.checks.create({ - ...context.repo, - name: "Compressed Size", - head_sha: context.payload.pull_request.head.sha, - status: "in_progress", - }); - - return async (details) => { - await octokit.checks.update({ - ...context.repo, - check_run_id: check.data.id, - completed_at: new Date().toISOString(), - status: "completed", - ...details, - }); - }; -} - -(async () => { - try { - const token = getInput("repo-token", { required: true }); - const octokit = new GitHub(token); - await run(octokit, context, token); - } 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 49fce603e..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 fs = require("fs"); -const { exec } = require("@actions/exec"); - -const getInput = (key) => - ({ - "build-script": "make bootstrap benchmark_setup", - benchmark: "make -s run_benchmark", - "minimum-change-threshold": 5, - "use-check": "no", - "repo-token": process.env.GITHUB_TOKEN, - }[key]); -exports.getInput = getInput; - -exports.runBenchmark = async () => { - let benchmarkBuffers = []; - await exec(getInput("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 ? "+" : "") + delta.toLocaleString("en-US") + "ms"; - 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 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, - time.toLocaleString("en-US") + "ms", - getDeltaText(delta, difference), - iconForDifference(difference), - ]; - 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 (showTotal) { - const totalDifference = ((totalDelta / totalTime) * 100) | 0; - let totalDeltaText = getDeltaText(totalDelta, totalDifference); - let totalIcon = iconForDifference(totalDifference); - out = `**Total Time:** ${totalTime.toLocaleString( - "en-US" - )}ms\n\n${out}`; - out = `**Time Change:** ${totalDeltaText} ${totalIcon}\n\n${out}`; - } - - return out; -}; - -/** - * Convert a string "true"/"yes"/"1" argument value to a boolean - * @param {string} v - */ -exports.toBool = (v) => /^(1|true|yes)$/.test(v); diff --git a/package-lock.json b/package-lock.json index d39bc8474..55981f7bd 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,67 +1,413 @@ { "name": "javascript-kit-swift", - "version": "0.12.0", - "lockfileVersion": 2, + "version": "0.0.0", + "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "javascript-kit-swift", - "version": "0.12.0", + "version": "0.0.0", "license": "MIT", "devDependencies": { - "@rollup/plugin-typescript": "^8.3.0", - "prettier": "2.5.1", - "rollup": "^2.63.0", - "tslib": "^2.3.1", - "typescript": "^4.5.5" + "@bjorn3/browser_wasi_shim": "^0.4.1", + "@rollup/plugin-typescript": "^12.1.2", + "@types/node": "^22.13.14", + "playwright": "^1.51.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.0", - "resolved": "https://registry.npmjs.org/@rollup/plugin-typescript/-/plugin-typescript-8.3.0.tgz", - "integrity": "sha512-I5FpSvLbtAdwJ+naznv+B4sjXZUcIvLLceYpITAn7wAP8W0wqc5noLdGIp9HGVntNhRWXctwPYrSSFQxtl0FPA==", + "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", @@ -69,6 +415,7 @@ "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", "dev": true, "hasInstallScript": true, + "license": "MIT", "optional": true, "os": [ "darwin" @@ -78,102 +425,224 @@ } }, "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.1", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.51.1.tgz", + "integrity": "sha512-kkx+MB2KQRkyxjYPc3a0wLZZoDczmppyGJIvQ43l+aZihkaVvmu/21kiyaHeHjiFxjxNNFnUncKmcGIyOojsaw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "playwright-core": "1.51.1" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "fsevents": "2.3.2" + } + }, + "node_modules/playwright-core": { + "version": "1.51.1", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.51.1.tgz", + "integrity": "sha512-/crRMj8+j/Nq5s8QcvegseuyeZPxpQCZb6HNk3Sos3BlZyAknRjoyJPFWkpNn8v0+P3WiwqFF8P+zQo4eqiNuw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "playwright-core": "cli.js" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/prettier": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/prettier/-/prettier-2.5.1.tgz", - "integrity": "sha512-vBZcPRUR5MZJwoyi3ZoyQlc1rXeEck8KgeC9AwwOn+exuxLxq5toTRDTSaVrXHxelDMHy9zlicw8u66yxoSUFg==", + "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.63.0", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-2.63.0.tgz", - "integrity": "sha512-nps0idjmD+NXl6OREfyYXMn/dar3WGcyKn+KBzPdaLecub3x/LrId0wUcthcr8oZUAcZAR8NKcfGGFlNgGL1kQ==", + "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" }, @@ -182,144 +651,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.5.5", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.5.5.tgz", - "integrity": "sha512-TCTIul70LyWe6IJWT8QSYeA54WQe8EjQFU4wY52Fasj5UKx88LNYKCgBEHcOMOrFF1rKGbD8v/xcNWVUq9SymA==", + "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.0", - "resolved": "https://registry.npmjs.org/@rollup/plugin-typescript/-/plugin-typescript-8.3.0.tgz", - "integrity": "sha512-I5FpSvLbtAdwJ+naznv+B4sjXZUcIvLLceYpITAn7wAP8W0wqc5noLdGIp9HGVntNhRWXctwPYrSSFQxtl0FPA==", - "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" - } - }, - "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 - }, - "prettier": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/prettier/-/prettier-2.5.1.tgz", - "integrity": "sha512-vBZcPRUR5MZJwoyi3ZoyQlc1rXeEck8KgeC9AwwOn+exuxLxq5toTRDTSaVrXHxelDMHy9zlicw8u66yxoSUFg==", - "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" + "node": ">=14.17" } }, - "rollup": { - "version": "2.63.0", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-2.63.0.tgz", - "integrity": "sha512-nps0idjmD+NXl6OREfyYXMn/dar3WGcyKn+KBzPdaLecub3x/LrId0wUcthcr8oZUAcZAR8NKcfGGFlNgGL1kQ==", - "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.5.5", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.5.5.tgz", - "integrity": "sha512-TCTIul70LyWe6IJWT8QSYeA54WQe8EjQFU4wY52Fasj5UKx88LNYKCgBEHcOMOrFF1rKGbD8v/xcNWVUq9SymA==", + "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 47da6fe88..867adb988 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "javascript-kit-swift", - "version": "0.12.0", + "version": "0.0.0", "description": "A runtime library of JavaScriptKit which is Swift framework to interact with JavaScript through WebAssembly.", "main": "Runtime/lib/index.js", "module": "Runtime/lib/index.mjs", @@ -34,10 +34,13 @@ "author": "swiftwasm", "license": "MIT", "devDependencies": { - "@rollup/plugin-typescript": "^8.3.0", - "prettier": "2.5.1", - "rollup": "^2.63.0", - "tslib": "^2.3.1", - "typescript": "^4.5.5" + "@bjorn3/browser_wasi_shim": "^0.4.1", + "@rollup/plugin-typescript": "^12.1.2", + "@types/node": "^22.13.14", + "playwright": "^1.51.0", + "prettier": "3.5.3", + "rollup": "^4.37.0", + "rollup-plugin-dts": "^6.2.1", + "typescript": "^5.8.2" } } diff --git a/scripts/install-toolchain.sh b/scripts/install-toolchain.sh deleted file mode 100755 index ef085aaaf..000000000 --- a/scripts/install-toolchain.sh +++ /dev/null @@ -1,42 +0,0 @@ -#!/bin/bash -set -eu - -scripts_dir="$(cd "$(dirname $0)" && pwd)" - -default_swift_version="$(cat $scripts_dir/../.swift-version)" -SWIFT_VERSION="${SWIFT_VERSION:-$default_swift_version}" -swift_tag="swift-$SWIFT_VERSION" - -if [ -z "$(which swiftenv)" ]; then - echo "swiftenv not installed, please install it before this script." - exit 1 -fi - -if [ ! -z "$(swiftenv versions | grep $SWIFT_VERSION)" ]; then - echo "$SWIFT_VERSION is already installed." - exit 0 -fi - -case $(uname -s) in - Darwin) - toolchain_download="$swift_tag-macos_x86_64.pkg" - ;; - Linux) - if [ $(grep RELEASE /etc/lsb-release) == "DISTRIB_RELEASE=18.04" ]; then - toolchain_download="$swift_tag-ubuntu18.04_x86_64.tar.gz" - elif [ $(grep RELEASE /etc/lsb-release) == "DISTRIB_RELEASE=20.04" ]; then - toolchain_download="$swift_tag-ubuntu20.04_x86_64.tar.gz" - else - echo "Unknown Ubuntu version" - exit 1 - fi - ;; - *) - echo "Unrecognised platform $(uname -s)" - exit 1 - ;; -esac - -toolchain_download_url="https://github.com/swiftwasm/swift/releases/download/$swift_tag/$toolchain_download" - -swiftenv install "$toolchain_download_url"