From 6ae7311fc4c711a14956d4186a423e65a4dc76c3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pedro=20G=C3=B3mez?= Date: Thu, 31 Mar 2022 10:34:57 +0200 Subject: [PATCH 001/373] Improve error messages when JS code throws exceptions (#173) * Remove a not neede try/catch when evaluating JSFunction values so we can improve error reporting by not hiding the information we get form the JS crash * Modify JSThrowingFunction to be based on the safe version of _call_function_with_this and _call_function instead of the unsafe version we created * Update swjs_call_function_unsafe to notify wasm when there is an exception Co-authored-by: Jed Fox * Modify swjs_call_function_with_this_unsafe to notify WASM there was an exception as Jed suggested * Recover the already written test cases for JSThrowingFunction and create a new test case for the new functionality * Rename unsafe fuctions to no_catch and simplify invokeNonThrowingJSFunction implementation * Bump swjs_library_version version Co-authored-by: Jed Fox --- .../Sources/PrimaryTests/UnitTestUtils.swift | 6 ++ .../Sources/PrimaryTests/main.swift | 41 +++++++++- IntegrationTests/bin/primary-tests.js | 8 ++ Runtime/src/index.ts | 81 ++++++++++++++++++- Runtime/src/types.ts | 17 ++++ .../FundamentalObjects/JSFunction.swift | 19 ++--- .../JSThrowingFunction.swift | 28 +++++++ Sources/JavaScriptKit/XcodeSupport.swift | 15 ++++ Sources/_CJavaScriptKit/_CJavaScriptKit.c | 2 +- .../_CJavaScriptKit/include/_CJavaScriptKit.h | 33 ++++++++ 10 files changed, 236 insertions(+), 14 deletions(-) diff --git a/IntegrationTests/TestSuites/Sources/PrimaryTests/UnitTestUtils.swift b/IntegrationTests/TestSuites/Sources/PrimaryTests/UnitTestUtils.swift index 87ab72bc4..ab11bc017 100644 --- a/IntegrationTests/TestSuites/Sources/PrimaryTests/UnitTestUtils.swift +++ b/IntegrationTests/TestSuites/Sources/PrimaryTests/UnitTestUtils.swift @@ -103,6 +103,12 @@ func expectThrow(_ body: @autoclosure () throws -> T, file: StaticString = #f throw MessageError("Expect to throw an exception", file: file, line: line, column: column) } +func wrapUnsafeThrowableFunction(_ body: @escaping () -> Void, file: StaticString = #file, line: UInt = #line, column: UInt = #column) throws -> Error { + JSObject.global.callThrowingClosure.function!(JSClosure { _ in + body() + return .undefined + }) +} func expectNotNil(_ value: T?, file: StaticString = #file, line: UInt = #line, column: UInt = #column) throws { switch value { case .some: return diff --git a/IntegrationTests/TestSuites/Sources/PrimaryTests/main.swift b/IntegrationTests/TestSuites/Sources/PrimaryTests/main.swift index e876b60e0..8136f345e 100644 --- a/IntegrationTests/TestSuites/Sources/PrimaryTests/main.swift +++ b/IntegrationTests/TestSuites/Sources/PrimaryTests/main.swift @@ -700,7 +700,6 @@ try test("Exception") { // } // ``` // - let globalObject1 = JSObject.global.globalObject1 let prop_9: JSValue = globalObject1.prop_9 @@ -735,6 +734,46 @@ try test("Exception") { try expectNotNil(errorObject3) } +try test("Unhandled 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 wrapUnsafeThrowableFunction { _ = prop_9.object!.func1!() } + try expectEqual(error1 is JSValue, true) + let errorObject = JSError(from: error1 as! JSValue) + try expectNotNil(errorObject) + + let error2 = try wrapUnsafeThrowableFunction { _ = prop_9.object!.func2!() } + try expectEqual(error2 is JSValue, true) + let errorString = try expectString(error2 as! JSValue) + try expectEqual(errorString, "String Error") + + let error3 = try wrapUnsafeThrowableFunction { _ = prop_9.object!.func3!() } + try expectEqual(error3 is JSValue, true) + let errorNumber = try expectNumber(error3 as! JSValue) + try expectEqual(errorNumber, 3.0) +} + /// 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. diff --git a/IntegrationTests/bin/primary-tests.js b/IntegrationTests/bin/primary-tests.js index 79c62776a..597590bd4 100644 --- a/IntegrationTests/bin/primary-tests.js +++ b/IntegrationTests/bin/primary-tests.js @@ -82,6 +82,14 @@ global.Animal = function (name, age, isCat) { } }; +global.callThrowingClosure = (c) => { + try { + c() + } catch (error) { + return error + } +} + const { startWasiTask } = require("../lib"); startWasiTask("./dist/PrimaryTests.wasm").catch((err) => { diff --git a/Runtime/src/index.ts b/Runtime/src/index.ts index baf9ffd17..06fecc286 100644 --- a/Runtime/src/index.ts +++ b/Runtime/src/index.ts @@ -14,7 +14,7 @@ export class SwiftRuntime { private _instance: WebAssembly.Instance | null; private _memory: Memory | null; private _closureDeallocator: SwiftClosureDeallocator | null; - private version: number = 705; + private version: number = 706; private textDecoder = new TextDecoder("utf-8"); private textEncoder = new TextEncoder(); // Only support utf-8 @@ -226,6 +226,44 @@ export class SwiftRuntime { this.memory ); }, + swjs_call_function_no_catch: ( + ref: ref, + argv: pointer, + argc: number, + kind_ptr: pointer, + payload1_ptr: pointer, + payload2_ptr: pointer + ) => { + const func = this.memory.getObject(ref); + let isException = true; + try { + const result = Reflect.apply( + func, + undefined, + JSValue.decodeArray(argv, argc, this.memory) + ); + JSValue.write( + result, + kind_ptr, + payload1_ptr, + payload2_ptr, + false, + this.memory + ); + isException = false; + } finally { + if (isException) { + JSValue.write( + undefined, + kind_ptr, + payload1_ptr, + payload2_ptr, + true, + this.memory + ); + } + } + }, swjs_call_function_with_this: ( obj_ref: ref, @@ -265,6 +303,47 @@ export class SwiftRuntime { this.memory ); }, + + swjs_call_function_with_this_no_catch: ( + 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 isException = true; + try { + const result = Reflect.apply( + func, + obj, + JSValue.decodeArray(argv, argc, this.memory) + ); + JSValue.write( + result, + kind_ptr, + payload1_ptr, + payload2_ptr, + false, + this.memory + ); + isException = false + } finally { + if (isException) { + JSValue.write( + undefined, + kind_ptr, + payload1_ptr, + payload2_ptr, + true, + this.memory + ); + } + } + }, swjs_call_new: (ref: ref, argv: pointer, argc: number) => { const constructor = this.memory.getObject(ref); const instance = Reflect.construct( diff --git a/Runtime/src/types.ts b/Runtime/src/types.ts index 4f613cc6a..b291cd913 100644 --- a/Runtime/src/types.ts +++ b/Runtime/src/types.ts @@ -58,6 +58,14 @@ export interface ImportedFunctions { payload1_ptr: pointer, payload2_ptr: pointer ): void; + swjs_call_function_no_catch( + 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, @@ -67,6 +75,15 @@ export interface ImportedFunctions { payload1_ptr: pointer, payload2_ptr: pointer ): void; + swjs_call_function_with_this_no_catch( + 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, diff --git a/Sources/JavaScriptKit/FundamentalObjects/JSFunction.swift b/Sources/JavaScriptKit/FundamentalObjects/JSFunction.swift index d9d66ff94..0d3a917c0 100644 --- a/Sources/JavaScriptKit/FundamentalObjects/JSFunction.swift +++ b/Sources/JavaScriptKit/FundamentalObjects/JSFunction.swift @@ -19,7 +19,7 @@ public class JSFunction: JSObject { /// - Returns: The result of this call. @discardableResult public func callAsFunction(this: JSObject? = nil, arguments: [ConvertibleToJSValue]) -> JSValue { - try! invokeJSFunction(self, arguments: arguments, this: this) + invokeNonThrowingJSFunction(self, arguments: arguments, this: this) } /// A variadic arguments version of `callAsFunction`. @@ -84,30 +84,27 @@ public class JSFunction: JSObject { } } -internal func invokeJSFunction(_ jsFunc: JSFunction, arguments: [ConvertibleToJSValue], this: JSObject?) throws -> JSValue { - let (result, isException) = arguments.withRawJSValues { rawValues in - rawValues.withUnsafeBufferPointer { bufferPointer -> (JSValue, Bool) in +private func invokeNonThrowingJSFunction(_ jsFunc: JSFunction, arguments: [ConvertibleToJSValue], this: JSObject?) -> JSValue { + arguments.withRawJSValues { rawValues in + rawValues.withUnsafeBufferPointer { bufferPointer -> (JSValue) 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, + _call_function_with_this_no_catch(thisId, jsFunc.id, argv, Int32(argc), &kindAndFlags, &payload1, &payload2) } else { - _call_function( + _call_function_no_catch( jsFunc.id, argv, Int32(argc), &kindAndFlags, &payload1, &payload2 ) } + assert(!kindAndFlags.isException) let result = RawJSValue(kind: kindAndFlags.kind, payload1: payload1, payload2: payload2) - return (result.jsValue(), kindAndFlags.isException) + return result.jsValue() } } - if isException { - throw result - } - return result } diff --git a/Sources/JavaScriptKit/FundamentalObjects/JSThrowingFunction.swift b/Sources/JavaScriptKit/FundamentalObjects/JSThrowingFunction.swift index 0fe96d318..adcd82a63 100644 --- a/Sources/JavaScriptKit/FundamentalObjects/JSThrowingFunction.swift +++ b/Sources/JavaScriptKit/FundamentalObjects/JSThrowingFunction.swift @@ -62,3 +62,31 @@ public class JSThrowingFunction { try new(arguments: arguments) } } + +private func invokeJSFunction(_ jsFunc: JSFunction, arguments: [ConvertibleToJSValue], this: JSObject?) throws -> JSValue { + let (result, isException) = arguments.withRawJSValues { rawValues in + rawValues.withUnsafeBufferPointer { bufferPointer -> (JSValue, Bool) 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 result = RawJSValue(kind: kindAndFlags.kind, payload1: payload1, payload2: payload2) + return (result.jsValue(), kindAndFlags.isException) + } + } + if isException { + throw result + } + return result +} diff --git a/Sources/JavaScriptKit/XcodeSupport.swift b/Sources/JavaScriptKit/XcodeSupport.swift index 5bb02e3a3..013c49e2e 100644 --- a/Sources/JavaScriptKit/XcodeSupport.swift +++ b/Sources/JavaScriptKit/XcodeSupport.swift @@ -53,6 +53,13 @@ import _CJavaScriptKit _: UnsafeMutablePointer!, _: UnsafeMutablePointer! ) { fatalError() } + func _call_function_no_catch( + _: JavaScriptObjectRef, + _: UnsafePointer!, _: Int32, + _: UnsafeMutablePointer!, + _: UnsafeMutablePointer!, + _: UnsafeMutablePointer! + ) { fatalError() } func _call_function_with_this( _: JavaScriptObjectRef, _: JavaScriptObjectRef, @@ -61,6 +68,14 @@ import _CJavaScriptKit _: UnsafeMutablePointer!, _: UnsafeMutablePointer! ) { fatalError() } + func _call_function_with_this_no_catch( + _: JavaScriptObjectRef, + _: JavaScriptObjectRef, + _: UnsafePointer!, _: Int32, + _: UnsafeMutablePointer!, + _: UnsafeMutablePointer!, + _: UnsafeMutablePointer! + ) { fatalError() } func _call_new( _: JavaScriptObjectRef, _: UnsafePointer!, _: Int32 diff --git a/Sources/_CJavaScriptKit/_CJavaScriptKit.c b/Sources/_CJavaScriptKit/_CJavaScriptKit.c index 38329ff14..c263b8f71 100644 --- a/Sources/_CJavaScriptKit/_CJavaScriptKit.c +++ b/Sources/_CJavaScriptKit/_CJavaScriptKit.c @@ -36,7 +36,7 @@ void swjs_cleanup_host_function_call(void *argv_buffer) { /// this and `SwiftRuntime.version` in `./Runtime/src/index.ts`. __attribute__((export_name("swjs_library_version"))) int swjs_library_version(void) { - return 705; + return 706; } int _library_features(void); diff --git a/Sources/_CJavaScriptKit/include/_CJavaScriptKit.h b/Sources/_CJavaScriptKit/include/_CJavaScriptKit.h index 8979cee56..ce0bf5862 100644 --- a/Sources/_CJavaScriptKit/include/_CJavaScriptKit.h +++ b/Sources/_CJavaScriptKit/include/_CJavaScriptKit.h @@ -170,6 +170,21 @@ extern void _call_function(const JavaScriptObjectRef ref, const RawJSValue *argv JavaScriptPayload1 *result_payload1, JavaScriptPayload2 *result_payload2); +/// `_call_function` 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_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_no_catch"))) +extern void _call_function_no_catch(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`. /// /// @param _this The value of `this` provided for the call to `func_ref`. @@ -188,6 +203,24 @@ extern void _call_function_with_this(const JavaScriptObjectRef _this, JavaScriptPayload1 *result_payload1, JavaScriptPayload2 *result_payload2); +/// `_call_function_with_this` 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_no_catch"))) +extern void _call_function_with_this_no_catch(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. /// /// @param ref The target JavaScript constructor to call. From cffe72cb975e7500aced5df5f005ea616a7c6f96 Mon Sep 17 00:00:00 2001 From: Max Desiatov Date: Thu, 31 Mar 2022 11:07:15 +0100 Subject: [PATCH 002/373] Update npm dependencies (#175) `@wasmer/wasi` and `@wasmer/wasmfs` are kept at old versions in `Example/package.json`. They had some substantial changes and I'd like to update them in `carton` first to verify that all works fine. --- Example/package-lock.json | 859 +++++++++++++++++++++++--------------- Example/package.json | 6 +- package-lock.json | 59 +-- package.json | 8 +- 4 files changed, 555 insertions(+), 377 deletions(-) diff --git a/Example/package-lock.json b/Example/package-lock.json index 4c2c31c2e..139abdecf 100644 --- a/Example/package-lock.json +++ b/Example/package-lock.json @@ -14,9 +14,9 @@ "javascript-kit-swift": "file:.." }, "devDependencies": { - "webpack": "^5.64.2", - "webpack-cli": "^4.9.1", - "webpack-dev-server": "^4.5.0" + "webpack": "^5.70.0", + "webpack-cli": "^4.9.2", + "webpack-dev-server": "^4.7.4" } }, "..": { @@ -24,11 +24,11 @@ "version": "0.12.0", "license": "MIT", "devDependencies": { - "@rollup/plugin-typescript": "^8.3.0", - "prettier": "2.5.1", - "rollup": "^2.63.0", + "@rollup/plugin-typescript": "^8.3.1", + "prettier": "2.6.1", + "rollup": "^2.70.0", "tslib": "^2.3.1", - "typescript": "^4.5.5" + "typescript": "^4.6.3" } }, "../node_modules/prettier": { @@ -98,10 +98,48 @@ "node": ">= 8" } }, + "node_modules/@types/body-parser": { + "version": "1.19.2", + "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.2.tgz", + "integrity": "sha512-ALYone6pm6QmwZoAgeyNksccT9Q4AWZQ6PvfwR37GT6r6FWUPguq6sUmNGSMV2Wr761oQoBxwGGa6DR5o1DC9g==", + "dev": true, + "dependencies": { + "@types/connect": "*", + "@types/node": "*" + } + }, + "node_modules/@types/bonjour": { + "version": "3.5.10", + "resolved": "https://registry.npmjs.org/@types/bonjour/-/bonjour-3.5.10.tgz", + "integrity": "sha512-p7ienRMiS41Nu2/igbJxxLDWrSZ0WxM8UQgCeO9KhoVF7cOVFkrKsiDr1EsJIla8vV3oEEjGcz11jc5yimhzZw==", + "dev": true, + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/connect": { + "version": "3.4.35", + "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.35.tgz", + "integrity": "sha512-cdeYyv4KWoEgpBISTxWvqYsVy444DOqehiF3fM3ne10AmJ62RSyNkUnxMJXHQWRQQX2eR94m5y1IZyDwBjV9FQ==", + "dev": true, + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/connect-history-api-fallback": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/@types/connect-history-api-fallback/-/connect-history-api-fallback-1.3.5.tgz", + "integrity": "sha512-h8QJa8xSb1WD4fpKBDcATDNGXghFj6/3GRWG6dhmRcu0RX1Ubasur2Uvx5aeEwlf0MwblEC2bMzzMQntxnw/Cw==", + "dev": true, + "dependencies": { + "@types/express-serve-static-core": "*", + "@types/node": "*" + } + }, "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==", + "version": "8.4.1", + "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-8.4.1.tgz", + "integrity": "sha512-GE44+DNEyxxh2Kc6ro/VkIj+9ma0pO0bwv9+uHSyBrikYOHr8zYcdPvnBOp1aw8s+CjRvuSx7CyWqRrNFQ59mA==", "dev": true, "dependencies": { "@types/estree": "*", @@ -109,9 +147,9 @@ } }, "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==", + "version": "3.7.3", + "resolved": "https://registry.npmjs.org/@types/eslint-scope/-/eslint-scope-3.7.3.tgz", + "integrity": "sha512-PB3ldyrcnAicT35TWPs5IcwKD8S333HMaa2VVv4+wdvebJkjWuW/xESoB8IwRcog8HYVYamb1g/R31Qv5Bx03g==", "dev": true, "dependencies": { "@types/eslint": "*", @@ -119,11 +157,34 @@ } }, "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==", + "version": "0.0.51", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-0.0.51.tgz", + "integrity": "sha512-CuPgU6f3eT/XgKKPqKd/gLZV1Xmvf1a2R5POBOGQa6uv82xpls89HU5zKeVoyR8XzHd1RGNOlQlvUe3CFkjWNQ==", "dev": true }, + "node_modules/@types/express": { + "version": "4.17.13", + "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.13.tgz", + "integrity": "sha512-6bSZTPaTIACxn48l50SR+axgrqm6qXFIxrdAKaG6PaJk3+zuUr35hBlgT7vOmJcum+OEaIBLtHV/qloEAFITeA==", + "dev": true, + "dependencies": { + "@types/body-parser": "*", + "@types/express-serve-static-core": "^4.17.18", + "@types/qs": "*", + "@types/serve-static": "*" + } + }, + "node_modules/@types/express-serve-static-core": { + "version": "4.17.28", + "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.17.28.tgz", + "integrity": "sha512-P1BJAEAW3E2DJUlkgq4tOL3RyMunoWXqbSCygWo5ZIWTjUgN1YnaXWW4VWl/oc8vs/XoYibEGBKP0uZyF4AHig==", + "dev": true, + "dependencies": { + "@types/node": "*", + "@types/qs": "*", + "@types/range-parser": "*" + } + }, "node_modules/@types/http-proxy": { "version": "1.17.7", "resolved": "https://registry.npmjs.org/@types/http-proxy/-/http-proxy-1.17.7.tgz", @@ -139,18 +200,73 @@ "integrity": "sha512-qcUXuemtEu+E5wZSJHNxUXeCZhAfXKQ41D+duX+VYPde7xyEVZci+/oXKJL13tnRs9lR2pr4fod59GT6/X1/yQ==", "dev": true }, + "node_modules/@types/mime": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.2.tgz", + "integrity": "sha512-YATxVxgRqNH6nHEIsvg6k2Boc1JHI9ZbH5iWFFv/MTkchz3b1ieGDa5T0a9RznNdI0KhVbdbWSN+KWWrQZRxTw==", + "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/qs": { + "version": "6.9.7", + "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.7.tgz", + "integrity": "sha512-FGa1F62FT09qcrueBA6qYTrJPVDzah9a+493+o2PCXsesWHIn27G98TsSMs3WPNbZIEj4+VJf6saSFpvD+3Zsw==", + "dev": true + }, + "node_modules/@types/range-parser": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.4.tgz", + "integrity": "sha512-EEhsLsD6UsDM1yFhAvy0Cjr6VwmpMWqFBCb9w07wVugF7w9nfajxLuVmngTIpgS6svCnm6Vaw+MZhoDCKnOfsw==", + "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/@types/serve-index": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/@types/serve-index/-/serve-index-1.9.1.tgz", + "integrity": "sha512-d/Hs3nWDxNL2xAczmOVZNj92YZCS6RGxfBPjKzuu/XirCgXdpKEb88dYNbrYGint6IVWLNP+yonwVAuRC0T2Dg==", + "dev": true, + "dependencies": { + "@types/express": "*" + } + }, + "node_modules/@types/serve-static": { + "version": "1.13.10", + "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.13.10.tgz", + "integrity": "sha512-nCkHGI4w7ZgAdNkrEu0bv+4xNV/XDqW+DydknebMOQwkpDGx8G+HTlj7R7ABI8i8nKxVw0wtKPi1D+lPOkh4YQ==", + "dev": true, + "dependencies": { + "@types/mime": "^1", + "@types/node": "*" + } + }, + "node_modules/@types/sockjs": { + "version": "0.3.33", + "resolved": "https://registry.npmjs.org/@types/sockjs/-/sockjs-0.3.33.tgz", + "integrity": "sha512-f0KEEe05NvUnat+boPTZ0dgaLZ4SfSouXUgv5noUiefG2ajgKjmETo9ZJyuqsl7dfl2aHlLJUiki6B4ZYldiiw==", + "dev": true, + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/ws": { + "version": "8.5.3", + "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.5.3.tgz", + "integrity": "sha512-6YOoWjruKj1uLf3INHH7D3qTXwFfEsg1kf3c0uDdSBJwfa/llkwIjrAGV7j7mVgGNbzTQ3HiHKKDXl6bJPD97w==", + "dev": true, + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@wasmer/wasi": { "version": "0.12.0", "integrity": "sha512-FJhLZKAfLWm/yjQI7eCRHNbA8ezmb7LSpUYFkHruZXs2mXk2+DaQtSElEtOoNrVQ4vApTyVaAd5/b7uEu8w6wQ==", @@ -317,9 +433,9 @@ } }, "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==", + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@webpack-cli/configtest/-/configtest-1.1.1.tgz", + "integrity": "sha512-1FBc1f9G4P/AxMqIgfZgeOTuRnwZMten8E7zap5zgpPInnCrP8D4Q81+4CWIch8i/Nf7nXjP0v6CjjbHOrXhKg==", "dev": true, "peerDependencies": { "webpack": "4.x.x || 5.x.x", @@ -327,9 +443,9 @@ } }, "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==", + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/@webpack-cli/info/-/info-1.4.1.tgz", + "integrity": "sha512-PKVGmazEq3oAo46Q63tpMr4HipI3OPfP7LiNOEJg963RMgT0rqheag28NCML0o3GIzA3DmxP1ZIAv9oTX1CUIA==", "dev": true, "dependencies": { "envinfo": "^7.7.3" @@ -339,9 +455,9 @@ } }, "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==", + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/@webpack-cli/serve/-/serve-1.6.1.tgz", + "integrity": "sha512-gNGTiTrjEVQ0OcVnzsRSqTxaBSr+dmTfm+qJsCDluky8uhdLWep7Gcr62QsAKHTMxjCS/8nEITsmFAhfIx+QSw==", "dev": true, "peerDependencies": { "webpack-cli": "4.x.x" @@ -444,9 +560,9 @@ } }, "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==", + "version": "8.11.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.11.0.tgz", + "integrity": "sha512-wGgprdCvMalC0BztXvitD2hC04YffAvtsUn93JbGXYLAtCUO4xd17mCCZQxUOItiBwZvJScWo8NIvQMQ71rdpg==", "dev": true, "dependencies": { "fast-deep-equal": "^3.1.1", @@ -738,10 +854,16 @@ } }, "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==", + "version": "3.5.3", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz", + "integrity": "sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==", "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + ], "dependencies": { "anymatch": "~3.1.2", "braces": "~3.0.2", @@ -1062,9 +1184,9 @@ } }, "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==", + "version": "5.9.2", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.9.2.tgz", + "integrity": "sha512-GIm3fQfwLJ8YZx2smuHpBKkXC1yOk+OBEmKckVyL0i/ea8mqDEykK3ld5dgH1QYPNyT/lIllxV2LULnxCHaHkA==", "dev": true, "dependencies": { "graceful-fs": "^4.2.4", @@ -1498,8 +1620,9 @@ } }, "node_modules/graceful-fs": { - "version": "4.2.6", - "integrity": "sha512-nTnJ528pbqxYanhpDYsi4Rd8MAeaBA67+RZ10CM1m3bTAVFEDcd5AuA4a6W5YkGZ1iNXHzZz8T6TBKLeBuNriQ==", + "version": "4.2.9", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.9.tgz", + "integrity": "sha512-NtNxqUcXgpW2iMrfqSfR73Glt39K+BLwWsPs94yR63v45T0Wbej7eRmL5cWfwEgqXnmjQp3zaJTshdRW/qC2ZQ==", "dev": true }, "node_modules/handle-thing": { @@ -1728,24 +1851,6 @@ "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", @@ -1760,15 +1865,6 @@ "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==", @@ -1862,18 +1958,6 @@ "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", @@ -2217,12 +2301,12 @@ "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==", + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-1.3.1.tgz", + "integrity": "sha512-dPEtOeMvF9VMcYV/1Wb8CPoVAXtp6MKMlcbAt4ddqmGqUJ6fQZFXkNZNkNlfevtNkGtaSoXf/vNNNSvgrdXwtA==", "dev": true, "engines": { - "node": ">= 6.0.0" + "node": ">= 6.13.0" } }, "node_modules/node-releases": { @@ -2338,30 +2422,6 @@ "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", @@ -2417,18 +2477,6 @@ "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", @@ -2591,15 +2639,6 @@ "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", @@ -2869,12 +2908,15 @@ "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==", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/selfsigned/-/selfsigned-2.0.1.tgz", + "integrity": "sha512-LmME957M1zOsUhG+67rAjKfiWFox3SBxE/yymatMZsAx+oMrJ0YQ8AToOnyCm7xbeg2ep37IHLxdu0o2MavQOQ==", "dev": true, "dependencies": { - "node-forge": "^0.10.0" + "node-forge": "^1" + }, + "engines": { + "node": ">=10" } }, "node_modules/send": { @@ -3320,20 +3362,6 @@ "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=" @@ -3364,9 +3392,9 @@ } }, "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==", + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.3.1.tgz", + "integrity": "sha512-x0t0JuydIo8qCNctdDrn1OzH/qDzk2+rdCOC3YzumZ42fiMqmQ7T3xQurykYMhYfHaPHTp4ZxAx2NfUo1K6QaA==", "dev": true, "dependencies": { "glob-to-regexp": "^0.4.1", @@ -3385,13 +3413,13 @@ } }, "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==", + "version": "5.70.0", + "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.70.0.tgz", + "integrity": "sha512-ZMWWy8CeuTTjCxbeaQI21xSswseF2oNOwc70QSKNePvmxE7XW36i7vpBMYZFAUHPwQiEbNGCEYIOOlyRbdGmxw==", "dev": true, "dependencies": { - "@types/eslint-scope": "^3.7.0", - "@types/estree": "^0.0.50", + "@types/eslint-scope": "^3.7.3", + "@types/estree": "^0.0.51", "@webassemblyjs/ast": "1.11.1", "@webassemblyjs/wasm-edit": "1.11.1", "@webassemblyjs/wasm-parser": "1.11.1", @@ -3399,12 +3427,12 @@ "acorn-import-assertions": "^1.7.6", "browserslist": "^4.14.5", "chrome-trace-event": "^1.0.2", - "enhanced-resolve": "^5.8.3", + "enhanced-resolve": "^5.9.2", "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", + "graceful-fs": "^4.2.9", "json-parse-better-errors": "^1.0.2", "loader-runner": "^4.2.0", "mime-types": "^2.1.27", @@ -3412,8 +3440,8 @@ "schema-utils": "^3.1.0", "tapable": "^2.1.1", "terser-webpack-plugin": "^5.1.3", - "watchpack": "^2.2.0", - "webpack-sources": "^3.2.2" + "watchpack": "^2.3.1", + "webpack-sources": "^3.2.3" }, "bin": { "webpack": "bin/webpack.js" @@ -3432,15 +3460,15 @@ } }, "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==", + "version": "4.9.2", + "resolved": "https://registry.npmjs.org/webpack-cli/-/webpack-cli-4.9.2.tgz", + "integrity": "sha512-m3/AACnBBzK/kMTcxWHcZFPrw/eQuY4Df1TxvIWfWM2x7mRqBQCqKEd96oCUa9jkapLBaFfRce33eGDb4Pr7YQ==", "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", + "@webpack-cli/configtest": "^1.1.1", + "@webpack-cli/info": "^1.4.1", + "@webpack-cli/serve": "^1.6.1", "colorette": "^2.0.14", "commander": "^7.0.0", "execa": "^5.0.0", @@ -3484,13 +3512,13 @@ } }, "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==", + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/webpack-dev-middleware/-/webpack-dev-middleware-5.3.1.tgz", + "integrity": "sha512-81EujCKkyles2wphtdrnPg/QqegC/AtqNH//mQkBYSMqwFVCQrxM6ktB2O/SPlZy7LqeEfTbV3cZARGQz6umhg==", "dev": true, "dependencies": { "colorette": "^2.0.10", - "memfs": "^3.2.2", + "memfs": "^3.4.1", "mime-types": "^2.1.31", "range-parser": "^1.2.1", "schema-utils": "^4.0.0" @@ -3507,9 +3535,9 @@ } }, "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==", + "version": "8.11.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.11.0.tgz", + "integrity": "sha512-wGgprdCvMalC0BztXvitD2hC04YffAvtsUn93JbGXYLAtCUO4xd17mCCZQxUOItiBwZvJScWo8NIvQMQ71rdpg==", "dev": true, "dependencies": { "fast-deep-equal": "^3.1.1", @@ -3547,9 +3575,9 @@ "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==", + "version": "3.4.1", + "resolved": "https://registry.npmjs.org/memfs/-/memfs-3.4.1.tgz", + "integrity": "sha512-1c9VPVvW5P7I85c35zAdEr1TD5+F11IToIHIlrVIcflfnzPkJa0ZoYEoEdYDP8KgPFoSZ/opDrUsAoZWym3mtw==", "dev": true, "dependencies": { "fs-monkey": "1.0.3" @@ -3578,36 +3606,41 @@ } }, "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==", + "version": "4.7.4", + "resolved": "https://registry.npmjs.org/webpack-dev-server/-/webpack-dev-server-4.7.4.tgz", + "integrity": "sha512-nfdsb02Zi2qzkNmgtZjkrMOcXnYZ6FLKcQwpxT7MvmHKc+oTtDsBju8j+NMyAygZ9GW1jMEUpy3itHtqgEhe1A==", "dev": true, "dependencies": { + "@types/bonjour": "^3.5.9", + "@types/connect-history-api-fallback": "^1.3.5", + "@types/express": "^4.17.13", + "@types/serve-index": "^1.9.1", + "@types/sockjs": "^0.3.33", + "@types/ws": "^8.2.2", "ansi-html-community": "^0.0.8", "bonjour": "^3.5.0", - "chokidar": "^3.5.2", + "chokidar": "^3.5.3", "colorette": "^2.0.10", "compression": "^1.7.4", "connect-history-api-fallback": "^1.6.0", + "default-gateway": "^6.0.3", "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", + "schema-utils": "^4.0.0", + "selfsigned": "^2.0.0", "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" + "webpack-dev-middleware": "^5.3.1", + "ws": "^8.4.2" }, "bin": { "webpack-dev-server": "bin/webpack-dev-server.js" @@ -3624,6 +3657,34 @@ } } }, + "node_modules/webpack-dev-server/node_modules/ajv": { + "version": "8.11.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.11.0.tgz", + "integrity": "sha512-wGgprdCvMalC0BztXvitD2hC04YffAvtsUn93JbGXYLAtCUO4xd17mCCZQxUOItiBwZvJScWo8NIvQMQ71rdpg==", + "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-server/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-server/node_modules/ipaddr.js": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-2.0.1.tgz", @@ -3633,6 +3694,31 @@ "node": ">= 10" } }, + "node_modules/webpack-dev-server/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-server/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-merge": { "version": "5.8.0", "resolved": "https://registry.npmjs.org/webpack-merge/-/webpack-merge-5.8.0.tgz", @@ -3647,9 +3733,9 @@ } }, "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==", + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-3.2.3.tgz", + "integrity": "sha512-/DyMEOrDgLKKIG0fmvtz+4dUX/3Ghozwgm6iPp8KRhvn+eQf9+Q7GWxVNMk3+uCPWfdXYC4ExGBckIXdFEfH1w==", "dev": true, "engines": { "node": ">=10.13.0" @@ -3702,9 +3788,9 @@ "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==", + "version": "8.5.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.5.0.tgz", + "integrity": "sha512-BWX0SWVgLPzYwF8lTzEy1egjhS4S4OEAHfsO8o65WOVsrnSRGaSiUaa9e0ggGlkMTtBlmOpEXiie9RUcBO86qg==", "dev": true, "engines": { "node": ">=10.0.0" @@ -3756,10 +3842,48 @@ "fastq": "^1.6.0" } }, + "@types/body-parser": { + "version": "1.19.2", + "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.2.tgz", + "integrity": "sha512-ALYone6pm6QmwZoAgeyNksccT9Q4AWZQ6PvfwR37GT6r6FWUPguq6sUmNGSMV2Wr761oQoBxwGGa6DR5o1DC9g==", + "dev": true, + "requires": { + "@types/connect": "*", + "@types/node": "*" + } + }, + "@types/bonjour": { + "version": "3.5.10", + "resolved": "https://registry.npmjs.org/@types/bonjour/-/bonjour-3.5.10.tgz", + "integrity": "sha512-p7ienRMiS41Nu2/igbJxxLDWrSZ0WxM8UQgCeO9KhoVF7cOVFkrKsiDr1EsJIla8vV3oEEjGcz11jc5yimhzZw==", + "dev": true, + "requires": { + "@types/node": "*" + } + }, + "@types/connect": { + "version": "3.4.35", + "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.35.tgz", + "integrity": "sha512-cdeYyv4KWoEgpBISTxWvqYsVy444DOqehiF3fM3ne10AmJ62RSyNkUnxMJXHQWRQQX2eR94m5y1IZyDwBjV9FQ==", + "dev": true, + "requires": { + "@types/node": "*" + } + }, + "@types/connect-history-api-fallback": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/@types/connect-history-api-fallback/-/connect-history-api-fallback-1.3.5.tgz", + "integrity": "sha512-h8QJa8xSb1WD4fpKBDcATDNGXghFj6/3GRWG6dhmRcu0RX1Ubasur2Uvx5aeEwlf0MwblEC2bMzzMQntxnw/Cw==", + "dev": true, + "requires": { + "@types/express-serve-static-core": "*", + "@types/node": "*" + } + }, "@types/eslint": { - "version": "8.2.0", - "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-8.2.0.tgz", - "integrity": "sha512-74hbvsnc+7TEDa1z5YLSe4/q8hGYB3USNvCuzHUJrjPV6hXaq8IXcngCrHkuvFt0+8rFz7xYXrHgNayIX0UZvQ==", + "version": "8.4.1", + "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-8.4.1.tgz", + "integrity": "sha512-GE44+DNEyxxh2Kc6ro/VkIj+9ma0pO0bwv9+uHSyBrikYOHr8zYcdPvnBOp1aw8s+CjRvuSx7CyWqRrNFQ59mA==", "dev": true, "requires": { "@types/estree": "*", @@ -3767,9 +3891,9 @@ } }, "@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==", + "version": "3.7.3", + "resolved": "https://registry.npmjs.org/@types/eslint-scope/-/eslint-scope-3.7.3.tgz", + "integrity": "sha512-PB3ldyrcnAicT35TWPs5IcwKD8S333HMaa2VVv4+wdvebJkjWuW/xESoB8IwRcog8HYVYamb1g/R31Qv5Bx03g==", "dev": true, "requires": { "@types/eslint": "*", @@ -3777,11 +3901,34 @@ } }, "@types/estree": { - "version": "0.0.50", - "resolved": "https://registry.npmjs.org/@types/estree/-/estree-0.0.50.tgz", - "integrity": "sha512-C6N5s2ZFtuZRj54k2/zyRhNDjJwwcViAM3Nbm8zjBpbqAdZ00mr0CFxvSKeO8Y/e03WVFLpQMdHYVfUd6SB+Hw==", + "version": "0.0.51", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-0.0.51.tgz", + "integrity": "sha512-CuPgU6f3eT/XgKKPqKd/gLZV1Xmvf1a2R5POBOGQa6uv82xpls89HU5zKeVoyR8XzHd1RGNOlQlvUe3CFkjWNQ==", "dev": true }, + "@types/express": { + "version": "4.17.13", + "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.13.tgz", + "integrity": "sha512-6bSZTPaTIACxn48l50SR+axgrqm6qXFIxrdAKaG6PaJk3+zuUr35hBlgT7vOmJcum+OEaIBLtHV/qloEAFITeA==", + "dev": true, + "requires": { + "@types/body-parser": "*", + "@types/express-serve-static-core": "^4.17.18", + "@types/qs": "*", + "@types/serve-static": "*" + } + }, + "@types/express-serve-static-core": { + "version": "4.17.28", + "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.17.28.tgz", + "integrity": "sha512-P1BJAEAW3E2DJUlkgq4tOL3RyMunoWXqbSCygWo5ZIWTjUgN1YnaXWW4VWl/oc8vs/XoYibEGBKP0uZyF4AHig==", + "dev": true, + "requires": { + "@types/node": "*", + "@types/qs": "*", + "@types/range-parser": "*" + } + }, "@types/http-proxy": { "version": "1.17.7", "resolved": "https://registry.npmjs.org/@types/http-proxy/-/http-proxy-1.17.7.tgz", @@ -3797,18 +3944,73 @@ "integrity": "sha512-qcUXuemtEu+E5wZSJHNxUXeCZhAfXKQ41D+duX+VYPde7xyEVZci+/oXKJL13tnRs9lR2pr4fod59GT6/X1/yQ==", "dev": true }, + "@types/mime": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.2.tgz", + "integrity": "sha512-YATxVxgRqNH6nHEIsvg6k2Boc1JHI9ZbH5iWFFv/MTkchz3b1ieGDa5T0a9RznNdI0KhVbdbWSN+KWWrQZRxTw==", + "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/qs": { + "version": "6.9.7", + "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.7.tgz", + "integrity": "sha512-FGa1F62FT09qcrueBA6qYTrJPVDzah9a+493+o2PCXsesWHIn27G98TsSMs3WPNbZIEj4+VJf6saSFpvD+3Zsw==", + "dev": true + }, + "@types/range-parser": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.4.tgz", + "integrity": "sha512-EEhsLsD6UsDM1yFhAvy0Cjr6VwmpMWqFBCb9w07wVugF7w9nfajxLuVmngTIpgS6svCnm6Vaw+MZhoDCKnOfsw==", + "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 }, + "@types/serve-index": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/@types/serve-index/-/serve-index-1.9.1.tgz", + "integrity": "sha512-d/Hs3nWDxNL2xAczmOVZNj92YZCS6RGxfBPjKzuu/XirCgXdpKEb88dYNbrYGint6IVWLNP+yonwVAuRC0T2Dg==", + "dev": true, + "requires": { + "@types/express": "*" + } + }, + "@types/serve-static": { + "version": "1.13.10", + "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.13.10.tgz", + "integrity": "sha512-nCkHGI4w7ZgAdNkrEu0bv+4xNV/XDqW+DydknebMOQwkpDGx8G+HTlj7R7ABI8i8nKxVw0wtKPi1D+lPOkh4YQ==", + "dev": true, + "requires": { + "@types/mime": "^1", + "@types/node": "*" + } + }, + "@types/sockjs": { + "version": "0.3.33", + "resolved": "https://registry.npmjs.org/@types/sockjs/-/sockjs-0.3.33.tgz", + "integrity": "sha512-f0KEEe05NvUnat+boPTZ0dgaLZ4SfSouXUgv5noUiefG2ajgKjmETo9ZJyuqsl7dfl2aHlLJUiki6B4ZYldiiw==", + "dev": true, + "requires": { + "@types/node": "*" + } + }, + "@types/ws": { + "version": "8.5.3", + "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.5.3.tgz", + "integrity": "sha512-6YOoWjruKj1uLf3INHH7D3qTXwFfEsg1kf3c0uDdSBJwfa/llkwIjrAGV7j7mVgGNbzTQ3HiHKKDXl6bJPD97w==", + "dev": true, + "requires": { + "@types/node": "*" + } + }, "@wasmer/wasi": { "version": "0.12.0", "integrity": "sha512-FJhLZKAfLWm/yjQI7eCRHNbA8ezmb7LSpUYFkHruZXs2mXk2+DaQtSElEtOoNrVQ4vApTyVaAd5/b7uEu8w6wQ==", @@ -3975,25 +4177,25 @@ } }, "@webpack-cli/configtest": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@webpack-cli/configtest/-/configtest-1.1.0.tgz", - "integrity": "sha512-ttOkEkoalEHa7RaFYpM0ErK1xc4twg3Am9hfHhL7MVqlHebnkYd2wuI/ZqTDj0cVzZho6PdinY0phFZV3O0Mzg==", + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@webpack-cli/configtest/-/configtest-1.1.1.tgz", + "integrity": "sha512-1FBc1f9G4P/AxMqIgfZgeOTuRnwZMten8E7zap5zgpPInnCrP8D4Q81+4CWIch8i/Nf7nXjP0v6CjjbHOrXhKg==", "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==", + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/@webpack-cli/info/-/info-1.4.1.tgz", + "integrity": "sha512-PKVGmazEq3oAo46Q63tpMr4HipI3OPfP7LiNOEJg963RMgT0rqheag28NCML0o3GIzA3DmxP1ZIAv9oTX1CUIA==", "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==", + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/@webpack-cli/serve/-/serve-1.6.1.tgz", + "integrity": "sha512-gNGTiTrjEVQ0OcVnzsRSqTxaBSr+dmTfm+qJsCDluky8uhdLWep7Gcr62QsAKHTMxjCS/8nEITsmFAhfIx+QSw==", "dev": true, "requires": {} }, @@ -4063,9 +4265,9 @@ }, "dependencies": { "ajv": { - "version": "8.8.2", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.8.2.tgz", - "integrity": "sha512-x9VuX+R/jcFj1DHo/fCp99esgGDWiHENrKxaCENuCxpoMCmAt/COCGVDwA7kleEpEzJjDnvh3yGoOuLu0Dtllw==", + "version": "8.11.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.11.0.tgz", + "integrity": "sha512-wGgprdCvMalC0BztXvitD2hC04YffAvtsUn93JbGXYLAtCUO4xd17mCCZQxUOItiBwZvJScWo8NIvQMQ71rdpg==", "dev": true, "requires": { "fast-deep-equal": "^3.1.1", @@ -4277,9 +4479,9 @@ "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==", + "version": "3.5.3", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz", + "integrity": "sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==", "dev": true, "requires": { "anymatch": "~3.1.2", @@ -4543,9 +4745,9 @@ } }, "enhanced-resolve": { - "version": "5.8.3", - "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.8.3.tgz", - "integrity": "sha512-EGAbGvH7j7Xt2nc0E7D99La1OiEs8LnyimkRgwExpUMScN6O+3x9tIWs7PLQZVNx4YD+00skHXPXi1yQHpAmZA==", + "version": "5.9.2", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.9.2.tgz", + "integrity": "sha512-GIm3fQfwLJ8YZx2smuHpBKkXC1yOk+OBEmKckVyL0i/ea8mqDEykK3ld5dgH1QYPNyT/lIllxV2LULnxCHaHkA==", "dev": true, "requires": { "graceful-fs": "^4.2.4", @@ -4878,8 +5080,9 @@ } }, "graceful-fs": { - "version": "4.2.6", - "integrity": "sha512-nTnJ528pbqxYanhpDYsi4Rd8MAeaBA67+RZ10CM1m3bTAVFEDcd5AuA4a6W5YkGZ1iNXHzZz8T6TBKLeBuNriQ==", + "version": "4.2.9", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.9.tgz", + "integrity": "sha512-NtNxqUcXgpW2iMrfqSfR73Glt39K+BLwWsPs94yR63v45T0Wbej7eRmL5cWfwEgqXnmjQp3zaJTshdRW/qC2ZQ==", "dev": true }, "handle-thing": { @@ -5059,18 +5262,6 @@ "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", @@ -5082,12 +5273,6 @@ "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==", @@ -5145,15 +5330,6 @@ "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", @@ -5231,11 +5407,11 @@ "javascript-kit-swift": { "version": "file:..", "requires": { - "@rollup/plugin-typescript": "^8.3.0", - "prettier": "2.5.1", - "rollup": "^2.63.0", + "@rollup/plugin-typescript": "^8.3.1", + "prettier": "2.6.1", + "rollup": "^2.70.0", "tslib": "^2.3.1", - "typescript": "^4.5.5" + "typescript": "^4.6.3" }, "dependencies": { "prettier": { @@ -5428,9 +5604,9 @@ "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==", + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-1.3.1.tgz", + "integrity": "sha512-dPEtOeMvF9VMcYV/1Wb8CPoVAXtp6MKMlcbAt4ddqmGqUJ6fQZFXkNZNkNlfevtNkGtaSoXf/vNNNSvgrdXwtA==", "dev": true }, "node-releases": { @@ -5513,21 +5689,6 @@ "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", @@ -5565,15 +5726,6 @@ "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", @@ -5699,11 +5851,6 @@ "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", @@ -5877,12 +6024,12 @@ "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==", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/selfsigned/-/selfsigned-2.0.1.tgz", + "integrity": "sha512-LmME957M1zOsUhG+67rAjKfiWFox3SBxE/yymatMZsAx+oMrJ0YQ8AToOnyCm7xbeg2ep37IHLxdu0o2MavQOQ==", "dev": true, "requires": { - "node-forge": "^0.10.0" + "node-forge": "^1" } }, "send": { @@ -6218,22 +6365,6 @@ "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=" @@ -6254,9 +6385,9 @@ "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==", + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.3.1.tgz", + "integrity": "sha512-x0t0JuydIo8qCNctdDrn1OzH/qDzk2+rdCOC3YzumZ42fiMqmQ7T3xQurykYMhYfHaPHTp4ZxAx2NfUo1K6QaA==", "dev": true, "requires": { "glob-to-regexp": "^0.4.1", @@ -6272,13 +6403,13 @@ } }, "webpack": { - "version": "5.64.2", - "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.64.2.tgz", - "integrity": "sha512-4KGc0+Ozi0aS3EaLNRvEppfZUer+CaORKqL6OBjDLZOPf9YfN8leagFzwe6/PoBdHFxc/utKArl8LMC0Ivtmdg==", + "version": "5.70.0", + "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.70.0.tgz", + "integrity": "sha512-ZMWWy8CeuTTjCxbeaQI21xSswseF2oNOwc70QSKNePvmxE7XW36i7vpBMYZFAUHPwQiEbNGCEYIOOlyRbdGmxw==", "dev": true, "requires": { - "@types/eslint-scope": "^3.7.0", - "@types/estree": "^0.0.50", + "@types/eslint-scope": "^3.7.3", + "@types/estree": "^0.0.51", "@webassemblyjs/ast": "1.11.1", "@webassemblyjs/wasm-edit": "1.11.1", "@webassemblyjs/wasm-parser": "1.11.1", @@ -6286,12 +6417,12 @@ "acorn-import-assertions": "^1.7.6", "browserslist": "^4.14.5", "chrome-trace-event": "^1.0.2", - "enhanced-resolve": "^5.8.3", + "enhanced-resolve": "^5.9.2", "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", + "graceful-fs": "^4.2.9", "json-parse-better-errors": "^1.0.2", "loader-runner": "^4.2.0", "mime-types": "^2.1.27", @@ -6299,20 +6430,20 @@ "schema-utils": "^3.1.0", "tapable": "^2.1.1", "terser-webpack-plugin": "^5.1.3", - "watchpack": "^2.2.0", - "webpack-sources": "^3.2.2" + "watchpack": "^2.3.1", + "webpack-sources": "^3.2.3" } }, "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==", + "version": "4.9.2", + "resolved": "https://registry.npmjs.org/webpack-cli/-/webpack-cli-4.9.2.tgz", + "integrity": "sha512-m3/AACnBBzK/kMTcxWHcZFPrw/eQuY4Df1TxvIWfWM2x7mRqBQCqKEd96oCUa9jkapLBaFfRce33eGDb4Pr7YQ==", "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", + "@webpack-cli/configtest": "^1.1.1", + "@webpack-cli/info": "^1.4.1", + "@webpack-cli/serve": "^1.6.1", "colorette": "^2.0.14", "commander": "^7.0.0", "execa": "^5.0.0", @@ -6332,22 +6463,22 @@ } }, "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==", + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/webpack-dev-middleware/-/webpack-dev-middleware-5.3.1.tgz", + "integrity": "sha512-81EujCKkyles2wphtdrnPg/QqegC/AtqNH//mQkBYSMqwFVCQrxM6ktB2O/SPlZy7LqeEfTbV3cZARGQz6umhg==", "dev": true, "requires": { "colorette": "^2.0.10", - "memfs": "^3.2.2", + "memfs": "^3.4.1", "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==", + "version": "8.11.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.11.0.tgz", + "integrity": "sha512-wGgprdCvMalC0BztXvitD2hC04YffAvtsUn93JbGXYLAtCUO4xd17mCCZQxUOItiBwZvJScWo8NIvQMQ71rdpg==", "dev": true, "requires": { "fast-deep-equal": "^3.1.1", @@ -6378,9 +6509,9 @@ "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==", + "version": "3.4.1", + "resolved": "https://registry.npmjs.org/memfs/-/memfs-3.4.1.tgz", + "integrity": "sha512-1c9VPVvW5P7I85c35zAdEr1TD5+F11IToIHIlrVIcflfnzPkJa0ZoYEoEdYDP8KgPFoSZ/opDrUsAoZWym3mtw==", "dev": true, "requires": { "fs-monkey": "1.0.3" @@ -6401,43 +6532,87 @@ } }, "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==", + "version": "4.7.4", + "resolved": "https://registry.npmjs.org/webpack-dev-server/-/webpack-dev-server-4.7.4.tgz", + "integrity": "sha512-nfdsb02Zi2qzkNmgtZjkrMOcXnYZ6FLKcQwpxT7MvmHKc+oTtDsBju8j+NMyAygZ9GW1jMEUpy3itHtqgEhe1A==", "dev": true, "requires": { + "@types/bonjour": "^3.5.9", + "@types/connect-history-api-fallback": "^1.3.5", + "@types/express": "^4.17.13", + "@types/serve-index": "^1.9.1", + "@types/sockjs": "^0.3.33", + "@types/ws": "^8.2.2", "ansi-html-community": "^0.0.8", "bonjour": "^3.5.0", - "chokidar": "^3.5.2", + "chokidar": "^3.5.3", "colorette": "^2.0.10", "compression": "^1.7.4", "connect-history-api-fallback": "^1.6.0", + "default-gateway": "^6.0.3", "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", + "schema-utils": "^4.0.0", + "selfsigned": "^2.0.0", "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" + "webpack-dev-middleware": "^5.3.1", + "ws": "^8.4.2" }, "dependencies": { + "ajv": { + "version": "8.11.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.11.0.tgz", + "integrity": "sha512-wGgprdCvMalC0BztXvitD2hC04YffAvtsUn93JbGXYLAtCUO4xd17mCCZQxUOItiBwZvJScWo8NIvQMQ71rdpg==", + "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" + } + }, "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 + }, + "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 + }, + "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" + } } } }, @@ -6452,9 +6627,9 @@ } }, "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==", + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-3.2.3.tgz", + "integrity": "sha512-/DyMEOrDgLKKIG0fmvtz+4dUX/3Ghozwgm6iPp8KRhvn+eQf9+Q7GWxVNMk3+uCPWfdXYC4ExGBckIXdFEfH1w==", "dev": true }, "websocket-driver": { @@ -6492,9 +6667,9 @@ "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==", + "version": "8.5.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.5.0.tgz", + "integrity": "sha512-BWX0SWVgLPzYwF8lTzEy1egjhS4S4OEAHfsO8o65WOVsrnSRGaSiUaa9e0ggGlkMTtBlmOpEXiie9RUcBO86qg==", "dev": true, "requires": {} } diff --git a/Example/package.json b/Example/package.json index f7f9a49c5..52c72a2a4 100644 --- a/Example/package.json +++ b/Example/package.json @@ -9,9 +9,9 @@ "javascript-kit-swift": "file:.." }, "devDependencies": { - "webpack": "^5.64.2", - "webpack-cli": "^4.9.1", - "webpack-dev-server": "^4.5.0" + "webpack": "^5.70.0", + "webpack-cli": "^4.9.2", + "webpack-dev-server": "^4.7.4" }, "scripts": { "build": "webpack", diff --git a/package-lock.json b/package-lock.json index d39bc8474..4b354b6a3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,17 +9,17 @@ "version": "0.12.0", "license": "MIT", "devDependencies": { - "@rollup/plugin-typescript": "^8.3.0", - "prettier": "2.5.1", - "rollup": "^2.63.0", + "@rollup/plugin-typescript": "^8.3.1", + "prettier": "2.6.1", + "rollup": "^2.70.0", "tslib": "^2.3.1", - "typescript": "^4.5.5" + "typescript": "^4.6.3" } }, "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": "8.3.1", + "resolved": "https://registry.npmjs.org/@rollup/plugin-typescript/-/plugin-typescript-8.3.1.tgz", + "integrity": "sha512-84rExe3ICUBXzqNX48WZV2Jp3OddjTMX97O2Py6D1KJaGSwWp0mDHXj+bCGNJqWHIEKDIT2U0sDjhP4czKi6cA==", "dev": true, "dependencies": { "@rollup/pluginutils": "^3.1.0", @@ -126,15 +126,18 @@ } }, "node_modules/prettier": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/prettier/-/prettier-2.5.1.tgz", - "integrity": "sha512-vBZcPRUR5MZJwoyi3ZoyQlc1rXeEck8KgeC9AwwOn+exuxLxq5toTRDTSaVrXHxelDMHy9zlicw8u66yxoSUFg==", + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-2.6.1.tgz", + "integrity": "sha512-8UVbTBYGwN37Bs9LERmxCPjdvPxlEowx2urIL6urHzdb3SDq4B/Z6xLFCblrSnE4iKWcS6ziJ3aOYrc1kz/E2A==", "dev": true, "bin": { "prettier": "bin-prettier.js" }, "engines": { "node": ">=10.13.0" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" } }, "node_modules/resolve": { @@ -155,9 +158,9 @@ } }, "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": "2.70.1", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-2.70.1.tgz", + "integrity": "sha512-CRYsI5EuzLbXdxC6RnYhOuRdtz4bhejPMSWjsFLfVM/7w/85n2szZv6yExqUXsBdz5KT8eoubeyDUDjhLHEslA==", "dev": true, "bin": { "rollup": "dist/bin/rollup" @@ -188,9 +191,9 @@ "dev": true }, "node_modules/typescript": { - "version": "4.5.5", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.5.5.tgz", - "integrity": "sha512-TCTIul70LyWe6IJWT8QSYeA54WQe8EjQFU4wY52Fasj5UKx88LNYKCgBEHcOMOrFF1rKGbD8v/xcNWVUq9SymA==", + "version": "4.6.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.6.3.tgz", + "integrity": "sha512-yNIatDa5iaofVozS/uQJEl3JRWLKKGJKh6Yaiv0GLGSuhpFJe7P3SbHZ8/yjAHRQwKRoA6YZqlfjXWmVzoVSMw==", "dev": true, "bin": { "tsc": "bin/tsc", @@ -203,9 +206,9 @@ }, "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==", + "version": "8.3.1", + "resolved": "https://registry.npmjs.org/@rollup/plugin-typescript/-/plugin-typescript-8.3.1.tgz", + "integrity": "sha512-84rExe3ICUBXzqNX48WZV2Jp3OddjTMX97O2Py6D1KJaGSwWp0mDHXj+bCGNJqWHIEKDIT2U0sDjhP4czKi6cA==", "dev": true, "requires": { "@rollup/pluginutils": "^3.1.0", @@ -279,9 +282,9 @@ "dev": true }, "prettier": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/prettier/-/prettier-2.5.1.tgz", - "integrity": "sha512-vBZcPRUR5MZJwoyi3ZoyQlc1rXeEck8KgeC9AwwOn+exuxLxq5toTRDTSaVrXHxelDMHy9zlicw8u66yxoSUFg==", + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-2.6.1.tgz", + "integrity": "sha512-8UVbTBYGwN37Bs9LERmxCPjdvPxlEowx2urIL6urHzdb3SDq4B/Z6xLFCblrSnE4iKWcS6ziJ3aOYrc1kz/E2A==", "dev": true }, "resolve": { @@ -296,9 +299,9 @@ } }, "rollup": { - "version": "2.63.0", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-2.63.0.tgz", - "integrity": "sha512-nps0idjmD+NXl6OREfyYXMn/dar3WGcyKn+KBzPdaLecub3x/LrId0wUcthcr8oZUAcZAR8NKcfGGFlNgGL1kQ==", + "version": "2.70.1", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-2.70.1.tgz", + "integrity": "sha512-CRYsI5EuzLbXdxC6RnYhOuRdtz4bhejPMSWjsFLfVM/7w/85n2szZv6yExqUXsBdz5KT8eoubeyDUDjhLHEslA==", "dev": true, "requires": { "fsevents": "~2.3.2" @@ -317,9 +320,9 @@ "dev": true }, "typescript": { - "version": "4.5.5", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.5.5.tgz", - "integrity": "sha512-TCTIul70LyWe6IJWT8QSYeA54WQe8EjQFU4wY52Fasj5UKx88LNYKCgBEHcOMOrFF1rKGbD8v/xcNWVUq9SymA==", + "version": "4.6.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.6.3.tgz", + "integrity": "sha512-yNIatDa5iaofVozS/uQJEl3JRWLKKGJKh6Yaiv0GLGSuhpFJe7P3SbHZ8/yjAHRQwKRoA6YZqlfjXWmVzoVSMw==", "dev": true } } diff --git a/package.json b/package.json index 47da6fe88..77bb7ea05 100644 --- a/package.json +++ b/package.json @@ -34,10 +34,10 @@ "author": "swiftwasm", "license": "MIT", "devDependencies": { - "@rollup/plugin-typescript": "^8.3.0", - "prettier": "2.5.1", - "rollup": "^2.63.0", + "@rollup/plugin-typescript": "^8.3.1", + "prettier": "2.6.1", + "rollup": "^2.70.0", "tslib": "^2.3.1", - "typescript": "^4.5.5" + "typescript": "^4.6.3" } } From 72314d7d56b9cadbb7cf1d48541e4053ea415663 Mon Sep 17 00:00:00 2001 From: Max Desiatov Date: Thu, 31 Mar 2022 23:10:15 +0100 Subject: [PATCH 003/373] Bump version to 0.13.0, update `CHANGELOG.md` (#177) I'd be happy to wait a bit more to include DOMKit improvements, but #173 has been highly requested to be included in a release sooner rather than later. After all, this means that we have less pressure to rush DOMKit-related changes out in this release. --- CHANGELOG.md | 22 ++++++++++++++++++++++ package-lock.json | 4 ++-- package.json | 2 +- 3 files changed, 25 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index cef363202..23706a906 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,25 @@ +# 0.13.0 (31 March 2022) + +This a small improvement release that 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. diff --git a/package-lock.json b/package-lock.json index 4b354b6a3..6bce2ca52 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "javascript-kit-swift", - "version": "0.12.0", + "version": "0.13.0", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "javascript-kit-swift", - "version": "0.12.0", + "version": "0.13.0", "license": "MIT", "devDependencies": { "@rollup/plugin-typescript": "^8.3.1", diff --git a/package.json b/package.json index 77bb7ea05..26b05b29e 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "javascript-kit-swift", - "version": "0.12.0", + "version": "0.13.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", From 2011cc0422eb820c36d7cc275e0992ec5ca9ab34 Mon Sep 17 00:00:00 2001 From: Max Desiatov Date: Thu, 31 Mar 2022 23:11:42 +0100 Subject: [PATCH 004/373] Fix wording and formatting in `CHANGELOG.md` --- CHANGELOG.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 23706a906..4569f705f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,6 @@ # 0.13.0 (31 March 2022) -This a small improvement release that improves handling of JavaScript exceptions and compatibility with Xcode. +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! @@ -19,7 +19,6 @@ Thanks to [@kateinoigakukun](https://github.com/kateinoigakukun), [@pedrovgs](ht - 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. From 0a38d705e0c310f236599e2a0d62afa8012578db Mon Sep 17 00:00:00 2001 From: Max Desiatov Date: Fri, 1 Apr 2022 17:32:38 +0100 Subject: [PATCH 005/373] Add 5.6 release and macOS 12 with Xcode 13.3 to CI matrix (#176) * Add macOS 12 with Xcode 13.3 to CI matrix * Clean up comment * Update test.yml * Add `wasm-5.6-SNAPSHOT-2022-03-23-a` to CI matrix * Pass `-mexec-model=reactor` in tests `Makefile` * Use `-Xclang-linker` * Use both `-Xswiftc` and `-Xclang-linker` * Pass `-Xswiftc` twice * Use `wasm-5.6.0-RELEASE` --- .github/workflows/test.yml | 16 +++++++++++++--- IntegrationTests/Makefile | 1 + 2 files changed, 14 insertions(+), 3 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index b3992b7dc..0f2418e9c 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -8,9 +8,10 @@ jobs: name: Build and Test strategy: matrix: - os: [macos-10.15, macos-11, ubuntu-18.04, ubuntu-20.04] + os: [macos-10.15, macos-11, macos-12, ubuntu-18.04, ubuntu-20.04] toolchain: - wasm-5.5.0-RELEASE + - wasm-5.6.0-RELEASE runs-on: ${{ matrix.os }} steps: - name: Checkout @@ -29,9 +30,18 @@ jobs: 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-10.15 + xcode: Xcode_12.4 + - os: macos-11 + xcode: Xcode_13.2.1 + - os: macos-12 + xcode: Xcode_13.3 + runs-on: ${{ matrix.os }} steps: - uses: actions/checkout@v2 - run: swift build env: - DEVELOPER_DIR: /Applications/Xcode_13.2.1.app/Contents/Developer/ + DEVELOPER_DIR: /Applications/${{ matrix.xcode }}.app/Contents/Developer/ diff --git a/IntegrationTests/Makefile b/IntegrationTests/Makefile index 6e1a4dd05..575a8f20d 100644 --- a/IntegrationTests/Makefile +++ b/IntegrationTests/Makefile @@ -7,6 +7,7 @@ TestSuites/.build/$(CONFIGURATION)/%.wasm: FORCE --product $(basename $(notdir $@)) \ --triple wasm32-unknown-wasi \ --configuration $(CONFIGURATION) \ + -Xswiftc -Xclang-linker -Xswiftc -mexec-model=reactor \ $(SWIFT_BUILD_FLAGS) dist/%.wasm: TestSuites/.build/$(CONFIGURATION)/%.wasm From 081784b34ad94c91eddccca58f1592ce181328ad Mon Sep 17 00:00:00 2001 From: Jed Fox Date: Fri, 1 Apr 2022 17:06:18 -0400 Subject: [PATCH 006/373] Add support for Symbol objects via JSSymbol (#179) --- .../Sources/PrimaryTests/main.swift | 24 +++++++ Runtime/src/js-value.ts | 6 ++ Runtime/src/object-heap.ts | 24 +++---- .../JavaScriptKit/ConvertibleToJSValue.swift | 5 ++ .../FundamentalObjects/JSFunction.swift | 17 +++-- .../FundamentalObjects/JSObject.swift | 8 +++ .../FundamentalObjects/JSSymbol.swift | 56 ++++++++++++++++ Sources/JavaScriptKit/JSValue.swift | 66 ++++++++++++------- .../_CJavaScriptKit/include/_CJavaScriptKit.h | 5 ++ 9 files changed, 164 insertions(+), 47 deletions(-) create mode 100644 Sources/JavaScriptKit/FundamentalObjects/JSSymbol.swift diff --git a/IntegrationTests/TestSuites/Sources/PrimaryTests/main.swift b/IntegrationTests/TestSuites/Sources/PrimaryTests/main.swift index 8136f345e..07937b01a 100644 --- a/IntegrationTests/TestSuites/Sources/PrimaryTests/main.swift +++ b/IntegrationTests/TestSuites/Sources/PrimaryTests/main.swift @@ -804,4 +804,28 @@ try test("Hashable Conformance") { try expectEqual(firstHash, secondHash) } +try test("Symbols") { + let symbol1 = JSSymbol("abc") + let symbol2 = JSSymbol("abc") + try expectNotEqual(symbol1, symbol2) + try expectEqual(symbol1.name, symbol2.name) + try expectEqual(symbol1.name, "abc") + + try expectEqual(JSSymbol.iterator, JSSymbol.iterator) + + // let hasInstanceClass = { + // prop: Object.assign(function () {}, { + // [Symbol.hasInstance]() { return true } + // }) + // }.prop + let hasInstanceObject = JSObject.global.Object.function!.new() + hasInstanceObject.prop = JSClosure { _ in .undefined }.jsValue() + let hasInstanceClass = hasInstanceObject.prop.function! + hasInstanceClass[JSSymbol.hasInstance] = JSClosure { _ in + return .boolean(true) + }.jsValue() + try expectEqual(hasInstanceClass[JSSymbol.hasInstance].function!().boolean, true) + try expectEqual(JSObject.global.Object.isInstanceOf(hasInstanceClass), true) +} + Expectation.wait(expectations) diff --git a/Runtime/src/js-value.ts b/Runtime/src/js-value.ts index 61f7b486a..67ac5d46a 100644 --- a/Runtime/src/js-value.ts +++ b/Runtime/src/js-value.ts @@ -10,6 +10,7 @@ export enum Kind { Null = 4, Undefined = 5, Function = 6, + Symbol = 7, } export const decode = ( @@ -102,6 +103,11 @@ export const write = ( memory.writeUint32(payload1_ptr, memory.retain(value)); break; } + case "symbol": { + memory.writeUint32(kind_ptr, exceptionBit | Kind.Symbol); + memory.writeUint32(payload1_ptr, memory.retain(value)); + break; + } default: throw new Error(`Type "${typeof value}" is not supported yet`); } diff --git a/Runtime/src/object-heap.ts b/Runtime/src/object-heap.ts index 61f1925fe..2f9b1fdf3 100644 --- a/Runtime/src/object-heap.ts +++ b/Runtime/src/object-heap.ts @@ -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/Sources/JavaScriptKit/ConvertibleToJSValue.swift b/Sources/JavaScriptKit/ConvertibleToJSValue.swift index 7917c32cb..deb4a523c 100644 --- a/Sources/JavaScriptKit/ConvertibleToJSValue.swift +++ b/Sources/JavaScriptKit/ConvertibleToJSValue.swift @@ -194,6 +194,8 @@ extension RawJSValue: ConvertibleToJSValue { return .undefined case .function: return .function(JSFunction(id: UInt32(payload1))) + case .symbol: + return .symbol(JSSymbol(id: UInt32(payload1))) } } } @@ -225,6 +227,9 @@ extension JSValue { case let .function(functionRef): kind = .function payload1 = JavaScriptPayload1(functionRef.id) + case let .symbol(symbolRef): + kind = .symbol + payload1 = JavaScriptPayload1(symbolRef.id) } let rawValue = RawJSValue(kind: kind, payload1: payload1, payload2: payload2) return body(rawValue) diff --git a/Sources/JavaScriptKit/FundamentalObjects/JSFunction.swift b/Sources/JavaScriptKit/FundamentalObjects/JSFunction.swift index 0d3a917c0..6a05a8de2 100644 --- a/Sources/JavaScriptKit/FundamentalObjects/JSFunction.swift +++ b/Sources/JavaScriptKit/FundamentalObjects/JSFunction.swift @@ -11,7 +11,6 @@ import _CJavaScriptKit /// ``` /// public class JSFunction: JSObject { - /// 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. @@ -19,7 +18,7 @@ public class JSFunction: JSObject { /// - Returns: The result of this call. @discardableResult public func callAsFunction(this: JSObject? = nil, arguments: [ConvertibleToJSValue]) -> JSValue { - invokeNonThrowingJSFunction(self, arguments: arguments, this: this) + invokeNonThrowingJSFunction(self, arguments: arguments, this: this).jsValue() } /// A variadic arguments version of `callAsFunction`. @@ -41,7 +40,7 @@ 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: _call_new(self.id, bufferPointer.baseAddress!, Int32(bufferPointer.count))) } } } @@ -75,7 +74,7 @@ public class JSFunction: JSObject { fatalError("unavailable") } - public override class func construct(from value: JSValue) -> Self? { + override public class func construct(from value: JSValue) -> Self? { return value.function as? Self } @@ -84,9 +83,9 @@ public class JSFunction: JSObject { } } -private func invokeNonThrowingJSFunction(_ jsFunc: JSFunction, arguments: [ConvertibleToJSValue], this: JSObject?) -> JSValue { +func invokeNonThrowingJSFunction(_ jsFunc: JSFunction, arguments: [ConvertibleToJSValue], this: JSObject?) -> RawJSValue { arguments.withRawJSValues { rawValues in - rawValues.withUnsafeBufferPointer { bufferPointer -> (JSValue) in + rawValues.withUnsafeBufferPointer { bufferPointer in let argv = bufferPointer.baseAddress let argc = bufferPointer.count var kindAndFlags = JavaScriptValueKindAndFlags() @@ -94,8 +93,8 @@ private func invokeNonThrowingJSFunction(_ jsFunc: JSFunction, arguments: [Conve var payload2 = JavaScriptPayload2() if let thisId = this?.id { _call_function_with_this_no_catch(thisId, - jsFunc.id, argv, Int32(argc), - &kindAndFlags, &payload1, &payload2) + jsFunc.id, argv, Int32(argc), + &kindAndFlags, &payload1, &payload2) } else { _call_function_no_catch( jsFunc.id, argv, Int32(argc), @@ -104,7 +103,7 @@ private func invokeNonThrowingJSFunction(_ jsFunc: JSFunction, arguments: [Conve } assert(!kindAndFlags.isException) let result = RawJSValue(kind: kindAndFlags.kind, payload1: payload1, payload2: payload2) - return result.jsValue() + return result } } } diff --git a/Sources/JavaScriptKit/FundamentalObjects/JSObject.swift b/Sources/JavaScriptKit/FundamentalObjects/JSObject.swift index 3bafe60b5..0768817e2 100644 --- a/Sources/JavaScriptKit/FundamentalObjects/JSObject.swift +++ b/Sources/JavaScriptKit/FundamentalObjects/JSObject.swift @@ -77,6 +77,14 @@ public class JSObject: Equatable { set { 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 { getJSValue(this: self, symbol: name) } + set { setJSValue(this: self, symbol: name, value: newValue) } + } + /// A modifier to call methods as throwing methods capturing `this` /// /// diff --git a/Sources/JavaScriptKit/FundamentalObjects/JSSymbol.swift b/Sources/JavaScriptKit/FundamentalObjects/JSSymbol.swift new file mode 100644 index 000000000..3ec1b2902 --- /dev/null +++ b/Sources/JavaScriptKit/FundamentalObjects/JSSymbol.swift @@ -0,0 +1,56 @@ +import _CJavaScriptKit + +private let Symbol = JSObject.global.Symbol.function! + +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 = invokeNonThrowingJSFunction(Symbol, arguments: [description], this: nil) + 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) + } + + 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 + } +} + +extension JSSymbol { + public static let asyncIterator: JSSymbol! = Symbol.asyncIterator.symbol + public static let hasInstance: JSSymbol! = Symbol.hasInstance.symbol + public static let isConcatSpreadable: JSSymbol! = Symbol.isConcatSpreadable.symbol + public static let iterator: JSSymbol! = Symbol.iterator.symbol + public static let match: JSSymbol! = Symbol.match.symbol + public static let matchAll: JSSymbol! = Symbol.matchAll.symbol + public static let replace: JSSymbol! = Symbol.replace.symbol + public static let search: JSSymbol! = Symbol.search.symbol + public static let species: JSSymbol! = Symbol.species.symbol + public static let split: JSSymbol! = Symbol.split.symbol + public static let toPrimitive: JSSymbol! = Symbol.toPrimitive.symbol + public static let toStringTag: JSSymbol! = Symbol.toStringTag.symbol + public static let unscopables: JSSymbol! = Symbol.unscopables.symbol +} diff --git a/Sources/JavaScriptKit/JSValue.swift b/Sources/JavaScriptKit/JSValue.swift index 34bb78232..b363db679 100644 --- a/Sources/JavaScriptKit/JSValue.swift +++ b/Sources/JavaScriptKit/JSValue.swift @@ -10,6 +10,7 @@ public enum JSValue: Equatable { case null case undefined case function(JSFunction) + case symbol(JSSymbol) /// Returns the `Bool` value of this JS value if its type is boolean. /// If not, returns `nil`. @@ -67,6 +68,13 @@ public enum JSValue: Equatable { } } + public var symbol: JSSymbol? { + switch self { + case let .symbol(symbol): return symbol + default: return nil + } + } + /// Returns the `true` if this JS value is null. /// If not, returns `false`. public var isNull: Bool { @@ -80,23 +88,23 @@ public enum JSValue: Equatable { } } -extension JSValue { +public extension JSValue { /// 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) { + subscript(dynamicMember name: String) -> ((ConvertibleToJSValue...) -> JSValue) { object![dynamicMember: name]! } /// An unsafe convenience method of `JSObject.subscript(_ index: Int) -> JSValue` /// - Precondition: `self` must be a JavaScript Object. - public subscript(dynamicMember name: String) -> JSValue { + subscript(dynamicMember name: String) -> JSValue { get { self.object![name] } 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 { + subscript(_ index: Int) -> JSValue { get { object![index] } set { object![index] = newValue } } @@ -104,15 +112,14 @@ extension JSValue { extension JSValue: Swift.Error {} -extension JSValue { - public func fromJSValue() -> Type? where Type: ConstructibleFromJSValue { +public extension JSValue { + func fromJSValue() -> Type? where Type: ConstructibleFromJSValue { return Type.construct(from: self) } } -extension JSValue { - - public static func string(_ value: String) -> JSValue { +public extension JSValue { + static func string(_ value: String) -> JSValue { .string(JSString(value)) } @@ -141,12 +148,12 @@ extension JSValue { /// eventListenter.release() /// ``` @available(*, deprecated, message: "Please create JSClosure directly and manage its lifetime manually.") - public static func function(_ body: @escaping ([JSValue]) -> JSValue) -> JSValue { + 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.") - public static func function(_ closure: JSClosure) -> JSValue { + static func function(_ closure: JSClosure) -> JSValue { .object(closure) } } @@ -170,7 +177,7 @@ extension JSValue: ExpressibleByFloatLiteral { } extension JSValue: ExpressibleByNilLiteral { - public init(nilLiteral: ()) { + public init(nilLiteral _: ()) { self = .null } } @@ -205,14 +212,28 @@ public func setJSValue(this: JSObject, index: Int32, value: JSValue) { } } -extension JSValue { - /// Return `true` if this value is an instance of the passed `constructor` function. - /// Returns `false` for everything except objects and functions. - /// - Parameter constructor: The constructor function to check. - /// - Returns: The result of `instanceof` in the JavaScript environment. - public func isInstanceOf(_ constructor: JSFunction) -> Bool { +public func getJSValue(this: JSObject, symbol: JSSymbol) -> JSValue { + var rawValue = RawJSValue() + _get_prop(this.id, symbol.id, + &rawValue.kind, + &rawValue.payload1, &rawValue.payload2) + return rawValue.jsValue() +} + +public func setJSValue(this: JSObject, symbol: JSSymbol, value: JSValue) { + value.withRawJSValue { rawValue in + _set_prop(this.id, symbol.id, rawValue.kind, rawValue.payload1, rawValue.payload2) + } +} + +public extension JSValue { + /// Return `true` if this value is an instance of the passed `constructor` function. + /// Returns `false` for everything except objects and functions. + /// - Parameter constructor: The constructor function to check. + /// - Returns: The result of `instanceof` in the JavaScript environment. + func isInstanceOf(_ constructor: JSFunction) -> Bool { switch self { - case .boolean, .string, .number, .null, .undefined: + case .boolean, .string, .number, .null, .undefined, .symbol: return false case let .object(ref): return ref.isInstanceOf(constructor) @@ -227,11 +248,12 @@ extension JSValue: CustomStringConvertible { switch self { case let .boolean(boolean): return boolean.description - case .string(let string): + case let .string(string): return string.description - case .number(let number): + case let .number(number): return number.description - case .object(let object), .function(let object as JSObject): + case let .object(object), let .function(object as JSObject), + .symbol(let object as JSObject): return object.toString!().fromJSValue()! case .null: return "null" diff --git a/Sources/_CJavaScriptKit/include/_CJavaScriptKit.h b/Sources/_CJavaScriptKit/include/_CJavaScriptKit.h index ce0bf5862..daf405141 100644 --- a/Sources/_CJavaScriptKit/include/_CJavaScriptKit.h +++ b/Sources/_CJavaScriptKit/include/_CJavaScriptKit.h @@ -21,6 +21,7 @@ typedef enum __attribute__((enum_extensibility(closed))) { JavaScriptValueKindNull = 4, JavaScriptValueKindUndefined = 5, JavaScriptValueKindFunction = 6, + JavaScriptValueKindSymbol = 7, } JavaScriptValueKind; typedef struct { @@ -60,6 +61,10 @@ typedef double JavaScriptPayload2; /// payload1: the target `JavaScriptHostFuncRef` /// payload2: 0 /// +/// For symbol value: +/// payload1: `JavaScriptObjectRef` +/// payload2: 0 +/// typedef struct { JavaScriptValueKind kind; JavaScriptPayload1 payload1; From 95d0c4cd78b48ffc7e19c618d57c3244917be09a Mon Sep 17 00:00:00 2001 From: Jed Fox Date: Mon, 4 Apr 2022 11:55:24 -0400 Subject: [PATCH 007/373] Updates for DOMKit (#174) --- .../Sources/PrimaryTests/main.swift | 24 +++--- .../JavaScriptEventLoop.swift | 4 +- Sources/JavaScriptEventLoop/JobQueue.swift | 6 +- .../JavaScriptKit/BasicObjects/JSError.swift | 2 +- .../BasicObjects/JSPromise.swift | 8 +- .../BasicObjects/JSTypedArray.swift | 13 +-- .../JavaScriptKit/ConvertibleToJSValue.swift | 81 +++++++++++-------- .../FundamentalObjects/JSClosure.swift | 4 +- .../FundamentalObjects/JSFunction.swift | 4 +- .../FundamentalObjects/JSObject.swift | 20 ++++- .../JSThrowingFunction.swift | 4 +- Sources/JavaScriptKit/JSBridgedType.swift | 12 +-- Sources/JavaScriptKit/JSValue.swift | 6 +- 13 files changed, 108 insertions(+), 80 deletions(-) diff --git a/IntegrationTests/TestSuites/Sources/PrimaryTests/main.swift b/IntegrationTests/TestSuites/Sources/PrimaryTests/main.swift index 07937b01a..bf4c9bc4f 100644 --- a/IntegrationTests/TestSuites/Sources/PrimaryTests/main.swift +++ b/IntegrationTests/TestSuites/Sources/PrimaryTests/main.swift @@ -356,14 +356,14 @@ try test("Call Function With This") { try test("Object Conversion") { let array1 = [1, 2, 3] - let jsArray1 = array1.jsValue().object! + 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! + let jsArray2 = array2.jsValue.object! try expectEqual(jsArray2.length, .number(3)) try expectEqual(jsArray2[0], .number(1)) try expectEqual(jsArray2[1], .string("str")) @@ -374,11 +374,11 @@ try test("Object Conversion") { try expectEqual(jsArray2[4], .object(jsArray1)) - let dict1: [String: ConvertibleToJSValue] = [ - "prop1": 1, - "prop2": "foo", + let dict1: [String: JSValue] = [ + "prop1": 1.jsValue, + "prop2": "foo".jsValue, ] - let jsDict1 = dict1.jsValue().object! + let jsDict1 = dict1.jsValue.object! try expectEqual(jsDict1.prop1, .number(1)) try expectEqual(jsDict1.prop2, .string("foo")) } @@ -425,7 +425,7 @@ try test("Closure Identifiers") { #endif func checkArray(_ array: [T]) throws where T: TypedArrayElement & Equatable { - try expectEqual(toString(JSTypedArray(array).jsValue().object!), jsStringify(array)) + try expectEqual(toString(JSTypedArray(array).jsValue.object!), jsStringify(array)) try checkArrayUnsafeBytes(array) } @@ -488,7 +488,7 @@ try test("TypedArray_Mutation") { for i in 0..<100 { try expectEqual(i, array[i]) } - try expectEqual(toString(array.jsValue().object!), jsStringify(Array(0..<100))) + try expectEqual(toString(array.jsValue.object!), jsStringify(Array(0..<100))) } try test("Date") { @@ -797,9 +797,9 @@ try test("Hashable Conformance") { let objectConstructor = JSObject.global.Object.function! let obj = objectConstructor.new() - obj.a = 1.jsValue() + obj.a = 1.jsValue let firstHash = obj.hashValue - obj.b = 2.jsValue() + obj.b = 2.jsValue let secondHash = obj.hashValue try expectEqual(firstHash, secondHash) } @@ -819,11 +819,11 @@ try test("Symbols") { // }) // }.prop let hasInstanceObject = JSObject.global.Object.function!.new() - hasInstanceObject.prop = JSClosure { _ in .undefined }.jsValue() + hasInstanceObject.prop = JSClosure { _ in .undefined }.jsValue let hasInstanceClass = hasInstanceObject.prop.function! hasInstanceClass[JSSymbol.hasInstance] = JSClosure { _ in return .boolean(true) - }.jsValue() + }.jsValue try expectEqual(hasInstanceClass[JSSymbol.hasInstance].function!().boolean, true) try expectEqual(JSObject.global.Object.isInstanceOf(hasInstanceClass), true) } diff --git a/Sources/JavaScriptEventLoop/JavaScriptEventLoop.swift b/Sources/JavaScriptEventLoop/JavaScriptEventLoop.swift index b68889c42..8ff30c8aa 100644 --- a/Sources/JavaScriptEventLoop/JavaScriptEventLoop.swift +++ b/Sources/JavaScriptEventLoop/JavaScriptEventLoop.swift @@ -5,7 +5,7 @@ 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, *) public final class JavaScriptEventLoop: SerialExecutor, @unchecked Sendable { /// A function that queues a given closure as a microtask into JavaScript event loop. @@ -97,7 +97,7 @@ public final class JavaScriptEventLoop: SerialExecutor, @unchecked Sendable { } } -@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, *) public extension JSPromise { /// Wait for the promise to complete, returning (or throwing) its result. var value: JSValue { diff --git a/Sources/JavaScriptEventLoop/JobQueue.swift b/Sources/JavaScriptEventLoop/JobQueue.swift index 56090d120..44b2f7249 100644 --- a/Sources/JavaScriptEventLoop/JobQueue.swift +++ b/Sources/JavaScriptEventLoop/JobQueue.swift @@ -6,13 +6,13 @@ 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 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) extension JavaScriptEventLoop { func insertJobQueue(job newJob: UnownedJob) { @@ -58,7 +58,7 @@ extension JavaScriptEventLoop { } } -@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, *) fileprivate extension UnownedJob { private func asImpl() -> UnsafeMutablePointer<_CJavaScriptEventLoop.Job> { unsafeBitCast(self, to: UnsafeMutablePointer<_CJavaScriptEventLoop.Job>.self) diff --git a/Sources/JavaScriptKit/BasicObjects/JSError.swift b/Sources/JavaScriptKit/BasicObjects/JSError.swift index 305f1d9d5..cbdac8d6e 100644 --- a/Sources/JavaScriptKit/BasicObjects/JSError.swift +++ b/Sources/JavaScriptKit/BasicObjects/JSError.swift @@ -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..0aa44cadd 100644 --- a/Sources/JavaScriptKit/BasicObjects/JSPromise.swift +++ b/Sources/JavaScriptKit/BasicObjects/JSPromise.swift @@ -79,7 +79,7 @@ public final class JSPromise: JSBridgedClass { @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!) } @@ -90,10 +90,10 @@ public final class JSPromise: JSBridgedClass { public func then(success: @escaping (JSValue) -> ConvertibleToJSValue, failure: @escaping (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!) } @@ -103,7 +103,7 @@ public final class JSPromise: JSBridgedClass { @discardableResult public func `catch`(failure: @escaping (JSValue) -> ConvertibleToJSValue) -> JSPromise { let closure = JSOneshotClosure { - return failure($0[0]).jsValue() + failure($0[0]).jsValue } return .init(unsafelyWrapping: jsObject.catch!(closure).object!) } diff --git a/Sources/JavaScriptKit/BasicObjects/JSTypedArray.swift b/Sources/JavaScriptKit/BasicObjects/JSTypedArray.swift index e073d7c29..ebcf35959 100644 --- a/Sources/JavaScriptKit/BasicObjects/JSTypedArray.swift +++ b/Sources/JavaScriptKit/BasicObjects/JSTypedArray.swift @@ -13,7 +13,7 @@ public protocol TypedArrayElement: ConvertibleToJSValue, ConstructibleFromJSValu /// 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 } + public class var constructor: JSFunction { Element.typedArrayClass } public var jsObject: JSObject public subscript(_ index: Int) -> Element { @@ -21,7 +21,7 @@ public class JSTypedArray: JSBridgedClass, ExpressibleByArrayLiteral wh return Element.construct(from: jsObject[index])! } set { - self.jsObject[index] = newValue.jsValue() + self.jsObject[index] = newValue.jsValue } } @@ -30,7 +30,7 @@ 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) { @@ -45,7 +45,7 @@ public class JSTypedArray: JSBridgedClass, ExpressibleByArrayLiteral wh /// - Parameter array: The array that will be copied to create a new instance of TypedArray public convenience init(_ array: [Element]) { let jsArrayRef = array.withUnsafeBufferPointer { ptr in - _create_typed_array(Element.typedArrayClass.id, ptr.baseAddress!, Int32(array.count)) + _create_typed_array(Self.constructor.id, ptr.baseAddress!, Int32(array.count)) } self.init(unsafelyWrapping: JSObject(id: jsArrayRef)) } @@ -116,7 +116,10 @@ extension Int8: TypedArrayElement { extension UInt8: TypedArrayElement { public static var typedArrayClass = JSObject.global.Uint8Array.function! } -// TODO: Support Uint8ClampedArray? + +public class JSUInt8ClampedArray: JSTypedArray { + public class override var constructor: JSFunction { JSObject.global.Uint8ClampedArray.function! } +} extension Int16: TypedArrayElement { public static var typedArrayClass = JSObject.global.Int16Array.function! diff --git a/Sources/JavaScriptKit/ConvertibleToJSValue.swift b/Sources/JavaScriptKit/ConvertibleToJSValue.swift index deb4a523c..83680ad02 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,67 @@ 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 { .number(Double(self)) } } extension UInt: ConvertibleToJSValue { - public func jsValue() -> JSValue { .number(Double(self)) } + public var jsValue: JSValue { .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)) } + public var 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)) } + public var 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 { @@ -84,17 +89,21 @@ extension JSObject: JSValueCompatible { private let objectConstructor = JSObject.global.Object.function! private let arrayConstructor = JSObject.global.Array.function! -extension Dictionary where Value: ConvertibleToJSValue, Key == String { - public func jsValue() -> JSValue { - Swift.Dictionary.jsValue(self)() +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) } } -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 +113,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)]() @@ -131,25 +140,29 @@ extension Optional: ConstructibleFromJSValue where Wrapped: ConstructibleFromJSV } 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 let .some(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 { +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) } @@ -176,7 +189,7 @@ extension Array: ConstructibleFromJSValue where Element: ConstructibleFromJSValu } extension RawJSValue: ConvertibleToJSValue { - public func jsValue() -> JSValue { + public var jsValue: JSValue { switch kind { case .invalid: fatalError() @@ -243,7 +256,7 @@ extension Array where Element == ConvertibleToJSValue { _ 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) } diff --git a/Sources/JavaScriptKit/FundamentalObjects/JSClosure.swift b/Sources/JavaScriptKit/FundamentalObjects/JSClosure.swift index f8c2632c9..cbd44bd6e 100644 --- a/Sources/JavaScriptKit/FundamentalObjects/JSClosure.swift +++ b/Sources/JavaScriptKit/FundamentalObjects/JSClosure.swift @@ -137,9 +137,7 @@ func _call_host_function_impl( guard let (_, hostFunc) = JSClosure.sharedClosures[hostFuncRef] else { fatalError("The function was already released") } - let arguments = UnsafeBufferPointer(start: argv, count: Int(argc)).map { - $0.jsValue() - } + let arguments = UnsafeBufferPointer(start: argv, count: Int(argc)).map(\.jsValue) let result = hostFunc(arguments) let callbackFuncRef = JSFunction(id: callbackFuncRef) _ = callbackFuncRef(result) diff --git a/Sources/JavaScriptKit/FundamentalObjects/JSFunction.swift b/Sources/JavaScriptKit/FundamentalObjects/JSFunction.swift index 6a05a8de2..9cec5dad0 100644 --- a/Sources/JavaScriptKit/FundamentalObjects/JSFunction.swift +++ b/Sources/JavaScriptKit/FundamentalObjects/JSFunction.swift @@ -18,7 +18,7 @@ public class JSFunction: JSObject { /// - Returns: The result of this call. @discardableResult public func callAsFunction(this: JSObject? = nil, arguments: [ConvertibleToJSValue]) -> JSValue { - invokeNonThrowingJSFunction(self, arguments: arguments, this: this).jsValue() + invokeNonThrowingJSFunction(self, arguments: arguments, this: this).jsValue } /// A variadic arguments version of `callAsFunction`. @@ -78,7 +78,7 @@ public class JSFunction: JSObject { return value.function as? Self } - override public func jsValue() -> JSValue { + override public var jsValue: JSValue { .function(self) } } diff --git a/Sources/JavaScriptKit/FundamentalObjects/JSObject.swift b/Sources/JavaScriptKit/FundamentalObjects/JSObject.swift index 0768817e2..427648bcc 100644 --- a/Sources/JavaScriptKit/FundamentalObjects/JSObject.swift +++ b/Sources/JavaScriptKit/FundamentalObjects/JSObject.swift @@ -39,6 +39,24 @@ 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 @@ -134,7 +152,7 @@ public class JSObject: Equatable { return value.object as? Self } - public func jsValue() -> JSValue { + public var jsValue: JSValue { .object(self) } } diff --git a/Sources/JavaScriptKit/FundamentalObjects/JSThrowingFunction.swift b/Sources/JavaScriptKit/FundamentalObjects/JSThrowingFunction.swift index adcd82a63..3e21f0e1b 100644 --- a/Sources/JavaScriptKit/FundamentalObjects/JSThrowingFunction.swift +++ b/Sources/JavaScriptKit/FundamentalObjects/JSThrowingFunction.swift @@ -50,7 +50,7 @@ public class JSThrowingFunction { ) if exceptionKind.isException { let exception = RawJSValue(kind: exceptionKind.kind, payload1: exceptionPayload1, payload2: exceptionPayload2) - return .failure(exception.jsValue()) + return .failure(exception.jsValue) } return .success(JSObject(id: resultObj)) } @@ -82,7 +82,7 @@ private func invokeJSFunction(_ jsFunc: JSFunction, arguments: [ConvertibleToJSV ) } let result = RawJSValue(kind: kindAndFlags.kind, payload1: payload1, payload2: payload2) - return (result.jsValue(), kindAndFlags.isException) + return (result.jsValue, kindAndFlags.isException) } } if isException { diff --git a/Sources/JavaScriptKit/JSBridgedType.swift b/Sources/JavaScriptKit/JSBridgedType.swift index acd1fa6ef..235d78331 100644 --- a/Sources/JavaScriptKit/JSBridgedType.swift +++ b/Sources/JavaScriptKit/JSBridgedType.swift @@ -1,21 +1,16 @@ /// 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.init(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. @@ -33,7 +28,8 @@ 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) diff --git a/Sources/JavaScriptKit/JSValue.swift b/Sources/JavaScriptKit/JSValue.swift index b363db679..b001dc7ab 100644 --- a/Sources/JavaScriptKit/JSValue.swift +++ b/Sources/JavaScriptKit/JSValue.swift @@ -187,7 +187,7 @@ public func getJSValue(this: JSObject, name: JSString) -> JSValue { _get_prop(this.id, name.asInternalJSRef(), &rawValue.kind, &rawValue.payload1, &rawValue.payload2) - return rawValue.jsValue() + return rawValue.jsValue } public func setJSValue(this: JSObject, name: JSString, value: JSValue) { @@ -201,7 +201,7 @@ public func getJSValue(this: JSObject, index: Int32) -> JSValue { _get_subscript(this.id, index, &rawValue.kind, &rawValue.payload1, &rawValue.payload2) - return rawValue.jsValue() + return rawValue.jsValue } public func setJSValue(this: JSObject, index: Int32, value: JSValue) { @@ -217,7 +217,7 @@ public func getJSValue(this: JSObject, symbol: JSSymbol) -> JSValue { _get_prop(this.id, symbol.id, &rawValue.kind, &rawValue.payload1, &rawValue.payload2) - return rawValue.jsValue() + return rawValue.jsValue } public func setJSValue(this: JSObject, symbol: JSSymbol, value: JSValue) { From 990479ef616eb14bec7fce2a29b9f5695df65a1d Mon Sep 17 00:00:00 2001 From: Yuta Saito Date: Thu, 7 Apr 2022 03:39:06 +0900 Subject: [PATCH 008/373] Revert "Add support for Symbol objects via JSSymbol (#179)" This reverts commit 081784b34ad94c91eddccca58f1592ce181328ad. --- .../Sources/PrimaryTests/main.swift | 23 ------- Runtime/src/js-value.ts | 6 -- Runtime/src/object-heap.ts | 24 ++++--- .../JavaScriptKit/ConvertibleToJSValue.swift | 5 -- .../FundamentalObjects/JSFunction.swift | 17 ++--- .../FundamentalObjects/JSObject.swift | 8 --- .../FundamentalObjects/JSSymbol.swift | 56 ---------------- Sources/JavaScriptKit/JSValue.swift | 66 +++++++------------ .../_CJavaScriptKit/include/_CJavaScriptKit.h | 5 -- 9 files changed, 47 insertions(+), 163 deletions(-) delete mode 100644 Sources/JavaScriptKit/FundamentalObjects/JSSymbol.swift diff --git a/IntegrationTests/TestSuites/Sources/PrimaryTests/main.swift b/IntegrationTests/TestSuites/Sources/PrimaryTests/main.swift index bf4c9bc4f..ff42d3358 100644 --- a/IntegrationTests/TestSuites/Sources/PrimaryTests/main.swift +++ b/IntegrationTests/TestSuites/Sources/PrimaryTests/main.swift @@ -804,28 +804,5 @@ try test("Hashable Conformance") { try expectEqual(firstHash, secondHash) } -try test("Symbols") { - let symbol1 = JSSymbol("abc") - let symbol2 = JSSymbol("abc") - try expectNotEqual(symbol1, symbol2) - try expectEqual(symbol1.name, symbol2.name) - try expectEqual(symbol1.name, "abc") - - try expectEqual(JSSymbol.iterator, JSSymbol.iterator) - - // let hasInstanceClass = { - // prop: Object.assign(function () {}, { - // [Symbol.hasInstance]() { return true } - // }) - // }.prop - let hasInstanceObject = JSObject.global.Object.function!.new() - hasInstanceObject.prop = JSClosure { _ in .undefined }.jsValue - let hasInstanceClass = hasInstanceObject.prop.function! - hasInstanceClass[JSSymbol.hasInstance] = JSClosure { _ in - return .boolean(true) - }.jsValue - try expectEqual(hasInstanceClass[JSSymbol.hasInstance].function!().boolean, true) - try expectEqual(JSObject.global.Object.isInstanceOf(hasInstanceClass), true) -} Expectation.wait(expectations) diff --git a/Runtime/src/js-value.ts b/Runtime/src/js-value.ts index 67ac5d46a..61f7b486a 100644 --- a/Runtime/src/js-value.ts +++ b/Runtime/src/js-value.ts @@ -10,7 +10,6 @@ export enum Kind { Null = 4, Undefined = 5, Function = 6, - Symbol = 7, } export const decode = ( @@ -103,11 +102,6 @@ export const write = ( memory.writeUint32(payload1_ptr, memory.retain(value)); break; } - case "symbol": { - memory.writeUint32(kind_ptr, exceptionBit | Kind.Symbol); - memory.writeUint32(payload1_ptr, memory.retain(value)); - break; - } default: throw new Error(`Type "${typeof value}" is not supported yet`); } diff --git a/Runtime/src/object-heap.ts b/Runtime/src/object-heap.ts index 2f9b1fdf3..61f1925fe 100644 --- a/Runtime/src/object-heap.ts +++ b/Runtime/src/object-heap.ts @@ -22,25 +22,33 @@ export class SwiftRuntimeHeap { } retain(value: any) { + const isObject = typeof value == "object"; const entry = this._heapEntryByValue.get(value); - if (entry) { + if (isObject && entry) { entry.rc++; return entry.id; } const id = this._heapNextKey++; this._heapValueById.set(id, value); - this._heapEntryByValue.set(value, { id: id, rc: 1 }); + if (isObject) { + this._heapEntryByValue.set(value, { id: id, rc: 1 }); + } return id; } release(ref: 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); + 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); + } } referenceHeap(ref: ref) { diff --git a/Sources/JavaScriptKit/ConvertibleToJSValue.swift b/Sources/JavaScriptKit/ConvertibleToJSValue.swift index 83680ad02..10d29a1a6 100644 --- a/Sources/JavaScriptKit/ConvertibleToJSValue.swift +++ b/Sources/JavaScriptKit/ConvertibleToJSValue.swift @@ -207,8 +207,6 @@ extension RawJSValue: ConvertibleToJSValue { return .undefined case .function: return .function(JSFunction(id: UInt32(payload1))) - case .symbol: - return .symbol(JSSymbol(id: UInt32(payload1))) } } } @@ -240,9 +238,6 @@ extension JSValue { case let .function(functionRef): kind = .function payload1 = JavaScriptPayload1(functionRef.id) - case let .symbol(symbolRef): - kind = .symbol - payload1 = JavaScriptPayload1(symbolRef.id) } let rawValue = RawJSValue(kind: kind, payload1: payload1, payload2: payload2) return body(rawValue) diff --git a/Sources/JavaScriptKit/FundamentalObjects/JSFunction.swift b/Sources/JavaScriptKit/FundamentalObjects/JSFunction.swift index 9cec5dad0..cbc17a6f5 100644 --- a/Sources/JavaScriptKit/FundamentalObjects/JSFunction.swift +++ b/Sources/JavaScriptKit/FundamentalObjects/JSFunction.swift @@ -11,6 +11,7 @@ import _CJavaScriptKit /// ``` /// public class JSFunction: JSObject { + /// 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. @@ -18,7 +19,7 @@ public class JSFunction: JSObject { /// - Returns: The result of this call. @discardableResult public func callAsFunction(this: JSObject? = nil, arguments: [ConvertibleToJSValue]) -> JSValue { - invokeNonThrowingJSFunction(self, arguments: arguments, this: this).jsValue + invokeNonThrowingJSFunction(self, arguments: arguments, this: this) } /// A variadic arguments version of `callAsFunction`. @@ -40,7 +41,7 @@ public class JSFunction: JSObject { public func new(arguments: [ConvertibleToJSValue]) -> JSObject { arguments.withRawJSValues { rawValues in rawValues.withUnsafeBufferPointer { bufferPointer in - JSObject(id: _call_new(self.id, bufferPointer.baseAddress!, Int32(bufferPointer.count))) + return JSObject(id: _call_new(self.id, bufferPointer.baseAddress!, Int32(bufferPointer.count))) } } } @@ -74,7 +75,7 @@ public class JSFunction: JSObject { fatalError("unavailable") } - override public class func construct(from value: JSValue) -> Self? { + public override class func construct(from value: JSValue) -> Self? { return value.function as? Self } @@ -83,9 +84,9 @@ public class JSFunction: JSObject { } } -func invokeNonThrowingJSFunction(_ jsFunc: JSFunction, arguments: [ConvertibleToJSValue], this: JSObject?) -> RawJSValue { +private func invokeNonThrowingJSFunction(_ jsFunc: JSFunction, arguments: [ConvertibleToJSValue], this: JSObject?) -> JSValue { arguments.withRawJSValues { rawValues in - rawValues.withUnsafeBufferPointer { bufferPointer in + rawValues.withUnsafeBufferPointer { bufferPointer -> (JSValue) in let argv = bufferPointer.baseAddress let argc = bufferPointer.count var kindAndFlags = JavaScriptValueKindAndFlags() @@ -93,8 +94,8 @@ func invokeNonThrowingJSFunction(_ jsFunc: JSFunction, arguments: [ConvertibleTo var payload2 = JavaScriptPayload2() if let thisId = this?.id { _call_function_with_this_no_catch(thisId, - jsFunc.id, argv, Int32(argc), - &kindAndFlags, &payload1, &payload2) + jsFunc.id, argv, Int32(argc), + &kindAndFlags, &payload1, &payload2) } else { _call_function_no_catch( jsFunc.id, argv, Int32(argc), @@ -103,7 +104,7 @@ func invokeNonThrowingJSFunction(_ jsFunc: JSFunction, arguments: [ConvertibleTo } assert(!kindAndFlags.isException) let result = RawJSValue(kind: kindAndFlags.kind, payload1: payload1, payload2: payload2) - return result + return result.jsValue() } } } diff --git a/Sources/JavaScriptKit/FundamentalObjects/JSObject.swift b/Sources/JavaScriptKit/FundamentalObjects/JSObject.swift index 427648bcc..659d9212a 100644 --- a/Sources/JavaScriptKit/FundamentalObjects/JSObject.swift +++ b/Sources/JavaScriptKit/FundamentalObjects/JSObject.swift @@ -95,14 +95,6 @@ public class JSObject: Equatable { set { 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 { getJSValue(this: self, symbol: name) } - set { setJSValue(this: self, symbol: name, value: newValue) } - } - /// A modifier to call methods as throwing methods capturing `this` /// /// diff --git a/Sources/JavaScriptKit/FundamentalObjects/JSSymbol.swift b/Sources/JavaScriptKit/FundamentalObjects/JSSymbol.swift deleted file mode 100644 index 3ec1b2902..000000000 --- a/Sources/JavaScriptKit/FundamentalObjects/JSSymbol.swift +++ /dev/null @@ -1,56 +0,0 @@ -import _CJavaScriptKit - -private let Symbol = JSObject.global.Symbol.function! - -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 = invokeNonThrowingJSFunction(Symbol, arguments: [description], this: nil) - 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) - } - - 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 - } -} - -extension JSSymbol { - public static let asyncIterator: JSSymbol! = Symbol.asyncIterator.symbol - public static let hasInstance: JSSymbol! = Symbol.hasInstance.symbol - public static let isConcatSpreadable: JSSymbol! = Symbol.isConcatSpreadable.symbol - public static let iterator: JSSymbol! = Symbol.iterator.symbol - public static let match: JSSymbol! = Symbol.match.symbol - public static let matchAll: JSSymbol! = Symbol.matchAll.symbol - public static let replace: JSSymbol! = Symbol.replace.symbol - public static let search: JSSymbol! = Symbol.search.symbol - public static let species: JSSymbol! = Symbol.species.symbol - public static let split: JSSymbol! = Symbol.split.symbol - public static let toPrimitive: JSSymbol! = Symbol.toPrimitive.symbol - public static let toStringTag: JSSymbol! = Symbol.toStringTag.symbol - public static let unscopables: JSSymbol! = Symbol.unscopables.symbol -} diff --git a/Sources/JavaScriptKit/JSValue.swift b/Sources/JavaScriptKit/JSValue.swift index b001dc7ab..03c2a81ab 100644 --- a/Sources/JavaScriptKit/JSValue.swift +++ b/Sources/JavaScriptKit/JSValue.swift @@ -10,7 +10,6 @@ public enum JSValue: Equatable { case null case undefined case function(JSFunction) - case symbol(JSSymbol) /// Returns the `Bool` value of this JS value if its type is boolean. /// If not, returns `nil`. @@ -68,13 +67,6 @@ public enum JSValue: Equatable { } } - public var symbol: JSSymbol? { - switch self { - case let .symbol(symbol): return symbol - default: return nil - } - } - /// Returns the `true` if this JS value is null. /// If not, returns `false`. public var isNull: Bool { @@ -88,23 +80,23 @@ public enum JSValue: Equatable { } } -public extension JSValue { +extension JSValue { /// An unsafe convenience method of `JSObject.subscript(_ name: String) -> ((ConvertibleToJSValue...) -> JSValue)?` /// - Precondition: `self` must be a JavaScript Object and specified member should be a callable object. - subscript(dynamicMember name: String) -> ((ConvertibleToJSValue...) -> JSValue) { + public subscript(dynamicMember name: String) -> ((ConvertibleToJSValue...) -> JSValue) { object![dynamicMember: name]! } /// An unsafe convenience method of `JSObject.subscript(_ index: Int) -> JSValue` /// - Precondition: `self` must be a JavaScript Object. - subscript(dynamicMember name: String) -> JSValue { + public subscript(dynamicMember name: String) -> JSValue { get { self.object![name] } set { self.object![name] = newValue } } /// An unsafe convenience method of `JSObject.subscript(_ index: Int) -> JSValue` /// - Precondition: `self` must be a JavaScript Object. - subscript(_ index: Int) -> JSValue { + public subscript(_ index: Int) -> JSValue { get { object![index] } set { object![index] = newValue } } @@ -112,14 +104,15 @@ public extension JSValue { extension JSValue: Swift.Error {} -public extension JSValue { - func fromJSValue() -> Type? where Type: ConstructibleFromJSValue { +extension JSValue { + public func fromJSValue() -> Type? where Type: ConstructibleFromJSValue { return Type.construct(from: self) } } -public extension JSValue { - static func string(_ value: String) -> JSValue { +extension JSValue { + + public static func string(_ value: String) -> JSValue { .string(JSString(value)) } @@ -148,12 +141,12 @@ public extension JSValue { /// eventListenter.release() /// ``` @available(*, deprecated, message: "Please create JSClosure directly and manage its lifetime manually.") - static func function(_ body: @escaping ([JSValue]) -> JSValue) -> JSValue { + public static func function(_ body: @escaping ([JSValue]) -> JSValue) -> JSValue { .object(JSClosure(body)) } @available(*, deprecated, renamed: "object", message: "JSClosure is no longer a subclass of JSFunction. Use .object(closure) instead.") - static func function(_ closure: JSClosure) -> JSValue { + public static func function(_ closure: JSClosure) -> JSValue { .object(closure) } } @@ -177,7 +170,7 @@ extension JSValue: ExpressibleByFloatLiteral { } extension JSValue: ExpressibleByNilLiteral { - public init(nilLiteral _: ()) { + public init(nilLiteral: ()) { self = .null } } @@ -212,28 +205,14 @@ public func setJSValue(this: JSObject, index: Int32, value: JSValue) { } } -public func getJSValue(this: JSObject, symbol: JSSymbol) -> JSValue { - var rawValue = RawJSValue() - _get_prop(this.id, symbol.id, - &rawValue.kind, - &rawValue.payload1, &rawValue.payload2) - return rawValue.jsValue -} - -public func setJSValue(this: JSObject, symbol: JSSymbol, value: JSValue) { - value.withRawJSValue { rawValue in - _set_prop(this.id, symbol.id, rawValue.kind, rawValue.payload1, rawValue.payload2) - } -} - -public extension JSValue { - /// Return `true` if this value is an instance of the passed `constructor` function. - /// Returns `false` for everything except objects and functions. - /// - Parameter constructor: The constructor function to check. - /// - Returns: The result of `instanceof` in the JavaScript environment. - func isInstanceOf(_ constructor: JSFunction) -> Bool { +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. + public func isInstanceOf(_ constructor: JSFunction) -> Bool { switch self { - case .boolean, .string, .number, .null, .undefined, .symbol: + case .boolean, .string, .number, .null, .undefined: return false case let .object(ref): return ref.isInstanceOf(constructor) @@ -248,12 +227,11 @@ extension JSValue: CustomStringConvertible { switch self { case let .boolean(boolean): return boolean.description - case let .string(string): + case .string(let string): return string.description - case let .number(number): + case .number(let number): return number.description - case let .object(object), let .function(object as JSObject), - .symbol(let object as JSObject): + case .object(let object), .function(let object as JSObject): return object.toString!().fromJSValue()! case .null: return "null" diff --git a/Sources/_CJavaScriptKit/include/_CJavaScriptKit.h b/Sources/_CJavaScriptKit/include/_CJavaScriptKit.h index daf405141..ce0bf5862 100644 --- a/Sources/_CJavaScriptKit/include/_CJavaScriptKit.h +++ b/Sources/_CJavaScriptKit/include/_CJavaScriptKit.h @@ -21,7 +21,6 @@ typedef enum __attribute__((enum_extensibility(closed))) { JavaScriptValueKindNull = 4, JavaScriptValueKindUndefined = 5, JavaScriptValueKindFunction = 6, - JavaScriptValueKindSymbol = 7, } JavaScriptValueKind; typedef struct { @@ -61,10 +60,6 @@ typedef double JavaScriptPayload2; /// payload1: the target `JavaScriptHostFuncRef` /// payload2: 0 /// -/// For symbol value: -/// payload1: `JavaScriptObjectRef` -/// payload2: 0 -/// typedef struct { JavaScriptValueKind kind; JavaScriptPayload1 payload1; From d7ed468afed62940d1fdc3310f123e3f99679b26 Mon Sep 17 00:00:00 2001 From: Yuta Saito Date: Thu, 7 Apr 2022 03:44:49 +0900 Subject: [PATCH 009/373] Export `main` and execute `_initialize` and `main` in test entry IntegrationTests are unintentionally disabled since 0a38d705e0c310f236599e2a0d62afa8012578db because reactor mode doesn't export `_start` and `WASI.start` doesn't do anything except setting the instance to WASI module. So export `main` manually, and call `_initialize` according to the "new-style commands" described in https://github.com/WebAssembly/WASI/blob/59cbe140561db52fc505555e859de884e0ee7f00/legacy/application-abi.md#current-unstable-abi Symbol support (081784b34ad94c91eddccca58f1592ce181328ad) broke tests silently, so revert it for now. --- IntegrationTests/Makefile | 1 + IntegrationTests/lib.js | 2 ++ 2 files changed, 3 insertions(+) diff --git a/IntegrationTests/Makefile b/IntegrationTests/Makefile index 575a8f20d..21e8be315 100644 --- a/IntegrationTests/Makefile +++ b/IntegrationTests/Makefile @@ -8,6 +8,7 @@ TestSuites/.build/$(CONFIGURATION)/%.wasm: FORCE --triple wasm32-unknown-wasi \ --configuration $(CONFIGURATION) \ -Xswiftc -Xclang-linker -Xswiftc -mexec-model=reactor \ + -Xlinker --export=main \ $(SWIFT_BUILD_FLAGS) dist/%.wasm: TestSuites/.build/$(CONFIGURATION)/%.wasm diff --git a/IntegrationTests/lib.js b/IntegrationTests/lib.js index ee0a6a33f..c55ed3b4d 100644 --- a/IntegrationTests/lib.js +++ b/IntegrationTests/lib.js @@ -45,6 +45,8 @@ const startWasiTask = async (wasmPath) => { swift.setInstance(instance); // Start the WebAssembly WASI instance! wasi.start(instance); + instance.exports._initialize(); + instance.exports.main(); }; module.exports = { startWasiTask }; From 9a3b7d5f316e6d1709bd25e3e0392efca357f525 Mon Sep 17 00:00:00 2001 From: Max Desiatov Date: Fri, 8 Apr 2022 09:42:08 +0100 Subject: [PATCH 010/373] Bump version to 0.14.0, update `CHANGELOG.md` (#181) New release is required to resolve https://github.com/TokamakUI/Tokamak/pull/475. I've also cleaned up formatting in `.ts` file and clarified version incompatibility error message. --- .swift-version | 2 +- CHANGELOG.md | 57 ++++++++++++++++++++++++++------------------ Runtime/src/index.ts | 12 +++++----- package-lock.json | 4 ++-- package.json | 2 +- 5 files changed, 44 insertions(+), 33 deletions(-) diff --git a/.swift-version b/.swift-version index 871073e10..08ddfb781 100644 --- a/.swift-version +++ b/.swift-version @@ -1 +1 @@ -wasm-5.5.0-RELEASE +wasm-5.6.0-RELEASE diff --git a/CHANGELOG.md b/CHANGELOG.md index 4569f705f..3d157e9b2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,13 @@ +# 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/). + +**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. @@ -21,7 +31,7 @@ Thanks to [@kateinoigakukun](https://github.com/kateinoigakukun), [@pedrovgs](ht # 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:** @@ -40,9 +50,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:** @@ -52,7 +63,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. @@ -64,12 +75,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)) @@ -97,7 +108,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] @@ -107,18 +118,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:** @@ -200,7 +211,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] @@ -278,10 +289,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/Runtime/src/index.ts b/Runtime/src/index.ts index 06fecc286..f2898f09e 100644 --- a/Runtime/src/index.ts +++ b/Runtime/src/index.ts @@ -29,7 +29,8 @@ export class SwiftRuntime { this._instance = instance; 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 }` ); @@ -177,10 +178,9 @@ export class SwiftRuntime { }, swjs_decode_string: (bytes_ptr: pointer, length: number) => { - const bytes = this.memory.bytes().subarray( - bytes_ptr, - bytes_ptr + length - ); + const bytes = this.memory + .bytes() + .subarray(bytes_ptr, bytes_ptr + length); const string = this.textDecoder.decode(bytes); return this.memory.retain(string); }, @@ -330,7 +330,7 @@ export class SwiftRuntime { false, this.memory ); - isException = false + isException = false; } finally { if (isException) { JSValue.write( diff --git a/package-lock.json b/package-lock.json index 6bce2ca52..9d0372017 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "javascript-kit-swift", - "version": "0.13.0", + "version": "0.14.0", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "javascript-kit-swift", - "version": "0.13.0", + "version": "0.14.0", "license": "MIT", "devDependencies": { "@rollup/plugin-typescript": "^8.3.1", diff --git a/package.json b/package.json index 26b05b29e..e24c39e27 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "javascript-kit-swift", - "version": "0.13.0", + "version": "0.14.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", From ed79d5928bd16376e6bfd47f6e391624901f9912 Mon Sep 17 00:00:00 2001 From: Max Desiatov Date: Fri, 8 Apr 2022 09:57:00 +0100 Subject: [PATCH 011/373] Clarify breaking changes in `CHANGELOG.md` --- CHANGELOG.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3d157e9b2..34864d1d8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,14 @@ 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) From ca19e88e5e0a70e5a2755e372477236c7c501341 Mon Sep 17 00:00:00 2001 From: Max Desiatov Date: Fri, 8 Apr 2022 13:10:05 +0100 Subject: [PATCH 012/373] Fix deprecation warning in `JSFunction.swift` (#182) --- Sources/JavaScriptKit/FundamentalObjects/JSFunction.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/JavaScriptKit/FundamentalObjects/JSFunction.swift b/Sources/JavaScriptKit/FundamentalObjects/JSFunction.swift index cbc17a6f5..4e0019f3c 100644 --- a/Sources/JavaScriptKit/FundamentalObjects/JSFunction.swift +++ b/Sources/JavaScriptKit/FundamentalObjects/JSFunction.swift @@ -104,7 +104,7 @@ private func invokeNonThrowingJSFunction(_ jsFunc: JSFunction, arguments: [Conve } assert(!kindAndFlags.isException) let result = RawJSValue(kind: kindAndFlags.kind, payload1: payload1, payload2: payload2) - return result.jsValue() + return result.jsValue } } } From 6b07c4f187b899518a2b4f8ed90d022b67400e15 Mon Sep 17 00:00:00 2001 From: Jed Fox Date: Fri, 8 Apr 2022 16:46:19 -0400 Subject: [PATCH 013/373] Revert "Revert "Add support for Symbol objects via JSSymbol (#179)"" This reverts commit 990479ef616eb14bec7fce2a29b9f5695df65a1d --- .../Sources/PrimaryTests/main.swift | 23 +++++++ Runtime/src/js-value.ts | 6 ++ Runtime/src/object-heap.ts | 24 +++---- .../JavaScriptKit/ConvertibleToJSValue.swift | 5 ++ .../FundamentalObjects/JSFunction.swift | 17 +++-- .../FundamentalObjects/JSObject.swift | 8 +++ .../FundamentalObjects/JSSymbol.swift | 56 ++++++++++++++++ Sources/JavaScriptKit/JSValue.swift | 66 ++++++++++++------- .../_CJavaScriptKit/include/_CJavaScriptKit.h | 5 ++ 9 files changed, 163 insertions(+), 47 deletions(-) create mode 100644 Sources/JavaScriptKit/FundamentalObjects/JSSymbol.swift diff --git a/IntegrationTests/TestSuites/Sources/PrimaryTests/main.swift b/IntegrationTests/TestSuites/Sources/PrimaryTests/main.swift index ff42d3358..bf4c9bc4f 100644 --- a/IntegrationTests/TestSuites/Sources/PrimaryTests/main.swift +++ b/IntegrationTests/TestSuites/Sources/PrimaryTests/main.swift @@ -804,5 +804,28 @@ try test("Hashable Conformance") { try expectEqual(firstHash, secondHash) } +try test("Symbols") { + let symbol1 = JSSymbol("abc") + let symbol2 = JSSymbol("abc") + try expectNotEqual(symbol1, symbol2) + try expectEqual(symbol1.name, symbol2.name) + try expectEqual(symbol1.name, "abc") + + try expectEqual(JSSymbol.iterator, JSSymbol.iterator) + + // let hasInstanceClass = { + // prop: Object.assign(function () {}, { + // [Symbol.hasInstance]() { return true } + // }) + // }.prop + let hasInstanceObject = JSObject.global.Object.function!.new() + hasInstanceObject.prop = JSClosure { _ in .undefined }.jsValue + let hasInstanceClass = hasInstanceObject.prop.function! + hasInstanceClass[JSSymbol.hasInstance] = JSClosure { _ in + return .boolean(true) + }.jsValue + try expectEqual(hasInstanceClass[JSSymbol.hasInstance].function!().boolean, true) + try expectEqual(JSObject.global.Object.isInstanceOf(hasInstanceClass), true) +} Expectation.wait(expectations) diff --git a/Runtime/src/js-value.ts b/Runtime/src/js-value.ts index 61f7b486a..67ac5d46a 100644 --- a/Runtime/src/js-value.ts +++ b/Runtime/src/js-value.ts @@ -10,6 +10,7 @@ export enum Kind { Null = 4, Undefined = 5, Function = 6, + Symbol = 7, } export const decode = ( @@ -102,6 +103,11 @@ export const write = ( memory.writeUint32(payload1_ptr, memory.retain(value)); break; } + case "symbol": { + memory.writeUint32(kind_ptr, exceptionBit | Kind.Symbol); + memory.writeUint32(payload1_ptr, memory.retain(value)); + break; + } default: throw new Error(`Type "${typeof value}" is not supported yet`); } diff --git a/Runtime/src/object-heap.ts b/Runtime/src/object-heap.ts index 61f1925fe..2f9b1fdf3 100644 --- a/Runtime/src/object-heap.ts +++ b/Runtime/src/object-heap.ts @@ -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/Sources/JavaScriptKit/ConvertibleToJSValue.swift b/Sources/JavaScriptKit/ConvertibleToJSValue.swift index 10d29a1a6..83680ad02 100644 --- a/Sources/JavaScriptKit/ConvertibleToJSValue.swift +++ b/Sources/JavaScriptKit/ConvertibleToJSValue.swift @@ -207,6 +207,8 @@ extension RawJSValue: ConvertibleToJSValue { return .undefined case .function: return .function(JSFunction(id: UInt32(payload1))) + case .symbol: + return .symbol(JSSymbol(id: UInt32(payload1))) } } } @@ -238,6 +240,9 @@ extension JSValue { case let .function(functionRef): kind = .function payload1 = JavaScriptPayload1(functionRef.id) + case let .symbol(symbolRef): + kind = .symbol + payload1 = JavaScriptPayload1(symbolRef.id) } let rawValue = RawJSValue(kind: kind, payload1: payload1, payload2: payload2) return body(rawValue) diff --git a/Sources/JavaScriptKit/FundamentalObjects/JSFunction.swift b/Sources/JavaScriptKit/FundamentalObjects/JSFunction.swift index 4e0019f3c..9cec5dad0 100644 --- a/Sources/JavaScriptKit/FundamentalObjects/JSFunction.swift +++ b/Sources/JavaScriptKit/FundamentalObjects/JSFunction.swift @@ -11,7 +11,6 @@ import _CJavaScriptKit /// ``` /// public class JSFunction: JSObject { - /// 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. @@ -19,7 +18,7 @@ public class JSFunction: JSObject { /// - Returns: The result of this call. @discardableResult public func callAsFunction(this: JSObject? = nil, arguments: [ConvertibleToJSValue]) -> JSValue { - invokeNonThrowingJSFunction(self, arguments: arguments, this: this) + invokeNonThrowingJSFunction(self, arguments: arguments, this: this).jsValue } /// A variadic arguments version of `callAsFunction`. @@ -41,7 +40,7 @@ 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: _call_new(self.id, bufferPointer.baseAddress!, Int32(bufferPointer.count))) } } } @@ -75,7 +74,7 @@ public class JSFunction: JSObject { fatalError("unavailable") } - public override class func construct(from value: JSValue) -> Self? { + override public class func construct(from value: JSValue) -> Self? { return value.function as? Self } @@ -84,9 +83,9 @@ public class JSFunction: JSObject { } } -private func invokeNonThrowingJSFunction(_ jsFunc: JSFunction, arguments: [ConvertibleToJSValue], this: JSObject?) -> JSValue { +func invokeNonThrowingJSFunction(_ jsFunc: JSFunction, arguments: [ConvertibleToJSValue], this: JSObject?) -> RawJSValue { arguments.withRawJSValues { rawValues in - rawValues.withUnsafeBufferPointer { bufferPointer -> (JSValue) in + rawValues.withUnsafeBufferPointer { bufferPointer in let argv = bufferPointer.baseAddress let argc = bufferPointer.count var kindAndFlags = JavaScriptValueKindAndFlags() @@ -94,8 +93,8 @@ private func invokeNonThrowingJSFunction(_ jsFunc: JSFunction, arguments: [Conve var payload2 = JavaScriptPayload2() if let thisId = this?.id { _call_function_with_this_no_catch(thisId, - jsFunc.id, argv, Int32(argc), - &kindAndFlags, &payload1, &payload2) + jsFunc.id, argv, Int32(argc), + &kindAndFlags, &payload1, &payload2) } else { _call_function_no_catch( jsFunc.id, argv, Int32(argc), @@ -104,7 +103,7 @@ private func invokeNonThrowingJSFunction(_ jsFunc: JSFunction, arguments: [Conve } assert(!kindAndFlags.isException) let result = RawJSValue(kind: kindAndFlags.kind, payload1: payload1, payload2: payload2) - return result.jsValue + return result } } } diff --git a/Sources/JavaScriptKit/FundamentalObjects/JSObject.swift b/Sources/JavaScriptKit/FundamentalObjects/JSObject.swift index 659d9212a..427648bcc 100644 --- a/Sources/JavaScriptKit/FundamentalObjects/JSObject.swift +++ b/Sources/JavaScriptKit/FundamentalObjects/JSObject.swift @@ -95,6 +95,14 @@ public class JSObject: Equatable { set { 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 { getJSValue(this: self, symbol: name) } + set { setJSValue(this: self, symbol: name, value: newValue) } + } + /// A modifier to call methods as throwing methods capturing `this` /// /// diff --git a/Sources/JavaScriptKit/FundamentalObjects/JSSymbol.swift b/Sources/JavaScriptKit/FundamentalObjects/JSSymbol.swift new file mode 100644 index 000000000..3ec1b2902 --- /dev/null +++ b/Sources/JavaScriptKit/FundamentalObjects/JSSymbol.swift @@ -0,0 +1,56 @@ +import _CJavaScriptKit + +private let Symbol = JSObject.global.Symbol.function! + +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 = invokeNonThrowingJSFunction(Symbol, arguments: [description], this: nil) + 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) + } + + 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 + } +} + +extension JSSymbol { + public static let asyncIterator: JSSymbol! = Symbol.asyncIterator.symbol + public static let hasInstance: JSSymbol! = Symbol.hasInstance.symbol + public static let isConcatSpreadable: JSSymbol! = Symbol.isConcatSpreadable.symbol + public static let iterator: JSSymbol! = Symbol.iterator.symbol + public static let match: JSSymbol! = Symbol.match.symbol + public static let matchAll: JSSymbol! = Symbol.matchAll.symbol + public static let replace: JSSymbol! = Symbol.replace.symbol + public static let search: JSSymbol! = Symbol.search.symbol + public static let species: JSSymbol! = Symbol.species.symbol + public static let split: JSSymbol! = Symbol.split.symbol + public static let toPrimitive: JSSymbol! = Symbol.toPrimitive.symbol + public static let toStringTag: JSSymbol! = Symbol.toStringTag.symbol + public static let unscopables: JSSymbol! = Symbol.unscopables.symbol +} diff --git a/Sources/JavaScriptKit/JSValue.swift b/Sources/JavaScriptKit/JSValue.swift index 03c2a81ab..b001dc7ab 100644 --- a/Sources/JavaScriptKit/JSValue.swift +++ b/Sources/JavaScriptKit/JSValue.swift @@ -10,6 +10,7 @@ public enum JSValue: Equatable { case null case undefined case function(JSFunction) + case symbol(JSSymbol) /// Returns the `Bool` value of this JS value if its type is boolean. /// If not, returns `nil`. @@ -67,6 +68,13 @@ public enum JSValue: Equatable { } } + public var symbol: JSSymbol? { + switch self { + case let .symbol(symbol): return symbol + default: return nil + } + } + /// Returns the `true` if this JS value is null. /// If not, returns `false`. public var isNull: Bool { @@ -80,23 +88,23 @@ public enum JSValue: Equatable { } } -extension JSValue { +public extension JSValue { /// 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) { + subscript(dynamicMember name: String) -> ((ConvertibleToJSValue...) -> JSValue) { object![dynamicMember: name]! } /// An unsafe convenience method of `JSObject.subscript(_ index: Int) -> JSValue` /// - Precondition: `self` must be a JavaScript Object. - public subscript(dynamicMember name: String) -> JSValue { + subscript(dynamicMember name: String) -> JSValue { get { self.object![name] } 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 { + subscript(_ index: Int) -> JSValue { get { object![index] } set { object![index] = newValue } } @@ -104,15 +112,14 @@ extension JSValue { extension JSValue: Swift.Error {} -extension JSValue { - public func fromJSValue() -> Type? where Type: ConstructibleFromJSValue { +public extension JSValue { + func fromJSValue() -> Type? where Type: ConstructibleFromJSValue { return Type.construct(from: self) } } -extension JSValue { - - public static func string(_ value: String) -> JSValue { +public extension JSValue { + static func string(_ value: String) -> JSValue { .string(JSString(value)) } @@ -141,12 +148,12 @@ extension JSValue { /// eventListenter.release() /// ``` @available(*, deprecated, message: "Please create JSClosure directly and manage its lifetime manually.") - public static func function(_ body: @escaping ([JSValue]) -> JSValue) -> JSValue { + 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.") - public static func function(_ closure: JSClosure) -> JSValue { + static func function(_ closure: JSClosure) -> JSValue { .object(closure) } } @@ -170,7 +177,7 @@ extension JSValue: ExpressibleByFloatLiteral { } extension JSValue: ExpressibleByNilLiteral { - public init(nilLiteral: ()) { + public init(nilLiteral _: ()) { self = .null } } @@ -205,14 +212,28 @@ public func setJSValue(this: JSObject, index: Int32, value: JSValue) { } } -extension JSValue { - /// Return `true` if this value is an instance of the passed `constructor` function. - /// Returns `false` for everything except objects and functions. - /// - Parameter constructor: The constructor function to check. - /// - Returns: The result of `instanceof` in the JavaScript environment. - public func isInstanceOf(_ constructor: JSFunction) -> Bool { +public func getJSValue(this: JSObject, symbol: JSSymbol) -> JSValue { + var rawValue = RawJSValue() + _get_prop(this.id, symbol.id, + &rawValue.kind, + &rawValue.payload1, &rawValue.payload2) + return rawValue.jsValue +} + +public func setJSValue(this: JSObject, symbol: JSSymbol, value: JSValue) { + value.withRawJSValue { rawValue in + _set_prop(this.id, symbol.id, rawValue.kind, rawValue.payload1, rawValue.payload2) + } +} + +public extension JSValue { + /// Return `true` if this value is an instance of the passed `constructor` function. + /// Returns `false` for everything except objects and functions. + /// - Parameter constructor: The constructor function to check. + /// - Returns: The result of `instanceof` in the JavaScript environment. + func isInstanceOf(_ constructor: JSFunction) -> Bool { switch self { - case .boolean, .string, .number, .null, .undefined: + case .boolean, .string, .number, .null, .undefined, .symbol: return false case let .object(ref): return ref.isInstanceOf(constructor) @@ -227,11 +248,12 @@ extension JSValue: CustomStringConvertible { switch self { case let .boolean(boolean): return boolean.description - case .string(let string): + case let .string(string): return string.description - case .number(let number): + case let .number(number): return number.description - case .object(let object), .function(let object as JSObject): + case let .object(object), let .function(object as JSObject), + .symbol(let object as JSObject): return object.toString!().fromJSValue()! case .null: return "null" diff --git a/Sources/_CJavaScriptKit/include/_CJavaScriptKit.h b/Sources/_CJavaScriptKit/include/_CJavaScriptKit.h index ce0bf5862..daf405141 100644 --- a/Sources/_CJavaScriptKit/include/_CJavaScriptKit.h +++ b/Sources/_CJavaScriptKit/include/_CJavaScriptKit.h @@ -21,6 +21,7 @@ typedef enum __attribute__((enum_extensibility(closed))) { JavaScriptValueKindNull = 4, JavaScriptValueKindUndefined = 5, JavaScriptValueKindFunction = 6, + JavaScriptValueKindSymbol = 7, } JavaScriptValueKind; typedef struct { @@ -60,6 +61,10 @@ typedef double JavaScriptPayload2; /// payload1: the target `JavaScriptHostFuncRef` /// payload2: 0 /// +/// For symbol value: +/// payload1: `JavaScriptObjectRef` +/// payload2: 0 +/// typedef struct { JavaScriptValueKind kind; JavaScriptPayload1 payload1; From 85e1a11a2684362fa7e3dde0be7729cffc19e2a4 Mon Sep 17 00:00:00 2001 From: Jed Fox Date: Fri, 8 Apr 2022 16:46:29 -0400 Subject: [PATCH 014/373] Revert "Revert "Revert "Add support for Symbol objects via JSSymbol (#179)""" This reverts commit 6b07c4f187b899518a2b4f8ed90d022b67400e15. --- .../Sources/PrimaryTests/main.swift | 23 ------- Runtime/src/js-value.ts | 6 -- Runtime/src/object-heap.ts | 24 ++++--- .../JavaScriptKit/ConvertibleToJSValue.swift | 5 -- .../FundamentalObjects/JSFunction.swift | 17 ++--- .../FundamentalObjects/JSObject.swift | 8 --- .../FundamentalObjects/JSSymbol.swift | 56 ---------------- Sources/JavaScriptKit/JSValue.swift | 66 +++++++------------ .../_CJavaScriptKit/include/_CJavaScriptKit.h | 5 -- 9 files changed, 47 insertions(+), 163 deletions(-) delete mode 100644 Sources/JavaScriptKit/FundamentalObjects/JSSymbol.swift diff --git a/IntegrationTests/TestSuites/Sources/PrimaryTests/main.swift b/IntegrationTests/TestSuites/Sources/PrimaryTests/main.swift index bf4c9bc4f..ff42d3358 100644 --- a/IntegrationTests/TestSuites/Sources/PrimaryTests/main.swift +++ b/IntegrationTests/TestSuites/Sources/PrimaryTests/main.swift @@ -804,28 +804,5 @@ try test("Hashable Conformance") { try expectEqual(firstHash, secondHash) } -try test("Symbols") { - let symbol1 = JSSymbol("abc") - let symbol2 = JSSymbol("abc") - try expectNotEqual(symbol1, symbol2) - try expectEqual(symbol1.name, symbol2.name) - try expectEqual(symbol1.name, "abc") - - try expectEqual(JSSymbol.iterator, JSSymbol.iterator) - - // let hasInstanceClass = { - // prop: Object.assign(function () {}, { - // [Symbol.hasInstance]() { return true } - // }) - // }.prop - let hasInstanceObject = JSObject.global.Object.function!.new() - hasInstanceObject.prop = JSClosure { _ in .undefined }.jsValue - let hasInstanceClass = hasInstanceObject.prop.function! - hasInstanceClass[JSSymbol.hasInstance] = JSClosure { _ in - return .boolean(true) - }.jsValue - try expectEqual(hasInstanceClass[JSSymbol.hasInstance].function!().boolean, true) - try expectEqual(JSObject.global.Object.isInstanceOf(hasInstanceClass), true) -} Expectation.wait(expectations) diff --git a/Runtime/src/js-value.ts b/Runtime/src/js-value.ts index 67ac5d46a..61f7b486a 100644 --- a/Runtime/src/js-value.ts +++ b/Runtime/src/js-value.ts @@ -10,7 +10,6 @@ export enum Kind { Null = 4, Undefined = 5, Function = 6, - Symbol = 7, } export const decode = ( @@ -103,11 +102,6 @@ export const write = ( memory.writeUint32(payload1_ptr, memory.retain(value)); break; } - case "symbol": { - memory.writeUint32(kind_ptr, exceptionBit | Kind.Symbol); - memory.writeUint32(payload1_ptr, memory.retain(value)); - break; - } default: throw new Error(`Type "${typeof value}" is not supported yet`); } diff --git a/Runtime/src/object-heap.ts b/Runtime/src/object-heap.ts index 2f9b1fdf3..61f1925fe 100644 --- a/Runtime/src/object-heap.ts +++ b/Runtime/src/object-heap.ts @@ -22,25 +22,33 @@ export class SwiftRuntimeHeap { } retain(value: any) { + const isObject = typeof value == "object"; const entry = this._heapEntryByValue.get(value); - if (entry) { + if (isObject && entry) { entry.rc++; return entry.id; } const id = this._heapNextKey++; this._heapValueById.set(id, value); - this._heapEntryByValue.set(value, { id: id, rc: 1 }); + if (isObject) { + this._heapEntryByValue.set(value, { id: id, rc: 1 }); + } return id; } release(ref: 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); + 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); + } } referenceHeap(ref: ref) { diff --git a/Sources/JavaScriptKit/ConvertibleToJSValue.swift b/Sources/JavaScriptKit/ConvertibleToJSValue.swift index 83680ad02..10d29a1a6 100644 --- a/Sources/JavaScriptKit/ConvertibleToJSValue.swift +++ b/Sources/JavaScriptKit/ConvertibleToJSValue.swift @@ -207,8 +207,6 @@ extension RawJSValue: ConvertibleToJSValue { return .undefined case .function: return .function(JSFunction(id: UInt32(payload1))) - case .symbol: - return .symbol(JSSymbol(id: UInt32(payload1))) } } } @@ -240,9 +238,6 @@ extension JSValue { case let .function(functionRef): kind = .function payload1 = JavaScriptPayload1(functionRef.id) - case let .symbol(symbolRef): - kind = .symbol - payload1 = JavaScriptPayload1(symbolRef.id) } let rawValue = RawJSValue(kind: kind, payload1: payload1, payload2: payload2) return body(rawValue) diff --git a/Sources/JavaScriptKit/FundamentalObjects/JSFunction.swift b/Sources/JavaScriptKit/FundamentalObjects/JSFunction.swift index 9cec5dad0..4e0019f3c 100644 --- a/Sources/JavaScriptKit/FundamentalObjects/JSFunction.swift +++ b/Sources/JavaScriptKit/FundamentalObjects/JSFunction.swift @@ -11,6 +11,7 @@ import _CJavaScriptKit /// ``` /// public class JSFunction: JSObject { + /// 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. @@ -18,7 +19,7 @@ public class JSFunction: JSObject { /// - Returns: The result of this call. @discardableResult public func callAsFunction(this: JSObject? = nil, arguments: [ConvertibleToJSValue]) -> JSValue { - invokeNonThrowingJSFunction(self, arguments: arguments, this: this).jsValue + invokeNonThrowingJSFunction(self, arguments: arguments, this: this) } /// A variadic arguments version of `callAsFunction`. @@ -40,7 +41,7 @@ public class JSFunction: JSObject { public func new(arguments: [ConvertibleToJSValue]) -> JSObject { arguments.withRawJSValues { rawValues in rawValues.withUnsafeBufferPointer { bufferPointer in - JSObject(id: _call_new(self.id, bufferPointer.baseAddress!, Int32(bufferPointer.count))) + return JSObject(id: _call_new(self.id, bufferPointer.baseAddress!, Int32(bufferPointer.count))) } } } @@ -74,7 +75,7 @@ public class JSFunction: JSObject { fatalError("unavailable") } - override public class func construct(from value: JSValue) -> Self? { + public override class func construct(from value: JSValue) -> Self? { return value.function as? Self } @@ -83,9 +84,9 @@ public class JSFunction: JSObject { } } -func invokeNonThrowingJSFunction(_ jsFunc: JSFunction, arguments: [ConvertibleToJSValue], this: JSObject?) -> RawJSValue { +private func invokeNonThrowingJSFunction(_ jsFunc: JSFunction, arguments: [ConvertibleToJSValue], this: JSObject?) -> JSValue { arguments.withRawJSValues { rawValues in - rawValues.withUnsafeBufferPointer { bufferPointer in + rawValues.withUnsafeBufferPointer { bufferPointer -> (JSValue) in let argv = bufferPointer.baseAddress let argc = bufferPointer.count var kindAndFlags = JavaScriptValueKindAndFlags() @@ -93,8 +94,8 @@ func invokeNonThrowingJSFunction(_ jsFunc: JSFunction, arguments: [ConvertibleTo var payload2 = JavaScriptPayload2() if let thisId = this?.id { _call_function_with_this_no_catch(thisId, - jsFunc.id, argv, Int32(argc), - &kindAndFlags, &payload1, &payload2) + jsFunc.id, argv, Int32(argc), + &kindAndFlags, &payload1, &payload2) } else { _call_function_no_catch( jsFunc.id, argv, Int32(argc), @@ -103,7 +104,7 @@ func invokeNonThrowingJSFunction(_ jsFunc: JSFunction, arguments: [ConvertibleTo } assert(!kindAndFlags.isException) let result = RawJSValue(kind: kindAndFlags.kind, payload1: payload1, payload2: payload2) - return result + return result.jsValue } } } diff --git a/Sources/JavaScriptKit/FundamentalObjects/JSObject.swift b/Sources/JavaScriptKit/FundamentalObjects/JSObject.swift index 427648bcc..659d9212a 100644 --- a/Sources/JavaScriptKit/FundamentalObjects/JSObject.swift +++ b/Sources/JavaScriptKit/FundamentalObjects/JSObject.swift @@ -95,14 +95,6 @@ public class JSObject: Equatable { set { 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 { getJSValue(this: self, symbol: name) } - set { setJSValue(this: self, symbol: name, value: newValue) } - } - /// A modifier to call methods as throwing methods capturing `this` /// /// diff --git a/Sources/JavaScriptKit/FundamentalObjects/JSSymbol.swift b/Sources/JavaScriptKit/FundamentalObjects/JSSymbol.swift deleted file mode 100644 index 3ec1b2902..000000000 --- a/Sources/JavaScriptKit/FundamentalObjects/JSSymbol.swift +++ /dev/null @@ -1,56 +0,0 @@ -import _CJavaScriptKit - -private let Symbol = JSObject.global.Symbol.function! - -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 = invokeNonThrowingJSFunction(Symbol, arguments: [description], this: nil) - 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) - } - - 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 - } -} - -extension JSSymbol { - public static let asyncIterator: JSSymbol! = Symbol.asyncIterator.symbol - public static let hasInstance: JSSymbol! = Symbol.hasInstance.symbol - public static let isConcatSpreadable: JSSymbol! = Symbol.isConcatSpreadable.symbol - public static let iterator: JSSymbol! = Symbol.iterator.symbol - public static let match: JSSymbol! = Symbol.match.symbol - public static let matchAll: JSSymbol! = Symbol.matchAll.symbol - public static let replace: JSSymbol! = Symbol.replace.symbol - public static let search: JSSymbol! = Symbol.search.symbol - public static let species: JSSymbol! = Symbol.species.symbol - public static let split: JSSymbol! = Symbol.split.symbol - public static let toPrimitive: JSSymbol! = Symbol.toPrimitive.symbol - public static let toStringTag: JSSymbol! = Symbol.toStringTag.symbol - public static let unscopables: JSSymbol! = Symbol.unscopables.symbol -} diff --git a/Sources/JavaScriptKit/JSValue.swift b/Sources/JavaScriptKit/JSValue.swift index b001dc7ab..03c2a81ab 100644 --- a/Sources/JavaScriptKit/JSValue.swift +++ b/Sources/JavaScriptKit/JSValue.swift @@ -10,7 +10,6 @@ public enum JSValue: Equatable { case null case undefined case function(JSFunction) - case symbol(JSSymbol) /// Returns the `Bool` value of this JS value if its type is boolean. /// If not, returns `nil`. @@ -68,13 +67,6 @@ public enum JSValue: Equatable { } } - public var symbol: JSSymbol? { - switch self { - case let .symbol(symbol): return symbol - default: return nil - } - } - /// Returns the `true` if this JS value is null. /// If not, returns `false`. public var isNull: Bool { @@ -88,23 +80,23 @@ public enum JSValue: Equatable { } } -public extension JSValue { +extension JSValue { /// An unsafe convenience method of `JSObject.subscript(_ name: String) -> ((ConvertibleToJSValue...) -> JSValue)?` /// - Precondition: `self` must be a JavaScript Object and specified member should be a callable object. - subscript(dynamicMember name: String) -> ((ConvertibleToJSValue...) -> JSValue) { + public subscript(dynamicMember name: String) -> ((ConvertibleToJSValue...) -> JSValue) { object![dynamicMember: name]! } /// An unsafe convenience method of `JSObject.subscript(_ index: Int) -> JSValue` /// - Precondition: `self` must be a JavaScript Object. - subscript(dynamicMember name: String) -> JSValue { + public subscript(dynamicMember name: String) -> JSValue { get { self.object![name] } set { self.object![name] = newValue } } /// An unsafe convenience method of `JSObject.subscript(_ index: Int) -> JSValue` /// - Precondition: `self` must be a JavaScript Object. - subscript(_ index: Int) -> JSValue { + public subscript(_ index: Int) -> JSValue { get { object![index] } set { object![index] = newValue } } @@ -112,14 +104,15 @@ public extension JSValue { extension JSValue: Swift.Error {} -public extension JSValue { - func fromJSValue() -> Type? where Type: ConstructibleFromJSValue { +extension JSValue { + public func fromJSValue() -> Type? where Type: ConstructibleFromJSValue { return Type.construct(from: self) } } -public extension JSValue { - static func string(_ value: String) -> JSValue { +extension JSValue { + + public static func string(_ value: String) -> JSValue { .string(JSString(value)) } @@ -148,12 +141,12 @@ public extension JSValue { /// eventListenter.release() /// ``` @available(*, deprecated, message: "Please create JSClosure directly and manage its lifetime manually.") - static func function(_ body: @escaping ([JSValue]) -> JSValue) -> JSValue { + public static func function(_ body: @escaping ([JSValue]) -> JSValue) -> JSValue { .object(JSClosure(body)) } @available(*, deprecated, renamed: "object", message: "JSClosure is no longer a subclass of JSFunction. Use .object(closure) instead.") - static func function(_ closure: JSClosure) -> JSValue { + public static func function(_ closure: JSClosure) -> JSValue { .object(closure) } } @@ -177,7 +170,7 @@ extension JSValue: ExpressibleByFloatLiteral { } extension JSValue: ExpressibleByNilLiteral { - public init(nilLiteral _: ()) { + public init(nilLiteral: ()) { self = .null } } @@ -212,28 +205,14 @@ public func setJSValue(this: JSObject, index: Int32, value: JSValue) { } } -public func getJSValue(this: JSObject, symbol: JSSymbol) -> JSValue { - var rawValue = RawJSValue() - _get_prop(this.id, symbol.id, - &rawValue.kind, - &rawValue.payload1, &rawValue.payload2) - return rawValue.jsValue -} - -public func setJSValue(this: JSObject, symbol: JSSymbol, value: JSValue) { - value.withRawJSValue { rawValue in - _set_prop(this.id, symbol.id, rawValue.kind, rawValue.payload1, rawValue.payload2) - } -} - -public extension JSValue { - /// Return `true` if this value is an instance of the passed `constructor` function. - /// Returns `false` for everything except objects and functions. - /// - Parameter constructor: The constructor function to check. - /// - Returns: The result of `instanceof` in the JavaScript environment. - func isInstanceOf(_ constructor: JSFunction) -> Bool { +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. + public func isInstanceOf(_ constructor: JSFunction) -> Bool { switch self { - case .boolean, .string, .number, .null, .undefined, .symbol: + case .boolean, .string, .number, .null, .undefined: return false case let .object(ref): return ref.isInstanceOf(constructor) @@ -248,12 +227,11 @@ extension JSValue: CustomStringConvertible { switch self { case let .boolean(boolean): return boolean.description - case let .string(string): + case .string(let string): return string.description - case let .number(number): + case .number(let number): return number.description - case let .object(object), let .function(object as JSObject), - .symbol(let object as JSObject): + case .object(let object), .function(let object as JSObject): return object.toString!().fromJSValue()! case .null: return "null" diff --git a/Sources/_CJavaScriptKit/include/_CJavaScriptKit.h b/Sources/_CJavaScriptKit/include/_CJavaScriptKit.h index daf405141..ce0bf5862 100644 --- a/Sources/_CJavaScriptKit/include/_CJavaScriptKit.h +++ b/Sources/_CJavaScriptKit/include/_CJavaScriptKit.h @@ -21,7 +21,6 @@ typedef enum __attribute__((enum_extensibility(closed))) { JavaScriptValueKindNull = 4, JavaScriptValueKindUndefined = 5, JavaScriptValueKindFunction = 6, - JavaScriptValueKindSymbol = 7, } JavaScriptValueKind; typedef struct { @@ -61,10 +60,6 @@ typedef double JavaScriptPayload2; /// payload1: the target `JavaScriptHostFuncRef` /// payload2: 0 /// -/// For symbol value: -/// payload1: `JavaScriptObjectRef` -/// payload2: 0 -/// typedef struct { JavaScriptValueKind kind; JavaScriptPayload1 payload1; From 7b0e0d27c156af4bafc5674c94719485b1599989 Mon Sep 17 00:00:00 2001 From: Jed Fox Date: Sat, 9 Apr 2022 12:37:04 -0400 Subject: [PATCH 015/373] Fix JSValueDecoder (#185) --- .../Sources/PrimaryTests/main.swift | 22 +++++++++++++++++++ Sources/JavaScriptKit/JSValueDecoder.swift | 2 +- 2 files changed, 23 insertions(+), 1 deletion(-) diff --git a/IntegrationTests/TestSuites/Sources/PrimaryTests/main.swift b/IntegrationTests/TestSuites/Sources/PrimaryTests/main.swift index ff42d3358..c10dc746d 100644 --- a/IntegrationTests/TestSuites/Sources/PrimaryTests/main.swift +++ b/IntegrationTests/TestSuites/Sources/PrimaryTests/main.swift @@ -804,5 +804,27 @@ try test("Hashable Conformance") { try expectEqual(firstHash, secondHash) } +struct AnimalStruct: Decodable { + let name: String + let age: Int + let isCat: Bool +} + +try test("JSValueDecoder") { + 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) + + try expectEqual(decodedTama.name, tama.name.string) + try expectEqual(decodedTama.name, "Tama") + + try expectEqual(decodedTama.age, tama.age.number.map(Int.init)) + try expectEqual(decodedTama.age, 3) + + try expectEqual(decodedTama.isCat, tama.isCat.boolean) + try expectEqual(decodedTama.isCat, true) +} + Expectation.wait(expectations) diff --git a/Sources/JavaScriptKit/JSValueDecoder.swift b/Sources/JavaScriptKit/JSValueDecoder.swift index c70dd8e27..b1d59af63 100644 --- a/Sources/JavaScriptKit/JSValueDecoder.swift +++ b/Sources/JavaScriptKit/JSValueDecoder.swift @@ -34,7 +34,7 @@ private struct _Decoder: Decoder { } private enum Object { - static let ref = JSObject.global.Object.object! + static let ref = JSObject.global.Object.function! static func keys(_ object: JSObject) -> [String] { let keys = ref.keys!(object).array! return keys.map { $0.string! } From 8c20a891db7b065ae0ac3a9ec165bf030cbdfdb9 Mon Sep 17 00:00:00 2001 From: Jed Fox Date: Sun, 10 Apr 2022 00:24:21 -0400 Subject: [PATCH 016/373] Re-add support for Symbol objects via JSSymbol (#183) * Revert "Revert "Revert "Revert "Add support for Symbol objects via JSSymbol (#179)"""" This reverts commit 85e1a11a2684362fa7e3dde0be7729cffc19e2a4. * Ignore .vscode * Drop Reflect.get/set in favor of subscript syntax * Also drop Reflect.{apply,construct} * Fix bad test --- .gitignore | 1 + .../Sources/PrimaryTests/main.swift | 26 ++++++++ Runtime/src/index.ts | 60 ++++++----------- Runtime/src/js-value.ts | 6 ++ Runtime/src/object-heap.ts | 24 +++---- .../JavaScriptKit/ConvertibleToJSValue.swift | 5 ++ .../FundamentalObjects/JSFunction.swift | 17 +++-- .../FundamentalObjects/JSObject.swift | 8 +++ .../FundamentalObjects/JSSymbol.swift | 56 ++++++++++++++++ Sources/JavaScriptKit/JSValue.swift | 66 ++++++++++++------- .../_CJavaScriptKit/include/_CJavaScriptKit.h | 5 ++ 11 files changed, 187 insertions(+), 87 deletions(-) create mode 100644 Sources/JavaScriptKit/FundamentalObjects/JSSymbol.swift diff --git a/.gitignore b/.gitignore index a24baa7da..5102946ea 100644 --- a/.gitignore +++ b/.gitignore @@ -6,3 +6,4 @@ node_modules /*.xcodeproj xcuserdata/ .swiftpm +.vscode diff --git a/IntegrationTests/TestSuites/Sources/PrimaryTests/main.swift b/IntegrationTests/TestSuites/Sources/PrimaryTests/main.swift index c10dc746d..6a9ff54c1 100644 --- a/IntegrationTests/TestSuites/Sources/PrimaryTests/main.swift +++ b/IntegrationTests/TestSuites/Sources/PrimaryTests/main.swift @@ -804,6 +804,32 @@ try test("Hashable Conformance") { try expectEqual(firstHash, secondHash) } +try test("Symbols") { + let symbol1 = JSSymbol("abc") + let symbol2 = JSSymbol("abc") + try expectNotEqual(symbol1, symbol2) + try expectEqual(symbol1.name, symbol2.name) + try expectEqual(symbol1.name, "abc") + + try expectEqual(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 + ) + try expectEqual(hasInstanceClass[JSSymbol.hasInstance].function!().boolean, true) + try expectEqual(JSObject.global.Object.isInstanceOf(hasInstanceClass), true) +} + struct AnimalStruct: Decodable { let name: String let age: Int diff --git a/Runtime/src/index.ts b/Runtime/src/index.ts index f2898f09e..15fc570ea 100644 --- a/Runtime/src/index.ts +++ b/Runtime/src/index.ts @@ -110,11 +110,9 @@ export class SwiftRuntime { payload2: number ) => { const obj = this.memory.getObject(ref); - Reflect.set( - obj, - this.memory.getObject(name), - JSValue.decode(kind, payload1, payload2, this.memory) - ); + const key = this.memory.getObject(name); + const value = JSValue.decode(kind, payload1, payload2, this.memory); + obj[key] = value; }, swjs_get_prop: ( @@ -125,7 +123,8 @@ export class SwiftRuntime { payload2_ptr: pointer ) => { const obj = this.memory.getObject(ref); - const result = Reflect.get(obj, this.memory.getObject(name)); + const key = this.memory.getObject(name); + const result = obj[key]; JSValue.write( result, kind_ptr, @@ -144,11 +143,8 @@ export class SwiftRuntime { payload2: number ) => { const obj = this.memory.getObject(ref); - Reflect.set( - obj, - index, - JSValue.decode(kind, payload1, payload2, this.memory) - ); + const value = JSValue.decode(kind, payload1, payload2, this.memory); + obj[index] = value; }, swjs_get_subscript: ( @@ -159,7 +155,7 @@ export class SwiftRuntime { payload2_ptr: pointer ) => { const obj = this.memory.getObject(ref); - const result = Reflect.get(obj, index); + const result = obj[index]; JSValue.write( result, kind_ptr, @@ -201,11 +197,8 @@ export class SwiftRuntime { const func = this.memory.getObject(ref); let result: any; try { - result = Reflect.apply( - func, - undefined, - JSValue.decodeArray(argv, argc, this.memory) - ); + const args = JSValue.decodeArray(argv, argc, this.memory); + result = func(...args); } catch (error) { JSValue.write( error, @@ -237,11 +230,8 @@ export class SwiftRuntime { const func = this.memory.getObject(ref); let isException = true; try { - const result = Reflect.apply( - func, - undefined, - JSValue.decodeArray(argv, argc, this.memory) - ); + const args = JSValue.decodeArray(argv, argc, this.memory); + const result = func(...args); JSValue.write( result, kind_ptr, @@ -278,11 +268,8 @@ export class SwiftRuntime { const func = this.memory.getObject(func_ref); let result: any; try { - result = Reflect.apply( - func, - obj, - JSValue.decodeArray(argv, argc, this.memory) - ); + const args = JSValue.decodeArray(argv, argc, this.memory); + result = func.apply(obj, args); } catch (error) { JSValue.write( error, @@ -317,11 +304,8 @@ export class SwiftRuntime { const func = this.memory.getObject(func_ref); let isException = true; try { - const result = Reflect.apply( - func, - obj, - JSValue.decodeArray(argv, argc, this.memory) - ); + const args = JSValue.decodeArray(argv, argc, this.memory); + const result = func.apply(obj, args); JSValue.write( result, kind_ptr, @@ -346,10 +330,8 @@ export class SwiftRuntime { }, 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) - ); + const args = JSValue.decodeArray(argv, argc, this.memory); + const instance = new constructor(...args); return this.memory.retain(instance); }, @@ -364,10 +346,8 @@ export class SwiftRuntime { const constructor = this.memory.getObject(ref); let result: any; try { - result = Reflect.construct( - constructor, - JSValue.decodeArray(argv, argc, this.memory) - ); + const args = JSValue.decodeArray(argv, argc, this.memory); + result = new constructor(...args); } catch (error) { JSValue.write( error, diff --git a/Runtime/src/js-value.ts b/Runtime/src/js-value.ts index 61f7b486a..67ac5d46a 100644 --- a/Runtime/src/js-value.ts +++ b/Runtime/src/js-value.ts @@ -10,6 +10,7 @@ export enum Kind { Null = 4, Undefined = 5, Function = 6, + Symbol = 7, } export const decode = ( @@ -102,6 +103,11 @@ export const write = ( memory.writeUint32(payload1_ptr, memory.retain(value)); break; } + case "symbol": { + memory.writeUint32(kind_ptr, exceptionBit | Kind.Symbol); + memory.writeUint32(payload1_ptr, memory.retain(value)); + break; + } default: throw new Error(`Type "${typeof value}" is not supported yet`); } diff --git a/Runtime/src/object-heap.ts b/Runtime/src/object-heap.ts index 61f1925fe..2f9b1fdf3 100644 --- a/Runtime/src/object-heap.ts +++ b/Runtime/src/object-heap.ts @@ -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/Sources/JavaScriptKit/ConvertibleToJSValue.swift b/Sources/JavaScriptKit/ConvertibleToJSValue.swift index 10d29a1a6..83680ad02 100644 --- a/Sources/JavaScriptKit/ConvertibleToJSValue.swift +++ b/Sources/JavaScriptKit/ConvertibleToJSValue.swift @@ -207,6 +207,8 @@ extension RawJSValue: ConvertibleToJSValue { return .undefined case .function: return .function(JSFunction(id: UInt32(payload1))) + case .symbol: + return .symbol(JSSymbol(id: UInt32(payload1))) } } } @@ -238,6 +240,9 @@ extension JSValue { case let .function(functionRef): kind = .function payload1 = JavaScriptPayload1(functionRef.id) + case let .symbol(symbolRef): + kind = .symbol + payload1 = JavaScriptPayload1(symbolRef.id) } let rawValue = RawJSValue(kind: kind, payload1: payload1, payload2: payload2) return body(rawValue) diff --git a/Sources/JavaScriptKit/FundamentalObjects/JSFunction.swift b/Sources/JavaScriptKit/FundamentalObjects/JSFunction.swift index 4e0019f3c..9cec5dad0 100644 --- a/Sources/JavaScriptKit/FundamentalObjects/JSFunction.swift +++ b/Sources/JavaScriptKit/FundamentalObjects/JSFunction.swift @@ -11,7 +11,6 @@ import _CJavaScriptKit /// ``` /// public class JSFunction: JSObject { - /// 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. @@ -19,7 +18,7 @@ public class JSFunction: JSObject { /// - Returns: The result of this call. @discardableResult public func callAsFunction(this: JSObject? = nil, arguments: [ConvertibleToJSValue]) -> JSValue { - invokeNonThrowingJSFunction(self, arguments: arguments, this: this) + invokeNonThrowingJSFunction(self, arguments: arguments, this: this).jsValue } /// A variadic arguments version of `callAsFunction`. @@ -41,7 +40,7 @@ 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: _call_new(self.id, bufferPointer.baseAddress!, Int32(bufferPointer.count))) } } } @@ -75,7 +74,7 @@ public class JSFunction: JSObject { fatalError("unavailable") } - public override class func construct(from value: JSValue) -> Self? { + override public class func construct(from value: JSValue) -> Self? { return value.function as? Self } @@ -84,9 +83,9 @@ public class JSFunction: JSObject { } } -private func invokeNonThrowingJSFunction(_ jsFunc: JSFunction, arguments: [ConvertibleToJSValue], this: JSObject?) -> JSValue { +func invokeNonThrowingJSFunction(_ jsFunc: JSFunction, arguments: [ConvertibleToJSValue], this: JSObject?) -> RawJSValue { arguments.withRawJSValues { rawValues in - rawValues.withUnsafeBufferPointer { bufferPointer -> (JSValue) in + rawValues.withUnsafeBufferPointer { bufferPointer in let argv = bufferPointer.baseAddress let argc = bufferPointer.count var kindAndFlags = JavaScriptValueKindAndFlags() @@ -94,8 +93,8 @@ private func invokeNonThrowingJSFunction(_ jsFunc: JSFunction, arguments: [Conve var payload2 = JavaScriptPayload2() if let thisId = this?.id { _call_function_with_this_no_catch(thisId, - jsFunc.id, argv, Int32(argc), - &kindAndFlags, &payload1, &payload2) + jsFunc.id, argv, Int32(argc), + &kindAndFlags, &payload1, &payload2) } else { _call_function_no_catch( jsFunc.id, argv, Int32(argc), @@ -104,7 +103,7 @@ private func invokeNonThrowingJSFunction(_ jsFunc: JSFunction, arguments: [Conve } assert(!kindAndFlags.isException) let result = RawJSValue(kind: kindAndFlags.kind, payload1: payload1, payload2: payload2) - return result.jsValue + return result } } } diff --git a/Sources/JavaScriptKit/FundamentalObjects/JSObject.swift b/Sources/JavaScriptKit/FundamentalObjects/JSObject.swift index 659d9212a..427648bcc 100644 --- a/Sources/JavaScriptKit/FundamentalObjects/JSObject.swift +++ b/Sources/JavaScriptKit/FundamentalObjects/JSObject.swift @@ -95,6 +95,14 @@ public class JSObject: Equatable { set { 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 { getJSValue(this: self, symbol: name) } + set { setJSValue(this: self, symbol: name, value: newValue) } + } + /// A modifier to call methods as throwing methods capturing `this` /// /// diff --git a/Sources/JavaScriptKit/FundamentalObjects/JSSymbol.swift b/Sources/JavaScriptKit/FundamentalObjects/JSSymbol.swift new file mode 100644 index 000000000..3ec1b2902 --- /dev/null +++ b/Sources/JavaScriptKit/FundamentalObjects/JSSymbol.swift @@ -0,0 +1,56 @@ +import _CJavaScriptKit + +private let Symbol = JSObject.global.Symbol.function! + +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 = invokeNonThrowingJSFunction(Symbol, arguments: [description], this: nil) + 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) + } + + 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 + } +} + +extension JSSymbol { + public static let asyncIterator: JSSymbol! = Symbol.asyncIterator.symbol + public static let hasInstance: JSSymbol! = Symbol.hasInstance.symbol + public static let isConcatSpreadable: JSSymbol! = Symbol.isConcatSpreadable.symbol + public static let iterator: JSSymbol! = Symbol.iterator.symbol + public static let match: JSSymbol! = Symbol.match.symbol + public static let matchAll: JSSymbol! = Symbol.matchAll.symbol + public static let replace: JSSymbol! = Symbol.replace.symbol + public static let search: JSSymbol! = Symbol.search.symbol + public static let species: JSSymbol! = Symbol.species.symbol + public static let split: JSSymbol! = Symbol.split.symbol + public static let toPrimitive: JSSymbol! = Symbol.toPrimitive.symbol + public static let toStringTag: JSSymbol! = Symbol.toStringTag.symbol + public static let unscopables: JSSymbol! = Symbol.unscopables.symbol +} diff --git a/Sources/JavaScriptKit/JSValue.swift b/Sources/JavaScriptKit/JSValue.swift index 03c2a81ab..b001dc7ab 100644 --- a/Sources/JavaScriptKit/JSValue.swift +++ b/Sources/JavaScriptKit/JSValue.swift @@ -10,6 +10,7 @@ public enum JSValue: Equatable { case null case undefined case function(JSFunction) + case symbol(JSSymbol) /// Returns the `Bool` value of this JS value if its type is boolean. /// If not, returns `nil`. @@ -67,6 +68,13 @@ public enum JSValue: Equatable { } } + public var symbol: JSSymbol? { + switch self { + case let .symbol(symbol): return symbol + default: return nil + } + } + /// Returns the `true` if this JS value is null. /// If not, returns `false`. public var isNull: Bool { @@ -80,23 +88,23 @@ public enum JSValue: Equatable { } } -extension JSValue { +public extension JSValue { /// 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) { + subscript(dynamicMember name: String) -> ((ConvertibleToJSValue...) -> JSValue) { object![dynamicMember: name]! } /// An unsafe convenience method of `JSObject.subscript(_ index: Int) -> JSValue` /// - Precondition: `self` must be a JavaScript Object. - public subscript(dynamicMember name: String) -> JSValue { + subscript(dynamicMember name: String) -> JSValue { get { self.object![name] } 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 { + subscript(_ index: Int) -> JSValue { get { object![index] } set { object![index] = newValue } } @@ -104,15 +112,14 @@ extension JSValue { extension JSValue: Swift.Error {} -extension JSValue { - public func fromJSValue() -> Type? where Type: ConstructibleFromJSValue { +public extension JSValue { + func fromJSValue() -> Type? where Type: ConstructibleFromJSValue { return Type.construct(from: self) } } -extension JSValue { - - public static func string(_ value: String) -> JSValue { +public extension JSValue { + static func string(_ value: String) -> JSValue { .string(JSString(value)) } @@ -141,12 +148,12 @@ extension JSValue { /// eventListenter.release() /// ``` @available(*, deprecated, message: "Please create JSClosure directly and manage its lifetime manually.") - public static func function(_ body: @escaping ([JSValue]) -> JSValue) -> JSValue { + 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.") - public static func function(_ closure: JSClosure) -> JSValue { + static func function(_ closure: JSClosure) -> JSValue { .object(closure) } } @@ -170,7 +177,7 @@ extension JSValue: ExpressibleByFloatLiteral { } extension JSValue: ExpressibleByNilLiteral { - public init(nilLiteral: ()) { + public init(nilLiteral _: ()) { self = .null } } @@ -205,14 +212,28 @@ public func setJSValue(this: JSObject, index: Int32, value: JSValue) { } } -extension JSValue { - /// Return `true` if this value is an instance of the passed `constructor` function. - /// Returns `false` for everything except objects and functions. - /// - Parameter constructor: The constructor function to check. - /// - Returns: The result of `instanceof` in the JavaScript environment. - public func isInstanceOf(_ constructor: JSFunction) -> Bool { +public func getJSValue(this: JSObject, symbol: JSSymbol) -> JSValue { + var rawValue = RawJSValue() + _get_prop(this.id, symbol.id, + &rawValue.kind, + &rawValue.payload1, &rawValue.payload2) + return rawValue.jsValue +} + +public func setJSValue(this: JSObject, symbol: JSSymbol, value: JSValue) { + value.withRawJSValue { rawValue in + _set_prop(this.id, symbol.id, rawValue.kind, rawValue.payload1, rawValue.payload2) + } +} + +public extension JSValue { + /// Return `true` if this value is an instance of the passed `constructor` function. + /// Returns `false` for everything except objects and functions. + /// - Parameter constructor: The constructor function to check. + /// - Returns: The result of `instanceof` in the JavaScript environment. + func isInstanceOf(_ constructor: JSFunction) -> Bool { switch self { - case .boolean, .string, .number, .null, .undefined: + case .boolean, .string, .number, .null, .undefined, .symbol: return false case let .object(ref): return ref.isInstanceOf(constructor) @@ -227,11 +248,12 @@ extension JSValue: CustomStringConvertible { switch self { case let .boolean(boolean): return boolean.description - case .string(let string): + case let .string(string): return string.description - case .number(let number): + case let .number(number): return number.description - case .object(let object), .function(let object as JSObject): + case let .object(object), let .function(object as JSObject), + .symbol(let object as JSObject): return object.toString!().fromJSValue()! case .null: return "null" diff --git a/Sources/_CJavaScriptKit/include/_CJavaScriptKit.h b/Sources/_CJavaScriptKit/include/_CJavaScriptKit.h index ce0bf5862..daf405141 100644 --- a/Sources/_CJavaScriptKit/include/_CJavaScriptKit.h +++ b/Sources/_CJavaScriptKit/include/_CJavaScriptKit.h @@ -21,6 +21,7 @@ typedef enum __attribute__((enum_extensibility(closed))) { JavaScriptValueKindNull = 4, JavaScriptValueKindUndefined = 5, JavaScriptValueKindFunction = 6, + JavaScriptValueKindSymbol = 7, } JavaScriptValueKind; typedef struct { @@ -60,6 +61,10 @@ typedef double JavaScriptPayload2; /// payload1: the target `JavaScriptHostFuncRef` /// payload2: 0 /// +/// For symbol value: +/// payload1: `JavaScriptObjectRef` +/// payload2: 0 +/// typedef struct { JavaScriptValueKind kind; JavaScriptPayload1 payload1; From a5a980d08648caef66ff5f5a4033f1079ac8878f Mon Sep 17 00:00:00 2001 From: Jed Fox Date: Sun, 10 Apr 2022 10:38:26 -0400 Subject: [PATCH 017/373] Cleanup & improvements to perf-tester (#186) --- ci/perf-tester/src/index.js | 154 +++++++++++++----------------------- ci/perf-tester/src/utils.js | 40 ++++------ 2 files changed, 71 insertions(+), 123 deletions(-) diff --git a/ci/perf-tester/src/index.js b/ci/perf-tester/src/index.js index f9495303d..e322e5f69 100644 --- a/ci/perf-tester/src/index.js +++ b/ci/perf-tester/src/index.js @@ -24,16 +24,15 @@ const { setFailed, startGroup, endGroup, debug } = require("@actions/core"); const { GitHub, context } = require("@actions/github"); const { exec } = require("@actions/exec"); const { - getInput, + config, runBenchmark, averageBenchmarks, toDiff, diffTable, - toBool, } = require("./utils.js"); -const benchmarkParallel = 2; -const benchmarkSerial = 2; +const benchmarkParallel = 4; +const benchmarkSerial = 4; const runBenchmarks = async () => { let results = []; for (let i = 0; i < benchmarkSerial; i++) { @@ -44,7 +43,10 @@ const runBenchmarks = async () => { return averageBenchmarks(results); }; -async function run(octokit, context, token) { +const perfActionComment = + ""; + +async function run(octokit, context) { const { number: pull_number } = context.issue; const pr = context.payload.pull_request; @@ -61,9 +63,8 @@ async function run(octokit, context, token) { `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); + startGroup(`[current] Build using '${config.buildScript}'`); + await exec(config.buildScript); endGroup(); startGroup(`[current] Running benchmark`); @@ -104,8 +105,8 @@ async function run(octokit, context, token) { } endGroup(); - startGroup(`[base] Build using '${buildScript}'`); - await exec(buildScript); + startGroup(`[base] Build using '${config.buildScript}'`); + await exec(config.buildScript); endGroup(); startGroup(`[base] Running benchmark`); @@ -118,10 +119,7 @@ async function run(octokit, context, token) { collapseUnchanged: true, omitUnchanged: false, showTotal: true, - minimumChangeThreshold: parseInt( - getInput("minimum-change-threshold"), - 10 - ), + minimumChangeThreshold: config.minimumChangeThreshold, }); let outputRawMarkdown = false; @@ -133,79 +131,58 @@ async function run(octokit, context, token) { const comment = { ...commentInfo, - body: - markdownDiff + - '\n\nperformance-action', + body: markdownDiff + "\n\n" + perfActionComment, }; - 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; + startGroup(`Updating stats PR comment`); + let commentId; + try { + const comments = (await octokit.issues.listComments(commentInfo)).data; + for (let i = comments.length; i--; ) { + const c = comments[i]; + if (c.user.type === "Bot" && c.body.includes(perfActionComment)) { + commentId = c.id; + break; + } } - } else { - startGroup(`Updating stats PR comment`); - let commentId; + } catch (e) { + console.log("Error checking for previous comments: " + e.message); + } + + if (commentId) { + console.log(`Updating previous comment #${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; - } - } + await octokit.issues.updateComment({ + ...context.repo, + comment_id: commentId, + body: comment.body, + }); } catch (e) { - console.log("Error checking for previous comments: " + e.message); + console.log("Error editing previous comment: " + e.message); + commentId = null; } + } - if (commentId) { - console.log(`Updating previous comment #${commentId}`); + // 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 { - await octokit.issues.updateComment({ - ...context.repo, - comment_id: commentId, + 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 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; - } + console.log("Error creating PR review."); + outputRawMarkdown = true; } } endGroup(); @@ -225,31 +202,10 @@ async function run(octokit, context, token) { 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); + const octokit = new GitHub(process.env.GITHUB_TOKEN); + await run(octokit, context); } catch (e) { setFailed(e.message); } diff --git a/ci/perf-tester/src/utils.js b/ci/perf-tester/src/utils.js index 49fce603e..a4b717ab7 100644 --- a/ci/perf-tester/src/utils.js +++ b/ci/perf-tester/src/utils.js @@ -20,22 +20,23 @@ 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; +const formatMS = (ms) => + `${ms.toLocaleString("en-US", { + maximumFractionDigits: 0, + })}ms`; + +const config = { + buildScript: "make bootstrap benchmark_setup", + benchmark: "make -s run_benchmark", + minimumChangeThreshold: 5, +}; +exports.config = config; exports.runBenchmark = async () => { let benchmarkBuffers = []; - await exec(getInput("benchmark"), [], { + await exec(config.benchmark, [], { listeners: { stdout: (data) => benchmarkBuffers.push(data), }, @@ -88,8 +89,7 @@ exports.toDiff = (before, after) => { * @param {number} difference */ function getDeltaText(delta, difference) { - let deltaText = - (delta > 0 ? "+" : "") + delta.toLocaleString("en-US") + "ms"; + let deltaText = (delta > 0 ? "+" : "") + formatMS(delta); if (delta && Math.abs(delta) > 1) { deltaText += ` (${Math.abs(difference)}%)`; } @@ -183,7 +183,7 @@ exports.diffTable = ( const columns = [ name, - time.toLocaleString("en-US") + "ms", + formatMS(time), getDeltaText(delta, difference), iconForDifference(difference), ]; @@ -198,24 +198,16 @@ exports.diffTable = ( if (unChangedRows.length !== 0) { const outUnchanged = markdownTable(unChangedRows); - out += `\n\n
ℹ️ View Unchanged\n\n${outUnchanged}\n\n
\n\n`; + 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 = `**Total Time:** ${formatMS(totalTime)}\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); From 34bf9e1d38efa139df51ef60aaf490115766ed66 Mon Sep 17 00:00:00 2001 From: Jed Fox Date: Fri, 22 Apr 2022 10:59:52 -0400 Subject: [PATCH 018/373] Add support for BigInts and BigInt-based TypedArrays (#184) --- .github/workflows/compatibility.yml | 1 + IntegrationTests/TestSuites/Package.swift | 3 +- .../TestSuites/Sources/PrimaryTests/I64.swift | 35 +++++++ .../Sources/PrimaryTests/main.swift | 2 +- IntegrationTests/bin/concurrency-tests.js | 2 + IntegrationTests/bin/primary-tests.js | 35 ++++--- Package.swift | 6 ++ README.md | 2 +- Runtime/src/index.ts | 29 ++++-- Runtime/src/js-value.ts | 37 ++++--- Runtime/src/memory.ts | 9 +- Runtime/src/types.ts | 14 ++- Runtime/tsconfig.json | 2 +- .../JavaScriptBigIntSupport/Int64+I64.swift | 13 +++ .../JSBigInt+I64.swift | 20 ++++ .../XcodeSupport.swift | 11 +++ .../JavaScriptEventLoop.swift | 6 +- .../BasicObjects/JSTypedArray.swift | 9 -- .../ConstructibleFromJSValue.swift | 96 +++++++------------ .../JavaScriptKit/ConvertibleToJSValue.swift | 24 ++--- .../FundamentalObjects/JSBigInt.swift | 34 +++++++ .../FundamentalObjects/JSObject.swift | 8 +- .../FundamentalObjects/JSSymbol.swift | 8 ++ Sources/JavaScriptKit/JSValue.swift | 32 +++---- Sources/JavaScriptKit/XcodeSupport.swift | 3 +- .../_CJavaScriptKit+I64.c | 1 + .../include/_CJavaScriptKit+I64.h | 23 +++++ .../include/module.modulemap | 4 + Sources/_CJavaScriptKit/_CJavaScriptKit.c | 2 +- .../_CJavaScriptKit/include/_CJavaScriptKit.h | 5 +- 30 files changed, 324 insertions(+), 152 deletions(-) create mode 100644 IntegrationTests/TestSuites/Sources/PrimaryTests/I64.swift create mode 100644 Sources/JavaScriptBigIntSupport/Int64+I64.swift create mode 100644 Sources/JavaScriptBigIntSupport/JSBigInt+I64.swift create mode 100644 Sources/JavaScriptBigIntSupport/XcodeSupport.swift create mode 100644 Sources/JavaScriptKit/FundamentalObjects/JSBigInt.swift create mode 100644 Sources/_CJavaScriptBigIntSupport/_CJavaScriptKit+I64.c create mode 100644 Sources/_CJavaScriptBigIntSupport/include/_CJavaScriptKit+I64.h create mode 100644 Sources/_CJavaScriptBigIntSupport/include/module.modulemap diff --git a/.github/workflows/compatibility.yml b/.github/workflows/compatibility.yml index e4534507e..422e08c48 100644 --- a/.github/workflows/compatibility.yml +++ b/.github/workflows/compatibility.yml @@ -22,3 +22,4 @@ jobs: make bootstrap cd Example/JavaScriptKitExample swift build --triple wasm32-unknown-wasi + swift build --triple wasm32-unknown-wasi -Xswiftc -DJAVASCRIPTKIT_WITHOUT_WEAKREFS diff --git a/IntegrationTests/TestSuites/Package.swift b/IntegrationTests/TestSuites/Package.swift index 0344d0499..297fa838a 100644 --- a/IntegrationTests/TestSuites/Package.swift +++ b/IntegrationTests/TestSuites/Package.swift @@ -8,7 +8,7 @@ let package = Package( // 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") + .macOS("12.0"), ], products: [ .executable( @@ -25,6 +25,7 @@ let package = Package( targets: [ .target(name: "CHelpers"), .target(name: "PrimaryTests", dependencies: [ + .product(name: "JavaScriptBigIntSupport", package: "JavaScriptKit"), "JavaScriptKit", "CHelpers", ]), diff --git a/IntegrationTests/TestSuites/Sources/PrimaryTests/I64.swift b/IntegrationTests/TestSuites/Sources/PrimaryTests/I64.swift new file mode 100644 index 000000000..bd0831e7d --- /dev/null +++ b/IntegrationTests/TestSuites/Sources/PrimaryTests/I64.swift @@ -0,0 +1,35 @@ +import JavaScriptBigIntSupport +import JavaScriptKit + +func testI64() throws { + try test("BigInt") { + func expectPassesThrough(signed value: Int64) throws { + let bigInt = JSBigInt(value) + try expectEqual(bigInt.description, value.description) + } + + func expectPassesThrough(unsigned value: UInt64) throws { + let bigInt = JSBigInt(unsigned: value) + try expectEqual(bigInt.description, value.description) + } + + try expectPassesThrough(signed: 0) + try expectPassesThrough(signed: 1 << 62) + try expectPassesThrough(signed: -2305) + for _ in 0 ..< 100 { + try expectPassesThrough(signed: .random(in: .min ... .max)) + } + try expectPassesThrough(signed: .min) + try expectPassesThrough(signed: .max) + + try expectPassesThrough(unsigned: 0) + try expectPassesThrough(unsigned: 1 << 62) + try expectPassesThrough(unsigned: 1 << 63) + try expectPassesThrough(unsigned: .min) + try expectPassesThrough(unsigned: .max) + try expectPassesThrough(unsigned: ~0) + for _ in 0 ..< 100 { + try expectPassesThrough(unsigned: .random(in: .min ... .max)) + } + } +} diff --git a/IntegrationTests/TestSuites/Sources/PrimaryTests/main.swift b/IntegrationTests/TestSuites/Sources/PrimaryTests/main.swift index 6a9ff54c1..a48c6fb00 100644 --- a/IntegrationTests/TestSuites/Sources/PrimaryTests/main.swift +++ b/IntegrationTests/TestSuites/Sources/PrimaryTests/main.swift @@ -852,5 +852,5 @@ try test("JSValueDecoder") { try expectEqual(decodedTama.isCat, true) } - +try testI64() Expectation.wait(expectations) diff --git a/IntegrationTests/bin/concurrency-tests.js b/IntegrationTests/bin/concurrency-tests.js index 2d705761f..47ef4abda 100644 --- a/IntegrationTests/bin/concurrency-tests.js +++ b/IntegrationTests/bin/concurrency-tests.js @@ -1,5 +1,7 @@ const { startWasiTask } = require("../lib"); +Error.stackTraceLimit = Infinity; + 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 index 597590bd4..94bcf7da5 100644 --- a/IntegrationTests/bin/primary-tests.js +++ b/IntegrationTests/bin/primary-tests.js @@ -1,3 +1,5 @@ +Error.stackTraceLimit = Infinity; + global.globalObject1 = { prop_1: { nested_prop: 1, @@ -44,23 +46,26 @@ global.globalObject1 = { throw "String Error"; }, func3: function () { - throw 3.0 + throw 3.0; }, }, eval_closure: function (fn) { - return fn(arguments[1]) + 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; + target: new Proxy( + { + nested: {}, + }, + { + set(target, key, value) { + global.globalObject1.observable_obj.set_called = true; + target[key] = value; + return true; + }, } - }) + ), }, }; @@ -79,16 +84,16 @@ global.Animal = function (name, age, isCat) { }; this.setName = function (name) { this.name = name; - } + }; }; -global.callThrowingClosure = (c) => { +global.callThrowingClosure = (c) => { try { - c() + c(); } catch (error) { - return error + return error; } -} +}; const { startWasiTask } = require("../lib"); diff --git a/Package.swift b/Package.swift index 8a75bed39..3d07321ac 100644 --- a/Package.swift +++ b/Package.swift @@ -7,6 +7,7 @@ let package = Package( products: [ .library(name: "JavaScriptKit", targets: ["JavaScriptKit"]), .library(name: "JavaScriptEventLoop", targets: ["JavaScriptEventLoop"]), + .library(name: "JavaScriptBigIntSupport", targets: ["JavaScriptBigIntSupport"]), ], targets: [ .target( @@ -14,6 +15,11 @@ let package = Package( dependencies: ["_CJavaScriptKit"] ), .target(name: "_CJavaScriptKit"), + .target( + name: "JavaScriptBigIntSupport", + dependencies: ["_CJavaScriptBigIntSupport", "JavaScriptKit"] + ), + .target(name: "_CJavaScriptBigIntSupport", dependencies: ["_CJavaScriptKit"]), .target( name: "JavaScriptEventLoop", dependencies: ["JavaScriptKit", "_CJavaScriptEventLoop"] diff --git a/README.md b/README.md index 46dc963a9..b5c64539a 100644 --- a/README.md +++ b/README.md @@ -149,7 +149,7 @@ JavaScript features should work, which currently includes: - Mobile Safari 14.8+ If you need to support older browser versions, you'll have to build with -`JAVASCRIPTKIT_WITHOUT_WEAKREFS` flag, passing `-Xswiftc -DJAVASCRIPTKIT_WITHOUT_WEAKREFS` flags +the `JAVASCRIPTKIT_WITHOUT_WEAKREFS` flag, passing `-Xswiftc -DJAVASCRIPTKIT_WITHOUT_WEAKREFS` flags when compiling. This should lower browser requirements to these versions: - Edge 16+ diff --git a/Runtime/src/index.ts b/Runtime/src/index.ts index 15fc570ea..dcd817a72 100644 --- a/Runtime/src/index.ts +++ b/Runtime/src/index.ts @@ -14,7 +14,7 @@ export class SwiftRuntime { private _instance: WebAssembly.Instance | null; private _memory: Memory | null; private _closureDeallocator: SwiftClosureDeallocator | null; - private version: number = 706; + private version: number = 707; private textDecoder = new TextDecoder("utf-8"); private textEncoder = new TextEncoder(); // Only support utf-8 @@ -114,7 +114,6 @@ export class SwiftRuntime { const value = JSValue.decode(kind, payload1, payload2, this.memory); obj[key] = value; }, - swjs_get_prop: ( ref: ref, name: ref, @@ -146,7 +145,6 @@ export class SwiftRuntime { const value = JSValue.decode(kind, payload1, payload2, this.memory); obj[index] = value; }, - swjs_get_subscript: ( ref: ref, index: number, @@ -172,7 +170,6 @@ export class SwiftRuntime { this.memory.writeUint32(bytes_ptr_result, bytes_ptr); return bytes.length; }, - swjs_decode_string: (bytes_ptr: pointer, length: number) => { const bytes = this.memory .bytes() @@ -180,7 +177,6 @@ export class SwiftRuntime { 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); @@ -290,7 +286,6 @@ export class SwiftRuntime { this.memory ); }, - swjs_call_function_with_this_no_catch: ( obj_ref: ref, func_ref: ref, @@ -328,13 +323,13 @@ export class SwiftRuntime { } } }, + swjs_call_new: (ref: ref, argv: pointer, argc: number) => { const constructor = this.memory.getObject(ref); const args = JSValue.decodeArray(argv, argc, this.memory); const instance = new constructor(...args); return this.memory.retain(instance); }, - swjs_call_throwing_new: ( ref: ref, argv: pointer, @@ -409,5 +404,25 @@ export class SwiftRuntime { swjs_release: (ref: ref) => { this.memory.release(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); + } + }, }; } diff --git a/Runtime/src/js-value.ts b/Runtime/src/js-value.ts index 67ac5d46a..c8896900f 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 { assertNever, pointer } from "./types"; -export enum Kind { - Invalid = -1, +export const enum Kind { Boolean = 0, String = 1, Number = 2, @@ -11,6 +10,7 @@ export enum Kind { Undefined = 5, Function = 6, Symbol = 7, + BigInt = 8, } export const decode = ( @@ -33,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: @@ -42,7 +44,7 @@ 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`); } }; @@ -73,7 +75,14 @@ export const write = ( memory.writeUint32(kind_ptr, exceptionBit | Kind.Null); return; } - switch (typeof value) { + + const writeRef = (kind: Kind) => { + memory.writeUint32(kind_ptr, exceptionBit | kind); + memory.writeUint32(payload1_ptr, memory.retain(value)); + }; + + const type = typeof value; + switch (type) { case "boolean": { memory.writeUint32(kind_ptr, exceptionBit | Kind.Boolean); memory.writeUint32(payload1_ptr, value ? 1 : 0); @@ -85,8 +94,7 @@ export const write = ( break; } case "string": { - memory.writeUint32(kind_ptr, exceptionBit | Kind.String); - memory.writeUint32(payload1_ptr, memory.retain(value)); + writeRef(Kind.String); break; } case "undefined": { @@ -94,21 +102,22 @@ export const write = ( break; } case "object": { - memory.writeUint32(kind_ptr, exceptionBit | Kind.Object); - memory.writeUint32(payload1_ptr, memory.retain(value)); + writeRef(Kind.Object); break; } case "function": { - memory.writeUint32(kind_ptr, exceptionBit | Kind.Function); - memory.writeUint32(payload1_ptr, memory.retain(value)); + writeRef(Kind.Function); break; } case "symbol": { - memory.writeUint32(kind_ptr, exceptionBit | Kind.Symbol); - memory.writeUint32(payload1_ptr, memory.retain(value)); + writeRef(Kind.Symbol); + break; + } + case "bigint": { + writeRef(Kind.BigInt); break; } default: - throw new Error(`Type "${typeof value}" is not supported yet`); + assertNever(type, `Type "${type}" is not supported yet`); } }; diff --git a/Runtime/src/memory.ts b/Runtime/src/memory.ts index 3c010a5f5..29f827623 100644 --- a/Runtime/src/memory.ts +++ b/Runtime/src/memory.ts @@ -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/types.ts b/Runtime/src/types.ts index b291cd913..bc6700877 100644 --- a/Runtime/src/types.ts +++ b/Runtime/src/types.ts @@ -2,6 +2,7 @@ import * as JSValue from "./js-value"; export type ref = number; export type pointer = number; +export type bool = number; export interface ExportedFunctions { swjs_library_version(): number; @@ -102,9 +103,11 @@ export interface ImportedFunctions { ): number; swjs_load_typed_array(ref: ref, buffer: pointer): void; swjs_release(ref: number): void; + swjs_i64_to_bigint(value: bigint, signed: bool): ref; + swjs_bigint_to_i64(ref: ref, signed: bool): bigint; } -export enum LibraryFeatures { +export const enum LibraryFeatures { WeakRefs = 1 << 0, } @@ -115,8 +118,11 @@ 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); +} diff --git a/Runtime/tsconfig.json b/Runtime/tsconfig.json index 8fa8e650d..8a53ba2c0 100644 --- a/Runtime/tsconfig.json +++ b/Runtime/tsconfig.json @@ -8,7 +8,7 @@ "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..cce10a1ba --- /dev/null +++ b/Sources/JavaScriptBigIntSupport/Int64+I64.swift @@ -0,0 +1,13 @@ +import JavaScriptKit + +extension UInt64: ConvertibleToJSValue, TypedArrayElement { + public static var typedArrayClass = JSObject.global.BigUint64Array.function! + + public var jsValue: JSValue { .bigInt(JSBigInt(unsigned: self)) } +} + +extension Int64: ConvertibleToJSValue, TypedArrayElement { + public static var typedArrayClass = 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..4c8b9bca7 --- /dev/null +++ b/Sources/JavaScriptBigIntSupport/JSBigInt+I64.swift @@ -0,0 +1,20 @@ +import _CJavaScriptBigIntSupport +@_spi(JSObject_id) import JavaScriptKit + +extension JSBigInt: JSBigIntExtended { + public var int64Value: Int64 { + _bigint_to_i64(id, true) + } + + public var uInt64Value: UInt64 { + UInt64(bitPattern: _bigint_to_i64(id, false)) + } + + public convenience init(_ value: Int64) { + self.init(id: _i64_to_bigint(value, true)) + } + + public convenience init(unsigned value: UInt64) { + self.init(id: _i64_to_bigint(Int64(bitPattern: value), false)) + } +} diff --git a/Sources/JavaScriptBigIntSupport/XcodeSupport.swift b/Sources/JavaScriptBigIntSupport/XcodeSupport.swift new file mode 100644 index 000000000..54912cec2 --- /dev/null +++ b/Sources/JavaScriptBigIntSupport/XcodeSupport.swift @@ -0,0 +1,11 @@ +import _CJavaScriptKit + +/// Note: +/// Define all runtime function stubs 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 _i64_to_bigint(_: Int64, _: Bool) -> JavaScriptObjectRef { fatalError() } + func _bigint_to_i64(_: JavaScriptObjectRef, _: Bool) -> Int64 { fatalError() } +#endif diff --git a/Sources/JavaScriptEventLoop/JavaScriptEventLoop.swift b/Sources/JavaScriptEventLoop/JavaScriptEventLoop.swift index 8ff30c8aa..53008c5e4 100644 --- a/Sources/JavaScriptEventLoop/JavaScriptEventLoop.swift +++ b/Sources/JavaScriptEventLoop/JavaScriptEventLoop.swift @@ -12,7 +12,7 @@ public final class JavaScriptEventLoop: SerialExecutor, @unchecked Sendable { /// See also: https://developer.mozilla.org/en-US/docs/Web/API/HTML_DOM_API/Microtask_guide let queueMicrotask: @Sendable (@escaping () -> Void) -> Void /// A function that invokes a given closure after a specified number of milliseconds. - let setTimeout: @Sendable (UInt64, @escaping () -> Void) -> Void + let setTimeout: @Sendable (Double, @escaping () -> Void) -> Void /// A mutable state to manage internal job queue /// Note that this should be guarded atomically when supporting multi-threaded environment. @@ -20,7 +20,7 @@ public final class JavaScriptEventLoop: SerialExecutor, @unchecked Sendable { private init( queueTask: @Sendable @escaping (@escaping () -> Void) -> Void, - setTimeout: @Sendable @escaping (UInt64, @escaping () -> Void) -> Void + setTimeout: @Sendable @escaping (Double, @escaping () -> Void) -> Void ) { self.queueMicrotask = queueTask self.setTimeout = setTimeout @@ -83,7 +83,7 @@ public final class JavaScriptEventLoop: SerialExecutor, @unchecked Sendable { private func enqueue(_ job: UnownedJob, withDelay nanoseconds: UInt64) { let milliseconds = nanoseconds / 1_000_000 - setTimeout(milliseconds, { + setTimeout(Double(milliseconds), { job._runSynchronously(on: self.asUnownedSerialExecutor()) }) } diff --git a/Sources/JavaScriptKit/BasicObjects/JSTypedArray.swift b/Sources/JavaScriptKit/BasicObjects/JSTypedArray.swift index ebcf35959..04c1710d6 100644 --- a/Sources/JavaScriptKit/BasicObjects/JSTypedArray.swift +++ b/Sources/JavaScriptKit/BasicObjects/JSTypedArray.swift @@ -135,15 +135,6 @@ extension UInt32: TypedArrayElement { public static var typedArrayClass = 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! } diff --git a/Sources/JavaScriptKit/ConstructibleFromJSValue.swift b/Sources/JavaScriptKit/ConstructibleFromJSValue.swift index 063597a9e..1f43658f0 100644 --- a/Sources/JavaScriptKit/ConstructibleFromJSValue.swift +++ b/Sources/JavaScriptKit/ConstructibleFromJSValue.swift @@ -20,80 +20,56 @@ 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 Int16: ConstructibleFromJSValue { - public static func construct(from value: JSValue) -> Self? { - value.number.map(Self.init) - } -} - -extension Int32: ConstructibleFromJSValue { - public static func construct(from value: JSValue) -> Self? { - value.number.map(Self.init) - } -} - -extension Int64: 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 UInt: 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 UInt8: ConstructibleFromJSValue { - public static func construct(from value: JSValue) -> Self? { - value.number.map(Self.init) +extension SignedInteger where Self: ConstructibleFromJSValue { + public init(_ bigInt: JSBigIntExtended) { + self.init(bigInt.int64Value) } -} - -extension UInt16: ConstructibleFromJSValue { public static func construct(from value: JSValue) -> Self? { - value.number.map(Self.init) + if let number = value.number { + return Self(number) + } + if let bigInt = value.bigInt as? JSBigIntExtended { + return Self(bigInt) + } + return nil } } +extension Int: ConstructibleFromJSValue {} +extension Int8: ConstructibleFromJSValue {} +extension Int16: ConstructibleFromJSValue {} +extension Int32: ConstructibleFromJSValue {} +extension Int64: ConstructibleFromJSValue {} -extension UInt32: ConstructibleFromJSValue { - public static func construct(from value: JSValue) -> Self? { - value.number.map(Self.init) +extension UnsignedInteger where Self: ConstructibleFromJSValue { + public init(_ bigInt: JSBigIntExtended) { + self.init(bigInt.uInt64Value) } -} - -extension UInt64: 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 + if let number = value.number { + return Self(number) + } + if let bigInt = value.bigInt as? JSBigIntExtended { + return Self(bigInt) + } + 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 83680ad02..572e867b0 100644 --- a/Sources/JavaScriptKit/ConvertibleToJSValue.swift +++ b/Sources/JavaScriptKit/ConvertibleToJSValue.swift @@ -26,11 +26,17 @@ extension Bool: ConvertibleToJSValue { } extension Int: ConvertibleToJSValue { - public var jsValue: JSValue { .number(Double(self)) } + public var jsValue: JSValue { + assert(Self.bitWidth == 32) + return .number(Double(self)) + } } extension UInt: ConvertibleToJSValue { - public var jsValue: JSValue { .number(Double(self)) } + public var jsValue: JSValue { + assert(Self.bitWidth == 32) + return .number(Double(self)) + } } extension Float: ConvertibleToJSValue { @@ -57,9 +63,6 @@ extension UInt32: ConvertibleToJSValue { public var jsValue: JSValue { .number(Double(self)) } } -extension UInt64: ConvertibleToJSValue { - public var jsValue: JSValue { .number(Double(self)) } -} extension Int8: ConvertibleToJSValue { public var jsValue: JSValue { .number(Double(self)) } @@ -73,10 +76,6 @@ extension Int32: ConvertibleToJSValue { public var jsValue: JSValue { .number(Double(self)) } } -extension Int64: ConvertibleToJSValue { - public var jsValue: JSValue { .number(Double(self)) } -} - extension JSString: ConvertibleToJSValue { public var jsValue: JSValue { .string(self) } } @@ -191,8 +190,6 @@ extension Array: ConstructibleFromJSValue where Element: ConstructibleFromJSValu extension RawJSValue: ConvertibleToJSValue { public var jsValue: JSValue { switch kind { - case .invalid: - fatalError() case .boolean: return .boolean(payload1 != 0) case .number: @@ -209,6 +206,8 @@ extension RawJSValue: ConvertibleToJSValue { return .function(JSFunction(id: UInt32(payload1))) case .symbol: return .symbol(JSSymbol(id: UInt32(payload1))) + case .bigInt: + return .bigInt(JSBigInt(id: UInt32(payload1))) } } } @@ -243,6 +242,9 @@ extension JSValue { case let .symbol(symbolRef): kind = .symbol payload1 = JavaScriptPayload1(symbolRef.id) + case let .bigInt(bigIntRef): + kind = .bigInt + payload1 = JavaScriptPayload1(bigIntRef.id) } let rawValue = RawJSValue(kind: kind, payload1: payload1, payload2: payload2) return body(rawValue) diff --git a/Sources/JavaScriptKit/FundamentalObjects/JSBigInt.swift b/Sources/JavaScriptKit/FundamentalObjects/JSBigInt.swift new file mode 100644 index 000000000..4513c14a7 --- /dev/null +++ b/Sources/JavaScriptKit/FundamentalObjects/JSBigInt.swift @@ -0,0 +1,34 @@ +import _CJavaScriptKit + +private let constructor = JSObject.global.BigInt.function! + +public final class JSBigInt: JSObject { + @_spi(JSObject_id) + override public init(id: JavaScriptObjectRef) { + super.init(id: id) + } + + override public class func construct(from value: JSValue) -> Self? { + value.bigInt as? Self + } + + 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/JSObject.swift b/Sources/JavaScriptKit/FundamentalObjects/JSObject.swift index 427648bcc..4e93853ea 100644 --- a/Sources/JavaScriptKit/FundamentalObjects/JSObject.swift +++ b/Sources/JavaScriptKit/FundamentalObjects/JSObject.swift @@ -16,8 +16,10 @@ import _CJavaScriptKit /// reference counting system. @dynamicMemberLookup public class JSObject: Equatable { - internal var id: JavaScriptObjectRef - init(id: JavaScriptObjectRef) { + @_spi(JSObject_id) + public var id: JavaScriptObjectRef + @_spi(JSObject_id) + public init(id: JavaScriptObjectRef) { self.id = id } @@ -120,7 +122,7 @@ public class JSObject: Equatable { /// let animal = JSObject.global.animal.object! /// try animal.throwing.validateAge!() /// ``` - public var `throwing`: JSThrowingObject { + public var throwing: JSThrowingObject { JSThrowingObject(self) } diff --git a/Sources/JavaScriptKit/FundamentalObjects/JSSymbol.swift b/Sources/JavaScriptKit/FundamentalObjects/JSSymbol.swift index 3ec1b2902..a0dea3937 100644 --- a/Sources/JavaScriptKit/FundamentalObjects/JSSymbol.swift +++ b/Sources/JavaScriptKit/FundamentalObjects/JSSymbol.swift @@ -37,6 +37,14 @@ public class JSSymbol: JSObject { public static func key(for symbol: JSSymbol) -> String? { Symbol.keyFor!(symbol).string } + + override public class func construct(from value: JSValue) -> Self? { + return value.symbol as? Self + } + + override public var jsValue: JSValue { + .symbol(self) + } } extension JSSymbol { diff --git a/Sources/JavaScriptKit/JSValue.swift b/Sources/JavaScriptKit/JSValue.swift index b001dc7ab..973dfcb5d 100644 --- a/Sources/JavaScriptKit/JSValue.swift +++ b/Sources/JavaScriptKit/JSValue.swift @@ -11,6 +11,7 @@ public enum JSValue: Equatable { 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`. @@ -68,6 +69,8 @@ public enum JSValue: Equatable { } } + /// Returns the `JSSymbol` of this JS value if its type is function. + /// If not, returns `nil`. public var symbol: JSSymbol? { switch self { case let .symbol(symbol): return symbol @@ -75,6 +78,15 @@ public enum JSValue: Equatable { } } + /// Returns the `JSBigInt` of this JS value if its type is function. + /// If not, returns `nil`. + public var bigInt: JSBigInt? { + switch self { + case let .bigInt(bigInt): return bigInt + default: return nil + } + } + /// Returns the `true` if this JS value is null. /// If not, returns `false`. public var isNull: Bool { @@ -233,7 +245,7 @@ public extension JSValue { /// - Returns: The result of `instanceof` in the JavaScript environment. func isInstanceOf(_ constructor: JSFunction) -> Bool { switch self { - case .boolean, .string, .number, .null, .undefined, .symbol: + case .boolean, .string, .number, .null, .undefined, .symbol, .bigInt: return false case let .object(ref): return ref.isInstanceOf(constructor) @@ -245,20 +257,8 @@ public extension JSValue { extension JSValue: CustomStringConvertible { public var description: String { - switch self { - case let .boolean(boolean): - return boolean.description - case let .string(string): - return string.description - case let .number(number): - return number.description - case let .object(object), let .function(object as JSObject), - .symbol(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! } } diff --git a/Sources/JavaScriptKit/XcodeSupport.swift b/Sources/JavaScriptKit/XcodeSupport.swift index 013c49e2e..4cde273f3 100644 --- a/Sources/JavaScriptKit/XcodeSupport.swift +++ b/Sources/JavaScriptKit/XcodeSupport.swift @@ -1,7 +1,7 @@ import _CJavaScriptKit /// Note: -/// Define all runtime functions stub which are imported from JavaScript environment. +/// Define all runtime function stubs 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. @@ -102,5 +102,4 @@ import _CJavaScriptKit _: 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..dc898c43c --- /dev/null +++ b/Sources/_CJavaScriptBigIntSupport/include/_CJavaScriptKit+I64.h @@ -0,0 +1,23 @@ + +#ifndef _CJavaScriptBigIntSupport_h +#define _CJavaScriptBigIntSupport_h + +#include <_CJavaScriptKit.h> + +/// 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. +__attribute__((__import_module__("javascript_kit"), + __import_name__("swjs_i64_to_bigint"))) +extern JavaScriptObjectRef _i64_to_bigint(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. +__attribute__((__import_module__("javascript_kit"), + __import_name__("swjs_bigint_to_i64"))) +extern long long _bigint_to_i64(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/_CJavaScriptKit/_CJavaScriptKit.c b/Sources/_CJavaScriptKit/_CJavaScriptKit.c index c263b8f71..f2f03b82d 100644 --- a/Sources/_CJavaScriptKit/_CJavaScriptKit.c +++ b/Sources/_CJavaScriptKit/_CJavaScriptKit.c @@ -36,7 +36,7 @@ void swjs_cleanup_host_function_call(void *argv_buffer) { /// this and `SwiftRuntime.version` in `./Runtime/src/index.ts`. __attribute__((export_name("swjs_library_version"))) int swjs_library_version(void) { - return 706; + return 707; } int _library_features(void); diff --git a/Sources/_CJavaScriptKit/include/_CJavaScriptKit.h b/Sources/_CJavaScriptKit/include/_CJavaScriptKit.h index daf405141..c3b56c14d 100644 --- a/Sources/_CJavaScriptKit/include/_CJavaScriptKit.h +++ b/Sources/_CJavaScriptKit/include/_CJavaScriptKit.h @@ -13,7 +13,6 @@ typedef unsigned int JavaScriptHostFuncRef; /// `JavaScriptValueKind` represents the kind of JavaScript primitive value. typedef enum __attribute__((enum_extensibility(closed))) { - JavaScriptValueKindInvalid = -1, JavaScriptValueKindBoolean = 0, JavaScriptValueKindString = 1, JavaScriptValueKindNumber = 2, @@ -22,6 +21,7 @@ typedef enum __attribute__((enum_extensibility(closed))) { JavaScriptValueKindUndefined = 5, JavaScriptValueKindFunction = 6, JavaScriptValueKindSymbol = 7, + JavaScriptValueKindBigInt = 8, } JavaScriptValueKind; typedef struct { @@ -61,7 +61,7 @@ typedef double JavaScriptPayload2; /// payload1: the target `JavaScriptHostFuncRef` /// payload2: 0 /// -/// For symbol value: +/// For symbol and bigint values: /// payload1: `JavaScriptObjectRef` /// payload2: 0 /// @@ -300,6 +300,7 @@ __attribute__((__import_module__("javascript_kit"), __import_name__("swjs_release"))) extern void _release(const JavaScriptObjectRef ref); + #endif #endif /* _CJavaScriptKit_h */ From e858322232520101be4cf2306660bd42b38c3422 Mon Sep 17 00:00:00 2001 From: Max Desiatov Date: Mon, 2 May 2022 14:26:08 +0100 Subject: [PATCH 019/373] Remove outdated `BigInt` support `FIXME` from `JSTypedArray` (#187) The linked issue has already been closed, so this comment doesn't seem to be relevant. --- Sources/JavaScriptKit/BasicObjects/JSTypedArray.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Sources/JavaScriptKit/BasicObjects/JSTypedArray.swift b/Sources/JavaScriptKit/BasicObjects/JSTypedArray.swift index 04c1710d6..39ec2aa21 100644 --- a/Sources/JavaScriptKit/BasicObjects/JSTypedArray.swift +++ b/Sources/JavaScriptKit/BasicObjects/JSTypedArray.swift @@ -10,8 +10,8 @@ public protocol TypedArrayElement: ConvertibleToJSValue, ConstructibleFromJSValu 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). +/// 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. public class JSTypedArray: JSBridgedClass, ExpressibleByArrayLiteral where Element: TypedArrayElement { public class var constructor: JSFunction { Element.typedArrayClass } public var jsObject: JSObject From 384ef252c70bd0b9988e1cef95b416e6ed48d82f Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 3 May 2022 09:49:06 +0000 Subject: [PATCH 020/373] Bump async from 2.6.3 to 2.6.4 in /Example (#188) --- Example/package-lock.json | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/Example/package-lock.json b/Example/package-lock.json index 139abdecf..65a52a803 100644 --- a/Example/package-lock.json +++ b/Example/package-lock.json @@ -21,7 +21,7 @@ }, "..": { "name": "javascript-kit-swift", - "version": "0.12.0", + "version": "0.14.0", "license": "MIT", "devDependencies": { "@rollup/plugin-typescript": "^8.3.1", @@ -642,8 +642,9 @@ } }, "node_modules/async": { - "version": "2.6.3", - "integrity": "sha512-zflvls11DCy+dQWzTW2dzuilv8Z5X/pjfmZOWba6TNIVDm+2UDaJmXSOXlasHKfNBs8oo3M0aT50fDEWfKZjXg==", + "version": "2.6.4", + "resolved": "https://registry.npmjs.org/async/-/async-2.6.4.tgz", + "integrity": "sha512-mzo5dfJYwAn29PeiJ0zvwTo04zj8HDJj0Mn8TD7sno7q12prdbnasKJHhkm2c1LgrhlJ0teaea8860oxi51mGA==", "dev": true, "dependencies": { "lodash": "^4.17.14" @@ -4325,8 +4326,9 @@ "dev": true }, "async": { - "version": "2.6.3", - "integrity": "sha512-zflvls11DCy+dQWzTW2dzuilv8Z5X/pjfmZOWba6TNIVDm+2UDaJmXSOXlasHKfNBs8oo3M0aT50fDEWfKZjXg==", + "version": "2.6.4", + "resolved": "https://registry.npmjs.org/async/-/async-2.6.4.tgz", + "integrity": "sha512-mzo5dfJYwAn29PeiJ0zvwTo04zj8HDJj0Mn8TD7sno7q12prdbnasKJHhkm2c1LgrhlJ0teaea8860oxi51mGA==", "dev": true, "requires": { "lodash": "^4.17.14" From e6b326dc3fc0c2fb27255cb3fd71f78f874d8578 Mon Sep 17 00:00:00 2001 From: Max Desiatov Date: Tue, 3 May 2022 13:13:38 +0100 Subject: [PATCH 021/373] Update toolchain references to 5.6.0 in `README.md` (#189) --- README.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index b5c64539a..42af64320 100644 --- a/README.md +++ b/README.md @@ -221,11 +221,11 @@ especially if you change anything in the JavaScript runtime parts. This is becau 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: +Just pass a toolchain archive URL for [the latest SwiftWasm 5.6 +release](https://github.com/swiftwasm/swift/releases/tag/swift-wasm-5.6.0-RELEASE) 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 +$ swiftenv install "https://github.com/swiftwasm/swift/releases/download/swift-wasm-5.6.0-RELEASE/swift-wasm-5.6.0-RELEASE-macos_$(uname -m).pkg" ``` You can also use the `install-toolchain.sh` helper script that uses a hardcoded toolchain snapshot: @@ -233,6 +233,6 @@ You can also use the `install-toolchain.sh` helper script that uses a hardcoded ```sh $ ./scripts/install-toolchain.sh $ swift --version -Swift version 5.5 (swiftlang-5.5.0) +Swift version 5.6 (swiftlang-5.6.0) Target: arm64-apple-darwin20.6.0 ``` From c1a0923c25990ec8a7b6fe83c79b9347f772a73d Mon Sep 17 00:00:00 2001 From: Yuta Saito Date: Wed, 11 May 2022 22:19:08 +0900 Subject: [PATCH 022/373] Test with Node.js's WASI implementation (#192) --- .github/workflows/test.yml | 25 +++++-- IntegrationTests/Makefile | 8 ++- IntegrationTests/bin/primary-tests.js | 2 +- IntegrationTests/lib.js | 95 +++++++++++++++++++-------- 4 files changed, 91 insertions(+), 39 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 0f2418e9c..819089a19 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -8,24 +8,35 @@ jobs: name: Build and Test strategy: matrix: - os: [macos-10.15, macos-11, macos-12, ubuntu-18.04, ubuntu-20.04] - toolchain: - - wasm-5.5.0-RELEASE - - wasm-5.6.0-RELEASE - runs-on: ${{ matrix.os }} + entry: + # Ensure that all host can install toolchain, build project, and run tests + - { os: macos-10.15, toolchain: wasm-5.6.0-RELEASE, wasi-backend: Node } + - { os: macos-11, toolchain: wasm-5.6.0-RELEASE, wasi-backend: Node } + - { os: macos-12, toolchain: wasm-5.6.0-RELEASE, wasi-backend: Node } + - { os: ubuntu-18.04, toolchain: wasm-5.6.0-RELEASE, wasi-backend: Node } + + # Ensure that test succeeds with all toolchains and wasi backend combinations + - { os: ubuntu-20.04, toolchain: wasm-5.5.0-RELEASE, wasi-backend: Node } + - { os: ubuntu-20.04, toolchain: wasm-5.6.0-RELEASE, wasi-backend: Node } + - { os: ubuntu-20.04, toolchain: wasm-5.5.0-RELEASE, wasi-backend: Wasmer } + - { os: ubuntu-20.04, toolchain: wasm-5.6.0-RELEASE, wasi-backend: Wasmer } + + runs-on: ${{ matrix.entry.os }} steps: - name: Checkout uses: actions/checkout@master with: fetch-depth: 1 - name: Run Test + env: + JAVASCRIPTKIT_WASI_BACKEND: ${{ matrix.entry.wasi-backend }} 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 + SWIFT_VERSION=${{ matrix.entry.toolchain }} make bootstrap + echo ${{ matrix.entry.toolchain }} > .swift-version make test native-build: # Check native build to make it easy to develop applications by Xcode diff --git a/IntegrationTests/Makefile b/IntegrationTests/Makefile index 21e8be315..312977482 100644 --- a/IntegrationTests/Makefile +++ b/IntegrationTests/Makefile @@ -1,6 +1,8 @@ CONFIGURATION ?= debug SWIFT_BUILD_FLAGS ?= +NODEJS = node --experimental-wasi-unstable-preview1 + FORCE: TestSuites/.build/$(CONFIGURATION)/%.wasm: FORCE swift build --package-path TestSuites \ @@ -27,18 +29,18 @@ benchmark_setup: build_rt dist/BenchmarkTests.wasm .PHONY: run_benchmark run_benchmark: - node bin/benchmark-tests.js + $(NODEJS) 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 + $(NODEJS) bin/primary-tests.js .PHONY: concurrency_test concurrency_test: build_rt dist/ConcurrencyTests.wasm - node bin/concurrency-tests.js + $(NODEJS) bin/concurrency-tests.js .PHONY: test test: concurrency_test primary_test diff --git a/IntegrationTests/bin/primary-tests.js b/IntegrationTests/bin/primary-tests.js index 94bcf7da5..2d977c3fd 100644 --- a/IntegrationTests/bin/primary-tests.js +++ b/IntegrationTests/bin/primary-tests.js @@ -95,7 +95,7 @@ global.callThrowingClosure = (c) => { } }; -const { startWasiTask } = require("../lib"); +const { startWasiTask, WASI } = require("../lib"); startWasiTask("./dist/PrimaryTests.wasm").catch((err) => { console.log(err); diff --git a/IntegrationTests/lib.js b/IntegrationTests/lib.js index c55ed3b4d..ed5c5b493 100644 --- a/IntegrationTests/lib.js +++ b/IntegrationTests/lib.js @@ -1,40 +1,81 @@ const SwiftRuntime = require("javascript-kit-swift").SwiftRuntime; -const WASI = require("@wasmer/wasi").WASI; +const WasmerWASI = require("@wasmer/wasi").WASI; const WasmFs = require("@wasmer/wasmfs").WasmFs; +const NodeWASI = require("wasi").WASI; 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; +const WASI = { + Wasmer: () => { + // 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); + }; + const wasi = new WasmerWASI({ + args: [], + env: {}, + bindings: { + ...WasmerWASI.defaultBindings, + fs: wasmFs.fs, + }, + }); + + return { + wasiImport: wasi.wasiImport, + start(instance) { + wasi.start(instance); + instance.exports._initialize(); + instance.exports.main(); + } } - return originalWriteSync(fd, buffer, offset, length, position); - }; - let wasi = new WASI({ - args: [], - env: {}, - bindings: { - ...WASI.defaultBindings, - fs: wasmFs.fs, - }, - }); + }, + Node: () => { + const wasi = new NodeWASI({ + args: [], + env: {}, + returnOnExit: true, + }) + + return { + wasiImport: wasi.wasiImport, + start(instance) { + wasi.initialize(instance); + instance.exports.main(); + } + } + }, +}; + +const selectWASIBackend = () => { + const value = process.env["JAVASCRIPTKIT_WASI_BACKEND"] + if (value) { + const backend = WASI[value]; + if (backend) { + return backend; + } + } + return WASI.Node; +}; +const startWasiTask = async (wasmPath, wasiConstructor = selectWASIBackend()) => { const swift = new SwiftRuntime(); // Fetch our Wasm File const wasmBinary = await readFile(wasmPath); + const wasi = wasiConstructor(); // Instantiate the WebAssembly file let { instance } = await WebAssembly.instantiate(wasmBinary, { @@ -45,8 +86,6 @@ const startWasiTask = async (wasmPath) => { swift.setInstance(instance); // Start the WebAssembly WASI instance! wasi.start(instance); - instance.exports._initialize(); - instance.exports.main(); }; -module.exports = { startWasiTask }; +module.exports = { startWasiTask, WASI }; From e022311ed98324ac176e9ffba45d7aab97f29a57 Mon Sep 17 00:00:00 2001 From: Max Desiatov Date: Thu, 12 May 2022 14:16:52 +0100 Subject: [PATCH 023/373] Supply JSKit runtime in SwiftPM resources (#193) Due to the fact that https://github.com/apple/swift-package-manager/pull/4306 will only be available starting with Swift 5.7, we can't generate these resources with a SwiftPM plugin. I propose storing the generated code in the repository directly for now. When 5.7 is released, we can switch to a SwiftPM plugin approach. Can be tested end-to-end with https://github.com/swiftwasm/carton/pull/335. Co-authored-by: Yuta Saito --- .gitattributes | 1 + .github/workflows/test.yml | 5 + Makefile | 5 + Package.swift | 5 +- Runtime/src/closure-heap.ts | 2 +- Runtime/src/index.ts | 8 +- Runtime/src/js-value.ts | 4 +- Runtime/src/memory.ts | 4 +- Runtime/src/object-heap.ts | 4 +- Runtime/src/types.ts | 2 +- Sources/JavaScriptKit/Runtime/index.js | 419 ++++++++++++++++++++++++ Sources/JavaScriptKit/Runtime/index.mjs | 409 +++++++++++++++++++++++ 12 files changed, 854 insertions(+), 14 deletions(-) create mode 100644 .gitattributes create mode 100644 Sources/JavaScriptKit/Runtime/index.js create mode 100644 Sources/JavaScriptKit/Runtime/index.mjs 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/workflows/test.yml b/.github/workflows/test.yml index 819089a19..e2d2e7d18 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -38,6 +38,11 @@ jobs: SWIFT_VERSION=${{ matrix.entry.toolchain }} make bootstrap echo ${{ matrix.entry.toolchain }} > .swift-version make test + - name: Check if SwiftPM resources are stale + run: | + make regenerate_swiftpm_resources + git diff --exit-code Sources/JavaScriptKit/Runtime + native-build: # Check native build to make it easy to develop applications by Xcode name: Build for native target diff --git a/Makefile b/Makefile index b48af9e2b..44d3de624 100644 --- a/Makefile +++ b/Makefile @@ -29,3 +29,8 @@ run_benchmark: .PHONY: perf-tester perf-tester: cd ci/perf-tester && npm ci + +.PHONY: regenerate_swiftpm_resources +regenerate_swiftpm_resources: + npm run build + cp Runtime/lib/index.js Runtime/lib/index.mjs Sources/JavaScriptKit/Runtime diff --git a/Package.swift b/Package.swift index 3d07321ac..d278e5ab9 100644 --- a/Package.swift +++ b/Package.swift @@ -1,4 +1,4 @@ -// swift-tools-version:5.2 +// swift-tools-version:5.3 import PackageDescription @@ -12,7 +12,8 @@ let package = Package( targets: [ .target( name: "JavaScriptKit", - dependencies: ["_CJavaScriptKit"] + dependencies: ["_CJavaScriptKit"], + resources: [.copy("Runtime")] ), .target(name: "_CJavaScriptKit"), .target( 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 dcd817a72..17c53696e 100644 --- a/Runtime/src/index.ts +++ b/Runtime/src/index.ts @@ -1,4 +1,4 @@ -import { SwiftClosureDeallocator } from "./closure-heap"; +import { SwiftClosureDeallocator } from "./closure-heap.js"; import { LibraryFeatures, ExportedFunctions, @@ -6,9 +6,9 @@ import { pointer, TypedArray, ImportedFunctions, -} from "./types"; -import * as JSValue from "./js-value"; -import { Memory } from "./memory"; +} from "./types.js"; +import * as JSValue from "./js-value.js"; +import { Memory } from "./memory.js"; export class SwiftRuntime { private _instance: WebAssembly.Instance | null; diff --git a/Runtime/src/js-value.ts b/Runtime/src/js-value.ts index c8896900f..c3c24c3a9 100644 --- a/Runtime/src/js-value.ts +++ b/Runtime/src/js-value.ts @@ -1,5 +1,5 @@ -import { Memory } from "./memory"; -import { assertNever, pointer } from "./types"; +import { Memory } from "./memory.js"; +import { assertNever, pointer } from "./types.js"; export const enum Kind { Boolean = 0, diff --git a/Runtime/src/memory.ts b/Runtime/src/memory.ts index 29f827623..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; diff --git a/Runtime/src/object-heap.ts b/Runtime/src/object-heap.ts index 2f9b1fdf3..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; diff --git a/Runtime/src/types.ts b/Runtime/src/types.ts index bc6700877..d587b2c8a 100644 --- a/Runtime/src/types.ts +++ b/Runtime/src/types.ts @@ -1,4 +1,4 @@ -import * as JSValue from "./js-value"; +import * as JSValue from "./js-value.js"; export type ref = number; export type pointer = number; diff --git a/Sources/JavaScriptKit/Runtime/index.js b/Sources/JavaScriptKit/Runtime/index.js new file mode 100644 index 000000000..68f1c433c --- /dev/null +++ b/Sources/JavaScriptKit/Runtime/index.js @@ -0,0 +1,419 @@ +(function (global, factory) { + typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports) : + typeof define === 'function' && define.amd ? define(['exports'], factory) : + (global = typeof globalThis !== 'undefined' ? globalThis : global || self, factory(global.JavaScriptKit = {})); +})(this, (function (exports) { 'use strict'; + + /// Memory lifetime of closures in Swift are managed by Swift side + class SwiftClosureDeallocator { + constructor(exports) { + if (typeof FinalizationRegistry === "undefined") { + throw new Error("The Swift part of JavaScriptKit was configured to require " + + "the availability of JavaScript WeakRefs. Please build " + + "with `-Xswiftc -DJAVASCRIPTKIT_WITHOUT_WEAKREFS` to " + + "disable features that use WeakRefs."); + } + this.functionRegistry = new FinalizationRegistry((id) => { + exports.swjs_free_host_function(id); + }); + } + track(func, func_ref) { + this.functionRegistry.register(func, func_ref); + } + } + + function assertNever(x, message) { + throw new Error(message); + } + + const decode = (kind, payload1, payload2, memory) => { + switch (kind) { + case 0 /* Boolean */: + switch (payload1) { + case 0: + return false; + case 1: + return true; + } + case 2 /* Number */: + return payload2; + case 1 /* String */: + case 3 /* Object */: + case 6 /* Function */: + case 7 /* Symbol */: + case 8 /* BigInt */: + return memory.getObject(payload1); + case 4 /* Null */: + return null; + case 5 /* Undefined */: + return undefined; + default: + assertNever(kind, `JSValue Type kind "${kind}" is not supported`); + } + }; + // Note: + // `decodeValues` assumes that the size of RawJSValue is 16. + const decodeArray = (ptr, length, memory) => { + let result = []; + 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); + result.push(decode(kind, payload1, payload2, memory)); + } + return result; + }; + const write = (value, kind_ptr, payload1_ptr, payload2_ptr, is_exception, memory) => { + const exceptionBit = (is_exception ? 1 : 0) << 31; + if (value === null) { + memory.writeUint32(kind_ptr, exceptionBit | 4 /* Null */); + return; + } + const writeRef = (kind) => { + memory.writeUint32(kind_ptr, exceptionBit | kind); + memory.writeUint32(payload1_ptr, memory.retain(value)); + }; + const type = typeof value; + switch (type) { + case "boolean": { + memory.writeUint32(kind_ptr, exceptionBit | 0 /* Boolean */); + memory.writeUint32(payload1_ptr, value ? 1 : 0); + break; + } + case "number": { + memory.writeUint32(kind_ptr, exceptionBit | 2 /* Number */); + memory.writeFloat64(payload2_ptr, value); + break; + } + case "string": { + writeRef(1 /* String */); + break; + } + case "undefined": { + memory.writeUint32(kind_ptr, exceptionBit | 5 /* Undefined */); + break; + } + case "object": { + writeRef(3 /* Object */); + break; + } + case "function": { + writeRef(6 /* Function */); + break; + } + case "symbol": { + writeRef(7 /* Symbol */); + break; + } + case "bigint": { + writeRef(8 /* BigInt */); + break; + } + default: + assertNever(type, `Type "${type}" is not supported yet`); + } + }; + + 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 SwiftRuntime { + constructor() { + this.version = 707; + this.textDecoder = new TextDecoder("utf-8"); + this.textEncoder = new TextEncoder(); // Only support utf-8 + /** @deprecated Use `wasmImports` instead */ + this.importObjects = () => this.wasmImports; + this.wasmImports = { + swjs_set_prop: (ref, name, kind, payload1, payload2) => { + const obj = this.memory.getObject(ref); + const key = this.memory.getObject(name); + const value = decode(kind, payload1, payload2, this.memory); + obj[key] = value; + }, + swjs_get_prop: (ref, name, kind_ptr, payload1_ptr, payload2_ptr) => { + const obj = this.memory.getObject(ref); + const key = this.memory.getObject(name); + const result = obj[key]; + write(result, kind_ptr, payload1_ptr, payload2_ptr, false, this.memory); + }, + swjs_set_subscript: (ref, index, kind, payload1, payload2) => { + const obj = this.memory.getObject(ref); + const value = decode(kind, payload1, payload2, this.memory); + obj[index] = value; + }, + swjs_get_subscript: (ref, index, kind_ptr, payload1_ptr, payload2_ptr) => { + const obj = this.memory.getObject(ref); + const result = obj[index]; + write(result, kind_ptr, payload1_ptr, payload2_ptr, false, this.memory); + }, + swjs_encode_string: (ref, bytes_ptr_result) => { + 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, length) => { + 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, buffer) => { + const bytes = this.memory.getObject(ref); + this.memory.writeBytes(buffer, bytes); + }, + swjs_call_function: (ref, argv, argc, kind_ptr, payload1_ptr, payload2_ptr) => { + const func = this.memory.getObject(ref); + let result; + try { + const args = decodeArray(argv, argc, this.memory); + result = func(...args); + } + catch (error) { + write(error, kind_ptr, payload1_ptr, payload2_ptr, true, this.memory); + return; + } + write(result, kind_ptr, payload1_ptr, payload2_ptr, false, this.memory); + }, + swjs_call_function_no_catch: (ref, argv, argc, kind_ptr, payload1_ptr, payload2_ptr) => { + const func = this.memory.getObject(ref); + let isException = true; + try { + const args = decodeArray(argv, argc, this.memory); + const result = func(...args); + write(result, kind_ptr, payload1_ptr, payload2_ptr, false, this.memory); + isException = false; + } + finally { + if (isException) { + write(undefined, kind_ptr, payload1_ptr, payload2_ptr, true, this.memory); + } + } + }, + swjs_call_function_with_this: (obj_ref, func_ref, argv, argc, kind_ptr, payload1_ptr, payload2_ptr) => { + const obj = this.memory.getObject(obj_ref); + const func = this.memory.getObject(func_ref); + let result; + try { + const args = decodeArray(argv, argc, this.memory); + result = func.apply(obj, args); + } + catch (error) { + write(error, kind_ptr, payload1_ptr, payload2_ptr, true, this.memory); + return; + } + write(result, kind_ptr, payload1_ptr, payload2_ptr, false, this.memory); + }, + swjs_call_function_with_this_no_catch: (obj_ref, func_ref, argv, argc, kind_ptr, payload1_ptr, payload2_ptr) => { + const obj = this.memory.getObject(obj_ref); + const func = this.memory.getObject(func_ref); + let isException = true; + try { + const args = decodeArray(argv, argc, this.memory); + const result = func.apply(obj, args); + write(result, kind_ptr, payload1_ptr, payload2_ptr, false, this.memory); + isException = false; + } + finally { + if (isException) { + write(undefined, kind_ptr, payload1_ptr, payload2_ptr, true, this.memory); + } + } + }, + swjs_call_new: (ref, argv, argc) => { + const constructor = this.memory.getObject(ref); + const args = decodeArray(argv, argc, this.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) => { + const constructor = this.memory.getObject(ref); + let result; + try { + const args = decodeArray(argv, argc, this.memory); + result = new constructor(...args); + } + catch (error) { + write(error, exception_kind_ptr, exception_payload1_ptr, exception_payload2_ptr, true, this.memory); + return -1; + } + write(null, exception_kind_ptr, exception_payload1_ptr, exception_payload2_ptr, false, this.memory); + return this.memory.retain(result); + }, + swjs_instanceof: (obj_ref, constructor_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) => { + var _a; + const func = (...args) => this.callHostFunction(host_func_id, 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); + const array = new ArrayType(this.memory.rawMemory.buffer, elementsPtr, length); + // Call `.slice()` to copy the memory + return this.memory.retain(array.slice()); + }, + swjs_load_typed_array: (ref, buffer) => { + const typedArray = this.memory.getObject(ref); + const bytes = new Uint8Array(typedArray.buffer); + this.memory.writeBytes(buffer, bytes); + }, + swjs_release: (ref) => { + this.memory.release(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); + } + }, + }; + this._instance = null; + this._memory = null; + this._closureDeallocator = null; + } + setInstance(instance) { + this._instance = instance; + 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}`); + } + } + get instance() { + if (!this._instance) + throw new Error("WebAssembly instance is not set yet"); + return this._instance; + } + get exports() { + return this.instance.exports; + } + get memory() { + if (!this._memory) { + this._memory = new Memory(this.instance.exports); + } + return this._memory; + } + get closureDeallocator() { + if (this._closureDeallocator) + return this._closureDeallocator; + const features = this.exports.swjs_library_features(); + const librarySupportsWeakRef = (features & 1 /* WeakRefs */) != 0; + if (librarySupportsWeakRef) { + this._closureDeallocator = new SwiftClosureDeallocator(this.exports); + } + return this._closureDeallocator; + } + callHostFunction(host_func_id, args) { + const argc = args.length; + const argv = this.exports.swjs_prepare_host_function_call(argc); + 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, this.memory); + } + let output; + // This ref is released by the swjs_call_host_function implementation + const callback_func_ref = this.memory.retain((result) => { + output = result; + }); + this.exports.swjs_call_host_function(host_func_id, argv, argc, callback_func_ref); + this.exports.swjs_cleanup_host_function_call(argv); + return output; + } + } + + exports.SwiftRuntime = SwiftRuntime; + + Object.defineProperty(exports, '__esModule', { value: true }); + +})); diff --git a/Sources/JavaScriptKit/Runtime/index.mjs b/Sources/JavaScriptKit/Runtime/index.mjs new file mode 100644 index 000000000..1874a8fee --- /dev/null +++ b/Sources/JavaScriptKit/Runtime/index.mjs @@ -0,0 +1,409 @@ +/// 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 decode = (kind, payload1, payload2, memory) => { + switch (kind) { + case 0 /* Boolean */: + switch (payload1) { + case 0: + return false; + case 1: + return true; + } + case 2 /* Number */: + return payload2; + case 1 /* String */: + case 3 /* Object */: + case 6 /* Function */: + case 7 /* Symbol */: + case 8 /* BigInt */: + return memory.getObject(payload1); + case 4 /* Null */: + return null; + case 5 /* Undefined */: + return undefined; + default: + assertNever(kind, `JSValue Type kind "${kind}" is not supported`); + } +}; +// Note: +// `decodeValues` assumes that the size of RawJSValue is 16. +const decodeArray = (ptr, length, memory) => { + let result = []; + 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); + result.push(decode(kind, payload1, payload2, memory)); + } + return result; +}; +const write = (value, kind_ptr, payload1_ptr, payload2_ptr, is_exception, memory) => { + const exceptionBit = (is_exception ? 1 : 0) << 31; + if (value === null) { + memory.writeUint32(kind_ptr, exceptionBit | 4 /* Null */); + return; + } + const writeRef = (kind) => { + memory.writeUint32(kind_ptr, exceptionBit | kind); + memory.writeUint32(payload1_ptr, memory.retain(value)); + }; + const type = typeof value; + switch (type) { + case "boolean": { + memory.writeUint32(kind_ptr, exceptionBit | 0 /* Boolean */); + memory.writeUint32(payload1_ptr, value ? 1 : 0); + break; + } + case "number": { + memory.writeUint32(kind_ptr, exceptionBit | 2 /* Number */); + memory.writeFloat64(payload2_ptr, value); + break; + } + case "string": { + writeRef(1 /* String */); + break; + } + case "undefined": { + memory.writeUint32(kind_ptr, exceptionBit | 5 /* Undefined */); + break; + } + case "object": { + writeRef(3 /* Object */); + break; + } + case "function": { + writeRef(6 /* Function */); + break; + } + case "symbol": { + writeRef(7 /* Symbol */); + break; + } + case "bigint": { + writeRef(8 /* BigInt */); + break; + } + default: + assertNever(type, `Type "${type}" is not supported yet`); + } +}; + +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 SwiftRuntime { + constructor() { + this.version = 707; + this.textDecoder = new TextDecoder("utf-8"); + this.textEncoder = new TextEncoder(); // Only support utf-8 + /** @deprecated Use `wasmImports` instead */ + this.importObjects = () => this.wasmImports; + this.wasmImports = { + swjs_set_prop: (ref, name, kind, payload1, payload2) => { + const obj = this.memory.getObject(ref); + const key = this.memory.getObject(name); + const value = decode(kind, payload1, payload2, this.memory); + obj[key] = value; + }, + swjs_get_prop: (ref, name, kind_ptr, payload1_ptr, payload2_ptr) => { + const obj = this.memory.getObject(ref); + const key = this.memory.getObject(name); + const result = obj[key]; + write(result, kind_ptr, payload1_ptr, payload2_ptr, false, this.memory); + }, + swjs_set_subscript: (ref, index, kind, payload1, payload2) => { + const obj = this.memory.getObject(ref); + const value = decode(kind, payload1, payload2, this.memory); + obj[index] = value; + }, + swjs_get_subscript: (ref, index, kind_ptr, payload1_ptr, payload2_ptr) => { + const obj = this.memory.getObject(ref); + const result = obj[index]; + write(result, kind_ptr, payload1_ptr, payload2_ptr, false, this.memory); + }, + swjs_encode_string: (ref, bytes_ptr_result) => { + 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, length) => { + 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, buffer) => { + const bytes = this.memory.getObject(ref); + this.memory.writeBytes(buffer, bytes); + }, + swjs_call_function: (ref, argv, argc, kind_ptr, payload1_ptr, payload2_ptr) => { + const func = this.memory.getObject(ref); + let result; + try { + const args = decodeArray(argv, argc, this.memory); + result = func(...args); + } + catch (error) { + write(error, kind_ptr, payload1_ptr, payload2_ptr, true, this.memory); + return; + } + write(result, kind_ptr, payload1_ptr, payload2_ptr, false, this.memory); + }, + swjs_call_function_no_catch: (ref, argv, argc, kind_ptr, payload1_ptr, payload2_ptr) => { + const func = this.memory.getObject(ref); + let isException = true; + try { + const args = decodeArray(argv, argc, this.memory); + const result = func(...args); + write(result, kind_ptr, payload1_ptr, payload2_ptr, false, this.memory); + isException = false; + } + finally { + if (isException) { + write(undefined, kind_ptr, payload1_ptr, payload2_ptr, true, this.memory); + } + } + }, + swjs_call_function_with_this: (obj_ref, func_ref, argv, argc, kind_ptr, payload1_ptr, payload2_ptr) => { + const obj = this.memory.getObject(obj_ref); + const func = this.memory.getObject(func_ref); + let result; + try { + const args = decodeArray(argv, argc, this.memory); + result = func.apply(obj, args); + } + catch (error) { + write(error, kind_ptr, payload1_ptr, payload2_ptr, true, this.memory); + return; + } + write(result, kind_ptr, payload1_ptr, payload2_ptr, false, this.memory); + }, + swjs_call_function_with_this_no_catch: (obj_ref, func_ref, argv, argc, kind_ptr, payload1_ptr, payload2_ptr) => { + const obj = this.memory.getObject(obj_ref); + const func = this.memory.getObject(func_ref); + let isException = true; + try { + const args = decodeArray(argv, argc, this.memory); + const result = func.apply(obj, args); + write(result, kind_ptr, payload1_ptr, payload2_ptr, false, this.memory); + isException = false; + } + finally { + if (isException) { + write(undefined, kind_ptr, payload1_ptr, payload2_ptr, true, this.memory); + } + } + }, + swjs_call_new: (ref, argv, argc) => { + const constructor = this.memory.getObject(ref); + const args = decodeArray(argv, argc, this.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) => { + const constructor = this.memory.getObject(ref); + let result; + try { + const args = decodeArray(argv, argc, this.memory); + result = new constructor(...args); + } + catch (error) { + write(error, exception_kind_ptr, exception_payload1_ptr, exception_payload2_ptr, true, this.memory); + return -1; + } + write(null, exception_kind_ptr, exception_payload1_ptr, exception_payload2_ptr, false, this.memory); + return this.memory.retain(result); + }, + swjs_instanceof: (obj_ref, constructor_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) => { + var _a; + const func = (...args) => this.callHostFunction(host_func_id, 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); + const array = new ArrayType(this.memory.rawMemory.buffer, elementsPtr, length); + // Call `.slice()` to copy the memory + return this.memory.retain(array.slice()); + }, + swjs_load_typed_array: (ref, buffer) => { + const typedArray = this.memory.getObject(ref); + const bytes = new Uint8Array(typedArray.buffer); + this.memory.writeBytes(buffer, bytes); + }, + swjs_release: (ref) => { + this.memory.release(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); + } + }, + }; + this._instance = null; + this._memory = null; + this._closureDeallocator = null; + } + setInstance(instance) { + this._instance = instance; + 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}`); + } + } + get instance() { + if (!this._instance) + throw new Error("WebAssembly instance is not set yet"); + return this._instance; + } + get exports() { + return this.instance.exports; + } + get memory() { + if (!this._memory) { + this._memory = new Memory(this.instance.exports); + } + return this._memory; + } + get closureDeallocator() { + if (this._closureDeallocator) + return this._closureDeallocator; + const features = this.exports.swjs_library_features(); + const librarySupportsWeakRef = (features & 1 /* WeakRefs */) != 0; + if (librarySupportsWeakRef) { + this._closureDeallocator = new SwiftClosureDeallocator(this.exports); + } + return this._closureDeallocator; + } + callHostFunction(host_func_id, args) { + const argc = args.length; + const argv = this.exports.swjs_prepare_host_function_call(argc); + 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, this.memory); + } + let output; + // This ref is released by the swjs_call_host_function implementation + const callback_func_ref = this.memory.retain((result) => { + output = result; + }); + this.exports.swjs_call_host_function(host_func_id, argv, argc, callback_func_ref); + this.exports.swjs_cleanup_host_function_call(argv); + return output; + } +} + +export { SwiftRuntime }; From ab3da740b2fb355f2c7b1a05e2eac11e541316ad Mon Sep 17 00:00:00 2001 From: Max Desiatov Date: Mon, 16 May 2022 17:50:06 +0100 Subject: [PATCH 024/373] Gracefully handle unavailable `JSBridgedClass` (#190) Force unwrapping in `class var constructor` may crash for classes that are unavailable in the current environment. For example, after https://github.com/swiftwasm/WebAPIKit/pull/38 was merged, it led to crashes in `getCanvas` calls due to these optional casts relying on `class var constructor` always succeeding: ```swift public static func construct(from value: JSValue) -> Self? { if let canvasRenderingContext2D: CanvasRenderingContext2D = value.fromJSValue() { return .canvasRenderingContext2D(canvasRenderingContext2D) } if let gpuCanvasContext: GPUCanvasContext = value.fromJSValue() { return .gpuCanvasContext(gpuCanvasContext) } if let imageBitmapRenderingContext: ImageBitmapRenderingContext = value.fromJSValue() { return .imageBitmapRenderingContext(imageBitmapRenderingContext) } if let webGL2RenderingContext: WebGL2RenderingContext = value.fromJSValue() { return .webGL2RenderingContext(webGL2RenderingContext) } if let webGLRenderingContext: WebGLRenderingContext = value.fromJSValue() { return .webGLRenderingContext(webGLRenderingContext) } return nil } ``` `if let gpuCanvasContext: GPUCanvasContext = value.fromJSValue()` branch crashes on browsers that don't have `GPUCanvasContext` enabled. As we currently don't have a better way to handle unavailable features, I propose making the result type of `static var constructor` requirement optional. This means you can still declare classes that are unavailable in the host JS environment. Conditional type casts are also available as they were, they will just always return `nil`, and initializers for these classes will return `nil` as well. --- .../JavaScriptKit/BasicObjects/JSArray.swift | 8 +-- .../JavaScriptKit/BasicObjects/JSDate.swift | 60 ++++++++--------- .../JavaScriptKit/BasicObjects/JSError.swift | 12 ++-- .../BasicObjects/JSPromise.swift | 66 ++++++++++--------- .../BasicObjects/JSTypedArray.swift | 22 ++++--- Sources/JavaScriptKit/JSBridgedType.swift | 20 +++--- 6 files changed, 98 insertions(+), 90 deletions(-) diff --git a/Sources/JavaScriptKit/BasicObjects/JSArray.swift b/Sources/JavaScriptKit/BasicObjects/JSArray.swift index 2452c17e7..2d971daf7 100644 --- a/Sources/JavaScriptKit/BasicObjects/JSArray.swift +++ b/Sources/JavaScriptKit/BasicObjects/JSArray.swift @@ -1,10 +1,10 @@ /// 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 let constructor = JSObject.global.Array.function static func isArray(_ object: JSObject) -> Bool { - constructor.isArray!(object).boolean! + constructor!.isArray!(object).boolean! } public let jsObject: JSObject @@ -94,8 +94,8 @@ private func getObjectValuesLength(_ object: JSObject) -> Int { return Int(values.length.number!) } -extension JSValue { - public var array: JSArray? { +public extension JSValue { + var array: JSArray? { object.flatMap(JSArray.init) } } diff --git a/Sources/JavaScriptKit/BasicObjects/JSDate.swift b/Sources/JavaScriptKit/BasicObjects/JSDate.swift index 3f38f5aba..5a0fd25ee 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 let constructor = 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 cbdac8d6e..1d1526a47 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. -*/ +/** 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 { /// The constructor function used to create new JavaScript `Error` objects. - public static let constructor = JSObject.global.Error.function! + public static let constructor = 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) { diff --git a/Sources/JavaScriptKit/BasicObjects/JSPromise.swift b/Sources/JavaScriptKit/BasicObjects/JSPromise.swift index 0aa44cadd..81a124447 100644 --- a/Sources/JavaScriptKit/BasicObjects/JSPromise.swift +++ b/Sources/JavaScriptKit/BasicObjects/JSPromise.swift @@ -1,14 +1,14 @@ /** 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`. + 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. -*/ + 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. + */ public final class JSPromise: JSBridgedClass { /// The underlying JavaScript `Promise` object. public let jsObject: JSObject @@ -18,35 +18,35 @@ 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`. - */ + 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`. - */ + 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) + 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) -> ()) -> ()) { + two closure that your code should call to either resolve or reject this `JSPromise` instance. + */ + 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 @@ -63,19 +63,19 @@ public final class JSPromise: JSBridgedClass { } return .undefined } - self.init(unsafelyWrapping: Self.constructor.new(closure)) + self.init(unsafelyWrapping: Self.constructor!.new(closure)) } 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!) } /** Schedules the `success` closure to be invoked on sucessful completion of `self`. - */ + */ @discardableResult public func then(success: @escaping (JSValue) -> ConvertibleToJSValue) -> JSPromise { let closure = JSOneshotClosure { @@ -85,10 +85,12 @@ public final class JSPromise: JSBridgedClass { } /** Schedules the `success` closure to be invoked on sucessful completion of `self`. - */ + */ @discardableResult - public func then(success: @escaping (JSValue) -> ConvertibleToJSValue, - failure: @escaping (JSValue) -> ConvertibleToJSValue) -> JSPromise { + public func then( + success: @escaping (JSValue) -> ConvertibleToJSValue, + failure: @escaping (JSValue) -> ConvertibleToJSValue + ) -> JSPromise { let successClosure = JSOneshotClosure { success($0[0]).jsValue } @@ -99,7 +101,7 @@ public final class JSPromise: JSBridgedClass { } /** Schedules the `failure` closure to be invoked on rejected completion of `self`. - */ + */ @discardableResult public func `catch`(failure: @escaping (JSValue) -> ConvertibleToJSValue) -> JSPromise { let closure = JSOneshotClosure { @@ -108,11 +110,11 @@ public final class JSPromise: JSBridgedClass { return .init(unsafelyWrapping: jsObject.catch!(closure).object!) } - /** 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 diff --git a/Sources/JavaScriptKit/BasicObjects/JSTypedArray.swift b/Sources/JavaScriptKit/BasicObjects/JSTypedArray.swift index 39ec2aa21..a32c22203 100644 --- a/Sources/JavaScriptKit/BasicObjects/JSTypedArray.swift +++ b/Sources/JavaScriptKit/BasicObjects/JSTypedArray.swift @@ -13,7 +13,7 @@ public protocol TypedArrayElement: ConvertibleToJSValue, ConstructibleFromJSValu /// 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. public class JSTypedArray: JSBridgedClass, ExpressibleByArrayLiteral where Element: TypedArrayElement { - public class var constructor: JSFunction { Element.typedArrayClass } + public class var constructor: JSFunction? { Element.typedArrayClass } public var jsObject: JSObject public subscript(_ index: Int) -> Element { @@ -21,7 +21,7 @@ public class JSTypedArray: JSBridgedClass, ExpressibleByArrayLiteral wh return Element.construct(from: jsObject[index])! } set { - self.jsObject[index] = newValue.jsValue + jsObject[index] = newValue.jsValue } } @@ -30,22 +30,23 @@ public class JSTypedArray: JSBridgedClass, ExpressibleByArrayLiteral wh /// /// - Parameter length: The number of elements that will be allocated. public init(length: Int) { - jsObject = Self.constructor.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(Self.constructor.id, ptr.baseAddress!, Int32(array.count)) + _create_typed_array(Self.constructor!.id, ptr.baseAddress!, Int32(array.count)) } self.init(unsafelyWrapping: JSObject(id: jsArrayRef)) } @@ -80,7 +81,7 @@ public class JSTypedArray: JSBridgedClass, ExpressibleByArrayLiteral wh let rawBuffer = malloc(bytesLength)! defer { free(rawBuffer) } _load_typed_array(jsObject.id, rawBuffer.assumingMemoryBound(to: UInt8.self)) - let length = lengthInBytes / MemoryLayout.size + 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) @@ -105,6 +106,7 @@ extension Int: TypedArrayElement { public static var typedArrayClass: JSFunction = valueForBitWidth(typeName: "Int", bitWidth: Int.bitWidth, when32: JSObject.global.Int32Array).function! } + extension UInt: TypedArrayElement { public static var typedArrayClass: JSFunction = valueForBitWidth(typeName: "UInt", bitWidth: Int.bitWidth, when32: JSObject.global.Uint32Array).function! @@ -113,17 +115,19 @@ extension UInt: TypedArrayElement { extension Int8: TypedArrayElement { public static var typedArrayClass = JSObject.global.Int8Array.function! } + extension UInt8: TypedArrayElement { public static var typedArrayClass = JSObject.global.Uint8Array.function! } public class JSUInt8ClampedArray: JSTypedArray { - public class override var constructor: JSFunction { JSObject.global.Uint8ClampedArray.function! } + override public class var constructor: JSFunction? { JSObject.global.Uint8ClampedArray.function! } } extension Int16: TypedArrayElement { public static var typedArrayClass = JSObject.global.Int16Array.function! } + extension UInt16: TypedArrayElement { public static var typedArrayClass = JSObject.global.Uint16Array.function! } @@ -131,6 +135,7 @@ extension UInt16: TypedArrayElement { extension Int32: TypedArrayElement { public static var typedArrayClass = JSObject.global.Int32Array.function! } + extension UInt32: TypedArrayElement { public static var typedArrayClass = JSObject.global.Uint32Array.function! } @@ -138,6 +143,7 @@ extension UInt32: TypedArrayElement { extension Float32: TypedArrayElement { public static var typedArrayClass = JSObject.global.Float32Array.function! } + extension Float64: TypedArrayElement { public static var typedArrayClass = JSObject.global.Float64Array.function! } diff --git a/Sources/JavaScriptKit/JSBridgedType.swift b/Sources/JavaScriptKit/JSBridgedType.swift index 235d78331..dcc0a3857 100644 --- a/Sources/JavaScriptKit/JSBridgedType.swift +++ b/Sources/JavaScriptKit/JSBridgedType.swift @@ -5,18 +5,18 @@ public protocol JSBridgedType: JSValueCompatible, CustomStringConvertible { init?(from value: JSValue) } -extension JSBridgedType { - public static func construct(from value: JSValue) -> Self? { - Self.init(from: value) +public extension JSBridgedType { + static func construct(from value: JSValue) -> Self? { + Self(from: value) } - public var description: String { jsValue.description } + 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` @@ -27,16 +27,16 @@ public protocol JSBridgedClass: JSBridgedType { init(unsafelyWrapping jsObject: JSObject) } -extension JSBridgedClass { - public var jsValue: JSValue { jsObject.jsValue } +public extension JSBridgedClass { + var jsValue: JSValue { jsObject.jsValue } - public init?(from value: JSValue) { + 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 } + init?(from object: JSObject) { + guard let constructor = Self.constructor, object.isInstanceOf(constructor) else { return nil } self.init(unsafelyWrapping: object) } } From df6651f037d5d0e76884a4d27fe66e1f59760b64 Mon Sep 17 00:00:00 2001 From: Yuta Saito Date: Tue, 17 May 2022 10:03:05 +0900 Subject: [PATCH 025/373] Improve JSKit diagnostics for use-after-free of JSClosure (#195) To mitigate the debugging difficulties, JSKit should provide more information in diagnostics. --- .../Sources/PrimaryTests/UnitTestUtils.swift | 6 ++++++ .../TestSuites/Sources/PrimaryTests/main.swift | 11 +++++++++++ Runtime/src/index.ts | 14 +++++++++----- Runtime/src/types.ts | 4 ++-- .../FundamentalObjects/JSClosure.swift | 18 ++++++++++++------ Sources/JavaScriptKit/Runtime/index.js | 14 +++++++++----- Sources/JavaScriptKit/Runtime/index.mjs | 14 +++++++++----- Sources/JavaScriptKit/XcodeSupport.swift | 2 +- Sources/_CJavaScriptKit/_CJavaScriptKit.c | 9 +++++---- .../_CJavaScriptKit/include/_CJavaScriptKit.h | 5 ++++- 10 files changed, 68 insertions(+), 29 deletions(-) diff --git a/IntegrationTests/TestSuites/Sources/PrimaryTests/UnitTestUtils.swift b/IntegrationTests/TestSuites/Sources/PrimaryTests/UnitTestUtils.swift index ab11bc017..571e0d6a3 100644 --- a/IntegrationTests/TestSuites/Sources/PrimaryTests/UnitTestUtils.swift +++ b/IntegrationTests/TestSuites/Sources/PrimaryTests/UnitTestUtils.swift @@ -94,6 +94,12 @@ func expectString(_ value: JSValue, file: StaticString = #file, line: UInt = #li } } +func expect(_ description: String, _ result: Bool, file: StaticString = #file, line: UInt = #line, column: UInt = #column) throws { + if !result { + throw MessageError(description, 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() diff --git a/IntegrationTests/TestSuites/Sources/PrimaryTests/main.swift b/IntegrationTests/TestSuites/Sources/PrimaryTests/main.swift index a48c6fb00..aede07ced 100644 --- a/IntegrationTests/TestSuites/Sources/PrimaryTests/main.swift +++ b/IntegrationTests/TestSuites/Sources/PrimaryTests/main.swift @@ -240,6 +240,17 @@ try test("Closure Lifetime") { // OneshotClosure won't call fatalError even if it's deallocated before `release` } #endif + +#if JAVASCRIPTKIT_WITHOUT_WEAKREFS + // Check diagnostics of use-after-free + do { + let c1 = JSClosure { $0[0] } + c1.release() + let error = try expectThrow(try evalClosure.throws(c1, JSValue.number(42.0))) as! JSValue + try expect("Error message should contains definition location", error.description.hasSuffix("PrimaryTests/main.swift:247")) + } +#endif + } try test("Host Function Registration") { diff --git a/Runtime/src/index.ts b/Runtime/src/index.ts index 17c53696e..3da0f2e47 100644 --- a/Runtime/src/index.ts +++ b/Runtime/src/index.ts @@ -14,7 +14,7 @@ export class SwiftRuntime { private _instance: WebAssembly.Instance | null; private _memory: Memory | null; private _closureDeallocator: SwiftClosureDeallocator | null; - private version: number = 707; + private version: number = 708; private textDecoder = new TextDecoder("utf-8"); private textEncoder = new TextEncoder(); // Only support utf-8 @@ -68,7 +68,7 @@ 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); for (let index = 0; index < args.length; index++) { @@ -88,12 +88,15 @@ export class SwiftRuntime { const callback_func_ref = this.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; } @@ -371,9 +374,10 @@ export class SwiftRuntime { return obj instanceof constructor; }, - swjs_create_function: (host_func_id: number) => { + 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, args); + this.callHostFunction(host_func_id, line, fileString, args); const func_ref = this.memory.retain(func); this.closureDeallocator?.track(func, func_ref); return func_ref; diff --git a/Runtime/src/types.ts b/Runtime/src/types.ts index d587b2c8a..913837e32 100644 --- a/Runtime/src/types.ts +++ b/Runtime/src/types.ts @@ -14,7 +14,7 @@ export interface ExportedFunctions { argv: pointer, argc: number, callback_func_ref: ref - ): void; + ): bool; swjs_free_host_function(host_func_id: number): void; } @@ -95,7 +95,7 @@ export interface ImportedFunctions { exception_payload2_ptr: pointer ): number; swjs_instanceof(obj_ref: ref, constructor_ref: ref): boolean; - swjs_create_function(host_func_id: number): number; + swjs_create_function(host_func_id: number, line: number, file: ref): number; swjs_create_typed_array( constructor_ref: ref, elementsPtr: pointer, diff --git a/Sources/JavaScriptKit/FundamentalObjects/JSClosure.swift b/Sources/JavaScriptKit/FundamentalObjects/JSClosure.swift index cbd44bd6e..48c4f9e82 100644 --- a/Sources/JavaScriptKit/FundamentalObjects/JSClosure.swift +++ b/Sources/JavaScriptKit/FundamentalObjects/JSClosure.swift @@ -15,13 +15,15 @@ public protocol JSClosureProtocol: JSValueCompatible { public class JSOneshotClosure: JSObject, JSClosureProtocol { private var hostFuncRef: JavaScriptHostFuncRef = 0 - public init(_ body: @escaping ([JSValue]) -> JSValue) { + public init(_ body: @escaping ([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) + id = withExtendedLifetime(JSString(file)) { file in + _create_function(hostFuncRef, line, file.asInternalJSRef()) + } // 3. Retain the given body in static storage by `funcRef`. JSClosure.sharedClosures[hostFuncRef] = (self, { @@ -72,13 +74,15 @@ public class JSClosure: JSObject, JSClosureProtocol { }) } - public init(_ body: @escaping ([JSValue]) -> JSValue) { + public init(_ body: @escaping ([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) + id = withExtendedLifetime(JSString(file)) { file in + _create_function(hostFuncRef, line, file.asInternalJSRef()) + } // 3. Retain the given body in static storage by `funcRef`. Self.sharedClosures[hostFuncRef] = (self, body) @@ -128,19 +132,21 @@ 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, _ callbackFuncRef: JavaScriptObjectRef -) { +) -> Bool { guard let (_, hostFunc) = JSClosure.sharedClosures[hostFuncRef] else { - fatalError("The function was already released") + return true } let arguments = UnsafeBufferPointer(start: argv, count: Int(argc)).map(\.jsValue) let result = hostFunc(arguments) let callbackFuncRef = JSFunction(id: callbackFuncRef) _ = callbackFuncRef(result) + return false } diff --git a/Sources/JavaScriptKit/Runtime/index.js b/Sources/JavaScriptKit/Runtime/index.js index 68f1c433c..8b6edbe3f 100644 --- a/Sources/JavaScriptKit/Runtime/index.js +++ b/Sources/JavaScriptKit/Runtime/index.js @@ -190,7 +190,7 @@ class SwiftRuntime { constructor() { - this.version = 707; + this.version = 708; this.textDecoder = new TextDecoder("utf-8"); this.textEncoder = new TextEncoder(); // Only support utf-8 /** @deprecated Use `wasmImports` instead */ @@ -318,9 +318,10 @@ const constructor = this.memory.getObject(constructor_ref); return obj instanceof constructor; }, - swjs_create_function: (host_func_id) => { + swjs_create_function: (host_func_id, line, file) => { var _a; - const func = (...args) => this.callHostFunction(host_func_id, args); + 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; @@ -393,7 +394,7 @@ } return this._closureDeallocator; } - callHostFunction(host_func_id, args) { + callHostFunction(host_func_id, line, file, args) { const argc = args.length; const argv = this.exports.swjs_prepare_host_function_call(argc); for (let index = 0; index < args.length; index++) { @@ -406,7 +407,10 @@ const callback_func_ref = this.memory.retain((result) => { output = result; }); - this.exports.swjs_call_host_function(host_func_id, argv, argc, callback_func_ref); + 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; } diff --git a/Sources/JavaScriptKit/Runtime/index.mjs b/Sources/JavaScriptKit/Runtime/index.mjs index 1874a8fee..465752eb5 100644 --- a/Sources/JavaScriptKit/Runtime/index.mjs +++ b/Sources/JavaScriptKit/Runtime/index.mjs @@ -184,7 +184,7 @@ class Memory { class SwiftRuntime { constructor() { - this.version = 707; + this.version = 708; this.textDecoder = new TextDecoder("utf-8"); this.textEncoder = new TextEncoder(); // Only support utf-8 /** @deprecated Use `wasmImports` instead */ @@ -312,9 +312,10 @@ class SwiftRuntime { const constructor = this.memory.getObject(constructor_ref); return obj instanceof constructor; }, - swjs_create_function: (host_func_id) => { + swjs_create_function: (host_func_id, line, file) => { var _a; - const func = (...args) => this.callHostFunction(host_func_id, args); + 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; @@ -387,7 +388,7 @@ class SwiftRuntime { } return this._closureDeallocator; } - callHostFunction(host_func_id, args) { + callHostFunction(host_func_id, line, file, args) { const argc = args.length; const argv = this.exports.swjs_prepare_host_function_call(argc); for (let index = 0; index < args.length; index++) { @@ -400,7 +401,10 @@ class SwiftRuntime { const callback_func_ref = this.memory.retain((result) => { output = result; }); - this.exports.swjs_call_host_function(host_func_id, argv, argc, callback_func_ref); + 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; } diff --git a/Sources/JavaScriptKit/XcodeSupport.swift b/Sources/JavaScriptKit/XcodeSupport.swift index 4cde273f3..a3c8aeb1a 100644 --- a/Sources/JavaScriptKit/XcodeSupport.swift +++ b/Sources/JavaScriptKit/XcodeSupport.swift @@ -91,7 +91,7 @@ import _CJavaScriptKit _: JavaScriptObjectRef, _: JavaScriptObjectRef ) -> Bool { fatalError() } - func _create_function(_: JavaScriptHostFuncRef) -> JavaScriptObjectRef { fatalError() } + func _create_function(_: JavaScriptHostFuncRef, _: UInt32, _: JavaScriptObjectRef) -> JavaScriptObjectRef { fatalError() } func _create_typed_array( _: JavaScriptObjectRef, _: UnsafePointer, diff --git a/Sources/_CJavaScriptKit/_CJavaScriptKit.c b/Sources/_CJavaScriptKit/_CJavaScriptKit.c index f2f03b82d..0bcc5eaca 100644 --- a/Sources/_CJavaScriptKit/_CJavaScriptKit.c +++ b/Sources/_CJavaScriptKit/_CJavaScriptKit.c @@ -1,17 +1,18 @@ #include "_CJavaScriptKit.h" #include +#include #if __wasm32__ -void _call_host_function_impl(const JavaScriptHostFuncRef host_func_ref, +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); @@ -36,7 +37,7 @@ void swjs_cleanup_host_function_call(void *argv_buffer) { /// this and `SwiftRuntime.version` in `./Runtime/src/index.ts`. __attribute__((export_name("swjs_library_version"))) int swjs_library_version(void) { - return 707; + return 708; } int _library_features(void); diff --git a/Sources/_CJavaScriptKit/include/_CJavaScriptKit.h b/Sources/_CJavaScriptKit/include/_CJavaScriptKit.h index c3b56c14d..fb07d1e09 100644 --- a/Sources/_CJavaScriptKit/include/_CJavaScriptKit.h +++ b/Sources/_CJavaScriptKit/include/_CJavaScriptKit.h @@ -268,10 +268,13 @@ extern bool _instanceof(const JavaScriptObjectRef obj, /// 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); +extern JavaScriptObjectRef _create_function(const JavaScriptHostFuncRef host_func_id, + unsigned int line, JavaScriptObjectRef file); /// Instantiate a new `TypedArray` object with given elements /// This is used to provide an efficient way to create `TypedArray`. From 2d7bc960eed438dce7355710ece43fa004bbb3ac Mon Sep 17 00:00:00 2001 From: Max Desiatov Date: Thu, 19 May 2022 14:17:16 +0100 Subject: [PATCH 026/373] Bump version to 0.15.0, update `CHANGELOG.md` (#196) * Bump version to 0.15.0, update `CHANGELOG.md` * Apply suggestions from code review Co-authored-by: Jed Fox Co-authored-by: Yuta Saito --- CHANGELOG.md | 33 +++++++++++++++++++++++++++++++++ Example/package-lock.json | 2 +- package-lock.json | 4 ++-- package.json | 2 +- 4 files changed, 37 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 34864d1d8..62f7fff55 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,36 @@ +# 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/). diff --git a/Example/package-lock.json b/Example/package-lock.json index 65a52a803..8d8358675 100644 --- a/Example/package-lock.json +++ b/Example/package-lock.json @@ -21,7 +21,7 @@ }, "..": { "name": "javascript-kit-swift", - "version": "0.14.0", + "version": "0.15.0", "license": "MIT", "devDependencies": { "@rollup/plugin-typescript": "^8.3.1", diff --git a/package-lock.json b/package-lock.json index 9d0372017..809b03030 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "javascript-kit-swift", - "version": "0.14.0", + "version": "0.15.0", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "javascript-kit-swift", - "version": "0.14.0", + "version": "0.15.0", "license": "MIT", "devDependencies": { "@rollup/plugin-typescript": "^8.3.1", diff --git a/package.json b/package.json index e24c39e27..90486f010 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "javascript-kit-swift", - "version": "0.14.0", + "version": "0.15.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", From f1ef51771550469c653f89060f8ad5a47b04ee55 Mon Sep 17 00:00:00 2001 From: Jed Fox Date: Sun, 5 Jun 2022 07:58:25 -0700 Subject: [PATCH 027/373] Add async closure support (#159) --- .../ConcurrencyTests/UnitTestUtils.swift | 12 +++ .../Sources/ConcurrencyTests/main.swift | 81 ++++++++++++++++- .../BasicObjects/JSPromise.swift | 89 ++++++++++++------- .../FundamentalObjects/JSClosure.swift | 35 ++++++++ 4 files changed, 186 insertions(+), 31 deletions(-) diff --git a/IntegrationTests/TestSuites/Sources/ConcurrencyTests/UnitTestUtils.swift b/IntegrationTests/TestSuites/Sources/ConcurrencyTests/UnitTestUtils.swift index 1557764a2..40c3165da 100644 --- a/IntegrationTests/TestSuites/Sources/ConcurrencyTests/UnitTestUtils.swift +++ b/IntegrationTests/TestSuites/Sources/ConcurrencyTests/UnitTestUtils.swift @@ -41,6 +41,18 @@ struct MessageError: Error { } } +func expectGTE( + _ lhs: T, _ rhs: T, + file: StaticString = #file, line: UInt = #line, column: UInt = #column +) throws { + if lhs < rhs { + throw MessageError( + "Expected \(lhs) to be greater than or equal to \(rhs)", + file: file, line: line, column: column + ) + } +} + func expectEqual( _ lhs: T, _ rhs: T, file: StaticString = #file, line: UInt = #line, column: UInt = #column diff --git a/IntegrationTests/TestSuites/Sources/ConcurrencyTests/main.swift b/IntegrationTests/TestSuites/Sources/ConcurrencyTests/main.swift index 5447032fe..3bce2aa8d 100644 --- a/IntegrationTests/TestSuites/Sources/ConcurrencyTests/main.swift +++ b/IntegrationTests/TestSuites/Sources/ConcurrencyTests/main.swift @@ -62,7 +62,7 @@ func entrypoint() async throws { 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 expectGTE(diff, 2) } try await asyncTest("Job reordering based on priority") { @@ -97,6 +97,85 @@ func entrypoint() async throws { _ = await (t3.value, t4.value, t5.value) try expectEqual(context.completed, ["t4", "t3", "t5"]) } + + try await asyncTest("Async JSClosure") { + let delayClosure = JSClosure.async { _ -> JSValue in + try await Task.sleep(nanoseconds: 2_000_000_000) + return JSValue.number(3) + } + let delayObject = JSObject.global.Object.function!.new() + delayObject.closure = delayClosure.jsValue + + let start = time(nil) + let promise = JSPromise(from: delayObject.closure!()) + try expectNotNil(promise) + let result = try await promise!.value + let diff = difftime(time(nil), start) + try expectGTE(diff, 2) + try expectEqual(result, .number(3)) + } + + try await asyncTest("Async JSPromise: then") { + let promise = JSPromise { resolve in + _ = JSObject.global.setTimeout!( + JSClosure { _ in + resolve(.success(JSValue.number(3))) + return .undefined + }.jsValue, + 1_000 + ) + } + let promise2 = promise.then { result in + try await Task.sleep(nanoseconds: 1_000_000_000) + return String(result.number!) + } + let start = time(nil) + let result = try await promise2.value + let diff = difftime(time(nil), start) + try expectGTE(diff, 2) + try expectEqual(result, .string("3.0")) + } + + try await asyncTest("Async JSPromise: then(success:failure:)") { + let promise = JSPromise { resolve in + _ = JSObject.global.setTimeout!( + JSClosure { _ in + resolve(.failure(JSError(message: "test").jsValue)) + return .undefined + }.jsValue, + 1_000 + ) + } + let promise2 = promise.then { _ in + throw JSError(message: "should not succeed") + } failure: { err in + return err + } + let result = try await promise2.value + try expectEqual(result.object?.message, .string("test")) + } + + try await asyncTest("Async JSPromise: catch") { + let promise = JSPromise { resolve in + _ = JSObject.global.setTimeout!( + JSClosure { _ in + resolve(.failure(JSError(message: "test").jsValue)) + return .undefined + }.jsValue, + 1_000 + ) + } + let promise2 = promise.catch { err in + try await Task.sleep(nanoseconds: 1_000_000_000) + return err + } + let start = time(nil) + let result = try await promise2.value + let diff = difftime(time(nil), start) + try expectGTE(diff, 2) + try expectEqual(result.object?.message, .string("test")) + } + // 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. diff --git a/Sources/JavaScriptKit/BasicObjects/JSPromise.swift b/Sources/JavaScriptKit/BasicObjects/JSPromise.swift index 81a124447..4b366d812 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 @@ -27,25 +17,27 @@ public final class JSPromise: JSBridgedClass { 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(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. - */ + /// 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 @@ -74,8 +66,7 @@ public final class JSPromise: JSBridgedClass { self.init(unsafelyWrapping: Self.constructor!.reject!(reason).object!) } - /** Schedules the `success` closure to be invoked on sucessful completion of `self`. - */ + /// Schedules the `success` closure to be invoked on successful completion of `self`. @discardableResult public func then(success: @escaping (JSValue) -> ConvertibleToJSValue) -> JSPromise { let closure = JSOneshotClosure { @@ -84,8 +75,19 @@ public final class JSPromise: JSBridgedClass { 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) 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 (JSValue) -> ConvertibleToJSValue, @@ -100,8 +102,24 @@ public final class JSPromise: JSBridgedClass { 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 then(success: @escaping (JSValue) async throws -> ConvertibleToJSValue, + failure: @escaping (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 (JSValue) -> ConvertibleToJSValue) -> JSPromise { let closure = JSOneshotClosure { @@ -110,9 +128,20 @@ public final class JSPromise: JSBridgedClass { return .init(unsafelyWrapping: jsObject.catch!(closure).object!) } - /** Schedules the `failure` closure to be invoked on either successful or rejected completion of - `self`. - */ + #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: @escaping (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`. @discardableResult public func finally(successOrFailure: @escaping () -> Void) -> JSPromise { let closure = JSOneshotClosure { _ in diff --git a/Sources/JavaScriptKit/FundamentalObjects/JSClosure.swift b/Sources/JavaScriptKit/FundamentalObjects/JSClosure.swift index 48c4f9e82..0bff81403 100644 --- a/Sources/JavaScriptKit/FundamentalObjects/JSClosure.swift +++ b/Sources/JavaScriptKit/FundamentalObjects/JSClosure.swift @@ -32,6 +32,13 @@ public class JSOneshotClosure: JSObject, JSClosureProtocol { }) } + #if compiler(>=5.5) + @available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) + public static func async(_ body: @escaping ([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() { @@ -88,6 +95,13 @@ public class JSClosure: JSObject, JSClosureProtocol { Self.sharedClosures[hostFuncRef] = (self, body) } + #if compiler(>=5.5) + @available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) + public static func async(_ body: @escaping ([JSValue]) async throws -> JSValue) -> JSClosure { + JSClosure(makeAsyncClosure(body)) + } + #endif + #if JAVASCRIPTKIT_WITHOUT_WEAKREFS deinit { guard isReleased else { @@ -97,6 +111,27 @@ public class JSClosure: JSObject, JSClosureProtocol { #endif } +#if compiler(>=5.5) +@available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) +private func makeAsyncClosure(_ body: @escaping ([JSValue]) async throws -> JSValue) -> (([JSValue]) -> JSValue) { + { arguments in + JSPromise { resolver in + Task { + do { + let result = try await body(arguments) + resolver(.success(result)) + } catch { + if let jsError = error as? JSError { + resolver(.failure(jsError.jsValue)) + } else { + resolver(.failure(JSError(message: String(describing: error)).jsValue)) + } + } + } + }.jsValue() + } +} +#endif // MARK: - `JSClosure` mechanism note // From 901de662207983d0c46d6b60d5b32fe50607275b Mon Sep 17 00:00:00 2001 From: Yuta Saito Date: Tue, 19 Jul 2022 18:08:44 +0900 Subject: [PATCH 028/373] Add async JSPromise.result property (#200) --- .../Sources/ConcurrencyTests/main.swift | 2 ++ .../JavaScriptEventLoop.swift | 18 ++++++++++++++++++ 2 files changed, 20 insertions(+) diff --git a/IntegrationTests/TestSuites/Sources/ConcurrencyTests/main.swift b/IntegrationTests/TestSuites/Sources/ConcurrencyTests/main.swift index 3bce2aa8d..1e48f459f 100644 --- a/IntegrationTests/TestSuites/Sources/ConcurrencyTests/main.swift +++ b/IntegrationTests/TestSuites/Sources/ConcurrencyTests/main.swift @@ -32,6 +32,7 @@ func entrypoint() async throws { resolve(.success(1)) }) try await expectEqual(p.value, 1) + try await expectEqual(p.result, .success(.number(1))) } try await asyncTest("await rejected Promise") { @@ -41,6 +42,7 @@ func entrypoint() async throws { let error = try await expectAsyncThrow(await p.value) let jsValue = try expectCast(error, to: JSValue.self) try expectEqual(jsValue, 3) + try await expectEqual(p.result, .failure(.number(3))) } try await asyncTest("Continuation") { diff --git a/Sources/JavaScriptEventLoop/JavaScriptEventLoop.swift b/Sources/JavaScriptEventLoop/JavaScriptEventLoop.swift index 53008c5e4..c6d10209a 100644 --- a/Sources/JavaScriptEventLoop/JavaScriptEventLoop.swift +++ b/Sources/JavaScriptEventLoop/JavaScriptEventLoop.swift @@ -116,6 +116,24 @@ public extension JSPromise { } } } + + /// Wait for the promise to complete, returning its result or exception as a Result. + var result: Result { + get async { + await withUnsafeContinuation { [self] continuation in + self.then( + success: { + continuation.resume(returning: .success($0)) + return JSValue.undefined + }, + failure: { + continuation.resume(returning: .failure($0)) + return JSValue.undefined + } + ) + } + } + } } #endif From 93f2dd568191c6f26d406386d3cd94ea69e72191 Mon Sep 17 00:00:00 2001 From: Yuta Saito Date: Tue, 19 Jul 2022 18:47:16 +0900 Subject: [PATCH 029/373] Test with uwasi implementation (#198) --- .github/workflows/test.yml | 2 ++ IntegrationTests/lib.js | 16 ++++++++++++++++ IntegrationTests/package-lock.json | 29 +++++++++++++++++++++++------ IntegrationTests/package.json | 1 + 4 files changed, 42 insertions(+), 6 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index e2d2e7d18..7a6b4c422 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -20,6 +20,8 @@ jobs: - { os: ubuntu-20.04, toolchain: wasm-5.6.0-RELEASE, wasi-backend: Node } - { os: ubuntu-20.04, toolchain: wasm-5.5.0-RELEASE, wasi-backend: Wasmer } - { os: ubuntu-20.04, toolchain: wasm-5.6.0-RELEASE, wasi-backend: Wasmer } + - { os: ubuntu-20.04, toolchain: wasm-5.5.0-RELEASE, wasi-backend: MicroWASI } + - { os: ubuntu-20.04, toolchain: wasm-5.6.0-RELEASE, wasi-backend: MicroWASI } runs-on: ${{ matrix.entry.os }} steps: diff --git a/IntegrationTests/lib.js b/IntegrationTests/lib.js index ed5c5b493..5ba5df6a3 100644 --- a/IntegrationTests/lib.js +++ b/IntegrationTests/lib.js @@ -2,6 +2,7 @@ const SwiftRuntime = require("javascript-kit-swift").SwiftRuntime; const WasmerWASI = require("@wasmer/wasi").WASI; const WasmFs = require("@wasmer/wasmfs").WasmFs; const NodeWASI = require("wasi").WASI; +const { WASI: MicroWASI, useAll } = require("uwasi"); const promisify = require("util").promisify; const fs = require("fs"); @@ -43,6 +44,21 @@ const WASI = { } } }, + MicroWASI: () => { + const wasi = new MicroWASI({ + args: [], + env: {}, + features: [useAll()], + }) + + return { + wasiImport: wasi.wasiImport, + start(instance) { + wasi.initialize(instance); + instance.exports.main(); + } + } + }, Node: () => { const wasi = new NodeWASI({ args: [], diff --git a/IntegrationTests/package-lock.json b/IntegrationTests/package-lock.json index f2e53f99f..cc6ed8de9 100644 --- a/IntegrationTests/package-lock.json +++ b/IntegrationTests/package-lock.json @@ -7,16 +7,20 @@ "dependencies": { "@wasmer/wasi": "^0.12.0", "@wasmer/wasmfs": "^0.12.0", - "javascript-kit-swift": "file:.." + "javascript-kit-swift": "file:..", + "uwasi": "^1.0.0" } }, "..": { "name": "javascript-kit-swift", - "version": "0.12.0", + "version": "0.14.0", "license": "MIT", "devDependencies": { - "prettier": "2.1.2", - "typescript": "^4.4.2" + "@rollup/plugin-typescript": "^8.3.1", + "prettier": "2.6.1", + "rollup": "^2.70.0", + "tslib": "^2.3.1", + "typescript": "^4.6.3" } }, "../node_modules/prettier": { @@ -214,6 +218,11 @@ "version": "1.0.2", "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=" }, + "node_modules/uwasi": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/uwasi/-/uwasi-1.0.0.tgz", + "integrity": "sha512-xnjYEegIsUDh7aXnT6s+pNK79adEQs5R+T+fds/fFdCEtoKFkH3ngwbp3jAJjB91VfPgOVUKIH+fNbg6Om8xAw==" + }, "node_modules/wrappy": { "version": "1.0.2", "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=" @@ -298,8 +307,11 @@ "javascript-kit-swift": { "version": "file:..", "requires": { - "prettier": "2.1.2", - "typescript": "^4.4.2" + "@rollup/plugin-typescript": "^8.3.1", + "prettier": "2.6.1", + "rollup": "^2.70.0", + "tslib": "^2.3.1", + "typescript": "^4.6.3" }, "dependencies": { "prettier": { @@ -387,6 +399,11 @@ "version": "1.0.2", "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=" }, + "uwasi": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/uwasi/-/uwasi-1.0.0.tgz", + "integrity": "sha512-xnjYEegIsUDh7aXnT6s+pNK79adEQs5R+T+fds/fFdCEtoKFkH3ngwbp3jAJjB91VfPgOVUKIH+fNbg6Om8xAw==" + }, "wrappy": { "version": "1.0.2", "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=" diff --git a/IntegrationTests/package.json b/IntegrationTests/package.json index 678ecec49..3458b7385 100644 --- a/IntegrationTests/package.json +++ b/IntegrationTests/package.json @@ -3,6 +3,7 @@ "dependencies": { "@wasmer/wasi": "^0.12.0", "@wasmer/wasmfs": "^0.12.0", + "uwasi": "^1.0.0", "javascript-kit-swift": "file:.." } } From ac92be3b35f13000fdc820cea5e7ec95e952c880 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sun, 24 Jul 2022 08:24:04 +0000 Subject: [PATCH 030/373] Bump terser from 5.10.0 to 5.14.2 in /Example (#201) --- Example/package-lock.json | 150 +++++++++++++++++++++++++++++--------- 1 file changed, 117 insertions(+), 33 deletions(-) diff --git a/Example/package-lock.json b/Example/package-lock.json index 8d8358675..6955ec2bd 100644 --- a/Example/package-lock.json +++ b/Example/package-lock.json @@ -63,6 +63,64 @@ "node": ">=10.0.0" } }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.2.tgz", + "integrity": "sha512-mh65xKQAzI6iBcFzwv28KVWSmCkdRBWoOh+bYQGW3+6OZvbbN3TqMGo5hqYxQniRcH9F2VZIoJCm4pa3BPDK/A==", + "dev": true, + "dependencies": { + "@jridgewell/set-array": "^1.0.1", + "@jridgewell/sourcemap-codec": "^1.4.10", + "@jridgewell/trace-mapping": "^0.3.9" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.0.tgz", + "integrity": "sha512-F2msla3tad+Mfht5cJq7LSXcdudKTWCVYUgw6pLFOOHSTtZlj6SWNYAp+AhuqLmWdBO2X5hPrLcu8cVP8fy28w==", + "dev": true, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/set-array": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.1.2.tgz", + "integrity": "sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw==", + "dev": true, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/source-map": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.2.tgz", + "integrity": "sha512-m7O9o2uR8k2ObDysZYzdfhb08VuEml5oWGiosa1VdaPZ/A6QyPkAJuwN0Q1lhULOf6B7MtQmHENS743hWtCrgw==", + "dev": true, + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.0", + "@jridgewell/trace-mapping": "^0.3.9" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.4.14", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.14.tgz", + "integrity": "sha512-XPSJHWmi394fuUuzDnGz1wiKqWfo1yXecHQMRf2l6hztTO+nPru658AyDngaBe7isIxEkRsPR3FZh+s7iVa4Uw==", + "dev": true + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.14", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.14.tgz", + "integrity": "sha512-bJWEfQ9lPTvm3SneWwRFVLzrh6nhjwqw7TUFFBEMzwvg7t7PCDenf2lDwqo4NQXzdpgBXyFgDWnQA+2vkruksQ==", + "dev": true, + "dependencies": { + "@jridgewell/resolve-uri": "^3.0.3", + "@jridgewell/sourcemap-codec": "^1.4.10" + } + }, "node_modules/@nodelib/fs.scandir": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", @@ -3242,13 +3300,14 @@ } }, "node_modules/terser": { - "version": "5.10.0", - "resolved": "https://registry.npmjs.org/terser/-/terser-5.10.0.tgz", - "integrity": "sha512-AMmF99DMfEDiRJfxfY5jj5wNH/bYO09cniSqhfoyxc8sFoYIgkJy86G04UoZU5VjlpnplVu0K6Tx6E9b5+DlHA==", + "version": "5.14.2", + "resolved": "https://registry.npmjs.org/terser/-/terser-5.14.2.tgz", + "integrity": "sha512-oL0rGeM/WFQCUd0y2QrWxYnq7tfSuKBiqTjRPWrRgB46WD/kiwHwF8T23z78H6Q6kGCuuHcPB+KULHRdxvVGQA==", "dev": true, "dependencies": { + "@jridgewell/source-map": "^0.3.2", + "acorn": "^8.5.0", "commander": "^2.20.0", - "source-map": "~0.7.2", "source-map-support": "~0.5.20" }, "bin": { @@ -3256,14 +3315,6 @@ }, "engines": { "node": ">=10" - }, - "peerDependencies": { - "acorn": "^8.5.0" - }, - "peerDependenciesMeta": { - "acorn": { - "optional": true - } } }, "node_modules/terser-webpack-plugin": { @@ -3300,15 +3351,6 @@ } } }, - "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==", @@ -3817,6 +3859,55 @@ "integrity": "sha512-6nFkfkmSeV/rqSaS4oWHgmpnYw194f6hmWF5is6b0J1naJZoiD0NTc9AiUwPHvWsowkjuHErCZT1wa0jg+BLIA==", "dev": true }, + "@jridgewell/gen-mapping": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.2.tgz", + "integrity": "sha512-mh65xKQAzI6iBcFzwv28KVWSmCkdRBWoOh+bYQGW3+6OZvbbN3TqMGo5hqYxQniRcH9F2VZIoJCm4pa3BPDK/A==", + "dev": true, + "requires": { + "@jridgewell/set-array": "^1.0.1", + "@jridgewell/sourcemap-codec": "^1.4.10", + "@jridgewell/trace-mapping": "^0.3.9" + } + }, + "@jridgewell/resolve-uri": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.0.tgz", + "integrity": "sha512-F2msla3tad+Mfht5cJq7LSXcdudKTWCVYUgw6pLFOOHSTtZlj6SWNYAp+AhuqLmWdBO2X5hPrLcu8cVP8fy28w==", + "dev": true + }, + "@jridgewell/set-array": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.1.2.tgz", + "integrity": "sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw==", + "dev": true + }, + "@jridgewell/source-map": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.2.tgz", + "integrity": "sha512-m7O9o2uR8k2ObDysZYzdfhb08VuEml5oWGiosa1VdaPZ/A6QyPkAJuwN0Q1lhULOf6B7MtQmHENS743hWtCrgw==", + "dev": true, + "requires": { + "@jridgewell/gen-mapping": "^0.3.0", + "@jridgewell/trace-mapping": "^0.3.9" + } + }, + "@jridgewell/sourcemap-codec": { + "version": "1.4.14", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.14.tgz", + "integrity": "sha512-XPSJHWmi394fuUuzDnGz1wiKqWfo1yXecHQMRf2l6hztTO+nPru658AyDngaBe7isIxEkRsPR3FZh+s7iVa4Uw==", + "dev": true + }, + "@jridgewell/trace-mapping": { + "version": "0.3.14", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.14.tgz", + "integrity": "sha512-bJWEfQ9lPTvm3SneWwRFVLzrh6nhjwqw7TUFFBEMzwvg7t7PCDenf2lDwqo4NQXzdpgBXyFgDWnQA+2vkruksQ==", + "dev": true, + "requires": { + "@jridgewell/resolve-uri": "^3.0.3", + "@jridgewell/sourcemap-codec": "^1.4.10" + } + }, "@nodelib/fs.scandir": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", @@ -6294,22 +6385,15 @@ } }, "terser": { - "version": "5.10.0", - "resolved": "https://registry.npmjs.org/terser/-/terser-5.10.0.tgz", - "integrity": "sha512-AMmF99DMfEDiRJfxfY5jj5wNH/bYO09cniSqhfoyxc8sFoYIgkJy86G04UoZU5VjlpnplVu0K6Tx6E9b5+DlHA==", + "version": "5.14.2", + "resolved": "https://registry.npmjs.org/terser/-/terser-5.14.2.tgz", + "integrity": "sha512-oL0rGeM/WFQCUd0y2QrWxYnq7tfSuKBiqTjRPWrRgB46WD/kiwHwF8T23z78H6Q6kGCuuHcPB+KULHRdxvVGQA==", "dev": true, "requires": { + "@jridgewell/source-map": "^0.3.2", + "acorn": "^8.5.0", "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": { From 8478665fcb3a21dfb202055770e871a977d96ab7 Mon Sep 17 00:00:00 2001 From: Yuta Saito Date: Mon, 1 Aug 2022 00:29:58 +0900 Subject: [PATCH 031/373] Add diagnostics for those who build with WASI command line ABI (#202) --- Runtime/src/index.ts | 8 ++++++++ Sources/JavaScriptKit/Runtime/index.js | 6 ++++++ Sources/JavaScriptKit/Runtime/index.mjs | 6 ++++++ 3 files changed, 20 insertions(+) diff --git a/Runtime/src/index.ts b/Runtime/src/index.ts index 3da0f2e47..77264223c 100644 --- a/Runtime/src/index.ts +++ b/Runtime/src/index.ts @@ -27,6 +27,14 @@ export class SwiftRuntime { 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. diff --git a/Sources/JavaScriptKit/Runtime/index.js b/Sources/JavaScriptKit/Runtime/index.js index 8b6edbe3f..eb55ebc58 100644 --- a/Sources/JavaScriptKit/Runtime/index.js +++ b/Sources/JavaScriptKit/Runtime/index.js @@ -365,6 +365,12 @@ } 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}`); diff --git a/Sources/JavaScriptKit/Runtime/index.mjs b/Sources/JavaScriptKit/Runtime/index.mjs index 465752eb5..8bd8043d9 100644 --- a/Sources/JavaScriptKit/Runtime/index.mjs +++ b/Sources/JavaScriptKit/Runtime/index.mjs @@ -359,6 +359,12 @@ class SwiftRuntime { } 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}`); From 36c672b18a5bcde6f4c52c3ebf1b7f4627382de7 Mon Sep 17 00:00:00 2001 From: Yuta Saito Date: Fri, 12 Aug 2022 22:50:17 +0900 Subject: [PATCH 032/373] Refine benchmark suite (#203) * Add several benchmark suite to find our bottleneck * Perform benchmark with release configuration * Add primitive conversion benchmark cases --- IntegrationTests/TestSuites/Package.swift | 2 +- .../Sources/BenchmarkTests/Benchmark.swift | 4 +- .../Sources/BenchmarkTests/main.swift | 69 ++++++++++++++++--- .../Sources/CHelpers/include/helpers.h | 5 ++ IntegrationTests/bin/benchmark-tests.js | 46 ++++++++++--- IntegrationTests/lib.js | 4 ++ Makefile | 4 +- 7 files changed, 111 insertions(+), 23 deletions(-) diff --git a/IntegrationTests/TestSuites/Package.swift b/IntegrationTests/TestSuites/Package.swift index 297fa838a..fac27db31 100644 --- a/IntegrationTests/TestSuites/Package.swift +++ b/IntegrationTests/TestSuites/Package.swift @@ -35,6 +35,6 @@ let package = Package( .product(name: "JavaScriptEventLoop", package: "JavaScriptKit"), ] ), - .target(name: "BenchmarkTests", dependencies: ["JavaScriptKit"]), + .target(name: "BenchmarkTests", dependencies: ["JavaScriptKit", "CHelpers"]), ] ) diff --git a/IntegrationTests/TestSuites/Sources/BenchmarkTests/Benchmark.swift b/IntegrationTests/TestSuites/Sources/BenchmarkTests/Benchmark.swift index 3cebb9d07..4562898fb 100644 --- a/IntegrationTests/TestSuites/Sources/BenchmarkTests/Benchmark.swift +++ b/IntegrationTests/TestSuites/Sources/BenchmarkTests/Benchmark.swift @@ -8,10 +8,10 @@ class Benchmark { let title: String let runner = JSObject.global.benchmarkRunner.function! - func testSuite(_ name: String, _ body: @escaping () -> Void) { + func testSuite(_ name: String, _ body: @escaping (Int) -> Void) { let jsBody = JSClosure { arguments -> JSValue in let iteration = Int(arguments[0].number!) - for _ in 0 ..< iteration { body() } + body(iteration) return .undefined } runner("\(title)/\(name)", jsBody) diff --git a/IntegrationTests/TestSuites/Sources/BenchmarkTests/main.swift b/IntegrationTests/TestSuites/Sources/BenchmarkTests/main.swift index df11f6f14..2803f0137 100644 --- a/IntegrationTests/TestSuites/Sources/BenchmarkTests/main.swift +++ b/IntegrationTests/TestSuites/Sources/BenchmarkTests/main.swift @@ -1,22 +1,75 @@ import JavaScriptKit +import CHelpers let serialization = Benchmark("Serialization") +let noopFunction = JSObject.global.noopFunction.function! + +serialization.testSuite("JavaScript function call through Wasm import") { n in + for _ in 0 ..< n { + benchmark_helper_noop() + } +} + +serialization.testSuite("JavaScript function call through Wasm import with int") { n in + for _ in 0 ..< n { + benchmark_helper_noop_with_int(42) + } +} + +serialization.testSuite("JavaScript function call from Swift") { n in + for _ in 0 ..< n { + _ = noopFunction() + } +} + let swiftInt: Double = 42 -serialization.testSuite("Swift Int to JavaScript") { +serialization.testSuite("Swift Int to JavaScript with assignment") { n in + let jsNumber = JSValue.number(swiftInt) + let object = JSObject.global + let key = JSString("numberValue") + for _ in 0 ..< n { + object[key] = jsNumber + } +} + +serialization.testSuite("Swift Int to JavaScript with call") { n in let jsNumber = JSValue.number(swiftInt) + for _ in 0 ..< n { + _ = noopFunction(jsNumber) + } +} + +serialization.testSuite("JavaScript Number to Swift Int") { n in let object = JSObject.global - for i in 0 ..< 100 { - object["numberValue\(i)"] = jsNumber + let key = JSString("jsNumber") + for _ in 0 ..< n { + _ = object[key].number } } let swiftString = "Hello, world" -serialization.testSuite("Swift String to JavaScript") { +serialization.testSuite("Swift String to JavaScript with assignment") { n in let jsString = JSValue.string(swiftString) let object = JSObject.global - for i in 0 ..< 100 { - object["stringValue\(i)"] = jsString + let key = JSString("stringValue") + for _ in 0 ..< n { + object[key] = jsString + } +} + +serialization.testSuite("Swift String to JavaScript with call") { n in + let jsString = JSValue.string(swiftString) + for _ in 0 ..< n { + _ = noopFunction(jsString) + } +} + +serialization.testSuite("JavaScript String to Swift String") { n in + let object = JSObject.global + let key = JSString("jsString") + for _ in 0 ..< n { + _ = object[key].string } } @@ -25,8 +78,8 @@ 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 { +objectHeap.testSuite("Increment and decrement RC") { n in + for _ in 0 ..< n { _ = global.objectHeapDummy } } diff --git a/IntegrationTests/TestSuites/Sources/CHelpers/include/helpers.h b/IntegrationTests/TestSuites/Sources/CHelpers/include/helpers.h index c5505a5a4..dea7a96d4 100644 --- a/IntegrationTests/TestSuites/Sources/CHelpers/include/helpers.h +++ b/IntegrationTests/TestSuites/Sources/CHelpers/include/helpers.h @@ -3,3 +3,8 @@ /// @param pages Number of memory pages to increase memory by. int growMemory(int pages); +__attribute__((__import_module__("benchmark_helper"), __import_name__("noop"))) +extern void benchmark_helper_noop(void); + +__attribute__((__import_module__("benchmark_helper"), __import_name__("noop_with_int"))) +extern void benchmark_helper_noop_with_int(int); diff --git a/IntegrationTests/bin/benchmark-tests.js b/IntegrationTests/bin/benchmark-tests.js index daf5a5e1b..424ce8199 100644 --- a/IntegrationTests/bin/benchmark-tests.js +++ b/IntegrationTests/bin/benchmark-tests.js @@ -1,41 +1,67 @@ const { startWasiTask } = require("../lib"); const { performance } = require("perf_hooks"); +const SAMPLE_ITERATION = 1000000 + global.benchmarkRunner = function (name, body) { console.log(`Running '${name}' ...`); const startTime = performance.now(); - body(5000); + body(SAMPLE_ITERATION); const endTime = performance.now(); console.log("done " + (endTime - startTime) + " ms"); }; +global.noopFunction = function () {} +global.jsNumber = 42 +global.jsString = "myString" + class JSBenchmark { constructor(title) { this.title = title; } testSuite(name, body) { benchmarkRunner(`${this.title}/${name}`, (iteration) => { - for (let idx = 0; idx < iteration; idx++) { - body(); - } + body(iteration); }); } } const serialization = new JSBenchmark("Serialization"); -serialization.testSuite("Write JavaScript number directly", () => { +serialization.testSuite("Call JavaScript function directly", (n) => { + for (let idx = 0; idx < n; idx++) { + global.noopFunction() + } +}); + +serialization.testSuite("Assign JavaScript number directly", (n) => { const jsNumber = 42; const object = global; - for (let idx = 0; idx < 100; idx++) { - object["numberValue" + idx] = jsNumber; + const key = "numberValue" + for (let idx = 0; idx < n; idx++) { + object[key] = jsNumber; } }); -serialization.testSuite("Write JavaScript string directly", () => { +serialization.testSuite("Call with JavaScript number directly", (n) => { + const jsNumber = 42; + for (let idx = 0; idx < n; idx++) { + global.noopFunction(jsNumber) + } +}); + +serialization.testSuite("Write JavaScript string directly", (n) => { const jsString = "Hello, world"; const object = global; - for (let idx = 0; idx < 100; idx++) { - object["stringValue" + idx] = jsString; + const key = "stringValue" + for (let idx = 0; idx < n; idx++) { + object[key] = jsString; + } +}); + +serialization.testSuite("Call with JavaScript string directly", (n) => { + const jsString = "Hello, world"; + for (let idx = 0; idx < n; idx++) { + global.noopFunction(jsString) } }); diff --git a/IntegrationTests/lib.js b/IntegrationTests/lib.js index 5ba5df6a3..a0af77527 100644 --- a/IntegrationTests/lib.js +++ b/IntegrationTests/lib.js @@ -97,6 +97,10 @@ const startWasiTask = async (wasmPath, wasiConstructor = selectWASIBackend()) => let { instance } = await WebAssembly.instantiate(wasmBinary, { wasi_snapshot_preview1: wasi.wasiImport, javascript_kit: swift.importObjects(), + benchmark_helper: { + noop: () => {}, + noop_with_int: (_) => {}, + } }); swift.setInstance(instance); diff --git a/Makefile b/Makefile index 44d3de624..bd93f2e60 100644 --- a/Makefile +++ b/Makefile @@ -20,11 +20,11 @@ test: .PHONY: benchmark_setup benchmark_setup: - cd IntegrationTests && make benchmark_setup + cd IntegrationTests && CONFIGURATION=release make benchmark_setup .PHONY: run_benchmark run_benchmark: - cd IntegrationTests && make -s run_benchmark + cd IntegrationTests && CONFIGURATION=release make -s run_benchmark .PHONY: perf-tester perf-tester: From 756c6ca2b4748ae1001bf25b60a9ba28227b9571 Mon Sep 17 00:00:00 2001 From: Max Desiatov Date: Sun, 14 Aug 2022 11:05:52 +0100 Subject: [PATCH 033/373] Support DocC generation in Swift Package Index (#205) This allows documentation for our products to be generated and hosted automatically by [Swift Package Index](https://swiftpackageindex.com/swiftwasm/JavaScriptKit). --- .spi.yml | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 .spi.yml 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 From 4c61f130c28a2dbe84864e82d478a022c538b1be Mon Sep 17 00:00:00 2001 From: Max Desiatov Date: Sun, 14 Aug 2022 11:16:05 +0100 Subject: [PATCH 034/373] Test native builds with Xcode 14.0 (#206) Xcode 14.0 beta is already available on GitHub Actions, let's try using it in our workflows. --- .github/workflows/test.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 7a6b4c422..454d580fd 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -57,6 +57,8 @@ jobs: xcode: Xcode_13.2.1 - os: macos-12 xcode: Xcode_13.3 + - os: macos-12 + xcode: Xcode_14.0 runs-on: ${{ matrix.os }} steps: - uses: actions/checkout@v2 From 9b865b21b0be700cbb641e51a5dde1dc81efcd56 Mon Sep 17 00:00:00 2001 From: Yuta Saito Date: Sun, 14 Aug 2022 21:33:36 +0900 Subject: [PATCH 035/373] Add Int64/UInt64 to Bigint slow conversion (#204) --- .../TestSuites/Sources/PrimaryTests/I64.swift | 4 +++ Runtime/src/index.ts | 25 ++++++++++++++++--- Runtime/src/types.ts | 1 + .../FundamentalObjects/JSBigInt.swift | 13 ++++++++++ Sources/JavaScriptKit/Runtime/index.js | 5 ++++ Sources/JavaScriptKit/Runtime/index.mjs | 5 ++++ Sources/JavaScriptKit/XcodeSupport.swift | 3 +++ .../_CJavaScriptKit/include/_CJavaScriptKit.h | 10 ++++++++ 8 files changed, 63 insertions(+), 3 deletions(-) diff --git a/IntegrationTests/TestSuites/Sources/PrimaryTests/I64.swift b/IntegrationTests/TestSuites/Sources/PrimaryTests/I64.swift index bd0831e7d..8d8dda331 100644 --- a/IntegrationTests/TestSuites/Sources/PrimaryTests/I64.swift +++ b/IntegrationTests/TestSuites/Sources/PrimaryTests/I64.swift @@ -6,11 +6,15 @@ func testI64() throws { func expectPassesThrough(signed value: Int64) throws { let bigInt = JSBigInt(value) try expectEqual(bigInt.description, value.description) + let bigInt2 = JSBigInt(_slowBridge: value) + try expectEqual(bigInt2.description, value.description) } func expectPassesThrough(unsigned value: UInt64) throws { let bigInt = JSBigInt(unsigned: value) try expectEqual(bigInt.description, value.description) + let bigInt2 = JSBigInt(_slowBridge: value) + try expectEqual(bigInt2.description, value.description) } try expectPassesThrough(signed: 0) diff --git a/Runtime/src/index.ts b/Runtime/src/index.ts index 77264223c..8f90776f1 100644 --- a/Runtime/src/index.ts +++ b/Runtime/src/index.ts @@ -76,7 +76,12 @@ export class SwiftRuntime { return this._closureDeallocator; } - private callHostFunction(host_func_id: number, line: number, file: string, 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); for (let index = 0; index < args.length; index++) { @@ -103,7 +108,9 @@ export class SwiftRuntime { callback_func_ref ); if (alreadyReleased) { - throw new Error(`The JSClosure has been already released by Swift side. The closure is created at ${file}:${line}`); + 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; @@ -382,7 +389,11 @@ export class SwiftRuntime { return obj instanceof constructor; }, - swjs_create_function: (host_func_id: number, line: number, file: ref) => { + 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); @@ -436,5 +447,13 @@ export class SwiftRuntime { 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) + ); + }, }; } diff --git a/Runtime/src/types.ts b/Runtime/src/types.ts index 913837e32..a6e3dd1d2 100644 --- a/Runtime/src/types.ts +++ b/Runtime/src/types.ts @@ -105,6 +105,7 @@ export interface ImportedFunctions { swjs_release(ref: number): void; swjs_i64_to_bigint(value: bigint, signed: bool): ref; swjs_bigint_to_i64(ref: ref, signed: bool): bigint; + swjs_i64_to_bigint_slow(lower: number, upper: number, signed: bool): ref; } export const enum LibraryFeatures { diff --git a/Sources/JavaScriptKit/FundamentalObjects/JSBigInt.swift b/Sources/JavaScriptKit/FundamentalObjects/JSBigInt.swift index 4513c14a7..724e969a1 100644 --- a/Sources/JavaScriptKit/FundamentalObjects/JSBigInt.swift +++ b/Sources/JavaScriptKit/FundamentalObjects/JSBigInt.swift @@ -7,6 +7,19 @@ public final class JSBigInt: JSObject { override public init(id: JavaScriptObjectRef) { super.init(id: id) } + + /// Instantiate a new `JSBigInt` with given Int64 value in a slow path + /// This doesn't require [JS-BigInt-integration](https://github.com/WebAssembly/JS-BigInt-integration) feature. + public init(_slowBridge value: Int64) { + let value = UInt64(bitPattern: value) + super.init(id: _i64_to_bigint_slow(UInt32(value & 0xffffffff), 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: _i64_to_bigint_slow(UInt32(value & 0xffffffff), UInt32(value >> 32), false)) + } override public class func construct(from value: JSValue) -> Self? { value.bigInt as? Self diff --git a/Sources/JavaScriptKit/Runtime/index.js b/Sources/JavaScriptKit/Runtime/index.js index eb55ebc58..43158fbab 100644 --- a/Sources/JavaScriptKit/Runtime/index.js +++ b/Sources/JavaScriptKit/Runtime/index.js @@ -358,6 +358,11 @@ 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)); + }, }; this._instance = null; this._memory = null; diff --git a/Sources/JavaScriptKit/Runtime/index.mjs b/Sources/JavaScriptKit/Runtime/index.mjs index 8bd8043d9..299bafdb5 100644 --- a/Sources/JavaScriptKit/Runtime/index.mjs +++ b/Sources/JavaScriptKit/Runtime/index.mjs @@ -352,6 +352,11 @@ class SwiftRuntime { 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)); + }, }; this._instance = null; this._memory = null; diff --git a/Sources/JavaScriptKit/XcodeSupport.swift b/Sources/JavaScriptKit/XcodeSupport.swift index a3c8aeb1a..5556cdba8 100644 --- a/Sources/JavaScriptKit/XcodeSupport.swift +++ b/Sources/JavaScriptKit/XcodeSupport.swift @@ -46,6 +46,9 @@ import _CJavaScriptKit _: JavaScriptObjectRef, _: UnsafeMutablePointer! ) { fatalError() } + func _i64_to_bigint_slow( + _: UInt32, _: UInt32, _: Bool + ) -> JavaScriptObjectRef { fatalError() } func _call_function( _: JavaScriptObjectRef, _: UnsafePointer!, _: Int32, diff --git a/Sources/_CJavaScriptKit/include/_CJavaScriptKit.h b/Sources/_CJavaScriptKit/include/_CJavaScriptKit.h index fb07d1e09..59923e02d 100644 --- a/Sources/_CJavaScriptKit/include/_CJavaScriptKit.h +++ b/Sources/_CJavaScriptKit/include/_CJavaScriptKit.h @@ -160,6 +160,16 @@ __attribute__((__import_module__("javascript_kit"), __import_name__("swjs_load_string"))) extern void _load_string(const JavaScriptObjectRef bytes, unsigned char *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. +__attribute__((__import_module__("javascript_kit"), + __import_name__("swjs_i64_to_bigint_slow"))) +extern JavaScriptObjectRef _i64_to_bigint_slow(unsigned int lower, unsigned int upper, bool is_signed); + /// `_call_function` calls JavaScript function with given arguments list. /// /// @param ref The target JavaScript function to call. From 6e626197a78f2e3144ca43dbbd4b550e1ba79a50 Mon Sep 17 00:00:00 2001 From: Max Desiatov Date: Wed, 17 Aug 2022 18:47:41 +0100 Subject: [PATCH 036/373] Add missing doc comments for more types (#208) I noticed that some of our types had no doc comments or had inconsistent formatting, here's a (potentially incomplete) fix for that. --- .../JavaScriptEventLoop.swift | 29 +++++++++++++++++++ .../JavaScriptKit/BasicObjects/JSArray.swift | 5 +++- .../JavaScriptKit/BasicObjects/JSDate.swift | 2 +- .../JavaScriptKit/BasicObjects/JSError.swift | 2 +- .../BasicObjects/JSTypedArray.swift | 7 +++-- .../FundamentalObjects/JSBigInt.swift | 3 ++ .../FundamentalObjects/JSClosure.swift | 6 ++-- .../FundamentalObjects/JSSymbol.swift | 3 ++ 8 files changed, 49 insertions(+), 8 deletions(-) diff --git a/Sources/JavaScriptEventLoop/JavaScriptEventLoop.swift b/Sources/JavaScriptEventLoop/JavaScriptEventLoop.swift index c6d10209a..412d321a6 100644 --- a/Sources/JavaScriptEventLoop/JavaScriptEventLoop.swift +++ b/Sources/JavaScriptEventLoop/JavaScriptEventLoop.swift @@ -5,6 +5,35 @@ import _CJavaScriptEventLoop #if compiler(>=5.5) +/** Singleton type responsible for integrating JavaScript event loop as a Swift concurrency executor, conforming to +`SerialExecutor` protocol from the standard library. To utilize it: + +1. Make sure that your target depends on `JavaScriptEventLoop` in your `Packages.swift`: + +```swift +.target( + name: "JavaScriptKitExample", + dependencies: [ + "JavaScriptKit", + .product(name: "JavaScriptEventLoop", package: "JavaScriptKit") + ] +) +``` + +2. Add an explicit import in the code that executes **before* you start using `await` and/or `Task` +APIs (most likely in `main.swift`): + +```swift +import JavaScriptEventLoop +``` + +3. Run this function **before* you start using `await` and/or `Task` APIs (again, most likely in +`main.swift`): + +```swift +JavaScriptEventLoop.installGlobalExecutor() +``` +*/ @available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) public final class JavaScriptEventLoop: SerialExecutor, @unchecked Sendable { diff --git a/Sources/JavaScriptKit/BasicObjects/JSArray.swift b/Sources/JavaScriptKit/BasicObjects/JSArray.swift index 2d971daf7..90dba72d8 100644 --- a/Sources/JavaScriptKit/BasicObjects/JSArray.swift +++ b/Sources/JavaScriptKit/BasicObjects/JSArray.swift @@ -1,4 +1,5 @@ -/// 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 @@ -35,6 +36,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 diff --git a/Sources/JavaScriptKit/BasicObjects/JSDate.swift b/Sources/JavaScriptKit/BasicObjects/JSDate.swift index 5a0fd25ee..767374125 100644 --- a/Sources/JavaScriptKit/BasicObjects/JSDate.swift +++ b/Sources/JavaScriptKit/BasicObjects/JSDate.swift @@ -1,4 +1,4 @@ -/** A wrapper around the [JavaScript Date +/** 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 diff --git a/Sources/JavaScriptKit/BasicObjects/JSError.swift b/Sources/JavaScriptKit/BasicObjects/JSError.swift index 1d1526a47..e9b006c81 100644 --- a/Sources/JavaScriptKit/BasicObjects/JSError.swift +++ b/Sources/JavaScriptKit/BasicObjects/JSError.swift @@ -1,4 +1,4 @@ -/** A wrapper around [the JavaScript Error +/** 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. */ diff --git a/Sources/JavaScriptKit/BasicObjects/JSTypedArray.swift b/Sources/JavaScriptKit/BasicObjects/JSTypedArray.swift index a32c22203..ee564ae51 100644 --- a/Sources/JavaScriptKit/BasicObjects/JSTypedArray.swift +++ b/Sources/JavaScriptKit/BasicObjects/JSTypedArray.swift @@ -10,8 +10,8 @@ public protocol TypedArrayElement: ConvertibleToJSValue, ConstructibleFromJSValu 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. +/// 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. public class JSTypedArray: JSBridgedClass, ExpressibleByArrayLiteral where Element: TypedArrayElement { public class var constructor: JSFunction? { Element.typedArrayClass } public var jsObject: JSObject @@ -120,6 +120,9 @@ extension UInt8: TypedArrayElement { public static var typedArrayClass = JSObject.global.Uint8Array.function! } +/// A wrapper around [the JavaScript `Uint8ClampedArray` +/// class](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Array) +/// that exposes its properties in a type-safe and Swifty way. public class JSUInt8ClampedArray: JSTypedArray { override public class var constructor: JSFunction? { JSObject.global.Uint8ClampedArray.function! } } diff --git a/Sources/JavaScriptKit/FundamentalObjects/JSBigInt.swift b/Sources/JavaScriptKit/FundamentalObjects/JSBigInt.swift index 724e969a1..5929f2889 100644 --- a/Sources/JavaScriptKit/FundamentalObjects/JSBigInt.swift +++ b/Sources/JavaScriptKit/FundamentalObjects/JSBigInt.swift @@ -2,6 +2,9 @@ import _CJavaScriptKit private let constructor = 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) { diff --git a/Sources/JavaScriptKit/FundamentalObjects/JSClosure.swift b/Sources/JavaScriptKit/FundamentalObjects/JSClosure.swift index 0bff81403..c19f3ba8b 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,8 +10,8 @@ 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 diff --git a/Sources/JavaScriptKit/FundamentalObjects/JSSymbol.swift b/Sources/JavaScriptKit/FundamentalObjects/JSSymbol.swift index a0dea3937..f0560ef98 100644 --- a/Sources/JavaScriptKit/FundamentalObjects/JSSymbol.swift +++ b/Sources/JavaScriptKit/FundamentalObjects/JSSymbol.swift @@ -2,6 +2,9 @@ import _CJavaScriptKit private let Symbol = JSObject.global.Symbol.function! +/// 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 } From 24a5698358a5931bbd36a6663761f4e18de3def2 Mon Sep 17 00:00:00 2001 From: Max Desiatov Date: Wed, 17 Aug 2022 18:51:34 +0100 Subject: [PATCH 037/373] Fix formatting in `installGlobalExecutor` doc comment --- Sources/JavaScriptEventLoop/JavaScriptEventLoop.swift | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/Sources/JavaScriptEventLoop/JavaScriptEventLoop.swift b/Sources/JavaScriptEventLoop/JavaScriptEventLoop.swift index 412d321a6..d8f4ad0ad 100644 --- a/Sources/JavaScriptEventLoop/JavaScriptEventLoop.swift +++ b/Sources/JavaScriptEventLoop/JavaScriptEventLoop.swift @@ -84,8 +84,9 @@ public final class JavaScriptEventLoop: SerialExecutor, @unchecked Sendable { /// 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() { guard !didInstallGlobalExecutor else { return } From 1afbfaf16598119a851dca39e3b447de6cf52589 Mon Sep 17 00:00:00 2001 From: Yuta Saito Date: Fri, 19 Aug 2022 00:55:00 +0900 Subject: [PATCH 038/373] Runtime Performance Optimization (#207) * Allocate function call argument buffer on stack * Revert "Allocate function call argument buffer on stack" This reverts commit 8d17f5c1c78a20f5814c2eb1280569c2289c7f59. * Reduce memory store for returned value kind * Add NODEJS_FLAGS to perform profiling by passing --prof * Revert "Revert "Allocate function call argument buffer on stack"" This reverts commit d68497860269c37b5e48aad03fead5f26f181cdb. * Revert "Revert "Revert "Allocate function call argument buffer on stack""" This reverts commit 4f850a032eb742af10900af47cb42e5a44c03d98. * Reduce retain/release dance caused by Optional * Don't escape JSFunction self * Add fast path for empty JSValue array * Skip re-creating DataView in decodeArray * make regenerate_swiftpm_resources * Apply the same techniques to call families * Reuse DataView as much as possible * npm run format * Optimize swjs_get_prop to reduce memory store * Optimize _get_subscript to reduce memory store * Rename writeV2 -> writeAndReturnKindBits * Improve doc comment style * Use writeAndReturnKindBits in write * Add rationale comments for write --- IntegrationTests/Makefile | 3 +- Runtime/src/index.ts | 190 +++++++---------- Runtime/src/js-value.ts | 61 +++--- Runtime/src/types.ts | 20 +- .../JavaScriptKit/ConvertibleToJSValue.swift | 3 + .../FundamentalObjects/JSFunction.swift | 76 ++++--- .../FundamentalObjects/JSSymbol.swift | 2 +- .../JSThrowingFunction.swift | 16 +- Sources/JavaScriptKit/JSValue.swift | 24 ++- Sources/JavaScriptKit/Runtime/index.js | 194 +++++++++--------- Sources/JavaScriptKit/Runtime/index.mjs | 194 +++++++++--------- Sources/JavaScriptKit/XcodeSupport.swift | 18 +- .../_CJavaScriptKit/include/_CJavaScriptKit.h | 81 ++++---- 13 files changed, 456 insertions(+), 426 deletions(-) diff --git a/IntegrationTests/Makefile b/IntegrationTests/Makefile index 312977482..57b99c8da 100644 --- a/IntegrationTests/Makefile +++ b/IntegrationTests/Makefile @@ -1,7 +1,8 @@ CONFIGURATION ?= debug SWIFT_BUILD_FLAGS ?= +NODEJS_FLAGS ?= -NODEJS = node --experimental-wasi-unstable-preview1 +NODEJS = node --experimental-wasi-unstable-preview1 $(NODEJS_FLAGS) FORCE: TestSuites/.build/$(CONFIGURATION)/%.wasm: FORCE diff --git a/Runtime/src/index.ts b/Runtime/src/index.ts index 8f90776f1..a9da3eb9f 100644 --- a/Runtime/src/index.ts +++ b/Runtime/src/index.ts @@ -84,21 +84,15 @@ export class SwiftRuntime { ) { 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; }); const alreadyReleased = this.exports.swjs_call_host_function( @@ -127,28 +121,28 @@ export class SwiftRuntime { payload1: number, payload2: number ) => { - const obj = this.memory.getObject(ref); - const key = this.memory.getObject(name); - const value = JSValue.decode(kind, payload1, payload2, this.memory); + 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, - kind_ptr: pointer, payload1_ptr: pointer, payload2_ptr: pointer ) => { - const obj = this.memory.getObject(ref); - const key = this.memory.getObject(name); + const memory = this.memory; + const obj = memory.getObject(ref); + const key = memory.getObject(name); const result = obj[key]; - JSValue.write( + return JSValue.writeAndReturnKindBits( result, - kind_ptr, payload1_ptr, payload2_ptr, false, - this.memory + memory ); }, @@ -159,22 +153,21 @@ export class SwiftRuntime { payload1: number, payload2: number ) => { - const obj = this.memory.getObject(ref); - const value = JSValue.decode(kind, payload1, payload2, this.memory); + 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, - kind_ptr: pointer, payload1_ptr: pointer, payload2_ptr: pointer ) => { const obj = this.memory.getObject(ref); const result = obj[index]; - JSValue.write( + return JSValue.writeAndReturnKindBits( result, - kind_ptr, payload1_ptr, payload2_ptr, false, @@ -183,50 +176,50 @@ export class SwiftRuntime { }, 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); + 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: (bytes_ptr: pointer, length: number) => { - const bytes = this.memory + const memory = this.memory; + const bytes = memory .bytes() .subarray(bytes_ptr, bytes_ptr + length); const string = this.textDecoder.decode(bytes); - return this.memory.retain(string); + return memory.retain(string); }, swjs_load_string: (ref: ref, buffer: pointer) => { - const bytes = this.memory.getObject(ref); - this.memory.writeBytes(buffer, bytes); + const memory = this.memory; + const bytes = memory.getObject(ref); + 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; + const memory = this.memory; + const func = memory.getObject(ref); + let result = undefined; try { - const args = JSValue.decodeArray(argv, argc, this.memory); + const args = JSValue.decodeArray(argv, argc, memory); result = func(...args); } catch (error) { - JSValue.write( + return JSValue.writeAndReturnKindBits( error, - kind_ptr, payload1_ptr, payload2_ptr, true, this.memory ); - return; } - JSValue.write( + return JSValue.writeAndReturnKindBits( result, - kind_ptr, payload1_ptr, payload2_ptr, false, @@ -237,36 +230,20 @@ export class SwiftRuntime { ref: ref, argv: pointer, argc: number, - kind_ptr: pointer, payload1_ptr: pointer, payload2_ptr: pointer ) => { - const func = this.memory.getObject(ref); - let isException = true; - try { - const args = JSValue.decodeArray(argv, argc, this.memory); - const result = func(...args); - JSValue.write( - result, - kind_ptr, - payload1_ptr, - payload2_ptr, - false, - this.memory - ); - isException = false; - } finally { - if (isException) { - JSValue.write( - undefined, - kind_ptr, - payload1_ptr, - payload2_ptr, - true, - this.memory - ); - } - } + 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, + false, + this.memory + ); }, swjs_call_function_with_this: ( @@ -274,30 +251,27 @@ export class SwiftRuntime { 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); + 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, this.memory); + const args = JSValue.decodeArray(argv, argc, memory); result = func.apply(obj, args); } catch (error) { - JSValue.write( + return JSValue.writeAndReturnKindBits( error, - kind_ptr, payload1_ptr, payload2_ptr, true, this.memory ); - return; } - JSValue.write( + return JSValue.writeAndReturnKindBits( result, - kind_ptr, payload1_ptr, payload2_ptr, false, @@ -309,42 +283,28 @@ export class SwiftRuntime { 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 isException = true; - try { - const args = JSValue.decodeArray(argv, argc, this.memory); - const result = func.apply(obj, args); - JSValue.write( - result, - kind_ptr, - payload1_ptr, - payload2_ptr, - false, - this.memory - ); - isException = false; - } finally { - if (isException) { - JSValue.write( - undefined, - kind_ptr, - payload1_ptr, - payload2_ptr, - true, - this.memory - ); - } - } + 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 + ); }, swjs_call_new: (ref: ref, argv: pointer, argc: number) => { - const constructor = this.memory.getObject(ref); - const args = JSValue.decodeArray(argv, argc, this.memory); + 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); }, @@ -356,10 +316,11 @@ export class SwiftRuntime { exception_payload1_ptr: pointer, exception_payload2_ptr: pointer ) => { - const constructor = this.memory.getObject(ref); + let memory = this.memory; + const constructor = memory.getObject(ref); let result: any; try { - const args = JSValue.decodeArray(argv, argc, this.memory); + const args = JSValue.decodeArray(argv, argc, memory); result = new constructor(...args); } catch (error) { JSValue.write( @@ -372,20 +333,22 @@ export class SwiftRuntime { ); return -1; } + memory = this.memory; JSValue.write( null, exception_kind_ptr, exception_payload1_ptr, exception_payload2_ptr, false, - this.memory + memory ); - return this.memory.retain(result); + return 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); + const memory = this.memory; + const obj = memory.getObject(obj_ref); + const constructor = memory.getObject(constructor_ref); return obj instanceof constructor; }, @@ -419,9 +382,10 @@ export class SwiftRuntime { }, swjs_load_typed_array: (ref: ref, buffer: pointer) => { - const typedArray = this.memory.getObject(ref); + const memory = this.memory; + const typedArray = memory.getObject(ref); const bytes = new Uint8Array(typedArray.buffer); - this.memory.writeBytes(buffer, bytes); + memory.writeBytes(buffer, bytes); }, swjs_release: (ref: ref) => { diff --git a/Runtime/src/js-value.ts b/Runtime/src/js-value.ts index c3c24c3a9..9ff3d065e 100644 --- a/Runtime/src/js-value.ts +++ b/Runtime/src/js-value.ts @@ -1,5 +1,5 @@ import { Memory } from "./memory.js"; -import { assertNever, pointer } from "./types.js"; +import { assertNever, JavaScriptValueKindAndFlags, pointer } from "./types.js"; export const enum Kind { Boolean = 0, @@ -51,17 +51,29 @@ export const decode = ( // 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, @@ -70,54 +82,57 @@ 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; } const writeRef = (kind: Kind) => { - memory.writeUint32(kind_ptr, exceptionBit | 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": { - writeRef(Kind.String); - break; + return writeRef(Kind.String); } case "undefined": { - memory.writeUint32(kind_ptr, exceptionBit | Kind.Undefined); - break; + return exceptionBit | Kind.Undefined; } case "object": { - writeRef(Kind.Object); - break; + return writeRef(Kind.Object); } case "function": { - writeRef(Kind.Function); - break; + return writeRef(Kind.Function); } case "symbol": { - writeRef(Kind.Symbol); - break; + return writeRef(Kind.Symbol); } case "bigint": { - writeRef(Kind.BigInt); - break; + return writeRef(Kind.BigInt); } default: assertNever(type, `Type "${type}" is not supported yet`); } + throw new Error("Unreachable"); }; diff --git a/Runtime/src/types.ts b/Runtime/src/types.ts index a6e3dd1d2..ff20999ea 100644 --- a/Runtime/src/types.ts +++ b/Runtime/src/types.ts @@ -3,6 +3,8 @@ import * as JSValue from "./js-value.js"; 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; @@ -30,10 +32,9 @@ export interface ImportedFunctions { swjs_get_prop( ref: number, name: number, - kind_ptr: pointer, payload1_ptr: pointer, payload2_ptr: pointer - ): void; + ): JavaScriptValueKind; swjs_set_subscript( ref: number, index: number, @@ -44,10 +45,9 @@ export interface ImportedFunctions { swjs_get_subscript( ref: number, index: number, - kind_ptr: pointer, payload1_ptr: pointer, payload2_ptr: pointer - ): void; + ): JavaScriptValueKind; swjs_encode_string(ref: number, bytes_ptr_result: pointer): number; swjs_decode_string(bytes_ptr: pointer, length: number): number; swjs_load_string(ref: number, buffer: pointer): void; @@ -55,36 +55,32 @@ export interface ImportedFunctions { ref: number, argv: pointer, argc: number, - kind_ptr: pointer, payload1_ptr: pointer, payload2_ptr: pointer - ): void; + ): JavaScriptValueKindAndFlags; swjs_call_function_no_catch( ref: number, argv: pointer, argc: number, - kind_ptr: pointer, payload1_ptr: pointer, payload2_ptr: pointer - ): void; + ): JavaScriptValueKindAndFlags; 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; + ): JavaScriptValueKindAndFlags; swjs_call_function_with_this_no_catch( obj_ref: ref, func_ref: ref, argv: pointer, argc: number, - kind_ptr: pointer, payload1_ptr: pointer, payload2_ptr: pointer - ): void; + ): JavaScriptValueKindAndFlags; swjs_call_new(ref: number, argv: pointer, argc: number): number; swjs_call_throwing_new( ref: number, diff --git a/Sources/JavaScriptKit/ConvertibleToJSValue.swift b/Sources/JavaScriptKit/ConvertibleToJSValue.swift index 572e867b0..638672ca5 100644 --- a/Sources/JavaScriptKit/ConvertibleToJSValue.swift +++ b/Sources/JavaScriptKit/ConvertibleToJSValue.swift @@ -253,6 +253,9 @@ extension JSValue { extension Array where Element == ConvertibleToJSValue { func withRawJSValues(_ body: ([RawJSValue]) -> T) -> T { + // fast path for empty array + guard self.count != 0 else { return body([]) } + func _withRawJSValues( _ values: [ConvertibleToJSValue], _ index: Int, _ results: inout [RawJSValue], _ body: ([RawJSValue]) -> T diff --git a/Sources/JavaScriptKit/FundamentalObjects/JSFunction.swift b/Sources/JavaScriptKit/FundamentalObjects/JSFunction.swift index 9cec5dad0..66c613402 100644 --- a/Sources/JavaScriptKit/FundamentalObjects/JSFunction.swift +++ b/Sources/JavaScriptKit/FundamentalObjects/JSFunction.swift @@ -17,16 +17,31 @@ public class JSFunction: JSObject { /// - arguments: Arguments to be passed to this function. /// - Returns: The result of this call. @discardableResult - public func callAsFunction(this: JSObject? = nil, arguments: [ConvertibleToJSValue]) -> JSValue { - invokeNonThrowingJSFunction(self, arguments: arguments, this: this).jsValue + 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: @@ -81,29 +96,44 @@ public class JSFunction: JSObject { override public var jsValue: JSValue { .function(self) } -} -func invokeNonThrowingJSFunction(_ jsFunc: JSFunction, arguments: [ConvertibleToJSValue], this: JSObject?) -> RawJSValue { - arguments.withRawJSValues { rawValues in - rawValues.withUnsafeBufferPointer { 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_no_catch(thisId, - jsFunc.id, argv, Int32(argc), - &kindAndFlags, &payload1, &payload2) - } else { - _call_function_no_catch( - jsFunc.id, argv, Int32(argc), - &kindAndFlags, &payload1, &payload2 + final func invokeNonThrowingJSFunction(arguments: [ConvertibleToJSValue]) -> RawJSValue { + let id = self.id + return arguments.withRawJSValues { rawValues in + rawValues.withUnsafeBufferPointer { bufferPointer in + let argv = bufferPointer.baseAddress + let argc = bufferPointer.count + var payload1 = JavaScriptPayload1() + var payload2 = JavaScriptPayload2() + let resultBitPattern = _call_function_no_catch( + id, argv, Int32(argc), + &payload1, &payload2 + ) + let kindAndFlags = unsafeBitCast(resultBitPattern, to: JavaScriptValueKindAndFlags.self) + assert(!kindAndFlags.isException) + let result = RawJSValue(kind: kindAndFlags.kind, payload1: payload1, payload2: payload2) + return result + } + } + } + + final func invokeNonThrowingJSFunction(arguments: [ConvertibleToJSValue], this: JSObject) -> RawJSValue { + let id = self.id + return arguments.withRawJSValues { rawValues in + rawValues.withUnsafeBufferPointer { bufferPointer in + let argv = bufferPointer.baseAddress + let argc = bufferPointer.count + var payload1 = JavaScriptPayload1() + var payload2 = JavaScriptPayload2() + let resultBitPattern = _call_function_with_this_no_catch(this.id, + id, argv, Int32(argc), + &payload1, &payload2 ) + let kindAndFlags = unsafeBitCast(resultBitPattern, to: JavaScriptValueKindAndFlags.self) + assert(!kindAndFlags.isException) + let result = RawJSValue(kind: kindAndFlags.kind, payload1: payload1, payload2: payload2) + return result } - assert(!kindAndFlags.isException) - let result = RawJSValue(kind: kindAndFlags.kind, payload1: payload1, payload2: payload2) - return result } } } diff --git a/Sources/JavaScriptKit/FundamentalObjects/JSSymbol.swift b/Sources/JavaScriptKit/FundamentalObjects/JSSymbol.swift index f0560ef98..f25ee1bd8 100644 --- a/Sources/JavaScriptKit/FundamentalObjects/JSSymbol.swift +++ b/Sources/JavaScriptKit/FundamentalObjects/JSSymbol.swift @@ -10,7 +10,7 @@ public class JSSymbol: JSObject { public init(_ description: JSString) { // can’t do `self =` so we have to get the ID manually - let result = invokeNonThrowingJSFunction(Symbol, arguments: [description], this: nil) + let result = Symbol.invokeNonThrowingJSFunction(arguments: [description]) precondition(result.kind == .symbol) super.init(id: UInt32(result.payload1)) } diff --git a/Sources/JavaScriptKit/FundamentalObjects/JSThrowingFunction.swift b/Sources/JavaScriptKit/FundamentalObjects/JSThrowingFunction.swift index 3e21f0e1b..705899000 100644 --- a/Sources/JavaScriptKit/FundamentalObjects/JSThrowingFunction.swift +++ b/Sources/JavaScriptKit/FundamentalObjects/JSThrowingFunction.swift @@ -64,6 +64,7 @@ public class JSThrowingFunction { } 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 @@ -72,14 +73,17 @@ private func invokeJSFunction(_ jsFunc: JSFunction, arguments: [ConvertibleToJSV var payload1 = JavaScriptPayload1() var payload2 = JavaScriptPayload2() if let thisId = this?.id { - _call_function_with_this(thisId, - jsFunc.id, argv, Int32(argc), - &kindAndFlags, &payload1, &payload2) + let resultBitPattern = _call_function_with_this( + thisId, id, argv, Int32(argc), + &payload1, &payload2 + ) + kindAndFlags = unsafeBitCast(resultBitPattern, to: JavaScriptValueKindAndFlags.self) } else { - _call_function( - jsFunc.id, argv, Int32(argc), - &kindAndFlags, &payload1, &payload2 + let resultBitPattern = _call_function( + id, argv, Int32(argc), + &payload1, &payload2 ) + kindAndFlags = unsafeBitCast(resultBitPattern, to: JavaScriptValueKindAndFlags.self) } let result = RawJSValue(kind: kindAndFlags.kind, payload1: payload1, payload2: payload2) return (result.jsValue, kindAndFlags.isException) diff --git a/Sources/JavaScriptKit/JSValue.swift b/Sources/JavaScriptKit/JSValue.swift index 973dfcb5d..58b28e079 100644 --- a/Sources/JavaScriptKit/JSValue.swift +++ b/Sources/JavaScriptKit/JSValue.swift @@ -196,9 +196,11 @@ extension JSValue: ExpressibleByNilLiteral { public func getJSValue(this: JSObject, name: JSString) -> JSValue { var rawValue = RawJSValue() - _get_prop(this.id, name.asInternalJSRef(), - &rawValue.kind, - &rawValue.payload1, &rawValue.payload2) + let rawBitPattern = _get_prop( + this.id, name.asInternalJSRef(), + &rawValue.payload1, &rawValue.payload2 + ) + rawValue.kind = unsafeBitCast(rawBitPattern, to: JavaScriptValueKind.self) return rawValue.jsValue } @@ -210,9 +212,11 @@ public func setJSValue(this: JSObject, name: JSString, value: JSValue) { public func getJSValue(this: JSObject, index: Int32) -> JSValue { var rawValue = RawJSValue() - _get_subscript(this.id, index, - &rawValue.kind, - &rawValue.payload1, &rawValue.payload2) + let rawBitPattern = _get_subscript( + this.id, index, + &rawValue.payload1, &rawValue.payload2 + ) + rawValue.kind = unsafeBitCast(rawBitPattern, to: JavaScriptValueKind.self) return rawValue.jsValue } @@ -226,9 +230,11 @@ public func setJSValue(this: JSObject, index: Int32, value: JSValue) { public func getJSValue(this: JSObject, symbol: JSSymbol) -> JSValue { var rawValue = RawJSValue() - _get_prop(this.id, symbol.id, - &rawValue.kind, - &rawValue.payload1, &rawValue.payload2) + let rawBitPattern = _get_prop( + this.id, symbol.id, + &rawValue.payload1, &rawValue.payload2 + ) + rawValue.kind = unsafeBitCast(rawBitPattern, to: JavaScriptValueKind.self) return rawValue.jsValue } diff --git a/Sources/JavaScriptKit/Runtime/index.js b/Sources/JavaScriptKit/Runtime/index.js index 43158fbab..02dc9382e 100644 --- a/Sources/JavaScriptKit/Runtime/index.js +++ b/Sources/JavaScriptKit/Runtime/index.js @@ -54,65 +54,72 @@ // 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 = 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. 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) { - memory.writeUint32(kind_ptr, exceptionBit | 4 /* Null */); - return; + return exceptionBit | 4 /* Null */; } const writeRef = (kind) => { - memory.writeUint32(kind_ptr, exceptionBit | kind); memory.writeUint32(payload1_ptr, memory.retain(value)); + return exceptionBit | kind; }; const type = typeof value; switch (type) { case "boolean": { - memory.writeUint32(kind_ptr, exceptionBit | 0 /* Boolean */); memory.writeUint32(payload1_ptr, value ? 1 : 0); - break; + return exceptionBit | 0 /* Boolean */; } case "number": { - memory.writeUint32(kind_ptr, exceptionBit | 2 /* Number */); memory.writeFloat64(payload2_ptr, value); - break; + return exceptionBit | 2 /* Number */; } case "string": { - writeRef(1 /* String */); - break; + return writeRef(1 /* String */); } case "undefined": { - memory.writeUint32(kind_ptr, exceptionBit | 5 /* Undefined */); - break; + return exceptionBit | 5 /* Undefined */; } case "object": { - writeRef(3 /* Object */); - break; + return writeRef(3 /* Object */); } case "function": { - writeRef(6 /* Function */); - break; + return writeRef(6 /* Function */); } case "symbol": { - writeRef(7 /* Symbol */); - break; + return writeRef(7 /* Symbol */); } case "bigint": { - writeRef(8 /* BigInt */); - break; + return writeRef(8 /* BigInt */); } default: assertNever(type, `Type "${type}" is not supported yet`); } + throw new Error("Unreachable"); }; let globalVariable; @@ -197,125 +204,120 @@ this.importObjects = () => this.wasmImports; this.wasmImports = { swjs_set_prop: (ref, name, kind, payload1, payload2) => { - const obj = this.memory.getObject(ref); - const key = this.memory.getObject(name); - const value = decode(kind, payload1, payload2, this.memory); + 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, kind_ptr, payload1_ptr, payload2_ptr) => { - const obj = this.memory.getObject(ref); - const key = this.memory.getObject(name); + 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]; - write(result, kind_ptr, payload1_ptr, payload2_ptr, false, this.memory); + return writeAndReturnKindBits(result, payload1_ptr, payload2_ptr, false, memory); }, swjs_set_subscript: (ref, index, kind, payload1, payload2) => { - const obj = this.memory.getObject(ref); - const value = decode(kind, payload1, payload2, this.memory); + const memory = this.memory; + const obj = memory.getObject(ref); + const value = decode(kind, payload1, payload2, memory); obj[index] = value; }, - swjs_get_subscript: (ref, index, kind_ptr, payload1_ptr, payload2_ptr) => { + swjs_get_subscript: (ref, index, payload1_ptr, payload2_ptr) => { const obj = this.memory.getObject(ref); const result = obj[index]; - write(result, kind_ptr, payload1_ptr, payload2_ptr, false, this.memory); + return writeAndReturnKindBits(result, payload1_ptr, payload2_ptr, false, this.memory); }, swjs_encode_string: (ref, bytes_ptr_result) => { - const bytes = this.textEncoder.encode(this.memory.getObject(ref)); - const bytes_ptr = this.memory.retain(bytes); - this.memory.writeUint32(bytes_ptr_result, bytes_ptr); + 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: (bytes_ptr, length) => { - const bytes = this.memory + const memory = this.memory; + const bytes = memory .bytes() .subarray(bytes_ptr, bytes_ptr + length); const string = this.textDecoder.decode(bytes); - return this.memory.retain(string); + return memory.retain(string); }, swjs_load_string: (ref, buffer) => { - const bytes = this.memory.getObject(ref); - this.memory.writeBytes(buffer, bytes); + const memory = this.memory; + const bytes = memory.getObject(ref); + memory.writeBytes(buffer, bytes); }, - swjs_call_function: (ref, argv, argc, kind_ptr, payload1_ptr, payload2_ptr) => { - const func = this.memory.getObject(ref); - let result; + 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, this.memory); + const args = decodeArray(argv, argc, memory); result = func(...args); } catch (error) { - write(error, kind_ptr, payload1_ptr, payload2_ptr, true, this.memory); - return; + return writeAndReturnKindBits(error, payload1_ptr, payload2_ptr, true, this.memory); } - write(result, kind_ptr, payload1_ptr, payload2_ptr, false, this.memory); + return writeAndReturnKindBits(result, payload1_ptr, payload2_ptr, false, this.memory); }, - swjs_call_function_no_catch: (ref, argv, argc, kind_ptr, payload1_ptr, payload2_ptr) => { - const func = this.memory.getObject(ref); - let isException = true; - try { - const args = decodeArray(argv, argc, this.memory); - const result = func(...args); - write(result, kind_ptr, payload1_ptr, payload2_ptr, false, this.memory); - isException = false; - } - finally { - if (isException) { - write(undefined, kind_ptr, payload1_ptr, payload2_ptr, true, 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, kind_ptr, payload1_ptr, payload2_ptr) => { - const obj = this.memory.getObject(obj_ref); - const func = this.memory.getObject(func_ref); + 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, this.memory); + const args = decodeArray(argv, argc, memory); result = func.apply(obj, args); } catch (error) { - write(error, kind_ptr, payload1_ptr, payload2_ptr, true, this.memory); - return; + return writeAndReturnKindBits(error, payload1_ptr, payload2_ptr, true, this.memory); } - write(result, kind_ptr, payload1_ptr, payload2_ptr, false, 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, kind_ptr, payload1_ptr, payload2_ptr) => { - const obj = this.memory.getObject(obj_ref); - const func = this.memory.getObject(func_ref); - let isException = true; - try { - const args = decodeArray(argv, argc, this.memory); - const result = func.apply(obj, args); - write(result, kind_ptr, payload1_ptr, payload2_ptr, false, this.memory); - isException = false; - } - finally { - if (isException) { - write(undefined, kind_ptr, payload1_ptr, payload2_ptr, true, 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 constructor = this.memory.getObject(ref); - const args = decodeArray(argv, argc, this.memory); + 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) => { - const constructor = this.memory.getObject(ref); + let memory = this.memory; + const constructor = memory.getObject(ref); let result; try { - const args = decodeArray(argv, argc, this.memory); + 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; } - write(null, exception_kind_ptr, exception_payload1_ptr, exception_payload2_ptr, false, this.memory); - return this.memory.retain(result); + 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 obj = this.memory.getObject(obj_ref); - const constructor = this.memory.getObject(constructor_ref); + const memory = this.memory; + const obj = memory.getObject(obj_ref); + const constructor = memory.getObject(constructor_ref); return obj instanceof constructor; }, swjs_create_function: (host_func_id, line, file) => { @@ -333,9 +335,10 @@ return this.memory.retain(array.slice()); }, swjs_load_typed_array: (ref, buffer) => { - const typedArray = this.memory.getObject(ref); + const memory = this.memory; + const typedArray = memory.getObject(ref); const bytes = new Uint8Array(typedArray.buffer); - this.memory.writeBytes(buffer, bytes); + memory.writeBytes(buffer, bytes); }, swjs_release: (ref) => { this.memory.release(ref); @@ -408,14 +411,15 @@ 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, this.memory); + 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 = this.memory.retain((result) => { + 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); diff --git a/Sources/JavaScriptKit/Runtime/index.mjs b/Sources/JavaScriptKit/Runtime/index.mjs index 299bafdb5..823ffca60 100644 --- a/Sources/JavaScriptKit/Runtime/index.mjs +++ b/Sources/JavaScriptKit/Runtime/index.mjs @@ -48,65 +48,72 @@ const decode = (kind, payload1, payload2, memory) => { // 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 = 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. 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) { - memory.writeUint32(kind_ptr, exceptionBit | 4 /* Null */); - return; + return exceptionBit | 4 /* Null */; } const writeRef = (kind) => { - memory.writeUint32(kind_ptr, exceptionBit | kind); memory.writeUint32(payload1_ptr, memory.retain(value)); + return exceptionBit | kind; }; const type = typeof value; switch (type) { case "boolean": { - memory.writeUint32(kind_ptr, exceptionBit | 0 /* Boolean */); memory.writeUint32(payload1_ptr, value ? 1 : 0); - break; + return exceptionBit | 0 /* Boolean */; } case "number": { - memory.writeUint32(kind_ptr, exceptionBit | 2 /* Number */); memory.writeFloat64(payload2_ptr, value); - break; + return exceptionBit | 2 /* Number */; } case "string": { - writeRef(1 /* String */); - break; + return writeRef(1 /* String */); } case "undefined": { - memory.writeUint32(kind_ptr, exceptionBit | 5 /* Undefined */); - break; + return exceptionBit | 5 /* Undefined */; } case "object": { - writeRef(3 /* Object */); - break; + return writeRef(3 /* Object */); } case "function": { - writeRef(6 /* Function */); - break; + return writeRef(6 /* Function */); } case "symbol": { - writeRef(7 /* Symbol */); - break; + return writeRef(7 /* Symbol */); } case "bigint": { - writeRef(8 /* BigInt */); - break; + return writeRef(8 /* BigInt */); } default: assertNever(type, `Type "${type}" is not supported yet`); } + throw new Error("Unreachable"); }; let globalVariable; @@ -191,125 +198,120 @@ class SwiftRuntime { this.importObjects = () => this.wasmImports; this.wasmImports = { swjs_set_prop: (ref, name, kind, payload1, payload2) => { - const obj = this.memory.getObject(ref); - const key = this.memory.getObject(name); - const value = decode(kind, payload1, payload2, this.memory); + 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, kind_ptr, payload1_ptr, payload2_ptr) => { - const obj = this.memory.getObject(ref); - const key = this.memory.getObject(name); + 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]; - write(result, kind_ptr, payload1_ptr, payload2_ptr, false, this.memory); + return writeAndReturnKindBits(result, payload1_ptr, payload2_ptr, false, memory); }, swjs_set_subscript: (ref, index, kind, payload1, payload2) => { - const obj = this.memory.getObject(ref); - const value = decode(kind, payload1, payload2, this.memory); + const memory = this.memory; + const obj = memory.getObject(ref); + const value = decode(kind, payload1, payload2, memory); obj[index] = value; }, - swjs_get_subscript: (ref, index, kind_ptr, payload1_ptr, payload2_ptr) => { + swjs_get_subscript: (ref, index, payload1_ptr, payload2_ptr) => { const obj = this.memory.getObject(ref); const result = obj[index]; - write(result, kind_ptr, payload1_ptr, payload2_ptr, false, this.memory); + return writeAndReturnKindBits(result, payload1_ptr, payload2_ptr, false, this.memory); }, swjs_encode_string: (ref, bytes_ptr_result) => { - const bytes = this.textEncoder.encode(this.memory.getObject(ref)); - const bytes_ptr = this.memory.retain(bytes); - this.memory.writeUint32(bytes_ptr_result, bytes_ptr); + 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: (bytes_ptr, length) => { - const bytes = this.memory + const memory = this.memory; + const bytes = memory .bytes() .subarray(bytes_ptr, bytes_ptr + length); const string = this.textDecoder.decode(bytes); - return this.memory.retain(string); + return memory.retain(string); }, swjs_load_string: (ref, buffer) => { - const bytes = this.memory.getObject(ref); - this.memory.writeBytes(buffer, bytes); + const memory = this.memory; + const bytes = memory.getObject(ref); + memory.writeBytes(buffer, bytes); }, - swjs_call_function: (ref, argv, argc, kind_ptr, payload1_ptr, payload2_ptr) => { - const func = this.memory.getObject(ref); - let result; + 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, this.memory); + const args = decodeArray(argv, argc, memory); result = func(...args); } catch (error) { - write(error, kind_ptr, payload1_ptr, payload2_ptr, true, this.memory); - return; + return writeAndReturnKindBits(error, payload1_ptr, payload2_ptr, true, this.memory); } - write(result, kind_ptr, payload1_ptr, payload2_ptr, false, this.memory); + return writeAndReturnKindBits(result, payload1_ptr, payload2_ptr, false, this.memory); }, - swjs_call_function_no_catch: (ref, argv, argc, kind_ptr, payload1_ptr, payload2_ptr) => { - const func = this.memory.getObject(ref); - let isException = true; - try { - const args = decodeArray(argv, argc, this.memory); - const result = func(...args); - write(result, kind_ptr, payload1_ptr, payload2_ptr, false, this.memory); - isException = false; - } - finally { - if (isException) { - write(undefined, kind_ptr, payload1_ptr, payload2_ptr, true, 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, kind_ptr, payload1_ptr, payload2_ptr) => { - const obj = this.memory.getObject(obj_ref); - const func = this.memory.getObject(func_ref); + 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, this.memory); + const args = decodeArray(argv, argc, memory); result = func.apply(obj, args); } catch (error) { - write(error, kind_ptr, payload1_ptr, payload2_ptr, true, this.memory); - return; + return writeAndReturnKindBits(error, payload1_ptr, payload2_ptr, true, this.memory); } - write(result, kind_ptr, payload1_ptr, payload2_ptr, false, 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, kind_ptr, payload1_ptr, payload2_ptr) => { - const obj = this.memory.getObject(obj_ref); - const func = this.memory.getObject(func_ref); - let isException = true; - try { - const args = decodeArray(argv, argc, this.memory); - const result = func.apply(obj, args); - write(result, kind_ptr, payload1_ptr, payload2_ptr, false, this.memory); - isException = false; - } - finally { - if (isException) { - write(undefined, kind_ptr, payload1_ptr, payload2_ptr, true, 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 constructor = this.memory.getObject(ref); - const args = decodeArray(argv, argc, this.memory); + 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) => { - const constructor = this.memory.getObject(ref); + let memory = this.memory; + const constructor = memory.getObject(ref); let result; try { - const args = decodeArray(argv, argc, this.memory); + 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; } - write(null, exception_kind_ptr, exception_payload1_ptr, exception_payload2_ptr, false, this.memory); - return this.memory.retain(result); + 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 obj = this.memory.getObject(obj_ref); - const constructor = this.memory.getObject(constructor_ref); + const memory = this.memory; + const obj = memory.getObject(obj_ref); + const constructor = memory.getObject(constructor_ref); return obj instanceof constructor; }, swjs_create_function: (host_func_id, line, file) => { @@ -327,9 +329,10 @@ class SwiftRuntime { return this.memory.retain(array.slice()); }, swjs_load_typed_array: (ref, buffer) => { - const typedArray = this.memory.getObject(ref); + const memory = this.memory; + const typedArray = memory.getObject(ref); const bytes = new Uint8Array(typedArray.buffer); - this.memory.writeBytes(buffer, bytes); + memory.writeBytes(buffer, bytes); }, swjs_release: (ref) => { this.memory.release(ref); @@ -402,14 +405,15 @@ class SwiftRuntime { 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, this.memory); + 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 = this.memory.retain((result) => { + 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); diff --git a/Sources/JavaScriptKit/XcodeSupport.swift b/Sources/JavaScriptKit/XcodeSupport.swift index 5556cdba8..9689cf3b0 100644 --- a/Sources/JavaScriptKit/XcodeSupport.swift +++ b/Sources/JavaScriptKit/XcodeSupport.swift @@ -16,10 +16,9 @@ import _CJavaScriptKit func _get_prop( _: JavaScriptObjectRef, _: JavaScriptObjectRef, - _: UnsafeMutablePointer!, _: UnsafeMutablePointer!, _: UnsafeMutablePointer! - ) { fatalError() } + ) -> UInt32 { fatalError() } func _set_subscript( _: JavaScriptObjectRef, _: Int32, @@ -30,10 +29,9 @@ import _CJavaScriptKit func _get_subscript( _: JavaScriptObjectRef, _: Int32, - _: UnsafeMutablePointer!, _: UnsafeMutablePointer!, _: UnsafeMutablePointer! - ) { fatalError() } + ) -> UInt32 { fatalError() } func _encode_string( _: JavaScriptObjectRef, _: UnsafeMutablePointer! @@ -52,33 +50,29 @@ import _CJavaScriptKit func _call_function( _: JavaScriptObjectRef, _: UnsafePointer!, _: Int32, - _: UnsafeMutablePointer!, _: UnsafeMutablePointer!, _: UnsafeMutablePointer! - ) { fatalError() } + ) -> UInt32 { fatalError() } func _call_function_no_catch( _: JavaScriptObjectRef, _: UnsafePointer!, _: Int32, - _: UnsafeMutablePointer!, _: UnsafeMutablePointer!, _: UnsafeMutablePointer! - ) { fatalError() } + ) -> UInt32 { fatalError() } func _call_function_with_this( _: JavaScriptObjectRef, _: JavaScriptObjectRef, _: UnsafePointer!, _: Int32, - _: UnsafeMutablePointer!, _: UnsafeMutablePointer!, _: UnsafeMutablePointer! - ) { fatalError() } + ) -> UInt32 { fatalError() } func _call_function_with_this_no_catch( _: JavaScriptObjectRef, _: JavaScriptObjectRef, _: UnsafePointer!, _: Int32, - _: UnsafeMutablePointer!, _: UnsafeMutablePointer!, _: UnsafeMutablePointer! - ) { fatalError() } + ) -> UInt32 { fatalError() } func _call_new( _: JavaScriptObjectRef, _: UnsafePointer!, _: Int32 diff --git a/Sources/_CJavaScriptKit/include/_CJavaScriptKit.h b/Sources/_CJavaScriptKit/include/_CJavaScriptKit.h index 59923e02d..3bac436f4 100644 --- a/Sources/_CJavaScriptKit/include/_CJavaScriptKit.h +++ b/Sources/_CJavaScriptKit/include/_CJavaScriptKit.h @@ -3,6 +3,7 @@ #include #include +#include /// `JavaScriptObjectRef` represents JavaScript object reference that is referenced by Swift side. /// This value is an address of `SwiftRuntimeHeap`. @@ -92,16 +93,17 @@ extern void _set_prop(const JavaScriptObjectRef _this, /// /// @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. +/// @return A `JavaScriptValueKind` bits represented as 32bit integer for the returned value. __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); +extern uint32_t _get_prop( + const JavaScriptObjectRef _this, + const JavaScriptObjectRef prop, + JavaScriptPayload1 *payload1, + JavaScriptPayload2 *payload2 +); /// `_set_subscript` sets a value of `_this` JavaScript object. /// @@ -122,16 +124,17 @@ extern void _set_subscript(const JavaScriptObjectRef _this, /// /// @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. +/// @return A `JavaScriptValueKind` bits represented as 32bit integer for the returned value. __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); +extern uint32_t _get_subscript( + const JavaScriptObjectRef _this, + const int index, + JavaScriptPayload1 *payload1, + JavaScriptPayload2 *payload2 +); /// `_encode_string` encodes the `str_obj` to bytes sequence and returns the length of bytes. /// @@ -175,30 +178,34 @@ extern JavaScriptObjectRef _i64_to_bigint_slow(unsigned int lower, unsigned int /// @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. +/// @return A `JavaScriptValueKindAndFlags` bits represented as 32bit integer for the returned value. __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); +extern uint32_t _call_function( + const JavaScriptObjectRef ref, const RawJSValue *argv, + const int argc, + JavaScriptPayload1 *result_payload1, + JavaScriptPayload2 *result_payload2 +); /// `_call_function` 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_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. +/// @return A `JavaScriptValueKindAndFlags` bits represented as 32bit integer for the returned value. __attribute__((__import_module__("javascript_kit"), __import_name__("swjs_call_function_no_catch"))) -extern void _call_function_no_catch(const JavaScriptObjectRef ref, const RawJSValue *argv, - const int argc, JavaScriptValueKindAndFlags *result_kind, - JavaScriptPayload1 *result_payload1, - JavaScriptPayload2 *result_payload2); +extern uint32_t _call_function_no_catch( + const JavaScriptObjectRef ref, const RawJSValue *argv, + const int argc, + JavaScriptPayload1 *result_payload1, + JavaScriptPayload2 *result_payload2 +); /// `_call_function_with_this` calls JavaScript function with given arguments list and given `_this`. /// @@ -206,17 +213,18 @@ extern void _call_function_no_catch(const JavaScriptObjectRef ref, const RawJSVa /// @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. +/// @return A `JavaScriptValueKindAndFlags` bits represented as 32bit integer for the returned value. __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); +extern uint32_t _call_function_with_this( + const JavaScriptObjectRef _this, + const JavaScriptObjectRef func_ref, + const RawJSValue *argv, const int argc, + JavaScriptPayload1 *result_payload1, + JavaScriptPayload2 *result_payload2 +); /// `_call_function_with_this` calls JavaScript function with given arguments list and given `_this` without capturing any exception. /// @@ -224,17 +232,18 @@ extern void _call_function_with_this(const JavaScriptObjectRef _this, /// @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. +/// @return A `JavaScriptValueKindAndFlags` bits represented as 32bit integer for the returned value. __attribute__((__import_module__("javascript_kit"), __import_name__("swjs_call_function_with_this_no_catch"))) -extern void _call_function_with_this_no_catch(const JavaScriptObjectRef _this, - const JavaScriptObjectRef func_ref, - const RawJSValue *argv, const int argc, - JavaScriptValueKindAndFlags *result_kind, - JavaScriptPayload1 *result_payload1, - JavaScriptPayload2 *result_payload2); +extern uint32_t _call_function_with_this_no_catch( + const JavaScriptObjectRef _this, + const JavaScriptObjectRef func_ref, + const RawJSValue *argv, const int argc, + JavaScriptPayload1 *result_payload1, + JavaScriptPayload2 *result_payload2 +); /// `_call_new` calls JavaScript object constructor with given arguments list. /// From a129f62171bf3610633313c2db6acdca41cdb195 Mon Sep 17 00:00:00 2001 From: Yuta Saito Date: Mon, 22 Aug 2022 20:41:59 +0900 Subject: [PATCH 039/373] Bump version to 0.16.0, update `CHANGELOG.md` --- CHANGELOG.md | 20 ++++++++++++++++++++ Example/package-lock.json | 2 +- package-lock.json | 4 ++-- package.json | 2 +- 4 files changed, 24 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 62f7fff55..17f460cd5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,23 @@ +# 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: diff --git a/Example/package-lock.json b/Example/package-lock.json index 6955ec2bd..b112f5885 100644 --- a/Example/package-lock.json +++ b/Example/package-lock.json @@ -21,7 +21,7 @@ }, "..": { "name": "javascript-kit-swift", - "version": "0.15.0", + "version": "0.16.0", "license": "MIT", "devDependencies": { "@rollup/plugin-typescript": "^8.3.1", diff --git a/package-lock.json b/package-lock.json index 809b03030..7d66e97ec 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "javascript-kit-swift", - "version": "0.15.0", + "version": "0.16.0", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "javascript-kit-swift", - "version": "0.15.0", + "version": "0.16.0", "license": "MIT", "devDependencies": { "@rollup/plugin-typescript": "^8.3.1", diff --git a/package.json b/package.json index 90486f010..c4d459201 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "javascript-kit-swift", - "version": "0.15.0", + "version": "0.16.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", From 449c04192bb5530895f4ce0c79167180c417820f Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 29 Aug 2022 12:01:20 +0900 Subject: [PATCH 040/373] Bump @actions/core from 1.2.6 to 1.9.1 in /ci/perf-tester (#209) Bumps [@actions/core](https://github.com/actions/toolkit/tree/HEAD/packages/core) from 1.2.6 to 1.9.1. - [Release notes](https://github.com/actions/toolkit/releases) - [Changelog](https://github.com/actions/toolkit/blob/main/packages/core/RELEASES.md) - [Commits](https://github.com/actions/toolkit/commits/HEAD/packages/core) --- updated-dependencies: - dependency-name: "@actions/core" dependency-type: direct:development ... Signed-off-by: dependabot[bot] Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- ci/perf-tester/package-lock.json | 74 ++++++++++++++++++++++++++++---- ci/perf-tester/package.json | 2 +- 2 files changed, 66 insertions(+), 10 deletions(-) diff --git a/ci/perf-tester/package-lock.json b/ci/perf-tester/package-lock.json index c0cbbbdb2..82918bd59 100644 --- a/ci/perf-tester/package-lock.json +++ b/ci/perf-tester/package-lock.json @@ -5,16 +5,20 @@ "packages": { "": { "devDependencies": { - "@actions/core": "^1.2.6", + "@actions/core": "^1.9.1", "@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 + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/@actions/core/-/core-1.9.1.tgz", + "integrity": "sha512-5ad+U2YGrmmiw6du20AQW5XuWo7UKN2052FjSV7MX+Wfjf8sCqcsZe62NfgHys4QI4/Y+vQvLKYL8jWtA1ZBTA==", + "dev": true, + "dependencies": { + "@actions/http-client": "^2.0.1", + "uuid": "^8.3.2" + } }, "node_modules/@actions/exec": { "version": "1.0.3", @@ -35,6 +39,15 @@ "@octokit/rest": "^16.15.0" } }, + "node_modules/@actions/http-client": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@actions/http-client/-/http-client-2.0.1.tgz", + "integrity": "sha512-PIXiMVtz6VvyaRsGY268qvj57hXQEpsYogYOu2nrQhlf+XCGmZstmuZBbAybUl1nQGnvS1k1eEsQ69ZoD7xlSw==", + "dev": true, + "dependencies": { + "tunnel": "^0.0.6" + } + }, "node_modules/@actions/io": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/@actions/io/-/io-1.0.2.tgz", @@ -412,6 +425,15 @@ "integrity": "sha1-gYT9NH2snNwYWZLzpmIuFLnZq2o=", "dev": true }, + "node_modules/tunnel": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/tunnel/-/tunnel-0.0.6.tgz", + "integrity": "sha512-1h/Lnq9yajKY2PEbBadPXj3VxsDDu844OnaAo52UVmIzIvwwtBPIuNvkjuzBlTWpfJyUbG3ez0KSBibQkj4ojg==", + "dev": true, + "engines": { + "node": ">=0.6.11 <=0.7.0 || >=0.7.3" + } + }, "node_modules/universal-user-agent": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/universal-user-agent/-/universal-user-agent-4.0.0.tgz", @@ -421,6 +443,15 @@ "os-name": "^3.1.0" } }, + "node_modules/uuid": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", + "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", + "dev": true, + "bin": { + "uuid": "dist/bin/uuid" + } + }, "node_modules/webidl-conversions": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", @@ -470,10 +501,14 @@ }, "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 + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/@actions/core/-/core-1.9.1.tgz", + "integrity": "sha512-5ad+U2YGrmmiw6du20AQW5XuWo7UKN2052FjSV7MX+Wfjf8sCqcsZe62NfgHys4QI4/Y+vQvLKYL8jWtA1ZBTA==", + "dev": true, + "requires": { + "@actions/http-client": "^2.0.1", + "uuid": "^8.3.2" + } }, "@actions/exec": { "version": "1.0.3", @@ -494,6 +529,15 @@ "@octokit/rest": "^16.15.0" } }, + "@actions/http-client": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@actions/http-client/-/http-client-2.0.1.tgz", + "integrity": "sha512-PIXiMVtz6VvyaRsGY268qvj57hXQEpsYogYOu2nrQhlf+XCGmZstmuZBbAybUl1nQGnvS1k1eEsQ69ZoD7xlSw==", + "dev": true, + "requires": { + "tunnel": "^0.0.6" + } + }, "@actions/io": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/@actions/io/-/io-1.0.2.tgz", @@ -815,6 +859,12 @@ "integrity": "sha1-gYT9NH2snNwYWZLzpmIuFLnZq2o=", "dev": true }, + "tunnel": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/tunnel/-/tunnel-0.0.6.tgz", + "integrity": "sha512-1h/Lnq9yajKY2PEbBadPXj3VxsDDu844OnaAo52UVmIzIvwwtBPIuNvkjuzBlTWpfJyUbG3ez0KSBibQkj4ojg==", + "dev": true + }, "universal-user-agent": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/universal-user-agent/-/universal-user-agent-4.0.0.tgz", @@ -824,6 +874,12 @@ "os-name": "^3.1.0" } }, + "uuid": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", + "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", + "dev": true + }, "webidl-conversions": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", diff --git a/ci/perf-tester/package.json b/ci/perf-tester/package.json index 97718ab9a..7a00de44d 100644 --- a/ci/perf-tester/package.json +++ b/ci/perf-tester/package.json @@ -2,7 +2,7 @@ "private": true, "main": "src/index.js", "devDependencies": { - "@actions/core": "^1.2.6", + "@actions/core": "^1.9.1", "@actions/exec": "^1.0.3", "@actions/github": "^2.0.1" } From 73cdff2ec2d42ba37b82b6cb18cd7a9d90822f29 Mon Sep 17 00:00:00 2001 From: Jed Fox Date: Mon, 19 Sep 2022 21:12:59 -0400 Subject: [PATCH 041/373] =?UTF-8?q?Remove=20baseline=20tests=20(e.g.=20?= =?UTF-8?q?=E2=80=9CCall=20JavaScript=20function=20directly=E2=80=9D)=20fr?= =?UTF-8?q?om=20comparison=20(#211)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Remove baseline tests (e.g. “Call JavaScript function directly”) from comparison * ) --- ci/perf-tester/src/utils.js | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/ci/perf-tester/src/utils.js b/ci/perf-tester/src/utils.js index a4b717ab7..c7ecd662b 100644 --- a/ci/perf-tester/src/utils.js +++ b/ci/perf-tester/src/utils.js @@ -168,6 +168,7 @@ exports.diffTable = ( ) => { let changedRows = []; let unChangedRows = []; + let baselineRows = []; let totalTime = 0; let totalDelta = 0; @@ -187,7 +188,9 @@ exports.diffTable = ( getDeltaText(delta, difference), iconForDifference(difference), ]; - if (isUnchanged && collapseUnchanged) { + if (name.includes('directly')) { + baselineRows.push(columns); + } else if (isUnchanged && collapseUnchanged) { unChangedRows.push(columns); } else { changedRows.push(columns); @@ -200,6 +203,11 @@ exports.diffTable = ( const outUnchanged = markdownTable(unChangedRows); out += `\n\n
View Unchanged\n\n${outUnchanged}\n\n
\n\n`; } + + if (baselineRows.length !== 0) { + const outBaseline = markdownTable(baselineRows.map(line => line.slice(0, 2))); + out += `\n\n
View Baselines\n\n${outBaseline}\n\n
\n\n`; + } if (showTotal) { const totalDifference = ((totalDelta / totalTime) * 100) | 0; From e9422fecd4523288fb70be92759009c119bc9db4 Mon Sep 17 00:00:00 2001 From: Yuta Saito Date: Tue, 27 Sep 2022 14:22:55 +0900 Subject: [PATCH 042/373] Add 5.7 toolchain matrix (#210) * Add 5.7 toolchain matrix * Reduce 5.5 toolchain matrix entries * Use stable version --- .github/workflows/test.yml | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 454d580fd..96c36e547 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -18,10 +18,11 @@ jobs: # Ensure that test succeeds with all toolchains and wasi backend combinations - { os: ubuntu-20.04, toolchain: wasm-5.5.0-RELEASE, wasi-backend: Node } - { os: ubuntu-20.04, toolchain: wasm-5.6.0-RELEASE, wasi-backend: Node } - - { os: ubuntu-20.04, toolchain: wasm-5.5.0-RELEASE, wasi-backend: Wasmer } + - { os: ubuntu-20.04, toolchain: wasm-5.7.1-RELEASE, wasi-backend: Node } - { os: ubuntu-20.04, toolchain: wasm-5.6.0-RELEASE, wasi-backend: Wasmer } - - { os: ubuntu-20.04, toolchain: wasm-5.5.0-RELEASE, wasi-backend: MicroWASI } + - { os: ubuntu-20.04, toolchain: wasm-5.7.1-RELEASE, wasi-backend: Wasmer } - { os: ubuntu-20.04, toolchain: wasm-5.6.0-RELEASE, wasi-backend: MicroWASI } + - { os: ubuntu-20.04, toolchain: wasm-5.7.1-RELEASE, wasi-backend: MicroWASI } runs-on: ${{ matrix.entry.os }} steps: From f72b207dc4f330114688861e0ea3922b2480b4b1 Mon Sep 17 00:00:00 2001 From: Yuta Saito Date: Tue, 27 Sep 2022 05:26:54 +0000 Subject: [PATCH 043/373] Add JavaScriptEventLoopTestSupport module to install executor --- Package.swift | 8 ++++++++ .../JavaScriptEventLoopTestSupport.swift | 9 +++++++++ .../_CJavaScriptEventLoopTestSupport.c | 19 +++++++++++++++++++ .../include/dummy.h | 0 4 files changed, 36 insertions(+) create mode 100644 Sources/JavaScriptEventLoopTestSupport/JavaScriptEventLoopTestSupport.swift create mode 100644 Sources/_CJavaScriptEventLoopTestSupport/_CJavaScriptEventLoopTestSupport.c create mode 100644 Sources/_CJavaScriptEventLoopTestSupport/include/dummy.h diff --git a/Package.swift b/Package.swift index d278e5ab9..5902bc9b5 100644 --- a/Package.swift +++ b/Package.swift @@ -26,5 +26,13 @@ let package = Package( dependencies: ["JavaScriptKit", "_CJavaScriptEventLoop"] ), .target(name: "_CJavaScriptEventLoop"), + .target( + name: "JavaScriptEventLoopTestSupport", + dependencies: [ + "_CJavaScriptEventLoopTestSupport", + .product(name: "JavaScriptEventLoop", package: "JavaScriptKit"), + ] + ), + .target(name: "_CJavaScriptEventLoopTestSupport"), ] ) diff --git a/Sources/JavaScriptEventLoopTestSupport/JavaScriptEventLoopTestSupport.swift b/Sources/JavaScriptEventLoopTestSupport/JavaScriptEventLoopTestSupport.swift new file mode 100644 index 000000000..368639304 --- /dev/null +++ b/Sources/JavaScriptEventLoopTestSupport/JavaScriptEventLoopTestSupport.swift @@ -0,0 +1,9 @@ +// This module just expose 'JavaScriptEventLoop.installGlobalExecutor' to C ABI +// See _CJavaScriptEventLoopTestSupport.c for why this is needed + +import JavaScriptEventLoop + +@_cdecl("swift_javascriptkit_activate_js_executor_impl") +func swift_javascriptkit_activate_js_executor_impl() { + JavaScriptEventLoop.installGlobalExecutor() +} 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 From 6fea6e5d18ae8e62f0576c19d60079c44f30eee7 Mon Sep 17 00:00:00 2001 From: Yuta Saito Date: Fri, 30 Sep 2022 12:47:20 +0000 Subject: [PATCH 044/373] Add short document about JavaScriptEventLoopTestSupport --- README.md | 17 ++++++++++++++++ .../JavaScriptEventLoopTestSupport.swift | 20 +++++++++++++++++-- 2 files changed, 35 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 42af64320..0c2c6988c 100644 --- a/README.md +++ b/README.md @@ -125,6 +125,23 @@ asyncButtonElement.onclick = .object(JSClosure { _ in _ = document.body.appendChild(asyncButtonElement) ``` +### `JavaScriptEventLoop` activation in XCTest suites + +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()` + + ## Requirements ### For developers diff --git a/Sources/JavaScriptEventLoopTestSupport/JavaScriptEventLoopTestSupport.swift b/Sources/JavaScriptEventLoopTestSupport/JavaScriptEventLoopTestSupport.swift index 368639304..d62979dd9 100644 --- a/Sources/JavaScriptEventLoopTestSupport/JavaScriptEventLoopTestSupport.swift +++ b/Sources/JavaScriptEventLoopTestSupport/JavaScriptEventLoopTestSupport.swift @@ -1,8 +1,24 @@ -// This module just expose 'JavaScriptEventLoop.installGlobalExecutor' to C ABI -// See _CJavaScriptEventLoopTestSupport.c for why this is needed +/// 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 @_cdecl("swift_javascriptkit_activate_js_executor_impl") func swift_javascriptkit_activate_js_executor_impl() { JavaScriptEventLoop.installGlobalExecutor() From d82c70f3592b4813eae90dd4833d8dcf231c8b02 Mon Sep 17 00:00:00 2001 From: Yuta Saito Date: Fri, 30 Sep 2022 12:50:21 +0000 Subject: [PATCH 045/373] Publish JavaScriptEventLoopTestSupport as a product --- Package.swift | 1 + 1 file changed, 1 insertion(+) diff --git a/Package.swift b/Package.swift index 5902bc9b5..b0b0520de 100644 --- a/Package.swift +++ b/Package.swift @@ -8,6 +8,7 @@ let package = Package( .library(name: "JavaScriptKit", targets: ["JavaScriptKit"]), .library(name: "JavaScriptEventLoop", targets: ["JavaScriptEventLoop"]), .library(name: "JavaScriptBigIntSupport", targets: ["JavaScriptBigIntSupport"]), + .library(name: "JavaScriptEventLoopTestSupport", targets: ["JavaScriptEventLoopTestSupport"]), ], targets: [ .target( From eb9d9f29fa92d277f4db065730a78b85804368d2 Mon Sep 17 00:00:00 2001 From: Yuta Saito Date: Fri, 30 Sep 2022 13:38:11 +0000 Subject: [PATCH 046/373] Add arg0 to wasi arguments because XCTest reads process arguments and it expects at least having one argument --- IntegrationTests/lib.js | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/IntegrationTests/lib.js b/IntegrationTests/lib.js index a0af77527..347348fb9 100644 --- a/IntegrationTests/lib.js +++ b/IntegrationTests/lib.js @@ -9,7 +9,7 @@ const fs = require("fs"); const readFile = promisify(fs.readFile); const WASI = { - Wasmer: () => { + Wasmer: ({ programName }) => { // Instantiate a new WASI Instance const wasmFs = new WasmFs(); // Output stdout and stderr to console @@ -27,7 +27,7 @@ const WASI = { return originalWriteSync(fd, buffer, offset, length, position); }; const wasi = new WasmerWASI({ - args: [], + args: [programName], env: {}, bindings: { ...WasmerWASI.defaultBindings, @@ -44,9 +44,9 @@ const WASI = { } } }, - MicroWASI: () => { + MicroWASI: ({ programName }) => { const wasi = new MicroWASI({ - args: [], + args: [programName], env: {}, features: [useAll()], }) @@ -59,9 +59,9 @@ const WASI = { } } }, - Node: () => { + Node: ({ programName }) => { const wasi = new NodeWASI({ - args: [], + args: [programName], env: {}, returnOnExit: true, }) @@ -91,7 +91,7 @@ const startWasiTask = async (wasmPath, wasiConstructor = selectWASIBackend()) => const swift = new SwiftRuntime(); // Fetch our Wasm File const wasmBinary = await readFile(wasmPath); - const wasi = wasiConstructor(); + const wasi = wasiConstructor({ programName: wasmPath }); // Instantiate the WebAssembly file let { instance } = await WebAssembly.instantiate(wasmBinary, { From 28bb7f2bfed89c781d267b289575a9ce31f52d80 Mon Sep 17 00:00:00 2001 From: Yuta Saito Date: Fri, 30 Sep 2022 13:38:48 +0000 Subject: [PATCH 047/373] Exit process when proc_exit is called on Node.js WASI because Node.js's WASI implementatin does't supprot proc_exit in async reactor model. See https://github.com/nodejs/node/blob/2a4452a53af65a13db4efae474162a7dcfd38dd5/lib/wasi.js#L121 --- IntegrationTests/lib.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/IntegrationTests/lib.js b/IntegrationTests/lib.js index 347348fb9..e708816cb 100644 --- a/IntegrationTests/lib.js +++ b/IntegrationTests/lib.js @@ -63,7 +63,7 @@ const WASI = { const wasi = new NodeWASI({ args: [programName], env: {}, - returnOnExit: true, + returnOnExit: false, }) return { From 08c36d2601b9e21da028115cfffe060d8b406d53 Mon Sep 17 00:00:00 2001 From: Yuta Saito Date: Fri, 30 Sep 2022 13:40:33 +0000 Subject: [PATCH 048/373] Add unit test target for JavaScriptEventLoopTestSupport --- Package.swift | 9 ++++++++- .../JavaScriptEventLoopTestSupportTests.swift | 15 +++++++++++++++ scripts/test-harness.js | 10 ++++++++++ 3 files changed, 33 insertions(+), 1 deletion(-) create mode 100644 Tests/JavaScriptEventLoopTestSupportTests/JavaScriptEventLoopTestSupportTests.swift create mode 100644 scripts/test-harness.js diff --git a/Package.swift b/Package.swift index b0b0520de..c8f55dd0b 100644 --- a/Package.swift +++ b/Package.swift @@ -31,9 +31,16 @@ let package = Package( name: "JavaScriptEventLoopTestSupport", dependencies: [ "_CJavaScriptEventLoopTestSupport", - .product(name: "JavaScriptEventLoop", package: "JavaScriptKit"), + "JavaScriptEventLoop", ] ), .target(name: "_CJavaScriptEventLoopTestSupport"), + .testTarget( + name: "JavaScriptEventLoopTestSupportTests", + dependencies: [ + "JavaScriptKit", + "JavaScriptEventLoopTestSupport" + ] + ), ] ) diff --git a/Tests/JavaScriptEventLoopTestSupportTests/JavaScriptEventLoopTestSupportTests.swift b/Tests/JavaScriptEventLoopTestSupportTests/JavaScriptEventLoopTestSupportTests.swift new file mode 100644 index 000000000..cca303a09 --- /dev/null +++ b/Tests/JavaScriptEventLoopTestSupportTests/JavaScriptEventLoopTestSupportTests.swift @@ -0,0 +1,15 @@ +import XCTest +import JavaScriptKit + +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/scripts/test-harness.js b/scripts/test-harness.js new file mode 100644 index 000000000..39a7dbe9a --- /dev/null +++ b/scripts/test-harness.js @@ -0,0 +1,10 @@ +Error.stackTraceLimit = Infinity; + +const { startWasiTask, WASI } = require("../IntegrationTests/lib"); + +const handleExitOrError = (error) => { + console.log(error); + process.exit(1); +} + +startWasiTask(process.argv[2]).catch(handleExitOrError); From 0c39b24267aa3fa2095cc9e9c34e0ff0e6cd9ca5 Mon Sep 17 00:00:00 2001 From: Yuta Saito Date: Fri, 30 Sep 2022 13:44:28 +0000 Subject: [PATCH 049/373] Add unit test job in Makefile --- Makefile | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/Makefile b/Makefile index bd93f2e60..58ccccdd5 100644 --- a/Makefile +++ b/Makefile @@ -12,6 +12,10 @@ build: .PHONY: test test: + @echo Running unit tests + swift build --build-tests --triple wasm32-unknown-wasi -Xswiftc -Xclang-linker -Xswiftc -mexec-model=reactor -Xlinker --export=main + node --experimental-wasi-unstable-preview1 scripts/test-harness.js ./.build/wasm32-unknown-wasi/debug/JavaScriptKitPackageTests.wasm + @echo Running integration tests cd IntegrationTests && \ CONFIGURATION=debug make test && \ CONFIGURATION=debug SWIFT_BUILD_FLAGS="-Xswiftc -DJAVASCRIPTKIT_WITHOUT_WEAKREFS" make test && \ From 3e94df2cead0ebfa335246414a9a3652dfb4cd49 Mon Sep 17 00:00:00 2001 From: Yuta Saito Date: Fri, 30 Sep 2022 13:48:02 +0000 Subject: [PATCH 050/373] Add API guard directives for native build --- .../JavaScriptEventLoopTestSupport.swift | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/Sources/JavaScriptEventLoopTestSupport/JavaScriptEventLoopTestSupport.swift b/Sources/JavaScriptEventLoopTestSupport/JavaScriptEventLoopTestSupport.swift index d62979dd9..9922de945 100644 --- a/Sources/JavaScriptEventLoopTestSupport/JavaScriptEventLoopTestSupport.swift +++ b/Sources/JavaScriptEventLoopTestSupport/JavaScriptEventLoopTestSupport.swift @@ -19,7 +19,13 @@ 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 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) @_cdecl("swift_javascriptkit_activate_js_executor_impl") func swift_javascriptkit_activate_js_executor_impl() { JavaScriptEventLoop.installGlobalExecutor() } + +#endif From b34c15f8e630dd7e85f8a073fcc05305c6efcd36 Mon Sep 17 00:00:00 2001 From: Yuta Saito Date: Fri, 30 Sep 2022 14:01:07 +0000 Subject: [PATCH 051/373] Run async unit tests only with 5.7 toolchain --- .github/workflows/test.yml | 15 ++++++++++----- Makefile | 9 ++++++--- 2 files changed, 16 insertions(+), 8 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 96c36e547..e6b0db563 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -25,22 +25,27 @@ jobs: - { os: ubuntu-20.04, toolchain: wasm-5.7.1-RELEASE, wasi-backend: MicroWASI } runs-on: ${{ matrix.entry.os }} + env: + JAVASCRIPTKIT_WASI_BACKEND: ${{ matrix.entry.wasi-backend }} + SWIFT_VERSION: ${{ matrix.entry.toolchain }} steps: - name: Checkout uses: actions/checkout@master with: fetch-depth: 1 - - name: Run Test - env: - JAVASCRIPTKIT_WASI_BACKEND: ${{ matrix.entry.wasi-backend }} + - name: Install swiftenv 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.entry.toolchain }} make bootstrap + echo $PATH >> $GITHUB_PATH + env >> $GITHUB_ENV echo ${{ matrix.entry.toolchain }} > .swift-version - make test + - run: make bootstrap + - run: make test + - run: make unittest + if: ${{ startsWith(matrix.toolchain, 'wasm-5.7.') }} - name: Check if SwiftPM resources are stale run: | make regenerate_swiftpm_resources diff --git a/Makefile b/Makefile index 58ccccdd5..7b8736221 100644 --- a/Makefile +++ b/Makefile @@ -12,9 +12,6 @@ build: .PHONY: test test: - @echo Running unit tests - swift build --build-tests --triple wasm32-unknown-wasi -Xswiftc -Xclang-linker -Xswiftc -mexec-model=reactor -Xlinker --export=main - node --experimental-wasi-unstable-preview1 scripts/test-harness.js ./.build/wasm32-unknown-wasi/debug/JavaScriptKitPackageTests.wasm @echo Running integration tests cd IntegrationTests && \ CONFIGURATION=debug make test && \ @@ -22,6 +19,12 @@ test: CONFIGURATION=release make test && \ CONFIGURATION=release SWIFT_BUILD_FLAGS="-Xswiftc -DJAVASCRIPTKIT_WITHOUT_WEAKREFS" make test +.PHONY: unittest +unittest: + @echo Running unit tests + swift build --build-tests --triple wasm32-unknown-wasi -Xswiftc -Xclang-linker -Xswiftc -mexec-model=reactor -Xlinker --export=main + node --experimental-wasi-unstable-preview1 scripts/test-harness.js ./.build/wasm32-unknown-wasi/debug/JavaScriptKitPackageTests.wasm + .PHONY: benchmark_setup benchmark_setup: cd IntegrationTests && CONFIGURATION=release make benchmark_setup From d0f49adff30ba0bfef2117a33dc6501341264a41 Mon Sep 17 00:00:00 2001 From: Yuta Saito Date: Mon, 3 Oct 2022 12:06:07 +0000 Subject: [PATCH 052/373] Expose JavaScriptEventLoop.queueMicrotask and .setTimeout This allows users to have more flexibility to customize. For example, this allows inserting operations before/after single job execution loop. e.g. It's useful to enable React batch rendering per job execution loop by `ReactDOM.unstable_batchedUpdates`. ```swift let original = JavaScriptEventLoop.shared.queueMicrotask JavaScriptEventLoop.shared.queueMicrotask = (job) => { ReactDOM.unstable_batchedUpdates(() => { original(job) }) } ``` --- Sources/JavaScriptEventLoop/JavaScriptEventLoop.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Sources/JavaScriptEventLoop/JavaScriptEventLoop.swift b/Sources/JavaScriptEventLoop/JavaScriptEventLoop.swift index d8f4ad0ad..7c4a1c905 100644 --- a/Sources/JavaScriptEventLoop/JavaScriptEventLoop.swift +++ b/Sources/JavaScriptEventLoop/JavaScriptEventLoop.swift @@ -39,9 +39,9 @@ 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: @Sendable (@escaping () -> Void) -> Void /// A function that invokes a given closure after a specified number of milliseconds. - let setTimeout: @Sendable (Double, @escaping () -> Void) -> Void + public var setTimeout: @Sendable (Double, @escaping () -> Void) -> Void /// A mutable state to manage internal job queue /// Note that this should be guarded atomically when supporting multi-threaded environment. From dac9d7b0342c5027fc74c8d2f212f10624123a7b Mon Sep 17 00:00:00 2001 From: Yuta Saito Date: Mon, 3 Oct 2022 17:45:34 +0000 Subject: [PATCH 053/373] Bump version to 0.17.0, update `CHANGELOG.md` --- CHANGELOG.md | 18 ++++++++++++++++++ Example/package-lock.json | 2 +- package-lock.json | 4 ++-- package.json | 2 +- 4 files changed, 22 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 17f460cd5..d0d336ed5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,21 @@ +# 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. diff --git a/Example/package-lock.json b/Example/package-lock.json index b112f5885..24b287a21 100644 --- a/Example/package-lock.json +++ b/Example/package-lock.json @@ -21,7 +21,7 @@ }, "..": { "name": "javascript-kit-swift", - "version": "0.16.0", + "version": "0.17.0", "license": "MIT", "devDependencies": { "@rollup/plugin-typescript": "^8.3.1", diff --git a/package-lock.json b/package-lock.json index 7d66e97ec..dcc23dc1b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "javascript-kit-swift", - "version": "0.16.0", + "version": "0.17.0", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "javascript-kit-swift", - "version": "0.16.0", + "version": "0.17.0", "license": "MIT", "devDependencies": { "@rollup/plugin-typescript": "^8.3.1", diff --git a/package.json b/package.json index c4d459201..8bd772b21 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "javascript-kit-swift", - "version": "0.16.0", + "version": "0.17.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", From 60ca6b5fe7dbca882544398f0a05eb37dc226e33 Mon Sep 17 00:00:00 2001 From: Yuta Saito Date: Sun, 9 Oct 2022 11:09:42 +0900 Subject: [PATCH 054/373] Use swiftwasm/setup-swiftwasm instead of swiftenv on CI (#215) * Use swiftwasm/setup-swiftwasm instead of swiftenv on CI * Update .github/workflows/perf.yml also * Update .github/workflows/compatibility.yml also --- .github/workflows/compatibility.yml | 7 ++----- .github/workflows/perf.yml | 7 ++----- .github/workflows/test.yml | 12 +++--------- Makefile | 1 - 4 files changed, 7 insertions(+), 20 deletions(-) diff --git a/.github/workflows/compatibility.yml b/.github/workflows/compatibility.yml index 422e08c48..489c7aac4 100644 --- a/.github/workflows/compatibility.yml +++ b/.github/workflows/compatibility.yml @@ -6,19 +6,16 @@ on: jobs: test: name: Check source code compatibility - runs-on: Ubuntu-18.04 + runs-on: ubuntu-20.04 steps: - name: Checkout uses: actions/checkout@v2 with: fetch-depth: 1 + - uses: swiftwasm/setup-swiftwasm@v1 - 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/perf.yml b/.github/workflows/perf.yml index f2014323a..2fdba41dd 100644 --- a/.github/workflows/perf.yml +++ b/.github/workflows/perf.yml @@ -4,18 +4,15 @@ on: [pull_request] jobs: perf: - runs-on: Ubuntu-18.04 + runs-on: ubuntu-20.04 steps: - name: Checkout uses: actions/checkout@master with: fetch-depth: 1 + - uses: swiftwasm/setup-swiftwasm@v1 - 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 diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index e6b0db563..d41966fe3 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -33,15 +33,9 @@ jobs: uses: actions/checkout@master with: fetch-depth: 1 - - name: Install swiftenv - run: | - git clone https://github.com/kylef/swiftenv.git ~/.swiftenv - export SWIFTENV_ROOT="$HOME/.swiftenv" - export PATH="$SWIFTENV_ROOT/bin:$PATH" - eval "$(swiftenv init -)" - echo $PATH >> $GITHUB_PATH - env >> $GITHUB_ENV - echo ${{ matrix.entry.toolchain }} > .swift-version + - uses: swiftwasm/setup-swiftwasm@v1 + with: + swift-version: ${{ matrix.entry.toolchain }} - run: make bootstrap - run: make test - run: make unittest diff --git a/Makefile b/Makefile index 7b8736221..ccf22798d 100644 --- a/Makefile +++ b/Makefile @@ -2,7 +2,6 @@ MAKEFILE_DIR := $(dir $(lastword $(MAKEFILE_LIST))) .PHONY: bootstrap bootstrap: - ./scripts/install-toolchain.sh npm ci .PHONY: build From 99c99657393bc39ae30ce7bd6b1c2165cc589e35 Mon Sep 17 00:00:00 2001 From: Yuta Saito Date: Wed, 26 Oct 2022 10:28:27 +0900 Subject: [PATCH 055/373] Support Clock-based sleep APIs (#216) --- .../Sources/ConcurrencyTests/main.swift | 89 ++++++++++++------- .../JavaScriptEventLoop.swift | 32 +++++++ .../include/_CJavaScriptEventLoop.h | 9 +- 3 files changed, 98 insertions(+), 32 deletions(-) diff --git a/IntegrationTests/TestSuites/Sources/ConcurrencyTests/main.swift b/IntegrationTests/TestSuites/Sources/ConcurrencyTests/main.swift index 1e48f459f..c80e48779 100644 --- a/IntegrationTests/TestSuites/Sources/ConcurrencyTests/main.swift +++ b/IntegrationTests/TestSuites/Sources/ConcurrencyTests/main.swift @@ -8,6 +8,16 @@ import Darwin #if compiler(>=5.5) +func performanceNow() -> Double { + return JSObject.global.performance.now.function!().number! +} + +func measure(_ block: () async throws -> Void) async rethrows -> Double { + let start = performanceNow() + try await block() + return performanceNow() - start +} + func entrypoint() async throws { struct E: Error, Equatable { let value: Int @@ -61,10 +71,10 @@ func entrypoint() async throws { } 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 expectGTE(diff, 2) + let diff = try await measure { + try await Task.sleep(nanoseconds: 200_000_000) + } + try expectGTE(diff, 200) } try await asyncTest("Job reordering based on priority") { @@ -102,19 +112,19 @@ func entrypoint() async throws { try await asyncTest("Async JSClosure") { let delayClosure = JSClosure.async { _ -> JSValue in - try await Task.sleep(nanoseconds: 2_000_000_000) + try await Task.sleep(nanoseconds: 200_000_000) return JSValue.number(3) } let delayObject = JSObject.global.Object.function!.new() delayObject.closure = delayClosure.jsValue - let start = time(nil) - let promise = JSPromise(from: delayObject.closure!()) - try expectNotNil(promise) - let result = try await promise!.value - let diff = difftime(time(nil), start) - try expectGTE(diff, 2) - try expectEqual(result, .number(3)) + let diff = try await measure { + let promise = JSPromise(from: delayObject.closure!()) + try expectNotNil(promise) + let result = try await promise!.value + try expectEqual(result, .number(3)) + } + try expectGTE(diff, 200) } try await asyncTest("Async JSPromise: then") { @@ -124,18 +134,18 @@ func entrypoint() async throws { resolve(.success(JSValue.number(3))) return .undefined }.jsValue, - 1_000 + 100 ) } let promise2 = promise.then { result in - try await Task.sleep(nanoseconds: 1_000_000_000) + try await Task.sleep(nanoseconds: 100_000_000) return String(result.number!) } - let start = time(nil) - let result = try await promise2.value - let diff = difftime(time(nil), start) - try expectGTE(diff, 2) - try expectEqual(result, .string("3.0")) + let diff = try await measure { + let result = try await promise2.value + try expectEqual(result, .string("3.0")) + } + try expectGTE(diff, 200) } try await asyncTest("Async JSPromise: then(success:failure:)") { @@ -145,7 +155,7 @@ func entrypoint() async throws { resolve(.failure(JSError(message: "test").jsValue)) return .undefined }.jsValue, - 1_000 + 100 ) } let promise2 = promise.then { _ in @@ -164,26 +174,43 @@ func entrypoint() async throws { resolve(.failure(JSError(message: "test").jsValue)) return .undefined }.jsValue, - 1_000 + 100 ) } let promise2 = promise.catch { err in - try await Task.sleep(nanoseconds: 1_000_000_000) + try await Task.sleep(nanoseconds: 100_000_000) return err } - let start = time(nil) - let result = try await promise2.value - let diff = difftime(time(nil), start) - try expectGTE(diff, 2) - try expectEqual(result.object?.message, .string("test")) + let diff = try await measure { + let result = try await promise2.value + try expectEqual(result.object?.message, .string("test")) + } + try expectGTE(diff, 200) } - // 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) + let diff = try await measure { + try await Task.sleep(nanoseconds: 100_000_000) + } + try expectGTE(diff, 100) + } + + #if compiler(>=5.7) + try await asyncTest("ContinuousClock.sleep") { + let diff = try await measure { + let c = ContinuousClock() + try await c.sleep(until: .now + .milliseconds(100)) + } + try expectGTE(diff, 99) + } + try await asyncTest("SuspendingClock.sleep") { + let diff = try await measure { + let c = SuspendingClock() + try await c.sleep(until: .now + .milliseconds(100)) + } + try expectGTE(diff, 99) } + #endif } diff --git a/Sources/JavaScriptEventLoop/JavaScriptEventLoop.swift b/Sources/JavaScriptEventLoop/JavaScriptEventLoop.swift index 7c4a1c905..72dc8f503 100644 --- a/Sources/JavaScriptEventLoop/JavaScriptEventLoop.swift +++ b/Sources/JavaScriptEventLoop/JavaScriptEventLoop.swift @@ -102,6 +102,14 @@ public final class JavaScriptEventLoop: SerialExecutor, @unchecked Sendable { } swift_task_enqueueGlobalWithDelay_hook = unsafeBitCast(swift_task_enqueueGlobalWithDelay_hook_impl, to: UnsafeMutableRawPointer?.self) + #if compiler(>=5.7) + typealias swift_task_enqueueGlobalWithDeadline_hook_Fn = @convention(thin) (Int64, Int64, Int64, Int64, Int32, UnownedJob, swift_task_enqueueGlobalWithDelay_original) -> Void + let swift_task_enqueueGlobalWithDeadline_hook_impl: swift_task_enqueueGlobalWithDeadline_hook_Fn = { sec, nsec, tsec, tnsec, clock, job, original in + JavaScriptEventLoop.shared.enqueue(job, withDelay: sec, nsec, tsec, tnsec, clock) + } + swift_task_enqueueGlobalWithDeadline_hook = unsafeBitCast(swift_task_enqueueGlobalWithDeadline_hook_impl, to: UnsafeMutableRawPointer?.self) + #endif + typealias swift_task_enqueueMainExecutor_hook_Fn = @convention(thin) (UnownedJob, swift_task_enqueueMainExecutor_original) -> Void let swift_task_enqueueMainExecutor_hook_impl: swift_task_enqueueMainExecutor_hook_Fn = { job, original in JavaScriptEventLoop.shared.enqueue(job) @@ -127,6 +135,30 @@ public final class JavaScriptEventLoop: SerialExecutor, @unchecked Sendable { } } +#if compiler(>=5.7) +/// Taken from https://github.com/apple/swift/blob/d375c972f12128ec6055ed5f5337bfcae3ec67d8/stdlib/public/Concurrency/Clock.swift#L84-L88 +@_silgen_name("swift_get_time") +internal func swift_get_time( + _ seconds: UnsafeMutablePointer, + _ nanoseconds: UnsafeMutablePointer, + _ clock: CInt) + +@available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) +extension JavaScriptEventLoop { + fileprivate func enqueue( + _ job: UnownedJob, withDelay seconds: Int64, _ nanoseconds: Int64, + _ toleranceSec: Int64, _ toleranceNSec: Int64, + _ clock: Int32 + ) { + var nowSec: Int64 = 0 + var nowNSec: Int64 = 0 + swift_get_time(&nowSec, &nowNSec, clock) + let delayNanosec = (seconds - nowSec) * 1_000_000_000 + (nanoseconds - nowNSec) + enqueue(job, withDelay: delayNanosec <= 0 ? 0 : UInt64(delayNanosec)) + } +} +#endif + @available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) public extension JSPromise { /// Wait for the promise to complete, returning (or throwing) its result. diff --git a/Sources/_CJavaScriptEventLoop/include/_CJavaScriptEventLoop.h b/Sources/_CJavaScriptEventLoop/include/_CJavaScriptEventLoop.h index 51c98afc6..b24d19d04 100644 --- a/Sources/_CJavaScriptEventLoop/include/_CJavaScriptEventLoop.h +++ b/Sources/_CJavaScriptEventLoop/include/_CJavaScriptEventLoop.h @@ -35,7 +35,14 @@ typedef SWIFT_CC(swift) void (*swift_task_enqueueGlobalWithDelay_original)( SWIFT_EXPORT_FROM(swift_Concurrency) void *_Nullable swift_task_enqueueGlobalWithDelay_hook; -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) +void *_Nullable swift_task_enqueueGlobalWithDeadline_hook; /// A hook to take over main executor enqueueing. typedef SWIFT_CC(swift) void (*swift_task_enqueueMainExecutor_original)( From dbf7a880cb7d119457f6784d367b6403bdd60fb5 Mon Sep 17 00:00:00 2001 From: Yuta Saito Date: Tue, 20 Dec 2022 00:04:10 +0900 Subject: [PATCH 056/373] Prefer `UInt(bitPattern:)` for object id to guarantee uniqueness (#219) Prefer UInt(bitPattern:) for object id to guarantee uniqueness Close https://github.com/swiftwasm/JavaScriptKit/issues/218 --- Sources/JavaScriptKit/FundamentalObjects/JSClosure.swift | 4 ++-- Sources/_CJavaScriptKit/include/_CJavaScriptKit.h | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Sources/JavaScriptKit/FundamentalObjects/JSClosure.swift b/Sources/JavaScriptKit/FundamentalObjects/JSClosure.swift index c19f3ba8b..ea15c6d28 100644 --- a/Sources/JavaScriptKit/FundamentalObjects/JSClosure.swift +++ b/Sources/JavaScriptKit/FundamentalObjects/JSClosure.swift @@ -20,7 +20,7 @@ public class JSOneshotClosure: JSObject, JSClosureProtocol { super.init(id: 0) // 2. Create a new JavaScript function which calls the given Swift function. - hostFuncRef = JavaScriptHostFuncRef(bitPattern: Int32(ObjectIdentifier(self).hashValue)) + hostFuncRef = JavaScriptHostFuncRef(bitPattern: ObjectIdentifier(self)) id = withExtendedLifetime(JSString(file)) { file in _create_function(hostFuncRef, line, file.asInternalJSRef()) } @@ -86,7 +86,7 @@ public class JSClosure: JSObject, JSClosureProtocol { super.init(id: 0) // 2. Create a new JavaScript function which calls the given Swift function. - hostFuncRef = JavaScriptHostFuncRef(bitPattern: Int32(ObjectIdentifier(self).hashValue)) + hostFuncRef = JavaScriptHostFuncRef(bitPattern: ObjectIdentifier(self)) id = withExtendedLifetime(JSString(file)) { file in _create_function(hostFuncRef, line, file.asInternalJSRef()) } diff --git a/Sources/_CJavaScriptKit/include/_CJavaScriptKit.h b/Sources/_CJavaScriptKit/include/_CJavaScriptKit.h index 3bac436f4..b60007ed0 100644 --- a/Sources/_CJavaScriptKit/include/_CJavaScriptKit.h +++ b/Sources/_CJavaScriptKit/include/_CJavaScriptKit.h @@ -10,7 +10,7 @@ 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))) { From 59e7b65d1b95db7cfcb9e53c43995d4a2fca85b9 Mon Sep 17 00:00:00 2001 From: Tatsuyuki Kobayashi Date: Sun, 25 Dec 2022 00:46:28 +0900 Subject: [PATCH 057/373] Fix wrong markdown in documentation (#221) Fix wrong markdown --- Sources/JavaScriptKit/BasicObjects/JSTypedArray.swift | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/Sources/JavaScriptKit/BasicObjects/JSTypedArray.swift b/Sources/JavaScriptKit/BasicObjects/JSTypedArray.swift index ee564ae51..c22e6c8cc 100644 --- a/Sources/JavaScriptKit/BasicObjects/JSTypedArray.swift +++ b/Sources/JavaScriptKit/BasicObjects/JSTypedArray.swift @@ -10,8 +10,9 @@ public protocol TypedArrayElement: ConvertibleToJSValue, ConstructibleFromJSValu 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. +/// A wrapper around all [JavaScript `TypedArray` +/// classes](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/TypedArray) +/// that exposes their properties in a type-safe way. public class JSTypedArray: JSBridgedClass, ExpressibleByArrayLiteral where Element: TypedArrayElement { public class var constructor: JSFunction? { Element.typedArrayClass } public var jsObject: JSObject From 886919dcb062fa9585f77a0e3bb1c8a139af92f2 Mon Sep 17 00:00:00 2001 From: Francisco Javier Trujillo Mata Date: Mon, 13 Mar 2023 11:56:26 +0100 Subject: [PATCH 058/373] Add withUnsafeBytesAsync function --- .../BasicObjects/JSTypedArray.swift | 25 +++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/Sources/JavaScriptKit/BasicObjects/JSTypedArray.swift b/Sources/JavaScriptKit/BasicObjects/JSTypedArray.swift index c22e6c8cc..e30fc3eb8 100644 --- a/Sources/JavaScriptKit/BasicObjects/JSTypedArray.swift +++ b/Sources/JavaScriptKit/BasicObjects/JSTypedArray.swift @@ -88,6 +88,31 @@ public class JSTypedArray: JSBridgedClass, ExpressibleByArrayLiteral wh let result = try body(bufferPtr) return result } + + /// Calls the given async closure with a pointer to a copy of the underlying bytes of the + /// array's storage. + /// + /// - Note: The pointer passed as an argument to `body` is valid only for the + /// lifetime of the closure. Do not escape it from the async closure for later + /// use. + /// + /// - Parameter body: A closure with an `UnsafeBufferPointer` parameter + /// that points to the contiguous storage for the array. + /// If `body` has a return value, that value is also + /// used as the return value for the `withUnsafeBytes(_:)` method. The + /// argument is valid only for the duration of the closure's execution. + /// - Returns: The return value, if any, of the `body`async closure parameter. + public func withUnsafeBytesAsync(_ body: (UnsafeBufferPointer) async throws -> R) async 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 await body(bufferPtr) + return result + } } // MARK: - Int and UInt support From 9c451401e08024d187b834f75781c05dada57ae7 Mon Sep 17 00:00:00 2001 From: Yuta Saito Date: Mon, 13 Mar 2023 12:15:46 +0000 Subject: [PATCH 059/373] Explicitly select SDKROOT by Xcode --- .github/workflows/test.yml | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index d41966fe3..655da183e 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -10,9 +10,9 @@ jobs: matrix: entry: # Ensure that all host can install toolchain, build project, and run tests - - { os: macos-10.15, toolchain: wasm-5.6.0-RELEASE, wasi-backend: Node } - - { os: macos-11, toolchain: wasm-5.6.0-RELEASE, wasi-backend: Node } - - { os: macos-12, toolchain: wasm-5.6.0-RELEASE, wasi-backend: Node } + - { os: macos-10.15, toolchain: wasm-5.6.0-RELEASE, wasi-backend: Node, xcode: Xcode_12.4.app } + - { os: macos-11, toolchain: wasm-5.6.0-RELEASE, wasi-backend: Node, xcode: Xcode_13.2.1.app } + - { os: macos-12, toolchain: wasm-5.6.0-RELEASE, wasi-backend: Node, xcode: Xcode_13.4.1.app } - { os: ubuntu-18.04, toolchain: wasm-5.6.0-RELEASE, wasi-backend: Node } # Ensure that test succeeds with all toolchains and wasi backend combinations @@ -33,6 +33,9 @@ jobs: uses: actions/checkout@master with: fetch-depth: 1 + - name: Select SDKROOT + if: ${{ matrix.entry.xcode }} + run: sudo xcode-select -s /Applications/${{ matrix.entry.xcode }} - uses: swiftwasm/setup-swiftwasm@v1 with: swift-version: ${{ matrix.entry.toolchain }} From 8fbe91dc9f4619e710e03aaa3f48f35b13292b6f Mon Sep 17 00:00:00 2001 From: Yuta Saito Date: Mon, 13 Mar 2023 12:21:16 +0000 Subject: [PATCH 060/373] Guard Concurrency feature use for older toolchain compatibility --- .../BasicObjects/JSTypedArray.swift | 53 ++++++++++--------- 1 file changed, 28 insertions(+), 25 deletions(-) diff --git a/Sources/JavaScriptKit/BasicObjects/JSTypedArray.swift b/Sources/JavaScriptKit/BasicObjects/JSTypedArray.swift index e30fc3eb8..f82864e86 100644 --- a/Sources/JavaScriptKit/BasicObjects/JSTypedArray.swift +++ b/Sources/JavaScriptKit/BasicObjects/JSTypedArray.swift @@ -88,31 +88,34 @@ public class JSTypedArray: JSBridgedClass, ExpressibleByArrayLiteral wh let result = try body(bufferPtr) return result } - - /// Calls the given async closure with a pointer to a copy of the underlying bytes of the - /// array's storage. - /// - /// - Note: The pointer passed as an argument to `body` is valid only for the - /// lifetime of the closure. Do not escape it from the async closure for later - /// use. - /// - /// - Parameter body: A closure with an `UnsafeBufferPointer` parameter - /// that points to the contiguous storage for the array. - /// If `body` has a return value, that value is also - /// used as the return value for the `withUnsafeBytes(_:)` method. The - /// argument is valid only for the duration of the closure's execution. - /// - Returns: The return value, if any, of the `body`async closure parameter. - public func withUnsafeBytesAsync(_ body: (UnsafeBufferPointer) async throws -> R) async 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 await body(bufferPtr) - return result - } + + #if compiler(>=5.5) + /// Calls the given async closure with a pointer to a copy of the underlying bytes of the + /// array's storage. + /// + /// - Note: The pointer passed as an argument to `body` is valid only for the + /// lifetime of the closure. Do not escape it from the async closure for later + /// use. + /// + /// - Parameter body: A closure with an `UnsafeBufferPointer` parameter + /// that points to the contiguous storage for the array. + /// If `body` has a return value, that value is also + /// used as the return value for the `withUnsafeBytes(_:)` method. The + /// argument is valid only for the duration of the closure's execution. + /// - Returns: The return value, if any, of the `body`async closure parameter. + @available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) + public func withUnsafeBytesAsync(_ body: (UnsafeBufferPointer) async throws -> R) async rethrows -> R { + let bytesLength = lengthInBytes + let rawBuffer = 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 await body(bufferPtr) + return result + } + #endif } // MARK: - Int and UInt support From 31ae8625ac5fcb2e64efefc4dc1a4aa879124234 Mon Sep 17 00:00:00 2001 From: Yuta Saito Date: Mon, 13 Mar 2023 12:37:00 +0000 Subject: [PATCH 061/373] Reduce byteLength calls for JSTypedArray --- Sources/JavaScriptKit/BasicObjects/JSTypedArray.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Sources/JavaScriptKit/BasicObjects/JSTypedArray.swift b/Sources/JavaScriptKit/BasicObjects/JSTypedArray.swift index f82864e86..0b5e0b3f9 100644 --- a/Sources/JavaScriptKit/BasicObjects/JSTypedArray.swift +++ b/Sources/JavaScriptKit/BasicObjects/JSTypedArray.swift @@ -82,7 +82,7 @@ public class JSTypedArray: JSBridgedClass, ExpressibleByArrayLiteral wh let rawBuffer = malloc(bytesLength)! defer { free(rawBuffer) } _load_typed_array(jsObject.id, rawBuffer.assumingMemoryBound(to: UInt8.self)) - let length = lengthInBytes / MemoryLayout.size + let length = bytesLength / MemoryLayout.size let boundPtr = rawBuffer.bindMemory(to: Element.self, capacity: length) let bufferPtr = UnsafeBufferPointer(start: boundPtr, count: length) let result = try body(bufferPtr) @@ -109,7 +109,7 @@ public class JSTypedArray: JSBridgedClass, ExpressibleByArrayLiteral wh let rawBuffer = malloc(bytesLength)! defer { free(rawBuffer) } _load_typed_array(jsObject.id, rawBuffer.assumingMemoryBound(to: UInt8.self)) - let length = lengthInBytes / MemoryLayout.size + let length = bytesLength / MemoryLayout.size let boundPtr = rawBuffer.bindMemory(to: Element.self, capacity: length) let bufferPtr = UnsafeBufferPointer(start: boundPtr, count: length) let result = try await body(bufferPtr) From 07a06f12ebaee662e2f2c6522fabfd175cb1edaa Mon Sep 17 00:00:00 2001 From: Yuta Saito Date: Mon, 13 Mar 2023 12:55:04 +0000 Subject: [PATCH 062/373] Use UnsafeMutableBufferPointer.allocate instead of malloc directly --- .../BasicObjects/JSTypedArray.swift | 28 ++++++++++++------- 1 file changed, 18 insertions(+), 10 deletions(-) diff --git a/Sources/JavaScriptKit/BasicObjects/JSTypedArray.swift b/Sources/JavaScriptKit/BasicObjects/JSTypedArray.swift index 0b5e0b3f9..963419c99 100644 --- a/Sources/JavaScriptKit/BasicObjects/JSTypedArray.swift +++ b/Sources/JavaScriptKit/BasicObjects/JSTypedArray.swift @@ -79,12 +79,16 @@ public class JSTypedArray: JSBridgedClass, ExpressibleByArrayLiteral wh /// - 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 rawBuffer = UnsafeMutableBufferPointer.allocate(capacity: bytesLength) + defer { rawBuffer.deallocate() } + let baseAddress = rawBuffer.baseAddress! + _load_typed_array(jsObject.id, baseAddress) let length = bytesLength / MemoryLayout.size - let boundPtr = rawBuffer.bindMemory(to: Element.self, capacity: length) - let bufferPtr = UnsafeBufferPointer(start: boundPtr, count: length) + let rawBaseAddress = UnsafeRawPointer(baseAddress) + let bufferPtr = UnsafeBufferPointer( + start: rawBaseAddress.assumingMemoryBound(to: Element.self), + count: length + ) let result = try body(bufferPtr) return result } @@ -106,12 +110,16 @@ public class JSTypedArray: JSBridgedClass, ExpressibleByArrayLiteral wh @available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) public func withUnsafeBytesAsync(_ body: (UnsafeBufferPointer) async throws -> R) async rethrows -> R { let bytesLength = lengthInBytes - let rawBuffer = malloc(bytesLength)! - defer { free(rawBuffer) } - _load_typed_array(jsObject.id, rawBuffer.assumingMemoryBound(to: UInt8.self)) + let rawBuffer = UnsafeMutableBufferPointer.allocate(capacity: bytesLength) + defer { rawBuffer.deallocate() } + let baseAddress = rawBuffer.baseAddress! + _load_typed_array(jsObject.id, baseAddress) let length = bytesLength / MemoryLayout.size - let boundPtr = rawBuffer.bindMemory(to: Element.self, capacity: length) - let bufferPtr = UnsafeBufferPointer(start: boundPtr, count: length) + let rawBaseAddress = UnsafeRawPointer(baseAddress) + let bufferPtr = UnsafeBufferPointer( + start: rawBaseAddress.assumingMemoryBound(to: Element.self), + count: length + ) let result = try await body(bufferPtr) return result } From 096584bb6959f16d97daf3ebf52039f98c36fdbf Mon Sep 17 00:00:00 2001 From: Yuta Saito Date: Mon, 13 Mar 2023 14:16:16 +0000 Subject: [PATCH 063/373] Bump version to 0.18.0, update `CHANGELOG.md` --- CHANGELOG.md | 17 +++++++++++++++++ Example/package-lock.json | 2 +- package-lock.json | 4 ++-- package.json | 2 +- 4 files changed, 21 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d0d336ed5..2f7d8d537 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,20 @@ +# 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`. diff --git a/Example/package-lock.json b/Example/package-lock.json index 24b287a21..547887116 100644 --- a/Example/package-lock.json +++ b/Example/package-lock.json @@ -21,7 +21,7 @@ }, "..": { "name": "javascript-kit-swift", - "version": "0.17.0", + "version": "0.18.0", "license": "MIT", "devDependencies": { "@rollup/plugin-typescript": "^8.3.1", diff --git a/package-lock.json b/package-lock.json index dcc23dc1b..a2ac922e1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "javascript-kit-swift", - "version": "0.17.0", + "version": "0.18.0", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "javascript-kit-swift", - "version": "0.17.0", + "version": "0.18.0", "license": "MIT", "devDependencies": { "@rollup/plugin-typescript": "^8.3.1", diff --git a/package.json b/package.json index 8bd772b21..a7faddd43 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "javascript-kit-swift", - "version": "0.17.0", + "version": "0.18.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", From a7f981ab31a7643c29e666943ce36905c407c688 Mon Sep 17 00:00:00 2001 From: Yuta Saito Date: Fri, 28 Apr 2023 08:59:52 +0000 Subject: [PATCH 064/373] Update 5.7 patch version --- .github/workflows/test.yml | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 655da183e..935ca571d 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -10,19 +10,19 @@ jobs: matrix: entry: # Ensure that all host can install toolchain, build project, and run tests - - { os: macos-10.15, toolchain: wasm-5.6.0-RELEASE, wasi-backend: Node, xcode: Xcode_12.4.app } - - { os: macos-11, toolchain: wasm-5.6.0-RELEASE, wasi-backend: Node, xcode: Xcode_13.2.1.app } - - { os: macos-12, toolchain: wasm-5.6.0-RELEASE, wasi-backend: Node, xcode: Xcode_13.4.1.app } - - { os: ubuntu-18.04, toolchain: wasm-5.6.0-RELEASE, wasi-backend: Node } + - { os: macos-10.15, toolchain: wasm-5.7.3-RELEASE, wasi-backend: Node, xcode: Xcode_12.4.app } + - { os: macos-11, toolchain: wasm-5.7.3-RELEASE, wasi-backend: Node, xcode: Xcode_13.2.1.app } + - { os: macos-12, toolchain: wasm-5.7.3-RELEASE, wasi-backend: Node, xcode: Xcode_13.4.1.app } + - { os: ubuntu-18.04, toolchain: wasm-5.7.3-RELEASE, wasi-backend: Node } # Ensure that test succeeds with all toolchains and wasi backend combinations - { os: ubuntu-20.04, toolchain: wasm-5.5.0-RELEASE, wasi-backend: Node } - { os: ubuntu-20.04, toolchain: wasm-5.6.0-RELEASE, wasi-backend: Node } - - { os: ubuntu-20.04, toolchain: wasm-5.7.1-RELEASE, wasi-backend: Node } + - { os: ubuntu-20.04, toolchain: wasm-5.7.3-RELEASE, wasi-backend: Node } - { os: ubuntu-20.04, toolchain: wasm-5.6.0-RELEASE, wasi-backend: Wasmer } - - { os: ubuntu-20.04, toolchain: wasm-5.7.1-RELEASE, wasi-backend: Wasmer } + - { os: ubuntu-20.04, toolchain: wasm-5.7.3-RELEASE, wasi-backend: Wasmer } - { os: ubuntu-20.04, toolchain: wasm-5.6.0-RELEASE, wasi-backend: MicroWASI } - - { os: ubuntu-20.04, toolchain: wasm-5.7.1-RELEASE, wasi-backend: MicroWASI } + - { os: ubuntu-20.04, toolchain: wasm-5.7.3-RELEASE, wasi-backend: MicroWASI } runs-on: ${{ matrix.entry.os }} env: From d29f14483ce3c47df33afa09897e059966a21068 Mon Sep 17 00:00:00 2001 From: Yuta Saito Date: Fri, 28 Apr 2023 09:08:46 +0000 Subject: [PATCH 065/373] Use Ubuntu 22.04 instead of obsolete 18.04 --- .github/workflows/test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 935ca571d..768c2747c 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -13,7 +13,7 @@ jobs: - { os: macos-10.15, toolchain: wasm-5.7.3-RELEASE, wasi-backend: Node, xcode: Xcode_12.4.app } - { os: macos-11, toolchain: wasm-5.7.3-RELEASE, wasi-backend: Node, xcode: Xcode_13.2.1.app } - { os: macos-12, toolchain: wasm-5.7.3-RELEASE, wasi-backend: Node, xcode: Xcode_13.4.1.app } - - { os: ubuntu-18.04, toolchain: wasm-5.7.3-RELEASE, wasi-backend: Node } + - { os: ubuntu-22.04, toolchain: wasm-5.7.3-RELEASE, wasi-backend: Node } # Ensure that test succeeds with all toolchains and wasi backend combinations - { os: ubuntu-20.04, toolchain: wasm-5.5.0-RELEASE, wasi-backend: Node } From 373a01109628083b2bce400e528f4f54a7912a8e Mon Sep 17 00:00:00 2001 From: Yuta Saito Date: Fri, 28 Apr 2023 09:15:48 +0000 Subject: [PATCH 066/373] Oops we don't support Ubuntu 22 in 5.7 release branch yet --- .github/workflows/test.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 768c2747c..4b408e518 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -13,7 +13,6 @@ jobs: - { os: macos-10.15, toolchain: wasm-5.7.3-RELEASE, wasi-backend: Node, xcode: Xcode_12.4.app } - { os: macos-11, toolchain: wasm-5.7.3-RELEASE, wasi-backend: Node, xcode: Xcode_13.2.1.app } - { os: macos-12, toolchain: wasm-5.7.3-RELEASE, wasi-backend: Node, xcode: Xcode_13.4.1.app } - - { os: ubuntu-22.04, toolchain: wasm-5.7.3-RELEASE, wasi-backend: Node } # Ensure that test succeeds with all toolchains and wasi backend combinations - { os: ubuntu-20.04, toolchain: wasm-5.5.0-RELEASE, wasi-backend: Node } From a962db316e6a6bc67b930278fdb3823e79f06a9f Mon Sep 17 00:00:00 2001 From: Yuta Saito Date: Fri, 28 Apr 2023 09:36:00 +0000 Subject: [PATCH 067/373] Add 5.8 toolchain matrix --- .github/workflows/test.yml | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 4b408e518..d12890f5d 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -10,18 +10,22 @@ jobs: matrix: entry: # Ensure that all host can install toolchain, build project, and run tests - - { os: macos-10.15, toolchain: wasm-5.7.3-RELEASE, wasi-backend: Node, xcode: Xcode_12.4.app } - - { os: macos-11, toolchain: wasm-5.7.3-RELEASE, wasi-backend: Node, xcode: Xcode_13.2.1.app } - - { os: macos-12, toolchain: wasm-5.7.3-RELEASE, wasi-backend: Node, xcode: Xcode_13.4.1.app } + - { os: macos-10.15, toolchain: wasm-5.8-SNAPSHOT-2023-04-26-a, wasi-backend: Node, xcode: Xcode_12.4.app } + - { os: macos-11, toolchain: wasm-5.8-SNAPSHOT-2023-04-26-a, wasi-backend: Node, xcode: Xcode_13.2.1.app } + - { os: macos-12, toolchain: wasm-5.8-SNAPSHOT-2023-04-26-a, wasi-backend: Node, xcode: Xcode_13.4.1.app } + - { os: ubuntu-22.04, toolchain: wasm-5.8-SNAPSHOT-2023-04-26-a, wasi-backend: Node } # Ensure that test succeeds with all toolchains and wasi backend combinations - { os: ubuntu-20.04, toolchain: wasm-5.5.0-RELEASE, wasi-backend: Node } - { os: ubuntu-20.04, toolchain: wasm-5.6.0-RELEASE, wasi-backend: Node } - { os: ubuntu-20.04, toolchain: wasm-5.7.3-RELEASE, wasi-backend: Node } + - { os: ubuntu-20.04, toolchain: wasm-5.8-SNAPSHOT-2023-04-26-a, wasi-backend: Node } - { os: ubuntu-20.04, toolchain: wasm-5.6.0-RELEASE, wasi-backend: Wasmer } - { os: ubuntu-20.04, toolchain: wasm-5.7.3-RELEASE, wasi-backend: Wasmer } + - { os: ubuntu-20.04, toolchain: wasm-5.8-SNAPSHOT-2023-04-26-a, wasi-backend: Wasmer } - { os: ubuntu-20.04, toolchain: wasm-5.6.0-RELEASE, wasi-backend: MicroWASI } - { os: ubuntu-20.04, toolchain: wasm-5.7.3-RELEASE, wasi-backend: MicroWASI } + - { os: ubuntu-20.04, toolchain: wasm-5.8-SNAPSHOT-2023-04-26-a, wasi-backend: MicroWASI } runs-on: ${{ matrix.entry.os }} env: From 1ef9e6cd1a47491dbe77f0a87068b0d05adb2577 Mon Sep 17 00:00:00 2001 From: Yuta Saito Date: Mon, 8 May 2023 00:27:25 +0000 Subject: [PATCH 068/373] Use stable release --- .github/workflows/test.yml | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index d12890f5d..0fc529b6a 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -10,22 +10,22 @@ jobs: matrix: entry: # Ensure that all host can install toolchain, build project, and run tests - - { os: macos-10.15, toolchain: wasm-5.8-SNAPSHOT-2023-04-26-a, wasi-backend: Node, xcode: Xcode_12.4.app } - - { os: macos-11, toolchain: wasm-5.8-SNAPSHOT-2023-04-26-a, wasi-backend: Node, xcode: Xcode_13.2.1.app } - - { os: macos-12, toolchain: wasm-5.8-SNAPSHOT-2023-04-26-a, wasi-backend: Node, xcode: Xcode_13.4.1.app } - - { os: ubuntu-22.04, toolchain: wasm-5.8-SNAPSHOT-2023-04-26-a, wasi-backend: Node } + - { os: macos-10.15, toolchain: wasm-5.8.0-RELEASE, wasi-backend: Node, xcode: Xcode_12.4.app } + - { os: macos-11, toolchain: wasm-5.8.0-RELEASE, wasi-backend: Node, xcode: Xcode_13.2.1.app } + - { os: macos-12, toolchain: wasm-5.8.0-RELEASE, wasi-backend: Node, xcode: Xcode_13.4.1.app } + - { os: ubuntu-22.04, toolchain: wasm-5.8.0-RELEASE, wasi-backend: Node } # Ensure that test succeeds with all toolchains and wasi backend combinations - { os: ubuntu-20.04, toolchain: wasm-5.5.0-RELEASE, wasi-backend: Node } - { os: ubuntu-20.04, toolchain: wasm-5.6.0-RELEASE, wasi-backend: Node } - { os: ubuntu-20.04, toolchain: wasm-5.7.3-RELEASE, wasi-backend: Node } - - { os: ubuntu-20.04, toolchain: wasm-5.8-SNAPSHOT-2023-04-26-a, wasi-backend: Node } + - { os: ubuntu-20.04, toolchain: wasm-5.8.0-RELEASE, wasi-backend: Node } - { os: ubuntu-20.04, toolchain: wasm-5.6.0-RELEASE, wasi-backend: Wasmer } - { os: ubuntu-20.04, toolchain: wasm-5.7.3-RELEASE, wasi-backend: Wasmer } - - { os: ubuntu-20.04, toolchain: wasm-5.8-SNAPSHOT-2023-04-26-a, wasi-backend: Wasmer } + - { os: ubuntu-20.04, toolchain: wasm-5.8.0-RELEASE, wasi-backend: Wasmer } - { os: ubuntu-20.04, toolchain: wasm-5.6.0-RELEASE, wasi-backend: MicroWASI } - { os: ubuntu-20.04, toolchain: wasm-5.7.3-RELEASE, wasi-backend: MicroWASI } - - { os: ubuntu-20.04, toolchain: wasm-5.8-SNAPSHOT-2023-04-26-a, wasi-backend: MicroWASI } + - { os: ubuntu-20.04, toolchain: wasm-5.8.0-RELEASE, wasi-backend: MicroWASI } runs-on: ${{ matrix.entry.os }} env: From 91567e909cfd4496378f5c679348a2dec9216065 Mon Sep 17 00:00:00 2001 From: Yuta Saito Date: Mon, 8 May 2023 04:35:57 +0000 Subject: [PATCH 069/373] Drop macOS 10.15 and add macOS 13 --- .github/workflows/test.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 0fc529b6a..c803379ce 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -10,9 +10,9 @@ jobs: matrix: entry: # Ensure that all host can install toolchain, build project, and run tests - - { os: macos-10.15, toolchain: wasm-5.8.0-RELEASE, wasi-backend: Node, xcode: Xcode_12.4.app } - { os: macos-11, toolchain: wasm-5.8.0-RELEASE, wasi-backend: Node, xcode: Xcode_13.2.1.app } - { os: macos-12, toolchain: wasm-5.8.0-RELEASE, wasi-backend: Node, xcode: Xcode_13.4.1.app } + - { os: macos-13, toolchain: wasm-5.8.0-RELEASE, wasi-backend: Node, xcode: Xcode_14.3.app } - { os: ubuntu-22.04, toolchain: wasm-5.8.0-RELEASE, wasi-backend: Node } # Ensure that test succeeds with all toolchains and wasi backend combinations @@ -57,14 +57,14 @@ jobs: strategy: matrix: include: - - os: macos-10.15 - xcode: Xcode_12.4 - os: macos-11 xcode: Xcode_13.2.1 - os: macos-12 xcode: Xcode_13.3 - os: macos-12 xcode: Xcode_14.0 + - os: macos-13 + xcode: Xcode_14.3 runs-on: ${{ matrix.os }} steps: - uses: actions/checkout@v2 From 32780ce4f4046c44db9ca24d8134a8b07f219cc8 Mon Sep 17 00:00:00 2001 From: Yuta Saito Date: Fri, 21 Jul 2023 10:03:56 +0900 Subject: [PATCH 070/373] Update README.md --- README.md | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/README.md b/README.md index 0c2c6988c..a5d0c7336 100644 --- a/README.md +++ b/README.md @@ -253,3 +253,12 @@ $ swift --version Swift version 5.6 (swiftlang-5.6.0) Target: arm64-apple-darwin20.6.0 ``` + +## Sponsoring + +[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. + + + + + From c283fa3cf069a56ecf770a5fd6ce69f5e6e5d091 Mon Sep 17 00:00:00 2001 From: Yuta Saito Date: Sun, 23 Jul 2023 10:14:42 +0900 Subject: [PATCH 071/373] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index a5d0c7336..7c27b0671 100644 --- a/README.md +++ b/README.md @@ -260,5 +260,5 @@ Target: arm64-apple-darwin20.6.0 - + From 64c95267c3c9a505e3326bd5821a1ad63097901e Mon Sep 17 00:00:00 2001 From: STREGA Date: Sat, 5 Aug 2023 15:04:29 -0400 Subject: [PATCH 072/373] Remove duplicated generic definition The functions parent scope already defines the same generic. --- Sources/JavaScriptKit/ConvertibleToJSValue.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/JavaScriptKit/ConvertibleToJSValue.swift b/Sources/JavaScriptKit/ConvertibleToJSValue.swift index 638672ca5..4b9bf8f03 100644 --- a/Sources/JavaScriptKit/ConvertibleToJSValue.swift +++ b/Sources/JavaScriptKit/ConvertibleToJSValue.swift @@ -256,7 +256,7 @@ extension Array where Element == ConvertibleToJSValue { // fast path for empty array guard self.count != 0 else { return body([]) } - func _withRawJSValues( + func _withRawJSValues( _ values: [ConvertibleToJSValue], _ index: Int, _ results: inout [RawJSValue], _ body: ([RawJSValue]) -> T ) -> T { From 710a5e326efe51107b9760b8c206e66832d3ef50 Mon Sep 17 00:00:00 2001 From: STREGA Date: Sat, 5 Aug 2023 15:19:32 -0400 Subject: [PATCH 073/373] Restrict wasi attributes to wasi builds Added simply to silence unknown attribute warnings when building for macOS using Xcode. --- .../_CJavaScriptBigIntSupport/include/_CJavaScriptKit+I64.h | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/Sources/_CJavaScriptBigIntSupport/include/_CJavaScriptKit+I64.h b/Sources/_CJavaScriptBigIntSupport/include/_CJavaScriptKit+I64.h index dc898c43c..a04be1c2e 100644 --- a/Sources/_CJavaScriptBigIntSupport/include/_CJavaScriptKit+I64.h +++ b/Sources/_CJavaScriptBigIntSupport/include/_CJavaScriptKit+I64.h @@ -8,16 +8,20 @@ /// /// @param value The value to convert. /// @param is_signed Whether to treat the value as a signed integer or not. +#if __wasi__ __attribute__((__import_module__("javascript_kit"), __import_name__("swjs_i64_to_bigint"))) +#endif extern JavaScriptObjectRef _i64_to_bigint(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. +#if __wasi__ __attribute__((__import_module__("javascript_kit"), __import_name__("swjs_bigint_to_i64"))) +#endif extern long long _bigint_to_i64(const JavaScriptObjectRef ref, bool is_signed); #endif /* _CJavaScriptBigIntSupport_h */ From a5b135d075179658920d1113566b8ce25559f2a9 Mon Sep 17 00:00:00 2001 From: STREGA Date: Tue, 15 Aug 2023 21:06:59 -0400 Subject: [PATCH 074/373] Fix deprecations --- Sources/JavaScriptEventLoop/JavaScriptEventLoop.swift | 4 ++++ Sources/JavaScriptEventLoop/JobQueue.swift | 4 ++++ 2 files changed, 8 insertions(+) diff --git a/Sources/JavaScriptEventLoop/JavaScriptEventLoop.swift b/Sources/JavaScriptEventLoop/JavaScriptEventLoop.swift index 72dc8f503..8e07ce44f 100644 --- a/Sources/JavaScriptEventLoop/JavaScriptEventLoop.swift +++ b/Sources/JavaScriptEventLoop/JavaScriptEventLoop.swift @@ -122,7 +122,11 @@ public final class JavaScriptEventLoop: SerialExecutor, @unchecked Sendable { private func enqueue(_ job: UnownedJob, withDelay nanoseconds: UInt64) { let milliseconds = nanoseconds / 1_000_000 setTimeout(Double(milliseconds), { + #if compiler(>=5.9) + job.runSynchronously(on: self.asUnownedSerialExecutor()) + #else job._runSynchronously(on: self.asUnownedSerialExecutor()) + #endif }) } diff --git a/Sources/JavaScriptEventLoop/JobQueue.swift b/Sources/JavaScriptEventLoop/JobQueue.swift index 44b2f7249..6cc0cfc35 100644 --- a/Sources/JavaScriptEventLoop/JobQueue.swift +++ b/Sources/JavaScriptEventLoop/JobQueue.swift @@ -43,7 +43,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 From f0bfc094a5b957ca9aa33391988cfe36ab1ecf5e Mon Sep 17 00:00:00 2001 From: Yuta Saito Date: Mon, 15 Jan 2024 21:38:56 +0900 Subject: [PATCH 075/373] Update `Executor/enqueue` conformance to use `ExecutorJob` --- Sources/JavaScriptEventLoop/JavaScriptEventLoop.swift | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/Sources/JavaScriptEventLoop/JavaScriptEventLoop.swift b/Sources/JavaScriptEventLoop/JavaScriptEventLoop.swift index 8e07ce44f..7f7e69971 100644 --- a/Sources/JavaScriptEventLoop/JavaScriptEventLoop.swift +++ b/Sources/JavaScriptEventLoop/JavaScriptEventLoop.swift @@ -130,9 +130,17 @@ public final class JavaScriptEventLoop: SerialExecutor, @unchecked Sendable { }) } + #if compiler(>=5.9) + public func enqueue(_ job: consuming ExecutorJob) { + // NOTE: Converting a `ExecutorJob` to an ``UnownedJob`` and invoking + // ``UnownedJob/runSynchronously(_:)` on it multiple times is undefined behavior. + insertJobQueue(job: UnownedJob(job)) + } + #else public func enqueue(_ job: UnownedJob) { insertJobQueue(job: job) } + #endif public func asUnownedSerialExecutor() -> UnownedSerialExecutor { return UnownedSerialExecutor(ordinary: self) From b31f36ce643b0b12e855c65ab2d387f4b5c5715f Mon Sep 17 00:00:00 2001 From: Yuta Saito Date: Mon, 15 Jan 2024 21:41:46 +0900 Subject: [PATCH 076/373] Update CI toolchain --- .github/workflows/test.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index c803379ce..bc492d01c 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -10,10 +10,10 @@ jobs: matrix: entry: # Ensure that all host can install toolchain, build project, and run tests - - { os: macos-11, toolchain: wasm-5.8.0-RELEASE, wasi-backend: Node, xcode: Xcode_13.2.1.app } - - { os: macos-12, toolchain: wasm-5.8.0-RELEASE, wasi-backend: Node, xcode: Xcode_13.4.1.app } - - { os: macos-13, toolchain: wasm-5.8.0-RELEASE, wasi-backend: Node, xcode: Xcode_14.3.app } - - { os: ubuntu-22.04, toolchain: wasm-5.8.0-RELEASE, wasi-backend: Node } + - { os: macos-11, toolchain: wasm-5.9-SNAPSHOT-2024-01-12-a, wasi-backend: Node, xcode: Xcode_13.2.1.app } + - { os: macos-12, toolchain: wasm-5.9-SNAPSHOT-2024-01-12-a, wasi-backend: Node, xcode: Xcode_13.4.1.app } + - { os: macos-13, toolchain: wasm-5.9-SNAPSHOT-2024-01-12-a, wasi-backend: Node, xcode: Xcode_14.3.app } + - { os: ubuntu-22.04, toolchain: wasm-5.9-SNAPSHOT-2024-01-12-a, wasi-backend: Node } # Ensure that test succeeds with all toolchains and wasi backend combinations - { os: ubuntu-20.04, toolchain: wasm-5.5.0-RELEASE, wasi-backend: Node } From a70c1e5dc4d931e97e66a98e74712ec29326efcb Mon Sep 17 00:00:00 2001 From: Yuta Saito Date: Mon, 15 Jan 2024 21:50:40 +0900 Subject: [PATCH 077/373] Drop 5.5, 5.6 support --- .github/workflows/test.yml | 5 ----- 1 file changed, 5 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index bc492d01c..053121911 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -16,14 +16,10 @@ jobs: - { os: ubuntu-22.04, toolchain: wasm-5.9-SNAPSHOT-2024-01-12-a, wasi-backend: Node } # Ensure that test succeeds with all toolchains and wasi backend combinations - - { os: ubuntu-20.04, toolchain: wasm-5.5.0-RELEASE, wasi-backend: Node } - - { os: ubuntu-20.04, toolchain: wasm-5.6.0-RELEASE, wasi-backend: Node } - { os: ubuntu-20.04, toolchain: wasm-5.7.3-RELEASE, wasi-backend: Node } - { os: ubuntu-20.04, toolchain: wasm-5.8.0-RELEASE, wasi-backend: Node } - - { os: ubuntu-20.04, toolchain: wasm-5.6.0-RELEASE, wasi-backend: Wasmer } - { os: ubuntu-20.04, toolchain: wasm-5.7.3-RELEASE, wasi-backend: Wasmer } - { os: ubuntu-20.04, toolchain: wasm-5.8.0-RELEASE, wasi-backend: Wasmer } - - { os: ubuntu-20.04, toolchain: wasm-5.6.0-RELEASE, wasi-backend: MicroWASI } - { os: ubuntu-20.04, toolchain: wasm-5.7.3-RELEASE, wasi-backend: MicroWASI } - { os: ubuntu-20.04, toolchain: wasm-5.8.0-RELEASE, wasi-backend: MicroWASI } @@ -45,7 +41,6 @@ jobs: - run: make bootstrap - run: make test - run: make unittest - if: ${{ startsWith(matrix.toolchain, 'wasm-5.7.') }} - name: Check if SwiftPM resources are stale run: | make regenerate_swiftpm_resources From dd511ff6398bf9782deb86d457b5c02f07efbf3b Mon Sep 17 00:00:00 2001 From: Yuta Saito Date: Mon, 15 Jan 2024 21:51:03 +0900 Subject: [PATCH 078/373] Check wasmer/wasi and uwasi with 5.9 toolchain --- .github/workflows/test.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 053121911..aadda11af 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -20,8 +20,10 @@ jobs: - { os: ubuntu-20.04, toolchain: wasm-5.8.0-RELEASE, wasi-backend: Node } - { os: ubuntu-20.04, toolchain: wasm-5.7.3-RELEASE, wasi-backend: Wasmer } - { os: ubuntu-20.04, toolchain: wasm-5.8.0-RELEASE, wasi-backend: Wasmer } + - { os: ubuntu-20.04, toolchain: wasm-5.9-SNAPSHOT-2024-01-12-a, wasi-backend: Wasmer } - { os: ubuntu-20.04, toolchain: wasm-5.7.3-RELEASE, wasi-backend: MicroWASI } - { os: ubuntu-20.04, toolchain: wasm-5.8.0-RELEASE, wasi-backend: MicroWASI } + - { os: ubuntu-20.04, toolchain: wasm-5.9-SNAPSHOT-2024-01-12-a, wasi-backend: MicroWASI } runs-on: ${{ matrix.entry.os }} env: From 598c8b963e79f76cc05e5359bed6865ff0b51059 Mon Sep 17 00:00:00 2001 From: Yuta Saito Date: Mon, 15 Jan 2024 21:51:08 +0900 Subject: [PATCH 079/373] Fix for Node.js 20 --- IntegrationTests/TestSuites/Sources/ConcurrencyTests/main.swift | 2 +- IntegrationTests/lib.js | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/IntegrationTests/TestSuites/Sources/ConcurrencyTests/main.swift b/IntegrationTests/TestSuites/Sources/ConcurrencyTests/main.swift index c80e48779..96c2711b2 100644 --- a/IntegrationTests/TestSuites/Sources/ConcurrencyTests/main.swift +++ b/IntegrationTests/TestSuites/Sources/ConcurrencyTests/main.swift @@ -9,7 +9,7 @@ import Darwin #if compiler(>=5.5) func performanceNow() -> Double { - return JSObject.global.performance.now.function!().number! + return JSObject.global.performance.now().number! } func measure(_ block: () async throws -> Void) async rethrows -> Double { diff --git a/IntegrationTests/lib.js b/IntegrationTests/lib.js index e708816cb..1d7b6342d 100644 --- a/IntegrationTests/lib.js +++ b/IntegrationTests/lib.js @@ -64,6 +64,7 @@ const WASI = { args: [programName], env: {}, returnOnExit: false, + version: "preview1", }) return { From 3f0343bc8e11f986acd5d9a93421e75892e8cc1d Mon Sep 17 00:00:00 2001 From: Yuta Saito Date: Mon, 15 Jan 2024 14:00:18 +0000 Subject: [PATCH 080/373] Ensure that async tests run all checks --- .../TestSuites/Sources/ConcurrencyTests/UnitTestUtils.swift | 2 ++ 1 file changed, 2 insertions(+) diff --git a/IntegrationTests/TestSuites/Sources/ConcurrencyTests/UnitTestUtils.swift b/IntegrationTests/TestSuites/Sources/ConcurrencyTests/UnitTestUtils.swift index 40c3165da..acd81e6d9 100644 --- a/IntegrationTests/TestSuites/Sources/ConcurrencyTests/UnitTestUtils.swift +++ b/IntegrationTests/TestSuites/Sources/ConcurrencyTests/UnitTestUtils.swift @@ -15,6 +15,7 @@ func test(_ name: String, testBlock: () throws -> Void) throws { print(error) throw error } + print("✅ \(name)") } func asyncTest(_ name: String, testBlock: () async throws -> Void) async throws -> Void { @@ -26,6 +27,7 @@ func asyncTest(_ name: String, testBlock: () async throws -> Void) async throws print(error) throw error } + print("✅ \(name)") } struct MessageError: Error { From 3b0541a69d3a76e1a3bc418b8b9ab67c484b0cfd Mon Sep 17 00:00:00 2001 From: Yuta Saito Date: Mon, 15 Jan 2024 14:02:18 +0000 Subject: [PATCH 081/373] Drop the workaround for old SwiftPM --- .../Sources/ConcurrencyTests/main.swift | 33 ------------------- 1 file changed, 33 deletions(-) diff --git a/IntegrationTests/TestSuites/Sources/ConcurrencyTests/main.swift b/IntegrationTests/TestSuites/Sources/ConcurrencyTests/main.swift index 96c2711b2..ece58b317 100644 --- a/IntegrationTests/TestSuites/Sources/ConcurrencyTests/main.swift +++ b/IntegrationTests/TestSuites/Sources/ConcurrencyTests/main.swift @@ -6,8 +6,6 @@ import WASILibc import Darwin #endif -#if compiler(>=5.5) - func performanceNow() -> Double { return JSObject.global.performance.now().number! } @@ -213,32 +211,6 @@ func entrypoint() async throws { #endif } - -// 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 { @@ -247,8 +219,3 @@ Task { print(error) } } - -#endif - - -#endif From 1e62ce4bb0e8b08cc3429bd799aef9f8df03f6f7 Mon Sep 17 00:00:00 2001 From: Yuta Saito Date: Mon, 15 Jan 2024 14:07:59 +0000 Subject: [PATCH 082/373] Update uwasi 1.2.0 With clock_gettime fix and stdio buffering issue --- IntegrationTests/package-lock.json | 16 ++++++++-------- IntegrationTests/package.json | 2 +- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/IntegrationTests/package-lock.json b/IntegrationTests/package-lock.json index cc6ed8de9..8ccbcdc21 100644 --- a/IntegrationTests/package-lock.json +++ b/IntegrationTests/package-lock.json @@ -8,12 +8,12 @@ "@wasmer/wasi": "^0.12.0", "@wasmer/wasmfs": "^0.12.0", "javascript-kit-swift": "file:..", - "uwasi": "^1.0.0" + "uwasi": "^1.2.0" } }, "..": { "name": "javascript-kit-swift", - "version": "0.14.0", + "version": "0.18.0", "license": "MIT", "devDependencies": { "@rollup/plugin-typescript": "^8.3.1", @@ -219,9 +219,9 @@ "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=" }, "node_modules/uwasi": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/uwasi/-/uwasi-1.0.0.tgz", - "integrity": "sha512-xnjYEegIsUDh7aXnT6s+pNK79adEQs5R+T+fds/fFdCEtoKFkH3ngwbp3jAJjB91VfPgOVUKIH+fNbg6Om8xAw==" + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/uwasi/-/uwasi-1.2.0.tgz", + "integrity": "sha512-+U3ajjQgx/Xh1/ZNrgH0EzM5qI2czr94oz3DPDwTvUIlM4SFpDjTqJzDA3xcqlTmpp2YGpxApmjwZfablMUoOg==" }, "node_modules/wrappy": { "version": "1.0.2", @@ -400,9 +400,9 @@ "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=" }, "uwasi": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/uwasi/-/uwasi-1.0.0.tgz", - "integrity": "sha512-xnjYEegIsUDh7aXnT6s+pNK79adEQs5R+T+fds/fFdCEtoKFkH3ngwbp3jAJjB91VfPgOVUKIH+fNbg6Om8xAw==" + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/uwasi/-/uwasi-1.2.0.tgz", + "integrity": "sha512-+U3ajjQgx/Xh1/ZNrgH0EzM5qI2czr94oz3DPDwTvUIlM4SFpDjTqJzDA3xcqlTmpp2YGpxApmjwZfablMUoOg==" }, "wrappy": { "version": "1.0.2", diff --git a/IntegrationTests/package.json b/IntegrationTests/package.json index 3458b7385..a7d756165 100644 --- a/IntegrationTests/package.json +++ b/IntegrationTests/package.json @@ -3,7 +3,7 @@ "dependencies": { "@wasmer/wasi": "^0.12.0", "@wasmer/wasmfs": "^0.12.0", - "uwasi": "^1.0.0", + "uwasi": "^1.2.0", "javascript-kit-swift": "file:.." } } From e77144f9937b0d547ac07d4929afdbb00fda3fba Mon Sep 17 00:00:00 2001 From: Yuta Saito Date: Mon, 15 Jan 2024 14:16:50 +0000 Subject: [PATCH 083/373] Skip unit tests with uwasi --- .github/workflows/test.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index aadda11af..c129040f9 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -43,6 +43,9 @@ jobs: - run: make bootstrap - run: make test - 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 From f3214f9bc1a60ebaea3360f1873891cc65e57f39 Mon Sep 17 00:00:00 2001 From: Yuta Saito Date: Tue, 16 Jan 2024 12:56:52 +0000 Subject: [PATCH 084/373] Bump version to 0.19.0, update `CHANGELOG.md` --- CHANGELOG.md | 14 ++++++++++++++ Example/package-lock.json | 2 +- package-lock.json | 4 ++-- package.json | 2 +- 4 files changed, 18 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2f7d8d537..27137e9c8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,17 @@ +# 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 diff --git a/Example/package-lock.json b/Example/package-lock.json index 547887116..38124aea1 100644 --- a/Example/package-lock.json +++ b/Example/package-lock.json @@ -21,7 +21,7 @@ }, "..": { "name": "javascript-kit-swift", - "version": "0.18.0", + "version": "0.19.0", "license": "MIT", "devDependencies": { "@rollup/plugin-typescript": "^8.3.1", diff --git a/package-lock.json b/package-lock.json index a2ac922e1..88ac7d6d7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "javascript-kit-swift", - "version": "0.18.0", + "version": "0.19.0", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "javascript-kit-swift", - "version": "0.18.0", + "version": "0.19.0", "license": "MIT", "devDependencies": { "@rollup/plugin-typescript": "^8.3.1", diff --git a/package.json b/package.json index a7faddd43..39b958d0c 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "javascript-kit-swift", - "version": "0.18.0", + "version": "0.19.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", From 5bca895210def00f5b4683b0fea2972647e78476 Mon Sep 17 00:00:00 2001 From: Yuta Saito Date: Tue, 16 Jan 2024 12:59:18 +0000 Subject: [PATCH 085/373] Bump toolchain to wasm-5.9.1-RELEASE --- .github/workflows/test.yml | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index c129040f9..8f5689e69 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -10,20 +10,20 @@ jobs: matrix: entry: # Ensure that all host can install toolchain, build project, and run tests - - { os: macos-11, toolchain: wasm-5.9-SNAPSHOT-2024-01-12-a, wasi-backend: Node, xcode: Xcode_13.2.1.app } - - { os: macos-12, toolchain: wasm-5.9-SNAPSHOT-2024-01-12-a, wasi-backend: Node, xcode: Xcode_13.4.1.app } - - { os: macos-13, toolchain: wasm-5.9-SNAPSHOT-2024-01-12-a, wasi-backend: Node, xcode: Xcode_14.3.app } - - { os: ubuntu-22.04, toolchain: wasm-5.9-SNAPSHOT-2024-01-12-a, wasi-backend: Node } + - { os: macos-11, toolchain: wasm-5.9.1-RELEASE, wasi-backend: Node, xcode: Xcode_13.2.1.app } + - { os: macos-12, toolchain: wasm-5.9.1-RELEASE, wasi-backend: Node, xcode: Xcode_13.4.1.app } + - { os: macos-13, toolchain: wasm-5.9.1-RELEASE, wasi-backend: Node, xcode: Xcode_14.3.app } + - { os: ubuntu-22.04, toolchain: wasm-5.9.1-RELEASE, wasi-backend: Node } # Ensure that test succeeds with all toolchains and wasi backend combinations - { os: ubuntu-20.04, toolchain: wasm-5.7.3-RELEASE, wasi-backend: Node } - { os: ubuntu-20.04, toolchain: wasm-5.8.0-RELEASE, wasi-backend: Node } - { os: ubuntu-20.04, toolchain: wasm-5.7.3-RELEASE, wasi-backend: Wasmer } - { os: ubuntu-20.04, toolchain: wasm-5.8.0-RELEASE, wasi-backend: Wasmer } - - { os: ubuntu-20.04, toolchain: wasm-5.9-SNAPSHOT-2024-01-12-a, wasi-backend: Wasmer } + - { os: ubuntu-20.04, toolchain: wasm-5.9.1-RELEASE, wasi-backend: Wasmer } - { os: ubuntu-20.04, toolchain: wasm-5.7.3-RELEASE, wasi-backend: MicroWASI } - { os: ubuntu-20.04, toolchain: wasm-5.8.0-RELEASE, wasi-backend: MicroWASI } - - { os: ubuntu-20.04, toolchain: wasm-5.9-SNAPSHOT-2024-01-12-a, wasi-backend: MicroWASI } + - { os: ubuntu-20.04, toolchain: wasm-5.9.1-RELEASE, wasi-backend: MicroWASI } runs-on: ${{ matrix.entry.os }} env: From 88abb1357bee8d7461b41ec0641afa1107056db2 Mon Sep 17 00:00:00 2001 From: Yuta Saito Date: Tue, 6 Feb 2024 19:02:53 +0900 Subject: [PATCH 086/373] Fix availability marker for Swift 5.9 compiler targeting host machine When building JavaScriptKit with Xcode, targeting host machine, the `func enqueue(_: ExecutorJob)` must be guarded by `@available` attribute explicitly. And due to the 5.9 compiler issue, it emit some migration warnings too consevatively. This commit suppress the warning by updating minimum available OS versions. Those versions should not be a problem when building for Wasm. --- .../JavaScriptEventLoop.swift | 17 +++++++++++------ Sources/JavaScriptEventLoop/JobQueue.swift | 2 +- .../JavaScriptEventLoopTestSupport.swift | 2 +- 3 files changed, 13 insertions(+), 8 deletions(-) diff --git a/Sources/JavaScriptEventLoop/JavaScriptEventLoop.swift b/Sources/JavaScriptEventLoop/JavaScriptEventLoop.swift index 7f7e69971..7f6783062 100644 --- a/Sources/JavaScriptEventLoop/JavaScriptEventLoop.swift +++ b/Sources/JavaScriptEventLoop/JavaScriptEventLoop.swift @@ -34,7 +34,7 @@ import JavaScriptEventLoop JavaScriptEventLoop.installGlobalExecutor() ``` */ -@available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) +@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. @@ -92,7 +92,7 @@ public final class JavaScriptEventLoop: SerialExecutor, @unchecked Sendable { 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) @@ -112,7 +112,7 @@ public final class JavaScriptEventLoop: SerialExecutor, @unchecked Sendable { 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) @@ -130,15 +130,20 @@ public final class JavaScriptEventLoop: SerialExecutor, @unchecked Sendable { }) } + 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. - insertJobQueue(job: UnownedJob(job)) + unsafeEnqueue(UnownedJob(job)) } #else public func enqueue(_ job: UnownedJob) { - insertJobQueue(job: job) + unsafeEnqueue(job) } #endif @@ -155,7 +160,7 @@ internal func swift_get_time( _ nanoseconds: UnsafeMutablePointer, _ clock: CInt) -@available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) +@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, diff --git a/Sources/JavaScriptEventLoop/JobQueue.swift b/Sources/JavaScriptEventLoop/JobQueue.swift index 6cc0cfc35..5ad71f0a0 100644 --- a/Sources/JavaScriptEventLoop/JobQueue.swift +++ b/Sources/JavaScriptEventLoop/JobQueue.swift @@ -12,7 +12,7 @@ struct QueueState: Sendable { fileprivate var isSpinning: Bool = false } -@available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) +@available(macOS 14.0, iOS 17.0, watchOS 10.0, tvOS 17.0, *) extension JavaScriptEventLoop { func insertJobQueue(job newJob: UnownedJob) { diff --git a/Sources/JavaScriptEventLoopTestSupport/JavaScriptEventLoopTestSupport.swift b/Sources/JavaScriptEventLoopTestSupport/JavaScriptEventLoopTestSupport.swift index 9922de945..64e6776d4 100644 --- a/Sources/JavaScriptEventLoopTestSupport/JavaScriptEventLoopTestSupport.swift +++ b/Sources/JavaScriptEventLoopTestSupport/JavaScriptEventLoopTestSupport.swift @@ -22,7 +22,7 @@ import JavaScriptEventLoop #if compiler(>=5.5) -@available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) +@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() { JavaScriptEventLoop.installGlobalExecutor() From da7e39ca5967ba8690f1e88974c3c4984b999699 Mon Sep 17 00:00:00 2001 From: Yuta Saito Date: Tue, 6 Feb 2024 19:16:02 +0900 Subject: [PATCH 087/373] Bump version to 0.19.1, update `CHANGELOG.md` --- CHANGELOG.md | 8 ++++++++ package-lock.json | 4 ++-- package.json | 2 +- 3 files changed, 11 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 27137e9c8..b3ad2907e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,11 @@ +# 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 diff --git a/package-lock.json b/package-lock.json index 88ac7d6d7..de4053ab6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "javascript-kit-swift", - "version": "0.19.0", + "version": "0.19.1", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "javascript-kit-swift", - "version": "0.19.0", + "version": "0.19.1", "license": "MIT", "devDependencies": { "@rollup/plugin-typescript": "^8.3.1", diff --git a/package.json b/package.json index 39b958d0c..3219e4073 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "javascript-kit-swift", - "version": "0.19.0", + "version": "0.19.1", "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", From 4361765d2df6fcba40eb7370f401b5551e1ea550 Mon Sep 17 00:00:00 2001 From: Yuta Saito Date: Wed, 7 Feb 2024 16:46:35 +0900 Subject: [PATCH 088/373] Remove deprecated swift-doc Just use https://swiftpackageindex.com/swiftwasm/JavaScriptKit/main/documentation/javascriptkit instead --- .github/workflows/documentation.yml | 26 -------------------------- 1 file changed, 26 deletions(-) delete mode 100644 .github/workflows/documentation.yml 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 From f6006fe8477f70b71a7c688b8e498b8d0f7b06fd Mon Sep 17 00:00:00 2001 From: IKEDA Sho Date: Sat, 23 Mar 2024 01:15:16 +0900 Subject: [PATCH 089/373] [CI] macos-14 https://github.blog/changelog/2024-01-30-github-actions-macos-14-sonoma-is-now-available/ --- .github/workflows/test.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 8f5689e69..1368810cf 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -13,6 +13,7 @@ jobs: - { os: macos-11, toolchain: wasm-5.9.1-RELEASE, wasi-backend: Node, xcode: Xcode_13.2.1.app } - { os: macos-12, toolchain: wasm-5.9.1-RELEASE, wasi-backend: Node, xcode: Xcode_13.4.1.app } - { os: macos-13, toolchain: wasm-5.9.1-RELEASE, wasi-backend: Node, xcode: Xcode_14.3.app } + - { os: macos-14, toolchain: wasm-5.9.1-RELEASE, wasi-backend: Node, xcode: Xcode_15.2.app } - { os: ubuntu-22.04, toolchain: wasm-5.9.1-RELEASE, wasi-backend: Node } # Ensure that test succeeds with all toolchains and wasi backend combinations @@ -65,6 +66,8 @@ jobs: xcode: Xcode_14.0 - os: macos-13 xcode: Xcode_14.3 + - os: macos-14 + xcode: Xcode_15.2 runs-on: ${{ matrix.os }} steps: - uses: actions/checkout@v2 From 07f89140d557b47fb129a803073d13f16a77fa02 Mon Sep 17 00:00:00 2001 From: ikesyo Date: Sat, 23 Mar 2024 01:43:17 +0900 Subject: [PATCH 090/373] [CI] Drop macos-11 since that is deprecated and will be removed in Q2 2024 --- .github/workflows/test.yml | 3 --- 1 file changed, 3 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 1368810cf..ff1acd73d 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -10,7 +10,6 @@ jobs: matrix: entry: # Ensure that all host can install toolchain, build project, and run tests - - { os: macos-11, toolchain: wasm-5.9.1-RELEASE, wasi-backend: Node, xcode: Xcode_13.2.1.app } - { os: macos-12, toolchain: wasm-5.9.1-RELEASE, wasi-backend: Node, xcode: Xcode_13.4.1.app } - { os: macos-13, toolchain: wasm-5.9.1-RELEASE, wasi-backend: Node, xcode: Xcode_14.3.app } - { os: macos-14, toolchain: wasm-5.9.1-RELEASE, wasi-backend: Node, xcode: Xcode_15.2.app } @@ -58,8 +57,6 @@ jobs: strategy: matrix: include: - - os: macos-11 - xcode: Xcode_13.2.1 - os: macos-12 xcode: Xcode_13.3 - os: macos-12 From 14aec38334e34e12312cc2f8282fa8b8f8e632e5 Mon Sep 17 00:00:00 2001 From: ikesyo Date: Sat, 23 Mar 2024 08:13:16 +0900 Subject: [PATCH 091/373] Update swift-tools-version to reflect the supported Swift versions --- .github/workflows/compatibility.yml | 2 ++ .github/workflows/perf.yml | 2 ++ .github/workflows/test.yml | 2 -- Example/JavaScriptKitExample/Package.swift | 2 +- IntegrationTests/TestSuites/Package.swift | 2 +- Package.swift | 2 +- 6 files changed, 7 insertions(+), 5 deletions(-) diff --git a/.github/workflows/compatibility.yml b/.github/workflows/compatibility.yml index 489c7aac4..ba9dcbb3e 100644 --- a/.github/workflows/compatibility.yml +++ b/.github/workflows/compatibility.yml @@ -13,6 +13,8 @@ jobs: with: fetch-depth: 1 - uses: swiftwasm/setup-swiftwasm@v1 + with: + swift-version: wasm-5.9.1-RELEASE - name: Run Test run: | set -eux diff --git a/.github/workflows/perf.yml b/.github/workflows/perf.yml index 2fdba41dd..cb9f60262 100644 --- a/.github/workflows/perf.yml +++ b/.github/workflows/perf.yml @@ -11,6 +11,8 @@ jobs: with: fetch-depth: 1 - uses: swiftwasm/setup-swiftwasm@v1 + with: + swift-version: wasm-5.9.1-RELEASE - name: Run Benchmark run: | make bootstrap diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index ff1acd73d..a368b8392 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -57,8 +57,6 @@ jobs: strategy: matrix: include: - - os: macos-12 - xcode: Xcode_13.3 - os: macos-12 xcode: Xcode_14.0 - os: macos-13 diff --git a/Example/JavaScriptKitExample/Package.swift b/Example/JavaScriptKitExample/Package.swift index ecee23bad..35b08ed25 100644 --- a/Example/JavaScriptKitExample/Package.swift +++ b/Example/JavaScriptKitExample/Package.swift @@ -1,4 +1,4 @@ -// swift-tools-version:5.2 +// swift-tools-version:5.7 import PackageDescription diff --git a/IntegrationTests/TestSuites/Package.swift b/IntegrationTests/TestSuites/Package.swift index fac27db31..9888e7388 100644 --- a/IntegrationTests/TestSuites/Package.swift +++ b/IntegrationTests/TestSuites/Package.swift @@ -1,4 +1,4 @@ -// swift-tools-version:5.2 +// swift-tools-version:5.7 import PackageDescription diff --git a/Package.swift b/Package.swift index c8f55dd0b..d9f33839e 100644 --- a/Package.swift +++ b/Package.swift @@ -1,4 +1,4 @@ -// swift-tools-version:5.3 +// swift-tools-version:5.7 import PackageDescription From 1b1a04b6665d2a1a2d0308bd38a1ab8185188bbc Mon Sep 17 00:00:00 2001 From: ikesyo Date: Sun, 24 Mar 2024 10:14:13 +0900 Subject: [PATCH 092/373] [CI] Update actions and configure Dependabot --- .github/dependabot.yml | 11 +++++++++++ .github/workflows/compatibility.yml | 4 +--- .github/workflows/npm-publish.yml | 4 ++-- .github/workflows/perf.yml | 4 +--- .github/workflows/test.yml | 6 ++---- 5 files changed, 17 insertions(+), 12 deletions(-) create mode 100644 .github/dependabot.yml 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 index ba9dcbb3e..85783d730 100644 --- a/.github/workflows/compatibility.yml +++ b/.github/workflows/compatibility.yml @@ -9,9 +9,7 @@ jobs: runs-on: ubuntu-20.04 steps: - name: Checkout - uses: actions/checkout@v2 - with: - fetch-depth: 1 + uses: actions/checkout@v4 - uses: swiftwasm/setup-swiftwasm@v1 with: swift-version: wasm-5.9.1-RELEASE diff --git a/.github/workflows/npm-publish.yml b/.github/workflows/npm-publish.yml index e6a887d3d..96ef37a64 100644 --- a/.github/workflows/npm-publish.yml +++ b/.github/workflows/npm-publish.yml @@ -11,8 +11,8 @@ jobs: publish-npm: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 - - uses: actions/setup-node@v1 + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 with: node-version: 12 registry-url: https://registry.npmjs.org/ diff --git a/.github/workflows/perf.yml b/.github/workflows/perf.yml index cb9f60262..f2ffdcc5e 100644 --- a/.github/workflows/perf.yml +++ b/.github/workflows/perf.yml @@ -7,9 +7,7 @@ jobs: runs-on: ubuntu-20.04 steps: - name: Checkout - uses: actions/checkout@master - with: - fetch-depth: 1 + uses: actions/checkout@v4 - uses: swiftwasm/setup-swiftwasm@v1 with: swift-version: wasm-5.9.1-RELEASE diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index a368b8392..121d22236 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -31,9 +31,7 @@ jobs: SWIFT_VERSION: ${{ matrix.entry.toolchain }} steps: - name: Checkout - uses: actions/checkout@master - with: - fetch-depth: 1 + uses: actions/checkout@v4 - name: Select SDKROOT if: ${{ matrix.entry.xcode }} run: sudo xcode-select -s /Applications/${{ matrix.entry.xcode }} @@ -65,7 +63,7 @@ jobs: xcode: Xcode_15.2 runs-on: ${{ matrix.os }} steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v4 - run: swift build env: DEVELOPER_DIR: /Applications/${{ matrix.xcode }}.app/Contents/Developer/ From 32538ece74f059c22310676446737a7ba0a3e652 Mon Sep 17 00:00:00 2001 From: omochimetaru Date: Sat, 6 Apr 2024 02:09:25 +0900 Subject: [PATCH 093/373] Fix Optional implementation for ConstructibleFromJSValue (#238) fix optional decode --- Sources/JavaScriptKit/ConvertibleToJSValue.swift | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/Sources/JavaScriptKit/ConvertibleToJSValue.swift b/Sources/JavaScriptKit/ConvertibleToJSValue.swift index 4b9bf8f03..ebf24c74c 100644 --- a/Sources/JavaScriptKit/ConvertibleToJSValue.swift +++ b/Sources/JavaScriptKit/ConvertibleToJSValue.swift @@ -131,9 +131,10 @@ 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) } } } From 3e07fdcca96a0fa423aed4929d8e8f440059219c Mon Sep 17 00:00:00 2001 From: Yuta Saito Date: Sun, 7 Apr 2024 13:59:12 +0900 Subject: [PATCH 094/373] Inherit JSFunction from JSClosure (#239) There is no reason not to make JSClosure to be compatible with JSFunction. We can treat JSClosure as a JSFunction and call it from not only JavaScript but also Swift. --- .../Sources/PrimaryTests/UnitTestUtils.swift | 1 + .../Sources/PrimaryTests/main.swift | 25 ++++++++----------- .../FundamentalObjects/JSClosure.swift | 2 +- 3 files changed, 12 insertions(+), 16 deletions(-) diff --git a/IntegrationTests/TestSuites/Sources/PrimaryTests/UnitTestUtils.swift b/IntegrationTests/TestSuites/Sources/PrimaryTests/UnitTestUtils.swift index 571e0d6a3..199a4cbdf 100644 --- a/IntegrationTests/TestSuites/Sources/PrimaryTests/UnitTestUtils.swift +++ b/IntegrationTests/TestSuites/Sources/PrimaryTests/UnitTestUtils.swift @@ -14,6 +14,7 @@ func test(_ name: String, testBlock: () throws -> Void) throws { print(error) throw error } + print("✅ \(name)") } struct MessageError: Error { diff --git a/IntegrationTests/TestSuites/Sources/PrimaryTests/main.swift b/IntegrationTests/TestSuites/Sources/PrimaryTests/main.swift index aede07ced..a3e27573a 100644 --- a/IntegrationTests/TestSuites/Sources/PrimaryTests/main.swift +++ b/IntegrationTests/TestSuites/Sources/PrimaryTests/main.swift @@ -251,6 +251,16 @@ try test("Closure Lifetime") { } #endif + do { + let c1 = JSClosure { _ in .number(4) } + try expectEqual(c1(), .number(4)) + } + + do { + let c1 = JSClosure { _ in fatalError("Crash while closure evaluation") } + let error = try expectThrow(try evalClosure.throws(c1)) as! JSValue + try expectEqual(error.description, "RuntimeError: unreachable") + } } try test("Host Function Registration") { @@ -420,21 +430,6 @@ try test("ObjectRef Lifetime") { #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) diff --git a/Sources/JavaScriptKit/FundamentalObjects/JSClosure.swift b/Sources/JavaScriptKit/FundamentalObjects/JSClosure.swift index ea15c6d28..441dd2a6c 100644 --- a/Sources/JavaScriptKit/FundamentalObjects/JSClosure.swift +++ b/Sources/JavaScriptKit/FundamentalObjects/JSClosure.swift @@ -61,7 +61,7 @@ public class JSOneshotClosure: JSObject, JSClosureProtocol { /// button.removeEventListener!("click", JSValue.function(eventListenter)) /// ``` /// -public class JSClosure: JSObject, JSClosureProtocol { +public class JSClosure: JSFunction, JSClosureProtocol { // Note: Retain the closure object itself also to avoid funcRef conflicts fileprivate static var sharedClosures: [JavaScriptHostFuncRef: (object: JSObject, body: ([JSValue]) -> JSValue)] = [:] From 384d6686939a0ab7103b00ab942b3a3edc7f46e3 Mon Sep 17 00:00:00 2001 From: omochimetaru Date: Sun, 7 Apr 2024 17:09:10 +0900 Subject: [PATCH 095/373] Fix object decode (#241) * fix JSObject.construct(from:) * add unittest * more precise tests --- .../Sources/PrimaryTests/UnitTestUtils.swift | 7 ++++ .../Sources/PrimaryTests/main.swift | 42 +++++++++++++++++++ IntegrationTests/bin/primary-tests.js | 7 ++++ IntegrationTests/package-lock.json | 2 +- .../FundamentalObjects/JSBigInt.swift | 4 -- .../FundamentalObjects/JSFunction.swift | 4 -- .../FundamentalObjects/JSObject.swift | 18 +++++++- .../FundamentalObjects/JSSymbol.swift | 4 -- 8 files changed, 73 insertions(+), 15 deletions(-) diff --git a/IntegrationTests/TestSuites/Sources/PrimaryTests/UnitTestUtils.swift b/IntegrationTests/TestSuites/Sources/PrimaryTests/UnitTestUtils.swift index 199a4cbdf..c4f9a9fb1 100644 --- a/IntegrationTests/TestSuites/Sources/PrimaryTests/UnitTestUtils.swift +++ b/IntegrationTests/TestSuites/Sources/PrimaryTests/UnitTestUtils.swift @@ -123,6 +123,13 @@ func expectNotNil(_ value: T?, file: StaticString = #file, line: UInt = #line throw MessageError("Expect a non-nil value", file: file, line: line, column: column) } } +func expectNil(_ value: T?, file: StaticString = #file, line: UInt = #line, column: UInt = #column) throws { + switch value { + case .some: + throw MessageError("Expect an nil", file: file, line: line, column: column) + case .none: return + } +} class Expectation { private(set) var isFulfilled: Bool = false diff --git a/IntegrationTests/TestSuites/Sources/PrimaryTests/main.swift b/IntegrationTests/TestSuites/Sources/PrimaryTests/main.swift index a3e27573a..a9d127109 100644 --- a/IntegrationTests/TestSuites/Sources/PrimaryTests/main.swift +++ b/IntegrationTests/TestSuites/Sources/PrimaryTests/main.swift @@ -337,6 +337,48 @@ try test("New Object Construction") { try expectEqual(dog1Bark(), .string("wan")) } +try test("Object Decoding") { + /* + ```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 + + try expectNotNil(JSObject.construct(from: object)) + try expectEqual(JSObject.construct(from: function).map { $0 is JSFunction }, .some(true)) + try expectEqual(JSObject.construct(from: symbol).map { $0 is JSSymbol }, .some(true)) + try expectEqual(JSObject.construct(from: bigInt).map { $0 is JSBigInt }, .some(true)) + + try expectNil(JSFunction.construct(from: object)) + try expectNotNil(JSFunction.construct(from: function)) + try expectNil(JSFunction.construct(from: symbol)) + try expectNil(JSFunction.construct(from: bigInt)) + + try expectNil(JSSymbol.construct(from: object)) + try expectNil(JSSymbol.construct(from: function)) + try expectNotNil(JSSymbol.construct(from: symbol)) + try expectNil(JSSymbol.construct(from: bigInt)) + + try expectNil(JSBigInt.construct(from: object)) + try expectNil(JSBigInt.construct(from: function)) + try expectNil(JSBigInt.construct(from: symbol)) + try expectNotNil(JSBigInt.construct(from: bigInt)) +} + try test("Call Function With This") { // ```js // global.Animal = function(name, age, isCat) { diff --git a/IntegrationTests/bin/primary-tests.js b/IntegrationTests/bin/primary-tests.js index 2d977c3fd..50532ceac 100644 --- a/IntegrationTests/bin/primary-tests.js +++ b/IntegrationTests/bin/primary-tests.js @@ -95,6 +95,13 @@ global.callThrowingClosure = (c) => { } }; +global.objectDecodingTest = { + obj: {}, + fn: () => {}, + sym: Symbol("s"), + bi: BigInt(3) +}; + const { startWasiTask, WASI } = require("../lib"); startWasiTask("./dist/PrimaryTests.wasm").catch((err) => { diff --git a/IntegrationTests/package-lock.json b/IntegrationTests/package-lock.json index 8ccbcdc21..8aff33b98 100644 --- a/IntegrationTests/package-lock.json +++ b/IntegrationTests/package-lock.json @@ -13,7 +13,7 @@ }, "..": { "name": "javascript-kit-swift", - "version": "0.18.0", + "version": "0.19.1", "license": "MIT", "devDependencies": { "@rollup/plugin-typescript": "^8.3.1", diff --git a/Sources/JavaScriptKit/FundamentalObjects/JSBigInt.swift b/Sources/JavaScriptKit/FundamentalObjects/JSBigInt.swift index 5929f2889..104d194e3 100644 --- a/Sources/JavaScriptKit/FundamentalObjects/JSBigInt.swift +++ b/Sources/JavaScriptKit/FundamentalObjects/JSBigInt.swift @@ -24,10 +24,6 @@ public final class JSBigInt: JSObject { super.init(id: _i64_to_bigint_slow(UInt32(value & 0xffffffff), UInt32(value >> 32), false)) } - override public class func construct(from value: JSValue) -> Self? { - value.bigInt as? Self - } - override public var jsValue: JSValue { .bigInt(self) } diff --git a/Sources/JavaScriptKit/FundamentalObjects/JSFunction.swift b/Sources/JavaScriptKit/FundamentalObjects/JSFunction.swift index 66c613402..1de95fd36 100644 --- a/Sources/JavaScriptKit/FundamentalObjects/JSFunction.swift +++ b/Sources/JavaScriptKit/FundamentalObjects/JSFunction.swift @@ -89,10 +89,6 @@ public class JSFunction: JSObject { fatalError("unavailable") } - override public class func construct(from value: JSValue) -> Self? { - return value.function as? Self - } - override public var jsValue: JSValue { .function(self) } diff --git a/Sources/JavaScriptKit/FundamentalObjects/JSObject.swift b/Sources/JavaScriptKit/FundamentalObjects/JSObject.swift index 4e93853ea..04e7f3d59 100644 --- a/Sources/JavaScriptKit/FundamentalObjects/JSObject.swift +++ b/Sources/JavaScriptKit/FundamentalObjects/JSObject.swift @@ -150,8 +150,22 @@ public class JSObject: Equatable { 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 var jsValue: JSValue { diff --git a/Sources/JavaScriptKit/FundamentalObjects/JSSymbol.swift b/Sources/JavaScriptKit/FundamentalObjects/JSSymbol.swift index f25ee1bd8..f5d194e25 100644 --- a/Sources/JavaScriptKit/FundamentalObjects/JSSymbol.swift +++ b/Sources/JavaScriptKit/FundamentalObjects/JSSymbol.swift @@ -41,10 +41,6 @@ public class JSSymbol: JSObject { Symbol.keyFor!(symbol).string } - override public class func construct(from value: JSValue) -> Self? { - return value.symbol as? Self - } - override public var jsValue: JSValue { .symbol(self) } From 3b5af3d442179900455307c725fe6a111a714b27 Mon Sep 17 00:00:00 2001 From: Yuta Saito Date: Thu, 11 Apr 2024 11:16:19 +0900 Subject: [PATCH 096/373] Bump version to 0.19.2, update `CHANGELOG.md` --- CHANGELOG.md | 17 +++++++++++++++++ package-lock.json | 4 ++-- package.json | 2 +- 3 files changed, 20 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b3ad2907e..d860de07b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,20 @@ +# 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 diff --git a/package-lock.json b/package-lock.json index de4053ab6..c6d12ca2a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "javascript-kit-swift", - "version": "0.19.1", + "version": "0.19.2", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "javascript-kit-swift", - "version": "0.19.1", + "version": "0.19.2", "license": "MIT", "devDependencies": { "@rollup/plugin-typescript": "^8.3.1", diff --git a/package.json b/package.json index 3219e4073..0cfd8ff5b 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "javascript-kit-swift", - "version": "0.19.1", + "version": "0.19.2", "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", From d9a4a9f30938abbf71eaa7b9d2c63f5411ea3492 Mon Sep 17 00:00:00 2001 From: Yuta Saito Date: Fri, 12 Apr 2024 18:02:47 +0900 Subject: [PATCH 097/373] Fix `JSClosure` leak (#240) * Fix retain cycle in JSClosure by weak reference to the closure holder ```swift let c1 = JSClosure { _ in .undefined } consume c1 ``` did not release the `JSClosure` itself and it leaked the underlying JavaScript closure too because `JSClosure` -> JS closure thunk -> Closure registry entry -> `JSClosure` reference cycle was not broken when using FinalizationRegistry. (Without FR, it was broken by manual `release` call.) Note that weakening the reference does not violates the contract that function reference should be unique because holding a weak reference does deinit but not deallocate the object, so ObjectIdentifier is not reused until the weak reference in the registry is removed. * Fix the test suite for violation of the closure lifetime contract The test suite was not properly releasing the closures but they have not been revealed as a problem because those closures were leaked conservatively. * Report where the closure is created when it violates the lifetime contract This additional information will help developers to find the root cause * Flatten two-level object reference in ClosureEntry --- .../Sources/ConcurrencyTests/main.swift | 10 +- .../Sources/PrimaryTests/UnitTestUtils.swift | 2 +- .../Sources/PrimaryTests/main.swift | 18 +++- .../FundamentalObjects/JSClosure.swift | 101 ++++++++++++++---- 4 files changed, 103 insertions(+), 28 deletions(-) diff --git a/IntegrationTests/TestSuites/Sources/ConcurrencyTests/main.swift b/IntegrationTests/TestSuites/Sources/ConcurrencyTests/main.swift index ece58b317..46eeeab69 100644 --- a/IntegrationTests/TestSuites/Sources/ConcurrencyTests/main.swift +++ b/IntegrationTests/TestSuites/Sources/ConcurrencyTests/main.swift @@ -123,12 +123,16 @@ func entrypoint() async throws { try expectEqual(result, .number(3)) } try expectGTE(diff, 200) +#if JAVASCRIPTKIT_WITHOUT_WEAKREFS + delayObject.closure = nil + delayClosure.release() +#endif } try await asyncTest("Async JSPromise: then") { let promise = JSPromise { resolve in _ = JSObject.global.setTimeout!( - JSClosure { _ in + JSOneshotClosure { _ in resolve(.success(JSValue.number(3))) return .undefined }.jsValue, @@ -149,7 +153,7 @@ func entrypoint() async throws { try await asyncTest("Async JSPromise: then(success:failure:)") { let promise = JSPromise { resolve in _ = JSObject.global.setTimeout!( - JSClosure { _ in + JSOneshotClosure { _ in resolve(.failure(JSError(message: "test").jsValue)) return .undefined }.jsValue, @@ -168,7 +172,7 @@ func entrypoint() async throws { try await asyncTest("Async JSPromise: catch") { let promise = JSPromise { resolve in _ = JSObject.global.setTimeout!( - JSClosure { _ in + JSOneshotClosure { _ in resolve(.failure(JSError(message: "test").jsValue)) return .undefined }.jsValue, diff --git a/IntegrationTests/TestSuites/Sources/PrimaryTests/UnitTestUtils.swift b/IntegrationTests/TestSuites/Sources/PrimaryTests/UnitTestUtils.swift index c4f9a9fb1..098570d9b 100644 --- a/IntegrationTests/TestSuites/Sources/PrimaryTests/UnitTestUtils.swift +++ b/IntegrationTests/TestSuites/Sources/PrimaryTests/UnitTestUtils.swift @@ -111,7 +111,7 @@ func expectThrow(_ body: @autoclosure () throws -> T, file: StaticString = #f } func wrapUnsafeThrowableFunction(_ body: @escaping () -> Void, file: StaticString = #file, line: UInt = #line, column: UInt = #column) throws -> Error { - JSObject.global.callThrowingClosure.function!(JSClosure { _ in + JSObject.global.callThrowingClosure.function!(JSOneshotClosure { _ in body() return .undefined }) diff --git a/IntegrationTests/TestSuites/Sources/PrimaryTests/main.swift b/IntegrationTests/TestSuites/Sources/PrimaryTests/main.swift index a9d127109..5a81e94cd 100644 --- a/IntegrationTests/TestSuites/Sources/PrimaryTests/main.swift +++ b/IntegrationTests/TestSuites/Sources/PrimaryTests/main.swift @@ -254,12 +254,18 @@ try test("Closure Lifetime") { do { let c1 = JSClosure { _ in .number(4) } try expectEqual(c1(), .number(4)) +#if JAVASCRIPTKIT_WITHOUT_WEAKREFS + c1.release() +#endif } do { let c1 = JSClosure { _ in fatalError("Crash while closure evaluation") } let error = try expectThrow(try evalClosure.throws(c1)) as! JSValue try expectEqual(error.description, "RuntimeError: unreachable") +#if JAVASCRIPTKIT_WITHOUT_WEAKREFS + c1.release() +#endif } } @@ -866,16 +872,24 @@ try test("Symbols") { // }.prop // Object.defineProperty(hasInstanceClass, Symbol.hasInstance, { value: () => true }) let hasInstanceObject = JSObject.global.Object.function!.new() - hasInstanceObject.prop = JSClosure { _ in .undefined }.jsValue + let hasInstanceObjectClosure = JSClosure { _ in .undefined } + hasInstanceObject.prop = hasInstanceObjectClosure.jsValue let hasInstanceClass = hasInstanceObject.prop.function! let propertyDescriptor = JSObject.global.Object.function!.new() - propertyDescriptor.value = JSClosure { _ in .boolean(true) }.jsValue + let propertyDescriptorClosure = JSClosure { _ in .boolean(true) } + propertyDescriptor.value = propertyDescriptorClosure.jsValue _ = JSObject.global.Object.function!.defineProperty!( hasInstanceClass, JSSymbol.hasInstance, propertyDescriptor ) try expectEqual(hasInstanceClass[JSSymbol.hasInstance].function!().boolean, true) try expectEqual(JSObject.global.Object.isInstanceOf(hasInstanceClass), true) +#if JAVASCRIPTKIT_WITHOUT_WEAKREFS + hasInstanceObject.prop = .undefined + propertyDescriptor.value = .undefined + hasInstanceObjectClosure.release() + propertyDescriptorClosure.release() +#endif } struct AnimalStruct: Decodable { diff --git a/Sources/JavaScriptKit/FundamentalObjects/JSClosure.swift b/Sources/JavaScriptKit/FundamentalObjects/JSClosure.swift index 441dd2a6c..c3a0c886e 100644 --- a/Sources/JavaScriptKit/FundamentalObjects/JSClosure.swift +++ b/Sources/JavaScriptKit/FundamentalObjects/JSClosure.swift @@ -19,17 +19,15 @@ public class JSOneshotClosure: JSObject, JSClosureProtocol { // 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: ObjectIdentifier(self)) - id = withExtendedLifetime(JSString(file)) { file in - _create_function(hostFuncRef, line, file.asInternalJSRef()) - } - - // 3. Retain the given body in static storage by `funcRef`. - JSClosure.sharedClosures[hostFuncRef] = (self, { + // 2. Retain the given body in static storage + // Leak the self object globally and release once it's called + hostFuncRef = JSClosure.sharedClosures.register(ObjectIdentifier(self), object: .strong(self), body: { defer { self.release() } return body($0) }) + id = withExtendedLifetime(JSString(file)) { file in + _create_function(hostFuncRef, line, file.asInternalJSRef()) + } } #if compiler(>=5.5) @@ -42,7 +40,7 @@ public class JSOneshotClosure: JSObject, JSClosureProtocol { /// Release this function resource. /// After calling `release`, calling this function from JavaScript will fail. public func release() { - JSClosure.sharedClosures[hostFuncRef] = nil + JSClosure.sharedClosures.unregister(hostFuncRef) } } @@ -62,13 +60,13 @@ public class JSOneshotClosure: JSObject, JSClosureProtocol { /// ``` /// public class JSClosure: JSFunction, JSClosureProtocol { - - // Note: Retain the closure object itself also to avoid funcRef conflicts - fileprivate static var sharedClosures: [JavaScriptHostFuncRef: (object: JSObject, body: ([JSValue]) -> JSValue)] = [:] + fileprivate static var sharedClosures = SharedJSClosureRegistry() private var hostFuncRef: JavaScriptHostFuncRef = 0 #if JAVASCRIPTKIT_WITHOUT_WEAKREFS + private let file: String + private let line: UInt32 private var isReleased: Bool = false #endif @@ -82,30 +80,35 @@ public class JSClosure: JSFunction, JSClosureProtocol { } public init(_ body: @escaping ([JSValue]) -> JSValue, file: String = #fileID, line: UInt32 = #line) { + #if JAVASCRIPTKIT_WITHOUT_WEAKREFS + self.file = file + self.line = line + #endif // 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: ObjectIdentifier(self)) + // 2. Retain the given body in static storage + hostFuncRef = Self.sharedClosures.register( + ObjectIdentifier(self), object: .weak(self), body: body + ) + id = withExtendedLifetime(JSString(file)) { file in _create_function(hostFuncRef, line, file.asInternalJSRef()) } - // 3. Retain the given body in static storage by `funcRef`. - Self.sharedClosures[hostFuncRef] = (self, body) } #if compiler(>=5.5) @available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) - public static func async(_ body: @escaping ([JSValue]) async throws -> JSValue) -> JSClosure { - JSClosure(makeAsyncClosure(body)) + public static func async(_ body: @escaping ([JSValue]) async throws -> JSValue, file: String = #fileID, line: UInt32 = #line) -> JSClosure { + JSClosure(makeAsyncClosure(body), file: file, line: line) } #endif #if JAVASCRIPTKIT_WITHOUT_WEAKREFS deinit { guard isReleased else { - fatalError("release() must be called on JSClosure objects manually before they are deallocated") + fatalError("release() must be called on JSClosure object (\(file):\(line)) manually before they are deallocated") } } #endif @@ -133,6 +136,60 @@ private func makeAsyncClosure(_ body: @escaping ([JSValue]) async throws -> JSVa } #endif +/// Registry for Swift closures that are referenced from JavaScript. +private struct SharedJSClosureRegistry { + struct ClosureEntry { + // Note: Retain the closure object itself also to avoid funcRef conflicts. + var object: AnyObjectReference + var body: ([JSValue]) -> JSValue + + init(object: AnyObjectReference, body: @escaping ([JSValue]) -> JSValue) { + self.object = object + self.body = body + } + } + enum AnyObjectReference { + case strong(AnyObject) + case weak(WeakObject) + + static func `weak`(_ object: AnyObject) -> AnyObjectReference { + .weak(SharedJSClosureRegistry.WeakObject(underlying: object)) + } + } + struct WeakObject { + weak var underlying: AnyObject? + init(underlying: AnyObject) { + self.underlying = underlying + } + } + private var closures: [JavaScriptHostFuncRef: ClosureEntry] = [:] + + /// Register a Swift closure to be called from JavaScript. + /// - Parameters: + /// - hint: A hint to identify the closure. + /// - object: The object should be retained until the closure is released from JavaScript. + /// - body: The closure to be called from JavaScript. + /// - Returns: An unique identifier for the registered closure. + mutating func register( + _ hint: ObjectIdentifier, + object: AnyObjectReference, body: @escaping ([JSValue]) -> JSValue + ) -> JavaScriptHostFuncRef { + let ref = JavaScriptHostFuncRef(bitPattern: hint) + closures[ref] = ClosureEntry(object: object, body: body) + return ref + } + + /// Unregister a Swift closure from the registry. + mutating func unregister(_ ref: JavaScriptHostFuncRef) { + closures[ref] = nil + } + + /// Get the Swift closure from the registry. + subscript(_ ref: JavaScriptHostFuncRef) -> (([JSValue]) -> JSValue)? { + closures[ref]?.body + } +} + // MARK: - `JSClosure` mechanism note // // 1. Create a thunk in the JavaScript world, which has a reference @@ -174,7 +231,7 @@ func _call_host_function_impl( _ argv: UnsafePointer, _ argc: Int32, _ callbackFuncRef: JavaScriptObjectRef ) -> Bool { - guard let (_, hostFunc) = JSClosure.sharedClosures[hostFuncRef] else { + guard let hostFunc = JSClosure.sharedClosures[hostFuncRef] else { return true } let arguments = UnsafeBufferPointer(start: argv, count: Int(argc)).map(\.jsValue) @@ -195,7 +252,7 @@ func _call_host_function_impl( extension JSClosure { public func release() { isReleased = true - Self.sharedClosures[hostFuncRef] = nil + Self.sharedClosures.unregister(hostFuncRef) } } @@ -213,6 +270,6 @@ extension JSClosure { @_cdecl("_free_host_function_impl") func _free_host_function_impl(_ hostFuncRef: JavaScriptHostFuncRef) { - JSClosure.sharedClosures[hostFuncRef] = nil + JSClosure.sharedClosures.unregister(hostFuncRef) } #endif From 8780e5f005933c1d83d74942a14f78c1fc03e499 Mon Sep 17 00:00:00 2001 From: Yuta Saito Date: Fri, 12 Apr 2024 09:20:55 +0000 Subject: [PATCH 098/373] Revert "Fix `JSClosure` leak (#240)" This reverts commit d9a4a9f30938abbf71eaa7b9d2c63f5411ea3492. --- .../Sources/ConcurrencyTests/main.swift | 10 +- .../Sources/PrimaryTests/UnitTestUtils.swift | 2 +- .../Sources/PrimaryTests/main.swift | 18 +--- .../FundamentalObjects/JSClosure.swift | 101 ++++-------------- 4 files changed, 28 insertions(+), 103 deletions(-) diff --git a/IntegrationTests/TestSuites/Sources/ConcurrencyTests/main.swift b/IntegrationTests/TestSuites/Sources/ConcurrencyTests/main.swift index 46eeeab69..ece58b317 100644 --- a/IntegrationTests/TestSuites/Sources/ConcurrencyTests/main.swift +++ b/IntegrationTests/TestSuites/Sources/ConcurrencyTests/main.swift @@ -123,16 +123,12 @@ func entrypoint() async throws { try expectEqual(result, .number(3)) } try expectGTE(diff, 200) -#if JAVASCRIPTKIT_WITHOUT_WEAKREFS - delayObject.closure = nil - delayClosure.release() -#endif } try await asyncTest("Async JSPromise: then") { let promise = JSPromise { resolve in _ = JSObject.global.setTimeout!( - JSOneshotClosure { _ in + JSClosure { _ in resolve(.success(JSValue.number(3))) return .undefined }.jsValue, @@ -153,7 +149,7 @@ func entrypoint() async throws { try await asyncTest("Async JSPromise: then(success:failure:)") { let promise = JSPromise { resolve in _ = JSObject.global.setTimeout!( - JSOneshotClosure { _ in + JSClosure { _ in resolve(.failure(JSError(message: "test").jsValue)) return .undefined }.jsValue, @@ -172,7 +168,7 @@ func entrypoint() async throws { try await asyncTest("Async JSPromise: catch") { let promise = JSPromise { resolve in _ = JSObject.global.setTimeout!( - JSOneshotClosure { _ in + JSClosure { _ in resolve(.failure(JSError(message: "test").jsValue)) return .undefined }.jsValue, diff --git a/IntegrationTests/TestSuites/Sources/PrimaryTests/UnitTestUtils.swift b/IntegrationTests/TestSuites/Sources/PrimaryTests/UnitTestUtils.swift index 098570d9b..c4f9a9fb1 100644 --- a/IntegrationTests/TestSuites/Sources/PrimaryTests/UnitTestUtils.swift +++ b/IntegrationTests/TestSuites/Sources/PrimaryTests/UnitTestUtils.swift @@ -111,7 +111,7 @@ func expectThrow(_ body: @autoclosure () throws -> T, file: StaticString = #f } func wrapUnsafeThrowableFunction(_ body: @escaping () -> Void, file: StaticString = #file, line: UInt = #line, column: UInt = #column) throws -> Error { - JSObject.global.callThrowingClosure.function!(JSOneshotClosure { _ in + JSObject.global.callThrowingClosure.function!(JSClosure { _ in body() return .undefined }) diff --git a/IntegrationTests/TestSuites/Sources/PrimaryTests/main.swift b/IntegrationTests/TestSuites/Sources/PrimaryTests/main.swift index 5a81e94cd..a9d127109 100644 --- a/IntegrationTests/TestSuites/Sources/PrimaryTests/main.swift +++ b/IntegrationTests/TestSuites/Sources/PrimaryTests/main.swift @@ -254,18 +254,12 @@ try test("Closure Lifetime") { do { let c1 = JSClosure { _ in .number(4) } try expectEqual(c1(), .number(4)) -#if JAVASCRIPTKIT_WITHOUT_WEAKREFS - c1.release() -#endif } do { let c1 = JSClosure { _ in fatalError("Crash while closure evaluation") } let error = try expectThrow(try evalClosure.throws(c1)) as! JSValue try expectEqual(error.description, "RuntimeError: unreachable") -#if JAVASCRIPTKIT_WITHOUT_WEAKREFS - c1.release() -#endif } } @@ -872,24 +866,16 @@ try test("Symbols") { // }.prop // Object.defineProperty(hasInstanceClass, Symbol.hasInstance, { value: () => true }) let hasInstanceObject = JSObject.global.Object.function!.new() - let hasInstanceObjectClosure = JSClosure { _ in .undefined } - hasInstanceObject.prop = hasInstanceObjectClosure.jsValue + hasInstanceObject.prop = JSClosure { _ in .undefined }.jsValue let hasInstanceClass = hasInstanceObject.prop.function! let propertyDescriptor = JSObject.global.Object.function!.new() - let propertyDescriptorClosure = JSClosure { _ in .boolean(true) } - propertyDescriptor.value = propertyDescriptorClosure.jsValue + propertyDescriptor.value = JSClosure { _ in .boolean(true) }.jsValue _ = JSObject.global.Object.function!.defineProperty!( hasInstanceClass, JSSymbol.hasInstance, propertyDescriptor ) try expectEqual(hasInstanceClass[JSSymbol.hasInstance].function!().boolean, true) try expectEqual(JSObject.global.Object.isInstanceOf(hasInstanceClass), true) -#if JAVASCRIPTKIT_WITHOUT_WEAKREFS - hasInstanceObject.prop = .undefined - propertyDescriptor.value = .undefined - hasInstanceObjectClosure.release() - propertyDescriptorClosure.release() -#endif } struct AnimalStruct: Decodable { diff --git a/Sources/JavaScriptKit/FundamentalObjects/JSClosure.swift b/Sources/JavaScriptKit/FundamentalObjects/JSClosure.swift index c3a0c886e..441dd2a6c 100644 --- a/Sources/JavaScriptKit/FundamentalObjects/JSClosure.swift +++ b/Sources/JavaScriptKit/FundamentalObjects/JSClosure.swift @@ -19,15 +19,17 @@ public class JSOneshotClosure: JSObject, JSClosureProtocol { // 1. Fill `id` as zero at first to access `self` to get `ObjectIdentifier`. super.init(id: 0) - // 2. Retain the given body in static storage - // Leak the self object globally and release once it's called - hostFuncRef = JSClosure.sharedClosures.register(ObjectIdentifier(self), object: .strong(self), body: { - defer { self.release() } - return body($0) - }) + // 2. Create a new JavaScript function which calls the given Swift function. + hostFuncRef = JavaScriptHostFuncRef(bitPattern: ObjectIdentifier(self)) id = withExtendedLifetime(JSString(file)) { file in _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) + }) } #if compiler(>=5.5) @@ -40,7 +42,7 @@ public class JSOneshotClosure: JSObject, JSClosureProtocol { /// Release this function resource. /// After calling `release`, calling this function from JavaScript will fail. public func release() { - JSClosure.sharedClosures.unregister(hostFuncRef) + JSClosure.sharedClosures[hostFuncRef] = nil } } @@ -60,13 +62,13 @@ public class JSOneshotClosure: JSObject, JSClosureProtocol { /// ``` /// public class JSClosure: JSFunction, JSClosureProtocol { - fileprivate static var sharedClosures = SharedJSClosureRegistry() + + // Note: Retain the closure object itself also to avoid funcRef conflicts + fileprivate static var sharedClosures: [JavaScriptHostFuncRef: (object: JSObject, body: ([JSValue]) -> JSValue)] = [:] private var hostFuncRef: JavaScriptHostFuncRef = 0 #if JAVASCRIPTKIT_WITHOUT_WEAKREFS - private let file: String - private let line: UInt32 private var isReleased: Bool = false #endif @@ -80,35 +82,30 @@ public class JSClosure: JSFunction, JSClosureProtocol { } public init(_ body: @escaping ([JSValue]) -> JSValue, file: String = #fileID, line: UInt32 = #line) { - #if JAVASCRIPTKIT_WITHOUT_WEAKREFS - self.file = file - self.line = line - #endif // 1. Fill `id` as zero at first to access `self` to get `ObjectIdentifier`. super.init(id: 0) - // 2. Retain the given body in static storage - hostFuncRef = Self.sharedClosures.register( - ObjectIdentifier(self), object: .weak(self), body: body - ) - + // 2. Create a new JavaScript function which calls the given Swift function. + hostFuncRef = JavaScriptHostFuncRef(bitPattern: ObjectIdentifier(self)) id = withExtendedLifetime(JSString(file)) { file in _create_function(hostFuncRef, line, file.asInternalJSRef()) } + // 3. Retain the given body in static storage by `funcRef`. + Self.sharedClosures[hostFuncRef] = (self, body) } #if compiler(>=5.5) @available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) - public static func async(_ body: @escaping ([JSValue]) async throws -> JSValue, file: String = #fileID, line: UInt32 = #line) -> JSClosure { - JSClosure(makeAsyncClosure(body), file: file, line: line) + public static func async(_ body: @escaping ([JSValue]) async throws -> JSValue) -> JSClosure { + JSClosure(makeAsyncClosure(body)) } #endif #if JAVASCRIPTKIT_WITHOUT_WEAKREFS deinit { guard isReleased else { - fatalError("release() must be called on JSClosure object (\(file):\(line)) manually before they are deallocated") + fatalError("release() must be called on JSClosure objects manually before they are deallocated") } } #endif @@ -136,60 +133,6 @@ private func makeAsyncClosure(_ body: @escaping ([JSValue]) async throws -> JSVa } #endif -/// Registry for Swift closures that are referenced from JavaScript. -private struct SharedJSClosureRegistry { - struct ClosureEntry { - // Note: Retain the closure object itself also to avoid funcRef conflicts. - var object: AnyObjectReference - var body: ([JSValue]) -> JSValue - - init(object: AnyObjectReference, body: @escaping ([JSValue]) -> JSValue) { - self.object = object - self.body = body - } - } - enum AnyObjectReference { - case strong(AnyObject) - case weak(WeakObject) - - static func `weak`(_ object: AnyObject) -> AnyObjectReference { - .weak(SharedJSClosureRegistry.WeakObject(underlying: object)) - } - } - struct WeakObject { - weak var underlying: AnyObject? - init(underlying: AnyObject) { - self.underlying = underlying - } - } - private var closures: [JavaScriptHostFuncRef: ClosureEntry] = [:] - - /// Register a Swift closure to be called from JavaScript. - /// - Parameters: - /// - hint: A hint to identify the closure. - /// - object: The object should be retained until the closure is released from JavaScript. - /// - body: The closure to be called from JavaScript. - /// - Returns: An unique identifier for the registered closure. - mutating func register( - _ hint: ObjectIdentifier, - object: AnyObjectReference, body: @escaping ([JSValue]) -> JSValue - ) -> JavaScriptHostFuncRef { - let ref = JavaScriptHostFuncRef(bitPattern: hint) - closures[ref] = ClosureEntry(object: object, body: body) - return ref - } - - /// Unregister a Swift closure from the registry. - mutating func unregister(_ ref: JavaScriptHostFuncRef) { - closures[ref] = nil - } - - /// Get the Swift closure from the registry. - subscript(_ ref: JavaScriptHostFuncRef) -> (([JSValue]) -> JSValue)? { - closures[ref]?.body - } -} - // MARK: - `JSClosure` mechanism note // // 1. Create a thunk in the JavaScript world, which has a reference @@ -231,7 +174,7 @@ func _call_host_function_impl( _ argv: UnsafePointer, _ argc: Int32, _ callbackFuncRef: JavaScriptObjectRef ) -> Bool { - guard let hostFunc = JSClosure.sharedClosures[hostFuncRef] else { + guard let (_, hostFunc) = JSClosure.sharedClosures[hostFuncRef] else { return true } let arguments = UnsafeBufferPointer(start: argv, count: Int(argc)).map(\.jsValue) @@ -252,7 +195,7 @@ func _call_host_function_impl( extension JSClosure { public func release() { isReleased = true - Self.sharedClosures.unregister(hostFuncRef) + Self.sharedClosures[hostFuncRef] = nil } } @@ -270,6 +213,6 @@ extension JSClosure { @_cdecl("_free_host_function_impl") func _free_host_function_impl(_ hostFuncRef: JavaScriptHostFuncRef) { - JSClosure.sharedClosures.unregister(hostFuncRef) + JSClosure.sharedClosures[hostFuncRef] = nil } #endif From b682179d838d480696810222f01acc72b27cf1cd Mon Sep 17 00:00:00 2001 From: kuhl Date: Thu, 2 May 2024 22:05:28 -0600 Subject: [PATCH 099/373] Update README file to include new carton 1.0 implementation. (#243) * Update README file to include new carton 1.0 implementation. * Update README to include the updated Carton 1.0 dependency to the JavaScriptKit Example. * Collapsible Legacy Section. * Fix Readme Package description for carton usage. * Add horizontal rules to Legacy block for readability. * Added instruction to run carton from swift in README. --- README.md | 34 ++++++++++++++++++++++++++++++++-- 1 file changed, 32 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 7c27b0671..401e68893 100644 --- a/README.md +++ b/README.md @@ -66,7 +66,7 @@ a few additional steps though (you can skip these steps if your app depends on name: "JavaScriptKitExample", dependencies: [ "JavaScriptKit", - .product(name: "JavaScriptEventLoop", package: "JavaScriptKit") + .product(name: "JavaScriptEventLoop", package: "JavaScriptKit"), ] ) ``` @@ -179,7 +179,33 @@ Not all of these versions are tested on regular basis though, compatibility repo ## 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). +bundler](https://carton.dev). Add carton to your swift package dependencies: + +```diff +dependencies: [ ++ .package(url: "https://github.com/swiftwasm/carton", from: "1.0.0"), +], +``` + +Now you can activate the package dependency through swift: + +``` +swift run carton dev +``` + +If you have multiple products in your package, you can also used the product flag: + +``` +swift run carton dev --product MyApp +``` + +> [!WARNING] +> - If you already use `carton` before 0.x.x versions via Homebrew, you can remove it with `brew uninstall carton` and install the new version as a SwiftPM dependency. +> - Also please remove the old `.build` directory before using the new `carton` + +
Legacy Installation + +--- As a part of these steps you'll install `carton` via [Homebrew](https://brew.sh/) on macOS (you can also use the @@ -218,6 +244,10 @@ carton init --template basic 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 From 68376c5a049e156809ed48189f9e368c0d0d761b Mon Sep 17 00:00:00 2001 From: kuhl Date: Sun, 5 May 2024 22:14:14 -0600 Subject: [PATCH 100/373] Update Carton context on README. (#245) --- README.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 401e68893..63f432caa 100644 --- a/README.md +++ b/README.md @@ -248,10 +248,10 @@ 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. +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) From 74c35de1af3c7ec030d25a60eaa8d1446968d6fd Mon Sep 17 00:00:00 2001 From: Yuta Saito Date: Wed, 8 May 2024 22:18:58 +0900 Subject: [PATCH 101/373] Support latest nightly snapshot (#246) * Allow async main entry point with JavaScriptEventLoop This change allows the Swift program to have an async main entry point when the JavaScriptEventLoop is installed as the global executor. * Remove Wasmer WASI tests We no longer use it in carton * Support the latest nightly snapshot * Migrate to ESM * Fix host build --- .github/workflows/test.yml | 4 +- IntegrationTests/Makefile | 3 +- .../Sources/PrimaryTests/main.swift | 2 +- IntegrationTests/bin/benchmark-tests.js | 4 +- IntegrationTests/bin/concurrency-tests.js | 2 +- IntegrationTests/bin/primary-tests.js | 2 +- IntegrationTests/lib.js | 67 +--- IntegrationTests/package-lock.json | 328 +----------------- IntegrationTests/package.json | 3 +- Makefile | 4 +- Runtime/src/index.ts | 39 +++ Runtime/src/js-value.ts | 8 +- Runtime/src/types.ts | 1 + .../JavaScriptEventLoop.swift | 14 + Sources/JavaScriptKit/Runtime/index.js | 38 ++ Sources/JavaScriptKit/Runtime/index.mjs | 38 ++ Sources/JavaScriptKit/XcodeSupport.swift | 8 +- .../include/_CJavaScriptEventLoop.h | 19 +- .../_CJavaScriptKit/include/_CJavaScriptKit.h | 3 + scripts/test-harness.js | 10 - scripts/test-harness.mjs | 15 + 21 files changed, 200 insertions(+), 412 deletions(-) delete mode 100644 scripts/test-harness.js create mode 100644 scripts/test-harness.mjs diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 121d22236..1767b1385 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -18,12 +18,10 @@ jobs: # Ensure that test succeeds with all toolchains and wasi backend combinations - { os: ubuntu-20.04, toolchain: wasm-5.7.3-RELEASE, wasi-backend: Node } - { os: ubuntu-20.04, toolchain: wasm-5.8.0-RELEASE, wasi-backend: Node } - - { os: ubuntu-20.04, toolchain: wasm-5.7.3-RELEASE, wasi-backend: Wasmer } - - { os: ubuntu-20.04, toolchain: wasm-5.8.0-RELEASE, wasi-backend: Wasmer } - - { os: ubuntu-20.04, toolchain: wasm-5.9.1-RELEASE, wasi-backend: Wasmer } - { os: ubuntu-20.04, toolchain: wasm-5.7.3-RELEASE, wasi-backend: MicroWASI } - { os: ubuntu-20.04, toolchain: wasm-5.8.0-RELEASE, wasi-backend: MicroWASI } - { os: ubuntu-20.04, toolchain: wasm-5.9.1-RELEASE, wasi-backend: MicroWASI } + - { os: ubuntu-22.04, toolchain: wasm-DEVELOPMENT-SNAPSHOT-2024-05-02-a, wasi-backend: Node } runs-on: ${{ matrix.entry.os }} env: diff --git a/IntegrationTests/Makefile b/IntegrationTests/Makefile index 57b99c8da..3329225f1 100644 --- a/IntegrationTests/Makefile +++ b/IntegrationTests/Makefile @@ -11,7 +11,8 @@ TestSuites/.build/$(CONFIGURATION)/%.wasm: FORCE --triple wasm32-unknown-wasi \ --configuration $(CONFIGURATION) \ -Xswiftc -Xclang-linker -Xswiftc -mexec-model=reactor \ - -Xlinker --export=main \ + -Xlinker --export-if-defined=main -Xlinker --export-if-defined=__main_argc_argv \ + --static-swift-stdlib -Xswiftc -static-stdlib \ $(SWIFT_BUILD_FLAGS) dist/%.wasm: TestSuites/.build/$(CONFIGURATION)/%.wasm diff --git a/IntegrationTests/TestSuites/Sources/PrimaryTests/main.swift b/IntegrationTests/TestSuites/Sources/PrimaryTests/main.swift index a9d127109..716151034 100644 --- a/IntegrationTests/TestSuites/Sources/PrimaryTests/main.swift +++ b/IntegrationTests/TestSuites/Sources/PrimaryTests/main.swift @@ -400,7 +400,7 @@ try test("Call Function With This") { let setName = try expectFunction(getJSValue(this: cat1, name: "setName")) // Direct call without this - try expectEqual(getIsCat(), .undefined) + _ = try expectThrow(try getIsCat.throws()) // Call with this let gotIsCat = getIsCat(this: cat1) diff --git a/IntegrationTests/bin/benchmark-tests.js b/IntegrationTests/bin/benchmark-tests.js index 424ce8199..0d8b5410a 100644 --- a/IntegrationTests/bin/benchmark-tests.js +++ b/IntegrationTests/bin/benchmark-tests.js @@ -1,5 +1,5 @@ -const { startWasiTask } = require("../lib"); -const { performance } = require("perf_hooks"); +import { startWasiTask } from "../lib.js"; +import { performance } from "perf_hooks"; const SAMPLE_ITERATION = 1000000 diff --git a/IntegrationTests/bin/concurrency-tests.js b/IntegrationTests/bin/concurrency-tests.js index 47ef4abda..02489c959 100644 --- a/IntegrationTests/bin/concurrency-tests.js +++ b/IntegrationTests/bin/concurrency-tests.js @@ -1,4 +1,4 @@ -const { startWasiTask } = require("../lib"); +import { startWasiTask } from "../lib.js"; Error.stackTraceLimit = Infinity; diff --git a/IntegrationTests/bin/primary-tests.js b/IntegrationTests/bin/primary-tests.js index 50532ceac..36ac65812 100644 --- a/IntegrationTests/bin/primary-tests.js +++ b/IntegrationTests/bin/primary-tests.js @@ -102,7 +102,7 @@ global.objectDecodingTest = { bi: BigInt(3) }; -const { startWasiTask, WASI } = require("../lib"); +import { startWasiTask } from "../lib.js"; startWasiTask("./dist/PrimaryTests.wasm").catch((err) => { console.log(err); diff --git a/IntegrationTests/lib.js b/IntegrationTests/lib.js index 1d7b6342d..2ed9a918d 100644 --- a/IntegrationTests/lib.js +++ b/IntegrationTests/lib.js @@ -1,49 +1,9 @@ -const SwiftRuntime = require("javascript-kit-swift").SwiftRuntime; -const WasmerWASI = require("@wasmer/wasi").WASI; -const WasmFs = require("@wasmer/wasmfs").WasmFs; -const NodeWASI = require("wasi").WASI; -const { WASI: MicroWASI, useAll } = require("uwasi"); - -const promisify = require("util").promisify; -const fs = require("fs"); -const readFile = promisify(fs.readFile); +import { SwiftRuntime } from "javascript-kit-swift" +import { WASI as NodeWASI } from "wasi" +import { WASI as MicroWASI, useAll } from "uwasi" +import * as fs from "fs/promises" const WASI = { - Wasmer: ({ programName }) => { - // 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); - }; - const wasi = new WasmerWASI({ - args: [programName], - env: {}, - bindings: { - ...WasmerWASI.defaultBindings, - fs: wasmFs.fs, - }, - }); - - return { - wasiImport: wasi.wasiImport, - start(instance) { - wasi.start(instance); - instance.exports._initialize(); - instance.exports.main(); - } - } - }, MicroWASI: ({ programName }) => { const wasi = new MicroWASI({ args: [programName], @@ -53,9 +13,9 @@ const WASI = { return { wasiImport: wasi.wasiImport, - start(instance) { + start(instance, swift) { wasi.initialize(instance); - instance.exports.main(); + swift.main(); } } }, @@ -63,15 +23,18 @@ const WASI = { const wasi = new NodeWASI({ args: [programName], env: {}, + preopens: { + "/": "./", + }, returnOnExit: false, version: "preview1", }) return { wasiImport: wasi.wasiImport, - start(instance) { + start(instance, swift) { wasi.initialize(instance); - instance.exports.main(); + swift.main(); } } }, @@ -88,10 +51,10 @@ const selectWASIBackend = () => { return WASI.Node; }; -const startWasiTask = async (wasmPath, wasiConstructor = selectWASIBackend()) => { +export const startWasiTask = async (wasmPath, wasiConstructor = selectWASIBackend()) => { const swift = new SwiftRuntime(); // Fetch our Wasm File - const wasmBinary = await readFile(wasmPath); + const wasmBinary = await fs.readFile(wasmPath); const wasi = wasiConstructor({ programName: wasmPath }); // Instantiate the WebAssembly file @@ -106,7 +69,5 @@ const startWasiTask = async (wasmPath, wasiConstructor = selectWASIBackend()) => swift.setInstance(instance); // Start the WebAssembly WASI instance! - wasi.start(instance); + wasi.start(instance, swift); }; - -module.exports = { startWasiTask, WASI }; diff --git a/IntegrationTests/package-lock.json b/IntegrationTests/package-lock.json index 8aff33b98..d0b914f04 100644 --- a/IntegrationTests/package-lock.json +++ b/IntegrationTests/package-lock.json @@ -5,15 +5,13 @@ "packages": { "": { "dependencies": { - "@wasmer/wasi": "^0.12.0", - "@wasmer/wasmfs": "^0.12.0", "javascript-kit-swift": "file:..", "uwasi": "^1.2.0" } }, "..": { "name": "javascript-kit-swift", - "version": "0.19.1", + "version": "0.19.2", "license": "MIT", "devDependencies": { "@rollup/plugin-typescript": "^8.3.1", @@ -46,264 +44,17 @@ "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/uwasi": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/uwasi/-/uwasi-1.2.0.tgz", "integrity": "sha512-+U3ajjQgx/Xh1/ZNrgH0EzM5qI2czr94oz3DPDwTvUIlM4SFpDjTqJzDA3xcqlTmpp2YGpxApmjwZfablMUoOg==" - }, - "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": { @@ -326,87 +77,10 @@ } } }, - "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=" - }, "uwasi": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/uwasi/-/uwasi-1.2.0.tgz", "integrity": "sha512-+U3ajjQgx/Xh1/ZNrgH0EzM5qI2czr94oz3DPDwTvUIlM4SFpDjTqJzDA3xcqlTmpp2YGpxApmjwZfablMUoOg==" - }, - "wrappy": { - "version": "1.0.2", - "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=" } } } diff --git a/IntegrationTests/package.json b/IntegrationTests/package.json index a7d756165..8491e91fb 100644 --- a/IntegrationTests/package.json +++ b/IntegrationTests/package.json @@ -1,8 +1,7 @@ { "private": true, + "type": "module", "dependencies": { - "@wasmer/wasi": "^0.12.0", - "@wasmer/wasmfs": "^0.12.0", "uwasi": "^1.2.0", "javascript-kit-swift": "file:.." } diff --git a/Makefile b/Makefile index ccf22798d..e71734a5f 100644 --- a/Makefile +++ b/Makefile @@ -21,8 +21,8 @@ test: .PHONY: unittest unittest: @echo Running unit tests - swift build --build-tests --triple wasm32-unknown-wasi -Xswiftc -Xclang-linker -Xswiftc -mexec-model=reactor -Xlinker --export=main - node --experimental-wasi-unstable-preview1 scripts/test-harness.js ./.build/wasm32-unknown-wasi/debug/JavaScriptKitPackageTests.wasm + swift build --build-tests --triple wasm32-unknown-wasi -Xswiftc -Xclang-linker -Xswiftc -mexec-model=reactor -Xlinker --export-if-defined=main -Xlinker --export-if-defined=__main_argc_argv --static-swift-stdlib -Xswiftc -static-stdlib + node --experimental-wasi-unstable-preview1 scripts/test-harness.mjs ./.build/wasm32-unknown-wasi/debug/JavaScriptKitPackageTests.wasm .PHONY: benchmark_setup benchmark_setup: diff --git a/Runtime/src/index.ts b/Runtime/src/index.ts index a9da3eb9f..ac0adde2a 100644 --- a/Runtime/src/index.ts +++ b/Runtime/src/index.ts @@ -45,6 +45,27 @@ export class SwiftRuntime { } } + 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; + } + } + private get instance() { if (!this._instance) throw new Error("WebAssembly instance is not set yet"); @@ -419,5 +440,23 @@ export class SwiftRuntime { signed ? BigInt.asIntN(64, value) : BigInt.asUintN(64, value) ); }, + swjs_unsafe_event_loop_yield: () => { + throw new UnsafeEventLoopYield(); + }, }; } + +/// 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/js-value.ts b/Runtime/src/js-value.ts index 9ff3d065e..1b142de05 100644 --- a/Runtime/src/js-value.ts +++ b/Runtime/src/js-value.ts @@ -82,7 +82,13 @@ export const write = ( is_exception: boolean, memory: Memory ) => { - const kind = writeAndReturnKindBits(value, payload1_ptr, payload2_ptr, is_exception, memory); + const kind = writeAndReturnKindBits( + value, + payload1_ptr, + payload2_ptr, + is_exception, + memory + ); memory.writeUint32(kind_ptr, kind); }; diff --git a/Runtime/src/types.ts b/Runtime/src/types.ts index ff20999ea..55f945b64 100644 --- a/Runtime/src/types.ts +++ b/Runtime/src/types.ts @@ -102,6 +102,7 @@ export interface ImportedFunctions { swjs_i64_to_bigint(value: bigint, signed: bool): ref; swjs_bigint_to_i64(ref: ref, signed: bool): bigint; swjs_i64_to_bigint_slow(lower: number, upper: number, signed: bool): ref; + swjs_unsafe_event_loop_yield: () => void; } export const enum LibraryFeatures { diff --git a/Sources/JavaScriptEventLoop/JavaScriptEventLoop.swift b/Sources/JavaScriptEventLoop/JavaScriptEventLoop.swift index 7f6783062..04aedb940 100644 --- a/Sources/JavaScriptEventLoop/JavaScriptEventLoop.swift +++ b/Sources/JavaScriptEventLoop/JavaScriptEventLoop.swift @@ -1,5 +1,6 @@ import JavaScriptKit import _CJavaScriptEventLoop +import _CJavaScriptKit // NOTE: `@available` annotations are semantically wrong, but they make it easier to develop applications targeting WebAssembly in Xcode. @@ -90,6 +91,14 @@ public final class JavaScriptEventLoop: SerialExecutor, @unchecked Sendable { public static func installGlobalExecutor() { guard !didInstallGlobalExecutor else { return } + #if compiler(>=5.9) + typealias swift_task_asyncMainDrainQueue_hook_Fn = @convention(thin) (swift_task_asyncMainDrainQueue_original, swift_task_asyncMainDrainQueue_override) -> Void + let swift_task_asyncMainDrainQueue_hook_impl: swift_task_asyncMainDrainQueue_hook_Fn = { _, _ in + _unsafe_event_loop_yield() + } + swift_task_asyncMainDrainQueue_hook = unsafeBitCast(swift_task_asyncMainDrainQueue_hook_impl, to: UnsafeMutableRawPointer?.self) + #endif + typealias swift_task_enqueueGlobal_hook_Fn = @convention(thin) (UnownedJob, swift_task_enqueueGlobal_original) -> Void let swift_task_enqueueGlobal_hook_impl: swift_task_enqueueGlobal_hook_Fn = { job, original in JavaScriptEventLoop.shared.unsafeEnqueue(job) @@ -216,3 +225,8 @@ public extension JSPromise { } #endif + +// See `Sources/JavaScriptKit/XcodeSupport.swift` for rationale of the stub functions. +#if !arch(wasm32) + func _unsafe_event_loop_yield() { fatalError() } +#endif diff --git a/Sources/JavaScriptKit/Runtime/index.js b/Sources/JavaScriptKit/Runtime/index.js index 02dc9382e..9cd1995ac 100644 --- a/Sources/JavaScriptKit/Runtime/index.js +++ b/Sources/JavaScriptKit/Runtime/index.js @@ -366,6 +366,9 @@ (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(); + }, }; this._instance = null; this._memory = null; @@ -384,6 +387,26 @@ 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; + } + } get instance() { if (!this._instance) throw new Error("WebAssembly instance is not set yet"); @@ -430,6 +453,21 @@ return output; } } + /// This error is thrown when yielding event loop control from `swift_task_asyncMainDrainQueue` + /// to JavaScript. This is usually thrown when: + /// - The entry point of the Swift program is `func main() async` + /// - The Swift Concurrency's global executor is hooked by `JavaScriptEventLoop.installGlobalExecutor()` + /// - Calling exported `main` or `__main_argc_argv` function from JavaScript + /// + /// This exception must be caught by the caller of the exported function and the caller should + /// catch this exception and just ignore it. + /// + /// FAQ: Why this error is thrown? + /// This error is thrown to unwind the call stack of the Swift program and return the control to + /// the JavaScript side. Otherwise, the `swift_task_asyncMainDrainQueue` ends up with `abort()` + /// because the event loop expects `exit()` call before the end of the event loop. + class UnsafeEventLoopYield extends Error { + } exports.SwiftRuntime = SwiftRuntime; diff --git a/Sources/JavaScriptKit/Runtime/index.mjs b/Sources/JavaScriptKit/Runtime/index.mjs index 823ffca60..78c99457c 100644 --- a/Sources/JavaScriptKit/Runtime/index.mjs +++ b/Sources/JavaScriptKit/Runtime/index.mjs @@ -360,6 +360,9 @@ class SwiftRuntime { (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(); + }, }; this._instance = null; this._memory = null; @@ -378,6 +381,26 @@ class SwiftRuntime { 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; + } + } get instance() { if (!this._instance) throw new Error("WebAssembly instance is not set yet"); @@ -424,5 +447,20 @@ class SwiftRuntime { return output; } } +/// This error is thrown when yielding event loop control from `swift_task_asyncMainDrainQueue` +/// to JavaScript. This is usually thrown when: +/// - The entry point of the Swift program is `func main() async` +/// - The Swift Concurrency's global executor is hooked by `JavaScriptEventLoop.installGlobalExecutor()` +/// - Calling exported `main` or `__main_argc_argv` function from JavaScript +/// +/// This exception must be caught by the caller of the exported function and the caller should +/// catch this exception and just ignore it. +/// +/// FAQ: Why this error is thrown? +/// This error is thrown to unwind the call stack of the Swift program and return the control to +/// the JavaScript side. Otherwise, the `swift_task_asyncMainDrainQueue` ends up with `abort()` +/// because the event loop expects `exit()` call before the end of the event loop. +class UnsafeEventLoopYield extends Error { +} export { SwiftRuntime }; diff --git a/Sources/JavaScriptKit/XcodeSupport.swift b/Sources/JavaScriptKit/XcodeSupport.swift index 9689cf3b0..ac5f117b4 100644 --- a/Sources/JavaScriptKit/XcodeSupport.swift +++ b/Sources/JavaScriptKit/XcodeSupport.swift @@ -1,10 +1,10 @@ import _CJavaScriptKit /// Note: -/// Define all runtime function stubs 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. +/// Define stubs for runtime functions which are usually imported from JavaScript environment. +/// JavaScriptKit itself supports only WebAssembly target, but it should be able +/// to be built for host platforms like macOS or Linux for tentative IDE support. +/// (ideally, IDE should build for WebAssembly target though) #if !arch(wasm32) func _set_prop( _: JavaScriptObjectRef, diff --git a/Sources/_CJavaScriptEventLoop/include/_CJavaScriptEventLoop.h b/Sources/_CJavaScriptEventLoop/include/_CJavaScriptEventLoop.h index b24d19d04..2880772d6 100644 --- a/Sources/_CJavaScriptEventLoop/include/_CJavaScriptEventLoop.h +++ b/Sources/_CJavaScriptEventLoop/include/_CJavaScriptEventLoop.h @@ -27,13 +27,13 @@ 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; /// 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; typedef SWIFT_CC(swift) void (*swift_task_enqueueGlobalWithDeadline_original)( long long sec, @@ -42,12 +42,23 @@ typedef SWIFT_CC(swift) void (*swift_task_enqueueGlobalWithDeadline_original)( long long tnsec, int clock, Job *_Nonnull job); SWIFT_EXPORT_FROM(swift_Concurrency) -void *_Nullable swift_task_enqueueGlobalWithDeadline_hook; +extern void *_Nullable swift_task_enqueueGlobalWithDeadline_hook; /// 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; + +/// 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; #endif diff --git a/Sources/_CJavaScriptKit/include/_CJavaScriptKit.h b/Sources/_CJavaScriptKit/include/_CJavaScriptKit.h index b60007ed0..a9d8738af 100644 --- a/Sources/_CJavaScriptKit/include/_CJavaScriptKit.h +++ b/Sources/_CJavaScriptKit/include/_CJavaScriptKit.h @@ -322,6 +322,9 @@ __attribute__((__import_module__("javascript_kit"), __import_name__("swjs_release"))) extern void _release(const JavaScriptObjectRef ref); +__attribute__((__import_module__("javascript_kit"), + __import_name__("swjs_unsafe_event_loop_yield"))) +extern void _unsafe_event_loop_yield(void); #endif diff --git a/scripts/test-harness.js b/scripts/test-harness.js deleted file mode 100644 index 39a7dbe9a..000000000 --- a/scripts/test-harness.js +++ /dev/null @@ -1,10 +0,0 @@ -Error.stackTraceLimit = Infinity; - -const { startWasiTask, WASI } = require("../IntegrationTests/lib"); - -const handleExitOrError = (error) => { - console.log(error); - process.exit(1); -} - -startWasiTask(process.argv[2]).catch(handleExitOrError); diff --git a/scripts/test-harness.mjs b/scripts/test-harness.mjs new file mode 100644 index 000000000..b0384d4de --- /dev/null +++ b/scripts/test-harness.mjs @@ -0,0 +1,15 @@ +Error.stackTraceLimit = Infinity; + +import { startWasiTask } from "../IntegrationTests/lib.js"; + +if (process.env["JAVASCRIPTKIT_WASI_BACKEND"] === "MicroWASI") { + console.log("Skipping XCTest tests for MicroWASI because it is not supported yet."); + process.exit(0); +} + +const handleExitOrError = (error) => { + console.log(error); + process.exit(1); +} + +startWasiTask(process.argv[2]).catch(handleExitOrError); From 9ad94b93a5f3a4ead4cb784ecc222908d7570542 Mon Sep 17 00:00:00 2001 From: Yuta Saito Date: Wed, 8 May 2024 13:20:31 +0000 Subject: [PATCH 102/373] Suppress warning about executableTarget --- IntegrationTests/TestSuites/Package.swift | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/IntegrationTests/TestSuites/Package.swift b/IntegrationTests/TestSuites/Package.swift index 9888e7388..95b47f94c 100644 --- a/IntegrationTests/TestSuites/Package.swift +++ b/IntegrationTests/TestSuites/Package.swift @@ -24,17 +24,17 @@ let package = Package( dependencies: [.package(name: "JavaScriptKit", path: "../../")], targets: [ .target(name: "CHelpers"), - .target(name: "PrimaryTests", dependencies: [ + .executableTarget(name: "PrimaryTests", dependencies: [ .product(name: "JavaScriptBigIntSupport", package: "JavaScriptKit"), "JavaScriptKit", "CHelpers", ]), - .target( + .executableTarget( name: "ConcurrencyTests", dependencies: [ .product(name: "JavaScriptEventLoop", package: "JavaScriptKit"), ] ), - .target(name: "BenchmarkTests", dependencies: ["JavaScriptKit", "CHelpers"]), + .executableTarget(name: "BenchmarkTests", dependencies: ["JavaScriptKit", "CHelpers"]), ] ) From 8449f87bc103df0d3cc3702838a83d5a97580b55 Mon Sep 17 00:00:00 2001 From: Yuta Saito Date: Mon, 27 May 2024 21:06:58 +0900 Subject: [PATCH 103/373] Use Swift SDK for development snapshot testing in CI (#248) --- .github/actions/install-swift/action.yml | 44 ++++++++++++++++++++++++ .github/workflows/test.yml | 25 +++++++++++++- IntegrationTests/Makefile | 1 - Makefile | 20 +++++++---- 4 files changed, 81 insertions(+), 9 deletions(-) create mode 100644 .github/actions/install-swift/action.yml diff --git a/.github/actions/install-swift/action.yml b/.github/actions/install-swift/action.yml new file mode 100644 index 000000000..bdc4c9345 --- /dev/null +++ b/.github/actions/install-swift/action.yml @@ -0,0 +1,44 @@ +inputs: + swift-dir: + description: The directory name part of the distribution URL + required: true + swift-version: + description: Git tag indicating the Swift version + 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: Download Swift + shell: bash + run: curl -fLO https://download.swift.org/${{ inputs.swift-dir }}/${{ inputs.swift-version }}/${{ inputs.swift-version }}-ubuntu22.04.tar.gz + working-directory: ${{ env.RUNNER_TEMP }} + + - name: Unarchive and Install Swift + shell: bash + run: sudo tar -xf ${{ inputs.swift-version }}-ubuntu22.04.tar.gz --strip-components=2 -C /usr/local + working-directory: ${{ env.RUNNER_TEMP }} diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 1767b1385..128c3098f 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -21,7 +21,19 @@ jobs: - { os: ubuntu-20.04, toolchain: wasm-5.7.3-RELEASE, wasi-backend: MicroWASI } - { os: ubuntu-20.04, toolchain: wasm-5.8.0-RELEASE, wasi-backend: MicroWASI } - { os: ubuntu-20.04, toolchain: wasm-5.9.1-RELEASE, wasi-backend: MicroWASI } - - { os: ubuntu-22.04, toolchain: wasm-DEVELOPMENT-SNAPSHOT-2024-05-02-a, wasi-backend: Node } + - os: ubuntu-22.04 + toolchain: DEVELOPMENT-SNAPSHOT-2024-05-01-a + swift-sdk: + id: DEVELOPMENT-SNAPSHOT-2024-05-25-a-wasm32-unknown-wasi + download-url: "https://github.com/swiftwasm/swift/releases/download/swift-wasm-DEVELOPMENT-SNAPSHOT-2024-05-25-a/swift-wasm-DEVELOPMENT-SNAPSHOT-2024-05-25-a-wasm32-unknown-wasi.artifactbundle.zip" + wasi-backend: Node + # TODO: Enable this once we support threads in JavaScriptKit + # - os: ubuntu-22.04 + # toolchain: DEVELOPMENT-SNAPSHOT-2024-05-01-a + # swift-sdk: + # id: DEVELOPMENT-SNAPSHOT-2024-05-25-a-wasm32-unknown-wasip1-threads + # download-url: "https://github.com/swiftwasm/swift/releases/download/swift-wasm-DEVELOPMENT-SNAPSHOT-2024-05-25-a/swift-wasm-DEVELOPMENT-SNAPSHOT-2024-05-25-a-wasm32-unknown-wasip1-threads.artifactbundle.zip" + # wasi-backend: Node runs-on: ${{ matrix.entry.os }} env: @@ -34,8 +46,19 @@ jobs: if: ${{ matrix.entry.xcode }} run: sudo xcode-select -s /Applications/${{ matrix.entry.xcode }} - uses: swiftwasm/setup-swiftwasm@v1 + if: ${{ matrix.entry.swift-sdk == null }} with: swift-version: ${{ matrix.entry.toolchain }} + - uses: ./.github/actions/install-swift + if: ${{ matrix.entry.swift-sdk }} + with: + swift-dir: development/ubuntu2204 + swift-version: swift-${{ matrix.entry.toolchain }} + - name: Install Swift SDK + if: ${{ matrix.entry.swift-sdk }} + run: | + swift sdk install "${{ matrix.entry.swift-sdk.download-url }}" + echo "SWIFT_SDK_ID=${{ matrix.entry.swift-sdk.id }}" >> $GITHUB_ENV - run: make bootstrap - run: make test - run: make unittest diff --git a/IntegrationTests/Makefile b/IntegrationTests/Makefile index 3329225f1..30ffef297 100644 --- a/IntegrationTests/Makefile +++ b/IntegrationTests/Makefile @@ -8,7 +8,6 @@ FORCE: TestSuites/.build/$(CONFIGURATION)/%.wasm: FORCE swift build --package-path TestSuites \ --product $(basename $(notdir $@)) \ - --triple wasm32-unknown-wasi \ --configuration $(CONFIGURATION) \ -Xswiftc -Xclang-linker -Xswiftc -mexec-model=reactor \ -Xlinker --export-if-defined=main -Xlinker --export-if-defined=__main_argc_argv \ diff --git a/Makefile b/Makefile index e71734a5f..4714d9151 100644 --- a/Makefile +++ b/Makefile @@ -1,5 +1,11 @@ MAKEFILE_DIR := $(dir $(lastword $(MAKEFILE_LIST))) +ifeq ($(SWIFT_SDK_ID),) +SWIFT_BUILD_FLAGS := --triple wasm32-unknown-wasi +else +SWIFT_BUILD_FLAGS := --swift-sdk $(SWIFT_SDK_ID) +endif + .PHONY: bootstrap bootstrap: npm ci @@ -13,24 +19,24 @@ build: test: @echo Running integration tests 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 + CONFIGURATION=debug SWIFT_BUILD_FLAGS="$(SWIFT_BUILD_FLAGS)" $(MAKE) test && \ + CONFIGURATION=debug SWIFT_BUILD_FLAGS="$(SWIFT_BUILD_FLAGS) -Xswiftc -DJAVASCRIPTKIT_WITHOUT_WEAKREFS" $(MAKE) test && \ + CONFIGURATION=release SWIFT_BUILD_FLAGS="$(SWIFT_BUILD_FLAGS)" $(MAKE) test && \ + CONFIGURATION=release SWIFT_BUILD_FLAGS="$(SWIFT_BUILD_FLAGS) -Xswiftc -DJAVASCRIPTKIT_WITHOUT_WEAKREFS" $(MAKE) test .PHONY: unittest unittest: @echo Running unit tests - swift build --build-tests --triple wasm32-unknown-wasi -Xswiftc -Xclang-linker -Xswiftc -mexec-model=reactor -Xlinker --export-if-defined=main -Xlinker --export-if-defined=__main_argc_argv --static-swift-stdlib -Xswiftc -static-stdlib + swift build --build-tests -Xswiftc -Xclang-linker -Xswiftc -mexec-model=reactor -Xlinker --export-if-defined=main -Xlinker --export-if-defined=__main_argc_argv --static-swift-stdlib -Xswiftc -static-stdlib $(SWIFT_BUILD_FLAGS) node --experimental-wasi-unstable-preview1 scripts/test-harness.mjs ./.build/wasm32-unknown-wasi/debug/JavaScriptKitPackageTests.wasm .PHONY: benchmark_setup benchmark_setup: - cd IntegrationTests && CONFIGURATION=release make benchmark_setup + SWIFT_BUILD_FLAGS="$(SWIFT_BUILD_FLAGS)" CONFIGURATION=release $(MAKE) -C IntegrationTests benchmark_setup .PHONY: run_benchmark run_benchmark: - cd IntegrationTests && CONFIGURATION=release make -s run_benchmark + SWIFT_BUILD_FLAGS="$(SWIFT_BUILD_FLAGS)" CONFIGURATION=release $(MAKE) -s -C IntegrationTests run_benchmark .PHONY: perf-tester perf-tester: From ea069824e205056292f6e50da995975477841432 Mon Sep 17 00:00:00 2001 From: Yuta Saito Date: Mon, 27 May 2024 21:38:57 +0900 Subject: [PATCH 104/373] Add `sharedMemory` option to allow threads with shared memory (#247) * Add `sharedMemory` option to allow threads with shared memory * Enable thread enabled Swift SDK testing in CI * Support shared memory in test harness * Don't use symlink directory in the path of the program name This is a workaround for the issue that the argv0 program path containing a symlink directory in the path causes `Bundle.main` to crash. --- .github/workflows/test.yml | 13 +- IntegrationTests/lib.js | 39 +- Makefile | 2 +- Runtime/src/index.ts | 580 ++++++++++++------------ Sources/JavaScriptKit/Runtime/index.js | 196 ++++---- Sources/JavaScriptKit/Runtime/index.mjs | 196 ++++---- 6 files changed, 551 insertions(+), 475 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 128c3098f..26227b530 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -27,13 +27,12 @@ jobs: id: DEVELOPMENT-SNAPSHOT-2024-05-25-a-wasm32-unknown-wasi download-url: "https://github.com/swiftwasm/swift/releases/download/swift-wasm-DEVELOPMENT-SNAPSHOT-2024-05-25-a/swift-wasm-DEVELOPMENT-SNAPSHOT-2024-05-25-a-wasm32-unknown-wasi.artifactbundle.zip" wasi-backend: Node - # TODO: Enable this once we support threads in JavaScriptKit - # - os: ubuntu-22.04 - # toolchain: DEVELOPMENT-SNAPSHOT-2024-05-01-a - # swift-sdk: - # id: DEVELOPMENT-SNAPSHOT-2024-05-25-a-wasm32-unknown-wasip1-threads - # download-url: "https://github.com/swiftwasm/swift/releases/download/swift-wasm-DEVELOPMENT-SNAPSHOT-2024-05-25-a/swift-wasm-DEVELOPMENT-SNAPSHOT-2024-05-25-a-wasm32-unknown-wasip1-threads.artifactbundle.zip" - # wasi-backend: Node + - os: ubuntu-22.04 + toolchain: DEVELOPMENT-SNAPSHOT-2024-05-01-a + swift-sdk: + id: DEVELOPMENT-SNAPSHOT-2024-05-25-a-wasm32-unknown-wasip1-threads + download-url: "https://github.com/swiftwasm/swift/releases/download/swift-wasm-DEVELOPMENT-SNAPSHOT-2024-05-25-a/swift-wasm-DEVELOPMENT-SNAPSHOT-2024-05-25-a-wasm32-unknown-wasip1-threads.artifactbundle.zip" + wasi-backend: Node runs-on: ${{ matrix.entry.os }} env: diff --git a/IntegrationTests/lib.js b/IntegrationTests/lib.js index 2ed9a918d..fe25cf679 100644 --- a/IntegrationTests/lib.js +++ b/IntegrationTests/lib.js @@ -2,11 +2,12 @@ import { SwiftRuntime } from "javascript-kit-swift" import { WASI as NodeWASI } from "wasi" import { WASI as MicroWASI, useAll } from "uwasi" import * as fs from "fs/promises" +import path from "path"; const WASI = { MicroWASI: ({ programName }) => { const wasi = new MicroWASI({ - args: [programName], + args: [path.basename(programName)], env: {}, features: [useAll()], }) @@ -21,7 +22,7 @@ const WASI = { }, Node: ({ programName }) => { const wasi = new NodeWASI({ - args: [programName], + args: [path.basename(programName)], env: {}, preopens: { "/": "./", @@ -51,21 +52,49 @@ const selectWASIBackend = () => { return WASI.Node; }; +function isUsingSharedMemory(module) { + const imports = WebAssembly.Module.imports(module); + for (const entry of imports) { + if (entry.module === "env" && entry.name === "memory" && entry.kind == "memory") { + return true; + } + } + return false; +} + export const startWasiTask = async (wasmPath, wasiConstructor = selectWASIBackend()) => { const swift = new SwiftRuntime(); // Fetch our Wasm File const wasmBinary = await fs.readFile(wasmPath); const wasi = wasiConstructor({ programName: wasmPath }); - // Instantiate the WebAssembly file - let { instance } = await WebAssembly.instantiate(wasmBinary, { + const module = await WebAssembly.compile(wasmBinary); + + const importObject = { wasi_snapshot_preview1: wasi.wasiImport, javascript_kit: swift.importObjects(), benchmark_helper: { noop: () => {}, noop_with_int: (_) => {}, } - }); + }; + + if (isUsingSharedMemory(module)) { + importObject["env"] = { + // We don't have JS API to get memory descriptor of imported memory + // at this moment, so we assume 256 pages (16MB) memory is enough + // large for initial memory size. + memory: new WebAssembly.Memory({ initial: 256, maximum: 16384, shared: true }), + }; + importObject["wasi"] = { + "thread-spawn": () => { + throw new Error("thread-spawn not implemented"); + } + } + } + + // Instantiate the WebAssembly file + const instance = await WebAssembly.instantiate(module, importObject); swift.setInstance(instance); // Start the WebAssembly WASI instance! diff --git a/Makefile b/Makefile index 4714d9151..7108f3189 100644 --- a/Makefile +++ b/Makefile @@ -28,7 +28,7 @@ test: unittest: @echo Running unit tests swift build --build-tests -Xswiftc -Xclang-linker -Xswiftc -mexec-model=reactor -Xlinker --export-if-defined=main -Xlinker --export-if-defined=__main_argc_argv --static-swift-stdlib -Xswiftc -static-stdlib $(SWIFT_BUILD_FLAGS) - node --experimental-wasi-unstable-preview1 scripts/test-harness.mjs ./.build/wasm32-unknown-wasi/debug/JavaScriptKitPackageTests.wasm + node --experimental-wasi-unstable-preview1 scripts/test-harness.mjs ./.build/debug/JavaScriptKitPackageTests.wasm .PHONY: benchmark_setup benchmark_setup: diff --git a/Runtime/src/index.ts b/Runtime/src/index.ts index ac0adde2a..605ce2d06 100644 --- a/Runtime/src/index.ts +++ b/Runtime/src/index.ts @@ -10,19 +10,25 @@ import { import * as JSValue from "./js-value.js"; import { Memory } from "./memory.js"; +type SwiftRuntimeOptions = { + sharedMemory?: boolean; +}; + export class SwiftRuntime { private _instance: WebAssembly.Instance | null; private _memory: Memory | null; private _closureDeallocator: SwiftClosureDeallocator | null; + private options: SwiftRuntimeOptions; private version: number = 708; private textDecoder = new TextDecoder("utf-8"); private textEncoder = new TextEncoder(); // Only support utf-8 - constructor() { + constructor(options?: SwiftRuntimeOptions) { this._instance = null; this._memory = null; this._closureDeallocator = null; + this.options = options || {}; } setInstance(instance: WebAssembly.Instance) { @@ -134,316 +140,330 @@ 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 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 - ); - }, + get wasmImports(): ImportedFunctions { + 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 + ); + }, - 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, - false, - this.memory - ); - }, + 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, + false, + 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: (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_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 { + 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 + ); + }, + 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); - result = func(...args); - } catch (error) { + const result = func(...args); return JSValue.writeAndReturnKindBits( - error, + result, payload1_ptr, payload2_ptr, - true, + false, this.memory ); - } - return JSValue.writeAndReturnKindBits( - result, - payload1_ptr, - payload2_ptr, - false, - this.memory - ); - }, - 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, - false, - 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 { + 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); - } catch (error) { return JSValue.writeAndReturnKindBits( - error, + result, payload1_ptr, payload2_ptr, - true, + false, 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 - ); - }, + }, - 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 { + 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); - result = new constructor(...args); - } catch (error) { + 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; - } - memory = this.memory; - JSValue.write( - null, - exception_kind_ptr, - exception_payload1_ptr, - exception_payload2_ptr, - false, - memory - ); - return memory.retain(result); - }, + 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_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_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_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); - const array = new ArrayType( - this.memory.rawMemory.buffer, - elementsPtr, - length - ); - // Call `.slice()` to copy the memory - return this.memory.retain(array.slice()); - }, + 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 + ); + // Call `.slice()` to copy the memory + return this.memory.retain(array.slice()); + }, - 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_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: (ref: ref) => { + this.memory.release(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); + 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}`); } - 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(); - }, - }; + 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(); + }, + }; + } } /// This error is thrown when yielding event loop control from `swift_task_asyncMainDrainQueue` diff --git a/Sources/JavaScriptKit/Runtime/index.js b/Sources/JavaScriptKit/Runtime/index.js index 9cd1995ac..2aaabce65 100644 --- a/Sources/JavaScriptKit/Runtime/index.js +++ b/Sources/JavaScriptKit/Runtime/index.js @@ -196,13 +196,97 @@ } class SwiftRuntime { - constructor() { + constructor(options) { this.version = 708; this.textDecoder = new TextDecoder("utf-8"); this.textEncoder = new TextEncoder(); // Only support utf-8 /** @deprecated Use `wasmImports` instead */ this.importObjects = () => this.wasmImports; - this.wasmImports = { + this._instance = null; + this._memory = null; + this._closureDeallocator = 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; + } + } + get instance() { + if (!this._instance) + throw new Error("WebAssembly instance is not set yet"); + return this._instance; + } + get exports() { + return this.instance.exports; + } + get memory() { + if (!this._memory) { + this._memory = new Memory(this.instance.exports); + } + return this._memory; + } + get closureDeallocator() { + if (this._closureDeallocator) + return this._closureDeallocator; + const features = this.exports.swjs_library_features(); + const librarySupportsWeakRef = (features & 1 /* WeakRefs */) != 0; + if (librarySupportsWeakRef) { + this._closureDeallocator = new SwiftClosureDeallocator(this.exports); + } + return this._closureDeallocator; + } + callHostFunction(host_func_id, line, file, args) { + const argc = args.length; + const argv = this.exports.swjs_prepare_host_function_call(argc); + const memory = this.memory; + for (let index = 0; index < args.length; index++) { + const argument = args[index]; + const base = argv + 16 * index; + write(argument, base, base + 4, base + 8, false, memory); + } + let output; + // This ref is released by the swjs_call_host_function implementation + const callback_func_ref = memory.retain((result) => { + output = result; + }); + const alreadyReleased = this.exports.swjs_call_host_function(host_func_id, argv, argc, callback_func_ref); + if (alreadyReleased) { + throw new Error(`The JSClosure has been already released by Swift side. The closure is created at ${file}:${line}`); + } + this.exports.swjs_cleanup_host_function_call(argv); + return output; + } + get wasmImports() { + return { swjs_set_prop: (ref, name, kind, payload1, payload2) => { const memory = this.memory; const obj = memory.getObject(ref); @@ -235,14 +319,25 @@ memory.writeUint32(bytes_ptr_result, bytes_ptr); return bytes.length; }, - swjs_decode_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_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); @@ -370,87 +465,6 @@ throw new UnsafeEventLoopYield(); }, }; - this._instance = null; - this._memory = null; - this._closureDeallocator = null; - } - 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; - } - } - get instance() { - if (!this._instance) - throw new Error("WebAssembly instance is not set yet"); - return this._instance; - } - get exports() { - return this.instance.exports; - } - get memory() { - if (!this._memory) { - this._memory = new Memory(this.instance.exports); - } - return this._memory; - } - get closureDeallocator() { - if (this._closureDeallocator) - return this._closureDeallocator; - const features = this.exports.swjs_library_features(); - const librarySupportsWeakRef = (features & 1 /* WeakRefs */) != 0; - if (librarySupportsWeakRef) { - this._closureDeallocator = new SwiftClosureDeallocator(this.exports); - } - return this._closureDeallocator; - } - callHostFunction(host_func_id, line, file, args) { - const argc = args.length; - const argv = this.exports.swjs_prepare_host_function_call(argc); - const memory = this.memory; - for (let index = 0; index < args.length; index++) { - const argument = args[index]; - const base = argv + 16 * index; - write(argument, base, base + 4, base + 8, false, memory); - } - let output; - // This ref is released by the swjs_call_host_function implementation - const callback_func_ref = memory.retain((result) => { - output = result; - }); - const alreadyReleased = this.exports.swjs_call_host_function(host_func_id, argv, argc, callback_func_ref); - if (alreadyReleased) { - throw new Error(`The JSClosure has been already released by Swift side. The closure is created at ${file}:${line}`); - } - this.exports.swjs_cleanup_host_function_call(argv); - return output; } } /// This error is thrown when yielding event loop control from `swift_task_asyncMainDrainQueue` diff --git a/Sources/JavaScriptKit/Runtime/index.mjs b/Sources/JavaScriptKit/Runtime/index.mjs index 78c99457c..52de118b5 100644 --- a/Sources/JavaScriptKit/Runtime/index.mjs +++ b/Sources/JavaScriptKit/Runtime/index.mjs @@ -190,13 +190,97 @@ class Memory { } class SwiftRuntime { - constructor() { + constructor(options) { this.version = 708; this.textDecoder = new TextDecoder("utf-8"); this.textEncoder = new TextEncoder(); // Only support utf-8 /** @deprecated Use `wasmImports` instead */ this.importObjects = () => this.wasmImports; - this.wasmImports = { + this._instance = null; + this._memory = null; + this._closureDeallocator = 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; + } + } + get instance() { + if (!this._instance) + throw new Error("WebAssembly instance is not set yet"); + return this._instance; + } + get exports() { + return this.instance.exports; + } + get memory() { + if (!this._memory) { + this._memory = new Memory(this.instance.exports); + } + return this._memory; + } + get closureDeallocator() { + if (this._closureDeallocator) + return this._closureDeallocator; + const features = this.exports.swjs_library_features(); + const librarySupportsWeakRef = (features & 1 /* WeakRefs */) != 0; + if (librarySupportsWeakRef) { + this._closureDeallocator = new SwiftClosureDeallocator(this.exports); + } + return this._closureDeallocator; + } + callHostFunction(host_func_id, line, file, args) { + const argc = args.length; + const argv = this.exports.swjs_prepare_host_function_call(argc); + const memory = this.memory; + for (let index = 0; index < args.length; index++) { + const argument = args[index]; + const base = argv + 16 * index; + write(argument, base, base + 4, base + 8, false, memory); + } + let output; + // This ref is released by the swjs_call_host_function implementation + const callback_func_ref = memory.retain((result) => { + output = result; + }); + const alreadyReleased = this.exports.swjs_call_host_function(host_func_id, argv, argc, callback_func_ref); + if (alreadyReleased) { + throw new Error(`The JSClosure has been already released by Swift side. The closure is created at ${file}:${line}`); + } + this.exports.swjs_cleanup_host_function_call(argv); + return output; + } + get wasmImports() { + return { swjs_set_prop: (ref, name, kind, payload1, payload2) => { const memory = this.memory; const obj = memory.getObject(ref); @@ -229,14 +313,25 @@ class SwiftRuntime { memory.writeUint32(bytes_ptr_result, bytes_ptr); return bytes.length; }, - swjs_decode_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_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); @@ -364,87 +459,6 @@ class SwiftRuntime { throw new UnsafeEventLoopYield(); }, }; - this._instance = null; - this._memory = null; - this._closureDeallocator = null; - } - 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; - } - } - get instance() { - if (!this._instance) - throw new Error("WebAssembly instance is not set yet"); - return this._instance; - } - get exports() { - return this.instance.exports; - } - get memory() { - if (!this._memory) { - this._memory = new Memory(this.instance.exports); - } - return this._memory; - } - get closureDeallocator() { - if (this._closureDeallocator) - return this._closureDeallocator; - const features = this.exports.swjs_library_features(); - const librarySupportsWeakRef = (features & 1 /* WeakRefs */) != 0; - if (librarySupportsWeakRef) { - this._closureDeallocator = new SwiftClosureDeallocator(this.exports); - } - return this._closureDeallocator; - } - callHostFunction(host_func_id, line, file, args) { - const argc = args.length; - const argv = this.exports.swjs_prepare_host_function_call(argc); - const memory = this.memory; - for (let index = 0; index < args.length; index++) { - const argument = args[index]; - const base = argv + 16 * index; - write(argument, base, base + 4, base + 8, false, memory); - } - let output; - // This ref is released by the swjs_call_host_function implementation - const callback_func_ref = memory.retain((result) => { - output = result; - }); - const alreadyReleased = this.exports.swjs_call_host_function(host_func_id, argv, argc, callback_func_ref); - if (alreadyReleased) { - throw new Error(`The JSClosure has been already released by Swift side. The closure is created at ${file}:${line}`); - } - this.exports.swjs_cleanup_host_function_call(argv); - return output; } } /// This error is thrown when yielding event loop control from `swift_task_asyncMainDrainQueue` From d88c3d46cdf113324cd72e3ac122c09622b14bcc Mon Sep 17 00:00:00 2001 From: Yuta Saito Date: Thu, 6 Jun 2024 01:50:07 +0900 Subject: [PATCH 105/373] Check 5.10 toolchain in CI (#249) * Check 5.10 RC toolchain * Use wasm-5.10.0-RELEASE --- .github/workflows/test.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 26227b530..aad5a3555 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -14,6 +14,7 @@ jobs: - { os: macos-13, toolchain: wasm-5.9.1-RELEASE, wasi-backend: Node, xcode: Xcode_14.3.app } - { os: macos-14, toolchain: wasm-5.9.1-RELEASE, wasi-backend: Node, xcode: Xcode_15.2.app } - { os: ubuntu-22.04, toolchain: wasm-5.9.1-RELEASE, wasi-backend: Node } + - { os: ubuntu-22.04, toolchain: wasm-5.10.0-RELEASE, wasi-backend: Node } # Ensure that test succeeds with all toolchains and wasi backend combinations - { os: ubuntu-20.04, toolchain: wasm-5.7.3-RELEASE, wasi-backend: Node } From 3a8137140fd6392f1742c71dc8d326fd65c1d200 Mon Sep 17 00:00:00 2001 From: Yuta Saito Date: Wed, 5 Jun 2024 16:53:58 +0000 Subject: [PATCH 106/373] Bump version to 0.19.3, update `CHANGELOG.md` --- CHANGELOG.md | 16 ++++++++++++++++ package-lock.json | 4 ++-- package.json | 2 +- 3 files changed, 19 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d860de07b..3427a540d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,19 @@ +# 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 diff --git a/package-lock.json b/package-lock.json index c6d12ca2a..962bc41ee 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "javascript-kit-swift", - "version": "0.19.2", + "version": "0.19.3", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "javascript-kit-swift", - "version": "0.19.2", + "version": "0.19.3", "license": "MIT", "devDependencies": { "@rollup/plugin-typescript": "^8.3.1", diff --git a/package.json b/package.json index 0cfd8ff5b..7d32453b1 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "javascript-kit-swift", - "version": "0.19.2", + "version": "0.19.3", "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", From 2bb06942d9e5118191f4aa38e81b8c80a66fbb52 Mon Sep 17 00:00:00 2001 From: Yuta Saito Date: Tue, 11 Jun 2024 20:14:01 +0900 Subject: [PATCH 107/373] Start migrating imported functions to the new definition style (#252) * Start migrating imported functions to the new definition style Use of different function names for C-name and Wasm's import name is a bit confusing. Also use of unprefixed function names in the C code is not a good practice. This commit starts migrating imported functions to the new naming convention and removes duplicated function definitions just for IDE support by using macro. * Migrate rest of imported functions * Migrate JavaScriptBigIntSupport module --- .../JSBigInt+I64.swift | 8 +- .../XcodeSupport.swift | 11 - .../JavaScriptEventLoop.swift | 7 +- .../BasicObjects/JSTypedArray.swift | 6 +- .../FundamentalObjects/JSBigInt.swift | 4 +- .../FundamentalObjects/JSClosure.swift | 4 +- .../FundamentalObjects/JSFunction.swift | 6 +- .../FundamentalObjects/JSObject.swift | 4 +- .../FundamentalObjects/JSString.swift | 10 +- .../JSThrowingFunction.swift | 6 +- Sources/JavaScriptKit/JSValue.swift | 12 +- Sources/JavaScriptKit/XcodeSupport.swift | 102 -------- .../include/_CJavaScriptKit+I64.h | 22 +- .../_CJavaScriptKit/include/_CJavaScriptKit.h | 219 ++++++++---------- 14 files changed, 136 insertions(+), 285 deletions(-) delete mode 100644 Sources/JavaScriptBigIntSupport/XcodeSupport.swift delete mode 100644 Sources/JavaScriptKit/XcodeSupport.swift diff --git a/Sources/JavaScriptBigIntSupport/JSBigInt+I64.swift b/Sources/JavaScriptBigIntSupport/JSBigInt+I64.swift index 4c8b9bca7..ef868bf1b 100644 --- a/Sources/JavaScriptBigIntSupport/JSBigInt+I64.swift +++ b/Sources/JavaScriptBigIntSupport/JSBigInt+I64.swift @@ -3,18 +3,18 @@ import _CJavaScriptBigIntSupport extension JSBigInt: JSBigIntExtended { public var int64Value: Int64 { - _bigint_to_i64(id, true) + swjs_bigint_to_i64(id, true) } public var uInt64Value: UInt64 { - UInt64(bitPattern: _bigint_to_i64(id, false)) + UInt64(bitPattern: swjs_bigint_to_i64(id, false)) } public convenience init(_ value: Int64) { - self.init(id: _i64_to_bigint(value, true)) + self.init(id: swjs_i64_to_bigint(value, true)) } public convenience init(unsigned value: UInt64) { - self.init(id: _i64_to_bigint(Int64(bitPattern: value), false)) + self.init(id: swjs_i64_to_bigint(Int64(bitPattern: value), false)) } } diff --git a/Sources/JavaScriptBigIntSupport/XcodeSupport.swift b/Sources/JavaScriptBigIntSupport/XcodeSupport.swift deleted file mode 100644 index 54912cec2..000000000 --- a/Sources/JavaScriptBigIntSupport/XcodeSupport.swift +++ /dev/null @@ -1,11 +0,0 @@ -import _CJavaScriptKit - -/// Note: -/// Define all runtime function stubs 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 _i64_to_bigint(_: Int64, _: Bool) -> JavaScriptObjectRef { fatalError() } - func _bigint_to_i64(_: JavaScriptObjectRef, _: Bool) -> Int64 { fatalError() } -#endif diff --git a/Sources/JavaScriptEventLoop/JavaScriptEventLoop.swift b/Sources/JavaScriptEventLoop/JavaScriptEventLoop.swift index 04aedb940..8f09279af 100644 --- a/Sources/JavaScriptEventLoop/JavaScriptEventLoop.swift +++ b/Sources/JavaScriptEventLoop/JavaScriptEventLoop.swift @@ -94,7 +94,7 @@ public final class JavaScriptEventLoop: SerialExecutor, @unchecked Sendable { #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 - _unsafe_event_loop_yield() + swjs_unsafe_event_loop_yield() } swift_task_asyncMainDrainQueue_hook = unsafeBitCast(swift_task_asyncMainDrainQueue_hook_impl, to: UnsafeMutableRawPointer?.self) #endif @@ -225,8 +225,3 @@ public extension JSPromise { } #endif - -// See `Sources/JavaScriptKit/XcodeSupport.swift` for rationale of the stub functions. -#if !arch(wasm32) - func _unsafe_event_loop_yield() { fatalError() } -#endif diff --git a/Sources/JavaScriptKit/BasicObjects/JSTypedArray.swift b/Sources/JavaScriptKit/BasicObjects/JSTypedArray.swift index 963419c99..6566e54f3 100644 --- a/Sources/JavaScriptKit/BasicObjects/JSTypedArray.swift +++ b/Sources/JavaScriptKit/BasicObjects/JSTypedArray.swift @@ -47,7 +47,7 @@ public class JSTypedArray: JSBridgedClass, ExpressibleByArrayLiteral wh /// - Parameter array: The array that will be copied to create a new instance of TypedArray public convenience init(_ array: [Element]) { let jsArrayRef = array.withUnsafeBufferPointer { ptr in - _create_typed_array(Self.constructor!.id, ptr.baseAddress!, Int32(array.count)) + swjs_create_typed_array(Self.constructor!.id, ptr.baseAddress!, Int32(array.count)) } self.init(unsafelyWrapping: JSObject(id: jsArrayRef)) } @@ -82,7 +82,7 @@ public class JSTypedArray: JSBridgedClass, ExpressibleByArrayLiteral wh let rawBuffer = UnsafeMutableBufferPointer.allocate(capacity: bytesLength) defer { rawBuffer.deallocate() } let baseAddress = rawBuffer.baseAddress! - _load_typed_array(jsObject.id, baseAddress) + swjs_load_typed_array(jsObject.id, baseAddress) let length = bytesLength / MemoryLayout.size let rawBaseAddress = UnsafeRawPointer(baseAddress) let bufferPtr = UnsafeBufferPointer( @@ -113,7 +113,7 @@ public class JSTypedArray: JSBridgedClass, ExpressibleByArrayLiteral wh let rawBuffer = UnsafeMutableBufferPointer.allocate(capacity: bytesLength) defer { rawBuffer.deallocate() } let baseAddress = rawBuffer.baseAddress! - _load_typed_array(jsObject.id, baseAddress) + swjs_load_typed_array(jsObject.id, baseAddress) let length = bytesLength / MemoryLayout.size let rawBaseAddress = UnsafeRawPointer(baseAddress) let bufferPtr = UnsafeBufferPointer( diff --git a/Sources/JavaScriptKit/FundamentalObjects/JSBigInt.swift b/Sources/JavaScriptKit/FundamentalObjects/JSBigInt.swift index 104d194e3..f3687246e 100644 --- a/Sources/JavaScriptKit/FundamentalObjects/JSBigInt.swift +++ b/Sources/JavaScriptKit/FundamentalObjects/JSBigInt.swift @@ -15,13 +15,13 @@ public final class JSBigInt: JSObject { /// 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: _i64_to_bigint_slow(UInt32(value & 0xffffffff), UInt32(value >> 32), true)) + super.init(id: swjs_i64_to_bigint_slow(UInt32(value & 0xffffffff), 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: _i64_to_bigint_slow(UInt32(value & 0xffffffff), UInt32(value >> 32), false)) + super.init(id: swjs_i64_to_bigint_slow(UInt32(value & 0xffffffff), UInt32(value >> 32), false)) } override public var jsValue: JSValue { diff --git a/Sources/JavaScriptKit/FundamentalObjects/JSClosure.swift b/Sources/JavaScriptKit/FundamentalObjects/JSClosure.swift index 441dd2a6c..6decbc814 100644 --- a/Sources/JavaScriptKit/FundamentalObjects/JSClosure.swift +++ b/Sources/JavaScriptKit/FundamentalObjects/JSClosure.swift @@ -22,7 +22,7 @@ public class JSOneshotClosure: JSObject, JSClosureProtocol { // 2. Create a new JavaScript function which calls the given Swift function. hostFuncRef = JavaScriptHostFuncRef(bitPattern: ObjectIdentifier(self)) id = withExtendedLifetime(JSString(file)) { file in - _create_function(hostFuncRef, line, file.asInternalJSRef()) + swjs_create_function(hostFuncRef, line, file.asInternalJSRef()) } // 3. Retain the given body in static storage by `funcRef`. @@ -88,7 +88,7 @@ public class JSClosure: JSFunction, JSClosureProtocol { // 2. Create a new JavaScript function which calls the given Swift function. hostFuncRef = JavaScriptHostFuncRef(bitPattern: ObjectIdentifier(self)) id = withExtendedLifetime(JSString(file)) { file in - _create_function(hostFuncRef, line, file.asInternalJSRef()) + swjs_create_function(hostFuncRef, line, file.asInternalJSRef()) } // 3. Retain the given body in static storage by `funcRef`. diff --git a/Sources/JavaScriptKit/FundamentalObjects/JSFunction.swift b/Sources/JavaScriptKit/FundamentalObjects/JSFunction.swift index 1de95fd36..543146133 100644 --- a/Sources/JavaScriptKit/FundamentalObjects/JSFunction.swift +++ b/Sources/JavaScriptKit/FundamentalObjects/JSFunction.swift @@ -55,7 +55,7 @@ public class JSFunction: JSObject { public func new(arguments: [ConvertibleToJSValue]) -> JSObject { arguments.withRawJSValues { rawValues in rawValues.withUnsafeBufferPointer { bufferPointer in - JSObject(id: _call_new(self.id, bufferPointer.baseAddress!, Int32(bufferPointer.count))) + JSObject(id: swjs_call_new(self.id, bufferPointer.baseAddress!, Int32(bufferPointer.count))) } } } @@ -101,7 +101,7 @@ public class JSFunction: JSObject { let argc = bufferPointer.count var payload1 = JavaScriptPayload1() var payload2 = JavaScriptPayload2() - let resultBitPattern = _call_function_no_catch( + let resultBitPattern = swjs_call_function_no_catch( id, argv, Int32(argc), &payload1, &payload2 ) @@ -121,7 +121,7 @@ public class JSFunction: JSObject { let argc = bufferPointer.count var payload1 = JavaScriptPayload1() var payload2 = JavaScriptPayload2() - let resultBitPattern = _call_function_with_this_no_catch(this.id, + let resultBitPattern = swjs_call_function_with_this_no_catch(this.id, id, argv, Int32(argc), &payload1, &payload2 ) diff --git a/Sources/JavaScriptKit/FundamentalObjects/JSObject.swift b/Sources/JavaScriptKit/FundamentalObjects/JSObject.swift index 04e7f3d59..1883cbf32 100644 --- a/Sources/JavaScriptKit/FundamentalObjects/JSObject.swift +++ b/Sources/JavaScriptKit/FundamentalObjects/JSObject.swift @@ -130,7 +130,7 @@ public class JSObject: Equatable { /// - 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) + swjs_instanceof(id, constructor.id) } static let _JS_Predef_Value_Global: JavaScriptObjectRef = 0 @@ -139,7 +139,7 @@ public class JSObject: Equatable { /// 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) - deinit { _release(id) } + deinit { swjs_release(id) } /// Returns a Boolean value indicating whether two values point to same objects. /// diff --git a/Sources/JavaScriptKit/FundamentalObjects/JSString.swift b/Sources/JavaScriptKit/FundamentalObjects/JSString.swift index 5621793d0..ee902f3ee 100644 --- a/Sources/JavaScriptKit/FundamentalObjects/JSString.swift +++ b/Sources/JavaScriptKit/FundamentalObjects/JSString.swift @@ -23,20 +23,20 @@ public struct JSString: LosslessStringConvertible, Equatable { lazy var jsRef: JavaScriptObjectRef = { self.shouldDealocateRef = 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) defer { free(buffer) - _release(bytesRef) + swjs_release(bytesRef) } - _load_string(bytesRef, buffer) + swjs_load_string(bytesRef, buffer) buffer[bytesLength] = 0 return String(decodingCString: UnsafePointer(buffer), as: UTF8.self) }() @@ -52,7 +52,7 @@ public struct JSString: LosslessStringConvertible, Equatable { deinit { guard shouldDealocateRef else { return } - _release(jsRef) + swjs_release(jsRef) } } diff --git a/Sources/JavaScriptKit/FundamentalObjects/JSThrowingFunction.swift b/Sources/JavaScriptKit/FundamentalObjects/JSThrowingFunction.swift index 705899000..4763a8779 100644 --- a/Sources/JavaScriptKit/FundamentalObjects/JSThrowingFunction.swift +++ b/Sources/JavaScriptKit/FundamentalObjects/JSThrowingFunction.swift @@ -44,7 +44,7 @@ public class JSThrowingFunction { var exceptionKind = JavaScriptValueKindAndFlags() var exceptionPayload1 = JavaScriptPayload1() var exceptionPayload2 = JavaScriptPayload2() - let resultObj = _call_throwing_new( + let resultObj = swjs_call_throwing_new( self.base.id, argv, Int32(argc), &exceptionKind, &exceptionPayload1, &exceptionPayload2 ) @@ -73,13 +73,13 @@ private func invokeJSFunction(_ jsFunc: JSFunction, arguments: [ConvertibleToJSV var payload1 = JavaScriptPayload1() var payload2 = JavaScriptPayload2() if let thisId = this?.id { - let resultBitPattern = _call_function_with_this( + let resultBitPattern = swjs_call_function_with_this( thisId, id, argv, Int32(argc), &payload1, &payload2 ) kindAndFlags = unsafeBitCast(resultBitPattern, to: JavaScriptValueKindAndFlags.self) } else { - let resultBitPattern = _call_function( + let resultBitPattern = swjs_call_function( id, argv, Int32(argc), &payload1, &payload2 ) diff --git a/Sources/JavaScriptKit/JSValue.swift b/Sources/JavaScriptKit/JSValue.swift index 58b28e079..852276149 100644 --- a/Sources/JavaScriptKit/JSValue.swift +++ b/Sources/JavaScriptKit/JSValue.swift @@ -196,7 +196,7 @@ extension JSValue: ExpressibleByNilLiteral { public func getJSValue(this: JSObject, name: JSString) -> JSValue { var rawValue = RawJSValue() - let rawBitPattern = _get_prop( + let rawBitPattern = swjs_get_prop( this.id, name.asInternalJSRef(), &rawValue.payload1, &rawValue.payload2 ) @@ -206,13 +206,13 @@ public func getJSValue(this: JSObject, name: JSString) -> 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() - let rawBitPattern = _get_subscript( + let rawBitPattern = swjs_get_subscript( this.id, index, &rawValue.payload1, &rawValue.payload2 ) @@ -222,7 +222,7 @@ public func getJSValue(this: JSObject, index: Int32) -> JSValue { public func setJSValue(this: JSObject, index: Int32, value: JSValue) { value.withRawJSValue { rawValue in - _set_subscript(this.id, index, + swjs_set_subscript(this.id, index, rawValue.kind, rawValue.payload1, rawValue.payload2) } @@ -230,7 +230,7 @@ public func setJSValue(this: JSObject, index: Int32, value: JSValue) { public func getJSValue(this: JSObject, symbol: JSSymbol) -> JSValue { var rawValue = RawJSValue() - let rawBitPattern = _get_prop( + let rawBitPattern = swjs_get_prop( this.id, symbol.id, &rawValue.payload1, &rawValue.payload2 ) @@ -240,7 +240,7 @@ public func getJSValue(this: JSObject, symbol: JSSymbol) -> JSValue { public func setJSValue(this: JSObject, symbol: JSSymbol, value: JSValue) { value.withRawJSValue { rawValue in - _set_prop(this.id, symbol.id, rawValue.kind, rawValue.payload1, rawValue.payload2) + swjs_set_prop(this.id, symbol.id, rawValue.kind, rawValue.payload1, rawValue.payload2) } } diff --git a/Sources/JavaScriptKit/XcodeSupport.swift b/Sources/JavaScriptKit/XcodeSupport.swift deleted file mode 100644 index ac5f117b4..000000000 --- a/Sources/JavaScriptKit/XcodeSupport.swift +++ /dev/null @@ -1,102 +0,0 @@ -import _CJavaScriptKit - -/// Note: -/// Define stubs for runtime functions which are usually imported from JavaScript environment. -/// JavaScriptKit itself supports only WebAssembly target, but it should be able -/// to be built for host platforms like macOS or Linux for tentative IDE support. -/// (ideally, IDE should build for WebAssembly target though) -#if !arch(wasm32) - func _set_prop( - _: JavaScriptObjectRef, - _: JavaScriptObjectRef, - _: JavaScriptValueKind, - _: JavaScriptPayload1, - _: JavaScriptPayload2 - ) { fatalError() } - func _get_prop( - _: JavaScriptObjectRef, - _: JavaScriptObjectRef, - _: UnsafeMutablePointer!, - _: UnsafeMutablePointer! - ) -> UInt32 { fatalError() } - func _set_subscript( - _: JavaScriptObjectRef, - _: Int32, - _: JavaScriptValueKind, - _: JavaScriptPayload1, - _: JavaScriptPayload2 - ) { fatalError() } - func _get_subscript( - _: JavaScriptObjectRef, - _: Int32, - _: UnsafeMutablePointer!, - _: UnsafeMutablePointer! - ) -> UInt32 { fatalError() } - func _encode_string( - _: JavaScriptObjectRef, - _: UnsafeMutablePointer! - ) -> Int32 { fatalError() } - func _decode_string( - _: UnsafePointer!, - _: Int32 - ) -> JavaScriptObjectRef { fatalError() } - func _load_string( - _: JavaScriptObjectRef, - _: UnsafeMutablePointer! - ) { fatalError() } - func _i64_to_bigint_slow( - _: UInt32, _: UInt32, _: Bool - ) -> JavaScriptObjectRef { fatalError() } - func _call_function( - _: JavaScriptObjectRef, - _: UnsafePointer!, _: Int32, - _: UnsafeMutablePointer!, - _: UnsafeMutablePointer! - ) -> UInt32 { fatalError() } - func _call_function_no_catch( - _: JavaScriptObjectRef, - _: UnsafePointer!, _: Int32, - _: UnsafeMutablePointer!, - _: UnsafeMutablePointer! - ) -> UInt32 { fatalError() } - func _call_function_with_this( - _: JavaScriptObjectRef, - _: JavaScriptObjectRef, - _: UnsafePointer!, _: Int32, - _: UnsafeMutablePointer!, - _: UnsafeMutablePointer! - ) -> UInt32 { fatalError() } - func _call_function_with_this_no_catch( - _: JavaScriptObjectRef, - _: JavaScriptObjectRef, - _: UnsafePointer!, _: Int32, - _: UnsafeMutablePointer!, - _: UnsafeMutablePointer! - ) -> UInt32 { 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, _: UInt32, _: JavaScriptObjectRef) -> 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/include/_CJavaScriptKit+I64.h b/Sources/_CJavaScriptBigIntSupport/include/_CJavaScriptKit+I64.h index a04be1c2e..69d25e47b 100644 --- a/Sources/_CJavaScriptBigIntSupport/include/_CJavaScriptKit+I64.h +++ b/Sources/_CJavaScriptBigIntSupport/include/_CJavaScriptKit+I64.h @@ -4,24 +4,26 @@ #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. -#if __wasi__ -__attribute__((__import_module__("javascript_kit"), - __import_name__("swjs_i64_to_bigint"))) -#endif -extern JavaScriptObjectRef _i64_to_bigint(const long long value, bool is_signed); +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. -#if __wasi__ -__attribute__((__import_module__("javascript_kit"), - __import_name__("swjs_bigint_to_i64"))) -#endif -extern long long _bigint_to_i64(const JavaScriptObjectRef ref, bool is_signed); +IMPORT_JS_FUNCTION(swjs_bigint_to_i64, long long, (const JavaScriptObjectRef ref, bool is_signed)) #endif /* _CJavaScriptBigIntSupport_h */ diff --git a/Sources/_CJavaScriptKit/include/_CJavaScriptKit.h b/Sources/_CJavaScriptKit/include/_CJavaScriptKit.h index a9d8738af..431b83615 100644 --- a/Sources/_CJavaScriptKit/include/_CJavaScriptKit.h +++ b/Sources/_CJavaScriptKit/include/_CJavaScriptKit.h @@ -73,95 +73,86 @@ 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); +IMPORT_JS_FUNCTION(swjs_set_prop, void, (const JavaScriptObjectRef _this, + const JavaScriptObjectRef prop, + const JavaScriptValueKind kind, + const JavaScriptPayload1 payload1, + const JavaScriptPayload2 payload2)) -/// `_get_prop` gets a value of `_this` JavaScript object. +/// 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 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. /// @return A `JavaScriptValueKind` bits represented as 32bit integer for the returned value. -__attribute__((__import_module__("javascript_kit"), - __import_name__("swjs_get_prop"))) -extern uint32_t _get_prop( - const JavaScriptObjectRef _this, - const JavaScriptObjectRef prop, - JavaScriptPayload1 *payload1, - JavaScriptPayload2 *payload2 -); +IMPORT_JS_FUNCTION(swjs_get_prop, uint32_t, (const JavaScriptObjectRef _this, + const JavaScriptObjectRef prop, + JavaScriptPayload1 *payload1, + JavaScriptPayload2 *payload2)) -/// `_set_subscript` sets a value of `_this` JavaScript object. +/// 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); +IMPORT_JS_FUNCTION(swjs_set_subscript, void, (const JavaScriptObjectRef _this, + const int index, + const JavaScriptValueKind kind, + const JavaScriptPayload1 payload1, + const JavaScriptPayload2 payload2)) -/// `_get_subscript` gets a value of `_this` JavaScript object. +/// 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 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. /// @return A `JavaScriptValueKind` bits represented as 32bit integer for the returned value. -__attribute__((__import_module__("javascript_kit"), - __import_name__("swjs_get_subscript"))) -extern uint32_t _get_subscript( - const JavaScriptObjectRef _this, - const int index, - JavaScriptPayload1 *payload1, - JavaScriptPayload2 *payload2 -); +/// get a value of `_this` JavaScript object. +IMPORT_JS_FUNCTION(swjs_get_subscript, uint32_t, (const JavaScriptObjectRef _this, + const int index, + JavaScriptPayload1 *payload1, + JavaScriptPayload2 *payload2)) -/// `_encode_string` encodes the `str_obj` to bytes sequence and returns the length of bytes. +/// 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 *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 *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 *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 @@ -169,11 +160,9 @@ extern void _load_string(const JavaScriptObjectRef bytes, unsigned char *buffer) /// @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. -__attribute__((__import_module__("javascript_kit"), - __import_name__("swjs_i64_to_bigint_slow"))) -extern JavaScriptObjectRef _i64_to_bigint_slow(unsigned int lower, unsigned int upper, bool is_signed); +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. @@ -181,16 +170,13 @@ extern JavaScriptObjectRef _i64_to_bigint_slow(unsigned int lower, unsigned int /// @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. -__attribute__((__import_module__("javascript_kit"), - __import_name__("swjs_call_function"))) -extern uint32_t _call_function( - const JavaScriptObjectRef ref, const RawJSValue *argv, - const int argc, - JavaScriptPayload1 *result_payload1, - JavaScriptPayload2 *result_payload2 -); +IMPORT_JS_FUNCTION(swjs_call_function, uint32_t, (const JavaScriptObjectRef ref, + const RawJSValue *argv, + const int argc, + JavaScriptPayload1 *result_payload1, + JavaScriptPayload2 *result_payload2)) -/// `_call_function` calls JavaScript function with given arguments list without capturing any exception +/// 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. @@ -198,16 +184,13 @@ extern uint32_t _call_function( /// @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. -__attribute__((__import_module__("javascript_kit"), - __import_name__("swjs_call_function_no_catch"))) -extern uint32_t _call_function_no_catch( - const JavaScriptObjectRef ref, const RawJSValue *argv, - const int argc, - JavaScriptPayload1 *result_payload1, - JavaScriptPayload2 *result_payload2 -); +IMPORT_JS_FUNCTION(swjs_call_function_no_catch, uint32_t, (const JavaScriptObjectRef ref, + const RawJSValue *argv, + const int argc, + JavaScriptPayload1 *result_payload1, + JavaScriptPayload2 *result_payload2)) -/// `_call_function_with_this` calls JavaScript function with given arguments list and given `_this`. +/// 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. @@ -216,17 +199,14 @@ extern uint32_t _call_function_no_catch( /// @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. -__attribute__((__import_module__("javascript_kit"), - __import_name__("swjs_call_function_with_this"))) -extern uint32_t _call_function_with_this( - const JavaScriptObjectRef _this, - const JavaScriptObjectRef func_ref, - const RawJSValue *argv, const int argc, - JavaScriptPayload1 *result_payload1, - JavaScriptPayload2 *result_payload2 -); +IMPORT_JS_FUNCTION(swjs_call_function_with_this, uint32_t, (const JavaScriptObjectRef _this, + const JavaScriptObjectRef func_ref, + const RawJSValue *argv, + const int argc, + JavaScriptPayload1 *result_payload1, + JavaScriptPayload2 *result_payload2)) -/// `_call_function_with_this` calls JavaScript function with given arguments list and given `_this` without capturing any exception. +/// 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. @@ -235,28 +215,24 @@ extern uint32_t _call_function_with_this( /// @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. -__attribute__((__import_module__("javascript_kit"), - __import_name__("swjs_call_function_with_this_no_catch"))) -extern uint32_t _call_function_with_this_no_catch( - const JavaScriptObjectRef _this, - const JavaScriptObjectRef func_ref, - const RawJSValue *argv, const int argc, - JavaScriptPayload1 *result_payload1, - JavaScriptPayload2 *result_payload2 -); +IMPORT_JS_FUNCTION(swjs_call_function_with_this_no_catch, uint32_t, (const JavaScriptObjectRef _this, + const JavaScriptObjectRef func_ref, + const RawJSValue *argv, + const int argc, + JavaScriptPayload1 *result_payload1, + JavaScriptPayload2 *result_payload2)) -/// `_call_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. /// @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 *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. @@ -265,67 +241,58 @@ 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); +IMPORT_JS_FUNCTION(swjs_call_throwing_new, JavaScriptObjectRef, (const JavaScriptObjectRef ref, + const RawJSValue *argv, + const int argc, + JavaScriptValueKindAndFlags *exception_kind, + JavaScriptPayload1 *exception_payload1, + JavaScriptPayload2 *exception_payload2)) -/// `_instanceof` acts like JavaScript `instanceof` operator. +/// 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. +/// 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, - unsigned int line, JavaScriptObjectRef file); +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 *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 *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); - -__attribute__((__import_module__("javascript_kit"), - __import_name__("swjs_unsafe_event_loop_yield"))) -extern void _unsafe_event_loop_yield(void); +IMPORT_JS_FUNCTION(swjs_release, void, (const JavaScriptObjectRef ref)) -#endif +/// 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)) #endif /* _CJavaScriptKit_h */ From 20f97eded34a2ca88515c053b5fbd9d40d9d1aaf Mon Sep 17 00:00:00 2001 From: Yuta Saito Date: Thu, 27 Jun 2024 15:16:31 +0900 Subject: [PATCH 108/373] Allocate `JavaScriptEventLoop` per thread in multi-threaded environment This change makes `JavaScriptEventLoop` to be allocated per thread in multi-threaded environment. This is necessary to ensure that a job enqueued in one thread is executed in the same thread because JSObject managed by a worker can only be accessed in the same thread. --- .../JavaScriptEventLoop.swift | 52 +++++++++++++++++-- .../_CJavaScriptEventLoop.c | 3 ++ .../include/_CJavaScriptEventLoop.h | 5 ++ 3 files changed, 56 insertions(+), 4 deletions(-) diff --git a/Sources/JavaScriptEventLoop/JavaScriptEventLoop.swift b/Sources/JavaScriptEventLoop/JavaScriptEventLoop.swift index 8f09279af..06522d2bc 100644 --- a/Sources/JavaScriptEventLoop/JavaScriptEventLoop.swift +++ b/Sources/JavaScriptEventLoop/JavaScriptEventLoop.swift @@ -57,7 +57,28 @@ public final class JavaScriptEventLoop: SerialExecutor, @unchecked Sendable { } /// A singleton instance of the Executor - public static let shared: JavaScriptEventLoop = { + public static var shared: JavaScriptEventLoop { + return _shared + } + + #if _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)) }) @@ -79,9 +100,13 @@ public final class JavaScriptEventLoop: SerialExecutor, @unchecked Sendable { } ) return eventLoop - }() + } private static var didInstallGlobalExecutor = false + fileprivate static var _mainThreadEventLoop: JavaScriptEventLoop! + fileprivate static var mainThreadEventLoop: JavaScriptEventLoop { + return _mainThreadEventLoop + } /// Set JavaScript event loop based executor to be the global executor /// Note that this should be called before any of the jobs are created. @@ -91,6 +116,10 @@ public final class JavaScriptEventLoop: SerialExecutor, @unchecked Sendable { public static func installGlobalExecutor() { guard !didInstallGlobalExecutor else { return } + // NOTE: We assume that this function is called before any of the jobs are created, so we can safely + // assume that we are in the main thread. + _mainThreadEventLoop = JavaScriptEventLoop.shared + #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 @@ -121,10 +150,10 @@ public final class JavaScriptEventLoop: SerialExecutor, @unchecked Sendable { typealias swift_task_enqueueMainExecutor_hook_Fn = @convention(thin) (UnownedJob, swift_task_enqueueMainExecutor_original) -> Void let swift_task_enqueueMainExecutor_hook_impl: swift_task_enqueueMainExecutor_hook_Fn = { job, original in - JavaScriptEventLoop.shared.unsafeEnqueue(job) + JavaScriptEventLoop.enqueueMainJob(job) } swift_task_enqueueMainExecutor_hook = unsafeBitCast(swift_task_enqueueMainExecutor_hook_impl, to: UnsafeMutableRawPointer?.self) - + didInstallGlobalExecutor = true } @@ -159,6 +188,21 @@ public final class JavaScriptEventLoop: SerialExecutor, @unchecked Sendable { public func asUnownedSerialExecutor() -> UnownedSerialExecutor { return UnownedSerialExecutor(ordinary: self) } + + public static func enqueueMainJob(_ job: consuming ExecutorJob) { + self.enqueueMainJob(UnownedJob(job)) + } + + static func enqueueMainJob(_ job: UnownedJob) { + let currentEventLoop = JavaScriptEventLoop.shared + if currentEventLoop === JavaScriptEventLoop.mainThreadEventLoop { + currentEventLoop.unsafeEnqueue(job) + } else { + // Notify the main thread to execute the job + let jobBitPattern = unsafeBitCast(job, to: UInt.self) + _ = JSObject.global.postMessage!(jobBitPattern) + } + } } #if compiler(>=5.7) diff --git a/Sources/_CJavaScriptEventLoop/_CJavaScriptEventLoop.c b/Sources/_CJavaScriptEventLoop/_CJavaScriptEventLoop.c index e69de29bb..009672933 100644 --- a/Sources/_CJavaScriptEventLoop/_CJavaScriptEventLoop.c +++ b/Sources/_CJavaScriptEventLoop/_CJavaScriptEventLoop.c @@ -0,0 +1,3 @@ +#include "_CJavaScriptEventLoop.h" + +_Thread_local void *swjs_thread_local_event_loop; diff --git a/Sources/_CJavaScriptEventLoop/include/_CJavaScriptEventLoop.h b/Sources/_CJavaScriptEventLoop/include/_CJavaScriptEventLoop.h index 2880772d6..890e26a01 100644 --- a/Sources/_CJavaScriptEventLoop/include/_CJavaScriptEventLoop.h +++ b/Sources/_CJavaScriptEventLoop/include/_CJavaScriptEventLoop.h @@ -61,4 +61,9 @@ typedef SWIFT_CC(swift) void (*swift_task_asyncMainDrainQueue_override)( SWIFT_EXPORT_FROM(swift_Concurrency) extern void *_Nullable swift_task_asyncMainDrainQueue_hook; + +/// MARK: - thread local storage + +extern _Thread_local void * _Nullable swjs_thread_local_event_loop; + #endif From 4ee27e6e2dd00c113bf18d762346a1bc61e85f1e Mon Sep 17 00:00:00 2001 From: Yuta Saito Date: Thu, 27 Jun 2024 15:18:20 +0900 Subject: [PATCH 109/373] Suppress concurrency warning about `JSObject.global` --- .../JavaScriptKit/FundamentalObjects/JSObject.swift | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/Sources/JavaScriptKit/FundamentalObjects/JSObject.swift b/Sources/JavaScriptKit/FundamentalObjects/JSObject.swift index 1883cbf32..861758497 100644 --- a/Sources/JavaScriptKit/FundamentalObjects/JSObject.swift +++ b/Sources/JavaScriptKit/FundamentalObjects/JSObject.swift @@ -137,7 +137,16 @@ public class JSObject: Equatable { /// 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 } + + // `JSObject` storage itself is immutable, and use of `JSObject.global` from other + // threads maintains the same semantics as `globalThis` in JavaScript. + #if compiler(>=5.10) + nonisolated(unsafe) + static let _global = JSObject(id: _JS_Predef_Value_Global) + #else + static let _global = JSObject(id: _JS_Predef_Value_Global) + #endif deinit { swjs_release(id) } From a0c4602607576c370edabc871c675131b59faedb Mon Sep 17 00:00:00 2001 From: Yuta Saito Date: Thu, 27 Jun 2024 15:26:12 +0900 Subject: [PATCH 110/373] `_runtime(multithreaded)` is only available in Swift 6.0 and later --- Sources/JavaScriptEventLoop/JavaScriptEventLoop.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/JavaScriptEventLoop/JavaScriptEventLoop.swift b/Sources/JavaScriptEventLoop/JavaScriptEventLoop.swift index 06522d2bc..67cd88042 100644 --- a/Sources/JavaScriptEventLoop/JavaScriptEventLoop.swift +++ b/Sources/JavaScriptEventLoop/JavaScriptEventLoop.swift @@ -61,7 +61,7 @@ public final class JavaScriptEventLoop: SerialExecutor, @unchecked Sendable { return _shared } - #if _runtime(_multithreaded) + #if compiler(>=6.0) && _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. From 888de173f108a831aeb10d52727094f8b2fe3ec8 Mon Sep 17 00:00:00 2001 From: Yuta Saito Date: Thu, 27 Jun 2024 15:36:34 +0900 Subject: [PATCH 111/373] Remove `enqueueMainJob` --- .../JavaScriptEventLoop.swift | 17 +---------------- 1 file changed, 1 insertion(+), 16 deletions(-) diff --git a/Sources/JavaScriptEventLoop/JavaScriptEventLoop.swift b/Sources/JavaScriptEventLoop/JavaScriptEventLoop.swift index 67cd88042..a47491982 100644 --- a/Sources/JavaScriptEventLoop/JavaScriptEventLoop.swift +++ b/Sources/JavaScriptEventLoop/JavaScriptEventLoop.swift @@ -150,7 +150,7 @@ public final class JavaScriptEventLoop: SerialExecutor, @unchecked Sendable { 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.enqueueMainJob(job) + JavaScriptEventLoop.shared.unsafeEnqueue(job) } swift_task_enqueueMainExecutor_hook = unsafeBitCast(swift_task_enqueueMainExecutor_hook_impl, to: UnsafeMutableRawPointer?.self) @@ -188,21 +188,6 @@ public final class JavaScriptEventLoop: SerialExecutor, @unchecked Sendable { public func asUnownedSerialExecutor() -> UnownedSerialExecutor { return UnownedSerialExecutor(ordinary: self) } - - public static func enqueueMainJob(_ job: consuming ExecutorJob) { - self.enqueueMainJob(UnownedJob(job)) - } - - static func enqueueMainJob(_ job: UnownedJob) { - let currentEventLoop = JavaScriptEventLoop.shared - if currentEventLoop === JavaScriptEventLoop.mainThreadEventLoop { - currentEventLoop.unsafeEnqueue(job) - } else { - // Notify the main thread to execute the job - let jobBitPattern = unsafeBitCast(job, to: UInt.self) - _ = JSObject.global.postMessage!(jobBitPattern) - } - } } #if compiler(>=5.7) From 9c9ae1dfb68364c1e823125908d7a532959f5d2b Mon Sep 17 00:00:00 2001 From: Yuta Saito Date: Thu, 27 Jun 2024 15:42:44 +0900 Subject: [PATCH 112/373] Drop Swift 5.7 support Swift 5.7 doesn't support short-circuit evaluation in `#if` conditions --- .github/workflows/test.yml | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index aad5a3555..2a4625d3c 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -17,11 +17,11 @@ jobs: - { os: ubuntu-22.04, toolchain: wasm-5.10.0-RELEASE, wasi-backend: Node } # Ensure that test succeeds with all toolchains and wasi backend combinations - - { os: ubuntu-20.04, toolchain: wasm-5.7.3-RELEASE, wasi-backend: Node } - { os: ubuntu-20.04, toolchain: wasm-5.8.0-RELEASE, wasi-backend: Node } - - { os: ubuntu-20.04, toolchain: wasm-5.7.3-RELEASE, wasi-backend: MicroWASI } + - { os: ubuntu-20.04, toolchain: wasm-5.10.0-RELEASE, wasi-backend: Node } - { os: ubuntu-20.04, toolchain: wasm-5.8.0-RELEASE, wasi-backend: MicroWASI } - { os: ubuntu-20.04, toolchain: wasm-5.9.1-RELEASE, wasi-backend: MicroWASI } + - { os: ubuntu-20.04, toolchain: wasm-5.10.0-RELEASE, wasi-backend: MicroWASI } - os: ubuntu-22.04 toolchain: DEVELOPMENT-SNAPSHOT-2024-05-01-a swift-sdk: @@ -76,8 +76,6 @@ jobs: strategy: matrix: include: - - os: macos-12 - xcode: Xcode_14.0 - os: macos-13 xcode: Xcode_14.3 - os: macos-14 From eb47bcbd93eed9e435e41d3ebf433126bc5507d4 Mon Sep 17 00:00:00 2001 From: Yuta Saito Date: Thu, 27 Jun 2024 15:54:13 +0900 Subject: [PATCH 113/373] Stop tracking main thread event loop --- Sources/JavaScriptEventLoop/JavaScriptEventLoop.swift | 8 -------- 1 file changed, 8 deletions(-) diff --git a/Sources/JavaScriptEventLoop/JavaScriptEventLoop.swift b/Sources/JavaScriptEventLoop/JavaScriptEventLoop.swift index a47491982..7a0364a5c 100644 --- a/Sources/JavaScriptEventLoop/JavaScriptEventLoop.swift +++ b/Sources/JavaScriptEventLoop/JavaScriptEventLoop.swift @@ -103,10 +103,6 @@ public final class JavaScriptEventLoop: SerialExecutor, @unchecked Sendable { } private static var didInstallGlobalExecutor = false - fileprivate static var _mainThreadEventLoop: JavaScriptEventLoop! - fileprivate static var mainThreadEventLoop: JavaScriptEventLoop { - return _mainThreadEventLoop - } /// Set JavaScript event loop based executor to be the global executor /// Note that this should be called before any of the jobs are created. @@ -116,10 +112,6 @@ public final class JavaScriptEventLoop: SerialExecutor, @unchecked Sendable { public static func installGlobalExecutor() { guard !didInstallGlobalExecutor else { return } - // NOTE: We assume that this function is called before any of the jobs are created, so we can safely - // assume that we are in the main thread. - _mainThreadEventLoop = JavaScriptEventLoop.shared - #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 From 268f613f00a16b05046f0b90674e7fff8cec8a21 Mon Sep 17 00:00:00 2001 From: Yuta Saito Date: Thu, 4 Jul 2024 18:54:39 +0900 Subject: [PATCH 114/373] Initial WebWorkerTaskExecutor WebWorkerTaskExecutor is an implementation of `TaskExecutor` protocol, which is introduced by [SE-0417] since Swift 6.0. This task executor runs tasks on Worker threads, which is useful for offloading computationally expensive tasks from the main thread. The `WebWorkerTaskExecutor` is designed to work with [Web Workers API] and Node.js's [`worker_threads` module]. [SE-0417]: https://github.com/swiftlang/swift-evolution/blob/main/proposals/0417-task-executor-preference.md [Web Workers API]: https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API [`worker_threads` module]: https://nodejs.org/api/worker_threads.html --- IntegrationTests/lib.js | 149 ++++-- Package.swift | 8 + Runtime/src/index.ts | 173 ++++++- Runtime/src/object-heap.ts | 18 +- Runtime/src/types.ts | 8 + .../JavaScriptEventLoop.swift | 26 +- Sources/JavaScriptEventLoop/JobQueue.swift | 2 +- .../WebWorkerTaskExecutor.swift | 455 ++++++++++++++++++ .../FundamentalObjects/JSClosure.swift | 20 +- .../_CJavaScriptEventLoop.c | 2 + .../include/_CJavaScriptEventLoop.h | 2 + Sources/_CJavaScriptKit/_CJavaScriptKit.c | 2 + .../_CJavaScriptKit/include/_CJavaScriptKit.h | 15 + .../WebWorkerTaskExecutorTests.swift | 154 ++++++ scripts/test-harness.mjs | 2 + 15 files changed, 999 insertions(+), 37 deletions(-) create mode 100644 Sources/JavaScriptEventLoop/WebWorkerTaskExecutor.swift create mode 100644 Tests/JavaScriptEventLoopTests/WebWorkerTaskExecutorTests.swift diff --git a/IntegrationTests/lib.js b/IntegrationTests/lib.js index fe25cf679..6f6ea4139 100644 --- a/IntegrationTests/lib.js +++ b/IntegrationTests/lib.js @@ -3,26 +3,30 @@ import { WASI as NodeWASI } from "wasi" import { WASI as MicroWASI, useAll } from "uwasi" import * as fs from "fs/promises" import path from "path"; +import { Worker, parentPort } from "node:worker_threads"; const WASI = { - MicroWASI: ({ programName }) => { + MicroWASI: ({ args }) => { const wasi = new MicroWASI({ - args: [path.basename(programName)], + args: args, env: {}, features: [useAll()], }) return { wasiImport: wasi.wasiImport, + setInstance(instance) { + wasi.instance = instance; + }, start(instance, swift) { wasi.initialize(instance); swift.main(); } } }, - Node: ({ programName }) => { + Node: ({ args }) => { const wasi = new NodeWASI({ - args: [path.basename(programName)], + args: args, env: {}, preopens: { "/": "./", @@ -44,12 +48,9 @@ const WASI = { const selectWASIBackend = () => { const value = process.env["JAVASCRIPTKIT_WASI_BACKEND"] if (value) { - const backend = WASI[value]; - if (backend) { - return backend; - } + return value; } - return WASI.Node; + return "Node" }; function isUsingSharedMemory(module) { @@ -62,33 +63,125 @@ function isUsingSharedMemory(module) { return false; } -export const startWasiTask = async (wasmPath, wasiConstructor = selectWASIBackend()) => { - const swift = new SwiftRuntime(); - // Fetch our Wasm File - const wasmBinary = await fs.readFile(wasmPath); - const wasi = wasiConstructor({ programName: wasmPath }); - - const module = await WebAssembly.compile(wasmBinary); - - const importObject = { +function constructBaseImportObject(wasi, swift) { + return { wasi_snapshot_preview1: wasi.wasiImport, - javascript_kit: swift.importObjects(), + javascript_kit: swift.wasmImports, benchmark_helper: { noop: () => {}, noop_with_int: (_) => {}, + }, + } +} + +export async function startWasiChildThread(event) { + const { module, programName, memory, tid, startArg } = event; + const swift = new SwiftRuntime({ + sharedMemory: true, + threadChannel: { + wakeUpMainThread: parentPort.postMessage.bind(parentPort), + listenWakeEventFromMainThread: (listener) => { + parentPort.on("message", listener) + } + } + }); + // Use uwasi for child threads because Node.js WASI cannot be used without calling + // `WASI.start` or `WASI.initialize`, which is already called in the main thread and + // will cause an error if called again. + const wasi = WASI.MicroWASI({ programName }); + + const importObject = constructBaseImportObject(wasi, swift); + + importObject["wasi"] = { + "thread-spawn": () => { + throw new Error("Cannot spawn a new thread from a worker thread") } }; + importObject["env"] = { memory }; + importObject["JavaScriptEventLoopTestSupportTests"] = { + "isMainThread": () => false, + } + + const instance = await WebAssembly.instantiate(module, importObject); + swift.setInstance(instance); + wasi.setInstance(instance); + swift.startThread(tid, startArg); +} + +class ThreadRegistry { + workers = new Map(); + nextTid = 1; + + spawnThread(module, programName, memory, startArg) { + const tid = this.nextTid++; + const selfFilePath = new URL(import.meta.url).pathname; + const worker = new Worker(` + const { parentPort } = require('node:worker_threads'); + + Error.stackTraceLimit = 100; + parentPort.once("message", async (event) => { + const { selfFilePath } = event; + const { startWasiChildThread } = await import(selfFilePath); + await startWasiChildThread(event); + }) + `, { type: "module", eval: true }) + + worker.on("error", (error) => { + console.error(`Worker thread ${tid} error:`, error); + }); + this.workers.set(tid, worker); + worker.postMessage({ selfFilePath, module, programName, memory, tid, startArg }); + return tid; + } + + worker(tid) { + return this.workers.get(tid); + } + + wakeUpWorkerThread(tid) { + const worker = this.workers.get(tid); + worker.postMessage(null); + } +} + +export const startWasiTask = async (wasmPath, wasiConstructorKey = selectWASIBackend()) => { + // Fetch our Wasm File + const wasmBinary = await fs.readFile(wasmPath); + const programName = wasmPath; + const args = [path.basename(programName)]; + args.push(...process.argv.slice(3)); + const wasi = WASI[wasiConstructorKey]({ args }); + + const module = await WebAssembly.compile(wasmBinary); + + const sharedMemory = isUsingSharedMemory(module); + const threadRegistry = new ThreadRegistry(); + const swift = new SwiftRuntime({ + sharedMemory, + threadChannel: { + wakeUpWorkerThread: threadRegistry.wakeUpWorkerThread.bind(threadRegistry), + listenMainJobFromWorkerThread: (tid, listener) => { + const worker = threadRegistry.worker(tid); + worker.on("message", listener); + } + } + }); + + const importObject = constructBaseImportObject(wasi, swift); + + importObject["JavaScriptEventLoopTestSupportTests"] = { + "isMainThread": () => true, + } - if (isUsingSharedMemory(module)) { - importObject["env"] = { - // We don't have JS API to get memory descriptor of imported memory - // at this moment, so we assume 256 pages (16MB) memory is enough - // large for initial memory size. - memory: new WebAssembly.Memory({ initial: 256, maximum: 16384, shared: true }), - }; + if (sharedMemory) { + // We don't have JS API to get memory descriptor of imported memory + // at this moment, so we assume 256 pages (16MB) memory is enough + // large for initial memory size. + const memory = new WebAssembly.Memory({ initial: 256, maximum: 16384, shared: true }) + importObject["env"] = { memory }; importObject["wasi"] = { - "thread-spawn": () => { - throw new Error("thread-spawn not implemented"); + "thread-spawn": (startArg) => { + return threadRegistry.spawnThread(module, programName, memory, startArg); } } } diff --git a/Package.swift b/Package.swift index d9f33839e..aa529c772 100644 --- a/Package.swift +++ b/Package.swift @@ -26,6 +26,14 @@ let package = Package( name: "JavaScriptEventLoop", dependencies: ["JavaScriptKit", "_CJavaScriptEventLoop"] ), + .testTarget( + name: "JavaScriptEventLoopTests", + dependencies: [ + "JavaScriptEventLoop", + "JavaScriptKit", + "JavaScriptEventLoopTestSupport", + ] + ), .target(name: "_CJavaScriptEventLoop"), .target( name: "JavaScriptEventLoopTestSupport", diff --git a/Runtime/src/index.ts b/Runtime/src/index.ts index 605ce2d06..f5cfb1ba6 100644 --- a/Runtime/src/index.ts +++ b/Runtime/src/index.ts @@ -10,8 +10,92 @@ import { import * as JSValue from "./js-value.js"; import { Memory } from "./memory.js"; -type SwiftRuntimeOptions = { +/** + * 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 jobs to each other using these functions. + * + * @example + * ```javascript + * // worker.js + * const runtime = new SwiftRuntime({ + * threadChannel: { + * wakeUpMainThread: (unownedJob) => { + * // Send the job to the main thread + * postMessage({ type: "job", unownedJob }); + * }, + * listenWakeEventFromMainThread: (listener) => { + * self.onmessage = (event) => { + * if (event.data.type === "wake") { + * listener(); + * } + * }; + * } + * } + * }); + * + * // main.js + * const worker = new Worker("worker.js"); + * const runtime = new SwiftRuntime({ + * threadChannel: { + * wakeUpWorkerThread: (tid) => { + * worker.postMessage({ type: "wake" }); + * }, + * listenMainJobFromWorkerThread: (tid, listener) => { + * worker.onmessage = (event) => { + * if (event.data.type === "job") { + * listener(event.data.unownedJob); + * } + * }; + * } + * } + * }); + * ``` + */ +export type SwiftRuntimeThreadChannel = + | { + /** + * This function is called when the Web Worker thread sends a job to the main thread. + * The unownedJob is the pointer to the unowned job object in the Web Worker thread. + * The job submitted by this function expected to be listened by `listenMainJobFromWorkerThread`. + */ + wakeUpMainThread: (unownedJob: number) => void; + /** + * This function is expected to be set in the worker thread and should listen + * to the wake event from the main thread sent by `wakeUpWorkerThread`. + * The passed listener function awakes the Web Worker thread. + */ + listenWakeEventFromMainThread: (listener: () => void) => void; + } + | { + /** + * This function is expected to be set in the main thread and called + * when the main thread sends a wake event to the Web Worker thread. + * The `tid` is the thread ID of the worker thread to be woken up. + * The wake event is expected to be listened by `listenWakeEventFromMainThread`. + */ + wakeUpWorkerThread: (tid: number) => void; + /** + * This function is expected to be set in the main thread and shuold listen + * to the main job sent by `wakeUpMainThread` from the worker thread. + */ + listenMainJobFromWorkerThread: ( + tid: number, + listener: (unownedJob: number) => void + ) => void; + }; + +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 { @@ -23,11 +107,14 @@ export class SwiftRuntime { 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; constructor(options?: SwiftRuntimeOptions) { this._instance = null; this._memory = null; this._closureDeallocator = null; + this.tid = null; this.options = options || {}; } @@ -72,6 +159,32 @@ export class SwiftRuntime { } } + /** + * 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"); @@ -462,6 +575,64 @@ export class SwiftRuntime { swjs_unsafe_event_loop_yield: () => { throw new UnsafeEventLoopYield(); }, + // This function is called by WebWorkerTaskExecutor on Web Worker thread. + swjs_send_job_to_main_thread: (unowned_job) => { + const threadChannel = this.options.threadChannel; + if (threadChannel && "wakeUpMainThread" in threadChannel) { + threadChannel.wakeUpMainThread(unowned_job); + } else { + throw new Error( + "wakeUpMainThread is not set in options given to SwiftRuntime. Please set it to send jobs to the main thread." + ); + } + }, + swjs_listen_wake_event_from_main_thread: () => { + // After the thread is started, + const swjs_wake_worker_thread = + this.exports.swjs_wake_worker_thread; + const threadChannel = this.options.threadChannel; + if ( + threadChannel && + "listenWakeEventFromMainThread" in threadChannel + ) { + threadChannel.listenWakeEventFromMainThread(() => { + swjs_wake_worker_thread(); + }); + } else { + throw new Error( + "listenWakeEventFromMainThread is not set in options given to SwiftRuntime. Please set it to listen to wake events from the main thread." + ); + } + }, + swjs_wake_up_worker_thread: (tid) => { + const threadChannel = this.options.threadChannel; + if (threadChannel && "wakeUpWorkerThread" in threadChannel) { + threadChannel.wakeUpWorkerThread(tid); + } else { + throw new Error( + "wakeUpWorkerThread is not set in options given to SwiftRuntime. Please set it to wake up worker threads." + ); + } + }, + swjs_listen_main_job_from_worker_thread: (tid) => { + const threadChannel = this.options.threadChannel; + if ( + threadChannel && + "listenMainJobFromWorkerThread" in threadChannel + ) { + threadChannel.listenMainJobFromWorkerThread( + tid, this.exports.swjs_enqueue_main_job_from_worker, + ); + } else { + throw new Error( + "listenMainJobFromWorkerThread is not set in options given to SwiftRuntime. Please set it to listen to jobs from worker threads." + ); + } + }, + swjs_get_worker_thread_id: () => { + // Main thread's tid is always -1 + return this.tid || -1; + }, }; } } diff --git a/Runtime/src/object-heap.ts b/Runtime/src/object-heap.ts index d59f5101e..98281b5ca 100644 --- a/Runtime/src/object-heap.ts +++ b/Runtime/src/object-heap.ts @@ -4,6 +4,7 @@ import { ref } from "./types.js"; type SwiftRuntimeHeapEntry = { id: number; rc: number; + released: boolean; }; export class SwiftRuntimeHeap { private _heapValueById: Map; @@ -15,7 +16,11 @@ export class SwiftRuntimeHeap { this._heapValueById.set(0, globalVariable); this._heapEntryByValue = new Map(); - this._heapEntryByValue.set(globalVariable, { id: 0, rc: 1 }); + this._heapEntryByValue.set(globalVariable, { + id: 0, + rc: 1, + released: false, + }); // Note: 0 is preserved for global this._heapNextKey = 1; @@ -29,13 +34,22 @@ export class SwiftRuntimeHeap { } const id = this._heapNextKey++; this._heapValueById.set(id, value); - this._heapEntryByValue.set(value, { id: id, rc: 1 }); + this._heapEntryByValue.set(value, { id: id, rc: 1, released: false }); return id; } release(ref: ref) { const value = this._heapValueById.get(ref); const entry = this._heapEntryByValue.get(value)!; + if (entry.released) { + console.error( + "Double release detected for reference " + ref, + entry + ); + throw new ReferenceError( + "Double release detected for reference " + ref + ); + } entry.rc--; if (entry.rc != 0) return; diff --git a/Runtime/src/types.ts b/Runtime/src/types.ts index 55f945b64..ed61555a8 100644 --- a/Runtime/src/types.ts +++ b/Runtime/src/types.ts @@ -19,6 +19,9 @@ export interface ExportedFunctions { ): bool; swjs_free_host_function(host_func_id: number): void; + + swjs_enqueue_main_job_from_worker(unowned_job: number): void; + swjs_wake_worker_thread(): void; } export interface ImportedFunctions { @@ -103,6 +106,11 @@ export interface ImportedFunctions { swjs_bigint_to_i64(ref: ref, signed: bool): bigint; swjs_i64_to_bigint_slow(lower: number, upper: number, signed: bool): ref; swjs_unsafe_event_loop_yield: () => void; + swjs_send_job_to_main_thread: (unowned_job: number) => void; + swjs_listen_wake_event_from_main_thread: () => void; + swjs_wake_up_worker_thread: (tid: number) => void; + swjs_listen_main_job_from_worker_thread: (tid: number) => void; + swjs_get_worker_thread_id: () => number; } export const enum LibraryFeatures { diff --git a/Sources/JavaScriptEventLoop/JavaScriptEventLoop.swift b/Sources/JavaScriptEventLoop/JavaScriptEventLoop.swift index 7a0364a5c..4ba186df5 100644 --- a/Sources/JavaScriptEventLoop/JavaScriptEventLoop.swift +++ b/Sources/JavaScriptEventLoop/JavaScriptEventLoop.swift @@ -1,6 +1,7 @@ import JavaScriptKit import _CJavaScriptEventLoop import _CJavaScriptKit +import Synchronization // NOTE: `@available` annotations are semantically wrong, but they make it easier to develop applications targeting WebAssembly in Xcode. @@ -56,7 +57,7 @@ public final class JavaScriptEventLoop: SerialExecutor, @unchecked Sendable { self.setTimeout = setTimeout } - /// A singleton instance of the Executor + /// A per-thread singleton instance of the Executor public static var shared: JavaScriptEventLoop { return _shared } @@ -142,6 +143,7 @@ public final class JavaScriptEventLoop: SerialExecutor, @unchecked Sendable { 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 + assert(false) JavaScriptEventLoop.shared.unsafeEnqueue(job) } swift_task_enqueueMainExecutor_hook = unsafeBitCast(swift_task_enqueueMainExecutor_hook_impl, to: UnsafeMutableRawPointer?.self) @@ -149,9 +151,8 @@ public final class JavaScriptEventLoop: SerialExecutor, @unchecked Sendable { didInstallGlobalExecutor = true } - private func enqueue(_ job: UnownedJob, withDelay nanoseconds: UInt64) { - let milliseconds = nanoseconds / 1_000_000 - setTimeout(Double(milliseconds), { + func enqueue(_ job: UnownedJob, withDelay nanoseconds: UInt64) { + enqueue(withDelay: nanoseconds, job: { #if compiler(>=5.9) job.runSynchronously(on: self.asUnownedSerialExecutor()) #else @@ -160,6 +161,23 @@ public final class JavaScriptEventLoop: SerialExecutor, @unchecked Sendable { }) } + func enqueue(withDelay nanoseconds: UInt64, job: @escaping () -> Void) { + let milliseconds = nanoseconds / 1_000_000 + setTimeout(Double(milliseconds), job) + } + + func enqueue( + withDelay seconds: Int64, _ nanoseconds: Int64, + _ toleranceSec: Int64, _ toleranceNSec: Int64, + _ clock: Int32, job: @escaping () -> Void + ) { + 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(withDelay: delayNanosec <= 0 ? 0 : UInt64(delayNanosec), job: job) + } + private func unsafeEnqueue(_ job: UnownedJob) { insertJobQueue(job: job) } diff --git a/Sources/JavaScriptEventLoop/JobQueue.swift b/Sources/JavaScriptEventLoop/JobQueue.swift index 5ad71f0a0..c6eb48b79 100644 --- a/Sources/JavaScriptEventLoop/JobQueue.swift +++ b/Sources/JavaScriptEventLoop/JobQueue.swift @@ -9,7 +9,7 @@ import _CJavaScriptEventLoop @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 + var isSpinning: Bool = false } @available(macOS 14.0, iOS 17.0, watchOS 10.0, tvOS 17.0, *) diff --git a/Sources/JavaScriptEventLoop/WebWorkerTaskExecutor.swift b/Sources/JavaScriptEventLoop/WebWorkerTaskExecutor.swift new file mode 100644 index 000000000..4b9b3215a --- /dev/null +++ b/Sources/JavaScriptEventLoop/WebWorkerTaskExecutor.swift @@ -0,0 +1,455 @@ +#if compiler(>=6.0) && _runtime(_multithreaded) // @_expose and @_extern are only available in Swift 6.0+ + +import JavaScriptKit +import _CJavaScriptKit +import _CJavaScriptEventLoop + +import Synchronization +#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. +/// +/// ## 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`. +/// +/// ## Usage +/// +/// ```swift +/// let executor = WebWorkerTaskExecutor(numberOfThreads: 4) +/// defer { executor.terminate() } +/// +/// await withTaskExecutorPreference(executor) { +/// // This block runs on the Web Worker thread. +/// await withTaskGroup(of: Int.self) { group in +/// for i in 0..<10 { +/// // Structured child works are executed on the Web Worker thread. +/// group.addTask { fibonacci(of: i) } +/// } +/// } +/// } +/// ```` +/// +/// ## 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. +/// } +/// ```` +/// +public final class WebWorkerTaskExecutor: TaskExecutor { + + /// 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) + jobQueue.withLock { 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 + } + } + } + + 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 wake-up 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_wake_event_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) + } + + /// 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.") + } + assert(state.load(ordering: .sequentiallyConsistent) == .running, "Invalid state: not running") + 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") + state.store(.terminated, ordering: .sequentiallyConsistent) + } + } + + 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...fromOpaque(ptr!).takeRetainedValue() + context.worker.start(executor: context.executor) + // The worker is started. Throw JS exception to unwind the call stack without + // reaching the `pthread_exit`, which is called immediately after this block. + swjs_unsafe_event_loop_yield() + return nil + }, ptr) + precondition(ret == 0, "Failed to create a thread") + } + // 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. + for worker in workers { + var tid: pid_t + repeat { + tid = worker.tid.load(ordering: .sequentiallyConsistent) + } while tid == 0 + swjs_listen_main_job_from_worker_thread(tid) + } + } + + func terminate() { + for worker in workers { + worker.terminate() + } + } + + func enqueue(_ job: consuming ExecutorJob) { + precondition(!workers.isEmpty, "No worker threads are available") + + let job = UnownedJob(job) + // 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 + + /// Create a new Web Worker task executor. + /// + /// - Parameter numberOfThreads: The number of Web Worker threads to spawn. + public init(numberOfThreads: Int) { + self.executor = Executor(numberOfThreads: numberOfThreads) + self.executor.start() + } + + /// Terminate child Web Worker threads. + /// Jobs enqueued to the executor after calling this method will be ignored. + /// + /// NOTE: 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() + } + + /// The number of Web Worker threads. + public var numberOfThreads: Int { + executor.numberOfThreads + } + + // MARK: TaskExecutor conformance + + /// Enqueue a job to the executor. + /// + /// NOTE: Called from the Swift Concurrency runtime. + public func enqueue(_ job: consuming ExecutorJob) { + Self.traceStatsIncrement(\.enqueueExecutor) + executor.enqueue(job) + } + + // MARK: Statistics + + /// Executor global statistics + internal struct ExecutorStats: CustomStringConvertible { + var sendJobToMainThread: Int = 0 + var recieveJobFromWorkerThread: Int = 0 + var enqueueGlobal: Int = 0 + var enqueueExecutor: Int = 0 + + var description: String { + "ExecutorStats(sendWtoM: \(sendJobToMainThread), recvWfromM: \(recieveJobFromWorkerThread)), 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 + + private static var _mainThread: pthread_t? + private static var _swift_task_enqueueGlobal_hook_original: UnsafeMutableRawPointer? + private static var _swift_task_enqueueGlobalWithDelay_hook_original: UnsafeMutableRawPointer? + private static var _swift_task_enqueueGlobalWithDeadline_hook_original: UnsafeMutableRawPointer? + + /// Install a global executor that forwards jobs from Web Worker threads to the main thread. + /// + /// This function must be called once before using the Web Worker task executor. + public static func installGlobalExecutor() { + // 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) + } +} + +/// 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. +@_expose(wasm, "swjs_enqueue_main_job_from_worker") +func _swjs_enqueue_main_job_from_worker(_ job: UnownedJob) { + WebWorkerTaskExecutor.traceStatsIncrement(\.recieveJobFromWorkerThread) + JavaScriptEventLoop.shared.enqueue(ExecutorJob(job)) +} + +@_expose(wasm, "swjs_wake_worker_thread") +func _swjs_wake_worker_thread() { + WebWorkerTaskExecutor.Worker.currentThread!.run() +} + +#endif + +fileprivate func trace(_ message: String) { +#if JAVASCRIPTKIT_TRACE + JSObject.global.process.stdout.write("[trace tid=\(swjs_get_worker_thread_id())] \(message)\n") +#endif +} diff --git a/Sources/JavaScriptKit/FundamentalObjects/JSClosure.swift b/Sources/JavaScriptKit/FundamentalObjects/JSClosure.swift index 6decbc814..75a8398fa 100644 --- a/Sources/JavaScriptKit/FundamentalObjects/JSClosure.swift +++ b/Sources/JavaScriptKit/FundamentalObjects/JSClosure.swift @@ -63,8 +63,26 @@ public class JSOneshotClosure: JSObject, JSClosureProtocol { /// public class JSClosure: JSFunction, JSClosureProtocol { + class SharedJSClosure { + private var storage: [JavaScriptHostFuncRef: (object: JSObject, body: ([JSValue]) -> JSValue)] = [:] + init() {} + + subscript(_ key: JavaScriptHostFuncRef) -> (object: JSObject, body: ([JSValue]) -> JSValue)? { + get { storage[key] } + set { storage[key] = newValue } + } + } + // Note: Retain the closure object itself also to avoid funcRef conflicts - fileprivate static var sharedClosures: [JavaScriptHostFuncRef: (object: JSObject, body: ([JSValue]) -> JSValue)] = [:] + fileprivate static var sharedClosures: SharedJSClosure { + if let swjs_thread_local_closures { + return Unmanaged.fromOpaque(swjs_thread_local_closures).takeUnretainedValue() + } else { + let shared = SharedJSClosure() + swjs_thread_local_closures = Unmanaged.passRetained(shared).toOpaque() + return shared + } + } private var hostFuncRef: JavaScriptHostFuncRef = 0 diff --git a/Sources/_CJavaScriptEventLoop/_CJavaScriptEventLoop.c b/Sources/_CJavaScriptEventLoop/_CJavaScriptEventLoop.c index 009672933..ebb05e1db 100644 --- a/Sources/_CJavaScriptEventLoop/_CJavaScriptEventLoop.c +++ b/Sources/_CJavaScriptEventLoop/_CJavaScriptEventLoop.c @@ -1,3 +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 890e26a01..4f1b9470c 100644 --- a/Sources/_CJavaScriptEventLoop/include/_CJavaScriptEventLoop.h +++ b/Sources/_CJavaScriptEventLoop/include/_CJavaScriptEventLoop.h @@ -66,4 +66,6 @@ extern void *_Nullable swift_task_asyncMainDrainQueue_hook; extern _Thread_local void * _Nullable swjs_thread_local_event_loop; +extern _Thread_local void * _Nullable swjs_thread_local_task_executor_worker; + #endif diff --git a/Sources/_CJavaScriptKit/_CJavaScriptKit.c b/Sources/_CJavaScriptKit/_CJavaScriptKit.c index 0bcc5eaca..3cc06af1c 100644 --- a/Sources/_CJavaScriptKit/_CJavaScriptKit.c +++ b/Sources/_CJavaScriptKit/_CJavaScriptKit.c @@ -47,4 +47,6 @@ int swjs_library_features(void) { return _library_features(); } +_Thread_local void *swjs_thread_local_closures; + #endif diff --git a/Sources/_CJavaScriptKit/include/_CJavaScriptKit.h b/Sources/_CJavaScriptKit/include/_CJavaScriptKit.h index 431b83615..dd7658649 100644 --- a/Sources/_CJavaScriptKit/include/_CJavaScriptKit.h +++ b/Sources/_CJavaScriptKit/include/_CJavaScriptKit.h @@ -295,4 +295,19 @@ IMPORT_JS_FUNCTION(swjs_release, void, (const JavaScriptObjectRef ref)) /// @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_wake_event_from_main_thread, void, (void)) + +IMPORT_JS_FUNCTION(swjs_wake_up_worker_thread, void, (int tid)) + +IMPORT_JS_FUNCTION(swjs_listen_main_job_from_worker_thread, void, (int tid)) + +IMPORT_JS_FUNCTION(swjs_get_worker_thread_id, int, (void)) + +/// MARK: - thread local storage + +// TODO: Rewrite closure system without global storage +extern _Thread_local void * _Nullable swjs_thread_local_closures; + #endif /* _CJavaScriptKit_h */ diff --git a/Tests/JavaScriptEventLoopTests/WebWorkerTaskExecutorTests.swift b/Tests/JavaScriptEventLoopTests/WebWorkerTaskExecutorTests.swift new file mode 100644 index 000000000..e4461620f --- /dev/null +++ b/Tests/JavaScriptEventLoopTests/WebWorkerTaskExecutorTests.swift @@ -0,0 +1,154 @@ +#if compiler(>=6.0) && _runtime(_multithreaded) +import XCTest +import JavaScriptKit +import _CJavaScriptKit // For swjs_get_worker_thread_id +@testable import JavaScriptEventLoop + +@_extern(wasm, module: "JavaScriptEventLoopTestSupportTests", name: "isMainThread") +func isMainThread() -> Bool + +final class WebWorkerTaskExecutorTests: XCTestCase { + override func setUp() { + WebWorkerTaskExecutor.installGlobalExecutor() + } + + func testTaskRunOnMainThread() async { + let executor = 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 { + let executor = WebWorkerTaskExecutor(numberOfThreads: 1) + await withTaskExecutorPreference(executor) { + XCTAssertFalse(isMainThread()) + } + } + + func testAwaitInsideTask() async throws { + let executor = WebWorkerTaskExecutor(numberOfThreads: 1) + + let task = Task(executorPreference: executor) { + await Task.yield() + _ = try await JSPromise.resolve(1).value + return isMainThread() + } + let taskRunOnMainThread = try await task.value + XCTAssertFalse(taskRunOnMainThread) + + executor.terminate() + } + + func testSleepInsideTask() async throws { + let executor = 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 { + let executor = 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 { + let executor = 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 { + let executor = 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.. { process.exit(1); } +Error.stackTraceLimit = Infinity; + startWasiTask(process.argv[2]).catch(handleExitOrError); From 91258e20ac7277a052e3be86e15cb9c29afbb2e3 Mon Sep 17 00:00:00 2001 From: Yuta Saito Date: Sat, 6 Jul 2024 10:30:42 +0000 Subject: [PATCH 115/373] Revert double free detection --- Runtime/src/object-heap.ts | 18 ++---------------- 1 file changed, 2 insertions(+), 16 deletions(-) diff --git a/Runtime/src/object-heap.ts b/Runtime/src/object-heap.ts index 98281b5ca..d59f5101e 100644 --- a/Runtime/src/object-heap.ts +++ b/Runtime/src/object-heap.ts @@ -4,7 +4,6 @@ import { ref } from "./types.js"; type SwiftRuntimeHeapEntry = { id: number; rc: number; - released: boolean; }; export class SwiftRuntimeHeap { private _heapValueById: Map; @@ -16,11 +15,7 @@ export class SwiftRuntimeHeap { this._heapValueById.set(0, globalVariable); this._heapEntryByValue = new Map(); - this._heapEntryByValue.set(globalVariable, { - id: 0, - rc: 1, - released: false, - }); + this._heapEntryByValue.set(globalVariable, { id: 0, rc: 1 }); // Note: 0 is preserved for global this._heapNextKey = 1; @@ -34,22 +29,13 @@ export class SwiftRuntimeHeap { } const id = this._heapNextKey++; this._heapValueById.set(id, value); - this._heapEntryByValue.set(value, { id: id, rc: 1, released: false }); + this._heapEntryByValue.set(value, { id: id, rc: 1 }); return id; } release(ref: ref) { const value = this._heapValueById.get(ref); const entry = this._heapEntryByValue.get(value)!; - if (entry.released) { - console.error( - "Double release detected for reference " + ref, - entry - ); - throw new ReferenceError( - "Double release detected for reference " + ref - ); - } entry.rc--; if (entry.rc != 0) return; From e3181b089191801fe135a74e765287812b8e10ab Mon Sep 17 00:00:00 2001 From: Yuta Saito Date: Sat, 6 Jul 2024 10:58:21 +0000 Subject: [PATCH 116/373] Revert debug changes in JavaScriptEventLoop --- .../JavaScriptEventLoop.swift | 24 +++---------------- Sources/JavaScriptEventLoop/JobQueue.swift | 2 +- 2 files changed, 4 insertions(+), 22 deletions(-) diff --git a/Sources/JavaScriptEventLoop/JavaScriptEventLoop.swift b/Sources/JavaScriptEventLoop/JavaScriptEventLoop.swift index 4ba186df5..e1e023e7f 100644 --- a/Sources/JavaScriptEventLoop/JavaScriptEventLoop.swift +++ b/Sources/JavaScriptEventLoop/JavaScriptEventLoop.swift @@ -1,7 +1,6 @@ import JavaScriptKit import _CJavaScriptEventLoop import _CJavaScriptKit -import Synchronization // NOTE: `@available` annotations are semantically wrong, but they make it easier to develop applications targeting WebAssembly in Xcode. @@ -143,7 +142,6 @@ public final class JavaScriptEventLoop: SerialExecutor, @unchecked Sendable { 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 - assert(false) JavaScriptEventLoop.shared.unsafeEnqueue(job) } swift_task_enqueueMainExecutor_hook = unsafeBitCast(swift_task_enqueueMainExecutor_hook_impl, to: UnsafeMutableRawPointer?.self) @@ -151,8 +149,9 @@ public final class JavaScriptEventLoop: SerialExecutor, @unchecked Sendable { didInstallGlobalExecutor = true } - func enqueue(_ job: UnownedJob, withDelay nanoseconds: UInt64) { - enqueue(withDelay: nanoseconds, job: { + private func enqueue(_ job: UnownedJob, withDelay nanoseconds: UInt64) { + let milliseconds = nanoseconds / 1_000_000 + setTimeout(Double(milliseconds), { #if compiler(>=5.9) job.runSynchronously(on: self.asUnownedSerialExecutor()) #else @@ -161,23 +160,6 @@ public final class JavaScriptEventLoop: SerialExecutor, @unchecked Sendable { }) } - func enqueue(withDelay nanoseconds: UInt64, job: @escaping () -> Void) { - let milliseconds = nanoseconds / 1_000_000 - setTimeout(Double(milliseconds), job) - } - - func enqueue( - withDelay seconds: Int64, _ nanoseconds: Int64, - _ toleranceSec: Int64, _ toleranceNSec: Int64, - _ clock: Int32, job: @escaping () -> Void - ) { - 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(withDelay: delayNanosec <= 0 ? 0 : UInt64(delayNanosec), job: job) - } - private func unsafeEnqueue(_ job: UnownedJob) { insertJobQueue(job: job) } diff --git a/Sources/JavaScriptEventLoop/JobQueue.swift b/Sources/JavaScriptEventLoop/JobQueue.swift index c6eb48b79..5ad71f0a0 100644 --- a/Sources/JavaScriptEventLoop/JobQueue.swift +++ b/Sources/JavaScriptEventLoop/JobQueue.swift @@ -9,7 +9,7 @@ import _CJavaScriptEventLoop @available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) struct QueueState: Sendable { fileprivate var headJob: UnownedJob? = nil - var isSpinning: Bool = false + fileprivate var isSpinning: Bool = false } @available(macOS 14.0, iOS 17.0, watchOS 10.0, tvOS 17.0, *) From 403511475c32023cf28274548c8592d374962fc7 Mon Sep 17 00:00:00 2001 From: Yuta Saito Date: Sat, 6 Jul 2024 11:03:20 +0000 Subject: [PATCH 117/373] make regenerate_swiftpm_resources --- Sources/JavaScriptKit/Runtime/index.js | 73 +++++++++++++++++++++++++ Sources/JavaScriptKit/Runtime/index.mjs | 73 +++++++++++++++++++++++++ 2 files changed, 146 insertions(+) diff --git a/Sources/JavaScriptKit/Runtime/index.js b/Sources/JavaScriptKit/Runtime/index.js index 2aaabce65..63667d300 100644 --- a/Sources/JavaScriptKit/Runtime/index.js +++ b/Sources/JavaScriptKit/Runtime/index.js @@ -205,6 +205,7 @@ this._instance = null; this._memory = null; this._closureDeallocator = null; + this.tid = null; this.options = options || {}; } setInstance(instance) { @@ -240,6 +241,31 @@ 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"); @@ -464,6 +490,53 @@ swjs_unsafe_event_loop_yield: () => { throw new UnsafeEventLoopYield(); }, + // This function is called by WebWorkerTaskExecutor on Web Worker thread. + swjs_send_job_to_main_thread: (unowned_job) => { + const threadChannel = this.options.threadChannel; + if (threadChannel && "wakeUpMainThread" in threadChannel) { + threadChannel.wakeUpMainThread(unowned_job); + } + else { + throw new Error("wakeUpMainThread is not set in options given to SwiftRuntime. Please set it to send jobs to the main thread."); + } + }, + swjs_listen_wake_event_from_main_thread: () => { + // After the thread is started, + const swjs_wake_worker_thread = this.exports.swjs_wake_worker_thread; + const threadChannel = this.options.threadChannel; + if (threadChannel && + "listenWakeEventFromMainThread" in threadChannel) { + threadChannel.listenWakeEventFromMainThread(() => { + swjs_wake_worker_thread(); + }); + } + else { + throw new Error("listenWakeEventFromMainThread is not set in options given to SwiftRuntime. Please set it to listen to wake events from the main thread."); + } + }, + swjs_wake_up_worker_thread: (tid) => { + const threadChannel = this.options.threadChannel; + if (threadChannel && "wakeUpWorkerThread" in threadChannel) { + threadChannel.wakeUpWorkerThread(tid); + } + else { + throw new Error("wakeUpWorkerThread is not set in options given to SwiftRuntime. Please set it to wake up worker threads."); + } + }, + swjs_listen_main_job_from_worker_thread: (tid) => { + const threadChannel = this.options.threadChannel; + if (threadChannel && + "listenMainJobFromWorkerThread" in threadChannel) { + threadChannel.listenMainJobFromWorkerThread(tid, this.exports.swjs_enqueue_main_job_from_worker); + } + else { + throw new Error("listenMainJobFromWorkerThread is not set in options given to SwiftRuntime. Please set it to listen to jobs from worker threads."); + } + }, + swjs_get_worker_thread_id: () => { + // Main thread's tid is always -1 + return this.tid || -1; + }, }; } } diff --git a/Sources/JavaScriptKit/Runtime/index.mjs b/Sources/JavaScriptKit/Runtime/index.mjs index 52de118b5..2f0558323 100644 --- a/Sources/JavaScriptKit/Runtime/index.mjs +++ b/Sources/JavaScriptKit/Runtime/index.mjs @@ -199,6 +199,7 @@ class SwiftRuntime { this._instance = null; this._memory = null; this._closureDeallocator = null; + this.tid = null; this.options = options || {}; } setInstance(instance) { @@ -234,6 +235,31 @@ class SwiftRuntime { 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"); @@ -458,6 +484,53 @@ class SwiftRuntime { swjs_unsafe_event_loop_yield: () => { throw new UnsafeEventLoopYield(); }, + // This function is called by WebWorkerTaskExecutor on Web Worker thread. + swjs_send_job_to_main_thread: (unowned_job) => { + const threadChannel = this.options.threadChannel; + if (threadChannel && "wakeUpMainThread" in threadChannel) { + threadChannel.wakeUpMainThread(unowned_job); + } + else { + throw new Error("wakeUpMainThread is not set in options given to SwiftRuntime. Please set it to send jobs to the main thread."); + } + }, + swjs_listen_wake_event_from_main_thread: () => { + // After the thread is started, + const swjs_wake_worker_thread = this.exports.swjs_wake_worker_thread; + const threadChannel = this.options.threadChannel; + if (threadChannel && + "listenWakeEventFromMainThread" in threadChannel) { + threadChannel.listenWakeEventFromMainThread(() => { + swjs_wake_worker_thread(); + }); + } + else { + throw new Error("listenWakeEventFromMainThread is not set in options given to SwiftRuntime. Please set it to listen to wake events from the main thread."); + } + }, + swjs_wake_up_worker_thread: (tid) => { + const threadChannel = this.options.threadChannel; + if (threadChannel && "wakeUpWorkerThread" in threadChannel) { + threadChannel.wakeUpWorkerThread(tid); + } + else { + throw new Error("wakeUpWorkerThread is not set in options given to SwiftRuntime. Please set it to wake up worker threads."); + } + }, + swjs_listen_main_job_from_worker_thread: (tid) => { + const threadChannel = this.options.threadChannel; + if (threadChannel && + "listenMainJobFromWorkerThread" in threadChannel) { + threadChannel.listenMainJobFromWorkerThread(tid, this.exports.swjs_enqueue_main_job_from_worker); + } + else { + throw new Error("listenMainJobFromWorkerThread is not set in options given to SwiftRuntime. Please set it to listen to jobs from worker threads."); + } + }, + swjs_get_worker_thread_id: () => { + // Main thread's tid is always -1 + return this.tid || -1; + }, }; } } From 56a81a4cf2fc20935624cadbdf819d63def4957a Mon Sep 17 00:00:00 2001 From: Yuta Saito Date: Sat, 6 Jul 2024 13:27:53 +0000 Subject: [PATCH 118/373] Add doc comment --- Sources/JavaScriptEventLoop/WebWorkerTaskExecutor.swift | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Sources/JavaScriptEventLoop/WebWorkerTaskExecutor.swift b/Sources/JavaScriptEventLoop/WebWorkerTaskExecutor.swift index 4b9b3215a..d2b458cc4 100644 --- a/Sources/JavaScriptEventLoop/WebWorkerTaskExecutor.swift +++ b/Sources/JavaScriptEventLoop/WebWorkerTaskExecutor.swift @@ -441,6 +441,8 @@ func _swjs_enqueue_main_job_from_worker(_ job: UnownedJob) { 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. @_expose(wasm, "swjs_wake_worker_thread") func _swjs_wake_worker_thread() { WebWorkerTaskExecutor.Worker.currentThread!.run() From 0ff82155e330a7dcd63c32f75c8fc22233b408e2 Mon Sep 17 00:00:00 2001 From: Yuta Saito Date: Sat, 6 Jul 2024 13:34:31 +0000 Subject: [PATCH 119/373] Transfer placeholder data to the worker thread For now, the data is not used in the worker thread, but it can be used in the future. --- Runtime/src/index.ts | 22 ++++++++++------------ 1 file changed, 10 insertions(+), 12 deletions(-) diff --git a/Runtime/src/index.ts b/Runtime/src/index.ts index f5cfb1ba6..493a266d9 100644 --- a/Runtime/src/index.ts +++ b/Runtime/src/index.ts @@ -22,13 +22,11 @@ import { Memory } from "./memory.js"; * threadChannel: { * wakeUpMainThread: (unownedJob) => { * // Send the job to the main thread - * postMessage({ type: "job", unownedJob }); + * postMessage(unownedJob); * }, * listenWakeEventFromMainThread: (listener) => { * self.onmessage = (event) => { - * if (event.data.type === "wake") { - * listener(); - * } + * listener(event.data); * }; * } * } @@ -38,14 +36,12 @@ import { Memory } from "./memory.js"; * const worker = new Worker("worker.js"); * const runtime = new SwiftRuntime({ * threadChannel: { - * wakeUpWorkerThread: (tid) => { - * worker.postMessage({ type: "wake" }); + * wakeUpWorkerThread: (tid, data) => { + * worker.postMessage(data); * }, * listenMainJobFromWorkerThread: (tid, listener) => { * worker.onmessage = (event) => { - * if (event.data.type === "job") { - * listener(event.data.unownedJob); - * } + listener(event.data); * }; * } * } @@ -65,16 +61,17 @@ export type SwiftRuntimeThreadChannel = * to the wake event from the main thread sent by `wakeUpWorkerThread`. * The passed listener function awakes the Web Worker thread. */ - listenWakeEventFromMainThread: (listener: () => void) => void; + listenWakeEventFromMainThread: (listener: (data: unknown) => void) => void; } | { /** * This function is expected to be set in the main thread and called * when the main thread sends a wake event to the Web Worker thread. * The `tid` is the thread ID of the worker thread to be woken up. + * The `data` is the data to be sent to the worker thread. * The wake event is expected to be listened by `listenWakeEventFromMainThread`. */ - wakeUpWorkerThread: (tid: number) => void; + wakeUpWorkerThread: (tid: number, data: unknown) => void; /** * This function is expected to be set in the main thread and shuold listen * to the main job sent by `wakeUpMainThread` from the worker thread. @@ -607,7 +604,8 @@ export class SwiftRuntime { swjs_wake_up_worker_thread: (tid) => { const threadChannel = this.options.threadChannel; if (threadChannel && "wakeUpWorkerThread" in threadChannel) { - threadChannel.wakeUpWorkerThread(tid); + // Currently, the data is not used, but it can be used in the future. + threadChannel.wakeUpWorkerThread(tid, {}); } else { throw new Error( "wakeUpWorkerThread is not set in options given to SwiftRuntime. Please set it to wake up worker threads." From 6992c32de0455e2d7e55ce48fda26d493fde6ce8 Mon Sep 17 00:00:00 2001 From: Yuta Saito Date: Sat, 6 Jul 2024 13:37:08 +0000 Subject: [PATCH 120/373] Update nightly toolchain --- .github/workflows/test.yml | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 2a4625d3c..edbc1e7b8 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -23,16 +23,16 @@ jobs: - { os: ubuntu-20.04, toolchain: wasm-5.9.1-RELEASE, wasi-backend: MicroWASI } - { os: ubuntu-20.04, toolchain: wasm-5.10.0-RELEASE, wasi-backend: MicroWASI } - os: ubuntu-22.04 - toolchain: DEVELOPMENT-SNAPSHOT-2024-05-01-a + toolchain: DEVELOPMENT-SNAPSHOT-2024-06-13-a swift-sdk: - id: DEVELOPMENT-SNAPSHOT-2024-05-25-a-wasm32-unknown-wasi - download-url: "https://github.com/swiftwasm/swift/releases/download/swift-wasm-DEVELOPMENT-SNAPSHOT-2024-05-25-a/swift-wasm-DEVELOPMENT-SNAPSHOT-2024-05-25-a-wasm32-unknown-wasi.artifactbundle.zip" + id: DEVELOPMENT-SNAPSHOT-2024-06-14-a-wasm32-unknown-wasi + download-url: "https://github.com/swiftwasm/swift/releases/download/swift-wasm-DEVELOPMENT-SNAPSHOT-2024-06-14-a/swift-wasm-DEVELOPMENT-SNAPSHOT-2024-06-14-a-wasm32-unknown-wasi.artifactbundle.zip" wasi-backend: Node - os: ubuntu-22.04 - toolchain: DEVELOPMENT-SNAPSHOT-2024-05-01-a + toolchain: DEVELOPMENT-SNAPSHOT-2024-06-13-a swift-sdk: - id: DEVELOPMENT-SNAPSHOT-2024-05-25-a-wasm32-unknown-wasip1-threads - download-url: "https://github.com/swiftwasm/swift/releases/download/swift-wasm-DEVELOPMENT-SNAPSHOT-2024-05-25-a/swift-wasm-DEVELOPMENT-SNAPSHOT-2024-05-25-a-wasm32-unknown-wasip1-threads.artifactbundle.zip" + id: DEVELOPMENT-SNAPSHOT-2024-06-14-a-wasm32-unknown-wasip1-threads + download-url: "https://github.com/swiftwasm/swift/releases/download/swift-wasm-DEVELOPMENT-SNAPSHOT-2024-06-14-a/swift-wasm-DEVELOPMENT-SNAPSHOT-2024-06-14-a-wasm32-unknown-wasip1-threads.artifactbundle.zip" wasi-backend: Node runs-on: ${{ matrix.entry.os }} From 8b9ed896b5f844a40da26b0a1ff532e2bf17893d Mon Sep 17 00:00:00 2001 From: Yuta Saito Date: Sat, 6 Jul 2024 13:41:15 +0000 Subject: [PATCH 121/373] make regenerate_swiftpm_resources --- Sources/JavaScriptKit/Runtime/index.js | 3 ++- Sources/JavaScriptKit/Runtime/index.mjs | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/Sources/JavaScriptKit/Runtime/index.js b/Sources/JavaScriptKit/Runtime/index.js index 63667d300..4498ee773 100644 --- a/Sources/JavaScriptKit/Runtime/index.js +++ b/Sources/JavaScriptKit/Runtime/index.js @@ -517,7 +517,8 @@ swjs_wake_up_worker_thread: (tid) => { const threadChannel = this.options.threadChannel; if (threadChannel && "wakeUpWorkerThread" in threadChannel) { - threadChannel.wakeUpWorkerThread(tid); + // Currently, the data is not used, but it can be used in the future. + threadChannel.wakeUpWorkerThread(tid, {}); } else { throw new Error("wakeUpWorkerThread is not set in options given to SwiftRuntime. Please set it to wake up worker threads."); diff --git a/Sources/JavaScriptKit/Runtime/index.mjs b/Sources/JavaScriptKit/Runtime/index.mjs index 2f0558323..7b470aaa0 100644 --- a/Sources/JavaScriptKit/Runtime/index.mjs +++ b/Sources/JavaScriptKit/Runtime/index.mjs @@ -511,7 +511,8 @@ class SwiftRuntime { swjs_wake_up_worker_thread: (tid) => { const threadChannel = this.options.threadChannel; if (threadChannel && "wakeUpWorkerThread" in threadChannel) { - threadChannel.wakeUpWorkerThread(tid); + // Currently, the data is not used, but it can be used in the future. + threadChannel.wakeUpWorkerThread(tid, {}); } else { throw new Error("wakeUpWorkerThread is not set in options given to SwiftRuntime. Please set it to wake up worker threads."); From 91611022e028c0fba2487ae67825ad439b734364 Mon Sep 17 00:00:00 2001 From: Yuta Saito Date: Sat, 6 Jul 2024 13:49:19 +0000 Subject: [PATCH 122/373] Fix internal compiler crash on CopyPropagation --- Sources/JavaScriptEventLoop/WebWorkerTaskExecutor.swift | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/Sources/JavaScriptEventLoop/WebWorkerTaskExecutor.swift b/Sources/JavaScriptEventLoop/WebWorkerTaskExecutor.swift index d2b458cc4..3b0acae9a 100644 --- a/Sources/JavaScriptEventLoop/WebWorkerTaskExecutor.swift +++ b/Sources/JavaScriptEventLoop/WebWorkerTaskExecutor.swift @@ -308,10 +308,9 @@ public final class WebWorkerTaskExecutor: TaskExecutor { } } - func enqueue(_ job: consuming ExecutorJob) { + func enqueue(_ job: UnownedJob) { precondition(!workers.isEmpty, "No worker threads are available") - let job = UnownedJob(job) // If the current thread is a worker thread, enqueue the job to the current worker. if let worker = Worker.currentThread { worker.enqueue(job) @@ -356,7 +355,7 @@ public final class WebWorkerTaskExecutor: TaskExecutor { /// Enqueue a job to the executor. /// /// NOTE: Called from the Swift Concurrency runtime. - public func enqueue(_ job: consuming ExecutorJob) { + public func enqueue(_ job: UnownedJob) { Self.traceStatsIncrement(\.enqueueExecutor) executor.enqueue(job) } From 3a999510d11c349eeafddd82b8d9a71680927f02 Mon Sep 17 00:00:00 2001 From: Yuta Saito Date: Sat, 6 Jul 2024 15:30:09 +0000 Subject: [PATCH 123/373] Stop blocking the main thread when starting Web Worker threads Seems like blocking the main thread also blocks the Web Worker threads from starting on some browsers. --- .../WebWorkerTaskExecutor.swift | 17 +++++++++---- .../WebWorkerTaskExecutorTests.swift | 24 +++++++++---------- 2 files changed, 25 insertions(+), 16 deletions(-) diff --git a/Sources/JavaScriptEventLoop/WebWorkerTaskExecutor.swift b/Sources/JavaScriptEventLoop/WebWorkerTaskExecutor.swift index 3b0acae9a..21cee87c9 100644 --- a/Sources/JavaScriptEventLoop/WebWorkerTaskExecutor.swift +++ b/Sources/JavaScriptEventLoop/WebWorkerTaskExecutor.swift @@ -263,7 +263,7 @@ public final class WebWorkerTaskExecutor: TaskExecutor { self.workers = workers } - func start() { + func start(timeout: Duration, checkInterval: Duration) async throws { class Context: @unchecked Sendable { let executor: WebWorkerTaskExecutor.Executor let worker: Worker @@ -293,10 +293,16 @@ public final class WebWorkerTaskExecutor: TaskExecutor { } // 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 { + fatalError("Worker thread initialization timeout exceeded (\(timeout))") + } tid = worker.tid.load(ordering: .sequentiallyConsistent) + try await clock.sleep(for: checkInterval) } while tid == 0 swjs_listen_main_job_from_worker_thread(tid) } @@ -330,10 +336,13 @@ public final class WebWorkerTaskExecutor: TaskExecutor { /// Create a new Web Worker task executor. /// - /// - Parameter numberOfThreads: The number of Web Worker threads to spawn. - public init(numberOfThreads: Int) { + /// - Parameters: + /// - numberOfThreads: The number of Web Worker threads to spawn. + /// - timeout: The timeout to wait for all worker threads to be started. + /// - checkInterval: The interval to check if all worker threads are started. + public init(numberOfThreads: Int, timeout: Duration = .seconds(3), checkInterval: Duration = .microseconds(5)) async throws { self.executor = Executor(numberOfThreads: numberOfThreads) - self.executor.start() + try await self.executor.start(timeout: timeout, checkInterval: checkInterval) } /// Terminate child Web Worker threads. diff --git a/Tests/JavaScriptEventLoopTests/WebWorkerTaskExecutorTests.swift b/Tests/JavaScriptEventLoopTests/WebWorkerTaskExecutorTests.swift index e4461620f..94e7635e4 100644 --- a/Tests/JavaScriptEventLoopTests/WebWorkerTaskExecutorTests.swift +++ b/Tests/JavaScriptEventLoopTests/WebWorkerTaskExecutorTests.swift @@ -12,8 +12,8 @@ final class WebWorkerTaskExecutorTests: XCTestCase { WebWorkerTaskExecutor.installGlobalExecutor() } - func testTaskRunOnMainThread() async { - let executor = WebWorkerTaskExecutor(numberOfThreads: 1) + func testTaskRunOnMainThread() async throws { + let executor = try await WebWorkerTaskExecutor(numberOfThreads: 1) XCTAssertTrue(isMainThread()) @@ -29,15 +29,15 @@ final class WebWorkerTaskExecutorTests: XCTestCase { executor.terminate() } - func testWithPreferenceBlock() async { - let executor = WebWorkerTaskExecutor(numberOfThreads: 1) + func testWithPreferenceBlock() async throws { + let executor = try await WebWorkerTaskExecutor(numberOfThreads: 1) await withTaskExecutorPreference(executor) { XCTAssertFalse(isMainThread()) } } func testAwaitInsideTask() async throws { - let executor = WebWorkerTaskExecutor(numberOfThreads: 1) + let executor = try await WebWorkerTaskExecutor(numberOfThreads: 1) let task = Task(executorPreference: executor) { await Task.yield() @@ -51,7 +51,7 @@ final class WebWorkerTaskExecutorTests: XCTestCase { } func testSleepInsideTask() async throws { - let executor = WebWorkerTaskExecutor(numberOfThreads: 1) + let executor = try await WebWorkerTaskExecutor(numberOfThreads: 1) let task = Task(executorPreference: executor) { XCTAssertFalse(isMainThread()) @@ -69,8 +69,8 @@ final class WebWorkerTaskExecutorTests: XCTestCase { executor.terminate() } - func testMainActorRun() async { - let executor = WebWorkerTaskExecutor(numberOfThreads: 1) + func testMainActorRun() async throws { + let executor = try await WebWorkerTaskExecutor(numberOfThreads: 1) let task = Task(executorPreference: executor) { await MainActor.run { @@ -87,8 +87,8 @@ final class WebWorkerTaskExecutorTests: XCTestCase { executor.terminate() } - func testTaskGroupRunOnSameThread() async { - let executor = WebWorkerTaskExecutor(numberOfThreads: 3) + func testTaskGroupRunOnSameThread() async throws { + let executor = try await WebWorkerTaskExecutor(numberOfThreads: 3) let mainTid = swjs_get_worker_thread_id() await withTaskExecutorPreference(executor) { @@ -112,8 +112,8 @@ final class WebWorkerTaskExecutorTests: XCTestCase { executor.terminate() } - func testTaskGroupRunOnDifferentThreads() async { - let executor = WebWorkerTaskExecutor(numberOfThreads: 2) + func testTaskGroupRunOnDifferentThreads() async throws { + let executor = try await WebWorkerTaskExecutor(numberOfThreads: 2) struct Item: Hashable { let type: String From 358633c07f7833a2745587458845efa698c38ba8 Mon Sep 17 00:00:00 2001 From: Yuta Saito Date: Sat, 6 Jul 2024 15:37:34 +0000 Subject: [PATCH 124/373] Add termination callback for worker threads --- Runtime/src/index.ts | 12 ++++++++++++ Runtime/src/types.ts | 1 + .../JavaScriptEventLoop/WebWorkerTaskExecutor.swift | 6 ++++++ Sources/JavaScriptKit/Runtime/index.js | 7 +++++++ Sources/JavaScriptKit/Runtime/index.mjs | 7 +++++++ Sources/_CJavaScriptKit/include/_CJavaScriptKit.h | 2 ++ 6 files changed, 35 insertions(+) diff --git a/Runtime/src/index.ts b/Runtime/src/index.ts index 493a266d9..341b2156c 100644 --- a/Runtime/src/index.ts +++ b/Runtime/src/index.ts @@ -80,6 +80,12 @@ export type SwiftRuntimeThreadChannel = tid: number, listener: (unownedJob: number) => void ) => void; + + /** + * This function is expected to be set in the main thread and called + * when the worker thread is terminated. + */ + terminateWorkerThread?: (tid: number) => void; }; export type SwiftRuntimeOptions = { @@ -627,6 +633,12 @@ export class SwiftRuntime { ); } }, + swjs_terminate_worker_thread: (tid) => { + 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; diff --git a/Runtime/src/types.ts b/Runtime/src/types.ts index ed61555a8..9aa36e96f 100644 --- a/Runtime/src/types.ts +++ b/Runtime/src/types.ts @@ -110,6 +110,7 @@ export interface ImportedFunctions { swjs_listen_wake_event_from_main_thread: () => void; swjs_wake_up_worker_thread: (tid: number) => void; swjs_listen_main_job_from_worker_thread: (tid: number) => void; + swjs_terminate_worker_thread: (tid: number) => void; swjs_get_worker_thread_id: () => number; } diff --git a/Sources/JavaScriptEventLoop/WebWorkerTaskExecutor.swift b/Sources/JavaScriptEventLoop/WebWorkerTaskExecutor.swift index 21cee87c9..9dd52bbf3 100644 --- a/Sources/JavaScriptEventLoop/WebWorkerTaskExecutor.swift +++ b/Sources/JavaScriptEventLoop/WebWorkerTaskExecutor.swift @@ -245,6 +245,12 @@ public final class WebWorkerTaskExecutor: TaskExecutor { func terminate() { trace("Worker.terminate") 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) } } diff --git a/Sources/JavaScriptKit/Runtime/index.js b/Sources/JavaScriptKit/Runtime/index.js index 4498ee773..4a5e8431a 100644 --- a/Sources/JavaScriptKit/Runtime/index.js +++ b/Sources/JavaScriptKit/Runtime/index.js @@ -534,6 +534,13 @@ throw new Error("listenMainJobFromWorkerThread is not set in options given to SwiftRuntime. Please set it to listen to jobs from worker threads."); } }, + 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; diff --git a/Sources/JavaScriptKit/Runtime/index.mjs b/Sources/JavaScriptKit/Runtime/index.mjs index 7b470aaa0..1a1830795 100644 --- a/Sources/JavaScriptKit/Runtime/index.mjs +++ b/Sources/JavaScriptKit/Runtime/index.mjs @@ -528,6 +528,13 @@ class SwiftRuntime { throw new Error("listenMainJobFromWorkerThread is not set in options given to SwiftRuntime. Please set it to listen to jobs from worker threads."); } }, + 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; diff --git a/Sources/_CJavaScriptKit/include/_CJavaScriptKit.h b/Sources/_CJavaScriptKit/include/_CJavaScriptKit.h index dd7658649..188f6b5db 100644 --- a/Sources/_CJavaScriptKit/include/_CJavaScriptKit.h +++ b/Sources/_CJavaScriptKit/include/_CJavaScriptKit.h @@ -303,6 +303,8 @@ IMPORT_JS_FUNCTION(swjs_wake_up_worker_thread, void, (int tid)) IMPORT_JS_FUNCTION(swjs_listen_main_job_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)) /// MARK: - thread local storage From a4daecdede926f532cd7edfddb068ede1b6e26f2 Mon Sep 17 00:00:00 2001 From: Yuta Saito Date: Sat, 6 Jul 2024 16:49:20 +0000 Subject: [PATCH 125/373] Generalize the thread channel functions not limited to wake-up events --- IntegrationTests/lib.js | 12 +- Runtime/src/index.ts | 151 ++++++++++-------- Runtime/src/types.ts | 4 +- .../WebWorkerTaskExecutor.swift | 6 +- Sources/JavaScriptKit/Runtime/index.js | 76 +++++---- Sources/JavaScriptKit/Runtime/index.mjs | 76 +++++---- .../_CJavaScriptKit/include/_CJavaScriptKit.h | 4 +- 7 files changed, 181 insertions(+), 148 deletions(-) diff --git a/IntegrationTests/lib.js b/IntegrationTests/lib.js index 6f6ea4139..ed66c7e86 100644 --- a/IntegrationTests/lib.js +++ b/IntegrationTests/lib.js @@ -79,8 +79,8 @@ export async function startWasiChildThread(event) { const swift = new SwiftRuntime({ sharedMemory: true, threadChannel: { - wakeUpMainThread: parentPort.postMessage.bind(parentPort), - listenWakeEventFromMainThread: (listener) => { + postMessageToMainThread: parentPort.postMessage.bind(parentPort), + listenMessageFromMainThread: (listener) => { parentPort.on("message", listener) } } @@ -138,9 +138,9 @@ class ThreadRegistry { return this.workers.get(tid); } - wakeUpWorkerThread(tid) { + wakeUpWorkerThread(tid, message) { const worker = this.workers.get(tid); - worker.postMessage(null); + worker.postMessage(message); } } @@ -159,8 +159,8 @@ export const startWasiTask = async (wasmPath, wasiConstructorKey = selectWASIBac const swift = new SwiftRuntime({ sharedMemory, threadChannel: { - wakeUpWorkerThread: threadRegistry.wakeUpWorkerThread.bind(threadRegistry), - listenMainJobFromWorkerThread: (tid, listener) => { + postMessageToWorkerThread: threadRegistry.wakeUpWorkerThread.bind(threadRegistry), + listenMessageFromWorkerThread: (tid, listener) => { const worker = threadRegistry.worker(tid); worker.on("message", listener); } diff --git a/Runtime/src/index.ts b/Runtime/src/index.ts index 341b2156c..4cf0ee65a 100644 --- a/Runtime/src/index.ts +++ b/Runtime/src/index.ts @@ -10,21 +10,27 @@ import { import * as JSValue from "./js-value.js"; import { Memory } from "./memory.js"; +type MainToWorkerMessage = { + type: "wake"; +}; + +type WorkerToMainMessage = { + type: "job"; + data: number; +}; + /** * 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 jobs to each other using these functions. + * can send messages to each other using these functions. * * @example * ```javascript * // worker.js * const runtime = new SwiftRuntime({ * threadChannel: { - * wakeUpMainThread: (unownedJob) => { - * // Send the job to the main thread - * postMessage(unownedJob); - * }, - * listenWakeEventFromMainThread: (listener) => { + * postMessageToMainThread: postMessage, + * listenMessageFromMainThread: (listener) => { * self.onmessage = (event) => { * listener(event.data); * }; @@ -36,10 +42,10 @@ import { Memory } from "./memory.js"; * const worker = new Worker("worker.js"); * const runtime = new SwiftRuntime({ * threadChannel: { - * wakeUpWorkerThread: (tid, data) => { + * postMessageToWorkerThread: (tid, data) => { * worker.postMessage(data); * }, - * listenMainJobFromWorkerThread: (tid, listener) => { + * listenMessageFromWorkerThread: (tid, listener) => { * worker.onmessage = (event) => { listener(event.data); * }; @@ -50,40 +56,42 @@ import { Memory } from "./memory.js"; */ export type SwiftRuntimeThreadChannel = | { - /** - * This function is called when the Web Worker thread sends a job to the main thread. - * The unownedJob is the pointer to the unowned job object in the Web Worker thread. - * The job submitted by this function expected to be listened by `listenMainJobFromWorkerThread`. - */ - wakeUpMainThread: (unownedJob: number) => void; + /** + * 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. + */ + postMessageToMainThread: (message: WorkerToMainMessage) => void; /** * This function is expected to be set in the worker thread and should listen - * to the wake event from the main thread sent by `wakeUpWorkerThread`. - * The passed listener function awakes the Web Worker thread. + * 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. */ - listenWakeEventFromMainThread: (listener: (data: unknown) => void) => void; + listenMessageFromMainThread: (listener: (message: MainToWorkerMessage) => void) => void; } | { /** - * This function is expected to be set in the main thread and called - * when the main thread sends a wake event to the Web Worker thread. - * The `tid` is the thread ID of the worker thread to be woken up. - * The `data` is the data to be sent to the worker thread. - * The wake event is expected to be listened by `listenWakeEventFromMainThread`. + * 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. */ - wakeUpWorkerThread: (tid: number, data: unknown) => void; + postMessageToWorkerThread: (tid: number, message: MainToWorkerMessage) => void; /** * This function is expected to be set in the main thread and shuold listen - * to the main job sent by `wakeUpMainThread` from the worker thread. + * to messsages 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. */ - listenMainJobFromWorkerThread: ( + listenMessageFromWorkerThread: ( tid: number, - listener: (unownedJob: number) => void + 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; }; @@ -578,60 +586,49 @@ export class SwiftRuntime { swjs_unsafe_event_loop_yield: () => { throw new UnsafeEventLoopYield(); }, - // This function is called by WebWorkerTaskExecutor on Web Worker thread. swjs_send_job_to_main_thread: (unowned_job) => { - const threadChannel = this.options.threadChannel; - if (threadChannel && "wakeUpMainThread" in threadChannel) { - threadChannel.wakeUpMainThread(unowned_job); - } else { - throw new Error( - "wakeUpMainThread is not set in options given to SwiftRuntime. Please set it to send jobs to the main thread." - ); - } + this.postMessageToMainThread({ type: "job", data: unowned_job }); }, - swjs_listen_wake_event_from_main_thread: () => { - // After the thread is started, - const swjs_wake_worker_thread = - this.exports.swjs_wake_worker_thread; + swjs_listen_message_from_main_thread: () => { const threadChannel = this.options.threadChannel; - if ( - threadChannel && - "listenWakeEventFromMainThread" in threadChannel - ) { - threadChannel.listenWakeEventFromMainThread(() => { - swjs_wake_worker_thread(); - }); - } else { + if (!(threadChannel && "listenMessageFromMainThread" in threadChannel)) { throw new Error( - "listenWakeEventFromMainThread is not set in options given to SwiftRuntime. Please set it to listen to wake events from the main thread." + "listenMessageFromMainThread is not set in options given to SwiftRuntime. Please set it to listen to wake events from the main thread." ); } + threadChannel.listenMessageFromMainThread((message) => { + switch (message.type) { + case "wake": + this.exports.swjs_wake_worker_thread(); + break; + default: + const unknownMessage: never = message.type; + throw new Error(`Unknown message type: ${unknownMessage}`); + } + }); }, swjs_wake_up_worker_thread: (tid) => { - const threadChannel = this.options.threadChannel; - if (threadChannel && "wakeUpWorkerThread" in threadChannel) { - // Currently, the data is not used, but it can be used in the future. - threadChannel.wakeUpWorkerThread(tid, {}); - } else { - throw new Error( - "wakeUpWorkerThread is not set in options given to SwiftRuntime. Please set it to wake up worker threads." - ); - } + this.postMessageToWorkerThread(tid, { type: "wake" }); }, - swjs_listen_main_job_from_worker_thread: (tid) => { + swjs_listen_message_from_worker_thread: (tid) => { const threadChannel = this.options.threadChannel; - if ( - threadChannel && - "listenMainJobFromWorkerThread" in threadChannel - ) { - threadChannel.listenMainJobFromWorkerThread( - tid, this.exports.swjs_enqueue_main_job_from_worker, - ); - } else { + if (!(threadChannel && "listenMessageFromWorkerThread" in threadChannel)) { throw new Error( - "listenMainJobFromWorkerThread is not set in options given to SwiftRuntime. Please set it to listen to jobs from worker threads." + "listenMessageFromWorkerThread is not set in options given to SwiftRuntime. Please set it to listen to jobs from worker threads." ); } + threadChannel.listenMessageFromWorkerThread( + tid, (message) => { + switch (message.type) { + case "job": + this.exports.swjs_enqueue_main_job_from_worker(message.data); + break; + default: + const unknownMessage: never = message.type; + throw new Error(`Unknown message type: ${unknownMessage}`); + } + }, + ); }, swjs_terminate_worker_thread: (tid) => { const threadChannel = this.options.threadChannel; @@ -645,6 +642,26 @@ export class SwiftRuntime { }, }; } + + private postMessageToMainThread(message: WorkerToMainMessage) { + 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); + } + + private postMessageToWorkerThread(tid: number, message: MainToWorkerMessage) { + 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); + } } /// This error is thrown when yielding event loop control from `swift_task_asyncMainDrainQueue` diff --git a/Runtime/src/types.ts b/Runtime/src/types.ts index 9aa36e96f..dd638acc5 100644 --- a/Runtime/src/types.ts +++ b/Runtime/src/types.ts @@ -107,9 +107,9 @@ export interface ImportedFunctions { swjs_i64_to_bigint_slow(lower: number, upper: number, signed: bool): ref; swjs_unsafe_event_loop_yield: () => void; swjs_send_job_to_main_thread: (unowned_job: number) => void; - swjs_listen_wake_event_from_main_thread: () => void; + swjs_listen_message_from_main_thread: () => void; swjs_wake_up_worker_thread: (tid: number) => void; - swjs_listen_main_job_from_worker_thread: (tid: number) => void; + swjs_listen_message_from_worker_thread: (tid: number) => void; swjs_terminate_worker_thread: (tid: number) => void; swjs_get_worker_thread_id: () => number; } diff --git a/Sources/JavaScriptEventLoop/WebWorkerTaskExecutor.swift b/Sources/JavaScriptEventLoop/WebWorkerTaskExecutor.swift index 9dd52bbf3..d1f7f64e2 100644 --- a/Sources/JavaScriptEventLoop/WebWorkerTaskExecutor.swift +++ b/Sources/JavaScriptEventLoop/WebWorkerTaskExecutor.swift @@ -188,10 +188,10 @@ public final class WebWorkerTaskExecutor: TaskExecutor { // `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 wake-up events from the main thread. + // 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_wake_event_from_main_thread() + 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. @@ -310,7 +310,7 @@ public final class WebWorkerTaskExecutor: TaskExecutor { tid = worker.tid.load(ordering: .sequentiallyConsistent) try await clock.sleep(for: checkInterval) } while tid == 0 - swjs_listen_main_job_from_worker_thread(tid) + swjs_listen_message_from_worker_thread(tid) } } diff --git a/Sources/JavaScriptKit/Runtime/index.js b/Sources/JavaScriptKit/Runtime/index.js index 4a5e8431a..9d29b4329 100644 --- a/Sources/JavaScriptKit/Runtime/index.js +++ b/Sources/JavaScriptKit/Runtime/index.js @@ -490,49 +490,43 @@ swjs_unsafe_event_loop_yield: () => { throw new UnsafeEventLoopYield(); }, - // This function is called by WebWorkerTaskExecutor on Web Worker thread. swjs_send_job_to_main_thread: (unowned_job) => { - const threadChannel = this.options.threadChannel; - if (threadChannel && "wakeUpMainThread" in threadChannel) { - threadChannel.wakeUpMainThread(unowned_job); - } - else { - throw new Error("wakeUpMainThread is not set in options given to SwiftRuntime. Please set it to send jobs to the main thread."); - } + this.postMessageToMainThread({ type: "job", data: unowned_job }); }, - swjs_listen_wake_event_from_main_thread: () => { - // After the thread is started, - const swjs_wake_worker_thread = this.exports.swjs_wake_worker_thread; + swjs_listen_message_from_main_thread: () => { const threadChannel = this.options.threadChannel; - if (threadChannel && - "listenWakeEventFromMainThread" in threadChannel) { - threadChannel.listenWakeEventFromMainThread(() => { - swjs_wake_worker_thread(); - }); - } - else { - throw new Error("listenWakeEventFromMainThread is not set in options given to SwiftRuntime. Please set it to listen to wake events from the main thread."); + 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."); } + threadChannel.listenMessageFromMainThread((message) => { + switch (message.type) { + case "wake": + this.exports.swjs_wake_worker_thread(); + break; + default: + const unknownMessage = message.type; + throw new Error(`Unknown message type: ${unknownMessage}`); + } + }); }, swjs_wake_up_worker_thread: (tid) => { - const threadChannel = this.options.threadChannel; - if (threadChannel && "wakeUpWorkerThread" in threadChannel) { - // Currently, the data is not used, but it can be used in the future. - threadChannel.wakeUpWorkerThread(tid, {}); - } - else { - throw new Error("wakeUpWorkerThread is not set in options given to SwiftRuntime. Please set it to wake up worker threads."); - } + this.postMessageToWorkerThread(tid, { type: "wake" }); }, - swjs_listen_main_job_from_worker_thread: (tid) => { + swjs_listen_message_from_worker_thread: (tid) => { const threadChannel = this.options.threadChannel; - if (threadChannel && - "listenMainJobFromWorkerThread" in threadChannel) { - threadChannel.listenMainJobFromWorkerThread(tid, this.exports.swjs_enqueue_main_job_from_worker); - } - else { - throw new Error("listenMainJobFromWorkerThread is not set in options given to SwiftRuntime. Please set it to listen to jobs from worker threads."); + 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."); } + threadChannel.listenMessageFromWorkerThread(tid, (message) => { + switch (message.type) { + case "job": + this.exports.swjs_enqueue_main_job_from_worker(message.data); + break; + default: + const unknownMessage = message.type; + throw new Error(`Unknown message type: ${unknownMessage}`); + } + }); }, swjs_terminate_worker_thread: (tid) => { var _a; @@ -547,6 +541,20 @@ }, }; } + postMessageToMainThread(message) { + 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); + } + postMessageToWorkerThread(tid, message) { + 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); + } } /// This error is thrown when yielding event loop control from `swift_task_asyncMainDrainQueue` /// to JavaScript. This is usually thrown when: diff --git a/Sources/JavaScriptKit/Runtime/index.mjs b/Sources/JavaScriptKit/Runtime/index.mjs index 1a1830795..9201b7712 100644 --- a/Sources/JavaScriptKit/Runtime/index.mjs +++ b/Sources/JavaScriptKit/Runtime/index.mjs @@ -484,49 +484,43 @@ class SwiftRuntime { swjs_unsafe_event_loop_yield: () => { throw new UnsafeEventLoopYield(); }, - // This function is called by WebWorkerTaskExecutor on Web Worker thread. swjs_send_job_to_main_thread: (unowned_job) => { - const threadChannel = this.options.threadChannel; - if (threadChannel && "wakeUpMainThread" in threadChannel) { - threadChannel.wakeUpMainThread(unowned_job); - } - else { - throw new Error("wakeUpMainThread is not set in options given to SwiftRuntime. Please set it to send jobs to the main thread."); - } + this.postMessageToMainThread({ type: "job", data: unowned_job }); }, - swjs_listen_wake_event_from_main_thread: () => { - // After the thread is started, - const swjs_wake_worker_thread = this.exports.swjs_wake_worker_thread; + swjs_listen_message_from_main_thread: () => { const threadChannel = this.options.threadChannel; - if (threadChannel && - "listenWakeEventFromMainThread" in threadChannel) { - threadChannel.listenWakeEventFromMainThread(() => { - swjs_wake_worker_thread(); - }); - } - else { - throw new Error("listenWakeEventFromMainThread is not set in options given to SwiftRuntime. Please set it to listen to wake events from the main thread."); + 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."); } + threadChannel.listenMessageFromMainThread((message) => { + switch (message.type) { + case "wake": + this.exports.swjs_wake_worker_thread(); + break; + default: + const unknownMessage = message.type; + throw new Error(`Unknown message type: ${unknownMessage}`); + } + }); }, swjs_wake_up_worker_thread: (tid) => { - const threadChannel = this.options.threadChannel; - if (threadChannel && "wakeUpWorkerThread" in threadChannel) { - // Currently, the data is not used, but it can be used in the future. - threadChannel.wakeUpWorkerThread(tid, {}); - } - else { - throw new Error("wakeUpWorkerThread is not set in options given to SwiftRuntime. Please set it to wake up worker threads."); - } + this.postMessageToWorkerThread(tid, { type: "wake" }); }, - swjs_listen_main_job_from_worker_thread: (tid) => { + swjs_listen_message_from_worker_thread: (tid) => { const threadChannel = this.options.threadChannel; - if (threadChannel && - "listenMainJobFromWorkerThread" in threadChannel) { - threadChannel.listenMainJobFromWorkerThread(tid, this.exports.swjs_enqueue_main_job_from_worker); - } - else { - throw new Error("listenMainJobFromWorkerThread is not set in options given to SwiftRuntime. Please set it to listen to jobs from worker threads."); + 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."); } + threadChannel.listenMessageFromWorkerThread(tid, (message) => { + switch (message.type) { + case "job": + this.exports.swjs_enqueue_main_job_from_worker(message.data); + break; + default: + const unknownMessage = message.type; + throw new Error(`Unknown message type: ${unknownMessage}`); + } + }); }, swjs_terminate_worker_thread: (tid) => { var _a; @@ -541,6 +535,20 @@ class SwiftRuntime { }, }; } + postMessageToMainThread(message) { + 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); + } + postMessageToWorkerThread(tid, message) { + 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); + } } /// This error is thrown when yielding event loop control from `swift_task_asyncMainDrainQueue` /// to JavaScript. This is usually thrown when: diff --git a/Sources/_CJavaScriptKit/include/_CJavaScriptKit.h b/Sources/_CJavaScriptKit/include/_CJavaScriptKit.h index 188f6b5db..1e539fde1 100644 --- a/Sources/_CJavaScriptKit/include/_CJavaScriptKit.h +++ b/Sources/_CJavaScriptKit/include/_CJavaScriptKit.h @@ -297,11 +297,11 @@ 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_wake_event_from_main_thread, void, (void)) +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_main_job_from_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)) From bfaba41ab62aaa89080f9d467eb68497e77a8a10 Mon Sep 17 00:00:00 2001 From: Yuta Saito Date: Wed, 10 Jul 2024 15:41:03 +0000 Subject: [PATCH 126/373] Bump version to 0.20.0, update `CHANGELOG.md` --- CHANGELOG.md | 11 +++++++++++ package-lock.json | 4 ++-- package.json | 2 +- 3 files changed, 14 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3427a540d..6cd6baa4a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,14 @@ +# 0.19.3 (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 diff --git a/package-lock.json b/package-lock.json index 962bc41ee..cb238d2c8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "javascript-kit-swift", - "version": "0.19.3", + "version": "0.20.0", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "javascript-kit-swift", - "version": "0.19.3", + "version": "0.20.0", "license": "MIT", "devDependencies": { "@rollup/plugin-typescript": "^8.3.1", diff --git a/package.json b/package.json index 7d32453b1..49f64e753 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "javascript-kit-swift", - "version": "0.19.3", + "version": "0.20.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", From 35ddaf61c4679beb01c02df9d20a7f119b9d2f23 Mon Sep 17 00:00:00 2001 From: Yuta Saito Date: Thu, 11 Jul 2024 16:24:11 +0900 Subject: [PATCH 127/373] Fix IDE build of WebWorkerTaskExecutor --- Sources/JavaScriptEventLoop/WebWorkerTaskExecutor.swift | 3 +++ 1 file changed, 3 insertions(+) diff --git a/Sources/JavaScriptEventLoop/WebWorkerTaskExecutor.swift b/Sources/JavaScriptEventLoop/WebWorkerTaskExecutor.swift index d1f7f64e2..170cb64d9 100644 --- a/Sources/JavaScriptEventLoop/WebWorkerTaskExecutor.swift +++ b/Sources/JavaScriptEventLoop/WebWorkerTaskExecutor.swift @@ -59,6 +59,7 @@ import Synchronization /// } /// ```` /// +@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 { /// A job worker dedicated to a single Web Worker thread. @@ -449,6 +450,7 @@ public final class WebWorkerTaskExecutor: TaskExecutor { /// 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, *) @_expose(wasm, "swjs_enqueue_main_job_from_worker") func _swjs_enqueue_main_job_from_worker(_ job: UnownedJob) { WebWorkerTaskExecutor.traceStatsIncrement(\.recieveJobFromWorkerThread) @@ -457,6 +459,7 @@ func _swjs_enqueue_main_job_from_worker(_ job: UnownedJob) { /// Wake up the worker thread. /// This function is called when a job is enqueued from the main thread to a worker thread. +@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) @_expose(wasm, "swjs_wake_worker_thread") func _swjs_wake_worker_thread() { WebWorkerTaskExecutor.Worker.currentThread!.run() From 5f2a9054712394a2ba875f187c8b3fd18ee042bb Mon Sep 17 00:00:00 2001 From: Yuta Saito Date: Thu, 11 Jul 2024 16:33:05 +0900 Subject: [PATCH 128/373] Fix IDE build by moving the TLS definition outside of the #if --- Sources/_CJavaScriptKit/_CJavaScriptKit.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Sources/_CJavaScriptKit/_CJavaScriptKit.c b/Sources/_CJavaScriptKit/_CJavaScriptKit.c index 3cc06af1c..a6e63a1b8 100644 --- a/Sources/_CJavaScriptKit/_CJavaScriptKit.c +++ b/Sources/_CJavaScriptKit/_CJavaScriptKit.c @@ -47,6 +47,6 @@ int swjs_library_features(void) { return _library_features(); } -_Thread_local void *swjs_thread_local_closures; - #endif + +_Thread_local void *swjs_thread_local_closures; From 877b0e0663133f920edea00d35d991582127dede Mon Sep 17 00:00:00 2001 From: Yuta Saito Date: Thu, 11 Jul 2024 16:38:55 +0900 Subject: [PATCH 129/373] Simplify the release workflow --- CHANGELOG.md | 7 ++++++- package-lock.json | 4 ++-- package.json | 2 +- 3 files changed, 9 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6cd6baa4a..9e9e0bd6f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,9 @@ -# 0.19.3 (11 July 2024) +> [!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. diff --git a/package-lock.json b/package-lock.json index cb238d2c8..18415649f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "javascript-kit-swift", - "version": "0.20.0", + "version": "0.0.0", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "javascript-kit-swift", - "version": "0.20.0", + "version": "0.0.0", "license": "MIT", "devDependencies": { "@rollup/plugin-typescript": "^8.3.1", diff --git a/package.json b/package.json index 49f64e753..e25d0a17b 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "javascript-kit-swift", - "version": "0.20.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", From 7496901a7e65db9b579cb7e0c27f1db5238c020d Mon Sep 17 00:00:00 2001 From: Yuta Saito Date: Fri, 12 Jul 2024 11:24:03 +0900 Subject: [PATCH 130/373] Simplify basic example --- Example/Makefile | 16 - Example/README.md | 9 - Example/package-lock.json | 6763 ----------------- Example/package.json | 22 - Example/public/index.html | 9 - Example/src/index.js | 54 - Example/webpack.config.js | 23 - .../Basic}/.gitignore | 0 .../Basic}/Package.swift | 13 +- Examples/Basic/README.md | 9 + .../Basic/Sources}/main.swift | 0 Examples/Basic/build.sh | 1 + Examples/Basic/index.html | 12 + Examples/Basic/index.js | 33 + 14 files changed, 59 insertions(+), 6905 deletions(-) delete mode 100644 Example/Makefile delete mode 100644 Example/README.md delete mode 100644 Example/package-lock.json delete mode 100644 Example/package.json delete mode 100644 Example/public/index.html delete mode 100644 Example/src/index.js delete mode 100644 Example/webpack.config.js rename {Example/JavaScriptKitExample => Examples/Basic}/.gitignore (100%) rename {Example/JavaScriptKitExample => Examples/Basic}/Package.swift (55%) create mode 100644 Examples/Basic/README.md rename {Example/JavaScriptKitExample/Sources/JavaScriptKitExample => Examples/Basic/Sources}/main.swift (100%) create mode 100755 Examples/Basic/build.sh create mode 100644 Examples/Basic/index.html create mode 100644 Examples/Basic/index.js 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 38124aea1..000000000 --- a/Example/package-lock.json +++ /dev/null @@ -1,6763 +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.70.0", - "webpack-cli": "^4.9.2", - "webpack-dev-server": "^4.7.4" - } - }, - "..": { - "name": "javascript-kit-swift", - "version": "0.19.0", - "license": "MIT", - "devDependencies": { - "@rollup/plugin-typescript": "^8.3.1", - "prettier": "2.6.1", - "rollup": "^2.70.0", - "tslib": "^2.3.1", - "typescript": "^4.6.3" - } - }, - "../node_modules/prettier": { - "version": "2.1.2", - "integrity": "sha512-16c7K+x4qVlJg9rEbXl7HEGmQyZlG4R9AgP+oHKRMsMsuk8s+ATStlf1NpDqyBI1HpVyfjLOeMhH2LvuNvV5Vg==", - "dev": true, - "bin": { - "prettier": "bin-prettier.js" - }, - "engines": { - "node": ">=10.13.0" - } - }, - "../node_modules/typescript": { - "version": "4.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/@jridgewell/gen-mapping": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.2.tgz", - "integrity": "sha512-mh65xKQAzI6iBcFzwv28KVWSmCkdRBWoOh+bYQGW3+6OZvbbN3TqMGo5hqYxQniRcH9F2VZIoJCm4pa3BPDK/A==", - "dev": true, - "dependencies": { - "@jridgewell/set-array": "^1.0.1", - "@jridgewell/sourcemap-codec": "^1.4.10", - "@jridgewell/trace-mapping": "^0.3.9" - }, - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/@jridgewell/resolve-uri": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.0.tgz", - "integrity": "sha512-F2msla3tad+Mfht5cJq7LSXcdudKTWCVYUgw6pLFOOHSTtZlj6SWNYAp+AhuqLmWdBO2X5hPrLcu8cVP8fy28w==", - "dev": true, - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/@jridgewell/set-array": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.1.2.tgz", - "integrity": "sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw==", - "dev": true, - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/@jridgewell/source-map": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.2.tgz", - "integrity": "sha512-m7O9o2uR8k2ObDysZYzdfhb08VuEml5oWGiosa1VdaPZ/A6QyPkAJuwN0Q1lhULOf6B7MtQmHENS743hWtCrgw==", - "dev": true, - "dependencies": { - "@jridgewell/gen-mapping": "^0.3.0", - "@jridgewell/trace-mapping": "^0.3.9" - } - }, - "node_modules/@jridgewell/sourcemap-codec": { - "version": "1.4.14", - "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.14.tgz", - "integrity": "sha512-XPSJHWmi394fuUuzDnGz1wiKqWfo1yXecHQMRf2l6hztTO+nPru658AyDngaBe7isIxEkRsPR3FZh+s7iVa4Uw==", - "dev": true - }, - "node_modules/@jridgewell/trace-mapping": { - "version": "0.3.14", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.14.tgz", - "integrity": "sha512-bJWEfQ9lPTvm3SneWwRFVLzrh6nhjwqw7TUFFBEMzwvg7t7PCDenf2lDwqo4NQXzdpgBXyFgDWnQA+2vkruksQ==", - "dev": true, - "dependencies": { - "@jridgewell/resolve-uri": "^3.0.3", - "@jridgewell/sourcemap-codec": "^1.4.10" - } - }, - "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/body-parser": { - "version": "1.19.2", - "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.2.tgz", - "integrity": "sha512-ALYone6pm6QmwZoAgeyNksccT9Q4AWZQ6PvfwR37GT6r6FWUPguq6sUmNGSMV2Wr761oQoBxwGGa6DR5o1DC9g==", - "dev": true, - "dependencies": { - "@types/connect": "*", - "@types/node": "*" - } - }, - "node_modules/@types/bonjour": { - "version": "3.5.10", - "resolved": "https://registry.npmjs.org/@types/bonjour/-/bonjour-3.5.10.tgz", - "integrity": "sha512-p7ienRMiS41Nu2/igbJxxLDWrSZ0WxM8UQgCeO9KhoVF7cOVFkrKsiDr1EsJIla8vV3oEEjGcz11jc5yimhzZw==", - "dev": true, - "dependencies": { - "@types/node": "*" - } - }, - "node_modules/@types/connect": { - "version": "3.4.35", - "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.35.tgz", - "integrity": "sha512-cdeYyv4KWoEgpBISTxWvqYsVy444DOqehiF3fM3ne10AmJ62RSyNkUnxMJXHQWRQQX2eR94m5y1IZyDwBjV9FQ==", - "dev": true, - "dependencies": { - "@types/node": "*" - } - }, - "node_modules/@types/connect-history-api-fallback": { - "version": "1.3.5", - "resolved": "https://registry.npmjs.org/@types/connect-history-api-fallback/-/connect-history-api-fallback-1.3.5.tgz", - "integrity": "sha512-h8QJa8xSb1WD4fpKBDcATDNGXghFj6/3GRWG6dhmRcu0RX1Ubasur2Uvx5aeEwlf0MwblEC2bMzzMQntxnw/Cw==", - "dev": true, - "dependencies": { - "@types/express-serve-static-core": "*", - "@types/node": "*" - } - }, - "node_modules/@types/eslint": { - "version": "8.4.1", - "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-8.4.1.tgz", - "integrity": "sha512-GE44+DNEyxxh2Kc6ro/VkIj+9ma0pO0bwv9+uHSyBrikYOHr8zYcdPvnBOp1aw8s+CjRvuSx7CyWqRrNFQ59mA==", - "dev": true, - "dependencies": { - "@types/estree": "*", - "@types/json-schema": "*" - } - }, - "node_modules/@types/eslint-scope": { - "version": "3.7.3", - "resolved": "https://registry.npmjs.org/@types/eslint-scope/-/eslint-scope-3.7.3.tgz", - "integrity": "sha512-PB3ldyrcnAicT35TWPs5IcwKD8S333HMaa2VVv4+wdvebJkjWuW/xESoB8IwRcog8HYVYamb1g/R31Qv5Bx03g==", - "dev": true, - "dependencies": { - "@types/eslint": "*", - "@types/estree": "*" - } - }, - "node_modules/@types/estree": { - "version": "0.0.51", - "resolved": "https://registry.npmjs.org/@types/estree/-/estree-0.0.51.tgz", - "integrity": "sha512-CuPgU6f3eT/XgKKPqKd/gLZV1Xmvf1a2R5POBOGQa6uv82xpls89HU5zKeVoyR8XzHd1RGNOlQlvUe3CFkjWNQ==", - "dev": true - }, - "node_modules/@types/express": { - "version": "4.17.13", - "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.13.tgz", - "integrity": "sha512-6bSZTPaTIACxn48l50SR+axgrqm6qXFIxrdAKaG6PaJk3+zuUr35hBlgT7vOmJcum+OEaIBLtHV/qloEAFITeA==", - "dev": true, - "dependencies": { - "@types/body-parser": "*", - "@types/express-serve-static-core": "^4.17.18", - "@types/qs": "*", - "@types/serve-static": "*" - } - }, - "node_modules/@types/express-serve-static-core": { - "version": "4.17.28", - "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.17.28.tgz", - "integrity": "sha512-P1BJAEAW3E2DJUlkgq4tOL3RyMunoWXqbSCygWo5ZIWTjUgN1YnaXWW4VWl/oc8vs/XoYibEGBKP0uZyF4AHig==", - "dev": true, - "dependencies": { - "@types/node": "*", - "@types/qs": "*", - "@types/range-parser": "*" - } - }, - "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/mime": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.2.tgz", - "integrity": "sha512-YATxVxgRqNH6nHEIsvg6k2Boc1JHI9ZbH5iWFFv/MTkchz3b1ieGDa5T0a9RznNdI0KhVbdbWSN+KWWrQZRxTw==", - "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/qs": { - "version": "6.9.7", - "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.7.tgz", - "integrity": "sha512-FGa1F62FT09qcrueBA6qYTrJPVDzah9a+493+o2PCXsesWHIn27G98TsSMs3WPNbZIEj4+VJf6saSFpvD+3Zsw==", - "dev": true - }, - "node_modules/@types/range-parser": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.4.tgz", - "integrity": "sha512-EEhsLsD6UsDM1yFhAvy0Cjr6VwmpMWqFBCb9w07wVugF7w9nfajxLuVmngTIpgS6svCnm6Vaw+MZhoDCKnOfsw==", - "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/@types/serve-index": { - "version": "1.9.1", - "resolved": "https://registry.npmjs.org/@types/serve-index/-/serve-index-1.9.1.tgz", - "integrity": "sha512-d/Hs3nWDxNL2xAczmOVZNj92YZCS6RGxfBPjKzuu/XirCgXdpKEb88dYNbrYGint6IVWLNP+yonwVAuRC0T2Dg==", - "dev": true, - "dependencies": { - "@types/express": "*" - } - }, - "node_modules/@types/serve-static": { - "version": "1.13.10", - "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.13.10.tgz", - "integrity": "sha512-nCkHGI4w7ZgAdNkrEu0bv+4xNV/XDqW+DydknebMOQwkpDGx8G+HTlj7R7ABI8i8nKxVw0wtKPi1D+lPOkh4YQ==", - "dev": true, - "dependencies": { - "@types/mime": "^1", - "@types/node": "*" - } - }, - "node_modules/@types/sockjs": { - "version": "0.3.33", - "resolved": "https://registry.npmjs.org/@types/sockjs/-/sockjs-0.3.33.tgz", - "integrity": "sha512-f0KEEe05NvUnat+boPTZ0dgaLZ4SfSouXUgv5noUiefG2ajgKjmETo9ZJyuqsl7dfl2aHlLJUiki6B4ZYldiiw==", - "dev": true, - "dependencies": { - "@types/node": "*" - } - }, - "node_modules/@types/ws": { - "version": "8.5.3", - "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.5.3.tgz", - "integrity": "sha512-6YOoWjruKj1uLf3INHH7D3qTXwFfEsg1kf3c0uDdSBJwfa/llkwIjrAGV7j7mVgGNbzTQ3HiHKKDXl6bJPD97w==", - "dev": true, - "dependencies": { - "@types/node": "*" - } - }, - "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.1", - "resolved": "https://registry.npmjs.org/@webpack-cli/configtest/-/configtest-1.1.1.tgz", - "integrity": "sha512-1FBc1f9G4P/AxMqIgfZgeOTuRnwZMten8E7zap5zgpPInnCrP8D4Q81+4CWIch8i/Nf7nXjP0v6CjjbHOrXhKg==", - "dev": true, - "peerDependencies": { - "webpack": "4.x.x || 5.x.x", - "webpack-cli": "4.x.x" - } - }, - "node_modules/@webpack-cli/info": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/@webpack-cli/info/-/info-1.4.1.tgz", - "integrity": "sha512-PKVGmazEq3oAo46Q63tpMr4HipI3OPfP7LiNOEJg963RMgT0rqheag28NCML0o3GIzA3DmxP1ZIAv9oTX1CUIA==", - "dev": true, - "dependencies": { - "envinfo": "^7.7.3" - }, - "peerDependencies": { - "webpack-cli": "4.x.x" - } - }, - "node_modules/@webpack-cli/serve": { - "version": "1.6.1", - "resolved": "https://registry.npmjs.org/@webpack-cli/serve/-/serve-1.6.1.tgz", - "integrity": "sha512-gNGTiTrjEVQ0OcVnzsRSqTxaBSr+dmTfm+qJsCDluky8uhdLWep7Gcr62QsAKHTMxjCS/8nEITsmFAhfIx+QSw==", - "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.11.0", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.11.0.tgz", - "integrity": "sha512-wGgprdCvMalC0BztXvitD2hC04YffAvtsUn93JbGXYLAtCUO4xd17mCCZQxUOItiBwZvJScWo8NIvQMQ71rdpg==", - "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.4", - "resolved": "https://registry.npmjs.org/async/-/async-2.6.4.tgz", - "integrity": "sha512-mzo5dfJYwAn29PeiJ0zvwTo04zj8HDJj0Mn8TD7sno7q12prdbnasKJHhkm2c1LgrhlJ0teaea8860oxi51mGA==", - "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.3", - "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz", - "integrity": "sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==", - "dev": true, - "funding": [ - { - "type": "individual", - "url": "https://paulmillr.com/funding/" - } - ], - "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.9.2", - "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.9.2.tgz", - "integrity": "sha512-GIm3fQfwLJ8YZx2smuHpBKkXC1yOk+OBEmKckVyL0i/ea8mqDEykK3ld5dgH1QYPNyT/lIllxV2LULnxCHaHkA==", - "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.9", - "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.9.tgz", - "integrity": "sha512-NtNxqUcXgpW2iMrfqSfR73Glt39K+BLwWsPs94yR63v45T0Wbej7eRmL5cWfwEgqXnmjQp3zaJTshdRW/qC2ZQ==", - "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/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/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-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": "1.3.1", - "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-1.3.1.tgz", - "integrity": "sha512-dPEtOeMvF9VMcYV/1Wb8CPoVAXtp6MKMlcbAt4ddqmGqUJ6fQZFXkNZNkNlfevtNkGtaSoXf/vNNNSvgrdXwtA==", - "dev": true, - "engines": { - "node": ">= 6.13.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-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-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/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": "2.0.1", - "resolved": "https://registry.npmjs.org/selfsigned/-/selfsigned-2.0.1.tgz", - "integrity": "sha512-LmME957M1zOsUhG+67rAjKfiWFox3SBxE/yymatMZsAx+oMrJ0YQ8AToOnyCm7xbeg2ep37IHLxdu0o2MavQOQ==", - "dev": true, - "dependencies": { - "node-forge": "^1" - }, - "engines": { - "node": ">=10" - } - }, - "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.14.2", - "resolved": "https://registry.npmjs.org/terser/-/terser-5.14.2.tgz", - "integrity": "sha512-oL0rGeM/WFQCUd0y2QrWxYnq7tfSuKBiqTjRPWrRgB46WD/kiwHwF8T23z78H6Q6kGCuuHcPB+KULHRdxvVGQA==", - "dev": true, - "dependencies": { - "@jridgewell/source-map": "^0.3.2", - "acorn": "^8.5.0", - "commander": "^2.20.0", - "source-map-support": "~0.5.20" - }, - "bin": { - "terser": "bin/terser" - }, - "engines": { - "node": ">=10" - } - }, - "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/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/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.3.1", - "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.3.1.tgz", - "integrity": "sha512-x0t0JuydIo8qCNctdDrn1OzH/qDzk2+rdCOC3YzumZ42fiMqmQ7T3xQurykYMhYfHaPHTp4ZxAx2NfUo1K6QaA==", - "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.70.0", - "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.70.0.tgz", - "integrity": "sha512-ZMWWy8CeuTTjCxbeaQI21xSswseF2oNOwc70QSKNePvmxE7XW36i7vpBMYZFAUHPwQiEbNGCEYIOOlyRbdGmxw==", - "dev": true, - "dependencies": { - "@types/eslint-scope": "^3.7.3", - "@types/estree": "^0.0.51", - "@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.9.2", - "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.9", - "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.3.1", - "webpack-sources": "^3.2.3" - }, - "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.2", - "resolved": "https://registry.npmjs.org/webpack-cli/-/webpack-cli-4.9.2.tgz", - "integrity": "sha512-m3/AACnBBzK/kMTcxWHcZFPrw/eQuY4Df1TxvIWfWM2x7mRqBQCqKEd96oCUa9jkapLBaFfRce33eGDb4Pr7YQ==", - "dev": true, - "dependencies": { - "@discoveryjs/json-ext": "^0.5.0", - "@webpack-cli/configtest": "^1.1.1", - "@webpack-cli/info": "^1.4.1", - "@webpack-cli/serve": "^1.6.1", - "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.3.1", - "resolved": "https://registry.npmjs.org/webpack-dev-middleware/-/webpack-dev-middleware-5.3.1.tgz", - "integrity": "sha512-81EujCKkyles2wphtdrnPg/QqegC/AtqNH//mQkBYSMqwFVCQrxM6ktB2O/SPlZy7LqeEfTbV3cZARGQz6umhg==", - "dev": true, - "dependencies": { - "colorette": "^2.0.10", - "memfs": "^3.4.1", - "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.11.0", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.11.0.tgz", - "integrity": "sha512-wGgprdCvMalC0BztXvitD2hC04YffAvtsUn93JbGXYLAtCUO4xd17mCCZQxUOItiBwZvJScWo8NIvQMQ71rdpg==", - "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.4.1", - "resolved": "https://registry.npmjs.org/memfs/-/memfs-3.4.1.tgz", - "integrity": "sha512-1c9VPVvW5P7I85c35zAdEr1TD5+F11IToIHIlrVIcflfnzPkJa0ZoYEoEdYDP8KgPFoSZ/opDrUsAoZWym3mtw==", - "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.7.4", - "resolved": "https://registry.npmjs.org/webpack-dev-server/-/webpack-dev-server-4.7.4.tgz", - "integrity": "sha512-nfdsb02Zi2qzkNmgtZjkrMOcXnYZ6FLKcQwpxT7MvmHKc+oTtDsBju8j+NMyAygZ9GW1jMEUpy3itHtqgEhe1A==", - "dev": true, - "dependencies": { - "@types/bonjour": "^3.5.9", - "@types/connect-history-api-fallback": "^1.3.5", - "@types/express": "^4.17.13", - "@types/serve-index": "^1.9.1", - "@types/sockjs": "^0.3.33", - "@types/ws": "^8.2.2", - "ansi-html-community": "^0.0.8", - "bonjour": "^3.5.0", - "chokidar": "^3.5.3", - "colorette": "^2.0.10", - "compression": "^1.7.4", - "connect-history-api-fallback": "^1.6.0", - "default-gateway": "^6.0.3", - "del": "^6.0.0", - "express": "^4.17.1", - "graceful-fs": "^4.2.6", - "html-entities": "^2.3.2", - "http-proxy-middleware": "^2.0.0", - "ipaddr.js": "^2.0.1", - "open": "^8.0.9", - "p-retry": "^4.5.0", - "portfinder": "^1.0.28", - "schema-utils": "^4.0.0", - "selfsigned": "^2.0.0", - "serve-index": "^1.9.1", - "sockjs": "^0.3.21", - "spdy": "^4.0.2", - "strip-ansi": "^7.0.0", - "webpack-dev-middleware": "^5.3.1", - "ws": "^8.4.2" - }, - "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/ajv": { - "version": "8.11.0", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.11.0.tgz", - "integrity": "sha512-wGgprdCvMalC0BztXvitD2hC04YffAvtsUn93JbGXYLAtCUO4xd17mCCZQxUOItiBwZvJScWo8NIvQMQ71rdpg==", - "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-server/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-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-dev-server/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-server/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-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.3", - "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-3.2.3.tgz", - "integrity": "sha512-/DyMEOrDgLKKIG0fmvtz+4dUX/3Ghozwgm6iPp8KRhvn+eQf9+Q7GWxVNMk3+uCPWfdXYC4ExGBckIXdFEfH1w==", - "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.5.0", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.5.0.tgz", - "integrity": "sha512-BWX0SWVgLPzYwF8lTzEy1egjhS4S4OEAHfsO8o65WOVsrnSRGaSiUaa9e0ggGlkMTtBlmOpEXiie9RUcBO86qg==", - "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 - }, - "@jridgewell/gen-mapping": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.2.tgz", - "integrity": "sha512-mh65xKQAzI6iBcFzwv28KVWSmCkdRBWoOh+bYQGW3+6OZvbbN3TqMGo5hqYxQniRcH9F2VZIoJCm4pa3BPDK/A==", - "dev": true, - "requires": { - "@jridgewell/set-array": "^1.0.1", - "@jridgewell/sourcemap-codec": "^1.4.10", - "@jridgewell/trace-mapping": "^0.3.9" - } - }, - "@jridgewell/resolve-uri": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.0.tgz", - "integrity": "sha512-F2msla3tad+Mfht5cJq7LSXcdudKTWCVYUgw6pLFOOHSTtZlj6SWNYAp+AhuqLmWdBO2X5hPrLcu8cVP8fy28w==", - "dev": true - }, - "@jridgewell/set-array": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.1.2.tgz", - "integrity": "sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw==", - "dev": true - }, - "@jridgewell/source-map": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.2.tgz", - "integrity": "sha512-m7O9o2uR8k2ObDysZYzdfhb08VuEml5oWGiosa1VdaPZ/A6QyPkAJuwN0Q1lhULOf6B7MtQmHENS743hWtCrgw==", - "dev": true, - "requires": { - "@jridgewell/gen-mapping": "^0.3.0", - "@jridgewell/trace-mapping": "^0.3.9" - } - }, - "@jridgewell/sourcemap-codec": { - "version": "1.4.14", - "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.14.tgz", - "integrity": "sha512-XPSJHWmi394fuUuzDnGz1wiKqWfo1yXecHQMRf2l6hztTO+nPru658AyDngaBe7isIxEkRsPR3FZh+s7iVa4Uw==", - "dev": true - }, - "@jridgewell/trace-mapping": { - "version": "0.3.14", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.14.tgz", - "integrity": "sha512-bJWEfQ9lPTvm3SneWwRFVLzrh6nhjwqw7TUFFBEMzwvg7t7PCDenf2lDwqo4NQXzdpgBXyFgDWnQA+2vkruksQ==", - "dev": true, - "requires": { - "@jridgewell/resolve-uri": "^3.0.3", - "@jridgewell/sourcemap-codec": "^1.4.10" - } - }, - "@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/body-parser": { - "version": "1.19.2", - "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.2.tgz", - "integrity": "sha512-ALYone6pm6QmwZoAgeyNksccT9Q4AWZQ6PvfwR37GT6r6FWUPguq6sUmNGSMV2Wr761oQoBxwGGa6DR5o1DC9g==", - "dev": true, - "requires": { - "@types/connect": "*", - "@types/node": "*" - } - }, - "@types/bonjour": { - "version": "3.5.10", - "resolved": "https://registry.npmjs.org/@types/bonjour/-/bonjour-3.5.10.tgz", - "integrity": "sha512-p7ienRMiS41Nu2/igbJxxLDWrSZ0WxM8UQgCeO9KhoVF7cOVFkrKsiDr1EsJIla8vV3oEEjGcz11jc5yimhzZw==", - "dev": true, - "requires": { - "@types/node": "*" - } - }, - "@types/connect": { - "version": "3.4.35", - "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.35.tgz", - "integrity": "sha512-cdeYyv4KWoEgpBISTxWvqYsVy444DOqehiF3fM3ne10AmJ62RSyNkUnxMJXHQWRQQX2eR94m5y1IZyDwBjV9FQ==", - "dev": true, - "requires": { - "@types/node": "*" - } - }, - "@types/connect-history-api-fallback": { - "version": "1.3.5", - "resolved": "https://registry.npmjs.org/@types/connect-history-api-fallback/-/connect-history-api-fallback-1.3.5.tgz", - "integrity": "sha512-h8QJa8xSb1WD4fpKBDcATDNGXghFj6/3GRWG6dhmRcu0RX1Ubasur2Uvx5aeEwlf0MwblEC2bMzzMQntxnw/Cw==", - "dev": true, - "requires": { - "@types/express-serve-static-core": "*", - "@types/node": "*" - } - }, - "@types/eslint": { - "version": "8.4.1", - "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-8.4.1.tgz", - "integrity": "sha512-GE44+DNEyxxh2Kc6ro/VkIj+9ma0pO0bwv9+uHSyBrikYOHr8zYcdPvnBOp1aw8s+CjRvuSx7CyWqRrNFQ59mA==", - "dev": true, - "requires": { - "@types/estree": "*", - "@types/json-schema": "*" - } - }, - "@types/eslint-scope": { - "version": "3.7.3", - "resolved": "https://registry.npmjs.org/@types/eslint-scope/-/eslint-scope-3.7.3.tgz", - "integrity": "sha512-PB3ldyrcnAicT35TWPs5IcwKD8S333HMaa2VVv4+wdvebJkjWuW/xESoB8IwRcog8HYVYamb1g/R31Qv5Bx03g==", - "dev": true, - "requires": { - "@types/eslint": "*", - "@types/estree": "*" - } - }, - "@types/estree": { - "version": "0.0.51", - "resolved": "https://registry.npmjs.org/@types/estree/-/estree-0.0.51.tgz", - "integrity": "sha512-CuPgU6f3eT/XgKKPqKd/gLZV1Xmvf1a2R5POBOGQa6uv82xpls89HU5zKeVoyR8XzHd1RGNOlQlvUe3CFkjWNQ==", - "dev": true - }, - "@types/express": { - "version": "4.17.13", - "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.13.tgz", - "integrity": "sha512-6bSZTPaTIACxn48l50SR+axgrqm6qXFIxrdAKaG6PaJk3+zuUr35hBlgT7vOmJcum+OEaIBLtHV/qloEAFITeA==", - "dev": true, - "requires": { - "@types/body-parser": "*", - "@types/express-serve-static-core": "^4.17.18", - "@types/qs": "*", - "@types/serve-static": "*" - } - }, - "@types/express-serve-static-core": { - "version": "4.17.28", - "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.17.28.tgz", - "integrity": "sha512-P1BJAEAW3E2DJUlkgq4tOL3RyMunoWXqbSCygWo5ZIWTjUgN1YnaXWW4VWl/oc8vs/XoYibEGBKP0uZyF4AHig==", - "dev": true, - "requires": { - "@types/node": "*", - "@types/qs": "*", - "@types/range-parser": "*" - } - }, - "@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/mime": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.2.tgz", - "integrity": "sha512-YATxVxgRqNH6nHEIsvg6k2Boc1JHI9ZbH5iWFFv/MTkchz3b1ieGDa5T0a9RznNdI0KhVbdbWSN+KWWrQZRxTw==", - "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/qs": { - "version": "6.9.7", - "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.7.tgz", - "integrity": "sha512-FGa1F62FT09qcrueBA6qYTrJPVDzah9a+493+o2PCXsesWHIn27G98TsSMs3WPNbZIEj4+VJf6saSFpvD+3Zsw==", - "dev": true - }, - "@types/range-parser": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.4.tgz", - "integrity": "sha512-EEhsLsD6UsDM1yFhAvy0Cjr6VwmpMWqFBCb9w07wVugF7w9nfajxLuVmngTIpgS6svCnm6Vaw+MZhoDCKnOfsw==", - "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 - }, - "@types/serve-index": { - "version": "1.9.1", - "resolved": "https://registry.npmjs.org/@types/serve-index/-/serve-index-1.9.1.tgz", - "integrity": "sha512-d/Hs3nWDxNL2xAczmOVZNj92YZCS6RGxfBPjKzuu/XirCgXdpKEb88dYNbrYGint6IVWLNP+yonwVAuRC0T2Dg==", - "dev": true, - "requires": { - "@types/express": "*" - } - }, - "@types/serve-static": { - "version": "1.13.10", - "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.13.10.tgz", - "integrity": "sha512-nCkHGI4w7ZgAdNkrEu0bv+4xNV/XDqW+DydknebMOQwkpDGx8G+HTlj7R7ABI8i8nKxVw0wtKPi1D+lPOkh4YQ==", - "dev": true, - "requires": { - "@types/mime": "^1", - "@types/node": "*" - } - }, - "@types/sockjs": { - "version": "0.3.33", - "resolved": "https://registry.npmjs.org/@types/sockjs/-/sockjs-0.3.33.tgz", - "integrity": "sha512-f0KEEe05NvUnat+boPTZ0dgaLZ4SfSouXUgv5noUiefG2ajgKjmETo9ZJyuqsl7dfl2aHlLJUiki6B4ZYldiiw==", - "dev": true, - "requires": { - "@types/node": "*" - } - }, - "@types/ws": { - "version": "8.5.3", - "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.5.3.tgz", - "integrity": "sha512-6YOoWjruKj1uLf3INHH7D3qTXwFfEsg1kf3c0uDdSBJwfa/llkwIjrAGV7j7mVgGNbzTQ3HiHKKDXl6bJPD97w==", - "dev": true, - "requires": { - "@types/node": "*" - } - }, - "@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.1", - "resolved": "https://registry.npmjs.org/@webpack-cli/configtest/-/configtest-1.1.1.tgz", - "integrity": "sha512-1FBc1f9G4P/AxMqIgfZgeOTuRnwZMten8E7zap5zgpPInnCrP8D4Q81+4CWIch8i/Nf7nXjP0v6CjjbHOrXhKg==", - "dev": true, - "requires": {} - }, - "@webpack-cli/info": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/@webpack-cli/info/-/info-1.4.1.tgz", - "integrity": "sha512-PKVGmazEq3oAo46Q63tpMr4HipI3OPfP7LiNOEJg963RMgT0rqheag28NCML0o3GIzA3DmxP1ZIAv9oTX1CUIA==", - "dev": true, - "requires": { - "envinfo": "^7.7.3" - } - }, - "@webpack-cli/serve": { - "version": "1.6.1", - "resolved": "https://registry.npmjs.org/@webpack-cli/serve/-/serve-1.6.1.tgz", - "integrity": "sha512-gNGTiTrjEVQ0OcVnzsRSqTxaBSr+dmTfm+qJsCDluky8uhdLWep7Gcr62QsAKHTMxjCS/8nEITsmFAhfIx+QSw==", - "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.11.0", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.11.0.tgz", - "integrity": "sha512-wGgprdCvMalC0BztXvitD2hC04YffAvtsUn93JbGXYLAtCUO4xd17mCCZQxUOItiBwZvJScWo8NIvQMQ71rdpg==", - "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.4", - "resolved": "https://registry.npmjs.org/async/-/async-2.6.4.tgz", - "integrity": "sha512-mzo5dfJYwAn29PeiJ0zvwTo04zj8HDJj0Mn8TD7sno7q12prdbnasKJHhkm2c1LgrhlJ0teaea8860oxi51mGA==", - "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.3", - "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz", - "integrity": "sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==", - "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.9.2", - "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.9.2.tgz", - "integrity": "sha512-GIm3fQfwLJ8YZx2smuHpBKkXC1yOk+OBEmKckVyL0i/ea8mqDEykK3ld5dgH1QYPNyT/lIllxV2LULnxCHaHkA==", - "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.9", - "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.9.tgz", - "integrity": "sha512-NtNxqUcXgpW2iMrfqSfR73Glt39K+BLwWsPs94yR63v45T0Wbej7eRmL5cWfwEgqXnmjQp3zaJTshdRW/qC2ZQ==", - "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==" - }, - "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 - }, - "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-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.1", - "prettier": "2.6.1", - "rollup": "^2.70.0", - "tslib": "^2.3.1", - "typescript": "^4.6.3" - }, - "dependencies": { - "prettier": { - "version": "2.1.2", - "integrity": "sha512-16c7K+x4qVlJg9rEbXl7HEGmQyZlG4R9AgP+oHKRMsMsuk8s+ATStlf1NpDqyBI1HpVyfjLOeMhH2LvuNvV5Vg==", - "dev": true - }, - "typescript": { - "version": "4.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": "1.3.1", - "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-1.3.1.tgz", - "integrity": "sha512-dPEtOeMvF9VMcYV/1Wb8CPoVAXtp6MKMlcbAt4ddqmGqUJ6fQZFXkNZNkNlfevtNkGtaSoXf/vNNNSvgrdXwtA==", - "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-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-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 - }, - "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": "2.0.1", - "resolved": "https://registry.npmjs.org/selfsigned/-/selfsigned-2.0.1.tgz", - "integrity": "sha512-LmME957M1zOsUhG+67rAjKfiWFox3SBxE/yymatMZsAx+oMrJ0YQ8AToOnyCm7xbeg2ep37IHLxdu0o2MavQOQ==", - "dev": true, - "requires": { - "node-forge": "^1" - } - }, - "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.14.2", - "resolved": "https://registry.npmjs.org/terser/-/terser-5.14.2.tgz", - "integrity": "sha512-oL0rGeM/WFQCUd0y2QrWxYnq7tfSuKBiqTjRPWrRgB46WD/kiwHwF8T23z78H6Q6kGCuuHcPB+KULHRdxvVGQA==", - "dev": true, - "requires": { - "@jridgewell/source-map": "^0.3.2", - "acorn": "^8.5.0", - "commander": "^2.20.0", - "source-map-support": "~0.5.20" - } - }, - "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" - } - }, - "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.3.1", - "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.3.1.tgz", - "integrity": "sha512-x0t0JuydIo8qCNctdDrn1OzH/qDzk2+rdCOC3YzumZ42fiMqmQ7T3xQurykYMhYfHaPHTp4ZxAx2NfUo1K6QaA==", - "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.70.0", - "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.70.0.tgz", - "integrity": "sha512-ZMWWy8CeuTTjCxbeaQI21xSswseF2oNOwc70QSKNePvmxE7XW36i7vpBMYZFAUHPwQiEbNGCEYIOOlyRbdGmxw==", - "dev": true, - "requires": { - "@types/eslint-scope": "^3.7.3", - "@types/estree": "^0.0.51", - "@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.9.2", - "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.9", - "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.3.1", - "webpack-sources": "^3.2.3" - } - }, - "webpack-cli": { - "version": "4.9.2", - "resolved": "https://registry.npmjs.org/webpack-cli/-/webpack-cli-4.9.2.tgz", - "integrity": "sha512-m3/AACnBBzK/kMTcxWHcZFPrw/eQuY4Df1TxvIWfWM2x7mRqBQCqKEd96oCUa9jkapLBaFfRce33eGDb4Pr7YQ==", - "dev": true, - "requires": { - "@discoveryjs/json-ext": "^0.5.0", - "@webpack-cli/configtest": "^1.1.1", - "@webpack-cli/info": "^1.4.1", - "@webpack-cli/serve": "^1.6.1", - "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.3.1", - "resolved": "https://registry.npmjs.org/webpack-dev-middleware/-/webpack-dev-middleware-5.3.1.tgz", - "integrity": "sha512-81EujCKkyles2wphtdrnPg/QqegC/AtqNH//mQkBYSMqwFVCQrxM6ktB2O/SPlZy7LqeEfTbV3cZARGQz6umhg==", - "dev": true, - "requires": { - "colorette": "^2.0.10", - "memfs": "^3.4.1", - "mime-types": "^2.1.31", - "range-parser": "^1.2.1", - "schema-utils": "^4.0.0" - }, - "dependencies": { - "ajv": { - "version": "8.11.0", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.11.0.tgz", - "integrity": "sha512-wGgprdCvMalC0BztXvitD2hC04YffAvtsUn93JbGXYLAtCUO4xd17mCCZQxUOItiBwZvJScWo8NIvQMQ71rdpg==", - "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.4.1", - "resolved": "https://registry.npmjs.org/memfs/-/memfs-3.4.1.tgz", - "integrity": "sha512-1c9VPVvW5P7I85c35zAdEr1TD5+F11IToIHIlrVIcflfnzPkJa0ZoYEoEdYDP8KgPFoSZ/opDrUsAoZWym3mtw==", - "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.7.4", - "resolved": "https://registry.npmjs.org/webpack-dev-server/-/webpack-dev-server-4.7.4.tgz", - "integrity": "sha512-nfdsb02Zi2qzkNmgtZjkrMOcXnYZ6FLKcQwpxT7MvmHKc+oTtDsBju8j+NMyAygZ9GW1jMEUpy3itHtqgEhe1A==", - "dev": true, - "requires": { - "@types/bonjour": "^3.5.9", - "@types/connect-history-api-fallback": "^1.3.5", - "@types/express": "^4.17.13", - "@types/serve-index": "^1.9.1", - "@types/sockjs": "^0.3.33", - "@types/ws": "^8.2.2", - "ansi-html-community": "^0.0.8", - "bonjour": "^3.5.0", - "chokidar": "^3.5.3", - "colorette": "^2.0.10", - "compression": "^1.7.4", - "connect-history-api-fallback": "^1.6.0", - "default-gateway": "^6.0.3", - "del": "^6.0.0", - "express": "^4.17.1", - "graceful-fs": "^4.2.6", - "html-entities": "^2.3.2", - "http-proxy-middleware": "^2.0.0", - "ipaddr.js": "^2.0.1", - "open": "^8.0.9", - "p-retry": "^4.5.0", - "portfinder": "^1.0.28", - "schema-utils": "^4.0.0", - "selfsigned": "^2.0.0", - "serve-index": "^1.9.1", - "sockjs": "^0.3.21", - "spdy": "^4.0.2", - "strip-ansi": "^7.0.0", - "webpack-dev-middleware": "^5.3.1", - "ws": "^8.4.2" - }, - "dependencies": { - "ajv": { - "version": "8.11.0", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.11.0.tgz", - "integrity": "sha512-wGgprdCvMalC0BztXvitD2hC04YffAvtsUn93JbGXYLAtCUO4xd17mCCZQxUOItiBwZvJScWo8NIvQMQ71rdpg==", - "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" - } - }, - "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 - }, - "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 - }, - "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-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.3", - "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-3.2.3.tgz", - "integrity": "sha512-/DyMEOrDgLKKIG0fmvtz+4dUX/3Ghozwgm6iPp8KRhvn+eQf9+Q7GWxVNMk3+uCPWfdXYC4ExGBckIXdFEfH1w==", - "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.5.0", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.5.0.tgz", - "integrity": "sha512-BWX0SWVgLPzYwF8lTzEy1egjhS4S4OEAHfsO8o65WOVsrnSRGaSiUaa9e0ggGlkMTtBlmOpEXiie9RUcBO86qg==", - "dev": true, - "requires": {} - } - } -} diff --git a/Example/package.json b/Example/package.json deleted file mode 100644 index 52c72a2a4..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.70.0", - "webpack-cli": "^4.9.2", - "webpack-dev-server": "^4.7.4" - }, - "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/Example/JavaScriptKitExample/.gitignore b/Examples/Basic/.gitignore similarity index 100% rename from Example/JavaScriptKitExample/.gitignore rename to Examples/Basic/.gitignore diff --git a/Example/JavaScriptKitExample/Package.swift b/Examples/Basic/Package.swift similarity index 55% rename from Example/JavaScriptKitExample/Package.swift rename to Examples/Basic/Package.swift index 35b08ed25..6484043f5 100644 --- a/Example/JavaScriptKitExample/Package.swift +++ b/Examples/Basic/Package.swift @@ -1,18 +1,13 @@ -// swift-tools-version:5.7 +// swift-tools-version:5.10 import PackageDescription let package = Package( - name: "JavaScriptKitExample", - products: [ - .executable( - name: "JavaScriptKitExample", targets: ["JavaScriptKitExample"] - ), - ], + name: "Basic", dependencies: [.package(name: "JavaScriptKit", path: "../../")], targets: [ - .target( - name: "JavaScriptKitExample", + .executableTarget( + name: "Basic", dependencies: [ "JavaScriptKit", .product(name: "JavaScriptEventLoop", package: "JavaScriptKit") 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/Example/JavaScriptKitExample/Sources/JavaScriptKitExample/main.swift b/Examples/Basic/Sources/main.swift similarity index 100% rename from Example/JavaScriptKitExample/Sources/JavaScriptKitExample/main.swift rename to Examples/Basic/Sources/main.swift diff --git a/Examples/Basic/build.sh b/Examples/Basic/build.sh new file mode 100755 index 000000000..f92c05639 --- /dev/null +++ b/Examples/Basic/build.sh @@ -0,0 +1 @@ +swift build --swift-sdk DEVELOPMENT-SNAPSHOT-2024-07-09-a-wasm32-unknown-wasi -Xswiftc -Xclang-linker -Xswiftc -mexec-model=reactor -Xlinker --export=__main_argc_argv diff --git a/Examples/Basic/index.html b/Examples/Basic/index.html new file mode 100644 index 000000000..d94796a09 --- /dev/null +++ b/Examples/Basic/index.html @@ -0,0 +1,12 @@ + + + + + Getting Started + + + + + + + diff --git a/Examples/Basic/index.js b/Examples/Basic/index.js new file mode 100644 index 000000000..e90769aa5 --- /dev/null +++ b/Examples/Basic/index.js @@ -0,0 +1,33 @@ +import { WASI, File, OpenFile, ConsoleStdout, PreopenDirectory } from 'https://esm.run/@bjorn3/browser_wasi_shim@0.3.0'; + +async function main(configuration = "debug") { + // Fetch our Wasm File + const response = await fetch(`./.build/${configuration}/Basic.wasm`); + // Create a new WASI system instance + const wasi = new WASI(/* args */["main.wasm"], /* env */[], /* fd */[ + new OpenFile(new File([])), // stdin + ConsoleStdout.lineBuffered((stdout) => { + console.log(stdout); + }), + ConsoleStdout.lineBuffered((stderr) => { + console.error(stderr); + }), + new PreopenDirectory("/", new Map()), + ]) + const { SwiftRuntime } = await import(`./.build/${configuration}/JavaScriptKit_JavaScriptKit.resources/Runtime/index.mjs`); + // Create a new Swift Runtime instance to interact with JS and Swift + const swift = new SwiftRuntime(); + // Instantiate the WebAssembly file + const { instance } = await WebAssembly.instantiateStreaming(response, { + wasi_snapshot_preview1: wasi.wasiImport, + javascript_kit: swift.wasmImports, + }); + // Set the WebAssembly instance to the Swift Runtime + swift.setInstance(instance); + // Start the WebAssembly WASI reactor instance + wasi.initialize(instance); + // Start Swift main function + swift.main() +}; + +main(); From 539ce7eca3004375906a426246e7734a146532c1 Mon Sep 17 00:00:00 2001 From: Yuta Saito Date: Fri, 12 Jul 2024 12:16:04 +0900 Subject: [PATCH 131/373] Add multi-threading example --- Examples/Multithreading/.gitignore | 8 ++ Examples/Multithreading/Package.resolved | 14 ++ Examples/Multithreading/Package.swift | 21 +++ Examples/Multithreading/README.md | 9 ++ .../Sources/JavaScript/index.js | 74 ++++++++++ .../Sources/JavaScript/instantiate.js | 29 ++++ .../Sources/JavaScript/worker.js | 28 ++++ .../Multithreading/Sources/MyApp/Scene.swift | 93 +++++++++++++ .../Multithreading/Sources/MyApp/main.swift | 128 ++++++++++++++++++ Examples/Multithreading/build.sh | 1 + Examples/Multithreading/index.html | 19 +++ Examples/Multithreading/serve.json | 14 ++ 12 files changed, 438 insertions(+) create mode 100644 Examples/Multithreading/.gitignore create mode 100644 Examples/Multithreading/Package.resolved create mode 100644 Examples/Multithreading/Package.swift create mode 100644 Examples/Multithreading/README.md create mode 100644 Examples/Multithreading/Sources/JavaScript/index.js create mode 100644 Examples/Multithreading/Sources/JavaScript/instantiate.js create mode 100644 Examples/Multithreading/Sources/JavaScript/worker.js create mode 100644 Examples/Multithreading/Sources/MyApp/Scene.swift create mode 100644 Examples/Multithreading/Sources/MyApp/main.swift create mode 100755 Examples/Multithreading/build.sh create mode 100644 Examples/Multithreading/index.html create mode 100644 Examples/Multithreading/serve.json diff --git a/Examples/Multithreading/.gitignore b/Examples/Multithreading/.gitignore new file mode 100644 index 000000000..0023a5340 --- /dev/null +++ b/Examples/Multithreading/.gitignore @@ -0,0 +1,8 @@ +.DS_Store +/.build +/Packages +xcuserdata/ +DerivedData/ +.swiftpm/configuration/registries.json +.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata +.netrc diff --git a/Examples/Multithreading/Package.resolved b/Examples/Multithreading/Package.resolved new file mode 100644 index 000000000..1354cc039 --- /dev/null +++ b/Examples/Multithreading/Package.resolved @@ -0,0 +1,14 @@ +{ + "originHash" : "e66f4c272838a860049b7e3528f1db03ee6ae99c2b21c3b6ea58a293be4db41b", + "pins" : [ + { + "identity" : "chibi-ray", + "kind" : "remoteSourceControl", + "location" : "https://github.com/kateinoigakukun/chibi-ray", + "state" : { + "revision" : "c8cab621a3338dd2f8e817d3785362409d3b8cf1" + } + } + ], + "version" : 3 +} diff --git a/Examples/Multithreading/Package.swift b/Examples/Multithreading/Package.swift new file mode 100644 index 000000000..91113a429 --- /dev/null +++ b/Examples/Multithreading/Package.swift @@ -0,0 +1,21 @@ +// swift-tools-version: 6.0 + +import PackageDescription + +let package = Package( + name: "Example", + 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..c95df2a8b --- /dev/null +++ b/Examples/Multithreading/README.md @@ -0,0 +1,9 @@ +# Multithreading 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-wasip1-threads.artifactbundle.zip +$ ./build.sh +$ npx serve +``` diff --git a/Examples/Multithreading/Sources/JavaScript/index.js b/Examples/Multithreading/Sources/JavaScript/index.js new file mode 100644 index 000000000..cc0c7e4e4 --- /dev/null +++ b/Examples/Multithreading/Sources/JavaScript/index.js @@ -0,0 +1,74 @@ +import { instantiate } from "./instantiate.js" +import * as WasmImportsParser from 'https://esm.run/wasm-imports-parser/polyfill.js'; + +// TODO: Remove this polyfill once the browser supports the WebAssembly Type Reflection JS API +// https://chromestatus.com/feature/5725002447978496 +globalThis.WebAssembly = WasmImportsParser.polyfill(globalThis.WebAssembly); + +class ThreadRegistry { + workers = new Map(); + nextTid = 1; + + constructor({ configuration }) { + this.configuration = configuration; + } + + spawnThread(worker, module, memory, startArg) { + const tid = this.nextTid++; + this.workers.set(tid, worker); + worker.postMessage({ module, memory, tid, startArg, configuration: this.configuration }); + return tid; + } + + listenMessageFromWorkerThread(tid, listener) { + const worker = this.workers.get(tid); + worker.onmessage = (event) => { + listener(event.data); + }; + } + + postMessageToWorkerThread(tid, data) { + const worker = this.workers.get(tid); + worker.postMessage(data); + } + + terminateWorkerThread(tid) { + const worker = this.workers.get(tid); + worker.terminate(); + this.workers.delete(tid); + } +} + +async function start(configuration = "release") { + const response = await fetch(`./.build/${configuration}/MyApp.wasm`); + const module = await WebAssembly.compileStreaming(response); + const memoryImport = WebAssembly.Module.imports(module).find(i => i.module === "env" && i.name === "memory"); + if (!memoryImport) { + throw new Error("Memory import not found"); + } + if (!memoryImport.type) { + throw new Error("Memory import type not found"); + } + const memoryType = memoryImport.type; + const memory = new WebAssembly.Memory({ initial: memoryType.minimum, maximum: memoryType.maximum, shared: true }); + const threads = new ThreadRegistry({ configuration }); + const { instance, swiftRuntime, wasi } = await instantiate({ + module, + threadChannel: threads, + addToImports(importObject) { + importObject["env"] = { memory } + importObject["wasi"] = { + "thread-spawn": (startArg) => { + const worker = new Worker("Sources/JavaScript/worker.js", { type: "module" }); + return threads.spawnThread(worker, module, memory, startArg); + } + }; + }, + configuration + }); + wasi.initialize(instance); + + swiftRuntime.main(); +} + +start(); diff --git a/Examples/Multithreading/Sources/JavaScript/instantiate.js b/Examples/Multithreading/Sources/JavaScript/instantiate.js new file mode 100644 index 000000000..e7b60504c --- /dev/null +++ b/Examples/Multithreading/Sources/JavaScript/instantiate.js @@ -0,0 +1,29 @@ +import { WASI, File, OpenFile, ConsoleStdout, PreopenDirectory } from 'https://esm.run/@bjorn3/browser_wasi_shim@0.3.0'; + +export async function instantiate({ module, addToImports, threadChannel, configuration }) { + const args = ["main.wasm"] + const env = [] + const fds = [ + new OpenFile(new File([])), // stdin + ConsoleStdout.lineBuffered((stdout) => { + console.log(stdout); + }), + ConsoleStdout.lineBuffered((stderr) => { + console.error(stderr); + }), + new PreopenDirectory("/", new Map()), + ]; + const wasi = new WASI(args, env, fds); + + const { SwiftRuntime } = await import(`/.build/${configuration}/JavaScriptKit_JavaScriptKit.resources/Runtime/index.mjs`); + const swiftRuntime = new SwiftRuntime({ sharedMemory: true, threadChannel }); + const importObject = { + wasi_snapshot_preview1: wasi.wasiImport, + javascript_kit: swiftRuntime.wasmImports, + }; + addToImports(importObject); + const instance = await WebAssembly.instantiate(module, importObject); + + swiftRuntime.setInstance(instance); + return { swiftRuntime, wasi, instance }; +} diff --git a/Examples/Multithreading/Sources/JavaScript/worker.js b/Examples/Multithreading/Sources/JavaScript/worker.js new file mode 100644 index 000000000..eadd42bef --- /dev/null +++ b/Examples/Multithreading/Sources/JavaScript/worker.js @@ -0,0 +1,28 @@ +import { instantiate } from "./instantiate.js" + +self.onmessage = async (event) => { + const { module, memory, tid, startArg, configuration } = event.data; + const { instance, wasi, swiftRuntime } = await instantiate({ + module, + threadChannel: { + postMessageToMainThread: (message) => { + // Send the job to the main thread + postMessage(message); + }, + listenMessageFromMainThread: (listener) => { + self.onmessage = (event) => listener(event.data); + } + }, + addToImports(importObject) { + importObject["env"] = { memory } + importObject["wasi"] = { + "thread-spawn": () => { throw new Error("Cannot spawn a new thread from a worker thread"); } + }; + }, + configuration + }); + + swiftRuntime.setInstance(instance); + wasi.inst = instance; + swiftRuntime.startThread(tid, startArg); +} diff --git a/Examples/Multithreading/Sources/MyApp/Scene.swift b/Examples/Multithreading/Sources/MyApp/Scene.swift new file mode 100644 index 000000000..e38857d5e --- /dev/null +++ b/Examples/Multithreading/Sources/MyApp/Scene.swift @@ -0,0 +1,93 @@ +import ChibiRay + +func createDemoScene() -> Scene { + return Scene( + width: 800, + height: 800, + 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..2317e9329 --- /dev/null +++ b/Examples/Multithreading/Sources/MyApp/main.swift @@ -0,0 +1,128 @@ +import ChibiRay +import JavaScriptKit +import JavaScriptEventLoop + +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 + + var checkTimer: JSValue? + checkTimer = JSObject.global.setInterval!(JSClosure { _ in + print("Checking thread work...") + renderInCanvas(ctx: ctx, image: imageView) + let renderSceneDuration = clock.now - start + renderTime.textContent = .string("Render time: \(renderSceneDuration)") + return .undefined + }, 250) + + await withTaskGroup(of: Void.self) { group in + let yStride = scene.height / concurrency + for i in 0.. + + + 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" + } + ] + }] +} From 0a673faede8aa51304c93478e7a71bf2a0b9e0f4 Mon Sep 17 00:00:00 2001 From: Yuta Saito Date: Fri, 12 Jul 2024 12:17:15 +0900 Subject: [PATCH 132/373] Update CI configuration for the new example directory --- .github/workflows/compatibility.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/compatibility.yml b/.github/workflows/compatibility.yml index 85783d730..8e62b8e94 100644 --- a/.github/workflows/compatibility.yml +++ b/.github/workflows/compatibility.yml @@ -17,6 +17,6 @@ jobs: run: | set -eux make bootstrap - cd Example/JavaScriptKitExample + cd Examples/Basic swift build --triple wasm32-unknown-wasi swift build --triple wasm32-unknown-wasi -Xswiftc -DJAVASCRIPTKIT_WITHOUT_WEAKREFS From 7e2316915b538b0ae7fed1399d011edd79dfadd7 Mon Sep 17 00:00:00 2001 From: Yuta Saito Date: Fri, 12 Jul 2024 12:19:26 +0900 Subject: [PATCH 133/373] Update toolchain version used to build the examples --- .github/workflows/compatibility.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/compatibility.yml b/.github/workflows/compatibility.yml index 8e62b8e94..e16785157 100644 --- a/.github/workflows/compatibility.yml +++ b/.github/workflows/compatibility.yml @@ -12,7 +12,7 @@ jobs: uses: actions/checkout@v4 - uses: swiftwasm/setup-swiftwasm@v1 with: - swift-version: wasm-5.9.1-RELEASE + swift-version: wasm-5.10.0-RELEASE - name: Run Test run: | set -eux From a3a0a47bdc7e04d3fa10c515169e2cf2595ea1d6 Mon Sep 17 00:00:00 2001 From: Yuta Saito Date: Tue, 16 Jul 2024 11:45:59 +0900 Subject: [PATCH 134/373] Update multithreading example to be compatible with older Xcode --- Examples/Multithreading/Package.swift | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Examples/Multithreading/Package.swift b/Examples/Multithreading/Package.swift index 91113a429..211f359a6 100644 --- a/Examples/Multithreading/Package.swift +++ b/Examples/Multithreading/Package.swift @@ -1,9 +1,10 @@ -// swift-tools-version: 6.0 +// 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"), From 502da6e0c27e42ace845bd8caf93b43a87066c35 Mon Sep 17 00:00:00 2001 From: Yuta Saito Date: Tue, 16 Jul 2024 12:55:00 +0900 Subject: [PATCH 135/373] Refactor multi-threading example to be able to switch background/forground --- .../Multithreading/Sources/MyApp/Scene.swift | 6 +- .../Multithreading/Sources/MyApp/main.swift | 75 +++++++++++++------ Examples/Multithreading/index.html | 65 ++++++++++++---- 3 files changed, 107 insertions(+), 39 deletions(-) diff --git a/Examples/Multithreading/Sources/MyApp/Scene.swift b/Examples/Multithreading/Sources/MyApp/Scene.swift index e38857d5e..5f6d467c5 100644 --- a/Examples/Multithreading/Sources/MyApp/Scene.swift +++ b/Examples/Multithreading/Sources/MyApp/Scene.swift @@ -1,9 +1,9 @@ import ChibiRay -func createDemoScene() -> Scene { +func createDemoScene(size: Int) -> Scene { return Scene( - width: 800, - height: 800, + width: size, + height: size, fov: 90, elements: [ .sphere( diff --git a/Examples/Multithreading/Sources/MyApp/main.swift b/Examples/Multithreading/Sources/MyApp/main.swift index 2317e9329..29cb89f2f 100644 --- a/Examples/Multithreading/Sources/MyApp/main.swift +++ b/Examples/Multithreading/Sources/MyApp/main.swift @@ -57,7 +57,7 @@ struct Work: Sendable { } } -func render(scene: Scene, ctx: JSObject, renderTime: JSObject, concurrency: Int, executor: WebWorkerTaskExecutor) async { +func render(scene: Scene, ctx: JSObject, renderTimeElement: JSObject, concurrency: Int, executor: (some TaskExecutor)?) async { let imageBuffer = UnsafeMutableBufferPointer.allocate(capacity: scene.width * scene.height) // Initialize the buffer with black color @@ -67,12 +67,16 @@ func render(scene: Scene, ctx: JSObject, renderTime: JSObject, concurrency: Int, 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) - let renderSceneDuration = clock.now - start - renderTime.textContent = .string("Render time: \(renderSceneDuration)") + updateRenderTime() return .undefined }, 250) @@ -94,30 +98,41 @@ func render(scene: Scene, ctx: JSObject, renderTime: JSObject, concurrency: Int, checkTimer = nil renderInCanvas(ctx: ctx, image: imageView) + updateRenderTime() imageBuffer.deallocate() print("All work done") } +func onClick() async throws { + let document = JSObject.global.document + + let canvasElement = document.getElementById("canvas").object! + let renderTimeElement = document.getElementById("render-time").object! + + let concurrency = max(Int(document.getElementById("concurrency").object!.value.string!) ?? 1, 1) + let background = document.getElementById("background").object!.checked.boolean! + let size = Int(document.getElementById("size").object!.value.string ?? "800")! + + let ctx = canvasElement.getContext!("2d").object! + + let scene = createDemoScene(size: size) + let executor = background ? try await WebWorkerTaskExecutor(numberOfThreads: concurrency) : nil + canvasElement.width = .number(Double(scene.width)) + canvasElement.height = .number(Double(scene.height)) + + await render(scene: scene, ctx: ctx, renderTimeElement: renderTimeElement, concurrency: concurrency, executor: executor) + executor?.terminate() + print("Render done") +} + func main() async throws { - let canvas = JSObject.global.document.getElementById("canvas").object! - let renderButton = JSObject.global.document.getElementById("render-button").object! - let concurrency = JSObject.global.document.getElementById("concurrency").object! - concurrency.value = JSObject.global.navigator.hardwareConcurrency - let scene = createDemoScene() - canvas.width = .number(Double(scene.width)) - canvas.height = .number(Double(scene.height)) - - _ = renderButton.addEventListener!("click", JSClosure { _ in + let renderButtonElement = JSObject.global.document.getElementById("render-button").object! + let concurrencyElement = JSObject.global.document.getElementById("concurrency").object! + concurrencyElement.value = JSObject.global.navigator.hardwareConcurrency + + _ = renderButtonElement.addEventListener!("click", JSClosure { _ in Task { - let canvas = JSObject.global.document.getElementById("canvas").object! - let renderTime = JSObject.global.document.getElementById("render-time").object! - let concurrency = JSObject.global.document.getElementById("concurrency").object! - let concurrencyValue = max(Int(concurrency.value.string!) ?? 1, 1) - let ctx = canvas.getContext!("2d").object! - let executor = try await WebWorkerTaskExecutor(numberOfThreads: concurrencyValue) - await render(scene: scene, ctx: ctx, renderTime: renderTime, concurrency: concurrencyValue, executor: executor) - executor.terminate() - print("Render done") + try await onClick() } return JSValue.undefined }) @@ -126,3 +141,21 @@ func main() async throws { 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/Multithreading/index.html b/Examples/Multithreading/index.html index bb4970707..6ed31039d 100644 --- a/Examples/Multithreading/index.html +++ b/Examples/Multithreading/index.html @@ -1,19 +1,54 @@ - - - Threading Example - - - -

Threading Example

+ + + + Threading Example + + + + + + +

Threading Example

+

-

- - - -

-

+ +
- - +
+ + +
+
+ + + +
+

+

+

🧵
+

+

+ + + From 396ff4519d30561d9a7589dfb022739af2e1a9e6 Mon Sep 17 00:00:00 2001 From: Yuta Saito Date: Tue, 16 Jul 2024 13:47:27 +0900 Subject: [PATCH 136/373] Use try-lock to append a job to the worker queue This change uses `withLockIfAvailable` to append a job to the worker queue not to use `atomic.wait` on the main thread, which is rejected by some browser engines. --- .../WebWorkerTaskExecutor.swift | 55 ++++++++++--------- 1 file changed, 29 insertions(+), 26 deletions(-) diff --git a/Sources/JavaScriptEventLoop/WebWorkerTaskExecutor.swift b/Sources/JavaScriptEventLoop/WebWorkerTaskExecutor.swift index 170cb64d9..5f382e2bc 100644 --- a/Sources/JavaScriptEventLoop/WebWorkerTaskExecutor.swift +++ b/Sources/JavaScriptEventLoop/WebWorkerTaskExecutor.swift @@ -138,34 +138,37 @@ public final class WebWorkerTaskExecutor: TaskExecutor { /// Enqueue a job to the worker. func enqueue(_ job: UnownedJob) { statsIncrement(\.enqueuedJobs) - jobQueue.withLock { 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) + 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 } - 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() { From 9f4e95edf71741cd8acad190d887a8306a2cc043 Mon Sep 17 00:00:00 2001 From: Francisco Javier Trujillo Mata Date: Tue, 30 Jul 2024 12:40:26 +0200 Subject: [PATCH 137/373] Update macro conditions for runtime usage --- Sources/JavaScriptEventLoop/JavaScriptEventLoop.swift | 2 +- Sources/JavaScriptEventLoop/WebWorkerTaskExecutor.swift | 2 +- Tests/JavaScriptEventLoopTests/WebWorkerTaskExecutorTests.swift | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Sources/JavaScriptEventLoop/JavaScriptEventLoop.swift b/Sources/JavaScriptEventLoop/JavaScriptEventLoop.swift index e1e023e7f..765746bb1 100644 --- a/Sources/JavaScriptEventLoop/JavaScriptEventLoop.swift +++ b/Sources/JavaScriptEventLoop/JavaScriptEventLoop.swift @@ -61,7 +61,7 @@ public final class JavaScriptEventLoop: SerialExecutor, @unchecked Sendable { return _shared } - #if compiler(>=6.0) && _runtime(_multithreaded) + #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. diff --git a/Sources/JavaScriptEventLoop/WebWorkerTaskExecutor.swift b/Sources/JavaScriptEventLoop/WebWorkerTaskExecutor.swift index 5f382e2bc..50aec7417 100644 --- a/Sources/JavaScriptEventLoop/WebWorkerTaskExecutor.swift +++ b/Sources/JavaScriptEventLoop/WebWorkerTaskExecutor.swift @@ -1,4 +1,4 @@ -#if compiler(>=6.0) && _runtime(_multithreaded) // @_expose and @_extern are only available in Swift 6.0+ +#if compiler(>=6.1) && _runtime(_multithreaded) // @_expose and @_extern are only available in Swift 6.0+ import JavaScriptKit import _CJavaScriptKit diff --git a/Tests/JavaScriptEventLoopTests/WebWorkerTaskExecutorTests.swift b/Tests/JavaScriptEventLoopTests/WebWorkerTaskExecutorTests.swift index 94e7635e4..a31c783d3 100644 --- a/Tests/JavaScriptEventLoopTests/WebWorkerTaskExecutorTests.swift +++ b/Tests/JavaScriptEventLoopTests/WebWorkerTaskExecutorTests.swift @@ -1,4 +1,4 @@ -#if compiler(>=6.0) && _runtime(_multithreaded) +#if compiler(>=6.1) && _runtime(_multithreaded) import XCTest import JavaScriptKit import _CJavaScriptKit // For swjs_get_worker_thread_id From 8611b7ae3dbf563f52bd00f06420c1eb3fe2f905 Mon Sep 17 00:00:00 2001 From: Yuta Saito Date: Tue, 30 Jul 2024 19:50:41 +0900 Subject: [PATCH 138/373] Distinguish 6.0 and main toolchains by `hasFeature(Extern)` The main toolchain still says it's 6.0, so we can' distinguish them by `#if compiler`. The feature is baseline in the main branch but not in the 6.0, so we can use it for now. --- Sources/JavaScriptEventLoop/JavaScriptEventLoop.swift | 2 +- Sources/JavaScriptEventLoop/WebWorkerTaskExecutor.swift | 2 +- Tests/JavaScriptEventLoopTests/WebWorkerTaskExecutorTests.swift | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Sources/JavaScriptEventLoop/JavaScriptEventLoop.swift b/Sources/JavaScriptEventLoop/JavaScriptEventLoop.swift index 765746bb1..4bdc8e899 100644 --- a/Sources/JavaScriptEventLoop/JavaScriptEventLoop.swift +++ b/Sources/JavaScriptEventLoop/JavaScriptEventLoop.swift @@ -61,7 +61,7 @@ public final class JavaScriptEventLoop: SerialExecutor, @unchecked Sendable { return _shared } - #if compiler(>=6.1) && _runtime(_multithreaded) + #if compiler(>=6.0) && hasFeature(Extern) && _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. diff --git a/Sources/JavaScriptEventLoop/WebWorkerTaskExecutor.swift b/Sources/JavaScriptEventLoop/WebWorkerTaskExecutor.swift index 50aec7417..c056fd2ad 100644 --- a/Sources/JavaScriptEventLoop/WebWorkerTaskExecutor.swift +++ b/Sources/JavaScriptEventLoop/WebWorkerTaskExecutor.swift @@ -1,4 +1,4 @@ -#if compiler(>=6.1) && _runtime(_multithreaded) // @_expose and @_extern are only available in Swift 6.0+ +#if compiler(>=6.0) && hasFeature(Extern) && _runtime(_multithreaded) // @_expose and @_extern are only available in Swift 6.0+ import JavaScriptKit import _CJavaScriptKit diff --git a/Tests/JavaScriptEventLoopTests/WebWorkerTaskExecutorTests.swift b/Tests/JavaScriptEventLoopTests/WebWorkerTaskExecutorTests.swift index a31c783d3..c331db8ec 100644 --- a/Tests/JavaScriptEventLoopTests/WebWorkerTaskExecutorTests.swift +++ b/Tests/JavaScriptEventLoopTests/WebWorkerTaskExecutorTests.swift @@ -1,4 +1,4 @@ -#if compiler(>=6.1) && _runtime(_multithreaded) +#if compiler(>=6.0) && hasFeature(Extern) && _runtime(_multithreaded) import XCTest import JavaScriptKit import _CJavaScriptKit // For swjs_get_worker_thread_id From b7f67c6260fda5688ec8201c73c0fd6a190f4b9b Mon Sep 17 00:00:00 2001 From: Francisco Javier Trujillo Mata Date: Tue, 30 Jul 2024 14:05:08 +0200 Subject: [PATCH 139/373] Using IsolatedAny2 instead Extern --- Sources/JavaScriptEventLoop/JavaScriptEventLoop.swift | 2 +- Sources/JavaScriptEventLoop/WebWorkerTaskExecutor.swift | 2 +- Tests/JavaScriptEventLoopTests/WebWorkerTaskExecutorTests.swift | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Sources/JavaScriptEventLoop/JavaScriptEventLoop.swift b/Sources/JavaScriptEventLoop/JavaScriptEventLoop.swift index 4bdc8e899..871f374e2 100644 --- a/Sources/JavaScriptEventLoop/JavaScriptEventLoop.swift +++ b/Sources/JavaScriptEventLoop/JavaScriptEventLoop.swift @@ -61,7 +61,7 @@ public final class JavaScriptEventLoop: SerialExecutor, @unchecked Sendable { return _shared } - #if compiler(>=6.0) && hasFeature(Extern) && _runtime(_multithreaded) + #if compiler(>=6.0) && hasFeature(IsolatedAny2) && _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. diff --git a/Sources/JavaScriptEventLoop/WebWorkerTaskExecutor.swift b/Sources/JavaScriptEventLoop/WebWorkerTaskExecutor.swift index c056fd2ad..dad1f959f 100644 --- a/Sources/JavaScriptEventLoop/WebWorkerTaskExecutor.swift +++ b/Sources/JavaScriptEventLoop/WebWorkerTaskExecutor.swift @@ -1,4 +1,4 @@ -#if compiler(>=6.0) && hasFeature(Extern) && _runtime(_multithreaded) // @_expose and @_extern are only available in Swift 6.0+ +#if compiler(>=6.0) && hasFeature(IsolatedAny2) && _runtime(_multithreaded) // @_expose and @_extern are only available in Swift 6.0+ import JavaScriptKit import _CJavaScriptKit diff --git a/Tests/JavaScriptEventLoopTests/WebWorkerTaskExecutorTests.swift b/Tests/JavaScriptEventLoopTests/WebWorkerTaskExecutorTests.swift index c331db8ec..589480cb9 100644 --- a/Tests/JavaScriptEventLoopTests/WebWorkerTaskExecutorTests.swift +++ b/Tests/JavaScriptEventLoopTests/WebWorkerTaskExecutorTests.swift @@ -1,4 +1,4 @@ -#if compiler(>=6.0) && hasFeature(Extern) && _runtime(_multithreaded) +#if compiler(>=6.0) && hasFeature(IsolatedAny2) && _runtime(_multithreaded) import XCTest import JavaScriptKit import _CJavaScriptKit // For swjs_get_worker_thread_id From 359b2a9ebff8e2996156782a00f2e481fcfa93d4 Mon Sep 17 00:00:00 2001 From: Yuta Saito Date: Wed, 31 Jul 2024 12:33:34 +0900 Subject: [PATCH 140/373] Soft-fail integer conversion from JS values that are not representable Close https://github.com/swiftwasm/JavaScriptKit/issues/258 --- .../Sources/PrimaryTests/main.swift | 16 +++++ .../ConstructibleFromJSValue.swift | 60 +++++++++++++++++-- 2 files changed, 72 insertions(+), 4 deletions(-) diff --git a/IntegrationTests/TestSuites/Sources/PrimaryTests/main.swift b/IntegrationTests/TestSuites/Sources/PrimaryTests/main.swift index 716151034..4fedf6f4c 100644 --- a/IntegrationTests/TestSuites/Sources/PrimaryTests/main.swift +++ b/IntegrationTests/TestSuites/Sources/PrimaryTests/main.swift @@ -81,6 +81,22 @@ try test("Value Construction") { 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) + + for source: JSValue in [ + .number(.infinity), .number(.nan), + .number(Double(UInt64.max).nextUp), .number(Double(Int64.min).nextDown) + ] { + try expectNil(Int.construct(from: source)) + try expectNil(Int8.construct(from: source)) + try expectNil(Int16.construct(from: source)) + try expectNil(Int32.construct(from: source)) + try expectNil(Int64.construct(from: source)) + try expectNil(UInt.construct(from: source)) + try expectNil(UInt8.construct(from: source)) + try expectNil(UInt16.construct(from: source)) + try expectNil(UInt32.construct(from: source)) + try expectNil(UInt64.construct(from: source)) + } } try test("Array Iterator") { diff --git a/Sources/JavaScriptKit/ConstructibleFromJSValue.swift b/Sources/JavaScriptKit/ConstructibleFromJSValue.swift index 1f43658f0..ce1e1c25f 100644 --- a/Sources/JavaScriptKit/ConstructibleFromJSValue.swift +++ b/Sources/JavaScriptKit/ConstructibleFromJSValue.swift @@ -35,15 +35,41 @@ extension Double: ConstructibleFromJSValue {} extension Float: ConstructibleFromJSValue {} 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: JSBigIntExtended) { + self.init(exactly: bigInt.int64Value) + } + + /// 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: JSBigIntExtended) { self.init(bigInt.int64Value) } + + /// 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? { if let number = value.number { - return Self(number) + return Self(exactly: number.rounded(.towardZero)) } if let bigInt = value.bigInt as? JSBigIntExtended { - return Self(bigInt) + return Self(exactly: bigInt) } return nil } @@ -55,15 +81,41 @@ extension Int32: ConstructibleFromJSValue {} extension Int64: ConstructibleFromJSValue {} extension UnsignedInteger where Self: ConstructibleFromJSValue { + + /// 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: JSBigIntExtended) { + self.init(exactly: bigInt.uInt64Value) + } + + /// 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: JSBigIntExtended) { self.init(bigInt.uInt64Value) } + + /// 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? { if let number = value.number { - return Self(number) + return Self(exactly: number.rounded(.towardZero)) } if let bigInt = value.bigInt as? JSBigIntExtended { - return Self(bigInt) + return Self(exactly: bigInt) } return nil } From 181061bcfb09af664bdea32795a8abdfd60e8228 Mon Sep 17 00:00:00 2001 From: Yuta Saito Date: Wed, 31 Jul 2024 12:42:43 +0900 Subject: [PATCH 141/373] Fix test case for use-after-free diagnostic message The line number in the diagnostic message was hardcoded but it was changed in the previous commit. --- IntegrationTests/TestSuites/Sources/PrimaryTests/main.swift | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/IntegrationTests/TestSuites/Sources/PrimaryTests/main.swift b/IntegrationTests/TestSuites/Sources/PrimaryTests/main.swift index 4fedf6f4c..67a51aa2e 100644 --- a/IntegrationTests/TestSuites/Sources/PrimaryTests/main.swift +++ b/IntegrationTests/TestSuites/Sources/PrimaryTests/main.swift @@ -260,10 +260,11 @@ try test("Closure Lifetime") { #if JAVASCRIPTKIT_WITHOUT_WEAKREFS // Check diagnostics of use-after-free do { + let c1Line = #line + 1 let c1 = JSClosure { $0[0] } c1.release() let error = try expectThrow(try evalClosure.throws(c1, JSValue.number(42.0))) as! JSValue - try expect("Error message should contains definition location", error.description.hasSuffix("PrimaryTests/main.swift:247")) + try expect("Error message should contains definition location", error.description.hasSuffix("PrimaryTests/main.swift:\(c1Line)")) } #endif From e6dff98fa72630acafa23d43131a14337393a556 Mon Sep 17 00:00:00 2001 From: Alexander Cyon Date: Mon, 19 Aug 2024 10:44:21 +0200 Subject: [PATCH 142/373] [typos] Fix some typos --- Runtime/src/index.ts | 4 ++-- Sources/JavaScriptEventLoop/WebWorkerTaskExecutor.swift | 6 +++--- Sources/JavaScriptKit/FundamentalObjects/JSClosure.swift | 6 +++--- Sources/JavaScriptKit/FundamentalObjects/JSString.swift | 8 ++++---- Sources/JavaScriptKit/JSValue.swift | 8 ++++---- Sources/_CJavaScriptKit/include/_CJavaScriptKit.h | 2 +- ci/perf-tester/src/index.js | 2 +- 7 files changed, 18 insertions(+), 18 deletions(-) diff --git a/Runtime/src/index.ts b/Runtime/src/index.ts index 4cf0ee65a..d6d82c04a 100644 --- a/Runtime/src/index.ts +++ b/Runtime/src/index.ts @@ -78,8 +78,8 @@ export type SwiftRuntimeThreadChannel = */ postMessageToWorkerThread: (tid: number, message: MainToWorkerMessage) => void; /** - * This function is expected to be set in the main thread and shuold listen - * to messsages sent by `postMessageToMainThread` from the worker thread. + * 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. */ diff --git a/Sources/JavaScriptEventLoop/WebWorkerTaskExecutor.swift b/Sources/JavaScriptEventLoop/WebWorkerTaskExecutor.swift index dad1f959f..732a2e22a 100644 --- a/Sources/JavaScriptEventLoop/WebWorkerTaskExecutor.swift +++ b/Sources/JavaScriptEventLoop/WebWorkerTaskExecutor.swift @@ -384,12 +384,12 @@ public final class WebWorkerTaskExecutor: TaskExecutor { /// Executor global statistics internal struct ExecutorStats: CustomStringConvertible { var sendJobToMainThread: Int = 0 - var recieveJobFromWorkerThread: Int = 0 + var receiveJobFromWorkerThread: Int = 0 var enqueueGlobal: Int = 0 var enqueueExecutor: Int = 0 var description: String { - "ExecutorStats(sendWtoM: \(sendJobToMainThread), recvWfromM: \(recieveJobFromWorkerThread)), enqueueGlobal: \(enqueueGlobal), enqueueExecutor: \(enqueueExecutor)" + "ExecutorStats(sendWtoM: \(sendJobToMainThread), recvWfromM: \(receiveJobFromWorkerThread)), enqueueGlobal: \(enqueueGlobal), enqueueExecutor: \(enqueueExecutor)" } } #if JAVASCRIPTKIT_STATS @@ -456,7 +456,7 @@ public final class WebWorkerTaskExecutor: TaskExecutor { @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) @_expose(wasm, "swjs_enqueue_main_job_from_worker") func _swjs_enqueue_main_job_from_worker(_ job: UnownedJob) { - WebWorkerTaskExecutor.traceStatsIncrement(\.recieveJobFromWorkerThread) + WebWorkerTaskExecutor.traceStatsIncrement(\.receiveJobFromWorkerThread) JavaScriptEventLoop.shared.enqueue(ExecutorJob(job)) } diff --git a/Sources/JavaScriptKit/FundamentalObjects/JSClosure.swift b/Sources/JavaScriptKit/FundamentalObjects/JSClosure.swift index 75a8398fa..80fa2cf94 100644 --- a/Sources/JavaScriptKit/FundamentalObjects/JSClosure.swift +++ b/Sources/JavaScriptKit/FundamentalObjects/JSClosure.swift @@ -51,14 +51,14 @@ 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: JSFunction, JSClosureProtocol { diff --git a/Sources/JavaScriptKit/FundamentalObjects/JSString.swift b/Sources/JavaScriptKit/FundamentalObjects/JSString.swift index ee902f3ee..99d4813f2 100644 --- a/Sources/JavaScriptKit/FundamentalObjects/JSString.swift +++ b/Sources/JavaScriptKit/FundamentalObjects/JSString.swift @@ -19,9 +19,9 @@ 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 swjs_decode_string(bufferPtr.baseAddress!, Int32(bufferPtr.count)) } @@ -47,11 +47,11 @@ public struct JSString: LosslessStringConvertible, Equatable { init(from jsRef: JavaScriptObjectRef) { self.jsRef = jsRef - self.shouldDealocateRef = true + self.shouldDeallocateRef = true } deinit { - guard shouldDealocateRef else { return } + guard shouldDeallocateRef else { return } swjs_release(jsRef) } } diff --git a/Sources/JavaScriptKit/JSValue.swift b/Sources/JavaScriptKit/JSValue.swift index 852276149..7f27e7f50 100644 --- a/Sources/JavaScriptKit/JSValue.swift +++ b/Sources/JavaScriptKit/JSValue.swift @@ -149,15 +149,15 @@ public 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.") static func function(_ body: @escaping ([JSValue]) -> JSValue) -> JSValue { diff --git a/Sources/_CJavaScriptKit/include/_CJavaScriptKit.h b/Sources/_CJavaScriptKit/include/_CJavaScriptKit.h index 1e539fde1..b8ef2b7b0 100644 --- a/Sources/_CJavaScriptKit/include/_CJavaScriptKit.h +++ b/Sources/_CJavaScriptKit/include/_CJavaScriptKit.h @@ -33,7 +33,7 @@ typedef struct { 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 diff --git a/ci/perf-tester/src/index.js b/ci/perf-tester/src/index.js index e322e5f69..6dd4a5e61 100644 --- a/ci/perf-tester/src/index.js +++ b/ci/perf-tester/src/index.js @@ -60,7 +60,7 @@ async function run(octokit, context) { } console.log( - `PR #${pull_number} is targetted at ${pr.base.ref} (${pr.base.sha})` + `PR #${pull_number} is targeted at ${pr.base.ref} (${pr.base.sha})` ); startGroup(`[current] Build using '${config.buildScript}'`); From ff1aa40be0364faf703e94b55a39a4fe77b214ff Mon Sep 17 00:00:00 2001 From: Yuta Saito Date: Thu, 26 Sep 2024 18:04:04 +0900 Subject: [PATCH 143/373] Use `compiler(>=6.1)` to gate for the main branch toolchain We used a hack to distinguish the main branch toolchain and 6.0 branch toolchain, but the main branch has been bumped to 6.1, so we can use `compiler(>=6.1)` to gate for the main branch toolchain. --- Sources/JavaScriptEventLoop/JavaScriptEventLoop.swift | 2 +- Sources/JavaScriptEventLoop/WebWorkerTaskExecutor.swift | 2 +- Tests/JavaScriptEventLoopTests/WebWorkerTaskExecutorTests.swift | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Sources/JavaScriptEventLoop/JavaScriptEventLoop.swift b/Sources/JavaScriptEventLoop/JavaScriptEventLoop.swift index 871f374e2..765746bb1 100644 --- a/Sources/JavaScriptEventLoop/JavaScriptEventLoop.swift +++ b/Sources/JavaScriptEventLoop/JavaScriptEventLoop.swift @@ -61,7 +61,7 @@ public final class JavaScriptEventLoop: SerialExecutor, @unchecked Sendable { return _shared } - #if compiler(>=6.0) && hasFeature(IsolatedAny2) && _runtime(_multithreaded) + #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. diff --git a/Sources/JavaScriptEventLoop/WebWorkerTaskExecutor.swift b/Sources/JavaScriptEventLoop/WebWorkerTaskExecutor.swift index 732a2e22a..4ccfebe2c 100644 --- a/Sources/JavaScriptEventLoop/WebWorkerTaskExecutor.swift +++ b/Sources/JavaScriptEventLoop/WebWorkerTaskExecutor.swift @@ -1,4 +1,4 @@ -#if compiler(>=6.0) && hasFeature(IsolatedAny2) && _runtime(_multithreaded) // @_expose and @_extern are only available in Swift 6.0+ +#if compiler(>=6.1) && _runtime(_multithreaded) // @_expose and @_extern are only available in Swift 6.0+ import JavaScriptKit import _CJavaScriptKit diff --git a/Tests/JavaScriptEventLoopTests/WebWorkerTaskExecutorTests.swift b/Tests/JavaScriptEventLoopTests/WebWorkerTaskExecutorTests.swift index 589480cb9..a31c783d3 100644 --- a/Tests/JavaScriptEventLoopTests/WebWorkerTaskExecutorTests.swift +++ b/Tests/JavaScriptEventLoopTests/WebWorkerTaskExecutorTests.swift @@ -1,4 +1,4 @@ -#if compiler(>=6.0) && hasFeature(IsolatedAny2) && _runtime(_multithreaded) +#if compiler(>=6.1) && _runtime(_multithreaded) import XCTest import JavaScriptKit import _CJavaScriptKit // For swjs_get_worker_thread_id From 69b0f497dfd82585d6e00f49d91554074eb3b7dd Mon Sep 17 00:00:00 2001 From: Yuta Saito Date: Thu, 26 Sep 2024 18:07:00 +0900 Subject: [PATCH 144/373] Check Xcode 16 compatibility --- .github/workflows/test.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index edbc1e7b8..bc07e6a56 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -76,10 +76,10 @@ jobs: strategy: matrix: include: - - os: macos-13 - xcode: Xcode_14.3 - os: macos-14 xcode: Xcode_15.2 + - os: macos-15 + xcode: Xcode_16 runs-on: ${{ matrix.os }} steps: - uses: actions/checkout@v4 From ba37ea450d0d3b99d2699b46141485f2b27ced4c Mon Sep 17 00:00:00 2001 From: Yuta Saito Date: Thu, 26 Sep 2024 18:39:48 +0900 Subject: [PATCH 145/373] Gate pthread usage on wasip1-threads --- .../JavaScriptEventLoop/WebWorkerTaskExecutor.swift | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/Sources/JavaScriptEventLoop/WebWorkerTaskExecutor.swift b/Sources/JavaScriptEventLoop/WebWorkerTaskExecutor.swift index 4ccfebe2c..a70312e3f 100644 --- a/Sources/JavaScriptEventLoop/WebWorkerTaskExecutor.swift +++ b/Sources/JavaScriptEventLoop/WebWorkerTaskExecutor.swift @@ -1,4 +1,4 @@ -#if compiler(>=6.1) && _runtime(_multithreaded) // @_expose and @_extern are only available in Swift 6.0+ +#if compiler(>=6.1) && _runtime(_multithreaded) // @_expose and @_extern are only available in Swift 6.1+ import JavaScriptKit import _CJavaScriptKit @@ -274,6 +274,7 @@ public final class WebWorkerTaskExecutor: TaskExecutor { } func start(timeout: Duration, checkInterval: Duration) async throws { + #if canImport(wasi_pthread) class Context: @unchecked Sendable { let executor: WebWorkerTaskExecutor.Executor let worker: Worker @@ -316,6 +317,9 @@ public final class WebWorkerTaskExecutor: TaskExecutor { } while tid == 0 swjs_listen_message_from_worker_thread(tid) } + #else + fatalError("Unsupported platform") + #endif } func terminate() { @@ -420,6 +424,7 @@ public final class WebWorkerTaskExecutor: TaskExecutor { /// /// This function must be called once before using the Web Worker task executor. public static func installGlobalExecutor() { + #if canImport(wasi_pthread) // Ensure this function is called only once. guard _mainThread == nil else { return } @@ -448,6 +453,9 @@ public final class WebWorkerTaskExecutor: TaskExecutor { } } swift_task_enqueueGlobal_hook = unsafeBitCast(swift_task_enqueueGlobal_hook_impl, to: UnsafeMutableRawPointer?.self) + #else + fatalError("Unsupported platform") + #endif } } From fe0b4aee0890e65533421b56827d48e6ab5a63ba Mon Sep 17 00:00:00 2001 From: Simon Leeb <52261246+sliemeobn@users.noreply.github.com> Date: Mon, 7 Oct 2024 23:00:59 +0200 Subject: [PATCH 146/373] getting rid of existentials, patched headers in C parts --- Package.swift | 3 +- .../BasicObjects/JSPromise.swift | 12 ++ .../JavaScriptKit/BasicObjects/JSTimer.swift | 37 +++- .../BasicObjects/JSTypedArray.swift | 3 +- .../JavaScriptKit/ConvertibleToJSValue.swift | 28 ++- .../FundamentalObjects/JSClosure.swift | 8 +- .../FundamentalObjects/JSFunction.swift | 183 ++++++++++++++---- .../FundamentalObjects/JSObject.swift | 36 +++- .../FundamentalObjects/JSString.swift | 7 +- .../FundamentalObjects/JSSymbol.swift | 4 + .../JSThrowingFunction.swift | 2 + Sources/JavaScriptKit/JSValue.swift | 2 + Sources/JavaScriptKit/JSValueDecoder.swift | 2 + Sources/_CJavaScriptKit/_CJavaScriptKit.c | 12 +- .../_CJavaScriptKit/include/_CJavaScriptKit.h | 4 + swift-build-embedded | 34 ++++ 16 files changed, 319 insertions(+), 58 deletions(-) create mode 100755 swift-build-embedded diff --git a/Package.swift b/Package.swift index aa529c772..fb7e5b4e5 100644 --- a/Package.swift +++ b/Package.swift @@ -14,7 +14,8 @@ let package = Package( .target( name: "JavaScriptKit", dependencies: ["_CJavaScriptKit"], - resources: [.copy("Runtime")] + //LES: TODO - make this conditional + // resources: [.copy("Runtime")] ), .target(name: "_CJavaScriptKit"), .target( diff --git a/Sources/JavaScriptKit/BasicObjects/JSPromise.swift b/Sources/JavaScriptKit/BasicObjects/JSPromise.swift index 4b366d812..a41a3e1ca 100644 --- a/Sources/JavaScriptKit/BasicObjects/JSPromise.swift +++ b/Sources/JavaScriptKit/BasicObjects/JSPromise.swift @@ -58,6 +58,7 @@ public final class JSPromise: JSBridgedClass { 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!) } @@ -65,7 +66,17 @@ public final class JSPromise: JSBridgedClass { public static func reject(_ reason: ConvertibleToJSValue) -> JSPromise { self.init(unsafelyWrapping: Self.constructor!.reject!(reason).object!) } +#else + public static func resolve(_ value: some ConvertibleToJSValue) -> JSPromise { + self.init(unsafelyWrapping: constructor!.resolve!(value).object!) + } + + 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 { @@ -150,4 +161,5 @@ public final class JSPromise: JSBridgedClass { } return .init(unsafelyWrapping: jsObject.finally!(closure).object!) } +#endif } diff --git a/Sources/JavaScriptKit/BasicObjects/JSTimer.swift b/Sources/JavaScriptKit/BasicObjects/JSTimer.swift index 228b7e83d..a5d7c5b8e 100644 --- a/Sources/JavaScriptKit/BasicObjects/JSTimer.swift +++ b/Sources/JavaScriptKit/BasicObjects/JSTimer.swift @@ -11,10 +11,33 @@ For invalidation you should either store the timer in an optional property and a 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): + closure.jsValue + case .repeating(let closure): + closure.jsValue + } + } + + func release() { + switch self { + case .oneshot(let closure): + closure.release() + case .repeating(let closure): + closure.release() + } + } + } + /// 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 @@ -35,21 +58,21 @@ public final class JSTimer { */ public init(millisecondsDelay: Double, isRepeating: Bool = false, callback: @escaping () -> ()) { if isRepeating { - closure = JSClosure { _ in + closure = .repeating(JSClosure { _ in callback() return .undefined - } + }) } else { - closure = JSOneshotClosure { _ in + 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 6566e54f3..57df7c865 100644 --- a/Sources/JavaScriptKit/BasicObjects/JSTypedArray.swift +++ b/Sources/JavaScriptKit/BasicObjects/JSTypedArray.swift @@ -1,7 +1,7 @@ // // Created by Manuel Burghard. Licensed unter MIT. // - +#if !hasFeature(Embedded) import _CJavaScriptKit /// A protocol that allows a Swift numeric type to be mapped to the JavaScript TypedArray that holds integers of its type @@ -187,3 +187,4 @@ extension Float32: TypedArrayElement { extension Float64: TypedArrayElement { public static var typedArrayClass = JSObject.global.Float64Array.function! } +#endif \ No newline at end of file diff --git a/Sources/JavaScriptKit/ConvertibleToJSValue.swift b/Sources/JavaScriptKit/ConvertibleToJSValue.swift index ebf24c74c..660d72f16 100644 --- a/Sources/JavaScriptKit/ConvertibleToJSValue.swift +++ b/Sources/JavaScriptKit/ConvertibleToJSValue.swift @@ -88,6 +88,7 @@ extension JSObject: JSValueCompatible { private let objectConstructor = JSObject.global.Object.function! private let arrayConstructor = JSObject.global.Array.function! +#if !hasFeature(Embedded) extension Dictionary where Value == ConvertibleToJSValue, Key == String { public var jsValue: JSValue { let object = objectConstructor.new() @@ -97,6 +98,7 @@ extension Dictionary where Value == ConvertibleToJSValue, Key == String { return .object(object) } } +#endif extension Dictionary: ConvertibleToJSValue where Value: ConvertibleToJSValue, Key == String { public var jsValue: JSValue { @@ -158,6 +160,7 @@ extension Array: ConvertibleToJSValue where Element: ConvertibleToJSValue { } } +#if !hasFeature(Embedded) extension Array where Element == ConvertibleToJSValue { public var jsValue: JSValue { let array = arrayConstructor.new(count) @@ -167,6 +170,7 @@ extension Array where Element == ConvertibleToJSValue { return .object(array) } } +#endif extension Array: ConstructibleFromJSValue where Element: ConstructibleFromJSValue { public static func construct(from value: JSValue) -> [Element]? { @@ -252,13 +256,13 @@ extension JSValue { } } -extension Array where Element == ConvertibleToJSValue { +extension Array where Element: ConvertibleToJSValue { func withRawJSValues(_ body: ([RawJSValue]) -> T) -> T { // fast path for empty array guard self.count != 0 else { return body([]) } func _withRawJSValues( - _ values: [ConvertibleToJSValue], _ index: Int, + _ values: Self, _ index: Int, _ results: inout [RawJSValue], _ body: ([RawJSValue]) -> T ) -> T { if index == values.count { return body(results) } @@ -272,8 +276,24 @@ 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 \ No newline at end of file diff --git a/Sources/JavaScriptKit/FundamentalObjects/JSClosure.swift b/Sources/JavaScriptKit/FundamentalObjects/JSClosure.swift index 80fa2cf94..a7a93ba75 100644 --- a/Sources/JavaScriptKit/FundamentalObjects/JSClosure.swift +++ b/Sources/JavaScriptKit/FundamentalObjects/JSClosure.swift @@ -32,7 +32,7 @@ public class JSOneshotClosure: JSObject, JSClosureProtocol { }) } - #if compiler(>=5.5) + #if compiler(>=5.5) && !hasFeature(Embedded) @available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) public static func async(_ body: @escaping ([JSValue]) async throws -> JSValue) -> JSOneshotClosure { JSOneshotClosure(makeAsyncClosure(body)) @@ -113,7 +113,7 @@ public class JSClosure: JSFunction, JSClosureProtocol { Self.sharedClosures[hostFuncRef] = (self, body) } - #if compiler(>=5.5) + #if compiler(>=5.5) && !hasFeature(Embedded) @available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) public static func async(_ body: @escaping ([JSValue]) async throws -> JSValue) -> JSClosure { JSClosure(makeAsyncClosure(body)) @@ -129,7 +129,7 @@ public class JSClosure: JSFunction, JSClosureProtocol { #endif } -#if compiler(>=5.5) +#if compiler(>=5.5) && !hasFeature(Embedded) @available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) private func makeAsyncClosure(_ body: @escaping ([JSValue]) async throws -> JSValue) -> (([JSValue]) -> JSValue) { { arguments in @@ -195,7 +195,7 @@ func _call_host_function_impl( guard let (_, hostFunc) = JSClosure.sharedClosures[hostFuncRef] else { return true } - let arguments = UnsafeBufferPointer(start: argv, count: Int(argc)).map(\.jsValue) + let arguments = UnsafeBufferPointer(start: argv, count: Int(argc)).map { $0.jsValue} let result = hostFunc(arguments) let callbackFuncRef = JSFunction(id: callbackFuncRef) _ = callbackFuncRef(result) diff --git a/Sources/JavaScriptKit/FundamentalObjects/JSFunction.swift b/Sources/JavaScriptKit/FundamentalObjects/JSFunction.swift index 543146133..ace2aa911 100644 --- a/Sources/JavaScriptKit/FundamentalObjects/JSFunction.swift +++ b/Sources/JavaScriptKit/FundamentalObjects/JSFunction.swift @@ -10,7 +10,8 @@ import _CJavaScriptKit /// alert("Hello, world") /// ``` /// -public class JSFunction: JSObject { +public class JSFunction: JSObject, _JSFunctionProtocol { +#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. @@ -60,6 +61,11 @@ public class JSFunction: JSObject { } } + /// 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 /// /// @@ -79,11 +85,21 @@ public class JSFunction: JSObject { JSThrowingFunction(self) } - /// A variadic arguments version of `new`. - public func new(_ arguments: ConvertibleToJSValue...) -> JSObject { - new(arguments: arguments) +#else + @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))) + } + } + } +#endif + @available(*, unavailable, message: "Please use JSClosure instead") public static func from(_: @escaping ([JSValue]) -> JSValue) -> JSFunction { fatalError("unavailable") @@ -93,43 +109,136 @@ public class JSFunction: JSObject { .function(self) } +#if hasFeature(Embedded) + final func invokeNonThrowingJSFunction(arguments: [JSValue]) -> RawJSValue { + arguments.withRawJSValues { invokeNonThrowingJSFunction(rawValues: $0) } + } + + final func invokeNonThrowingJSFunction(arguments: [JSValue], this: JSObject) -> RawJSValue { + arguments.withRawJSValues { invokeNonThrowingJSFunction(rawValues: $0, this: this) } + } +#else final func invokeNonThrowingJSFunction(arguments: [ConvertibleToJSValue]) -> RawJSValue { - let id = self.id - return arguments.withRawJSValues { rawValues in - rawValues.withUnsafeBufferPointer { bufferPointer in - let argv = bufferPointer.baseAddress - let argc = bufferPointer.count - var payload1 = JavaScriptPayload1() - var payload2 = JavaScriptPayload2() - let resultBitPattern = swjs_call_function_no_catch( - id, argv, Int32(argc), - &payload1, &payload2 - ) - let kindAndFlags = unsafeBitCast(resultBitPattern, to: JavaScriptValueKindAndFlags.self) - assert(!kindAndFlags.isException) - let result = RawJSValue(kind: kindAndFlags.kind, payload1: payload1, payload2: payload2) - return result - } - } + arguments.withRawJSValues { invokeNonThrowingJSFunction(rawValues: $0) } } final func invokeNonThrowingJSFunction(arguments: [ConvertibleToJSValue], this: JSObject) -> RawJSValue { - let id = self.id - return arguments.withRawJSValues { rawValues in - rawValues.withUnsafeBufferPointer { 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 = unsafeBitCast(resultBitPattern, to: JavaScriptValueKindAndFlags.self) - assert(!kindAndFlags.isException) - let result = RawJSValue(kind: kindAndFlags.kind, payload1: payload1, payload2: payload2) - return result - } + 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 payload1 = JavaScriptPayload1() + var payload2 = JavaScriptPayload2() + let resultBitPattern = swjs_call_function_no_catch( + id, argv, Int32(argc), + &payload1, &payload2 + ) + let kindAndFlags = unsafeBitCast(resultBitPattern, to: JavaScriptValueKindAndFlags.self) + #if !hasFeature(Embedded) + assert(!kindAndFlags.isException) + #endif + 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 = unsafeBitCast(resultBitPattern, to: JavaScriptValueKindAndFlags.self) + #if !hasFeature(Embedded) + assert(!kindAndFlags.isException) + #endif + let result = RawJSValue(kind: kindAndFlags.kind, payload1: payload1, payload2: payload2) + return result } } } + +public protocol _JSFunctionProtocol: JSFunction {} + +#if hasFeature(Embedded) +public extension _JSFunctionProtocol { + // hand-made "varidacs" for Embedded + + @discardableResult + func callAsFunction(this: JSObject) -> JSValue { + invokeNonThrowingJSFunction(arguments: [], this: this).jsValue + } + + @discardableResult + func callAsFunction(this: JSObject, _ arg0: some ConvertibleToJSValue) -> JSValue { + invokeNonThrowingJSFunction(arguments: [arg0.jsValue], this: this).jsValue + } + + @discardableResult + func callAsFunction(this: JSObject, _ arg0: some ConvertibleToJSValue, _ arg1: some ConvertibleToJSValue) -> JSValue { + invokeNonThrowingJSFunction(arguments: [arg0.jsValue, arg1.jsValue], this: this).jsValue + } + + @discardableResult + 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 + func callAsFunction(this: JSObject, arguments: [JSValue]) -> JSValue { + invokeNonThrowingJSFunction(arguments: arguments, this: this).jsValue + } + + @discardableResult + func callAsFunction() -> JSValue { + invokeNonThrowingJSFunction(arguments: []).jsValue + } + + @discardableResult + func callAsFunction(_ arg0: some ConvertibleToJSValue) -> JSValue { + invokeNonThrowingJSFunction(arguments: [arg0.jsValue]).jsValue + } + + @discardableResult + func callAsFunction(_ arg0: some ConvertibleToJSValue, _ arg1: some ConvertibleToJSValue) -> JSValue { + invokeNonThrowingJSFunction(arguments: [arg0.jsValue, arg1.jsValue]).jsValue + } + + @discardableResult + func callAsFunction(_ arg0: some ConvertibleToJSValue, _ arg1: some ConvertibleToJSValue, _ arg2: some ConvertibleToJSValue) -> JSValue { + invokeNonThrowingJSFunction(arguments: [arg0.jsValue, arg1.jsValue, arg2.jsValue]).jsValue + } + + func new() -> JSObject { + new(arguments: []) + } + + func new(_ arg0: some ConvertibleToJSValue) -> JSObject { + new(arguments: [arg0.jsValue]) + } + + func new(_ arg0: some ConvertibleToJSValue, _ arg1: some ConvertibleToJSValue) -> JSObject { + new(arguments: [arg0.jsValue, arg1.jsValue]) + } + + func new(_ arg0: some ConvertibleToJSValue, _ arg1: some ConvertibleToJSValue, _ arg2: some ConvertibleToJSValue) -> JSObject { + new(arguments: [arg0.jsValue, arg1.jsValue, arg2.jsValue]) + } + + 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]) + } + + 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 \ No newline at end of file diff --git a/Sources/JavaScriptKit/FundamentalObjects/JSObject.swift b/Sources/JavaScriptKit/FundamentalObjects/JSObject.swift index 861758497..3891520c8 100644 --- a/Sources/JavaScriptKit/FundamentalObjects/JSObject.swift +++ b/Sources/JavaScriptKit/FundamentalObjects/JSObject.swift @@ -15,7 +15,7 @@ import _CJavaScriptKit /// The lifetime of this object is managed by the JavaScript and Swift runtime bridge library with /// reference counting system. @dynamicMemberLookup -public class JSObject: Equatable { +public class JSObject: _JSObjectProtocol, Equatable { @_spi(JSObject_id) public var id: JavaScriptObjectRef @_spi(JSObject_id) @@ -23,6 +23,7 @@ public class JSObject: Equatable { self.id = id } +#if !hasFeature(Embedded) /// Returns the `name` member method binding this object as `this` context. /// /// e.g. @@ -65,6 +66,7 @@ public class JSObject: Equatable { 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. @@ -105,6 +107,7 @@ public class JSObject: Equatable { set { setJSValue(this: self, symbol: name, value: newValue) } } +#if !hasFeature(Embedded) /// A modifier to call methods as throwing methods capturing `this` /// /// @@ -125,6 +128,7 @@ public class JSObject: Equatable { 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. @@ -197,6 +201,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 @@ -224,3 +229,32 @@ public class JSThrowingObject { self[name] } } +#endif + +public protocol _JSObjectProtocol: JSObject { +} + +#if hasFeature(Embedded) +public extension _JSObjectProtocol { + @_disfavoredOverload + subscript(dynamicMember name: String) -> (() -> JSValue)? { + self[name].function.map { function in + { function(this: self) } + } + } + + @_disfavoredOverload + subscript(dynamicMember name: String) -> ((A0) -> JSValue)? { + self[name].function.map { function in + { function(this: self, $0) } + } + } + + @_disfavoredOverload + subscript(dynamicMember name: String) -> ((A0, A1) -> JSValue)? { + self[name].function.map { function in + { function(this: self, $0, $1) } + } + } +} +#endif \ No newline at end of file diff --git a/Sources/JavaScriptKit/FundamentalObjects/JSString.swift b/Sources/JavaScriptKit/FundamentalObjects/JSString.swift index 99d4813f2..2eec4ef42 100644 --- a/Sources/JavaScriptKit/FundamentalObjects/JSString.swift +++ b/Sources/JavaScriptKit/FundamentalObjects/JSString.swift @@ -31,9 +31,12 @@ public struct JSString: LosslessStringConvertible, Equatable { var bytesRef: JavaScriptObjectRef = 0 let bytesLength = Int(swjs_encode_string(jsRef, &bytesRef)) // +1 for null terminator - let buffer = malloc(Int(bytesLength + 1))!.assumingMemoryBound(to: UInt8.self) + // TODO: revert this back to malloc and free + // let buffer = malloc(Int(bytesLength + 1))!.assumingMemoryBound(to: UInt8.self) + let buffer = UnsafeMutablePointer.allocate(capacity: Int(bytesLength + 1)) defer { - free(buffer) + buffer.deallocate() + // free(buffer) swjs_release(bytesRef) } swjs_load_string(bytesRef, buffer) diff --git a/Sources/JavaScriptKit/FundamentalObjects/JSSymbol.swift b/Sources/JavaScriptKit/FundamentalObjects/JSSymbol.swift index f5d194e25..e7d1a2fd2 100644 --- a/Sources/JavaScriptKit/FundamentalObjects/JSSymbol.swift +++ b/Sources/JavaScriptKit/FundamentalObjects/JSSymbol.swift @@ -10,7 +10,11 @@ public class JSSymbol: JSObject { public init(_ description: JSString) { // can’t do `self =` so we have to get the ID manually + #if hasFeature(Embedded) + let result = Symbol.invokeNonThrowingJSFunction(arguments: [description.jsValue]) + #else let result = Symbol.invokeNonThrowingJSFunction(arguments: [description]) + #endif precondition(result.kind == .symbol) super.init(id: UInt32(result.payload1)) } diff --git a/Sources/JavaScriptKit/FundamentalObjects/JSThrowingFunction.swift b/Sources/JavaScriptKit/FundamentalObjects/JSThrowingFunction.swift index 4763a8779..95bc2bd9c 100644 --- a/Sources/JavaScriptKit/FundamentalObjects/JSThrowingFunction.swift +++ b/Sources/JavaScriptKit/FundamentalObjects/JSThrowingFunction.swift @@ -1,3 +1,4 @@ +#if !hasFeature(Embedded) import _CJavaScriptKit @@ -94,3 +95,4 @@ private func invokeJSFunction(_ jsFunc: JSFunction, arguments: [ConvertibleToJSV } return result } +#endif \ No newline at end of file diff --git a/Sources/JavaScriptKit/JSValue.swift b/Sources/JavaScriptKit/JSValue.swift index 7f27e7f50..5166986ff 100644 --- a/Sources/JavaScriptKit/JSValue.swift +++ b/Sources/JavaScriptKit/JSValue.swift @@ -101,11 +101,13 @@ public enum JSValue: Equatable { } public extension JSValue { +#if !hasFeature(Embedded) /// An unsafe convenience method of `JSObject.subscript(_ name: String) -> ((ConvertibleToJSValue...) -> JSValue)?` /// - Precondition: `self` must be a JavaScript Object and specified member should be a callable object. subscript(dynamicMember name: String) -> ((ConvertibleToJSValue...) -> JSValue) { object![dynamicMember: name]! } +#endif /// An unsafe convenience method of `JSObject.subscript(_ index: Int) -> JSValue` /// - Precondition: `self` must be a JavaScript Object. diff --git a/Sources/JavaScriptKit/JSValueDecoder.swift b/Sources/JavaScriptKit/JSValueDecoder.swift index b1d59af63..73ee9310c 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 @@ -248,3 +249,4 @@ public class JSValueDecoder { return try T(from: decoder) } } +#endif \ No newline at end of file diff --git a/Sources/_CJavaScriptKit/_CJavaScriptKit.c b/Sources/_CJavaScriptKit/_CJavaScriptKit.c index a6e63a1b8..c658bd545 100644 --- a/Sources/_CJavaScriptKit/_CJavaScriptKit.c +++ b/Sources/_CJavaScriptKit/_CJavaScriptKit.c @@ -1,8 +1,18 @@ #include "_CJavaScriptKit.h" +#if __wasm32__ +#if __Embedded +#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 -#if __wasm32__ +#endif bool _call_host_function_impl(const JavaScriptHostFuncRef host_func_ref, const RawJSValue *argv, const int argc, diff --git a/Sources/_CJavaScriptKit/include/_CJavaScriptKit.h b/Sources/_CJavaScriptKit/include/_CJavaScriptKit.h index b8ef2b7b0..7bbabf2c6 100644 --- a/Sources/_CJavaScriptKit/include/_CJavaScriptKit.h +++ b/Sources/_CJavaScriptKit/include/_CJavaScriptKit.h @@ -1,7 +1,11 @@ #ifndef _CJavaScriptKit_h #define _CJavaScriptKit_h +#if __Embedded +#include +#else #include +#endif #include #include diff --git a/swift-build-embedded b/swift-build-embedded new file mode 100755 index 000000000..247658466 --- /dev/null +++ b/swift-build-embedded @@ -0,0 +1,34 @@ +swift build --target JavaScriptKit \ + --triple wasm32-unknown-none-wasm \ + -Xswiftc -enable-experimental-feature -Xswiftc Embedded \ + -Xswiftc -enable-experimental-feature -Xswiftc Extern \ + -Xswiftc -wmo -Xswiftc -disable-cmo \ + -Xswiftc -Xfrontend -Xswiftc -gnone \ + -Xswiftc -Xfrontend -Xswiftc -disable-stack-protector \ + -Xswiftc -cxx-interoperability-mode=default \ + -Xcc -D__Embedded -Xcc -fdeclspec \ + -Xlinker --export-if-defined=__main_argc_argv \ + -Xswiftc -Xclang-linker -Xswiftc -mexec-model=reactor + + + # -Xlinker --export-if-defined=_initialize + # -Xswiftc -static-stdlib \ + + # --verbose + + # -Xswiftc -Xclang-linker -Xswiftc -nostdlib \ + +####### +# swift build -c release --product ExampleApp \ +# --sdk DEVELOPMENT-SNAPSHOT-2024-09-20-a-wasm32-unknown-wasi \ +# --triple wasm32-unknown-none-wasm \ +# -Xcc -D__Embedded \ +# -Xswiftc -enable-experimental-feature -Xswiftc Embedded \ +# -Xswiftc -enable-experimental-feature -Xswiftc Extern \ + # -Xswiftc -wmo -Xswiftc -disable-cmo \ + # -Xcc -fdeclspec \ + # -Xswiftc -Xclang-linker -Xswiftc -mexec-model=reactor \ + # -Xlinker --export-if-defined=__main_argc_argv \ + + +#cp .build/release/ExampleApp.wasm ./TestPage/app.wasm \ No newline at end of file From 11897e1dbd63e708a27bc31ff52449b441293084 Mon Sep 17 00:00:00 2001 From: Simon Leeb <52261246+sliemeobn@users.noreply.github.com> Date: Tue, 8 Oct 2024 00:26:42 +0200 Subject: [PATCH 147/373] fixed C bit field thing --- .../FundamentalObjects/JSFunction.swift | 29 +++++++++++++++---- .../_CJavaScriptKit/include/_CJavaScriptKit.h | 5 ++++ 2 files changed, 29 insertions(+), 5 deletions(-) diff --git a/Sources/JavaScriptKit/FundamentalObjects/JSFunction.swift b/Sources/JavaScriptKit/FundamentalObjects/JSFunction.swift index ace2aa911..f918687f3 100644 --- a/Sources/JavaScriptKit/FundamentalObjects/JSFunction.swift +++ b/Sources/JavaScriptKit/FundamentalObjects/JSFunction.swift @@ -137,10 +137,8 @@ public class JSFunction: JSObject, _JSFunctionProtocol { id, argv, Int32(argc), &payload1, &payload2 ) - let kindAndFlags = unsafeBitCast(resultBitPattern, to: JavaScriptValueKindAndFlags.self) - #if !hasFeature(Embedded) + let kindAndFlags = valueKindAndFlagsFromBits(resultBitPattern) assert(!kindAndFlags.isException) - #endif let result = RawJSValue(kind: kindAndFlags.kind, payload1: payload1, payload2: payload2) return result } @@ -156,7 +154,7 @@ public class JSFunction: JSObject, _JSFunctionProtocol { id, argv, Int32(argc), &payload1, &payload2 ) - let kindAndFlags = unsafeBitCast(resultBitPattern, to: JavaScriptValueKindAndFlags.self) + let kindAndFlags = valueKindAndFlagsFromBits(resultBitPattern) #if !hasFeature(Embedded) assert(!kindAndFlags.isException) #endif @@ -241,4 +239,25 @@ public extension _JSFunctionProtocol { new(arguments: [arg0.jsValue, arg1.jsValue, arg2.jsValue, arg3.jsValue, arg4.jsValue, arg5.jsValue, arg6.jsValue]) } } -#endif \ No newline at end of file + +// C bit fields seem to not work with Embedded +// in "normal mode" this is defined as a C struct +private struct JavaScriptValueKindAndFlags { + let errorBit: UInt32 = 1 << 32 + let kind: JavaScriptValueKind + let isException: Bool + + init(bitPattern: UInt32) { + self.kind = JavaScriptValueKind(rawValue: bitPattern & ~errorBit)! + self.isException = (bitPattern & errorBit) != 0 + } +} +#endif + +private func valueKindAndFlagsFromBits(_ bits: UInt32) -> JavaScriptValueKindAndFlags { + #if hasFeature(Embedded) + JavaScriptValueKindAndFlags(bitPattern: bits) + #else + unsafeBitCast(resultBitPattern, to: JavaScriptValueKindAndFlags.self) + #endif +} \ No newline at end of file diff --git a/Sources/_CJavaScriptKit/include/_CJavaScriptKit.h b/Sources/_CJavaScriptKit/include/_CJavaScriptKit.h index 7bbabf2c6..8daf7cdc6 100644 --- a/Sources/_CJavaScriptKit/include/_CJavaScriptKit.h +++ b/Sources/_CJavaScriptKit/include/_CJavaScriptKit.h @@ -29,10 +29,15 @@ typedef enum __attribute__((enum_extensibility(closed))) { JavaScriptValueKindBigInt = 8, } JavaScriptValueKind; +#if __Embedded +// something about the bit field widths is not working with embedded +typedef unsigned short JavaScriptValueKindAndFlags; +#else typedef struct { JavaScriptValueKind kind: 31; bool isException: 1; } JavaScriptValueKindAndFlags; +#endif typedef unsigned JavaScriptPayload1; typedef double JavaScriptPayload2; From 26b7a8e99cdacc09ba4b5b2da5c2964680c03477 Mon Sep 17 00:00:00 2001 From: Simon Leeb <52261246+sliemeobn@users.noreply.github.com> Date: Tue, 8 Oct 2024 12:46:24 +0200 Subject: [PATCH 148/373] added embedded example and conditional package flags --- Examples/Basic/build.sh | 2 +- Examples/Embedded/.gitignore | 6 + Examples/Embedded/Package.swift | 20 + Examples/Embedded/README.md | 6 + .../_thingsThatShouldNotBeNeeded.swift | 18 + .../Embedded/Sources/EmbeddedApp/main.swift | 23 + .../_bundling_does_not_work_with_embedded | 0 Examples/Embedded/_Runtime/index.js | 579 ++++++++++++++++++ Examples/Embedded/_Runtime/index.mjs | 569 +++++++++++++++++ Examples/Embedded/build.sh | 12 + Examples/Embedded/index.html | 12 + Examples/Embedded/index.js | 33 + Package.swift | 12 +- Sources/JavaScriptKit/Features.swift | 4 +- .../FundamentalObjects/JSClosure.swift | 12 +- .../FundamentalObjects/JSFunction.swift | 2 +- Sources/JavaScriptKit/JSValue.swift | 19 + Sources/_CJavaScriptKit/_CJavaScriptKit.c | 25 - swift-build-embedded | 34 - 19 files changed, 1316 insertions(+), 72 deletions(-) create mode 100644 Examples/Embedded/.gitignore create mode 100644 Examples/Embedded/Package.swift create mode 100644 Examples/Embedded/README.md create mode 100644 Examples/Embedded/Sources/EmbeddedApp/_thingsThatShouldNotBeNeeded.swift create mode 100644 Examples/Embedded/Sources/EmbeddedApp/main.swift create mode 100644 Examples/Embedded/_Runtime/_bundling_does_not_work_with_embedded create mode 100644 Examples/Embedded/_Runtime/index.js create mode 100644 Examples/Embedded/_Runtime/index.mjs create mode 100755 Examples/Embedded/build.sh create mode 100644 Examples/Embedded/index.html create mode 100644 Examples/Embedded/index.js delete mode 100755 swift-build-embedded diff --git a/Examples/Basic/build.sh b/Examples/Basic/build.sh index f92c05639..2e4c3735b 100755 --- a/Examples/Basic/build.sh +++ b/Examples/Basic/build.sh @@ -1 +1 @@ -swift build --swift-sdk DEVELOPMENT-SNAPSHOT-2024-07-09-a-wasm32-unknown-wasi -Xswiftc -Xclang-linker -Xswiftc -mexec-model=reactor -Xlinker --export=__main_argc_argv +swift build --swift-sdk DEVELOPMENT-SNAPSHOT-2024-09-20-a-wasm32-unknown-wasi -Xswiftc -Xclang-linker -Xswiftc -mexec-model=reactor -Xlinker --export=__main_argc_argv diff --git a/Examples/Embedded/.gitignore b/Examples/Embedded/.gitignore new file mode 100644 index 000000000..31492b35d --- /dev/null +++ b/Examples/Embedded/.gitignore @@ -0,0 +1,6 @@ +.DS_Store +/.build +/Packages +/*.xcodeproj +xcuserdata/ +Package.resolved \ No newline at end of file diff --git a/Examples/Embedded/Package.swift b/Examples/Embedded/Package.swift new file mode 100644 index 000000000..f0c03bd87 --- /dev/null +++ b/Examples/Embedded/Package.swift @@ -0,0 +1,20 @@ +// swift-tools-version:5.10 + +import PackageDescription + +let package = Package( + name: "Embedded", + dependencies: [ + .package(name: "JavaScriptKit", path: "../../"), + .package(url: "https://github.com/swifweb/EmbeddedFoundation", branch: "0.1.0") + ], + targets: [ + .executableTarget( + name: "EmbeddedApp", + dependencies: [ + "JavaScriptKit", + .product(name: "Foundation", package: "EmbeddedFoundation") + ] + ) + ] +) diff --git a/Examples/Embedded/README.md b/Examples/Embedded/README.md new file mode 100644 index 000000000..92dd6be40 --- /dev/null +++ b/Examples/Embedded/README.md @@ -0,0 +1,6 @@ +# Embedded example + +```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..50d838b96 --- /dev/null +++ b/Examples/Embedded/Sources/EmbeddedApp/_thingsThatShouldNotBeNeeded.swift @@ -0,0 +1,18 @@ +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() { + _ = _library_features + _ = _call_host_function_impl + _ = _free_host_function_impl +} + +// 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 +} diff --git a/Examples/Embedded/Sources/EmbeddedApp/main.swift b/Examples/Embedded/Sources/EmbeddedApp/main.swift new file mode 100644 index 000000000..345d8b0a7 --- /dev/null +++ b/Examples/Embedded/Sources/EmbeddedApp/main.swift @@ -0,0 +1,23 @@ +import JavaScriptKit + +let alert = JSObject.global.alert.function! +let document = JSObject.global.document + +print("Document title: \(document.title.string ?? "")") + +var divElement = document.createElement("div") +divElement.innerText = "Hello, world 2" +_ = document.body.appendChild(divElement) + +var buttonElement = document.createElement("button") +buttonElement.innerText = "Alert demo" +buttonElement.onclick = JSValue.object(JSClosure { _ in + divElement.innerText = "Hello, world 3" + return .undefined +}) + +_ = document.body.appendChild(buttonElement) + +func print(_ message: String) { + _ = JSObject.global.console.log(message) +} diff --git a/Examples/Embedded/_Runtime/_bundling_does_not_work_with_embedded b/Examples/Embedded/_Runtime/_bundling_does_not_work_with_embedded new file mode 100644 index 000000000..e69de29bb diff --git a/Examples/Embedded/_Runtime/index.js b/Examples/Embedded/_Runtime/index.js new file mode 100644 index 000000000..9d29b4329 --- /dev/null +++ b/Examples/Embedded/_Runtime/index.js @@ -0,0 +1,579 @@ +(function (global, factory) { + typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports) : + typeof define === 'function' && define.amd ? define(['exports'], factory) : + (global = typeof globalThis !== 'undefined' ? globalThis : global || self, factory(global.JavaScriptKit = {})); +})(this, (function (exports) { 'use strict'; + + /// Memory lifetime of closures in Swift are managed by Swift side + class SwiftClosureDeallocator { + constructor(exports) { + if (typeof FinalizationRegistry === "undefined") { + throw new Error("The Swift part of JavaScriptKit was configured to require " + + "the availability of JavaScript WeakRefs. Please build " + + "with `-Xswiftc -DJAVASCRIPTKIT_WITHOUT_WEAKREFS` to " + + "disable features that use WeakRefs."); + } + this.functionRegistry = new FinalizationRegistry((id) => { + exports.swjs_free_host_function(id); + }); + } + track(func, func_ref) { + this.functionRegistry.register(func, func_ref); + } + } + + function assertNever(x, message) { + throw new Error(message); + } + + const decode = (kind, payload1, payload2, memory) => { + switch (kind) { + case 0 /* Boolean */: + switch (payload1) { + case 0: + return false; + case 1: + return true; + } + case 2 /* Number */: + return payload2; + case 1 /* String */: + case 3 /* Object */: + case 6 /* Function */: + case 7 /* Symbol */: + case 8 /* BigInt */: + return memory.getObject(payload1); + case 4 /* Null */: + return null; + case 5 /* Undefined */: + return undefined; + default: + assertNever(kind, `JSValue Type kind "${kind}" is not supported`); + } + }; + // Note: + // `decodeValues` assumes that the size of RawJSValue is 16. + const decodeArray = (ptr, length, memory) => { + // fast path for empty array + if (length === 0) { + return []; + } + let result = []; + // It's safe to hold DataView here because WebAssembly.Memory.buffer won't + // change within this function. + const view = memory.dataView(); + for (let index = 0; index < length; index++) { + const base = ptr + 16 * index; + const kind = view.getUint32(base, true); + const payload1 = view.getUint32(base + 4, true); + const payload2 = view.getFloat64(base + 8, true); + result.push(decode(kind, payload1, payload2, memory)); + } + return result; + }; + // A helper function to encode a RawJSValue into a pointers. + // Please prefer to use `writeAndReturnKindBits` to avoid unnecessary + // memory stores. + // This function should be used only when kind flag is stored in memory. + const write = (value, kind_ptr, payload1_ptr, payload2_ptr, is_exception, memory) => { + const kind = writeAndReturnKindBits(value, payload1_ptr, payload2_ptr, is_exception, memory); + memory.writeUint32(kind_ptr, kind); + }; + const writeAndReturnKindBits = (value, payload1_ptr, payload2_ptr, is_exception, memory) => { + const exceptionBit = (is_exception ? 1 : 0) << 31; + if (value === null) { + return exceptionBit | 4 /* Null */; + } + const writeRef = (kind) => { + memory.writeUint32(payload1_ptr, memory.retain(value)); + return exceptionBit | kind; + }; + const type = typeof value; + switch (type) { + case "boolean": { + memory.writeUint32(payload1_ptr, value ? 1 : 0); + return exceptionBit | 0 /* Boolean */; + } + case "number": { + memory.writeFloat64(payload2_ptr, value); + return exceptionBit | 2 /* Number */; + } + case "string": { + return writeRef(1 /* String */); + } + case "undefined": { + return exceptionBit | 5 /* Undefined */; + } + case "object": { + return writeRef(3 /* Object */); + } + case "function": { + return writeRef(6 /* Function */); + } + case "symbol": { + return writeRef(7 /* Symbol */); + } + case "bigint": { + return writeRef(8 /* BigInt */); + } + default: + assertNever(type, `Type "${type}" is not supported yet`); + } + throw new Error("Unreachable"); + }; + + 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 SwiftRuntime { + constructor(options) { + this.version = 708; + this.textDecoder = new TextDecoder("utf-8"); + this.textEncoder = new TextEncoder(); // Only support utf-8 + /** @deprecated Use `wasmImports` instead */ + this.importObjects = () => this.wasmImports; + this._instance = null; + this._memory = null; + this._closureDeallocator = null; + this.tid = null; + this.options = options || {}; + } + setInstance(instance) { + this._instance = instance; + if (typeof this.exports._start === "function") { + throw new Error(`JavaScriptKit supports only WASI reactor ABI. + Please make sure you are building with: + -Xswiftc -Xclang-linker -Xswiftc -mexec-model=reactor + `); + } + if (this.exports.swjs_library_version() != this.version) { + throw new Error(`The versions of JavaScriptKit are incompatible. + WebAssembly runtime ${this.exports.swjs_library_version()} != JS runtime ${this.version}`); + } + } + main() { + const instance = this.instance; + try { + if (typeof instance.exports.main === "function") { + instance.exports.main(); + } + else if (typeof instance.exports.__main_argc_argv === "function") { + // Swift 6.0 and later use `__main_argc_argv` instead of `main`. + instance.exports.__main_argc_argv(0, 0); + } + } + catch (error) { + if (error instanceof UnsafeEventLoopYield) { + // Ignore the error + return; + } + // Rethrow other errors + throw error; + } + } + /** + * Start a new thread with the given `tid` and `startArg`, which + * is forwarded to the `wasi_thread_start` function. + * This function is expected to be called from the spawned Web Worker thread. + */ + startThread(tid, startArg) { + this.tid = tid; + const instance = this.instance; + try { + if (typeof instance.exports.wasi_thread_start === "function") { + instance.exports.wasi_thread_start(tid, startArg); + } + else { + throw new Error(`The WebAssembly module is not built for wasm32-unknown-wasip1-threads target.`); + } + } + catch (error) { + if (error instanceof UnsafeEventLoopYield) { + // Ignore the error + return; + } + // Rethrow other errors + throw error; + } + } + get instance() { + if (!this._instance) + throw new Error("WebAssembly instance is not set yet"); + return this._instance; + } + get exports() { + return this.instance.exports; + } + get memory() { + if (!this._memory) { + this._memory = new Memory(this.instance.exports); + } + return this._memory; + } + get closureDeallocator() { + if (this._closureDeallocator) + return this._closureDeallocator; + const features = this.exports.swjs_library_features(); + const librarySupportsWeakRef = (features & 1 /* WeakRefs */) != 0; + if (librarySupportsWeakRef) { + this._closureDeallocator = new SwiftClosureDeallocator(this.exports); + } + return this._closureDeallocator; + } + callHostFunction(host_func_id, line, file, args) { + const argc = args.length; + const argv = this.exports.swjs_prepare_host_function_call(argc); + const memory = this.memory; + for (let index = 0; index < args.length; index++) { + const argument = args[index]; + const base = argv + 16 * index; + write(argument, base, base + 4, base + 8, false, memory); + } + let output; + // This ref is released by the swjs_call_host_function implementation + const callback_func_ref = memory.retain((result) => { + output = result; + }); + const alreadyReleased = this.exports.swjs_call_host_function(host_func_id, argv, argc, callback_func_ref); + if (alreadyReleased) { + throw new Error(`The JSClosure has been already released by Swift side. The closure is created at ${file}:${line}`); + } + this.exports.swjs_cleanup_host_function_call(argv); + return output; + } + get wasmImports() { + return { + swjs_set_prop: (ref, name, kind, payload1, payload2) => { + const memory = this.memory; + const obj = memory.getObject(ref); + const key = memory.getObject(name); + const value = decode(kind, payload1, payload2, memory); + obj[key] = value; + }, + swjs_get_prop: (ref, name, payload1_ptr, payload2_ptr) => { + const memory = this.memory; + const obj = memory.getObject(ref); + const key = memory.getObject(name); + const result = obj[key]; + return writeAndReturnKindBits(result, payload1_ptr, payload2_ptr, false, memory); + }, + swjs_set_subscript: (ref, index, kind, payload1, payload2) => { + const memory = this.memory; + const obj = memory.getObject(ref); + const value = decode(kind, payload1, payload2, memory); + obj[index] = value; + }, + swjs_get_subscript: (ref, index, payload1_ptr, payload2_ptr) => { + const obj = this.memory.getObject(ref); + const result = obj[index]; + return writeAndReturnKindBits(result, payload1_ptr, payload2_ptr, false, this.memory); + }, + swjs_encode_string: (ref, bytes_ptr_result) => { + const memory = this.memory; + const bytes = this.textEncoder.encode(memory.getObject(ref)); + const bytes_ptr = memory.retain(bytes); + memory.writeUint32(bytes_ptr_result, bytes_ptr); + return bytes.length; + }, + swjs_decode_string: ( + // NOTE: TextDecoder can't decode typed arrays backed by SharedArrayBuffer + this.options.sharedMemory == true + ? ((bytes_ptr, length) => { + const memory = this.memory; + const bytes = memory + .bytes() + .slice(bytes_ptr, bytes_ptr + length); + const string = this.textDecoder.decode(bytes); + return memory.retain(string); + }) + : ((bytes_ptr, length) => { + const memory = this.memory; + const bytes = memory + .bytes() + .subarray(bytes_ptr, bytes_ptr + length); + const string = this.textDecoder.decode(bytes); + return memory.retain(string); + })), + swjs_load_string: (ref, buffer) => { + const memory = this.memory; + const bytes = memory.getObject(ref); + memory.writeBytes(buffer, bytes); + }, + swjs_call_function: (ref, argv, argc, payload1_ptr, payload2_ptr) => { + const memory = this.memory; + const func = memory.getObject(ref); + let result = undefined; + try { + const args = decodeArray(argv, argc, memory); + result = func(...args); + } + catch (error) { + return writeAndReturnKindBits(error, payload1_ptr, payload2_ptr, true, this.memory); + } + return writeAndReturnKindBits(result, payload1_ptr, payload2_ptr, false, this.memory); + }, + swjs_call_function_no_catch: (ref, argv, argc, payload1_ptr, payload2_ptr) => { + const memory = this.memory; + const func = memory.getObject(ref); + const args = decodeArray(argv, argc, memory); + const result = func(...args); + return writeAndReturnKindBits(result, payload1_ptr, payload2_ptr, false, this.memory); + }, + swjs_call_function_with_this: (obj_ref, func_ref, argv, argc, payload1_ptr, payload2_ptr) => { + const memory = this.memory; + const obj = memory.getObject(obj_ref); + const func = memory.getObject(func_ref); + let result; + try { + const args = decodeArray(argv, argc, memory); + result = func.apply(obj, args); + } + catch (error) { + return writeAndReturnKindBits(error, payload1_ptr, payload2_ptr, true, this.memory); + } + return writeAndReturnKindBits(result, payload1_ptr, payload2_ptr, false, this.memory); + }, + swjs_call_function_with_this_no_catch: (obj_ref, func_ref, argv, argc, payload1_ptr, payload2_ptr) => { + const memory = this.memory; + const obj = memory.getObject(obj_ref); + const func = memory.getObject(func_ref); + let result = undefined; + const args = decodeArray(argv, argc, memory); + result = func.apply(obj, args); + return writeAndReturnKindBits(result, payload1_ptr, payload2_ptr, false, this.memory); + }, + swjs_call_new: (ref, argv, argc) => { + const memory = this.memory; + const constructor = memory.getObject(ref); + const args = decodeArray(argv, argc, memory); + const instance = new constructor(...args); + return this.memory.retain(instance); + }, + swjs_call_throwing_new: (ref, argv, argc, exception_kind_ptr, exception_payload1_ptr, exception_payload2_ptr) => { + let memory = this.memory; + const constructor = memory.getObject(ref); + let result; + try { + const args = decodeArray(argv, argc, memory); + result = new constructor(...args); + } + catch (error) { + write(error, exception_kind_ptr, exception_payload1_ptr, exception_payload2_ptr, true, this.memory); + return -1; + } + memory = this.memory; + write(null, exception_kind_ptr, exception_payload1_ptr, exception_payload2_ptr, false, memory); + return memory.retain(result); + }, + swjs_instanceof: (obj_ref, constructor_ref) => { + const memory = this.memory; + const obj = memory.getObject(obj_ref); + const constructor = memory.getObject(constructor_ref); + return obj instanceof constructor; + }, + swjs_create_function: (host_func_id, line, file) => { + var _a; + const fileString = this.memory.getObject(file); + const func = (...args) => this.callHostFunction(host_func_id, line, fileString, args); + const func_ref = this.memory.retain(func); + (_a = this.closureDeallocator) === null || _a === void 0 ? void 0 : _a.track(func, func_ref); + return func_ref; + }, + swjs_create_typed_array: (constructor_ref, elementsPtr, length) => { + const ArrayType = this.memory.getObject(constructor_ref); + const array = new ArrayType(this.memory.rawMemory.buffer, elementsPtr, length); + // Call `.slice()` to copy the memory + return this.memory.retain(array.slice()); + }, + swjs_load_typed_array: (ref, buffer) => { + const memory = this.memory; + const typedArray = memory.getObject(ref); + const bytes = new Uint8Array(typedArray.buffer); + memory.writeBytes(buffer, bytes); + }, + swjs_release: (ref) => { + this.memory.release(ref); + }, + swjs_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."); + } + threadChannel.listenMessageFromMainThread((message) => { + switch (message.type) { + case "wake": + this.exports.swjs_wake_worker_thread(); + break; + default: + const unknownMessage = message.type; + 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."); + } + threadChannel.listenMessageFromWorkerThread(tid, (message) => { + switch (message.type) { + case "job": + this.exports.swjs_enqueue_main_job_from_worker(message.data); + break; + default: + const unknownMessage = message.type; + 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; + }, + }; + } + postMessageToMainThread(message) { + 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); + } + postMessageToWorkerThread(tid, message) { + 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); + } + } + /// This error is thrown when yielding event loop control from `swift_task_asyncMainDrainQueue` + /// to JavaScript. This is usually thrown when: + /// - The entry point of the Swift program is `func main() async` + /// - The Swift Concurrency's global executor is hooked by `JavaScriptEventLoop.installGlobalExecutor()` + /// - Calling exported `main` or `__main_argc_argv` function from JavaScript + /// + /// This exception must be caught by the caller of the exported function and the caller should + /// catch this exception and just ignore it. + /// + /// FAQ: Why this error is thrown? + /// This error is thrown to unwind the call stack of the Swift program and return the control to + /// the JavaScript side. Otherwise, the `swift_task_asyncMainDrainQueue` ends up with `abort()` + /// because the event loop expects `exit()` call before the end of the event loop. + class UnsafeEventLoopYield extends Error { + } + + exports.SwiftRuntime = SwiftRuntime; + + Object.defineProperty(exports, '__esModule', { value: true }); + +})); diff --git a/Examples/Embedded/_Runtime/index.mjs b/Examples/Embedded/_Runtime/index.mjs new file mode 100644 index 000000000..9201b7712 --- /dev/null +++ b/Examples/Embedded/_Runtime/index.mjs @@ -0,0 +1,569 @@ +/// 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 decode = (kind, payload1, payload2, memory) => { + switch (kind) { + case 0 /* Boolean */: + switch (payload1) { + case 0: + return false; + case 1: + return true; + } + case 2 /* Number */: + return payload2; + case 1 /* String */: + case 3 /* Object */: + case 6 /* Function */: + case 7 /* Symbol */: + case 8 /* BigInt */: + return memory.getObject(payload1); + case 4 /* Null */: + return null; + case 5 /* Undefined */: + return undefined; + default: + assertNever(kind, `JSValue Type kind "${kind}" is not supported`); + } +}; +// Note: +// `decodeValues` assumes that the size of RawJSValue is 16. +const decodeArray = (ptr, length, memory) => { + // fast path for empty array + if (length === 0) { + return []; + } + let result = []; + // It's safe to hold DataView here because WebAssembly.Memory.buffer won't + // change within this function. + const view = memory.dataView(); + for (let index = 0; index < length; index++) { + const base = ptr + 16 * index; + const kind = view.getUint32(base, true); + const payload1 = view.getUint32(base + 4, true); + const payload2 = view.getFloat64(base + 8, true); + result.push(decode(kind, payload1, payload2, memory)); + } + return result; +}; +// A helper function to encode a RawJSValue into a pointers. +// Please prefer to use `writeAndReturnKindBits` to avoid unnecessary +// memory stores. +// This function should be used only when kind flag is stored in memory. +const write = (value, kind_ptr, payload1_ptr, payload2_ptr, is_exception, memory) => { + const kind = writeAndReturnKindBits(value, payload1_ptr, payload2_ptr, is_exception, memory); + memory.writeUint32(kind_ptr, kind); +}; +const writeAndReturnKindBits = (value, payload1_ptr, payload2_ptr, is_exception, memory) => { + const exceptionBit = (is_exception ? 1 : 0) << 31; + if (value === null) { + return exceptionBit | 4 /* Null */; + } + const writeRef = (kind) => { + memory.writeUint32(payload1_ptr, memory.retain(value)); + return exceptionBit | kind; + }; + const type = typeof value; + switch (type) { + case "boolean": { + memory.writeUint32(payload1_ptr, value ? 1 : 0); + return exceptionBit | 0 /* Boolean */; + } + case "number": { + memory.writeFloat64(payload2_ptr, value); + return exceptionBit | 2 /* Number */; + } + case "string": { + return writeRef(1 /* String */); + } + case "undefined": { + return exceptionBit | 5 /* Undefined */; + } + case "object": { + return writeRef(3 /* Object */); + } + case "function": { + return writeRef(6 /* Function */); + } + case "symbol": { + return writeRef(7 /* Symbol */); + } + case "bigint": { + return writeRef(8 /* BigInt */); + } + default: + assertNever(type, `Type "${type}" is not supported yet`); + } + throw new Error("Unreachable"); +}; + +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 SwiftRuntime { + constructor(options) { + this.version = 708; + this.textDecoder = new TextDecoder("utf-8"); + this.textEncoder = new TextEncoder(); // Only support utf-8 + /** @deprecated Use `wasmImports` instead */ + this.importObjects = () => this.wasmImports; + this._instance = null; + this._memory = null; + this._closureDeallocator = null; + this.tid = null; + this.options = options || {}; + } + setInstance(instance) { + this._instance = instance; + if (typeof this.exports._start === "function") { + throw new Error(`JavaScriptKit supports only WASI reactor ABI. + Please make sure you are building with: + -Xswiftc -Xclang-linker -Xswiftc -mexec-model=reactor + `); + } + if (this.exports.swjs_library_version() != this.version) { + throw new Error(`The versions of JavaScriptKit are incompatible. + WebAssembly runtime ${this.exports.swjs_library_version()} != JS runtime ${this.version}`); + } + } + main() { + const instance = this.instance; + try { + if (typeof instance.exports.main === "function") { + instance.exports.main(); + } + else if (typeof instance.exports.__main_argc_argv === "function") { + // Swift 6.0 and later use `__main_argc_argv` instead of `main`. + instance.exports.__main_argc_argv(0, 0); + } + } + catch (error) { + if (error instanceof UnsafeEventLoopYield) { + // Ignore the error + return; + } + // Rethrow other errors + throw error; + } + } + /** + * Start a new thread with the given `tid` and `startArg`, which + * is forwarded to the `wasi_thread_start` function. + * This function is expected to be called from the spawned Web Worker thread. + */ + startThread(tid, startArg) { + this.tid = tid; + const instance = this.instance; + try { + if (typeof instance.exports.wasi_thread_start === "function") { + instance.exports.wasi_thread_start(tid, startArg); + } + else { + throw new Error(`The WebAssembly module is not built for wasm32-unknown-wasip1-threads target.`); + } + } + catch (error) { + if (error instanceof UnsafeEventLoopYield) { + // Ignore the error + return; + } + // Rethrow other errors + throw error; + } + } + get instance() { + if (!this._instance) + throw new Error("WebAssembly instance is not set yet"); + return this._instance; + } + get exports() { + return this.instance.exports; + } + get memory() { + if (!this._memory) { + this._memory = new Memory(this.instance.exports); + } + return this._memory; + } + get closureDeallocator() { + if (this._closureDeallocator) + return this._closureDeallocator; + const features = this.exports.swjs_library_features(); + const librarySupportsWeakRef = (features & 1 /* WeakRefs */) != 0; + if (librarySupportsWeakRef) { + this._closureDeallocator = new SwiftClosureDeallocator(this.exports); + } + return this._closureDeallocator; + } + callHostFunction(host_func_id, line, file, args) { + const argc = args.length; + const argv = this.exports.swjs_prepare_host_function_call(argc); + const memory = this.memory; + for (let index = 0; index < args.length; index++) { + const argument = args[index]; + const base = argv + 16 * index; + write(argument, base, base + 4, base + 8, false, memory); + } + let output; + // This ref is released by the swjs_call_host_function implementation + const callback_func_ref = memory.retain((result) => { + output = result; + }); + const alreadyReleased = this.exports.swjs_call_host_function(host_func_id, argv, argc, callback_func_ref); + if (alreadyReleased) { + throw new Error(`The JSClosure has been already released by Swift side. The closure is created at ${file}:${line}`); + } + this.exports.swjs_cleanup_host_function_call(argv); + return output; + } + get wasmImports() { + return { + swjs_set_prop: (ref, name, kind, payload1, payload2) => { + const memory = this.memory; + const obj = memory.getObject(ref); + const key = memory.getObject(name); + const value = decode(kind, payload1, payload2, memory); + obj[key] = value; + }, + swjs_get_prop: (ref, name, payload1_ptr, payload2_ptr) => { + const memory = this.memory; + const obj = memory.getObject(ref); + const key = memory.getObject(name); + const result = obj[key]; + return writeAndReturnKindBits(result, payload1_ptr, payload2_ptr, false, memory); + }, + swjs_set_subscript: (ref, index, kind, payload1, payload2) => { + const memory = this.memory; + const obj = memory.getObject(ref); + const value = decode(kind, payload1, payload2, memory); + obj[index] = value; + }, + swjs_get_subscript: (ref, index, payload1_ptr, payload2_ptr) => { + const obj = this.memory.getObject(ref); + const result = obj[index]; + return writeAndReturnKindBits(result, payload1_ptr, payload2_ptr, false, this.memory); + }, + swjs_encode_string: (ref, bytes_ptr_result) => { + const memory = this.memory; + const bytes = this.textEncoder.encode(memory.getObject(ref)); + const bytes_ptr = memory.retain(bytes); + memory.writeUint32(bytes_ptr_result, bytes_ptr); + return bytes.length; + }, + swjs_decode_string: ( + // NOTE: TextDecoder can't decode typed arrays backed by SharedArrayBuffer + this.options.sharedMemory == true + ? ((bytes_ptr, length) => { + const memory = this.memory; + const bytes = memory + .bytes() + .slice(bytes_ptr, bytes_ptr + length); + const string = this.textDecoder.decode(bytes); + return memory.retain(string); + }) + : ((bytes_ptr, length) => { + const memory = this.memory; + const bytes = memory + .bytes() + .subarray(bytes_ptr, bytes_ptr + length); + const string = this.textDecoder.decode(bytes); + return memory.retain(string); + })), + swjs_load_string: (ref, buffer) => { + const memory = this.memory; + const bytes = memory.getObject(ref); + memory.writeBytes(buffer, bytes); + }, + swjs_call_function: (ref, argv, argc, payload1_ptr, payload2_ptr) => { + const memory = this.memory; + const func = memory.getObject(ref); + let result = undefined; + try { + const args = decodeArray(argv, argc, memory); + result = func(...args); + } + catch (error) { + return writeAndReturnKindBits(error, payload1_ptr, payload2_ptr, true, this.memory); + } + return writeAndReturnKindBits(result, payload1_ptr, payload2_ptr, false, this.memory); + }, + swjs_call_function_no_catch: (ref, argv, argc, payload1_ptr, payload2_ptr) => { + const memory = this.memory; + const func = memory.getObject(ref); + const args = decodeArray(argv, argc, memory); + const result = func(...args); + return writeAndReturnKindBits(result, payload1_ptr, payload2_ptr, false, this.memory); + }, + swjs_call_function_with_this: (obj_ref, func_ref, argv, argc, payload1_ptr, payload2_ptr) => { + const memory = this.memory; + const obj = memory.getObject(obj_ref); + const func = memory.getObject(func_ref); + let result; + try { + const args = decodeArray(argv, argc, memory); + result = func.apply(obj, args); + } + catch (error) { + return writeAndReturnKindBits(error, payload1_ptr, payload2_ptr, true, this.memory); + } + return writeAndReturnKindBits(result, payload1_ptr, payload2_ptr, false, this.memory); + }, + swjs_call_function_with_this_no_catch: (obj_ref, func_ref, argv, argc, payload1_ptr, payload2_ptr) => { + const memory = this.memory; + const obj = memory.getObject(obj_ref); + const func = memory.getObject(func_ref); + let result = undefined; + const args = decodeArray(argv, argc, memory); + result = func.apply(obj, args); + return writeAndReturnKindBits(result, payload1_ptr, payload2_ptr, false, this.memory); + }, + swjs_call_new: (ref, argv, argc) => { + const memory = this.memory; + const constructor = memory.getObject(ref); + const args = decodeArray(argv, argc, memory); + const instance = new constructor(...args); + return this.memory.retain(instance); + }, + swjs_call_throwing_new: (ref, argv, argc, exception_kind_ptr, exception_payload1_ptr, exception_payload2_ptr) => { + let memory = this.memory; + const constructor = memory.getObject(ref); + let result; + try { + const args = decodeArray(argv, argc, memory); + result = new constructor(...args); + } + catch (error) { + write(error, exception_kind_ptr, exception_payload1_ptr, exception_payload2_ptr, true, this.memory); + return -1; + } + memory = this.memory; + write(null, exception_kind_ptr, exception_payload1_ptr, exception_payload2_ptr, false, memory); + return memory.retain(result); + }, + swjs_instanceof: (obj_ref, constructor_ref) => { + const memory = this.memory; + const obj = memory.getObject(obj_ref); + const constructor = memory.getObject(constructor_ref); + return obj instanceof constructor; + }, + swjs_create_function: (host_func_id, line, file) => { + var _a; + const fileString = this.memory.getObject(file); + const func = (...args) => this.callHostFunction(host_func_id, line, fileString, args); + const func_ref = this.memory.retain(func); + (_a = this.closureDeallocator) === null || _a === void 0 ? void 0 : _a.track(func, func_ref); + return func_ref; + }, + swjs_create_typed_array: (constructor_ref, elementsPtr, length) => { + const ArrayType = this.memory.getObject(constructor_ref); + const array = new ArrayType(this.memory.rawMemory.buffer, elementsPtr, length); + // Call `.slice()` to copy the memory + return this.memory.retain(array.slice()); + }, + swjs_load_typed_array: (ref, buffer) => { + const memory = this.memory; + const typedArray = memory.getObject(ref); + const bytes = new Uint8Array(typedArray.buffer); + memory.writeBytes(buffer, bytes); + }, + swjs_release: (ref) => { + this.memory.release(ref); + }, + swjs_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."); + } + threadChannel.listenMessageFromMainThread((message) => { + switch (message.type) { + case "wake": + this.exports.swjs_wake_worker_thread(); + break; + default: + const unknownMessage = message.type; + 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."); + } + threadChannel.listenMessageFromWorkerThread(tid, (message) => { + switch (message.type) { + case "job": + this.exports.swjs_enqueue_main_job_from_worker(message.data); + break; + default: + const unknownMessage = message.type; + 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; + }, + }; + } + postMessageToMainThread(message) { + 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); + } + postMessageToWorkerThread(tid, message) { + 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); + } +} +/// 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/Examples/Embedded/build.sh b/Examples/Embedded/build.sh new file mode 100755 index 000000000..e62caab6c --- /dev/null +++ b/Examples/Embedded/build.sh @@ -0,0 +1,12 @@ +EXPERIMENTAL_EMBEDDED_WASM=true swift build -c release --product EmbeddedApp \ + --triple wasm32-unknown-none-wasm \ + -Xswiftc -enable-experimental-feature -Xswiftc Embedded \ + -Xswiftc -enable-experimental-feature -Xswiftc Extern \ + -Xswiftc -wmo -Xswiftc -disable-cmo \ + -Xswiftc -Xfrontend -Xswiftc -gnone \ + -Xswiftc -Xfrontend -Xswiftc -disable-stack-protector \ + -Xswiftc -cxx-interoperability-mode=default \ + -Xcc -D__Embedded -Xcc -fdeclspec \ + -Xlinker --export-if-defined=__main_argc_argv \ + -Xlinker --export-if-defined=swjs_call_host_function \ + -Xswiftc -Xclang-linker -Xswiftc -mexec-model=reactor \ No newline at end of file diff --git a/Examples/Embedded/index.html b/Examples/Embedded/index.html new file mode 100644 index 000000000..d94796a09 --- /dev/null +++ b/Examples/Embedded/index.html @@ -0,0 +1,12 @@ + + + + + Getting Started + + + + + + + diff --git a/Examples/Embedded/index.js b/Examples/Embedded/index.js new file mode 100644 index 000000000..b95576135 --- /dev/null +++ b/Examples/Embedded/index.js @@ -0,0 +1,33 @@ +import { WASI, File, OpenFile, ConsoleStdout, PreopenDirectory } from 'https://esm.run/@bjorn3/browser_wasi_shim@0.3.0'; + +async function main(configuration = "release") { + // Fetch our Wasm File + const response = await fetch(`./.build/${configuration}/EmbeddedApp.wasm`); + // Create a new WASI system instance + const wasi = new WASI(/* args */["main.wasm"], /* env */[], /* fd */[ + new OpenFile(new File([])), // stdin + ConsoleStdout.lineBuffered((stdout) => { + console.log(stdout); + }), + ConsoleStdout.lineBuffered((stderr) => { + console.error(stderr); + }), + new PreopenDirectory("/", new Map()), + ]) + const { SwiftRuntime } = await import(`./_Runtime/index.mjs`); + // Create a new Swift Runtime instance to interact with JS and Swift + const swift = new SwiftRuntime(); + // Instantiate the WebAssembly file + const { instance } = await WebAssembly.instantiateStreaming(response, { + //wasi_snapshot_preview1: wasi.wasiImport, + javascript_kit: swift.wasmImports, + }); + // Set the WebAssembly instance to the Swift Runtime + swift.setInstance(instance); + // Start the WebAssembly WASI reactor instance + wasi.initialize(instance); + // Start Swift main function + swift.main() +}; + +main(); diff --git a/Package.swift b/Package.swift index fb7e5b4e5..fd9e84e36 100644 --- a/Package.swift +++ b/Package.swift @@ -1,6 +1,10 @@ // swift-tools-version:5.7 import PackageDescription +import Foundation + +// NOTE: needed for embedded customizations, ideally this will not be necessary at all in the future, or can be replaced with traits +let shouldBuildForEmbedded = ProcessInfo.processInfo.environment["EXPERIMENTAL_EMBEDDED_WASM"].flatMap(Bool.init) ?? false let package = Package( name: "JavaScriptKit", @@ -13,9 +17,11 @@ let package = Package( targets: [ .target( name: "JavaScriptKit", - dependencies: ["_CJavaScriptKit"], - //LES: TODO - make this conditional - // resources: [.copy("Runtime")] + dependencies: ["_CJavaScriptKit"], + resources: shouldBuildForEmbedded ? [] : [.copy("Runtime")], + swiftSettings: shouldBuildForEmbedded + ? [.unsafeFlags(["-Xfrontend", "-emit-empty-object-file"])] + : [] ), .target(name: "_CJavaScriptKit"), .target( diff --git a/Sources/JavaScriptKit/Features.swift b/Sources/JavaScriptKit/Features.swift index e479003c5..81bf6f9cf 100644 --- a/Sources/JavaScriptKit/Features.swift +++ b/Sources/JavaScriptKit/Features.swift @@ -2,8 +2,8 @@ enum LibraryFeatures { static let weakRefs: Int32 = 1 << 0 } -@_cdecl("_library_features") -func _library_features() -> Int32 { +@_expose(wasm, "swjs_library_features") +public func _library_features() -> Int32 { var features: Int32 = 0 #if !JAVASCRIPTKIT_WITHOUT_WEAKREFS features |= LibraryFeatures.weakRefs diff --git a/Sources/JavaScriptKit/FundamentalObjects/JSClosure.swift b/Sources/JavaScriptKit/FundamentalObjects/JSClosure.swift index a7a93ba75..c0a7c7885 100644 --- a/Sources/JavaScriptKit/FundamentalObjects/JSClosure.swift +++ b/Sources/JavaScriptKit/FundamentalObjects/JSClosure.swift @@ -186,8 +186,8 @@ private func makeAsyncClosure(_ body: @escaping ([JSValue]) async throws -> JSVa // └─────────────────────┴──────────────────────────┘ /// Returns true if the host function has been already released, otherwise false. -@_cdecl("_call_host_function_impl") -func _call_host_function_impl( +@_expose(wasm, "swjs_call_host_function") +public func _call_host_function_impl( _ hostFuncRef: JavaScriptHostFuncRef, _ argv: UnsafePointer, _ argc: Int32, _ callbackFuncRef: JavaScriptObjectRef @@ -217,8 +217,8 @@ extension JSClosure { } } -@_cdecl("_free_host_function_impl") -func _free_host_function_impl(_ hostFuncRef: JavaScriptHostFuncRef) {} +@_expose(wasm, "swjs_free_host_function") +public func _free_host_function_impl(_ hostFuncRef: JavaScriptHostFuncRef) {} #else @@ -229,8 +229,8 @@ extension JSClosure { } -@_cdecl("_free_host_function_impl") -func _free_host_function_impl(_ hostFuncRef: JavaScriptHostFuncRef) { +@_expose(wasm, "swjs_free_host_function") +public func _free_host_function_impl(_ hostFuncRef: JavaScriptHostFuncRef) { JSClosure.sharedClosures[hostFuncRef] = nil } #endif diff --git a/Sources/JavaScriptKit/FundamentalObjects/JSFunction.swift b/Sources/JavaScriptKit/FundamentalObjects/JSFunction.swift index f918687f3..ae5a4aa49 100644 --- a/Sources/JavaScriptKit/FundamentalObjects/JSFunction.swift +++ b/Sources/JavaScriptKit/FundamentalObjects/JSFunction.swift @@ -258,6 +258,6 @@ private func valueKindAndFlagsFromBits(_ bits: UInt32) -> JavaScriptValueKindAnd #if hasFeature(Embedded) JavaScriptValueKindAndFlags(bitPattern: bits) #else - unsafeBitCast(resultBitPattern, to: JavaScriptValueKindAndFlags.self) + unsafeBitCast(bits, to: JavaScriptValueKindAndFlags.self) #endif } \ No newline at end of file diff --git a/Sources/JavaScriptKit/JSValue.swift b/Sources/JavaScriptKit/JSValue.swift index 5166986ff..fe1400e24 100644 --- a/Sources/JavaScriptKit/JSValue.swift +++ b/Sources/JavaScriptKit/JSValue.swift @@ -270,3 +270,22 @@ extension JSValue: CustomStringConvertible { JSObject.global.String.function!(self).string! } } + +#if hasFeature(Embedded) +public extension JSValue { + @_disfavoredOverload + subscript(dynamicMember name: String) -> (() -> JSValue) { + object![dynamicMember: name]! + } + + @_disfavoredOverload + subscript(dynamicMember name: String) -> ((A0) -> JSValue) { + object![dynamicMember: name]! + } + + @_disfavoredOverload + subscript(dynamicMember name: String) -> ((A0, A1) -> JSValue) { + object![dynamicMember: name]! + } +} +#endif \ No newline at end of file diff --git a/Sources/_CJavaScriptKit/_CJavaScriptKit.c b/Sources/_CJavaScriptKit/_CJavaScriptKit.c index c658bd545..934b4639b 100644 --- a/Sources/_CJavaScriptKit/_CJavaScriptKit.c +++ b/Sources/_CJavaScriptKit/_CJavaScriptKit.c @@ -14,24 +14,6 @@ extern void *memcpy (void *__restrict, const void *__restrict, size_t); #endif -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"))) -bool swjs_call_host_function(const JavaScriptHostFuncRef host_func_ref, - const RawJSValue *argv, const int argc, - const JavaScriptObjectRef callback_func) { - return _call_host_function_impl(host_func_ref, argv, argc, callback_func); -} - -void _free_host_function_impl(const JavaScriptHostFuncRef host_func_ref); - -__attribute__((export_name("swjs_free_host_function"))) -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)); @@ -50,13 +32,6 @@ int swjs_library_version(void) { return 708; } -int _library_features(void); - -__attribute__((export_name("swjs_library_features"))) -int swjs_library_features(void) { - return _library_features(); -} - #endif _Thread_local void *swjs_thread_local_closures; diff --git a/swift-build-embedded b/swift-build-embedded deleted file mode 100755 index 247658466..000000000 --- a/swift-build-embedded +++ /dev/null @@ -1,34 +0,0 @@ -swift build --target JavaScriptKit \ - --triple wasm32-unknown-none-wasm \ - -Xswiftc -enable-experimental-feature -Xswiftc Embedded \ - -Xswiftc -enable-experimental-feature -Xswiftc Extern \ - -Xswiftc -wmo -Xswiftc -disable-cmo \ - -Xswiftc -Xfrontend -Xswiftc -gnone \ - -Xswiftc -Xfrontend -Xswiftc -disable-stack-protector \ - -Xswiftc -cxx-interoperability-mode=default \ - -Xcc -D__Embedded -Xcc -fdeclspec \ - -Xlinker --export-if-defined=__main_argc_argv \ - -Xswiftc -Xclang-linker -Xswiftc -mexec-model=reactor - - - # -Xlinker --export-if-defined=_initialize - # -Xswiftc -static-stdlib \ - - # --verbose - - # -Xswiftc -Xclang-linker -Xswiftc -nostdlib \ - -####### -# swift build -c release --product ExampleApp \ -# --sdk DEVELOPMENT-SNAPSHOT-2024-09-20-a-wasm32-unknown-wasi \ -# --triple wasm32-unknown-none-wasm \ -# -Xcc -D__Embedded \ -# -Xswiftc -enable-experimental-feature -Xswiftc Embedded \ -# -Xswiftc -enable-experimental-feature -Xswiftc Extern \ - # -Xswiftc -wmo -Xswiftc -disable-cmo \ - # -Xcc -fdeclspec \ - # -Xswiftc -Xclang-linker -Xswiftc -mexec-model=reactor \ - # -Xlinker --export-if-defined=__main_argc_argv \ - - -#cp .build/release/ExampleApp.wasm ./TestPage/app.wasm \ No newline at end of file From b01062425ba835744f247ca162a56242c4bc26f2 Mon Sep 17 00:00:00 2001 From: Simon Leeb <52261246+sliemeobn@users.noreply.github.com> Date: Tue, 8 Oct 2024 13:02:37 +0200 Subject: [PATCH 149/373] ...memmove, we meet again --- .../EmbeddedApp/_thingsThatShouldNotBeNeeded.swift | 11 +++++++++++ Examples/Embedded/Sources/EmbeddedApp/main.swift | 11 +++++++---- 2 files changed, 18 insertions(+), 4 deletions(-) diff --git a/Examples/Embedded/Sources/EmbeddedApp/_thingsThatShouldNotBeNeeded.swift b/Examples/Embedded/Sources/EmbeddedApp/_thingsThatShouldNotBeNeeded.swift index 50d838b96..e97965f66 100644 --- a/Examples/Embedded/Sources/EmbeddedApp/_thingsThatShouldNotBeNeeded.swift +++ b/Examples/Embedded/Sources/EmbeddedApp/_thingsThatShouldNotBeNeeded.swift @@ -16,3 +16,14 @@ func strlen(_ s: UnsafePointer) -> Int { } return p - s } + +// 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("memmove") +func memmove(_ dest: UnsafeMutableRawPointer, _ src: UnsafeRawPointer, _ n: Int) -> UnsafeMutableRawPointer { + let d = dest.assumingMemoryBound(to: UInt8.self) + let s = src.assumingMemoryBound(to: UInt8.self) + for i in 0.. Date: Tue, 8 Oct 2024 17:37:22 +0200 Subject: [PATCH 150/373] readme --- Examples/Embedded/README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Examples/Embedded/README.md b/Examples/Embedded/README.md index 92dd6be40..2f388fcdc 100644 --- a/Examples/Embedded/README.md +++ b/Examples/Embedded/README.md @@ -1,5 +1,7 @@ # Embedded example +Requires a recent DEVELOPMENT-SNAPSHOT toolchain. (tested with swift-DEVELOPMENT-SNAPSHOT-2024-09-25-a) + ```sh $ ./build.sh $ npx serve From 025b9fc8218c1c539d137d9a287523e6e9e97512 Mon Sep 17 00:00:00 2001 From: Simon Leeb <52261246+sliemeobn@users.noreply.github.com> Date: Tue, 8 Oct 2024 22:29:12 +0200 Subject: [PATCH 151/373] revert to cdecl-based exports for swift 5.10 compatibility --- Examples/Basic/Package.swift | 3 ++ .../_thingsThatShouldNotBeNeeded.swift | 6 +-- Sources/JavaScriptKit/Features.swift | 10 ++++- .../FundamentalObjects/JSClosure.swift | 30 ++++++++++++--- Sources/_CJavaScriptKit/_CJavaScriptKit.c | 38 ++++++++++++++++--- 5 files changed, 70 insertions(+), 17 deletions(-) diff --git a/Examples/Basic/Package.swift b/Examples/Basic/Package.swift index 6484043f5..aade23359 100644 --- a/Examples/Basic/Package.swift +++ b/Examples/Basic/Package.swift @@ -4,6 +4,9 @@ import PackageDescription let package = Package( name: "Basic", + platforms: [ + .macOS(.v14) + ], dependencies: [.package(name: "JavaScriptKit", path: "../../")], targets: [ .executableTarget( diff --git a/Examples/Embedded/Sources/EmbeddedApp/_thingsThatShouldNotBeNeeded.swift b/Examples/Embedded/Sources/EmbeddedApp/_thingsThatShouldNotBeNeeded.swift index e97965f66..20a26e085 100644 --- a/Examples/Embedded/Sources/EmbeddedApp/_thingsThatShouldNotBeNeeded.swift +++ b/Examples/Embedded/Sources/EmbeddedApp/_thingsThatShouldNotBeNeeded.swift @@ -2,9 +2,9 @@ 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() { - _ = _library_features - _ = _call_host_function_impl - _ = _free_host_function_impl + _ = _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 diff --git a/Sources/JavaScriptKit/Features.swift b/Sources/JavaScriptKit/Features.swift index 81bf6f9cf..112f3e943 100644 --- a/Sources/JavaScriptKit/Features.swift +++ b/Sources/JavaScriptKit/Features.swift @@ -2,11 +2,17 @@ enum LibraryFeatures { static let weakRefs: Int32 = 1 << 0 } -@_expose(wasm, "swjs_library_features") -public func _library_features() -> Int32 { +@_cdecl("_library_features") +func _library_features() -> Int32 { var features: Int32 = 0 #if !JAVASCRIPTKIT_WITHOUT_WEAKREFS features |= LibraryFeatures.weakRefs #endif return features } + +#if 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 \ No newline at end of file diff --git a/Sources/JavaScriptKit/FundamentalObjects/JSClosure.swift b/Sources/JavaScriptKit/FundamentalObjects/JSClosure.swift index c0a7c7885..1686f864a 100644 --- a/Sources/JavaScriptKit/FundamentalObjects/JSClosure.swift +++ b/Sources/JavaScriptKit/FundamentalObjects/JSClosure.swift @@ -186,8 +186,8 @@ private func makeAsyncClosure(_ body: @escaping ([JSValue]) async throws -> JSVa // └─────────────────────┴──────────────────────────┘ /// Returns true if the host function has been already released, otherwise false. -@_expose(wasm, "swjs_call_host_function") -public func _call_host_function_impl( +@_cdecl("_call_host_function_impl") +func _call_host_function_impl( _ hostFuncRef: JavaScriptHostFuncRef, _ argv: UnsafePointer, _ argc: Int32, _ callbackFuncRef: JavaScriptObjectRef @@ -217,8 +217,9 @@ extension JSClosure { } } -@_expose(wasm, "swjs_free_host_function") -public func _free_host_function_impl(_ hostFuncRef: JavaScriptHostFuncRef) {} + +@_cdecl("_free_host_function_impl") +func _free_host_function_impl(_ hostFuncRef: JavaScriptHostFuncRef) {} #else @@ -229,8 +230,25 @@ extension JSClosure { } -@_expose(wasm, "swjs_free_host_function") -public func _free_host_function_impl(_ hostFuncRef: JavaScriptHostFuncRef) { +@_cdecl("_free_host_function_impl") +func _free_host_function_impl(_ hostFuncRef: JavaScriptHostFuncRef) { JSClosure.sharedClosures[hostFuncRef] = nil } #endif + +#if 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 \ No newline at end of file diff --git a/Sources/_CJavaScriptKit/_CJavaScriptKit.c b/Sources/_CJavaScriptKit/_CJavaScriptKit.c index 934b4639b..6fc3fa916 100644 --- a/Sources/_CJavaScriptKit/_CJavaScriptKit.c +++ b/Sources/_CJavaScriptKit/_CJavaScriptKit.c @@ -13,6 +13,13 @@ extern void *memcpy (void *__restrict, const void *__restrict, size_t); #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; +} __attribute__((export_name("swjs_prepare_host_function_call"))) void *swjs_prepare_host_function_call(const int argc) { @@ -24,14 +31,33 @@ 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 708; +#ifndef __Embedded +// cdecls don't work in Embedded, also @_expose(wasm) can be used with Swift >=6.0 +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"))) +bool swjs_call_host_function(const JavaScriptHostFuncRef host_func_ref, + const RawJSValue *argv, const int argc, + const JavaScriptObjectRef callback_func) { + return _call_host_function_impl(host_func_ref, argv, argc, callback_func); +} + +void _free_host_function_impl(const JavaScriptHostFuncRef host_func_ref); + +__attribute__((export_name("swjs_free_host_function"))) +void swjs_free_host_function(const JavaScriptHostFuncRef host_func_ref) { + _free_host_function_impl(host_func_ref); } +int _library_features(void); + +__attribute__((export_name("swjs_library_features"))) +int swjs_library_features(void) { + return _library_features(); +} +#endif #endif _Thread_local void *swjs_thread_local_closures; From 77bbc8c3eed1b639614c8cf3bde3d484a36b655c Mon Sep 17 00:00:00 2001 From: Simon Leeb <52261246+sliemeobn@users.noreply.github.com> Date: Wed, 9 Oct 2024 10:10:41 +0200 Subject: [PATCH 152/373] added compiler gates for @_expose --- Sources/JavaScriptKit/Features.swift | 2 +- Sources/JavaScriptKit/FundamentalObjects/JSClosure.swift | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Sources/JavaScriptKit/Features.swift b/Sources/JavaScriptKit/Features.swift index 112f3e943..db6e00f26 100644 --- a/Sources/JavaScriptKit/Features.swift +++ b/Sources/JavaScriptKit/Features.swift @@ -11,7 +11,7 @@ func _library_features() -> Int32 { return features } -#if hasFeature(Embedded) +#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() } diff --git a/Sources/JavaScriptKit/FundamentalObjects/JSClosure.swift b/Sources/JavaScriptKit/FundamentalObjects/JSClosure.swift index 1686f864a..5d367ba38 100644 --- a/Sources/JavaScriptKit/FundamentalObjects/JSClosure.swift +++ b/Sources/JavaScriptKit/FundamentalObjects/JSClosure.swift @@ -236,7 +236,7 @@ func _free_host_function_impl(_ hostFuncRef: JavaScriptHostFuncRef) { } #endif -#if hasFeature(Embedded) +#if compiler(>=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( From 7f2f7c667243724d34d8bc6733ed05f62f70eea9 Mon Sep 17 00:00:00 2001 From: Simon Leeb <52261246+sliemeobn@users.noreply.github.com> Date: Wed, 9 Oct 2024 13:32:37 +0200 Subject: [PATCH 153/373] less embedded conditions and added comments --- .../FundamentalObjects/JSFunction.swift | 12 +++++++----- .../JavaScriptKit/FundamentalObjects/JSObject.swift | 4 ++++ .../JavaScriptKit/FundamentalObjects/JSString.swift | 5 +---- .../JavaScriptKit/FundamentalObjects/JSSymbol.swift | 4 ---- 4 files changed, 12 insertions(+), 13 deletions(-) diff --git a/Sources/JavaScriptKit/FundamentalObjects/JSFunction.swift b/Sources/JavaScriptKit/FundamentalObjects/JSFunction.swift index ae5a4aa49..443063981 100644 --- a/Sources/JavaScriptKit/FundamentalObjects/JSFunction.swift +++ b/Sources/JavaScriptKit/FundamentalObjects/JSFunction.swift @@ -84,8 +84,8 @@ public class JSFunction: JSObject, _JSFunctionProtocol { public var `throws`: JSThrowingFunction { JSThrowingFunction(self) } +#endif -#else @discardableResult public func callAsFunction(arguments: [JSValue]) -> JSValue { invokeNonThrowingJSFunction(arguments: arguments).jsValue @@ -98,7 +98,6 @@ public class JSFunction: JSObject, _JSFunctionProtocol { } } } -#endif @available(*, unavailable, message: "Please use JSClosure instead") public static func from(_: @escaping ([JSValue]) -> JSValue) -> JSFunction { @@ -109,7 +108,6 @@ public class JSFunction: JSObject, _JSFunctionProtocol { .function(self) } -#if hasFeature(Embedded) final func invokeNonThrowingJSFunction(arguments: [JSValue]) -> RawJSValue { arguments.withRawJSValues { invokeNonThrowingJSFunction(rawValues: $0) } } @@ -117,7 +115,8 @@ public class JSFunction: JSObject, _JSFunctionProtocol { final func invokeNonThrowingJSFunction(arguments: [JSValue], this: JSObject) -> RawJSValue { arguments.withRawJSValues { invokeNonThrowingJSFunction(rawValues: $0, this: this) } } -#else + +#if !hasFeature(Embedded) final func invokeNonThrowingJSFunction(arguments: [ConvertibleToJSValue]) -> RawJSValue { arguments.withRawJSValues { invokeNonThrowingJSFunction(rawValues: $0) } } @@ -164,11 +163,14 @@ public class JSFunction: JSObject, _JSFunctionProtocol { } } +/// Internal protocol to support generic arguments for `JSFunction`. +/// +/// In Swift Embedded, non-final classes cannot have generic methods. public protocol _JSFunctionProtocol: JSFunction {} #if hasFeature(Embedded) +// NOTE: once embedded supports variadic generics, we can remove these overloads public extension _JSFunctionProtocol { - // hand-made "varidacs" for Embedded @discardableResult func callAsFunction(this: JSObject) -> JSValue { diff --git a/Sources/JavaScriptKit/FundamentalObjects/JSObject.swift b/Sources/JavaScriptKit/FundamentalObjects/JSObject.swift index 3891520c8..6d8442540 100644 --- a/Sources/JavaScriptKit/FundamentalObjects/JSObject.swift +++ b/Sources/JavaScriptKit/FundamentalObjects/JSObject.swift @@ -231,10 +231,14 @@ public class JSThrowingObject { } #endif +/// Internal protocol to support generic arguments for `JSObject`. +/// +/// In Swift Embedded, non-final classes cannot have generic methods. public protocol _JSObjectProtocol: JSObject { } #if hasFeature(Embedded) +// NOTE: once embedded supports variadic generics, we can remove these overloads public extension _JSObjectProtocol { @_disfavoredOverload subscript(dynamicMember name: String) -> (() -> JSValue)? { diff --git a/Sources/JavaScriptKit/FundamentalObjects/JSString.swift b/Sources/JavaScriptKit/FundamentalObjects/JSString.swift index 2eec4ef42..686d1ba11 100644 --- a/Sources/JavaScriptKit/FundamentalObjects/JSString.swift +++ b/Sources/JavaScriptKit/FundamentalObjects/JSString.swift @@ -31,12 +31,9 @@ public struct JSString: LosslessStringConvertible, Equatable { var bytesRef: JavaScriptObjectRef = 0 let bytesLength = Int(swjs_encode_string(jsRef, &bytesRef)) // +1 for null terminator - // TODO: revert this back to malloc and free - // let buffer = malloc(Int(bytesLength + 1))!.assumingMemoryBound(to: UInt8.self) - let buffer = UnsafeMutablePointer.allocate(capacity: Int(bytesLength + 1)) + let buffer = UnsafeMutablePointer.allocate(capacity: bytesLength + 1) defer { buffer.deallocate() - // free(buffer) swjs_release(bytesRef) } swjs_load_string(bytesRef, buffer) diff --git a/Sources/JavaScriptKit/FundamentalObjects/JSSymbol.swift b/Sources/JavaScriptKit/FundamentalObjects/JSSymbol.swift index e7d1a2fd2..d768b6675 100644 --- a/Sources/JavaScriptKit/FundamentalObjects/JSSymbol.swift +++ b/Sources/JavaScriptKit/FundamentalObjects/JSSymbol.swift @@ -10,11 +10,7 @@ public class JSSymbol: JSObject { public init(_ description: JSString) { // can’t do `self =` so we have to get the ID manually - #if hasFeature(Embedded) let result = Symbol.invokeNonThrowingJSFunction(arguments: [description.jsValue]) - #else - let result = Symbol.invokeNonThrowingJSFunction(arguments: [description]) - #endif precondition(result.kind == .symbol) super.init(id: UInt32(result.payload1)) } From 69c58dda72f4c1161f06012969819023b033f22b Mon Sep 17 00:00:00 2001 From: Simon Leeb <52261246+sliemeobn@users.noreply.github.com> Date: Wed, 9 Oct 2024 14:41:44 +0200 Subject: [PATCH 154/373] added explicit returns for 5.8 compatibility --- Sources/JavaScriptKit/BasicObjects/JSTimer.swift | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/Sources/JavaScriptKit/BasicObjects/JSTimer.swift b/Sources/JavaScriptKit/BasicObjects/JSTimer.swift index a5d7c5b8e..d2eee6fcc 100644 --- a/Sources/JavaScriptKit/BasicObjects/JSTimer.swift +++ b/Sources/JavaScriptKit/BasicObjects/JSTimer.swift @@ -17,10 +17,8 @@ public final class JSTimer { var jsValue: JSValue { switch self { - case .oneshot(let closure): - closure.jsValue - case .repeating(let closure): - closure.jsValue + case .oneshot(let closure): return closure.jsValue + case .repeating(let closure): return closure.jsValue } } From 6d4a11417d894873acf080e7b6452a4914a97126 Mon Sep 17 00:00:00 2001 From: Yuta Saito Date: Thu, 10 Oct 2024 00:48:20 +0900 Subject: [PATCH 155/373] [skip ci] We no longer publish to npm --- .github/workflows/npm-publish.yml | 22 ---------------------- 1 file changed, 22 deletions(-) delete mode 100644 .github/workflows/npm-publish.yml diff --git a/.github/workflows/npm-publish.yml b/.github/workflows/npm-publish.yml deleted file mode 100644 index 96ef37a64..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@v4 - - uses: actions/setup-node@v4 - with: - node-version: 12 - registry-url: https://registry.npmjs.org/ - - run: npm ci - - run: npm publish - env: - NODE_AUTH_TOKEN: ${{secrets.npm_token}} From 0dbd39a79770c9b9ca4d952b074f188bd26fee21 Mon Sep 17 00:00:00 2001 From: Yuta Saito Date: Tue, 15 Oct 2024 07:28:40 +0900 Subject: [PATCH 156/373] Fix build for embedded platforms around JSBigIntExtended existential --- Sources/JavaScriptKit/ConstructibleFromJSValue.swift | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/Sources/JavaScriptKit/ConstructibleFromJSValue.swift b/Sources/JavaScriptKit/ConstructibleFromJSValue.swift index ce1e1c25f..f0e0ad431 100644 --- a/Sources/JavaScriptKit/ConstructibleFromJSValue.swift +++ b/Sources/JavaScriptKit/ConstructibleFromJSValue.swift @@ -40,7 +40,7 @@ extension SignedInteger where Self: ConstructibleFromJSValue { /// 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: JSBigIntExtended) { + public init?(exactly bigInt: some JSBigIntExtended) { self.init(exactly: bigInt.int64Value) } @@ -49,7 +49,7 @@ extension SignedInteger where Self: ConstructibleFromJSValue { /// Crash if the value is too large to fit in the `Self` type. /// /// - Parameter bigInt: The `JSBigIntExtended` to decode - public init(_ bigInt: JSBigIntExtended) { + public init(_ bigInt: some JSBigIntExtended) { self.init(bigInt.int64Value) } @@ -68,9 +68,11 @@ extension SignedInteger where Self: ConstructibleFromJSValue { 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 } } @@ -87,7 +89,7 @@ extension UnsignedInteger where Self: ConstructibleFromJSValue { /// 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: JSBigIntExtended) { + public init?(exactly bigInt: some JSBigIntExtended) { self.init(exactly: bigInt.uInt64Value) } @@ -96,7 +98,7 @@ extension UnsignedInteger where Self: ConstructibleFromJSValue { /// Crash if the value is negative or too large to fit in the `Self` type. /// /// - Parameter bigInt: The `JSBigIntExtended` to decode - public init(_ bigInt: JSBigIntExtended) { + public init(_ bigInt: some JSBigIntExtended) { self.init(bigInt.uInt64Value) } @@ -114,9 +116,11 @@ extension UnsignedInteger where Self: ConstructibleFromJSValue { 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 } } From a4636bbc5537216d59246b7c6de07edb6b0752c0 Mon Sep 17 00:00:00 2001 From: Yuta Saito Date: Tue, 15 Oct 2024 11:12:07 +0900 Subject: [PATCH 157/373] Simplify the embedded example * Remove EmbeddedFoundation dependency * Prefix the experimental feature flags with `JAVASCRIPTKIT_` * Remove the unnecessary C shim for `memmove`, which is now provided by `swift-dlmalloc` * Add the `arc4random_buf` shim for the latest Hashable support --- Examples/Embedded/Package.swift | 21 +++++++++++++++++-- .../_thingsThatShouldNotBeNeeded.swift | 14 +++++-------- Examples/Embedded/build.sh | 8 ++----- Package.swift | 2 +- 4 files changed, 27 insertions(+), 18 deletions(-) diff --git a/Examples/Embedded/Package.swift b/Examples/Embedded/Package.swift index f0c03bd87..4ebc6e841 100644 --- a/Examples/Embedded/Package.swift +++ b/Examples/Embedded/Package.swift @@ -6,14 +6,31 @@ let package = Package( name: "Embedded", dependencies: [ .package(name: "JavaScriptKit", path: "../../"), - .package(url: "https://github.com/swifweb/EmbeddedFoundation", branch: "0.1.0") + .package(url: "https://github.com/swiftwasm/swift-dlmalloc", branch: "0.1.0") ], targets: [ .executableTarget( name: "EmbeddedApp", dependencies: [ "JavaScriptKit", - .product(name: "Foundation", package: "EmbeddedFoundation") + .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" + ]) ] ) ] diff --git a/Examples/Embedded/Sources/EmbeddedApp/_thingsThatShouldNotBeNeeded.swift b/Examples/Embedded/Sources/EmbeddedApp/_thingsThatShouldNotBeNeeded.swift index 20a26e085..773f928d8 100644 --- a/Examples/Embedded/Sources/EmbeddedApp/_thingsThatShouldNotBeNeeded.swift +++ b/Examples/Embedded/Sources/EmbeddedApp/_thingsThatShouldNotBeNeeded.swift @@ -17,13 +17,9 @@ func strlen(_ s: UnsafePointer) -> Int { return p - s } -// 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("memmove") -func memmove(_ dest: UnsafeMutableRawPointer, _ src: UnsafeRawPointer, _ n: Int) -> UnsafeMutableRawPointer { - let d = dest.assumingMemoryBound(to: UInt8.self) - let s = src.assumingMemoryBound(to: UInt8.self) - for i in 0.. Date: Tue, 15 Oct 2024 11:46:49 +0900 Subject: [PATCH 158/373] Unify `JavaScriptValueKindAndFlags` type across non-/Embedded builds --- .../FundamentalObjects/JSFunction.swift | 24 ++++++------------- .../JSThrowingFunction.swift | 13 +++++----- .../_CJavaScriptKit/include/_CJavaScriptKit.h | 12 ++-------- 3 files changed, 16 insertions(+), 33 deletions(-) diff --git a/Sources/JavaScriptKit/FundamentalObjects/JSFunction.swift b/Sources/JavaScriptKit/FundamentalObjects/JSFunction.swift index 443063981..4620a3aa7 100644 --- a/Sources/JavaScriptKit/FundamentalObjects/JSFunction.swift +++ b/Sources/JavaScriptKit/FundamentalObjects/JSFunction.swift @@ -136,7 +136,7 @@ public class JSFunction: JSObject, _JSFunctionProtocol { id, argv, Int32(argc), &payload1, &payload2 ) - let kindAndFlags = valueKindAndFlagsFromBits(resultBitPattern) + let kindAndFlags = JavaScriptValueKindAndFlags(bitPattern: resultBitPattern) assert(!kindAndFlags.isException) let result = RawJSValue(kind: kindAndFlags.kind, payload1: payload1, payload2: payload2) return result @@ -153,7 +153,7 @@ public class JSFunction: JSObject, _JSFunctionProtocol { id, argv, Int32(argc), &payload1, &payload2 ) - let kindAndFlags = valueKindAndFlagsFromBits(resultBitPattern) + let kindAndFlags = JavaScriptValueKindAndFlags(bitPattern: resultBitPattern) #if !hasFeature(Embedded) assert(!kindAndFlags.isException) #endif @@ -241,25 +241,15 @@ public extension _JSFunctionProtocol { new(arguments: [arg0.jsValue, arg1.jsValue, arg2.jsValue, arg3.jsValue, arg4.jsValue, arg5.jsValue, arg6.jsValue]) } } +#endif -// C bit fields seem to not work with Embedded -// in "normal mode" this is defined as a C struct -private struct JavaScriptValueKindAndFlags { - let errorBit: UInt32 = 1 << 32 +internal struct JavaScriptValueKindAndFlags { + static var errorBit: UInt32 { 1 << 31 } let kind: JavaScriptValueKind let isException: Bool init(bitPattern: UInt32) { - self.kind = JavaScriptValueKind(rawValue: bitPattern & ~errorBit)! - self.isException = (bitPattern & errorBit) != 0 + self.kind = JavaScriptValueKind(rawValue: bitPattern & ~Self.errorBit)! + self.isException = (bitPattern & Self.errorBit) != 0 } } -#endif - -private func valueKindAndFlagsFromBits(_ bits: UInt32) -> JavaScriptValueKindAndFlags { - #if hasFeature(Embedded) - JavaScriptValueKindAndFlags(bitPattern: bits) - #else - unsafeBitCast(bits, to: JavaScriptValueKindAndFlags.self) - #endif -} \ No newline at end of file diff --git a/Sources/JavaScriptKit/FundamentalObjects/JSThrowingFunction.swift b/Sources/JavaScriptKit/FundamentalObjects/JSThrowingFunction.swift index 95bc2bd9c..8b4fc7cde 100644 --- a/Sources/JavaScriptKit/FundamentalObjects/JSThrowingFunction.swift +++ b/Sources/JavaScriptKit/FundamentalObjects/JSThrowingFunction.swift @@ -42,13 +42,14 @@ public class JSThrowingFunction { let argv = bufferPointer.baseAddress let argc = bufferPointer.count - var exceptionKind = JavaScriptValueKindAndFlags() + var exceptionRawKind = JavaScriptRawValueKindAndFlags() var exceptionPayload1 = JavaScriptPayload1() var exceptionPayload2 = JavaScriptPayload2() let resultObj = swjs_call_throwing_new( self.base.id, argv, Int32(argc), - &exceptionKind, &exceptionPayload1, &exceptionPayload2 + &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) @@ -70,7 +71,7 @@ private func invokeJSFunction(_ jsFunc: JSFunction, arguments: [ConvertibleToJSV rawValues.withUnsafeBufferPointer { bufferPointer -> (JSValue, Bool) in let argv = bufferPointer.baseAddress let argc = bufferPointer.count - var kindAndFlags = JavaScriptValueKindAndFlags() + let kindAndFlags: JavaScriptValueKindAndFlags var payload1 = JavaScriptPayload1() var payload2 = JavaScriptPayload2() if let thisId = this?.id { @@ -78,13 +79,13 @@ private func invokeJSFunction(_ jsFunc: JSFunction, arguments: [ConvertibleToJSV thisId, id, argv, Int32(argc), &payload1, &payload2 ) - kindAndFlags = unsafeBitCast(resultBitPattern, to: JavaScriptValueKindAndFlags.self) + kindAndFlags = JavaScriptValueKindAndFlags(bitPattern: resultBitPattern) } else { let resultBitPattern = swjs_call_function( id, argv, Int32(argc), &payload1, &payload2 ) - kindAndFlags = unsafeBitCast(resultBitPattern, to: JavaScriptValueKindAndFlags.self) + kindAndFlags = JavaScriptValueKindAndFlags(bitPattern: resultBitPattern) } let result = RawJSValue(kind: kindAndFlags.kind, payload1: payload1, payload2: payload2) return (result.jsValue, kindAndFlags.isException) @@ -95,4 +96,4 @@ private func invokeJSFunction(_ jsFunc: JSFunction, arguments: [ConvertibleToJSV } return result } -#endif \ No newline at end of file +#endif diff --git a/Sources/_CJavaScriptKit/include/_CJavaScriptKit.h b/Sources/_CJavaScriptKit/include/_CJavaScriptKit.h index 8daf7cdc6..cac103c3f 100644 --- a/Sources/_CJavaScriptKit/include/_CJavaScriptKit.h +++ b/Sources/_CJavaScriptKit/include/_CJavaScriptKit.h @@ -29,15 +29,7 @@ typedef enum __attribute__((enum_extensibility(closed))) { JavaScriptValueKindBigInt = 8, } JavaScriptValueKind; -#if __Embedded -// something about the bit field widths is not working with embedded -typedef unsigned short JavaScriptValueKindAndFlags; -#else -typedef struct { - JavaScriptValueKind kind: 31; - bool isException: 1; -} JavaScriptValueKindAndFlags; -#endif +typedef uint32_t JavaScriptRawValueKindAndFlags; typedef unsigned JavaScriptPayload1; typedef double JavaScriptPayload2; @@ -253,7 +245,7 @@ IMPORT_JS_FUNCTION(swjs_call_new, JavaScriptObjectRef, (const JavaScriptObjectRe IMPORT_JS_FUNCTION(swjs_call_throwing_new, JavaScriptObjectRef, (const JavaScriptObjectRef ref, const RawJSValue *argv, const int argc, - JavaScriptValueKindAndFlags *exception_kind, + JavaScriptRawValueKindAndFlags *exception_kind, JavaScriptPayload1 *exception_payload1, JavaScriptPayload2 *exception_payload2)) From c326ebff6bf97bc608d5cfb68b8be7ec07cee053 Mon Sep 17 00:00:00 2001 From: Yuta Saito Date: Tue, 15 Oct 2024 11:53:41 +0900 Subject: [PATCH 159/373] Add Embedded build to CI --- .github/workflows/test.yml | 16 ++++++++++++++++ Examples/Embedded/build.sh | 4 +++- 2 files changed, 19 insertions(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index bc07e6a56..8ba892e06 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -86,3 +86,19 @@ jobs: - run: swift build env: DEVELOPER_DIR: /Applications/${{ matrix.xcode }}.app/Contents/Developer/ + + embedded-build: + name: Build for embedded target + runs-on: ubuntu-22.04 + strategy: + matrix: + entry: + - os: ubuntu-22.04 + toolchain: DEVELOPMENT-SNAPSHOT-2024-09-25-a + steps: + - uses: actions/checkout@v4 + - uses: ./.github/actions/install-swift + with: + swift-dir: development/ubuntu2204 + swift-version: swift-${{ matrix.entry.toolchain }} + - run: ./Examples/Embedded/build.sh diff --git a/Examples/Embedded/build.sh b/Examples/Embedded/build.sh index d219136b3..e1c3e3c4e 100755 --- a/Examples/Embedded/build.sh +++ b/Examples/Embedded/build.sh @@ -1,4 +1,6 @@ -JAVASCRIPTKIT_EXPERIMENTAL_EMBEDDED_WASM=true swift build -c release --product EmbeddedApp \ +#!/bin/bash +package_dir="$(cd "$(dirname "$0")" && pwd)" +JAVASCRIPTKIT_EXPERIMENTAL_EMBEDDED_WASM=true swift build --package-path "$package_dir" -c release --product EmbeddedApp \ --triple wasm32-unknown-none-wasm \ -Xswiftc -enable-experimental-feature -Xswiftc Embedded \ -Xswiftc -enable-experimental-feature -Xswiftc Extern \ From 58e5b3c5ae15bc384c846bb347848f0215f59bee Mon Sep 17 00:00:00 2001 From: Yuta Saito Date: Tue, 15 Oct 2024 12:46:10 +0900 Subject: [PATCH 160/373] Remove unnecessary `_JSFunctionProtocol` --- .../JavaScriptKit/FundamentalObjects/JSFunction.swift | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/Sources/JavaScriptKit/FundamentalObjects/JSFunction.swift b/Sources/JavaScriptKit/FundamentalObjects/JSFunction.swift index 4620a3aa7..cbbf4a60f 100644 --- a/Sources/JavaScriptKit/FundamentalObjects/JSFunction.swift +++ b/Sources/JavaScriptKit/FundamentalObjects/JSFunction.swift @@ -10,7 +10,7 @@ import _CJavaScriptKit /// alert("Hello, world") /// ``` /// -public class JSFunction: JSObject, _JSFunctionProtocol { +public class JSFunction: JSObject { #if !hasFeature(Embedded) /// Call this function with given `arguments` and binding given `this` as context. /// - Parameters: @@ -163,14 +163,9 @@ public class JSFunction: JSObject, _JSFunctionProtocol { } } -/// Internal protocol to support generic arguments for `JSFunction`. -/// -/// In Swift Embedded, non-final classes cannot have generic methods. -public protocol _JSFunctionProtocol: JSFunction {} - #if hasFeature(Embedded) // NOTE: once embedded supports variadic generics, we can remove these overloads -public extension _JSFunctionProtocol { +public extension JSFunction { @discardableResult func callAsFunction(this: JSObject) -> JSValue { From 2df9dad212f2b3e0788fb46b6e34c1f4b8b3364e Mon Sep 17 00:00:00 2001 From: Yuta Saito Date: Tue, 15 Oct 2024 12:58:20 +0900 Subject: [PATCH 161/373] Add nullability annotations to the C API --- .../BasicObjects/JSTypedArray.swift | 4 +- .../_CJavaScriptKit/include/_CJavaScriptKit.h | 52 +++++++++---------- 2 files changed, 28 insertions(+), 28 deletions(-) diff --git a/Sources/JavaScriptKit/BasicObjects/JSTypedArray.swift b/Sources/JavaScriptKit/BasicObjects/JSTypedArray.swift index 57df7c865..2168292f7 100644 --- a/Sources/JavaScriptKit/BasicObjects/JSTypedArray.swift +++ b/Sources/JavaScriptKit/BasicObjects/JSTypedArray.swift @@ -47,7 +47,7 @@ public class JSTypedArray: JSBridgedClass, ExpressibleByArrayLiteral wh /// - Parameter array: The array that will be copied to create a new instance of TypedArray public convenience init(_ array: [Element]) { let jsArrayRef = array.withUnsafeBufferPointer { ptr in - swjs_create_typed_array(Self.constructor!.id, ptr.baseAddress!, Int32(array.count)) + swjs_create_typed_array(Self.constructor!.id, ptr.baseAddress, Int32(array.count)) } self.init(unsafelyWrapping: JSObject(id: jsArrayRef)) } @@ -187,4 +187,4 @@ extension Float32: TypedArrayElement { extension Float64: TypedArrayElement { public static var typedArrayClass = JSObject.global.Float64Array.function! } -#endif \ No newline at end of file +#endif diff --git a/Sources/_CJavaScriptKit/include/_CJavaScriptKit.h b/Sources/_CJavaScriptKit/include/_CJavaScriptKit.h index cac103c3f..f8279bff9 100644 --- a/Sources/_CJavaScriptKit/include/_CJavaScriptKit.h +++ b/Sources/_CJavaScriptKit/include/_CJavaScriptKit.h @@ -105,8 +105,8 @@ IMPORT_JS_FUNCTION(swjs_set_prop, void, (const JavaScriptObjectRef _this, /// @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 *payload1, - JavaScriptPayload2 *payload2)) + JavaScriptPayload1 * _Nonnull payload1, + JavaScriptPayload2 * _Nonnull payload2)) /// Sets a value of `_this` JavaScript object. /// @@ -131,8 +131,8 @@ IMPORT_JS_FUNCTION(swjs_set_subscript, void, (const JavaScriptObjectRef _this, /// get a value of `_this` JavaScript object. IMPORT_JS_FUNCTION(swjs_get_subscript, uint32_t, (const JavaScriptObjectRef _this, const int index, - JavaScriptPayload1 *payload1, - JavaScriptPayload2 *payload2)) + JavaScriptPayload1 * _Nonnull payload1, + JavaScriptPayload2 * _Nonnull payload2)) /// Encodes the `str_obj` to bytes sequence and returns the length of bytes. /// @@ -140,20 +140,20 @@ IMPORT_JS_FUNCTION(swjs_get_subscript, uint32_t, (const JavaScriptObjectRef _thi /// @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. -IMPORT_JS_FUNCTION(swjs_encode_string, int, (const JavaScriptObjectRef str_obj, JavaScriptObjectRef *bytes_result)) +IMPORT_JS_FUNCTION(swjs_encode_string, int, (const JavaScriptObjectRef str_obj, JavaScriptObjectRef * _Nonnull bytes_result)) /// 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. -IMPORT_JS_FUNCTION(swjs_decode_string, JavaScriptObjectRef, (const unsigned char *bytes_ptr, const int length)) +IMPORT_JS_FUNCTION(swjs_decode_string, JavaScriptObjectRef, (const unsigned char * _Nonnull bytes_ptr, const int length)) /// 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. -IMPORT_JS_FUNCTION(swjs_load_string, void, (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 @@ -172,10 +172,10 @@ IMPORT_JS_FUNCTION(swjs_i64_to_bigint_slow, JavaScriptObjectRef, (unsigned int l /// @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, uint32_t, (const JavaScriptObjectRef ref, - const RawJSValue *argv, + const RawJSValue * _Nullable argv, const int argc, - JavaScriptPayload1 *result_payload1, - JavaScriptPayload2 *result_payload2)) + JavaScriptPayload1 * _Nonnull result_payload1, + JavaScriptPayload2 * _Nonnull result_payload2)) /// Calls JavaScript function with given arguments list without capturing any exception /// @@ -186,10 +186,10 @@ IMPORT_JS_FUNCTION(swjs_call_function, uint32_t, (const JavaScriptObjectRef ref, /// @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 *argv, + const RawJSValue * _Nullable argv, const int argc, - JavaScriptPayload1 *result_payload1, - JavaScriptPayload2 *result_payload2)) + JavaScriptPayload1 * _Nonnull result_payload1, + JavaScriptPayload2 * _Nonnull result_payload2)) /// Calls JavaScript function with given arguments list and given `_this`. /// @@ -202,10 +202,10 @@ IMPORT_JS_FUNCTION(swjs_call_function_no_catch, uint32_t, (const JavaScriptObjec /// @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 *argv, + const RawJSValue * _Nullable argv, const int argc, - JavaScriptPayload1 *result_payload1, - JavaScriptPayload2 *result_payload2)) + JavaScriptPayload1 * _Nonnull result_payload1, + JavaScriptPayload2 * _Nonnull result_payload2)) /// Calls JavaScript function with given arguments list and given `_this` without capturing any exception. /// @@ -218,10 +218,10 @@ IMPORT_JS_FUNCTION(swjs_call_function_with_this, uint32_t, (const JavaScriptObje /// @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 *argv, + const RawJSValue * _Nullable argv, const int argc, - JavaScriptPayload1 *result_payload1, - JavaScriptPayload2 *result_payload2)) + JavaScriptPayload1 * _Nonnull result_payload1, + JavaScriptPayload2 * _Nonnull result_payload2)) /// Calls JavaScript object constructor with given arguments list. /// @@ -230,7 +230,7 @@ IMPORT_JS_FUNCTION(swjs_call_function_with_this_no_catch, uint32_t, (const JavaS /// @param argc The length of `argv``. /// @returns A reference to the constructed object. IMPORT_JS_FUNCTION(swjs_call_new, JavaScriptObjectRef, (const JavaScriptObjectRef ref, - const RawJSValue *argv, + const RawJSValue * _Nullable argv, const int argc)) /// Calls JavaScript object constructor with given arguments list. @@ -243,11 +243,11 @@ IMPORT_JS_FUNCTION(swjs_call_new, JavaScriptObjectRef, (const JavaScriptObjectRe /// @param exception_payload2 A result pointer of second payload of JavaScript value of thrown exception. /// @returns A reference to the constructed object. IMPORT_JS_FUNCTION(swjs_call_throwing_new, JavaScriptObjectRef, (const JavaScriptObjectRef ref, - const RawJSValue *argv, + const RawJSValue * _Nullable argv, const int argc, - JavaScriptRawValueKindAndFlags *exception_kind, - JavaScriptPayload1 *exception_payload1, - JavaScriptPayload2 *exception_payload2)) + JavaScriptRawValueKindAndFlags * _Nonnull exception_kind, + JavaScriptPayload1 * _Nonnull exception_payload1, + JavaScriptPayload2 * _Nonnull exception_payload2)) /// Acts like JavaScript `instanceof` operator. /// @@ -276,14 +276,14 @@ IMPORT_JS_FUNCTION(swjs_create_function, JavaScriptObjectRef, (const JavaScriptH /// @param length The length of `elements_ptr` /// @returns A reference to the constructed typed array IMPORT_JS_FUNCTION(swjs_create_typed_array, JavaScriptObjectRef, (const JavaScriptObjectRef constructor, - const void *elements_ptr, + 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. -IMPORT_JS_FUNCTION(swjs_load_typed_array, void, (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. /// From 89751127d7911eb6cada4e1b11d7fca0b5799f1f Mon Sep 17 00:00:00 2001 From: Yuta Saito Date: Tue, 15 Oct 2024 12:58:55 +0900 Subject: [PATCH 162/373] Suppress deprecation warning in JSTimer --- Sources/JavaScriptKit/BasicObjects/JSTimer.swift | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/Sources/JavaScriptKit/BasicObjects/JSTimer.swift b/Sources/JavaScriptKit/BasicObjects/JSTimer.swift index d2eee6fcc..231792a84 100644 --- a/Sources/JavaScriptKit/BasicObjects/JSTimer.swift +++ b/Sources/JavaScriptKit/BasicObjects/JSTimer.swift @@ -27,7 +27,11 @@ public final class JSTimer { case .oneshot(let closure): closure.release() case .repeating(let closure): +#if JAVASCRIPTKIT_WITHOUT_WEAKREFS closure.release() +#else + break // no-op +#endif } } } From b1b302414df436fe15d4e5c828424b3302ceb5f8 Mon Sep 17 00:00:00 2001 From: Yuta Saito Date: Tue, 15 Oct 2024 13:01:01 +0900 Subject: [PATCH 163/373] Suppress retroactive conformance warnings in Swift 6 --- Sources/JavaScriptBigIntSupport/Int64+I64.swift | 4 ++-- Sources/JavaScriptBigIntSupport/JSBigInt+I64.swift | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Sources/JavaScriptBigIntSupport/Int64+I64.swift b/Sources/JavaScriptBigIntSupport/Int64+I64.swift index cce10a1ba..fdd1d544f 100644 --- a/Sources/JavaScriptBigIntSupport/Int64+I64.swift +++ b/Sources/JavaScriptBigIntSupport/Int64+I64.swift @@ -1,12 +1,12 @@ import JavaScriptKit -extension UInt64: ConvertibleToJSValue, TypedArrayElement { +extension UInt64: JavaScriptKit.ConvertibleToJSValue, JavaScriptKit.TypedArrayElement { public static var typedArrayClass = JSObject.global.BigUint64Array.function! public var jsValue: JSValue { .bigInt(JSBigInt(unsigned: self)) } } -extension Int64: ConvertibleToJSValue, TypedArrayElement { +extension Int64: JavaScriptKit.ConvertibleToJSValue, JavaScriptKit.TypedArrayElement { public static var typedArrayClass = 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 index ef868bf1b..a8ac18cf6 100644 --- a/Sources/JavaScriptBigIntSupport/JSBigInt+I64.swift +++ b/Sources/JavaScriptBigIntSupport/JSBigInt+I64.swift @@ -1,7 +1,7 @@ import _CJavaScriptBigIntSupport @_spi(JSObject_id) import JavaScriptKit -extension JSBigInt: JSBigIntExtended { +extension JSBigInt: JavaScriptKit.JSBigIntExtended { public var int64Value: Int64 { swjs_bigint_to_i64(id, true) } From 85b3479f96c2fc4c7dbeeba0bfd36e64ddc3e54c Mon Sep 17 00:00:00 2001 From: Yuta Saito Date: Tue, 15 Oct 2024 13:37:59 +0900 Subject: [PATCH 164/373] Fix EmbeddedApp example --- .../EmbeddedApp/_thingsThatShouldNotBeNeeded.swift | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/Examples/Embedded/Sources/EmbeddedApp/_thingsThatShouldNotBeNeeded.swift b/Examples/Embedded/Sources/EmbeddedApp/_thingsThatShouldNotBeNeeded.swift index 773f928d8..8f45ccee9 100644 --- a/Examples/Embedded/Sources/EmbeddedApp/_thingsThatShouldNotBeNeeded.swift +++ b/Examples/Embedded/Sources/EmbeddedApp/_thingsThatShouldNotBeNeeded.swift @@ -17,9 +17,20 @@ func strlen(_ s: UnsafePointer) -> Int { 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.. Date: Tue, 15 Oct 2024 13:39:54 +0900 Subject: [PATCH 165/373] Use symlink instead of copying runtime files --- Examples/Embedded/_Runtime | 1 + .../_bundling_does_not_work_with_embedded | 0 Examples/Embedded/_Runtime/index.js | 579 ------------------ Examples/Embedded/_Runtime/index.mjs | 569 ----------------- 4 files changed, 1 insertion(+), 1148 deletions(-) create mode 120000 Examples/Embedded/_Runtime delete mode 100644 Examples/Embedded/_Runtime/_bundling_does_not_work_with_embedded delete mode 100644 Examples/Embedded/_Runtime/index.js delete mode 100644 Examples/Embedded/_Runtime/index.mjs diff --git a/Examples/Embedded/_Runtime b/Examples/Embedded/_Runtime new file mode 120000 index 000000000..af934baa2 --- /dev/null +++ b/Examples/Embedded/_Runtime @@ -0,0 +1 @@ +../../Sources/JavaScriptKit/Runtime \ No newline at end of file diff --git a/Examples/Embedded/_Runtime/_bundling_does_not_work_with_embedded b/Examples/Embedded/_Runtime/_bundling_does_not_work_with_embedded deleted file mode 100644 index e69de29bb..000000000 diff --git a/Examples/Embedded/_Runtime/index.js b/Examples/Embedded/_Runtime/index.js deleted file mode 100644 index 9d29b4329..000000000 --- a/Examples/Embedded/_Runtime/index.js +++ /dev/null @@ -1,579 +0,0 @@ -(function (global, factory) { - typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports) : - typeof define === 'function' && define.amd ? define(['exports'], factory) : - (global = typeof globalThis !== 'undefined' ? globalThis : global || self, factory(global.JavaScriptKit = {})); -})(this, (function (exports) { 'use strict'; - - /// Memory lifetime of closures in Swift are managed by Swift side - class SwiftClosureDeallocator { - constructor(exports) { - if (typeof FinalizationRegistry === "undefined") { - throw new Error("The Swift part of JavaScriptKit was configured to require " + - "the availability of JavaScript WeakRefs. Please build " + - "with `-Xswiftc -DJAVASCRIPTKIT_WITHOUT_WEAKREFS` to " + - "disable features that use WeakRefs."); - } - this.functionRegistry = new FinalizationRegistry((id) => { - exports.swjs_free_host_function(id); - }); - } - track(func, func_ref) { - this.functionRegistry.register(func, func_ref); - } - } - - function assertNever(x, message) { - throw new Error(message); - } - - const decode = (kind, payload1, payload2, memory) => { - switch (kind) { - case 0 /* Boolean */: - switch (payload1) { - case 0: - return false; - case 1: - return true; - } - case 2 /* Number */: - return payload2; - case 1 /* String */: - case 3 /* Object */: - case 6 /* Function */: - case 7 /* Symbol */: - case 8 /* BigInt */: - return memory.getObject(payload1); - case 4 /* Null */: - return null; - case 5 /* Undefined */: - return undefined; - default: - assertNever(kind, `JSValue Type kind "${kind}" is not supported`); - } - }; - // Note: - // `decodeValues` assumes that the size of RawJSValue is 16. - const decodeArray = (ptr, length, memory) => { - // fast path for empty array - if (length === 0) { - return []; - } - let result = []; - // It's safe to hold DataView here because WebAssembly.Memory.buffer won't - // change within this function. - const view = memory.dataView(); - for (let index = 0; index < length; index++) { - const base = ptr + 16 * index; - const kind = view.getUint32(base, true); - const payload1 = view.getUint32(base + 4, true); - const payload2 = view.getFloat64(base + 8, true); - result.push(decode(kind, payload1, payload2, memory)); - } - return result; - }; - // A helper function to encode a RawJSValue into a pointers. - // Please prefer to use `writeAndReturnKindBits` to avoid unnecessary - // memory stores. - // This function should be used only when kind flag is stored in memory. - const write = (value, kind_ptr, payload1_ptr, payload2_ptr, is_exception, memory) => { - const kind = writeAndReturnKindBits(value, payload1_ptr, payload2_ptr, is_exception, memory); - memory.writeUint32(kind_ptr, kind); - }; - const writeAndReturnKindBits = (value, payload1_ptr, payload2_ptr, is_exception, memory) => { - const exceptionBit = (is_exception ? 1 : 0) << 31; - if (value === null) { - return exceptionBit | 4 /* Null */; - } - const writeRef = (kind) => { - memory.writeUint32(payload1_ptr, memory.retain(value)); - return exceptionBit | kind; - }; - const type = typeof value; - switch (type) { - case "boolean": { - memory.writeUint32(payload1_ptr, value ? 1 : 0); - return exceptionBit | 0 /* Boolean */; - } - case "number": { - memory.writeFloat64(payload2_ptr, value); - return exceptionBit | 2 /* Number */; - } - case "string": { - return writeRef(1 /* String */); - } - case "undefined": { - return exceptionBit | 5 /* Undefined */; - } - case "object": { - return writeRef(3 /* Object */); - } - case "function": { - return writeRef(6 /* Function */); - } - case "symbol": { - return writeRef(7 /* Symbol */); - } - case "bigint": { - return writeRef(8 /* BigInt */); - } - default: - assertNever(type, `Type "${type}" is not supported yet`); - } - throw new Error("Unreachable"); - }; - - 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 SwiftRuntime { - constructor(options) { - this.version = 708; - this.textDecoder = new TextDecoder("utf-8"); - this.textEncoder = new TextEncoder(); // Only support utf-8 - /** @deprecated Use `wasmImports` instead */ - this.importObjects = () => this.wasmImports; - this._instance = null; - this._memory = null; - this._closureDeallocator = null; - this.tid = null; - this.options = options || {}; - } - setInstance(instance) { - this._instance = instance; - if (typeof this.exports._start === "function") { - throw new Error(`JavaScriptKit supports only WASI reactor ABI. - Please make sure you are building with: - -Xswiftc -Xclang-linker -Xswiftc -mexec-model=reactor - `); - } - if (this.exports.swjs_library_version() != this.version) { - throw new Error(`The versions of JavaScriptKit are incompatible. - WebAssembly runtime ${this.exports.swjs_library_version()} != JS runtime ${this.version}`); - } - } - main() { - const instance = this.instance; - try { - if (typeof instance.exports.main === "function") { - instance.exports.main(); - } - else if (typeof instance.exports.__main_argc_argv === "function") { - // Swift 6.0 and later use `__main_argc_argv` instead of `main`. - instance.exports.__main_argc_argv(0, 0); - } - } - catch (error) { - if (error instanceof UnsafeEventLoopYield) { - // Ignore the error - return; - } - // Rethrow other errors - throw error; - } - } - /** - * Start a new thread with the given `tid` and `startArg`, which - * is forwarded to the `wasi_thread_start` function. - * This function is expected to be called from the spawned Web Worker thread. - */ - startThread(tid, startArg) { - this.tid = tid; - const instance = this.instance; - try { - if (typeof instance.exports.wasi_thread_start === "function") { - instance.exports.wasi_thread_start(tid, startArg); - } - else { - throw new Error(`The WebAssembly module is not built for wasm32-unknown-wasip1-threads target.`); - } - } - catch (error) { - if (error instanceof UnsafeEventLoopYield) { - // Ignore the error - return; - } - // Rethrow other errors - throw error; - } - } - get instance() { - if (!this._instance) - throw new Error("WebAssembly instance is not set yet"); - return this._instance; - } - get exports() { - return this.instance.exports; - } - get memory() { - if (!this._memory) { - this._memory = new Memory(this.instance.exports); - } - return this._memory; - } - get closureDeallocator() { - if (this._closureDeallocator) - return this._closureDeallocator; - const features = this.exports.swjs_library_features(); - const librarySupportsWeakRef = (features & 1 /* WeakRefs */) != 0; - if (librarySupportsWeakRef) { - this._closureDeallocator = new SwiftClosureDeallocator(this.exports); - } - return this._closureDeallocator; - } - callHostFunction(host_func_id, line, file, args) { - const argc = args.length; - const argv = this.exports.swjs_prepare_host_function_call(argc); - const memory = this.memory; - for (let index = 0; index < args.length; index++) { - const argument = args[index]; - const base = argv + 16 * index; - write(argument, base, base + 4, base + 8, false, memory); - } - let output; - // This ref is released by the swjs_call_host_function implementation - const callback_func_ref = memory.retain((result) => { - output = result; - }); - const alreadyReleased = this.exports.swjs_call_host_function(host_func_id, argv, argc, callback_func_ref); - if (alreadyReleased) { - throw new Error(`The JSClosure has been already released by Swift side. The closure is created at ${file}:${line}`); - } - this.exports.swjs_cleanup_host_function_call(argv); - return output; - } - get wasmImports() { - return { - swjs_set_prop: (ref, name, kind, payload1, payload2) => { - const memory = this.memory; - const obj = memory.getObject(ref); - const key = memory.getObject(name); - const value = decode(kind, payload1, payload2, memory); - obj[key] = value; - }, - swjs_get_prop: (ref, name, payload1_ptr, payload2_ptr) => { - const memory = this.memory; - const obj = memory.getObject(ref); - const key = memory.getObject(name); - const result = obj[key]; - return writeAndReturnKindBits(result, payload1_ptr, payload2_ptr, false, memory); - }, - swjs_set_subscript: (ref, index, kind, payload1, payload2) => { - const memory = this.memory; - const obj = memory.getObject(ref); - const value = decode(kind, payload1, payload2, memory); - obj[index] = value; - }, - swjs_get_subscript: (ref, index, payload1_ptr, payload2_ptr) => { - const obj = this.memory.getObject(ref); - const result = obj[index]; - return writeAndReturnKindBits(result, payload1_ptr, payload2_ptr, false, this.memory); - }, - swjs_encode_string: (ref, bytes_ptr_result) => { - const memory = this.memory; - const bytes = this.textEncoder.encode(memory.getObject(ref)); - const bytes_ptr = memory.retain(bytes); - memory.writeUint32(bytes_ptr_result, bytes_ptr); - return bytes.length; - }, - swjs_decode_string: ( - // NOTE: TextDecoder can't decode typed arrays backed by SharedArrayBuffer - this.options.sharedMemory == true - ? ((bytes_ptr, length) => { - const memory = this.memory; - const bytes = memory - .bytes() - .slice(bytes_ptr, bytes_ptr + length); - const string = this.textDecoder.decode(bytes); - return memory.retain(string); - }) - : ((bytes_ptr, length) => { - const memory = this.memory; - const bytes = memory - .bytes() - .subarray(bytes_ptr, bytes_ptr + length); - const string = this.textDecoder.decode(bytes); - return memory.retain(string); - })), - swjs_load_string: (ref, buffer) => { - const memory = this.memory; - const bytes = memory.getObject(ref); - memory.writeBytes(buffer, bytes); - }, - swjs_call_function: (ref, argv, argc, payload1_ptr, payload2_ptr) => { - const memory = this.memory; - const func = memory.getObject(ref); - let result = undefined; - try { - const args = decodeArray(argv, argc, memory); - result = func(...args); - } - catch (error) { - return writeAndReturnKindBits(error, payload1_ptr, payload2_ptr, true, this.memory); - } - return writeAndReturnKindBits(result, payload1_ptr, payload2_ptr, false, this.memory); - }, - swjs_call_function_no_catch: (ref, argv, argc, payload1_ptr, payload2_ptr) => { - const memory = this.memory; - const func = memory.getObject(ref); - const args = decodeArray(argv, argc, memory); - const result = func(...args); - return writeAndReturnKindBits(result, payload1_ptr, payload2_ptr, false, this.memory); - }, - swjs_call_function_with_this: (obj_ref, func_ref, argv, argc, payload1_ptr, payload2_ptr) => { - const memory = this.memory; - const obj = memory.getObject(obj_ref); - const func = memory.getObject(func_ref); - let result; - try { - const args = decodeArray(argv, argc, memory); - result = func.apply(obj, args); - } - catch (error) { - return writeAndReturnKindBits(error, payload1_ptr, payload2_ptr, true, this.memory); - } - return writeAndReturnKindBits(result, payload1_ptr, payload2_ptr, false, this.memory); - }, - swjs_call_function_with_this_no_catch: (obj_ref, func_ref, argv, argc, payload1_ptr, payload2_ptr) => { - const memory = this.memory; - const obj = memory.getObject(obj_ref); - const func = memory.getObject(func_ref); - let result = undefined; - const args = decodeArray(argv, argc, memory); - result = func.apply(obj, args); - return writeAndReturnKindBits(result, payload1_ptr, payload2_ptr, false, this.memory); - }, - swjs_call_new: (ref, argv, argc) => { - const memory = this.memory; - const constructor = memory.getObject(ref); - const args = decodeArray(argv, argc, memory); - const instance = new constructor(...args); - return this.memory.retain(instance); - }, - swjs_call_throwing_new: (ref, argv, argc, exception_kind_ptr, exception_payload1_ptr, exception_payload2_ptr) => { - let memory = this.memory; - const constructor = memory.getObject(ref); - let result; - try { - const args = decodeArray(argv, argc, memory); - result = new constructor(...args); - } - catch (error) { - write(error, exception_kind_ptr, exception_payload1_ptr, exception_payload2_ptr, true, this.memory); - return -1; - } - memory = this.memory; - write(null, exception_kind_ptr, exception_payload1_ptr, exception_payload2_ptr, false, memory); - return memory.retain(result); - }, - swjs_instanceof: (obj_ref, constructor_ref) => { - const memory = this.memory; - const obj = memory.getObject(obj_ref); - const constructor = memory.getObject(constructor_ref); - return obj instanceof constructor; - }, - swjs_create_function: (host_func_id, line, file) => { - var _a; - const fileString = this.memory.getObject(file); - const func = (...args) => this.callHostFunction(host_func_id, line, fileString, args); - const func_ref = this.memory.retain(func); - (_a = this.closureDeallocator) === null || _a === void 0 ? void 0 : _a.track(func, func_ref); - return func_ref; - }, - swjs_create_typed_array: (constructor_ref, elementsPtr, length) => { - const ArrayType = this.memory.getObject(constructor_ref); - const array = new ArrayType(this.memory.rawMemory.buffer, elementsPtr, length); - // Call `.slice()` to copy the memory - return this.memory.retain(array.slice()); - }, - swjs_load_typed_array: (ref, buffer) => { - const memory = this.memory; - const typedArray = memory.getObject(ref); - const bytes = new Uint8Array(typedArray.buffer); - memory.writeBytes(buffer, bytes); - }, - swjs_release: (ref) => { - this.memory.release(ref); - }, - swjs_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."); - } - threadChannel.listenMessageFromMainThread((message) => { - switch (message.type) { - case "wake": - this.exports.swjs_wake_worker_thread(); - break; - default: - const unknownMessage = message.type; - 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."); - } - threadChannel.listenMessageFromWorkerThread(tid, (message) => { - switch (message.type) { - case "job": - this.exports.swjs_enqueue_main_job_from_worker(message.data); - break; - default: - const unknownMessage = message.type; - 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; - }, - }; - } - postMessageToMainThread(message) { - 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); - } - postMessageToWorkerThread(tid, message) { - 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); - } - } - /// This error is thrown when yielding event loop control from `swift_task_asyncMainDrainQueue` - /// to JavaScript. This is usually thrown when: - /// - The entry point of the Swift program is `func main() async` - /// - The Swift Concurrency's global executor is hooked by `JavaScriptEventLoop.installGlobalExecutor()` - /// - Calling exported `main` or `__main_argc_argv` function from JavaScript - /// - /// This exception must be caught by the caller of the exported function and the caller should - /// catch this exception and just ignore it. - /// - /// FAQ: Why this error is thrown? - /// This error is thrown to unwind the call stack of the Swift program and return the control to - /// the JavaScript side. Otherwise, the `swift_task_asyncMainDrainQueue` ends up with `abort()` - /// because the event loop expects `exit()` call before the end of the event loop. - class UnsafeEventLoopYield extends Error { - } - - exports.SwiftRuntime = SwiftRuntime; - - Object.defineProperty(exports, '__esModule', { value: true }); - -})); diff --git a/Examples/Embedded/_Runtime/index.mjs b/Examples/Embedded/_Runtime/index.mjs deleted file mode 100644 index 9201b7712..000000000 --- a/Examples/Embedded/_Runtime/index.mjs +++ /dev/null @@ -1,569 +0,0 @@ -/// Memory lifetime of closures in Swift are managed by Swift side -class SwiftClosureDeallocator { - constructor(exports) { - if (typeof FinalizationRegistry === "undefined") { - throw new Error("The Swift part of JavaScriptKit was configured to require " + - "the availability of JavaScript WeakRefs. Please build " + - "with `-Xswiftc -DJAVASCRIPTKIT_WITHOUT_WEAKREFS` to " + - "disable features that use WeakRefs."); - } - this.functionRegistry = new FinalizationRegistry((id) => { - exports.swjs_free_host_function(id); - }); - } - track(func, func_ref) { - this.functionRegistry.register(func, func_ref); - } -} - -function assertNever(x, message) { - throw new Error(message); -} - -const decode = (kind, payload1, payload2, memory) => { - switch (kind) { - case 0 /* Boolean */: - switch (payload1) { - case 0: - return false; - case 1: - return true; - } - case 2 /* Number */: - return payload2; - case 1 /* String */: - case 3 /* Object */: - case 6 /* Function */: - case 7 /* Symbol */: - case 8 /* BigInt */: - return memory.getObject(payload1); - case 4 /* Null */: - return null; - case 5 /* Undefined */: - return undefined; - default: - assertNever(kind, `JSValue Type kind "${kind}" is not supported`); - } -}; -// Note: -// `decodeValues` assumes that the size of RawJSValue is 16. -const decodeArray = (ptr, length, memory) => { - // fast path for empty array - if (length === 0) { - return []; - } - let result = []; - // It's safe to hold DataView here because WebAssembly.Memory.buffer won't - // change within this function. - const view = memory.dataView(); - for (let index = 0; index < length; index++) { - const base = ptr + 16 * index; - const kind = view.getUint32(base, true); - const payload1 = view.getUint32(base + 4, true); - const payload2 = view.getFloat64(base + 8, true); - result.push(decode(kind, payload1, payload2, memory)); - } - return result; -}; -// A helper function to encode a RawJSValue into a pointers. -// Please prefer to use `writeAndReturnKindBits` to avoid unnecessary -// memory stores. -// This function should be used only when kind flag is stored in memory. -const write = (value, kind_ptr, payload1_ptr, payload2_ptr, is_exception, memory) => { - const kind = writeAndReturnKindBits(value, payload1_ptr, payload2_ptr, is_exception, memory); - memory.writeUint32(kind_ptr, kind); -}; -const writeAndReturnKindBits = (value, payload1_ptr, payload2_ptr, is_exception, memory) => { - const exceptionBit = (is_exception ? 1 : 0) << 31; - if (value === null) { - return exceptionBit | 4 /* Null */; - } - const writeRef = (kind) => { - memory.writeUint32(payload1_ptr, memory.retain(value)); - return exceptionBit | kind; - }; - const type = typeof value; - switch (type) { - case "boolean": { - memory.writeUint32(payload1_ptr, value ? 1 : 0); - return exceptionBit | 0 /* Boolean */; - } - case "number": { - memory.writeFloat64(payload2_ptr, value); - return exceptionBit | 2 /* Number */; - } - case "string": { - return writeRef(1 /* String */); - } - case "undefined": { - return exceptionBit | 5 /* Undefined */; - } - case "object": { - return writeRef(3 /* Object */); - } - case "function": { - return writeRef(6 /* Function */); - } - case "symbol": { - return writeRef(7 /* Symbol */); - } - case "bigint": { - return writeRef(8 /* BigInt */); - } - default: - assertNever(type, `Type "${type}" is not supported yet`); - } - throw new Error("Unreachable"); -}; - -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 SwiftRuntime { - constructor(options) { - this.version = 708; - this.textDecoder = new TextDecoder("utf-8"); - this.textEncoder = new TextEncoder(); // Only support utf-8 - /** @deprecated Use `wasmImports` instead */ - this.importObjects = () => this.wasmImports; - this._instance = null; - this._memory = null; - this._closureDeallocator = null; - this.tid = null; - this.options = options || {}; - } - setInstance(instance) { - this._instance = instance; - if (typeof this.exports._start === "function") { - throw new Error(`JavaScriptKit supports only WASI reactor ABI. - Please make sure you are building with: - -Xswiftc -Xclang-linker -Xswiftc -mexec-model=reactor - `); - } - if (this.exports.swjs_library_version() != this.version) { - throw new Error(`The versions of JavaScriptKit are incompatible. - WebAssembly runtime ${this.exports.swjs_library_version()} != JS runtime ${this.version}`); - } - } - main() { - const instance = this.instance; - try { - if (typeof instance.exports.main === "function") { - instance.exports.main(); - } - else if (typeof instance.exports.__main_argc_argv === "function") { - // Swift 6.0 and later use `__main_argc_argv` instead of `main`. - instance.exports.__main_argc_argv(0, 0); - } - } - catch (error) { - if (error instanceof UnsafeEventLoopYield) { - // Ignore the error - return; - } - // Rethrow other errors - throw error; - } - } - /** - * Start a new thread with the given `tid` and `startArg`, which - * is forwarded to the `wasi_thread_start` function. - * This function is expected to be called from the spawned Web Worker thread. - */ - startThread(tid, startArg) { - this.tid = tid; - const instance = this.instance; - try { - if (typeof instance.exports.wasi_thread_start === "function") { - instance.exports.wasi_thread_start(tid, startArg); - } - else { - throw new Error(`The WebAssembly module is not built for wasm32-unknown-wasip1-threads target.`); - } - } - catch (error) { - if (error instanceof UnsafeEventLoopYield) { - // Ignore the error - return; - } - // Rethrow other errors - throw error; - } - } - get instance() { - if (!this._instance) - throw new Error("WebAssembly instance is not set yet"); - return this._instance; - } - get exports() { - return this.instance.exports; - } - get memory() { - if (!this._memory) { - this._memory = new Memory(this.instance.exports); - } - return this._memory; - } - get closureDeallocator() { - if (this._closureDeallocator) - return this._closureDeallocator; - const features = this.exports.swjs_library_features(); - const librarySupportsWeakRef = (features & 1 /* WeakRefs */) != 0; - if (librarySupportsWeakRef) { - this._closureDeallocator = new SwiftClosureDeallocator(this.exports); - } - return this._closureDeallocator; - } - callHostFunction(host_func_id, line, file, args) { - const argc = args.length; - const argv = this.exports.swjs_prepare_host_function_call(argc); - const memory = this.memory; - for (let index = 0; index < args.length; index++) { - const argument = args[index]; - const base = argv + 16 * index; - write(argument, base, base + 4, base + 8, false, memory); - } - let output; - // This ref is released by the swjs_call_host_function implementation - const callback_func_ref = memory.retain((result) => { - output = result; - }); - const alreadyReleased = this.exports.swjs_call_host_function(host_func_id, argv, argc, callback_func_ref); - if (alreadyReleased) { - throw new Error(`The JSClosure has been already released by Swift side. The closure is created at ${file}:${line}`); - } - this.exports.swjs_cleanup_host_function_call(argv); - return output; - } - get wasmImports() { - return { - swjs_set_prop: (ref, name, kind, payload1, payload2) => { - const memory = this.memory; - const obj = memory.getObject(ref); - const key = memory.getObject(name); - const value = decode(kind, payload1, payload2, memory); - obj[key] = value; - }, - swjs_get_prop: (ref, name, payload1_ptr, payload2_ptr) => { - const memory = this.memory; - const obj = memory.getObject(ref); - const key = memory.getObject(name); - const result = obj[key]; - return writeAndReturnKindBits(result, payload1_ptr, payload2_ptr, false, memory); - }, - swjs_set_subscript: (ref, index, kind, payload1, payload2) => { - const memory = this.memory; - const obj = memory.getObject(ref); - const value = decode(kind, payload1, payload2, memory); - obj[index] = value; - }, - swjs_get_subscript: (ref, index, payload1_ptr, payload2_ptr) => { - const obj = this.memory.getObject(ref); - const result = obj[index]; - return writeAndReturnKindBits(result, payload1_ptr, payload2_ptr, false, this.memory); - }, - swjs_encode_string: (ref, bytes_ptr_result) => { - const memory = this.memory; - const bytes = this.textEncoder.encode(memory.getObject(ref)); - const bytes_ptr = memory.retain(bytes); - memory.writeUint32(bytes_ptr_result, bytes_ptr); - return bytes.length; - }, - swjs_decode_string: ( - // NOTE: TextDecoder can't decode typed arrays backed by SharedArrayBuffer - this.options.sharedMemory == true - ? ((bytes_ptr, length) => { - const memory = this.memory; - const bytes = memory - .bytes() - .slice(bytes_ptr, bytes_ptr + length); - const string = this.textDecoder.decode(bytes); - return memory.retain(string); - }) - : ((bytes_ptr, length) => { - const memory = this.memory; - const bytes = memory - .bytes() - .subarray(bytes_ptr, bytes_ptr + length); - const string = this.textDecoder.decode(bytes); - return memory.retain(string); - })), - swjs_load_string: (ref, buffer) => { - const memory = this.memory; - const bytes = memory.getObject(ref); - memory.writeBytes(buffer, bytes); - }, - swjs_call_function: (ref, argv, argc, payload1_ptr, payload2_ptr) => { - const memory = this.memory; - const func = memory.getObject(ref); - let result = undefined; - try { - const args = decodeArray(argv, argc, memory); - result = func(...args); - } - catch (error) { - return writeAndReturnKindBits(error, payload1_ptr, payload2_ptr, true, this.memory); - } - return writeAndReturnKindBits(result, payload1_ptr, payload2_ptr, false, this.memory); - }, - swjs_call_function_no_catch: (ref, argv, argc, payload1_ptr, payload2_ptr) => { - const memory = this.memory; - const func = memory.getObject(ref); - const args = decodeArray(argv, argc, memory); - const result = func(...args); - return writeAndReturnKindBits(result, payload1_ptr, payload2_ptr, false, this.memory); - }, - swjs_call_function_with_this: (obj_ref, func_ref, argv, argc, payload1_ptr, payload2_ptr) => { - const memory = this.memory; - const obj = memory.getObject(obj_ref); - const func = memory.getObject(func_ref); - let result; - try { - const args = decodeArray(argv, argc, memory); - result = func.apply(obj, args); - } - catch (error) { - return writeAndReturnKindBits(error, payload1_ptr, payload2_ptr, true, this.memory); - } - return writeAndReturnKindBits(result, payload1_ptr, payload2_ptr, false, this.memory); - }, - swjs_call_function_with_this_no_catch: (obj_ref, func_ref, argv, argc, payload1_ptr, payload2_ptr) => { - const memory = this.memory; - const obj = memory.getObject(obj_ref); - const func = memory.getObject(func_ref); - let result = undefined; - const args = decodeArray(argv, argc, memory); - result = func.apply(obj, args); - return writeAndReturnKindBits(result, payload1_ptr, payload2_ptr, false, this.memory); - }, - swjs_call_new: (ref, argv, argc) => { - const memory = this.memory; - const constructor = memory.getObject(ref); - const args = decodeArray(argv, argc, memory); - const instance = new constructor(...args); - return this.memory.retain(instance); - }, - swjs_call_throwing_new: (ref, argv, argc, exception_kind_ptr, exception_payload1_ptr, exception_payload2_ptr) => { - let memory = this.memory; - const constructor = memory.getObject(ref); - let result; - try { - const args = decodeArray(argv, argc, memory); - result = new constructor(...args); - } - catch (error) { - write(error, exception_kind_ptr, exception_payload1_ptr, exception_payload2_ptr, true, this.memory); - return -1; - } - memory = this.memory; - write(null, exception_kind_ptr, exception_payload1_ptr, exception_payload2_ptr, false, memory); - return memory.retain(result); - }, - swjs_instanceof: (obj_ref, constructor_ref) => { - const memory = this.memory; - const obj = memory.getObject(obj_ref); - const constructor = memory.getObject(constructor_ref); - return obj instanceof constructor; - }, - swjs_create_function: (host_func_id, line, file) => { - var _a; - const fileString = this.memory.getObject(file); - const func = (...args) => this.callHostFunction(host_func_id, line, fileString, args); - const func_ref = this.memory.retain(func); - (_a = this.closureDeallocator) === null || _a === void 0 ? void 0 : _a.track(func, func_ref); - return func_ref; - }, - swjs_create_typed_array: (constructor_ref, elementsPtr, length) => { - const ArrayType = this.memory.getObject(constructor_ref); - const array = new ArrayType(this.memory.rawMemory.buffer, elementsPtr, length); - // Call `.slice()` to copy the memory - return this.memory.retain(array.slice()); - }, - swjs_load_typed_array: (ref, buffer) => { - const memory = this.memory; - const typedArray = memory.getObject(ref); - const bytes = new Uint8Array(typedArray.buffer); - memory.writeBytes(buffer, bytes); - }, - swjs_release: (ref) => { - this.memory.release(ref); - }, - swjs_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."); - } - threadChannel.listenMessageFromMainThread((message) => { - switch (message.type) { - case "wake": - this.exports.swjs_wake_worker_thread(); - break; - default: - const unknownMessage = message.type; - 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."); - } - threadChannel.listenMessageFromWorkerThread(tid, (message) => { - switch (message.type) { - case "job": - this.exports.swjs_enqueue_main_job_from_worker(message.data); - break; - default: - const unknownMessage = message.type; - 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; - }, - }; - } - postMessageToMainThread(message) { - 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); - } - postMessageToWorkerThread(tid, message) { - 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); - } -} -/// 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 }; From 43607e5d905e6811c8d970048b75e8257ab07e2f Mon Sep 17 00:00:00 2001 From: Yuta Saito Date: Tue, 15 Oct 2024 13:42:57 +0900 Subject: [PATCH 166/373] Remove unnecessary `_JSObjectProtocol` The workaround is no longer needed for the latest toolchain. --- .../JavaScriptKit/FundamentalObjects/JSObject.swift | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/Sources/JavaScriptKit/FundamentalObjects/JSObject.swift b/Sources/JavaScriptKit/FundamentalObjects/JSObject.swift index 6d8442540..82b1e6502 100644 --- a/Sources/JavaScriptKit/FundamentalObjects/JSObject.swift +++ b/Sources/JavaScriptKit/FundamentalObjects/JSObject.swift @@ -15,7 +15,7 @@ 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: _JSObjectProtocol, Equatable { +public class JSObject: Equatable { @_spi(JSObject_id) public var id: JavaScriptObjectRef @_spi(JSObject_id) @@ -231,15 +231,10 @@ public class JSThrowingObject { } #endif -/// Internal protocol to support generic arguments for `JSObject`. -/// -/// In Swift Embedded, non-final classes cannot have generic methods. -public protocol _JSObjectProtocol: JSObject { -} #if hasFeature(Embedded) // NOTE: once embedded supports variadic generics, we can remove these overloads -public extension _JSObjectProtocol { +public extension JSObject { @_disfavoredOverload subscript(dynamicMember name: String) -> (() -> JSValue)? { self[name].function.map { function in @@ -261,4 +256,4 @@ public extension _JSObjectProtocol { } } } -#endif \ No newline at end of file +#endif From e14a7562794e654fab00ac31ca318899c8fb6afe Mon Sep 17 00:00:00 2001 From: Simon Leeb <52261246+sliemeobn@users.noreply.github.com> Date: Wed, 30 Oct 2024 13:22:15 +0100 Subject: [PATCH 167/373] get rid of external flags for building --- Examples/Embedded/Package.swift | 6 ++++-- Examples/Embedded/build.sh | 11 +++-------- Package.swift | 18 ++++++++++++++---- 3 files changed, 21 insertions(+), 14 deletions(-) diff --git a/Examples/Embedded/Package.swift b/Examples/Embedded/Package.swift index 4ebc6e841..af5de3df6 100644 --- a/Examples/Embedded/Package.swift +++ b/Examples/Embedded/Package.swift @@ -16,7 +16,8 @@ let package = Package( .product(name: "dlmalloc", package: "swift-dlmalloc") ], cSettings: [ - .unsafeFlags(["-fdeclspec"]) + .unsafeFlags(["-fdeclspec"]), + .define("__Embedded"), ], swiftSettings: [ .enableExperimentalFeature("Embedded"), @@ -29,7 +30,8 @@ let package = Package( linkerSettings: [ .unsafeFlags([ "-Xclang-linker", "-nostdlib", - "-Xlinker", "--no-entry" + "-Xlinker", "--no-entry", + "-Xlinker", "--export-if-defined=__main_argc_argv" ]) ] ) diff --git a/Examples/Embedded/build.sh b/Examples/Embedded/build.sh index e1c3e3c4e..1fde1fe91 100755 --- a/Examples/Embedded/build.sh +++ b/Examples/Embedded/build.sh @@ -1,10 +1,5 @@ #!/bin/bash package_dir="$(cd "$(dirname "$0")" && pwd)" -JAVASCRIPTKIT_EXPERIMENTAL_EMBEDDED_WASM=true swift build --package-path "$package_dir" -c release --product EmbeddedApp \ - --triple wasm32-unknown-none-wasm \ - -Xswiftc -enable-experimental-feature -Xswiftc Embedded \ - -Xswiftc -enable-experimental-feature -Xswiftc Extern \ - -Xcc -D__Embedded -Xcc -fdeclspec \ - -Xlinker --export-if-defined=__main_argc_argv \ - -Xlinker --export-if-defined=swjs_call_host_function \ - -Xswiftc -Xclang-linker -Xswiftc -mexec-model=reactor +JAVASCRIPTKIT_EXPERIMENTAL_EMBEDDED_WASM=true \ + swift build --package-path "$package_dir" --product EmbeddedApp \ + -c release --triple wasm32-unknown-none-wasm diff --git a/Package.swift b/Package.swift index f94fb8bc8..9a659695d 100644 --- a/Package.swift +++ b/Package.swift @@ -1,4 +1,4 @@ -// swift-tools-version:5.7 +// swift-tools-version:5.8 import PackageDescription import Foundation @@ -19,11 +19,21 @@ let package = Package( name: "JavaScriptKit", dependencies: ["_CJavaScriptKit"], resources: shouldBuildForEmbedded ? [] : [.copy("Runtime")], + cSettings: shouldBuildForEmbedded ? [ + .unsafeFlags(["-fdeclspec"]), + .define("__Embedded"), + ] : nil, swiftSettings: shouldBuildForEmbedded - ? [.unsafeFlags(["-Xfrontend", "-emit-empty-object-file"])] - : [] + ? [ + .enableExperimentalFeature("Embedded"), + .enableExperimentalFeature("Extern"), + .unsafeFlags(["-Xfrontend", "-emit-empty-object-file"]) + ] : nil, + ), + .target( + name: "_CJavaScriptKit", + cSettings: shouldBuildForEmbedded ? [.define("__Embedded")] : nil ), - .target(name: "_CJavaScriptKit"), .target( name: "JavaScriptBigIntSupport", dependencies: ["_CJavaScriptBigIntSupport", "JavaScriptKit"] From d4348f336973fc3efd2bbe37b82a1740e7a447c7 Mon Sep 17 00:00:00 2001 From: Simon Leeb <52261246+sliemeobn@users.noreply.github.com> Date: Wed, 30 Oct 2024 13:47:24 +0100 Subject: [PATCH 168/373] use Context.environment instead of Foundation --- Package.swift | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/Package.swift b/Package.swift index 9a659695d..1c8a63195 100644 --- a/Package.swift +++ b/Package.swift @@ -1,10 +1,9 @@ // swift-tools-version:5.8 import PackageDescription -import Foundation // NOTE: needed for embedded customizations, ideally this will not be necessary at all in the future, or can be replaced with traits -let shouldBuildForEmbedded = ProcessInfo.processInfo.environment["JAVASCRIPTKIT_EXPERIMENTAL_EMBEDDED_WASM"].flatMap(Bool.init) ?? false +let shouldBuildForEmbedded = Context.environment["JAVASCRIPTKIT_EXPERIMENTAL_EMBEDDED_WASM"].flatMap(Bool.init) ?? false let package = Package( name: "JavaScriptKit", From fbd62f440d6b847d530a75fe3bc8b712ececf059 Mon Sep 17 00:00:00 2001 From: Simon Leeb <52261246+sliemeobn@users.noreply.github.com> Date: Wed, 30 Oct 2024 15:58:32 +0100 Subject: [PATCH 169/373] use __wasi__ instead of __Embedded --- Examples/Embedded/Package.swift | 5 +---- Package.swift | 8 ++------ Sources/_CJavaScriptKit/_CJavaScriptKit.c | 8 +++++--- Sources/_CJavaScriptKit/include/_CJavaScriptKit.h | 2 +- 4 files changed, 9 insertions(+), 14 deletions(-) diff --git a/Examples/Embedded/Package.swift b/Examples/Embedded/Package.swift index af5de3df6..227a049ff 100644 --- a/Examples/Embedded/Package.swift +++ b/Examples/Embedded/Package.swift @@ -15,10 +15,7 @@ let package = Package( "JavaScriptKit", .product(name: "dlmalloc", package: "swift-dlmalloc") ], - cSettings: [ - .unsafeFlags(["-fdeclspec"]), - .define("__Embedded"), - ], + cSettings: [.unsafeFlags(["-fdeclspec"])], swiftSettings: [ .enableExperimentalFeature("Embedded"), .enableExperimentalFeature("Extern"), diff --git a/Package.swift b/Package.swift index 1c8a63195..bf50843a0 100644 --- a/Package.swift +++ b/Package.swift @@ -19,8 +19,7 @@ let package = Package( dependencies: ["_CJavaScriptKit"], resources: shouldBuildForEmbedded ? [] : [.copy("Runtime")], cSettings: shouldBuildForEmbedded ? [ - .unsafeFlags(["-fdeclspec"]), - .define("__Embedded"), + .unsafeFlags(["-fdeclspec"]) ] : nil, swiftSettings: shouldBuildForEmbedded ? [ @@ -29,10 +28,7 @@ let package = Package( .unsafeFlags(["-Xfrontend", "-emit-empty-object-file"]) ] : nil, ), - .target( - name: "_CJavaScriptKit", - cSettings: shouldBuildForEmbedded ? [.define("__Embedded")] : nil - ), + .target(name: "_CJavaScriptKit"), .target( name: "JavaScriptBigIntSupport", dependencies: ["_CJavaScriptBigIntSupport", "JavaScriptKit"] diff --git a/Sources/_CJavaScriptKit/_CJavaScriptKit.c b/Sources/_CJavaScriptKit/_CJavaScriptKit.c index 6fc3fa916..424e9081b 100644 --- a/Sources/_CJavaScriptKit/_CJavaScriptKit.c +++ b/Sources/_CJavaScriptKit/_CJavaScriptKit.c @@ -1,6 +1,6 @@ #include "_CJavaScriptKit.h" #if __wasm32__ -#if __Embedded +#ifndef __wasi__ #if __has_include("malloc.h") #include #endif @@ -31,8 +31,10 @@ void swjs_cleanup_host_function_call(void *argv_buffer) { free(argv_buffer); } -#ifndef __Embedded -// cdecls don't work in Embedded, also @_expose(wasm) can be used with Swift >=6.0 +// 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); diff --git a/Sources/_CJavaScriptKit/include/_CJavaScriptKit.h b/Sources/_CJavaScriptKit/include/_CJavaScriptKit.h index f8279bff9..09689d711 100644 --- a/Sources/_CJavaScriptKit/include/_CJavaScriptKit.h +++ b/Sources/_CJavaScriptKit/include/_CJavaScriptKit.h @@ -1,7 +1,7 @@ #ifndef _CJavaScriptKit_h #define _CJavaScriptKit_h -#if __Embedded +#ifndef __wasi__ #include #else #include From e1821ea856e0760c35e751a2ce1e699923e1b5f9 Mon Sep 17 00:00:00 2001 From: Simon Leeb <52261246+sliemeobn@users.noreply.github.com> Date: Wed, 30 Oct 2024 16:59:49 +0100 Subject: [PATCH 170/373] removed trailing comma --- Package.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Package.swift b/Package.swift index bf50843a0..1f35305a1 100644 --- a/Package.swift +++ b/Package.swift @@ -26,7 +26,7 @@ let package = Package( .enableExperimentalFeature("Embedded"), .enableExperimentalFeature("Extern"), .unsafeFlags(["-Xfrontend", "-emit-empty-object-file"]) - ] : nil, + ] : nil ), .target(name: "_CJavaScriptKit"), .target( From dcdb1c55763c4e396058113a42f1d9bf08710869 Mon Sep 17 00:00:00 2001 From: Simon Leeb <52261246+sliemeobn@users.noreply.github.com> Date: Wed, 30 Oct 2024 17:14:23 +0100 Subject: [PATCH 171/373] using __has_include --- Sources/_CJavaScriptKit/include/_CJavaScriptKit.h | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Sources/_CJavaScriptKit/include/_CJavaScriptKit.h b/Sources/_CJavaScriptKit/include/_CJavaScriptKit.h index 09689d711..aa0b978a2 100644 --- a/Sources/_CJavaScriptKit/include/_CJavaScriptKit.h +++ b/Sources/_CJavaScriptKit/include/_CJavaScriptKit.h @@ -1,10 +1,10 @@ #ifndef _CJavaScriptKit_h #define _CJavaScriptKit_h -#ifndef __wasi__ -#include -#else +#if __has_include("stdlib.h") #include +#else +#include #endif #include #include From 441eddd7f326976cf50a29686aeed2738425ce80 Mon Sep 17 00:00:00 2001 From: Yuta Saito Date: Sat, 2 Nov 2024 21:36:42 +0900 Subject: [PATCH 172/373] Add Swift 6.0 to CI matrix --- .github/actions/install-swift/action.yml | 19 ++++------- .github/workflows/test.yml | 36 ++++++++++++-------- IntegrationTests/lib.js | 2 +- scripts/install-toolchain.sh | 42 ------------------------ 4 files changed, 30 insertions(+), 69 deletions(-) delete mode 100755 scripts/install-toolchain.sh diff --git a/.github/actions/install-swift/action.yml b/.github/actions/install-swift/action.yml index bdc4c9345..d6fbcc969 100644 --- a/.github/actions/install-swift/action.yml +++ b/.github/actions/install-swift/action.yml @@ -1,9 +1,8 @@ +name: 'Install Swift toolchain' +description: 'Install Swift toolchain tarball from URL' inputs: - swift-dir: - description: The directory name part of the distribution URL - required: true - swift-version: - description: Git tag indicating the Swift version + download-url: + description: 'URL to download Swift toolchain tarball' required: true runs: @@ -33,12 +32,6 @@ runs: zlib1g-dev curl - - name: Download Swift - shell: bash - run: curl -fLO https://download.swift.org/${{ inputs.swift-dir }}/${{ inputs.swift-version }}/${{ inputs.swift-version }}-ubuntu22.04.tar.gz - working-directory: ${{ env.RUNNER_TEMP }} - - - name: Unarchive and Install Swift + - name: Install Swift shell: bash - run: sudo tar -xf ${{ inputs.swift-version }}-ubuntu22.04.tar.gz --strip-components=2 -C /usr/local - working-directory: ${{ env.RUNNER_TEMP }} + run: curl -fL ${{ inputs.download-url }} | sudo tar xfz - --strip-components=2 -C /usr/local diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 8ba892e06..94fbd0518 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -23,22 +23,33 @@ jobs: - { os: ubuntu-20.04, toolchain: wasm-5.9.1-RELEASE, wasi-backend: MicroWASI } - { os: ubuntu-20.04, toolchain: wasm-5.10.0-RELEASE, wasi-backend: MicroWASI } - os: ubuntu-22.04 - toolchain: DEVELOPMENT-SNAPSHOT-2024-06-13-a + 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 swift-sdk: - id: DEVELOPMENT-SNAPSHOT-2024-06-14-a-wasm32-unknown-wasi - download-url: "https://github.com/swiftwasm/swift/releases/download/swift-wasm-DEVELOPMENT-SNAPSHOT-2024-06-14-a/swift-wasm-DEVELOPMENT-SNAPSHOT-2024-06-14-a-wasm32-unknown-wasi.artifactbundle.zip" + id: 6.0-SNAPSHOT-2024-10-29-a-wasm32-unknown-wasi + download-url: "https://github.com/swiftwasm/swift/releases/download/swift-wasm-6.0-SNAPSHOT-2024-10-29-a/swift-wasm-6.0-SNAPSHOT-2024-10-29-a-wasm32-unknown-wasi.artifactbundle.zip" + checksum: "434ce886e3e7a3ce56b2dd3b8cb7421810546a7b6305ccf39c130b4cb68de929" wasi-backend: Node - os: ubuntu-22.04 - toolchain: DEVELOPMENT-SNAPSHOT-2024-06-13-a + toolchain: + download-url: https://download.swift.org/development/ubuntu2204/swift-DEVELOPMENT-SNAPSHOT-2024-10-30-a/swift-DEVELOPMENT-SNAPSHOT-2024-10-30-a-ubuntu22.04.tar.gz swift-sdk: - id: DEVELOPMENT-SNAPSHOT-2024-06-14-a-wasm32-unknown-wasip1-threads - download-url: "https://github.com/swiftwasm/swift/releases/download/swift-wasm-DEVELOPMENT-SNAPSHOT-2024-06-14-a/swift-wasm-DEVELOPMENT-SNAPSHOT-2024-06-14-a-wasm32-unknown-wasip1-threads.artifactbundle.zip" + id: DEVELOPMENT-SNAPSHOT-2024-10-31-a-wasm32-unknown-wasi + download-url: "https://github.com/swiftwasm/swift/releases/download/swift-wasm-DEVELOPMENT-SNAPSHOT-2024-10-31-a/swift-wasm-DEVELOPMENT-SNAPSHOT-2024-10-31-a-wasm32-unknown-wasi.artifactbundle.zip" + checksum: "e42546397786ea6eaec2d9c07f9118a6f3428784cf3df3840a369f19700c1a69" + wasi-backend: Node + - os: ubuntu-22.04 + toolchain: + download-url: https://download.swift.org/development/ubuntu2204/swift-DEVELOPMENT-SNAPSHOT-2024-10-30-a/swift-DEVELOPMENT-SNAPSHOT-2024-10-30-a-ubuntu22.04.tar.gz + swift-sdk: + id: DEVELOPMENT-SNAPSHOT-2024-10-31-a-wasm32-unknown-wasip1-threads + download-url: "https://github.com/swiftwasm/swift/releases/download/swift-wasm-DEVELOPMENT-SNAPSHOT-2024-10-31-a/swift-wasm-DEVELOPMENT-SNAPSHOT-2024-10-31-a-wasm32-unknown-wasip1-threads.artifactbundle.zip" + checksum: "17dbbe61af6ca09c92ee2d68a56d5716530428e28c4c8358aa860cc4fcdc91ae" wasi-backend: Node runs-on: ${{ matrix.entry.os }} env: JAVASCRIPTKIT_WASI_BACKEND: ${{ matrix.entry.wasi-backend }} - SWIFT_VERSION: ${{ matrix.entry.toolchain }} steps: - name: Checkout uses: actions/checkout@v4 @@ -52,12 +63,11 @@ jobs: - uses: ./.github/actions/install-swift if: ${{ matrix.entry.swift-sdk }} with: - swift-dir: development/ubuntu2204 - swift-version: swift-${{ matrix.entry.toolchain }} + download-url: ${{ matrix.entry.toolchain.download-url }} - name: Install Swift SDK if: ${{ matrix.entry.swift-sdk }} run: | - swift sdk install "${{ matrix.entry.swift-sdk.download-url }}" + swift sdk install "${{ matrix.entry.swift-sdk.download-url }}" --checksum "${{ matrix.entry.swift-sdk.checksum }}" echo "SWIFT_SDK_ID=${{ matrix.entry.swift-sdk.id }}" >> $GITHUB_ENV - run: make bootstrap - run: make test @@ -94,11 +104,11 @@ jobs: matrix: entry: - os: ubuntu-22.04 - toolchain: DEVELOPMENT-SNAPSHOT-2024-09-25-a + toolchain: + download-url: https://download.swift.org/development/ubuntu2204/swift-DEVELOPMENT-SNAPSHOT-2024-10-30-a/swift-DEVELOPMENT-SNAPSHOT-2024-10-30-a-ubuntu22.04.tar.gz steps: - uses: actions/checkout@v4 - uses: ./.github/actions/install-swift with: - swift-dir: development/ubuntu2204 - swift-version: swift-${{ matrix.entry.toolchain }} + download-url: ${{ matrix.entry.toolchain.download-url }} - run: ./Examples/Embedded/build.sh diff --git a/IntegrationTests/lib.js b/IntegrationTests/lib.js index ed66c7e86..6c08cddde 100644 --- a/IntegrationTests/lib.js +++ b/IntegrationTests/lib.js @@ -177,7 +177,7 @@ export const startWasiTask = async (wasmPath, wasiConstructorKey = selectWASIBac // We don't have JS API to get memory descriptor of imported memory // at this moment, so we assume 256 pages (16MB) memory is enough // large for initial memory size. - const memory = new WebAssembly.Memory({ initial: 256, maximum: 16384, shared: true }) + const memory = new WebAssembly.Memory({ initial: 1024, maximum: 16384, shared: true }) importObject["env"] = { memory }; importObject["wasi"] = { "thread-spawn": (startArg) => { 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" From c6b60ac73f18fe4c3a390c1439e3ed19532a21de Mon Sep 17 00:00:00 2001 From: Yuta Saito Date: Sat, 2 Nov 2024 22:24:35 +0900 Subject: [PATCH 173/373] Enable the Extern feature explicitly --- Package.swift | 3 +++ 1 file changed, 3 insertions(+) diff --git a/Package.swift b/Package.swift index 1f35305a1..c33d7e71b 100644 --- a/Package.swift +++ b/Package.swift @@ -44,6 +44,9 @@ let package = Package( "JavaScriptEventLoop", "JavaScriptKit", "JavaScriptEventLoopTestSupport", + ], + swiftSettings: [ + .enableExperimentalFeature("Extern") ] ), .target(name: "_CJavaScriptEventLoop"), From 6b6a5edd61e392bc9a6bb0fe2c7597c8d4c5cd30 Mon Sep 17 00:00:00 2001 From: Yuta Saito Date: Thu, 7 Nov 2024 09:20:40 -0500 Subject: [PATCH 174/373] Update to stable 6.0.2 release --- .github/workflows/test.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 94fbd0518..8ed33aa5d 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -26,9 +26,9 @@ jobs: 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 swift-sdk: - id: 6.0-SNAPSHOT-2024-10-29-a-wasm32-unknown-wasi - download-url: "https://github.com/swiftwasm/swift/releases/download/swift-wasm-6.0-SNAPSHOT-2024-10-29-a/swift-wasm-6.0-SNAPSHOT-2024-10-29-a-wasm32-unknown-wasi.artifactbundle.zip" - checksum: "434ce886e3e7a3ce56b2dd3b8cb7421810546a7b6305ccf39c130b4cb68de929" + id: 6.0.2-RELEASE-wasm32-unknown-wasi + download-url: "https://github.com/swiftwasm/swift/releases/download/swift-wasm-6.0.2-RELEASE/swift-wasm-6.0.2-RELEASE-wasm32-unknown-wasi.artifactbundle.zip" + checksum: "6ffedb055cb9956395d9f435d03d53ebe9f6a8d45106b979d1b7f53358e1dcb4" wasi-backend: Node - os: ubuntu-22.04 toolchain: From afada107a8697dec81b1fce94b660db065c29976 Mon Sep 17 00:00:00 2001 From: Yuta Saito Date: Thu, 28 Nov 2024 08:49:27 +0900 Subject: [PATCH 175/373] Cover the case where a JSObject is deinitialized on a different thread --- IntegrationTests/lib.js | 1 + .../WebWorkerTaskExecutorTests.swift | 14 ++++++++++++++ 2 files changed, 15 insertions(+) diff --git a/IntegrationTests/lib.js b/IntegrationTests/lib.js index 6c08cddde..0172250d4 100644 --- a/IntegrationTests/lib.js +++ b/IntegrationTests/lib.js @@ -128,6 +128,7 @@ class ThreadRegistry { worker.on("error", (error) => { console.error(`Worker thread ${tid} error:`, error); + throw error; }); this.workers.set(tid, worker); worker.postMessage({ selfFilePath, module, programName, memory, tid, startArg }); diff --git a/Tests/JavaScriptEventLoopTests/WebWorkerTaskExecutorTests.swift b/Tests/JavaScriptEventLoopTests/WebWorkerTaskExecutorTests.swift index a31c783d3..fb19c2838 100644 --- a/Tests/JavaScriptEventLoopTests/WebWorkerTaskExecutorTests.swift +++ b/Tests/JavaScriptEventLoopTests/WebWorkerTaskExecutorTests.swift @@ -150,5 +150,19 @@ final class WebWorkerTaskExecutorTests: XCTestCase { } executor.terminate() } + +/* + func testDeinitJSObjectOnDifferentThread() async throws { + let executor = try await WebWorkerTaskExecutor(numberOfThreads: 1) + + var object: JSObject? = JSObject.global.Object.function!.new() + let task = Task(executorPreference: executor) { + object = nil + _ = object + } + await task.value + executor.terminate() + } +*/ } #endif From 45206f749419f94da78ff637ab222c63fbcc0842 Mon Sep 17 00:00:00 2001 From: Yuta Saito Date: Thu, 28 Nov 2024 11:04:24 +0900 Subject: [PATCH 176/373] Assert that `JSObject` is being accessed only from the owner thread --- Package.swift | 5 + .../FundamentalObjects/JSObject.swift | 96 +++++++++++++--- Sources/JavaScriptKit/ThreadLocal.swift | 103 ++++++++++++++++++ .../JavaScriptKitTests/ThreadLocalTests.swift | 34 ++++++ 4 files changed, 224 insertions(+), 14 deletions(-) create mode 100644 Sources/JavaScriptKit/ThreadLocal.swift create mode 100644 Tests/JavaScriptKitTests/ThreadLocalTests.swift diff --git a/Package.swift b/Package.swift index c33d7e71b..37c2d1f3c 100644 --- a/Package.swift +++ b/Package.swift @@ -58,6 +58,11 @@ let package = Package( ] ), .target(name: "_CJavaScriptEventLoopTestSupport"), + + .testTarget( + name: "JavaScriptKitTests", + dependencies: ["JavaScriptKit"] + ), .testTarget( name: "JavaScriptEventLoopTestSupportTests", dependencies: [ diff --git a/Sources/JavaScriptKit/FundamentalObjects/JSObject.swift b/Sources/JavaScriptKit/FundamentalObjects/JSObject.swift index 82b1e6502..788a2390d 100644 --- a/Sources/JavaScriptKit/FundamentalObjects/JSObject.swift +++ b/Sources/JavaScriptKit/FundamentalObjects/JSObject.swift @@ -1,5 +1,11 @@ import _CJavaScriptKit +#if canImport(wasi_pthread) + import wasi_pthread +#else + import Foundation // for pthread_t on non-wasi platforms +#endif + /// `JSObject` represents an object in JavaScript and supports dynamic member lookup. /// Any member access like `object.foo` will dynamically request the JavaScript and Swift /// runtime bridge library for a member with the specified name in this object. @@ -18,9 +24,35 @@ import _CJavaScriptKit public class JSObject: Equatable { @_spi(JSObject_id) public var id: JavaScriptObjectRef + +#if _runtime(_multithreaded) + private let ownerThread: pthread_t +#endif + @_spi(JSObject_id) public init(id: JavaScriptObjectRef) { self.id = id + self.ownerThread = pthread_self() + } + + /// 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 _runtime(_multithreaded) + precondition(pthread_equal(ownerThread, pthread_self()) != 0, "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 _runtime(_multithreaded) + precondition(pthread_equal(lhs.ownerThread, rhs.ownerThread) != 0, "JSObject is being accessed from a thread other than the owner thread: \(hint())") + #endif } #if !hasFeature(Embedded) @@ -79,32 +111,56 @@ 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 { getJSValue(this: self, symbol: name) } - set { setJSValue(this: self, symbol: name, value: newValue) } + 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) @@ -134,7 +190,8 @@ public class JSObject: Equatable { /// - Parameter constructor: The constructor function to check. /// - Returns: The result of `instanceof` in the JavaScript environment. public func isInstanceOf(_ constructor: JSFunction) -> Bool { - swjs_instanceof(id, constructor.id) + assertOnOwnerThread(hint: "calling 'isInstanceOf'") + return swjs_instanceof(id, constructor.id) } static let _JS_Predef_Value_Global: JavaScriptObjectRef = 0 @@ -145,14 +202,24 @@ public class JSObject: Equatable { // `JSObject` storage itself is immutable, and use of `JSObject.global` from other // threads maintains the same semantics as `globalThis` in JavaScript. - #if compiler(>=5.10) - nonisolated(unsafe) - static let _global = JSObject(id: _JS_Predef_Value_Global) + #if _runtime(_multithreaded) + @LazyThreadLocal(initialize: { + return JSObject(id: _JS_Predef_Value_Global) + }) + private static var _global: JSObject #else - static let _global = JSObject(id: _JS_Predef_Value_Global) + #if compiler(>=5.10) + nonisolated(unsafe) + static let _global = JSObject(id: _JS_Predef_Value_Global) + #else + static let _global = JSObject(id: _JS_Predef_Value_Global) + #endif #endif - deinit { swjs_release(id) } + deinit { + assertOnOwnerThread(hint: "deinitializing") + swjs_release(id) + } /// Returns a Boolean value indicating whether two values point to same objects. /// @@ -160,6 +227,7 @@ 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 } diff --git a/Sources/JavaScriptKit/ThreadLocal.swift b/Sources/JavaScriptKit/ThreadLocal.swift new file mode 100644 index 000000000..967f6e7db --- /dev/null +++ b/Sources/JavaScriptKit/ThreadLocal.swift @@ -0,0 +1,103 @@ +#if _runtime(_multithreaded) +#if canImport(wasi_pthread) +import wasi_pthread +#elseif canImport(Darwin) +import Darwin +#elseif canImport(Glibc) +import Glibc +#else +#error("Unsupported platform") +#endif + +@propertyWrapper +final class ThreadLocal: Sendable { + 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 + + 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() } + } + + class Box { + let value: Value + init(_ value: Value) { + self.value = value + } + } + + 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() } + } + + deinit { + if let oldPointer = pthread_getspecific(key) { + release(oldPointer) + } + pthread_key_delete(key) + } +} + +@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 + } +} + +#endif diff --git a/Tests/JavaScriptKitTests/ThreadLocalTests.swift b/Tests/JavaScriptKitTests/ThreadLocalTests.swift new file mode 100644 index 000000000..0641e6fd2 --- /dev/null +++ b/Tests/JavaScriptKitTests/ThreadLocalTests.swift @@ -0,0 +1,34 @@ +import XCTest +@testable import JavaScriptKit + +final class ThreadLocalTests: XCTestCase { + class MyHeapObject {} + + func testLeak() throws { + struct Check { + @ThreadLocal + var value: MyHeapObject? + } + weak var weakObject: MyHeapObject? + do { + let object = MyHeapObject() + weakObject = object + let check = Check() + check.value = object + XCTAssertNotNil(check.value) + XCTAssertTrue(check.value === object) + } + XCTAssertNil(weakObject) + } + + func testLazyThreadLocal() throws { + struct Check { + @LazyThreadLocal(initialize: { MyHeapObject() }) + var value: MyHeapObject + } + let check = Check() + let object1 = check.value + let object2 = check.value + XCTAssertTrue(object1 === object2) + } +} From a05e7981748e8880112655b9695ed5c920cb4a4e Mon Sep 17 00:00:00 2001 From: Yuta Saito Date: Thu, 28 Nov 2024 11:57:55 +0900 Subject: [PATCH 177/373] Avoid sharing JSObject by global variables --- .../JavaScriptBigIntSupport/Int64+I64.swift | 4 +-- .../JavaScriptKit/BasicObjects/JSArray.swift | 4 ++- .../JavaScriptKit/BasicObjects/JSDate.swift | 4 ++- .../JavaScriptKit/BasicObjects/JSError.swift | 4 ++- .../BasicObjects/JSTypedArray.swift | 31 ++++++++++++------- .../FundamentalObjects/JSBigInt.swift | 6 ++-- .../FundamentalObjects/JSObject.swift | 4 +++ .../FundamentalObjects/JSSymbol.swift | 26 ++++++++-------- Sources/JavaScriptKit/JSValueDecoder.swift | 5 ++- 9 files changed, 53 insertions(+), 35 deletions(-) diff --git a/Sources/JavaScriptBigIntSupport/Int64+I64.swift b/Sources/JavaScriptBigIntSupport/Int64+I64.swift index fdd1d544f..e361e72e9 100644 --- a/Sources/JavaScriptBigIntSupport/Int64+I64.swift +++ b/Sources/JavaScriptBigIntSupport/Int64+I64.swift @@ -1,13 +1,13 @@ import JavaScriptKit extension UInt64: JavaScriptKit.ConvertibleToJSValue, JavaScriptKit.TypedArrayElement { - public static var typedArrayClass = JSObject.global.BigUint64Array.function! + 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 = JSObject.global.BigInt64Array.function! + public static var typedArrayClass: JSFunction { JSObject.global.BigInt64Array.function! } public var jsValue: JSValue { .bigInt(JSBigInt(self)) } } diff --git a/Sources/JavaScriptKit/BasicObjects/JSArray.swift b/Sources/JavaScriptKit/BasicObjects/JSArray.swift index 90dba72d8..a431eb9a5 100644 --- a/Sources/JavaScriptKit/BasicObjects/JSArray.swift +++ b/Sources/JavaScriptKit/BasicObjects/JSArray.swift @@ -2,7 +2,9 @@ /// 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 } + @LazyThreadLocal(initialize: { JSObject.global.Array.function }) + private static var _constructor: JSFunction? static func isArray(_ object: JSObject) -> Bool { constructor!.isArray!(object).boolean! diff --git a/Sources/JavaScriptKit/BasicObjects/JSDate.swift b/Sources/JavaScriptKit/BasicObjects/JSDate.swift index 767374125..da31aca06 100644 --- a/Sources/JavaScriptKit/BasicObjects/JSDate.swift +++ b/Sources/JavaScriptKit/BasicObjects/JSDate.swift @@ -8,7 +8,9 @@ */ 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 } + @LazyThreadLocal(initialize: { JSObject.global.Date.function }) + private static var _constructor: JSFunction? /// The underlying JavaScript `Date` object. public let jsObject: JSObject diff --git a/Sources/JavaScriptKit/BasicObjects/JSError.swift b/Sources/JavaScriptKit/BasicObjects/JSError.swift index e9b006c81..559618e15 100644 --- a/Sources/JavaScriptKit/BasicObjects/JSError.swift +++ b/Sources/JavaScriptKit/BasicObjects/JSError.swift @@ -4,7 +4,9 @@ */ public final class JSError: Error, 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 } + @LazyThreadLocal(initialize: { JSObject.global.Error.function }) + private static var _constructor: JSFunction? /// The underlying JavaScript `Error` object. public let jsObject: JSObject diff --git a/Sources/JavaScriptKit/BasicObjects/JSTypedArray.swift b/Sources/JavaScriptKit/BasicObjects/JSTypedArray.swift index 2168292f7..bc80cd25c 100644 --- a/Sources/JavaScriptKit/BasicObjects/JSTypedArray.swift +++ b/Sources/JavaScriptKit/BasicObjects/JSTypedArray.swift @@ -47,7 +47,10 @@ public class JSTypedArray: JSBridgedClass, ExpressibleByArrayLiteral wh /// - Parameter array: The array that will be copied to create a new instance of TypedArray public convenience init(_ array: [Element]) { let jsArrayRef = array.withUnsafeBufferPointer { ptr in - swjs_create_typed_array(Self.constructor!.id, ptr.baseAddress, Int32(array.count)) + // Retain the constructor function to avoid it being released before calling `swjs_create_typed_array` + withExtendedLifetime(Self.constructor!) { ctor in + swjs_create_typed_array(ctor.id, ptr.baseAddress, Int32(array.count)) + } } self.init(unsafelyWrapping: JSObject(id: jsArrayRef)) } @@ -140,21 +143,27 @@ func valueForBitWidth(typeName: String, bitWidth: Int, when32: T) -> T { } extension Int: TypedArrayElement { - public static var typedArrayClass: JSFunction = + public static var typedArrayClass: JSFunction { _typedArrayClass } + @LazyThreadLocal(initialize: { valueForBitWidth(typeName: "Int", bitWidth: Int.bitWidth, when32: JSObject.global.Int32Array).function! + }) + private static var _typedArrayClass: JSFunction } extension UInt: TypedArrayElement { - public static var typedArrayClass: JSFunction = + public static var typedArrayClass: JSFunction { _typedArrayClass } + @LazyThreadLocal(initialize: { valueForBitWidth(typeName: "UInt", bitWidth: Int.bitWidth, when32: JSObject.global.Uint32Array).function! + }) + private static var _typedArrayClass: JSFunction } 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! } } /// A wrapper around [the JavaScript `Uint8ClampedArray` @@ -165,26 +174,26 @@ public class JSUInt8ClampedArray: JSTypedArray { } 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! } } 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! } } #endif diff --git a/Sources/JavaScriptKit/FundamentalObjects/JSBigInt.swift b/Sources/JavaScriptKit/FundamentalObjects/JSBigInt.swift index f3687246e..a8867f95c 100644 --- a/Sources/JavaScriptKit/FundamentalObjects/JSBigInt.swift +++ b/Sources/JavaScriptKit/FundamentalObjects/JSBigInt.swift @@ -1,6 +1,6 @@ import _CJavaScriptKit -private let constructor = JSObject.global.BigInt.function! +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) @@ -30,9 +30,9 @@ public final class JSBigInt: JSObject { public func clamped(bitSize: Int, signed: Bool) -> JSBigInt { if signed { - return constructor.asIntN!(bitSize, self).bigInt! + return constructor.asIntN(bitSize, self).bigInt! } else { - return constructor.asUintN!(bitSize, self).bigInt! + return constructor.asUintN(bitSize, self).bigInt! } } } diff --git a/Sources/JavaScriptKit/FundamentalObjects/JSObject.swift b/Sources/JavaScriptKit/FundamentalObjects/JSObject.swift index 788a2390d..25d863969 100644 --- a/Sources/JavaScriptKit/FundamentalObjects/JSObject.swift +++ b/Sources/JavaScriptKit/FundamentalObjects/JSObject.swift @@ -22,6 +22,10 @@ import _CJavaScriptKit /// reference counting system. @dynamicMemberLookup public class JSObject: Equatable { + internal static var constructor: JSFunction { _constructor } + @LazyThreadLocal(initialize: { JSObject.global.Object.function! }) + internal static var _constructor: JSFunction + @_spi(JSObject_id) public var id: JavaScriptObjectRef diff --git a/Sources/JavaScriptKit/FundamentalObjects/JSSymbol.swift b/Sources/JavaScriptKit/FundamentalObjects/JSSymbol.swift index d768b6675..567976c70 100644 --- a/Sources/JavaScriptKit/FundamentalObjects/JSSymbol.swift +++ b/Sources/JavaScriptKit/FundamentalObjects/JSSymbol.swift @@ -47,17 +47,17 @@ public class JSSymbol: JSObject { } extension JSSymbol { - public static let asyncIterator: JSSymbol! = Symbol.asyncIterator.symbol - public static let hasInstance: JSSymbol! = Symbol.hasInstance.symbol - public static let isConcatSpreadable: JSSymbol! = Symbol.isConcatSpreadable.symbol - public static let iterator: JSSymbol! = Symbol.iterator.symbol - public static let match: JSSymbol! = Symbol.match.symbol - public static let matchAll: JSSymbol! = Symbol.matchAll.symbol - public static let replace: JSSymbol! = Symbol.replace.symbol - public static let search: JSSymbol! = Symbol.search.symbol - public static let species: JSSymbol! = Symbol.species.symbol - public static let split: JSSymbol! = Symbol.split.symbol - public static let toPrimitive: JSSymbol! = Symbol.toPrimitive.symbol - public static let toStringTag: JSSymbol! = Symbol.toStringTag.symbol - public static let unscopables: JSSymbol! = Symbol.unscopables.symbol + 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/JSValueDecoder.swift b/Sources/JavaScriptKit/JSValueDecoder.swift index 73ee9310c..b2cf7b2a3 100644 --- a/Sources/JavaScriptKit/JSValueDecoder.swift +++ b/Sources/JavaScriptKit/JSValueDecoder.swift @@ -35,9 +35,8 @@ private struct _Decoder: Decoder { } private enum Object { - static let ref = JSObject.global.Object.function! static func keys(_ object: JSObject) -> [String] { - let keys = ref.keys!(object).array! + let keys = JSObject.constructor.keys!(object).array! return keys.map { $0.string! } } } @@ -249,4 +248,4 @@ public class JSValueDecoder { return try T(from: decoder) } } -#endif \ No newline at end of file +#endif From 9a141cbab35079891f3fea1c83cd1a7213364fff Mon Sep 17 00:00:00 2001 From: Yuta Saito Date: Thu, 28 Nov 2024 12:12:50 +0900 Subject: [PATCH 178/373] Gate the use of `_runtime(_multithreaded)` with `compiler(>=6.1)` --- Sources/JavaScriptKit/FundamentalObjects/JSObject.swift | 8 ++++---- Sources/JavaScriptKit/ThreadLocal.swift | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/Sources/JavaScriptKit/FundamentalObjects/JSObject.swift b/Sources/JavaScriptKit/FundamentalObjects/JSObject.swift index 25d863969..48cca6fc7 100644 --- a/Sources/JavaScriptKit/FundamentalObjects/JSObject.swift +++ b/Sources/JavaScriptKit/FundamentalObjects/JSObject.swift @@ -29,7 +29,7 @@ public class JSObject: Equatable { @_spi(JSObject_id) public var id: JavaScriptObjectRef -#if _runtime(_multithreaded) +#if compiler(>=6.1) && _runtime(_multithreaded) private let ownerThread: pthread_t #endif @@ -47,14 +47,14 @@ public class JSObject: Equatable { /// 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 _runtime(_multithreaded) + #if compiler(>=6.1) && _runtime(_multithreaded) precondition(pthread_equal(ownerThread, pthread_self()) != 0, "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 _runtime(_multithreaded) + #if compiler(>=6.1) && _runtime(_multithreaded) precondition(pthread_equal(lhs.ownerThread, rhs.ownerThread) != 0, "JSObject is being accessed from a thread other than the owner thread: \(hint())") #endif } @@ -206,7 +206,7 @@ public class JSObject: Equatable { // `JSObject` storage itself is immutable, and use of `JSObject.global` from other // threads maintains the same semantics as `globalThis` in JavaScript. - #if _runtime(_multithreaded) + #if compiler(>=6.1) && _runtime(_multithreaded) @LazyThreadLocal(initialize: { return JSObject(id: _JS_Predef_Value_Global) }) diff --git a/Sources/JavaScriptKit/ThreadLocal.swift b/Sources/JavaScriptKit/ThreadLocal.swift index 967f6e7db..a9026ebd5 100644 --- a/Sources/JavaScriptKit/ThreadLocal.swift +++ b/Sources/JavaScriptKit/ThreadLocal.swift @@ -1,4 +1,4 @@ -#if _runtime(_multithreaded) +#if compiler(>=6.1) && _runtime(_multithreaded) #if canImport(wasi_pthread) import wasi_pthread #elseif canImport(Darwin) From 49b207a79b2ae3d383f2dbf59a3c2f218198057a Mon Sep 17 00:00:00 2001 From: Yuta Saito Date: Thu, 28 Nov 2024 12:24:00 +0900 Subject: [PATCH 179/373] Add more tests for `ThreadLocal` and `LazyThreadLocal` --- .../WebWorkerTaskExecutorTests.swift | 51 ++++++++++++++++++- .../JavaScriptKitTests/ThreadLocalTests.swift | 16 ++++++ 2 files changed, 66 insertions(+), 1 deletion(-) diff --git a/Tests/JavaScriptEventLoopTests/WebWorkerTaskExecutorTests.swift b/Tests/JavaScriptEventLoopTests/WebWorkerTaskExecutorTests.swift index fb19c2838..645c6e388 100644 --- a/Tests/JavaScriptEventLoopTests/WebWorkerTaskExecutorTests.swift +++ b/Tests/JavaScriptEventLoopTests/WebWorkerTaskExecutorTests.swift @@ -1,7 +1,7 @@ #if compiler(>=6.1) && _runtime(_multithreaded) import XCTest -import JavaScriptKit import _CJavaScriptKit // For swjs_get_worker_thread_id +@testable import JavaScriptKit @testable import JavaScriptEventLoop @_extern(wasm, module: "JavaScriptEventLoopTestSupportTests", name: "isMainThread") @@ -151,6 +151,55 @@ final class WebWorkerTaskExecutorTests: XCTestCase { executor.terminate() } + func testThreadLocalPerThreadValues() async throws { + struct Check { + @ThreadLocal(boxing: ()) + static var value: Int? + } + let executor = try await WebWorkerTaskExecutor(numberOfThreads: 1) + XCTAssertNil(Check.value) + Check.value = 42 + XCTAssertEqual(Check.value, 42) + + let task = Task(executorPreference: executor) { + XCTAssertEqual(Check.value, nil) + Check.value = 100 + XCTAssertEqual(Check.value, 100) + return Check.value + } + let result = await task.value + XCTAssertEqual(result, 100) + XCTAssertEqual(Check.value, 42) + } + + func testLazyThreadLocalPerThreadInitialization() async throws { + struct Check { + static var valueToInitialize = 42 + static var countOfInitialization = 0 + @LazyThreadLocal(initialize: { + countOfInitialization += 1 + return valueToInitialize + }) + static var value: Int + } + let executor = try await WebWorkerTaskExecutor(numberOfThreads: 1) + XCTAssertEqual(Check.countOfInitialization, 0) + XCTAssertEqual(Check.value, 42) + XCTAssertEqual(Check.countOfInitialization, 1) + + Check.valueToInitialize = 100 + + let task = Task(executorPreference: executor) { + XCTAssertEqual(Check.countOfInitialization, 1) + XCTAssertEqual(Check.value, 100) + XCTAssertEqual(Check.countOfInitialization, 2) + return Check.value + } + let result = await task.value + XCTAssertEqual(result, 100) + XCTAssertEqual(Check.countOfInitialization, 2) + } + /* func testDeinitJSObjectOnDifferentThread() async throws { let executor = try await WebWorkerTaskExecutor(numberOfThreads: 1) diff --git a/Tests/JavaScriptKitTests/ThreadLocalTests.swift b/Tests/JavaScriptKitTests/ThreadLocalTests.swift index 0641e6fd2..5d176bd10 100644 --- a/Tests/JavaScriptKitTests/ThreadLocalTests.swift +++ b/Tests/JavaScriptKitTests/ThreadLocalTests.swift @@ -3,11 +3,16 @@ import XCTest final class ThreadLocalTests: XCTestCase { class MyHeapObject {} + struct MyStruct { + var object: MyHeapObject + } func testLeak() throws { struct Check { @ThreadLocal var value: MyHeapObject? + @ThreadLocal(boxing: ()) + var value2: MyStruct? } weak var weakObject: MyHeapObject? do { @@ -19,6 +24,17 @@ final class ThreadLocalTests: XCTestCase { XCTAssertTrue(check.value === object) } XCTAssertNil(weakObject) + + weak var weakObject2: MyHeapObject? + do { + let object = MyHeapObject() + weakObject2 = object + let check = Check() + check.value2 = MyStruct(object: object) + XCTAssertNotNil(check.value2) + XCTAssertTrue(check.value2!.object === object) + } + XCTAssertNil(weakObject2) } func testLazyThreadLocal() throws { From d4e0ee8eabcf8027537afd031559076bdd083d4c Mon Sep 17 00:00:00 2001 From: Yuta Saito Date: Thu, 28 Nov 2024 16:09:57 +0900 Subject: [PATCH 180/373] Restrict the use of `ThreadLocal` to immortal storage only --- .../FundamentalObjects/JSObject.swift | 17 +++------- Sources/JavaScriptKit/ThreadLocal.swift | 34 ++++++++++++++----- .../JavaScriptKitTests/ThreadLocalTests.swift | 27 +++++++-------- 3 files changed, 43 insertions(+), 35 deletions(-) diff --git a/Sources/JavaScriptKit/FundamentalObjects/JSObject.swift b/Sources/JavaScriptKit/FundamentalObjects/JSObject.swift index 48cca6fc7..9fd1b1248 100644 --- a/Sources/JavaScriptKit/FundamentalObjects/JSObject.swift +++ b/Sources/JavaScriptKit/FundamentalObjects/JSObject.swift @@ -206,19 +206,10 @@ public class JSObject: Equatable { // `JSObject` storage itself is immutable, and use of `JSObject.global` from other // threads maintains the same semantics as `globalThis` in JavaScript. - #if compiler(>=6.1) && _runtime(_multithreaded) - @LazyThreadLocal(initialize: { - return JSObject(id: _JS_Predef_Value_Global) - }) - private static var _global: JSObject - #else - #if compiler(>=5.10) - nonisolated(unsafe) - static let _global = JSObject(id: _JS_Predef_Value_Global) - #else - static let _global = JSObject(id: _JS_Predef_Value_Global) - #endif - #endif + @LazyThreadLocal(initialize: { + return JSObject(id: _JS_Predef_Value_Global) + }) + private static var _global: JSObject deinit { assertOnOwnerThread(hint: "deinitializing") diff --git a/Sources/JavaScriptKit/ThreadLocal.swift b/Sources/JavaScriptKit/ThreadLocal.swift index a9026ebd5..146c0c060 100644 --- a/Sources/JavaScriptKit/ThreadLocal.swift +++ b/Sources/JavaScriptKit/ThreadLocal.swift @@ -1,4 +1,3 @@ -#if compiler(>=6.1) && _runtime(_multithreaded) #if canImport(wasi_pthread) import wasi_pthread #elseif canImport(Darwin) @@ -9,8 +8,14 @@ import Glibc #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 { @@ -34,6 +39,8 @@ final class ThreadLocal: Sendable { 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) @@ -43,13 +50,15 @@ final class ThreadLocal: Sendable { self.release = { Unmanaged.fromOpaque($0).release() } } - class Box { + 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) @@ -65,15 +74,26 @@ final class ThreadLocal: Sendable { } self.release = { Unmanaged.fromOpaque($0).release() } } +#else + // Fallback implementation for platforms that don't support pthread + + var wrappedValue: Value? + + init() where Value: AnyObject { + wrappedValue = nil + } + init(boxing _: Void) { + wrappedValue = nil + } +#endif deinit { - if let oldPointer = pthread_getspecific(key) { - release(oldPointer) - } - pthread_key_delete(key) + 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 @@ -99,5 +119,3 @@ final class LazyThreadLocal: Sendable { self.initialValue = initialize } } - -#endif diff --git a/Tests/JavaScriptKitTests/ThreadLocalTests.swift b/Tests/JavaScriptKitTests/ThreadLocalTests.swift index 5d176bd10..761e82b51 100644 --- a/Tests/JavaScriptKitTests/ThreadLocalTests.swift +++ b/Tests/JavaScriptKitTests/ThreadLocalTests.swift @@ -10,18 +10,18 @@ final class ThreadLocalTests: XCTestCase { func testLeak() throws { struct Check { @ThreadLocal - var value: MyHeapObject? + static var value: MyHeapObject? @ThreadLocal(boxing: ()) - var value2: MyStruct? + static var value2: MyStruct? } weak var weakObject: MyHeapObject? do { let object = MyHeapObject() weakObject = object - let check = Check() - check.value = object - XCTAssertNotNil(check.value) - XCTAssertTrue(check.value === object) + Check.value = object + XCTAssertNotNil(Check.value) + XCTAssertTrue(Check.value === object) + Check.value = nil } XCTAssertNil(weakObject) @@ -29,10 +29,10 @@ final class ThreadLocalTests: XCTestCase { do { let object = MyHeapObject() weakObject2 = object - let check = Check() - check.value2 = MyStruct(object: object) - XCTAssertNotNil(check.value2) - XCTAssertTrue(check.value2!.object === object) + Check.value2 = MyStruct(object: object) + XCTAssertNotNil(Check.value2) + XCTAssertTrue(Check.value2!.object === object) + Check.value2 = nil } XCTAssertNil(weakObject2) } @@ -40,11 +40,10 @@ final class ThreadLocalTests: XCTestCase { func testLazyThreadLocal() throws { struct Check { @LazyThreadLocal(initialize: { MyHeapObject() }) - var value: MyHeapObject + static var value: MyHeapObject } - let check = Check() - let object1 = check.value - let object2 = check.value + let object1 = Check.value + let object2 = Check.value XCTAssertTrue(object1 === object2) } } From 9b7fda00dfbedc341ac64dac8bbca109f57157d6 Mon Sep 17 00:00:00 2001 From: Yuta Saito Date: Thu, 28 Nov 2024 16:21:13 +0900 Subject: [PATCH 181/373] Build fix for wasm32-unknown-wasi --- .../FundamentalObjects/JSObject.swift | 2 ++ Sources/JavaScriptKit/ThreadLocal.swift | 4 ++- .../JavaScriptKitTests/ThreadLocalTests.swift | 29 +++++++++---------- 3 files changed, 18 insertions(+), 17 deletions(-) diff --git a/Sources/JavaScriptKit/FundamentalObjects/JSObject.swift b/Sources/JavaScriptKit/FundamentalObjects/JSObject.swift index 9fd1b1248..c5eed713b 100644 --- a/Sources/JavaScriptKit/FundamentalObjects/JSObject.swift +++ b/Sources/JavaScriptKit/FundamentalObjects/JSObject.swift @@ -36,7 +36,9 @@ public class JSObject: Equatable { @_spi(JSObject_id) public init(id: JavaScriptObjectRef) { self.id = id +#if compiler(>=6.1) && _runtime(_multithreaded) self.ownerThread = pthread_self() +#endif } /// Asserts that the object is being accessed from the owner thread. diff --git a/Sources/JavaScriptKit/ThreadLocal.swift b/Sources/JavaScriptKit/ThreadLocal.swift index 146c0c060..0ad0188b0 100644 --- a/Sources/JavaScriptKit/ThreadLocal.swift +++ b/Sources/JavaScriptKit/ThreadLocal.swift @@ -1,5 +1,7 @@ +#if os(WASI) #if canImport(wasi_pthread) import wasi_pthread +#endif #elseif canImport(Darwin) import Darwin #elseif canImport(Glibc) @@ -77,7 +79,7 @@ final class ThreadLocal: Sendable { #else // Fallback implementation for platforms that don't support pthread - var wrappedValue: Value? + nonisolated(unsafe) var wrappedValue: Value? init() where Value: AnyObject { wrappedValue = nil diff --git a/Tests/JavaScriptKitTests/ThreadLocalTests.swift b/Tests/JavaScriptKitTests/ThreadLocalTests.swift index 761e82b51..55fcdadb4 100644 --- a/Tests/JavaScriptKitTests/ThreadLocalTests.swift +++ b/Tests/JavaScriptKitTests/ThreadLocalTests.swift @@ -9,19 +9,17 @@ final class ThreadLocalTests: XCTestCase { func testLeak() throws { struct Check { - @ThreadLocal - static var value: MyHeapObject? - @ThreadLocal(boxing: ()) - static var value2: MyStruct? + static let value = ThreadLocal() + static let value2 = ThreadLocal(boxing: ()) } weak var weakObject: MyHeapObject? do { let object = MyHeapObject() weakObject = object - Check.value = object - XCTAssertNotNil(Check.value) - XCTAssertTrue(Check.value === object) - Check.value = nil + Check.value.wrappedValue = object + XCTAssertNotNil(Check.value.wrappedValue) + XCTAssertTrue(Check.value.wrappedValue === object) + Check.value.wrappedValue = nil } XCTAssertNil(weakObject) @@ -29,21 +27,20 @@ final class ThreadLocalTests: XCTestCase { do { let object = MyHeapObject() weakObject2 = object - Check.value2 = MyStruct(object: object) - XCTAssertNotNil(Check.value2) - XCTAssertTrue(Check.value2!.object === object) - Check.value2 = nil + 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 { - @LazyThreadLocal(initialize: { MyHeapObject() }) - static var value: MyHeapObject + static let value = LazyThreadLocal(initialize: { MyHeapObject() }) } - let object1 = Check.value - let object2 = Check.value + let object1 = Check.value.wrappedValue + let object2 = Check.value.wrappedValue XCTAssertTrue(object1 === object2) } } From d5905281c190381049ea85ec851664bc093b2bbb Mon Sep 17 00:00:00 2001 From: Yuta Saito Date: Thu, 28 Nov 2024 16:24:01 +0900 Subject: [PATCH 182/373] Build fix for Swift 5.9 --- Sources/JavaScriptKit/FundamentalObjects/JSObject.swift | 2 -- Sources/JavaScriptKit/ThreadLocal.swift | 5 ++++- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/Sources/JavaScriptKit/FundamentalObjects/JSObject.swift b/Sources/JavaScriptKit/FundamentalObjects/JSObject.swift index c5eed713b..52c81c969 100644 --- a/Sources/JavaScriptKit/FundamentalObjects/JSObject.swift +++ b/Sources/JavaScriptKit/FundamentalObjects/JSObject.swift @@ -206,8 +206,6 @@ public class JSObject: Equatable { /// This allows access to the global properties and global names by accessing the `JSObject` returned. public static var global: JSObject { return _global } - // `JSObject` storage itself is immutable, and use of `JSObject.global` from other - // threads maintains the same semantics as `globalThis` in JavaScript. @LazyThreadLocal(initialize: { return JSObject(id: _JS_Predef_Value_Global) }) diff --git a/Sources/JavaScriptKit/ThreadLocal.swift b/Sources/JavaScriptKit/ThreadLocal.swift index 0ad0188b0..6d83c966c 100644 --- a/Sources/JavaScriptKit/ThreadLocal.swift +++ b/Sources/JavaScriptKit/ThreadLocal.swift @@ -78,8 +78,11 @@ final class ThreadLocal: Sendable { } #else // Fallback implementation for platforms that don't support pthread - + #if compiler(>=5.10) nonisolated(unsafe) var wrappedValue: Value? + #else + var wrappedValue: Value? + #endif init() where Value: AnyObject { wrappedValue = nil From e2f569a05779dd027739c64417393caa72be264b Mon Sep 17 00:00:00 2001 From: Yuta Saito Date: Thu, 28 Nov 2024 16:29:28 +0900 Subject: [PATCH 183/373] Build fix for embedded wasm --- Sources/JavaScriptKit/FundamentalObjects/JSObject.swift | 6 ++++-- Sources/JavaScriptKit/ThreadLocal.swift | 2 +- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/Sources/JavaScriptKit/FundamentalObjects/JSObject.swift b/Sources/JavaScriptKit/FundamentalObjects/JSObject.swift index 52c81c969..143cbdb39 100644 --- a/Sources/JavaScriptKit/FundamentalObjects/JSObject.swift +++ b/Sources/JavaScriptKit/FundamentalObjects/JSObject.swift @@ -1,7 +1,9 @@ import _CJavaScriptKit -#if canImport(wasi_pthread) - import wasi_pthread +#if arch(wasm32) + #if canImport(wasi_pthread) + import wasi_pthread + #endif #else import Foundation // for pthread_t on non-wasi platforms #endif diff --git a/Sources/JavaScriptKit/ThreadLocal.swift b/Sources/JavaScriptKit/ThreadLocal.swift index 6d83c966c..fe22c6abb 100644 --- a/Sources/JavaScriptKit/ThreadLocal.swift +++ b/Sources/JavaScriptKit/ThreadLocal.swift @@ -1,4 +1,4 @@ -#if os(WASI) +#if arch(wasm32) #if canImport(wasi_pthread) import wasi_pthread #endif From 5b79ddf40abd76a3709495c8be307158ae4dbd11 Mon Sep 17 00:00:00 2001 From: Yuta Saito Date: Thu, 28 Nov 2024 16:32:59 +0900 Subject: [PATCH 184/373] Suppress sendability warnings on single-threaded platform --- Sources/JavaScriptKit/ThreadLocal.swift | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/Sources/JavaScriptKit/ThreadLocal.swift b/Sources/JavaScriptKit/ThreadLocal.swift index fe22c6abb..9f5751c96 100644 --- a/Sources/JavaScriptKit/ThreadLocal.swift +++ b/Sources/JavaScriptKit/ThreadLocal.swift @@ -78,11 +78,14 @@ final class ThreadLocal: Sendable { } #else // Fallback implementation for platforms that don't support pthread - #if compiler(>=5.10) - nonisolated(unsafe) var wrappedValue: Value? - #else - var wrappedValue: Value? - #endif + 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 From 115ca293dbdf2ef4c2dba7cc5fa16354af0ac44c Mon Sep 17 00:00:00 2001 From: Yuta Saito Date: Thu, 28 Nov 2024 17:08:39 +0900 Subject: [PATCH 185/373] Add CONTRIBUTING.md --- CONTRIBUTING.md | 69 +++++++++++++++++++++++++++++++++++++++++++++++++ README.md | 27 ------------------- 2 files changed, 69 insertions(+), 27 deletions(-) create mode 100644 CONTRIBUTING.md diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 000000000..c286c33fb --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,69 @@ +# 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"; + 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"; + ) + ``` + +
+ +
+ 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 install "https://github.com/swiftwasm/swift/releases/download/$SWIFT_SDK_TAG/$SWIFT_SDK_TAG-wasm32-unknown-wasi.artifactbundle.zip"; + ) + ``` + +
+ +3. Install dependencies: + ```bash + make bootstrap + ``` + +### Running Tests +- Run unit tests: + ```bash + make unittest SWIFT_SDK_ID=wasm32-unknown-wasi + ``` +- Run integration tests: + ```bash + make test SWIFT_SDK_ID=wasm32-unknown-wasi + ``` + +## 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/README.md b/README.md index 63f432caa..4bc6d2d15 100644 --- a/README.md +++ b/README.md @@ -257,33 +257,6 @@ You can also build your project with webpack.js and a manually installed SwiftWa 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.6 -release](https://github.com/swiftwasm/swift/releases/tag/swift-wasm-5.6.0-RELEASE) appropriate for your platform: - -```sh -$ swiftenv install "https://github.com/swiftwasm/swift/releases/download/swift-wasm-5.6.0-RELEASE/swift-wasm-5.6.0-RELEASE-macos_$(uname -m).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.6 (swiftlang-5.6.0) -Target: arm64-apple-darwin20.6.0 -``` - ## Sponsoring [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. From e9158abfcc636e36b8d854ded94396efeba7437b Mon Sep 17 00:00:00 2001 From: Yuta Saito Date: Thu, 28 Nov 2024 18:28:56 +0900 Subject: [PATCH 186/373] Stop use of global variable as a object cache Instead, use `LazyThreadLocal` --- Sources/JavaScriptKit/BasicObjects/JSArray.swift | 4 ++-- Sources/JavaScriptKit/ConvertibleToJSValue.swift | 8 +++++--- Sources/JavaScriptKit/FundamentalObjects/JSSymbol.swift | 3 ++- 3 files changed, 9 insertions(+), 6 deletions(-) diff --git a/Sources/JavaScriptKit/BasicObjects/JSArray.swift b/Sources/JavaScriptKit/BasicObjects/JSArray.swift index a431eb9a5..95d14c637 100644 --- a/Sources/JavaScriptKit/BasicObjects/JSArray.swift +++ b/Sources/JavaScriptKit/BasicObjects/JSArray.swift @@ -93,9 +93,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/ConvertibleToJSValue.swift b/Sources/JavaScriptKit/ConvertibleToJSValue.swift index 660d72f16..a7f7da8b6 100644 --- a/Sources/JavaScriptKit/ConvertibleToJSValue.swift +++ b/Sources/JavaScriptKit/ConvertibleToJSValue.swift @@ -85,8 +85,10 @@ 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 } #if !hasFeature(Embedded) extension Dictionary where Value == ConvertibleToJSValue, Key == String { @@ -296,4 +298,4 @@ extension Array where Element == ConvertibleToJSValue { return _withRawJSValues(self, 0, &_results, body) } } -#endif \ No newline at end of file +#endif diff --git a/Sources/JavaScriptKit/FundamentalObjects/JSSymbol.swift b/Sources/JavaScriptKit/FundamentalObjects/JSSymbol.swift index 567976c70..42f63e010 100644 --- a/Sources/JavaScriptKit/FundamentalObjects/JSSymbol.swift +++ b/Sources/JavaScriptKit/FundamentalObjects/JSSymbol.swift @@ -1,6 +1,7 @@ import _CJavaScriptKit -private let Symbol = JSObject.global.Symbol.function! +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) From a7a57d059bf0e5cac584e4a49b7cb4fd785bb30e Mon Sep 17 00:00:00 2001 From: Yuta Saito Date: Thu, 28 Nov 2024 18:35:56 +0900 Subject: [PATCH 187/373] Add test case for `JSValueDecoder` on worker thread --- .../WebWorkerTaskExecutorTests.swift | 39 +++++++++++++++++++ 1 file changed, 39 insertions(+) diff --git a/Tests/JavaScriptEventLoopTests/WebWorkerTaskExecutorTests.swift b/Tests/JavaScriptEventLoopTests/WebWorkerTaskExecutorTests.swift index 645c6e388..2aab292fa 100644 --- a/Tests/JavaScriptEventLoopTests/WebWorkerTaskExecutorTests.swift +++ b/Tests/JavaScriptEventLoopTests/WebWorkerTaskExecutorTests.swift @@ -200,6 +200,45 @@ final class WebWorkerTaskExecutorTests: XCTestCase { XCTAssertEqual(Check.countOfInitialization, 2) } + 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 executor = try await WebWorkerTaskExecutor(numberOfThreads: 1) + let task = Task(executorPreference: executor) { + let json = """ + { + "prop_1": { + "nested_prop": 42 + }, + "prop_2": 100, + "prop_3": true, + "prop_7": 3.14, + "prop_8": "Hello, World!" + } + """ + let object = JSObject.global.JSON.parse(json) + let decoder = JSValueDecoder() + let decoded = try decoder.decode(DecodeMe.self, from: object) + return decoded + } + let result = try await task.value + 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!") + } + /* func testDeinitJSObjectOnDifferentThread() async throws { let executor = try await WebWorkerTaskExecutor(numberOfThreads: 1) From 288adb0b39b9f80d3199f49212157a4c26a9fde1 Mon Sep 17 00:00:00 2001 From: Yuta Saito Date: Thu, 28 Nov 2024 19:15:10 +0900 Subject: [PATCH 188/373] Test: Cover `JSArray.count` on worker thread --- .../WebWorkerTaskExecutor.swift | 13 ++++- .../WebWorkerTaskExecutorTests.swift | 53 ++++++++++++++----- 2 files changed, 51 insertions(+), 15 deletions(-) diff --git a/Sources/JavaScriptEventLoop/WebWorkerTaskExecutor.swift b/Sources/JavaScriptEventLoop/WebWorkerTaskExecutor.swift index a70312e3f..ef9f539f0 100644 --- a/Sources/JavaScriptEventLoop/WebWorkerTaskExecutor.swift +++ b/Sources/JavaScriptEventLoop/WebWorkerTaskExecutor.swift @@ -200,6 +200,7 @@ public final class WebWorkerTaskExecutor: TaskExecutor { 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. @@ -212,7 +213,14 @@ public final class WebWorkerTaskExecutor: TaskExecutor { guard let executor = parentTaskExecutor else { preconditionFailure("The worker must be started with a parent executor.") } - assert(state.load(ordering: .sequentiallyConsistent) == .running, "Invalid state: not running") + 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 @@ -247,7 +255,7 @@ public final class WebWorkerTaskExecutor: TaskExecutor { /// Terminate the worker. func terminate() { - trace("Worker.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 { @@ -283,6 +291,7 @@ public final class WebWorkerTaskExecutor: TaskExecutor { self.worker = worker } } + trace("Executor.start") // Start worker threads via pthread_create. for worker in workers { // NOTE: The context must be allocated on the heap because diff --git a/Tests/JavaScriptEventLoopTests/WebWorkerTaskExecutorTests.swift b/Tests/JavaScriptEventLoopTests/WebWorkerTaskExecutorTests.swift index 2aab292fa..726f4da75 100644 --- a/Tests/JavaScriptEventLoopTests/WebWorkerTaskExecutorTests.swift +++ b/Tests/JavaScriptEventLoopTests/WebWorkerTaskExecutorTests.swift @@ -38,6 +38,7 @@ final class WebWorkerTaskExecutorTests: XCTestCase { func testAwaitInsideTask() async throws { let executor = try await WebWorkerTaskExecutor(numberOfThreads: 1) + defer { executor.terminate() } let task = Task(executorPreference: executor) { await Task.yield() @@ -46,8 +47,6 @@ final class WebWorkerTaskExecutorTests: XCTestCase { } let taskRunOnMainThread = try await task.value XCTAssertFalse(taskRunOnMainThread) - - executor.terminate() } func testSleepInsideTask() async throws { @@ -170,6 +169,7 @@ final class WebWorkerTaskExecutorTests: XCTestCase { let result = await task.value XCTAssertEqual(result, 100) XCTAssertEqual(Check.value, 42) + executor.terminate() } func testLazyThreadLocalPerThreadInitialization() async throws { @@ -198,6 +198,7 @@ final class WebWorkerTaskExecutorTests: XCTestCase { let result = await task.value XCTAssertEqual(result, 100) XCTAssertEqual(Check.countOfInitialization, 2) + executor.terminate() } func testJSValueDecoderOnWorker() async throws { @@ -211,10 +212,10 @@ final class WebWorkerTaskExecutorTests: XCTestCase { let prop_3: Bool let prop_7: Float let prop_8: String + let prop_9: [String] } - let executor = try await WebWorkerTaskExecutor(numberOfThreads: 1) - let task = Task(executorPreference: executor) { + func decodeJob() throws { let json = """ { "prop_1": { @@ -223,20 +224,46 @@ final class WebWorkerTaskExecutorTests: XCTestCase { "prop_2": 100, "prop_3": true, "prop_7": 3.14, - "prop_8": "Hello, World!" + "prop_8": "Hello, World!", + "prop_9": ["a", "b", "c"] } """ let object = JSObject.global.JSON.parse(json) let decoder = JSValueDecoder() - let decoded = try decoder.decode(DecodeMe.self, from: object) - return decoded + 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) } - let result = try await task.value - 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!") + check() + let task = Task(executorPreference: executor) { + check() + } + await task.value + executor.terminate() } /* From c574eedeceb52acab75929bad8bb0f3cab09adb0 Mon Sep 17 00:00:00 2001 From: Yuta Saito Date: Mon, 2 Dec 2024 19:01:39 +0900 Subject: [PATCH 189/373] Expose `WebWorkerTaskExecutor` when compiling with toolchain < 6.1 --- .../WebWorkerTaskExecutor.swift | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/Sources/JavaScriptEventLoop/WebWorkerTaskExecutor.swift b/Sources/JavaScriptEventLoop/WebWorkerTaskExecutor.swift index ef9f539f0..5110f60db 100644 --- a/Sources/JavaScriptEventLoop/WebWorkerTaskExecutor.swift +++ b/Sources/JavaScriptEventLoop/WebWorkerTaskExecutor.swift @@ -1,10 +1,12 @@ -#if compiler(>=6.1) && _runtime(_multithreaded) // @_expose and @_extern are only available in Swift 6.1+ +#if compiler(>=6.0) // `TaskExecutor` is available since Swift 6.0 import JavaScriptKit import _CJavaScriptKit import _CJavaScriptEventLoop -import Synchronization +#if canImport(Synchronization) + import Synchronization +#endif #if canImport(wasi_pthread) import wasi_pthread import WASILibc @@ -282,7 +284,7 @@ public final class WebWorkerTaskExecutor: TaskExecutor { } func start(timeout: Duration, checkInterval: Duration) async throws { - #if canImport(wasi_pthread) + #if canImport(wasi_pthread) && compiler(>=6.1) && _runtime(_multithreaded) class Context: @unchecked Sendable { let executor: WebWorkerTaskExecutor.Executor let worker: Worker @@ -433,7 +435,7 @@ public final class WebWorkerTaskExecutor: TaskExecutor { /// /// This function must be called once before using the Web Worker task executor. public static func installGlobalExecutor() { - #if canImport(wasi_pthread) + #if canImport(wasi_pthread) && compiler(>=6.1) && _runtime(_multithreaded) // Ensure this function is called only once. guard _mainThread == nil else { return } @@ -471,7 +473,9 @@ public final class WebWorkerTaskExecutor: TaskExecutor { /// 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)) @@ -480,15 +484,17 @@ func _swjs_enqueue_main_job_from_worker(_ job: UnownedJob) { /// Wake up the worker thread. /// This function is called when a job is enqueued from the main thread to a worker thread. @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) +#if compiler(>=6.1) // @_expose and @_extern are only available in Swift 6.1+ @_expose(wasm, "swjs_wake_worker_thread") +#endif func _swjs_wake_worker_thread() { WebWorkerTaskExecutor.Worker.currentThread!.run() } -#endif - fileprivate 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) From e98362fac3f8b0842a2b3cb302542eb2b337554e Mon Sep 17 00:00:00 2001 From: Yuta Saito Date: Sun, 8 Dec 2024 09:46:13 +0000 Subject: [PATCH 190/373] [skip ci] Add `make regenerate_swiftpm_resources` to the contributing guide --- CONTRIBUTING.md | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index c286c33fb..f656032bf 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -65,5 +65,15 @@ Thank you for considering contributing to JavaScriptKit! We welcome contribution make test SWIFT_SDK_ID=wasm32-unknown-wasi ``` +### 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). From b7f361c2ed791996c1d5c385c46b02c11bb4156f Mon Sep 17 00:00:00 2001 From: Yuta Saito Date: Sun, 8 Dec 2024 09:46:48 +0000 Subject: [PATCH 191/373] [skip ci] Remove .swift-version file We support multiple Swift versions now, so we don't need to pin the version in the `.swift-version` file. --- .swift-version | 1 - 1 file changed, 1 deletion(-) delete mode 100644 .swift-version diff --git a/.swift-version b/.swift-version deleted file mode 100644 index 08ddfb781..000000000 --- a/.swift-version +++ /dev/null @@ -1 +0,0 @@ -wasm-5.6.0-RELEASE From 8bf446b5ce1071ec5d82e13d6f15c697c4c9a6f9 Mon Sep 17 00:00:00 2001 From: Yuta Saito Date: Sun, 8 Dec 2024 09:47:34 +0000 Subject: [PATCH 192/373] Fix empty TypedArray creation --- Runtime/src/index.ts | 9 +++++++++ Sources/JavaScriptKit/Runtime/index.js | 9 +++++++++ Sources/JavaScriptKit/Runtime/index.mjs | 9 +++++++++ .../JavaScriptKitTests/JSTypedArrayTests.swift | 18 ++++++++++++++++++ 4 files changed, 45 insertions(+) create mode 100644 Tests/JavaScriptKitTests/JSTypedArrayTests.swift diff --git a/Runtime/src/index.ts b/Runtime/src/index.ts index d6d82c04a..73f56411a 100644 --- a/Runtime/src/index.ts +++ b/Runtime/src/index.ts @@ -536,6 +536,15 @@ export class SwiftRuntime { ) => { 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, diff --git a/Sources/JavaScriptKit/Runtime/index.js b/Sources/JavaScriptKit/Runtime/index.js index 9d29b4329..223fed3e1 100644 --- a/Sources/JavaScriptKit/Runtime/index.js +++ b/Sources/JavaScriptKit/Runtime/index.js @@ -451,6 +451,15 @@ }, 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()); diff --git a/Sources/JavaScriptKit/Runtime/index.mjs b/Sources/JavaScriptKit/Runtime/index.mjs index 9201b7712..34e4dd13f 100644 --- a/Sources/JavaScriptKit/Runtime/index.mjs +++ b/Sources/JavaScriptKit/Runtime/index.mjs @@ -445,6 +445,15 @@ class SwiftRuntime { }, 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()); diff --git a/Tests/JavaScriptKitTests/JSTypedArrayTests.swift b/Tests/JavaScriptKitTests/JSTypedArrayTests.swift new file mode 100644 index 000000000..87b81ae16 --- /dev/null +++ b/Tests/JavaScriptKitTests/JSTypedArrayTests.swift @@ -0,0 +1,18 @@ +import XCTest +import JavaScriptKit + +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]()) + } +} From f5512253298e0845ac4918c858284e5b0f53ee8d Mon Sep 17 00:00:00 2001 From: Yuta Saito Date: Sun, 8 Dec 2024 10:08:31 +0000 Subject: [PATCH 193/373] Drop 5.9.1 and 5.8.0 toolchains from CI --- .github/workflows/test.yml | 4 ---- 1 file changed, 4 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 8ed33aa5d..e2802fb6d 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -10,16 +10,12 @@ jobs: matrix: entry: # Ensure that all host can install toolchain, build project, and run tests - - { os: macos-12, toolchain: wasm-5.9.1-RELEASE, wasi-backend: Node, xcode: Xcode_13.4.1.app } - - { os: macos-13, toolchain: wasm-5.9.1-RELEASE, wasi-backend: Node, xcode: Xcode_14.3.app } - { os: macos-14, toolchain: wasm-5.9.1-RELEASE, wasi-backend: Node, xcode: Xcode_15.2.app } - { os: ubuntu-22.04, toolchain: wasm-5.9.1-RELEASE, wasi-backend: Node } - { os: ubuntu-22.04, toolchain: wasm-5.10.0-RELEASE, wasi-backend: Node } # Ensure that test succeeds with all toolchains and wasi backend combinations - - { os: ubuntu-20.04, toolchain: wasm-5.8.0-RELEASE, wasi-backend: Node } - { os: ubuntu-20.04, toolchain: wasm-5.10.0-RELEASE, wasi-backend: Node } - - { os: ubuntu-20.04, toolchain: wasm-5.8.0-RELEASE, wasi-backend: MicroWASI } - { os: ubuntu-20.04, toolchain: wasm-5.9.1-RELEASE, wasi-backend: MicroWASI } - { os: ubuntu-20.04, toolchain: wasm-5.10.0-RELEASE, wasi-backend: MicroWASI } - os: ubuntu-22.04 From 985dccf6413cf6884f1ef3d1dbce1db5d062e68c Mon Sep 17 00:00:00 2001 From: Yuta Saito Date: Thu, 26 Dec 2024 23:47:48 +0900 Subject: [PATCH 194/373] Re-order the targets in Package.swift --- Package.swift | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/Package.swift b/Package.swift index 37c2d1f3c..f21a95cb5 100644 --- a/Package.swift +++ b/Package.swift @@ -29,15 +29,22 @@ let package = Package( ] : nil ), .target(name: "_CJavaScriptKit"), + .testTarget( + name: "JavaScriptKitTests", + dependencies: ["JavaScriptKit"] + ), + .target( name: "JavaScriptBigIntSupport", dependencies: ["_CJavaScriptBigIntSupport", "JavaScriptKit"] ), .target(name: "_CJavaScriptBigIntSupport", dependencies: ["_CJavaScriptKit"]), + .target( name: "JavaScriptEventLoop", dependencies: ["JavaScriptKit", "_CJavaScriptEventLoop"] ), + .target(name: "_CJavaScriptEventLoop"), .testTarget( name: "JavaScriptEventLoopTests", dependencies: [ @@ -49,7 +56,6 @@ let package = Package( .enableExperimentalFeature("Extern") ] ), - .target(name: "_CJavaScriptEventLoop"), .target( name: "JavaScriptEventLoopTestSupport", dependencies: [ @@ -58,11 +64,6 @@ let package = Package( ] ), .target(name: "_CJavaScriptEventLoopTestSupport"), - - .testTarget( - name: "JavaScriptKitTests", - dependencies: ["JavaScriptKit"] - ), .testTarget( name: "JavaScriptEventLoopTestSupportTests", dependencies: [ From 69e598b11427a4896c6743b37c60add32b627dea Mon Sep 17 00:00:00 2001 From: Ole Begemann Date: Mon, 13 Jan 2025 20:23:03 +0100 Subject: [PATCH 195/373] Add WASI SDK checksum to CONTRIBUTING.md --- CONTRIBUTING.md | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index f656032bf..2526556c6 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -27,11 +27,12 @@ Thank you for considering contributing to JavaScriptKit! We welcome contribution 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"; + 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"; ) ``` @@ -44,7 +45,8 @@ Thank you for considering contributing to JavaScriptKit! We welcome contribution ```bash ( SWIFT_SDK_TAG="swift-wasm-6.0.2-RELEASE"; - swift sdk install "https://github.com/swiftwasm/swift/releases/download/$SWIFT_SDK_TAG/$SWIFT_SDK_TAG-wasm32-unknown-wasi.artifactbundle.zip"; + 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"; ) ``` From 4afe04580a51bbe35e5e728868fc7705c0c2cc2f Mon Sep 17 00:00:00 2001 From: Ole Begemann Date: Mon, 13 Jan 2025 21:02:33 +0100 Subject: [PATCH 196/373] Fix inconsistent argument label This is the only of many overloads that does *not* remove the external argument label for one of its parameters. Looks like a copy/paste error. --- Sources/JavaScriptKit/FundamentalObjects/JSFunction.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/JavaScriptKit/FundamentalObjects/JSFunction.swift b/Sources/JavaScriptKit/FundamentalObjects/JSFunction.swift index cbbf4a60f..1463a75c6 100644 --- a/Sources/JavaScriptKit/FundamentalObjects/JSFunction.swift +++ b/Sources/JavaScriptKit/FundamentalObjects/JSFunction.swift @@ -228,7 +228,7 @@ public extension JSFunction { new(arguments: [arg0.jsValue, arg1.jsValue, arg2.jsValue]) } - func new(_ arg0: some ConvertibleToJSValue, _ arg1: some ConvertibleToJSValue, _ arg2: some ConvertibleToJSValue, arg3: some ConvertibleToJSValue) -> JSObject { + 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]) } From dccced130149d64da8f696261972183a8a54227b Mon Sep 17 00:00:00 2001 From: Ole Begemann Date: Wed, 15 Jan 2025 19:51:43 +0100 Subject: [PATCH 197/373] Allow calling JS functions with up to 7 arguments in Embedded Swift --- .../FundamentalObjects/JSFunction.swift | 178 +++++++++++++++++- .../FundamentalObjects/JSObject.swift | 79 +++++++- Sources/JavaScriptKit/JSValue.swift | 72 ++++++- 3 files changed, 315 insertions(+), 14 deletions(-) diff --git a/Sources/JavaScriptKit/FundamentalObjects/JSFunction.swift b/Sources/JavaScriptKit/FundamentalObjects/JSFunction.swift index 1463a75c6..498bbc3ea 100644 --- a/Sources/JavaScriptKit/FundamentalObjects/JSFunction.swift +++ b/Sources/JavaScriptKit/FundamentalObjects/JSFunction.swift @@ -164,7 +164,14 @@ public class JSFunction: JSObject { } #if hasFeature(Embedded) -// NOTE: once embedded supports variadic generics, we can remove these overloads +// 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. public extension JSFunction { @discardableResult @@ -178,15 +185,74 @@ public extension JSFunction { } @discardableResult - func callAsFunction(this: JSObject, _ arg0: some ConvertibleToJSValue, _ arg1: some ConvertibleToJSValue) -> JSValue { + func callAsFunction( + this: JSObject, + _ arg0: some ConvertibleToJSValue, + _ arg1: some ConvertibleToJSValue + ) -> JSValue { invokeNonThrowingJSFunction(arguments: [arg0.jsValue, arg1.jsValue], this: this).jsValue } @discardableResult - func callAsFunction(this: JSObject, _ arg0: some ConvertibleToJSValue, _ arg1: some ConvertibleToJSValue, _ arg2: some ConvertibleToJSValue) -> JSValue { + 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 + 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 + 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 + 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 + 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 func callAsFunction(this: JSObject, arguments: [JSValue]) -> JSValue { invokeNonThrowingJSFunction(arguments: arguments, this: this).jsValue @@ -203,15 +269,68 @@ public extension JSFunction { } @discardableResult - func callAsFunction(_ arg0: some ConvertibleToJSValue, _ arg1: some ConvertibleToJSValue) -> JSValue { + func callAsFunction( + _ arg0: some ConvertibleToJSValue, + _ arg1: some ConvertibleToJSValue + ) -> JSValue { invokeNonThrowingJSFunction(arguments: [arg0.jsValue, arg1.jsValue]).jsValue } @discardableResult - func callAsFunction(_ arg0: some ConvertibleToJSValue, _ arg1: some ConvertibleToJSValue, _ arg2: some ConvertibleToJSValue) -> JSValue { + func callAsFunction( + _ arg0: some ConvertibleToJSValue, + _ arg1: some ConvertibleToJSValue, + _ arg2: some ConvertibleToJSValue + ) -> JSValue { invokeNonThrowingJSFunction(arguments: [arg0.jsValue, arg1.jsValue, arg2.jsValue]).jsValue } + @discardableResult + 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 + 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 + 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 + 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 + } + func new() -> JSObject { new(arguments: []) } @@ -220,19 +339,60 @@ public extension JSFunction { new(arguments: [arg0.jsValue]) } - func new(_ arg0: some ConvertibleToJSValue, _ arg1: some ConvertibleToJSValue) -> JSObject { + func new( + _ arg0: some ConvertibleToJSValue, + _ arg1: some ConvertibleToJSValue + ) -> JSObject { new(arguments: [arg0.jsValue, arg1.jsValue]) } - func new(_ arg0: some ConvertibleToJSValue, _ arg1: some ConvertibleToJSValue, _ arg2: some ConvertibleToJSValue) -> JSObject { + func new( + _ arg0: some ConvertibleToJSValue, + _ arg1: some ConvertibleToJSValue, + _ arg2: some ConvertibleToJSValue + ) -> JSObject { new(arguments: [arg0.jsValue, arg1.jsValue, arg2.jsValue]) } - func new(_ arg0: some ConvertibleToJSValue, _ arg1: some ConvertibleToJSValue, _ arg2: some ConvertibleToJSValue, _ arg3: some ConvertibleToJSValue) -> JSObject { + 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]) } - func new(_ arg0: some ConvertibleToJSValue, _ arg1: some ConvertibleToJSValue, _ arg2: some ConvertibleToJSValue, _ arg3: some ConvertibleToJSValue, _ arg4: some ConvertibleToJSValue, _ arg5: some ConvertibleToJSValue, _ arg6: some ConvertibleToJSValue) -> JSObject { + 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]) + } + + 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]) + } + + 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]) } } diff --git a/Sources/JavaScriptKit/FundamentalObjects/JSObject.swift b/Sources/JavaScriptKit/FundamentalObjects/JSObject.swift index 143cbdb39..eb8fb643a 100644 --- a/Sources/JavaScriptKit/FundamentalObjects/JSObject.swift +++ b/Sources/JavaScriptKit/FundamentalObjects/JSObject.swift @@ -298,7 +298,14 @@ public class JSThrowingObject { #if hasFeature(Embedded) -// NOTE: once embedded supports variadic generics, we can remove these overloads +// 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. public extension JSObject { @_disfavoredOverload subscript(dynamicMember name: String) -> (() -> JSValue)? { @@ -315,10 +322,78 @@ public extension JSObject { } @_disfavoredOverload - subscript(dynamicMember name: String) -> ((A0, A1) -> JSValue)? { + subscript< + A0: ConvertibleToJSValue, + A1: ConvertibleToJSValue + >(dynamicMember name: String) -> ((A0, A1) -> JSValue)? { self[name].function.map { function in { function(this: self, $0, $1) } } } + + @_disfavoredOverload + 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 + 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 + 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 + 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 + 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/JSValue.swift b/Sources/JavaScriptKit/JSValue.swift index fe1400e24..ed44f50ea 100644 --- a/Sources/JavaScriptKit/JSValue.swift +++ b/Sources/JavaScriptKit/JSValue.swift @@ -272,9 +272,17 @@ extension JSValue: CustomStringConvertible { } #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. public extension JSValue { @_disfavoredOverload - subscript(dynamicMember name: String) -> (() -> JSValue) { + subscript(dynamicMember name: String) -> (() -> JSValue) { object![dynamicMember: name]! } @@ -284,8 +292,66 @@ public extension JSValue { } @_disfavoredOverload - subscript(dynamicMember name: String) -> ((A0, A1) -> JSValue) { + subscript< + A0: ConvertibleToJSValue, + A1: ConvertibleToJSValue + >(dynamicMember name: String) -> ((A0, A1) -> JSValue) { + object![dynamicMember: name]! + } + + @_disfavoredOverload + subscript< + A0: ConvertibleToJSValue, + A1: ConvertibleToJSValue, + A2: ConvertibleToJSValue + >(dynamicMember name: String) -> ((A0, A1, A2) -> JSValue) { + object![dynamicMember: name]! + } + + @_disfavoredOverload + subscript< + A0: ConvertibleToJSValue, + A1: ConvertibleToJSValue, + A2: ConvertibleToJSValue, + A3: ConvertibleToJSValue + >(dynamicMember name: String) -> ((A0, A1, A2, A3) -> JSValue) { + object![dynamicMember: name]! + } + + @_disfavoredOverload + subscript< + A0: ConvertibleToJSValue, + A1: ConvertibleToJSValue, + A2: ConvertibleToJSValue, + A3: ConvertibleToJSValue, + A4: ConvertibleToJSValue + >(dynamicMember name: String) -> ((A0, A1, A2, A3, A4) -> JSValue) { + object![dynamicMember: name]! + } + + @_disfavoredOverload + 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 + 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 \ No newline at end of file +#endif From 4263c1ce61ccf3f6c33ebe2954bb22b98663111a Mon Sep 17 00:00:00 2001 From: "Volodymyr B." Date: Sun, 23 Feb 2025 21:10:15 +0000 Subject: [PATCH 198/373] update readme --- README.md | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 4bc6d2d15..5c5b76370 100644 --- a/README.md +++ b/README.md @@ -178,12 +178,14 @@ Not all of these versions are tested on regular basis though, compatibility repo ## Usage in a browser application -The easiest way to get started with JavaScriptKit in your browser app is with [the `carton` +The easiest is to start with [Examples](/Examples) which has JavaScript glue runtime. + +Second option is to get started with JavaScriptKit in your browser app is with [the `carton` bundler](https://carton.dev). Add carton to your swift package dependencies: ```diff dependencies: [ -+ .package(url: "https://github.com/swiftwasm/carton", from: "1.0.0"), ++ .package(url: "https://github.com/swiftwasm/carton", from: "1.1.3"), ], ``` @@ -253,10 +255,6 @@ within it. You'll see `Hello, world!` output in the console. You can edit the ap 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. - ## Sponsoring [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. From 080933347280bb271ef689075f9554e8f25a53a9 Mon Sep 17 00:00:00 2001 From: "Volodymyr B." Date: Sun, 23 Feb 2025 21:11:10 +0000 Subject: [PATCH 199/373] update examples --- Examples/Basic/Package.swift | 2 +- Examples/Embedded/Package.swift | 5 +++-- Examples/Embedded/README.md | 2 +- 3 files changed, 5 insertions(+), 4 deletions(-) diff --git a/Examples/Basic/Package.swift b/Examples/Basic/Package.swift index aade23359..cc2ea0a0f 100644 --- a/Examples/Basic/Package.swift +++ b/Examples/Basic/Package.swift @@ -1,4 +1,4 @@ -// swift-tools-version:5.10 +// swift-tools-version:6.0 import PackageDescription diff --git a/Examples/Embedded/Package.swift b/Examples/Embedded/Package.swift index 227a049ff..f97638cc8 100644 --- a/Examples/Embedded/Package.swift +++ b/Examples/Embedded/Package.swift @@ -1,4 +1,4 @@ -// swift-tools-version:5.10 +// swift-tools-version:6.0 import PackageDescription @@ -32,5 +32,6 @@ let package = Package( ]) ] ) - ] + ], + swiftLanguageModes: [.v5] ) diff --git a/Examples/Embedded/README.md b/Examples/Embedded/README.md index 2f388fcdc..e99d659ff 100644 --- a/Examples/Embedded/README.md +++ b/Examples/Embedded/README.md @@ -1,6 +1,6 @@ # Embedded example -Requires a recent DEVELOPMENT-SNAPSHOT toolchain. (tested with swift-DEVELOPMENT-SNAPSHOT-2024-09-25-a) +Requires a recent DEVELOPMENT-SNAPSHOT toolchain. (tested with swift-6.1-DEVELOPMENT-SNAPSHOT-2025-02-21-a) ```sh $ ./build.sh From f9d3ff8a5f025133cb7e8ddafeaad405b201e01e Mon Sep 17 00:00:00 2001 From: Yuta Saito Date: Mon, 24 Feb 2025 00:07:33 +0000 Subject: [PATCH 200/373] Update compatibility CI --- .github/workflows/compatibility.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/compatibility.yml b/.github/workflows/compatibility.yml index e16785157..65e60ea4a 100644 --- a/.github/workflows/compatibility.yml +++ b/.github/workflows/compatibility.yml @@ -12,7 +12,7 @@ jobs: uses: actions/checkout@v4 - uses: swiftwasm/setup-swiftwasm@v1 with: - swift-version: wasm-5.10.0-RELEASE + swift-version: wasm-6.0.3-RELEASE - name: Run Test run: | set -eux From b4758bbf20d2a2dab2b6361f643f693b894dde46 Mon Sep 17 00:00:00 2001 From: Yuta Saito Date: Mon, 24 Feb 2025 00:11:42 +0000 Subject: [PATCH 201/373] Use --static-swift-stdlib to use Foundation --- .github/workflows/compatibility.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/compatibility.yml b/.github/workflows/compatibility.yml index 65e60ea4a..04e2aa0d3 100644 --- a/.github/workflows/compatibility.yml +++ b/.github/workflows/compatibility.yml @@ -18,5 +18,5 @@ jobs: set -eux make bootstrap cd Examples/Basic - swift build --triple wasm32-unknown-wasi - swift build --triple wasm32-unknown-wasi -Xswiftc -DJAVASCRIPTKIT_WITHOUT_WEAKREFS + swift build --triple wasm32-unknown-wasi --static-swift-stdlib + swift build --triple wasm32-unknown-wasi -Xswiftc -DJAVASCRIPTKIT_WITHOUT_WEAKREFS --static-swift-stdlib From 1738361da3252ec583c6111d40ffb7f8c48d0972 Mon Sep 17 00:00:00 2001 From: Yuta Saito Date: Mon, 24 Feb 2025 00:21:15 +0000 Subject: [PATCH 202/373] Skip Swift 6 concurrency restrictions for now --- Examples/Basic/Package.swift | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Examples/Basic/Package.swift b/Examples/Basic/Package.swift index cc2ea0a0f..ea70e6b20 100644 --- a/Examples/Basic/Package.swift +++ b/Examples/Basic/Package.swift @@ -16,5 +16,6 @@ let package = Package( .product(name: "JavaScriptEventLoop", package: "JavaScriptKit") ] ) - ] + ], + swiftLanguageVersions: [.v5] ) From 39ae1865653d05ace9d3685811141f812c433b52 Mon Sep 17 00:00:00 2001 From: Yuta Saito Date: Fri, 28 Feb 2025 09:01:15 +0000 Subject: [PATCH 203/373] CI: Use Swift SDK by default In other words, drop toolchain-style installation support --- .github/workflows/compatibility.yml | 12 ++++----- .github/workflows/perf.yml | 10 +++++--- .github/workflows/test.yml | 38 +++-------------------------- Examples/Basic/build.sh | 3 ++- Makefile | 5 +--- 5 files changed, 18 insertions(+), 50 deletions(-) diff --git a/.github/workflows/compatibility.yml b/.github/workflows/compatibility.yml index 04e2aa0d3..8994b624b 100644 --- a/.github/workflows/compatibility.yml +++ b/.github/workflows/compatibility.yml @@ -6,17 +6,15 @@ on: jobs: test: name: Check source code compatibility - runs-on: ubuntu-20.04 + runs-on: ubuntu-latest + container: swift:6.0.3 steps: - name: Checkout uses: actions/checkout@v4 - - uses: swiftwasm/setup-swiftwasm@v1 - with: - swift-version: wasm-6.0.3-RELEASE + - uses: swiftwasm/setup-swiftwasm@v2 - name: Run Test run: | set -eux - make bootstrap cd Examples/Basic - swift build --triple wasm32-unknown-wasi --static-swift-stdlib - swift build --triple wasm32-unknown-wasi -Xswiftc -DJAVASCRIPTKIT_WITHOUT_WEAKREFS --static-swift-stdlib + swift build --swift-sdk wasm32-unknown-wasi --static-swift-stdlib + swift build --swift-sdk wasm32-unknown-wasi -Xswiftc -DJAVASCRIPTKIT_WITHOUT_WEAKREFS --static-swift-stdlib diff --git a/.github/workflows/perf.yml b/.github/workflows/perf.yml index f2ffdcc5e..eb9178429 100644 --- a/.github/workflows/perf.yml +++ b/.github/workflows/perf.yml @@ -4,13 +4,15 @@ on: [pull_request] jobs: perf: - runs-on: ubuntu-20.04 + runs-on: ubuntu-latest + container: swift:6.0.3 steps: - name: Checkout uses: actions/checkout@v4 - - uses: swiftwasm/setup-swiftwasm@v1 - with: - swift-version: wasm-5.9.1-RELEASE + - uses: swiftwasm/setup-swiftwasm@v2 + - name: Install dependencies + run: | + apt-get update && apt-get install make nodejs npm -y - name: Run Benchmark run: | make bootstrap diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index e2802fb6d..daac3c50f 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -9,38 +9,17 @@ jobs: strategy: matrix: entry: - # Ensure that all host can install toolchain, build project, and run tests - - { os: macos-14, toolchain: wasm-5.9.1-RELEASE, wasi-backend: Node, xcode: Xcode_15.2.app } - - { os: ubuntu-22.04, toolchain: wasm-5.9.1-RELEASE, wasi-backend: Node } - - { os: ubuntu-22.04, toolchain: wasm-5.10.0-RELEASE, wasi-backend: Node } - - # Ensure that test succeeds with all toolchains and wasi backend combinations - - { os: ubuntu-20.04, toolchain: wasm-5.10.0-RELEASE, wasi-backend: Node } - - { os: ubuntu-20.04, toolchain: wasm-5.9.1-RELEASE, wasi-backend: MicroWASI } - - { os: ubuntu-20.04, toolchain: wasm-5.10.0-RELEASE, wasi-backend: MicroWASI } - 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 - swift-sdk: - id: 6.0.2-RELEASE-wasm32-unknown-wasi - download-url: "https://github.com/swiftwasm/swift/releases/download/swift-wasm-6.0.2-RELEASE/swift-wasm-6.0.2-RELEASE-wasm32-unknown-wasi.artifactbundle.zip" - checksum: "6ffedb055cb9956395d9f435d03d53ebe9f6a8d45106b979d1b7f53358e1dcb4" wasi-backend: Node - os: ubuntu-22.04 toolchain: download-url: https://download.swift.org/development/ubuntu2204/swift-DEVELOPMENT-SNAPSHOT-2024-10-30-a/swift-DEVELOPMENT-SNAPSHOT-2024-10-30-a-ubuntu22.04.tar.gz - swift-sdk: - id: DEVELOPMENT-SNAPSHOT-2024-10-31-a-wasm32-unknown-wasi - download-url: "https://github.com/swiftwasm/swift/releases/download/swift-wasm-DEVELOPMENT-SNAPSHOT-2024-10-31-a/swift-wasm-DEVELOPMENT-SNAPSHOT-2024-10-31-a-wasm32-unknown-wasi.artifactbundle.zip" - checksum: "e42546397786ea6eaec2d9c07f9118a6f3428784cf3df3840a369f19700c1a69" wasi-backend: Node - os: ubuntu-22.04 toolchain: download-url: https://download.swift.org/development/ubuntu2204/swift-DEVELOPMENT-SNAPSHOT-2024-10-30-a/swift-DEVELOPMENT-SNAPSHOT-2024-10-30-a-ubuntu22.04.tar.gz - swift-sdk: - id: DEVELOPMENT-SNAPSHOT-2024-10-31-a-wasm32-unknown-wasip1-threads - download-url: "https://github.com/swiftwasm/swift/releases/download/swift-wasm-DEVELOPMENT-SNAPSHOT-2024-10-31-a/swift-wasm-DEVELOPMENT-SNAPSHOT-2024-10-31-a-wasm32-unknown-wasip1-threads.artifactbundle.zip" - checksum: "17dbbe61af6ca09c92ee2d68a56d5716530428e28c4c8358aa860cc4fcdc91ae" wasi-backend: Node runs-on: ${{ matrix.entry.os }} @@ -49,22 +28,13 @@ jobs: steps: - name: Checkout uses: actions/checkout@v4 - - name: Select SDKROOT - if: ${{ matrix.entry.xcode }} - run: sudo xcode-select -s /Applications/${{ matrix.entry.xcode }} - - uses: swiftwasm/setup-swiftwasm@v1 - if: ${{ matrix.entry.swift-sdk == null }} - with: - swift-version: ${{ matrix.entry.toolchain }} - uses: ./.github/actions/install-swift - if: ${{ matrix.entry.swift-sdk }} with: download-url: ${{ matrix.entry.toolchain.download-url }} - - name: Install Swift SDK - if: ${{ matrix.entry.swift-sdk }} - run: | - swift sdk install "${{ matrix.entry.swift-sdk.download-url }}" --checksum "${{ matrix.entry.swift-sdk.checksum }}" - echo "SWIFT_SDK_ID=${{ matrix.entry.swift-sdk.id }}" >> $GITHUB_ENV + - uses: swiftwasm/setup-swiftwasm@v2 + id: setup-swiftwasm + - name: Configure Swift SDK + run: echo "SWIFT_SDK_ID=${{ steps.setup-swiftwasm.outputs.swift-sdk-id }}" >> $GITHUB_ENV - run: make bootstrap - run: make test - run: make unittest diff --git a/Examples/Basic/build.sh b/Examples/Basic/build.sh index 2e4c3735b..0e5761ecf 100755 --- a/Examples/Basic/build.sh +++ b/Examples/Basic/build.sh @@ -1 +1,2 @@ -swift build --swift-sdk DEVELOPMENT-SNAPSHOT-2024-09-20-a-wasm32-unknown-wasi -Xswiftc -Xclang-linker -Xswiftc -mexec-model=reactor -Xlinker --export=__main_argc_argv +#!/bin/bash +swift build --swift-sdk "${SWIFT_SDK_ID:-wasm32-unknown-wasi}" -Xswiftc -Xclang-linker -Xswiftc -mexec-model=reactor -Xlinker --export=__main_argc_argv diff --git a/Makefile b/Makefile index 7108f3189..1b653315c 100644 --- a/Makefile +++ b/Makefile @@ -1,10 +1,7 @@ MAKEFILE_DIR := $(dir $(lastword $(MAKEFILE_LIST))) -ifeq ($(SWIFT_SDK_ID),) -SWIFT_BUILD_FLAGS := --triple wasm32-unknown-wasi -else +SWIFT_SDK_ID ?= wasm32-unknown-wasi SWIFT_BUILD_FLAGS := --swift-sdk $(SWIFT_SDK_ID) -endif .PHONY: bootstrap bootstrap: From e6dd6d7fe3e959cb7960285e38ffb3de5c4104a7 Mon Sep 17 00:00:00 2001 From: Yuta Saito Date: Fri, 28 Feb 2025 09:16:26 +0000 Subject: [PATCH 204/373] Fix package-lock.json --- IntegrationTests/package-lock.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/IntegrationTests/package-lock.json b/IntegrationTests/package-lock.json index d0b914f04..9ea81b961 100644 --- a/IntegrationTests/package-lock.json +++ b/IntegrationTests/package-lock.json @@ -11,7 +11,7 @@ }, "..": { "name": "javascript-kit-swift", - "version": "0.19.2", + "version": "0.0.0", "license": "MIT", "devDependencies": { "@rollup/plugin-typescript": "^8.3.1", From 7af3f7fbf2fa5cb295adce74d78637d1b90b955d Mon Sep 17 00:00:00 2001 From: Yuta Saito Date: Fri, 28 Feb 2025 09:21:52 +0000 Subject: [PATCH 205/373] Stop using container image for perf --- .github/workflows/perf.yml | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/.github/workflows/perf.yml b/.github/workflows/perf.yml index eb9178429..501b16099 100644 --- a/.github/workflows/perf.yml +++ b/.github/workflows/perf.yml @@ -4,15 +4,14 @@ on: [pull_request] jobs: perf: - runs-on: ubuntu-latest - container: swift:6.0.3 + runs-on: ubuntu-24.04 steps: - name: Checkout uses: actions/checkout@v4 + - uses: ./.github/actions/install-swift + with: + download-url: https://download.swift.org/swift-6.0.3-release/ubuntu2404/swift-6.0.3-RELEASE/swift-6.0.3-RELEASE-ubuntu24.04.tar.gz - uses: swiftwasm/setup-swiftwasm@v2 - - name: Install dependencies - run: | - apt-get update && apt-get install make nodejs npm -y - name: Run Benchmark run: | make bootstrap From 28f34719df62d30655a9f81f6081aa8db9ce3d38 Mon Sep 17 00:00:00 2001 From: Yuta Saito Date: Tue, 4 Mar 2025 01:39:40 +0000 Subject: [PATCH 206/373] Concurrency: Use `LazyThreadLocal` without @PW syntax Unfortunately, `@LazyThreadLocal static var` is considered as concurrency-unsafe in Swift 6 mode even though the underlying PW storage is read-only and concurrency-safe. Also Swift bans `static let` with `@propertyWrapper` syntax, so we need to use `LazyThreadLocal` directly. See the discussion in the Swift forum: https://forums.swift.org/t/static-property-wrappers-and-strict-concurrency-in-5-10/70116/27 --- Sources/JavaScriptKit/BasicObjects/JSArray.swift | 5 ++--- Sources/JavaScriptKit/BasicObjects/JSDate.swift | 5 ++--- Sources/JavaScriptKit/BasicObjects/JSError.swift | 5 ++--- .../JavaScriptKit/BasicObjects/JSTypedArray.swift | 10 ++++------ .../JavaScriptKit/FundamentalObjects/JSObject.swift | 13 +++++-------- 5 files changed, 15 insertions(+), 23 deletions(-) diff --git a/Sources/JavaScriptKit/BasicObjects/JSArray.swift b/Sources/JavaScriptKit/BasicObjects/JSArray.swift index 95d14c637..56345d085 100644 --- a/Sources/JavaScriptKit/BasicObjects/JSArray.swift +++ b/Sources/JavaScriptKit/BasicObjects/JSArray.swift @@ -2,9 +2,8 @@ /// 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 var constructor: JSFunction? { _constructor } - @LazyThreadLocal(initialize: { JSObject.global.Array.function }) - private static var _constructor: JSFunction? + 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! diff --git a/Sources/JavaScriptKit/BasicObjects/JSDate.swift b/Sources/JavaScriptKit/BasicObjects/JSDate.swift index da31aca06..c8a6623a1 100644 --- a/Sources/JavaScriptKit/BasicObjects/JSDate.swift +++ b/Sources/JavaScriptKit/BasicObjects/JSDate.swift @@ -8,9 +8,8 @@ */ public final class JSDate: JSBridgedClass { /// The constructor function used to create new `Date` objects. - public static var constructor: JSFunction? { _constructor } - @LazyThreadLocal(initialize: { JSObject.global.Date.function }) - private static var _constructor: JSFunction? + 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 diff --git a/Sources/JavaScriptKit/BasicObjects/JSError.swift b/Sources/JavaScriptKit/BasicObjects/JSError.swift index 559618e15..937581d4b 100644 --- a/Sources/JavaScriptKit/BasicObjects/JSError.swift +++ b/Sources/JavaScriptKit/BasicObjects/JSError.swift @@ -4,9 +4,8 @@ */ public final class JSError: Error, JSBridgedClass { /// The constructor function used to create new JavaScript `Error` objects. - public static var constructor: JSFunction? { _constructor } - @LazyThreadLocal(initialize: { JSObject.global.Error.function }) - private static var _constructor: JSFunction? + 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 diff --git a/Sources/JavaScriptKit/BasicObjects/JSTypedArray.swift b/Sources/JavaScriptKit/BasicObjects/JSTypedArray.swift index bc80cd25c..dec834bbd 100644 --- a/Sources/JavaScriptKit/BasicObjects/JSTypedArray.swift +++ b/Sources/JavaScriptKit/BasicObjects/JSTypedArray.swift @@ -143,19 +143,17 @@ func valueForBitWidth(typeName: String, bitWidth: Int, when32: T) -> T { } extension Int: TypedArrayElement { - public static var typedArrayClass: JSFunction { _typedArrayClass } - @LazyThreadLocal(initialize: { + public static var typedArrayClass: JSFunction { _typedArrayClass.wrappedValue } + private static let _typedArrayClass = LazyThreadLocal(initialize: { valueForBitWidth(typeName: "Int", bitWidth: Int.bitWidth, when32: JSObject.global.Int32Array).function! }) - private static var _typedArrayClass: JSFunction } extension UInt: TypedArrayElement { - public static var typedArrayClass: JSFunction { _typedArrayClass } - @LazyThreadLocal(initialize: { + public static var typedArrayClass: JSFunction { _typedArrayClass.wrappedValue } + private static let _typedArrayClass = LazyThreadLocal(initialize: { valueForBitWidth(typeName: "UInt", bitWidth: Int.bitWidth, when32: JSObject.global.Uint32Array).function! }) - private static var _typedArrayClass: JSFunction } extension Int8: TypedArrayElement { diff --git a/Sources/JavaScriptKit/FundamentalObjects/JSObject.swift b/Sources/JavaScriptKit/FundamentalObjects/JSObject.swift index eb8fb643a..f74b337d8 100644 --- a/Sources/JavaScriptKit/FundamentalObjects/JSObject.swift +++ b/Sources/JavaScriptKit/FundamentalObjects/JSObject.swift @@ -24,9 +24,8 @@ import _CJavaScriptKit /// reference counting system. @dynamicMemberLookup public class JSObject: Equatable { - internal static var constructor: JSFunction { _constructor } - @LazyThreadLocal(initialize: { JSObject.global.Object.function! }) - internal static var _constructor: JSFunction + internal static var constructor: JSFunction { _constructor.wrappedValue } + private static let _constructor = LazyThreadLocal(initialize: { JSObject.global.Object.function! }) @_spi(JSObject_id) public var id: JavaScriptObjectRef @@ -206,12 +205,10 @@ public class JSObject: Equatable { /// A `JSObject` of the global scope object. /// This allows access to the global properties and global names by accessing the `JSObject` returned. - public static var global: JSObject { return _global } - - @LazyThreadLocal(initialize: { - return 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) }) - private static var _global: JSObject deinit { assertOnOwnerThread(hint: "deinitializing") From 917ab578aa4479055e87bbc59f17eeb90a4b6d3d Mon Sep 17 00:00:00 2001 From: Yuta Saito Date: Tue, 4 Mar 2025 01:45:10 +0000 Subject: [PATCH 207/373] Concurrency: Annotate `jsObject` property of `JSError` as `nonisolated(unsafe)` Even though `JSObject` is not a `Sendable` type, `JSError` must be `Sendable` because of `Error` conformance. For this reason, we need to annotate the `jsObject` property as `nonisolated(unsafe)` to suppress the compiler error. Accessing this property from a different isolation domain scheduled on a different thread will result in a runtime assertion failure, but better than corrupting memory. --- Sources/JavaScriptKit/BasicObjects/JSError.swift | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/Sources/JavaScriptKit/BasicObjects/JSError.swift b/Sources/JavaScriptKit/BasicObjects/JSError.swift index 937581d4b..290838626 100644 --- a/Sources/JavaScriptKit/BasicObjects/JSError.swift +++ b/Sources/JavaScriptKit/BasicObjects/JSError.swift @@ -8,7 +8,11 @@ public final class JSError: Error, JSBridgedClass { private static let _constructor = LazyThreadLocal(initialize: { JSObject.global.Error.function }) /// The underlying JavaScript `Error` object. - public let jsObject: JSObject + /// + /// NOTE: This property must be accessed from the thread that + /// the thrown `Error` object was created on. Otherwise, + /// it will result in a runtime assertion failure. + public nonisolated(unsafe) let jsObject: JSObject /// Creates a new instance of the JavaScript `Error` class with a given message. public init(message: String) { From 30f78ff7ebba29a7baca06a213918f13bfd6ff2b Mon Sep 17 00:00:00 2001 From: Yuta Saito Date: Tue, 4 Mar 2025 01:49:27 +0000 Subject: [PATCH 208/373] Concurrency: Update Package.swift tools version to 6.0 --- Package.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Package.swift b/Package.swift index f21a95cb5..4d4634b88 100644 --- a/Package.swift +++ b/Package.swift @@ -1,4 +1,4 @@ -// swift-tools-version:5.8 +// swift-tools-version:6.0 import PackageDescription From 2642df9275f0b87cd6838960f8cfee9f0e53c5fa Mon Sep 17 00:00:00 2001 From: Yuta Saito Date: Tue, 4 Mar 2025 01:56:16 +0000 Subject: [PATCH 209/373] Concurrency: Replace `swjs_thread_local_closures` with `LazyThreadLocal` --- .../FundamentalObjects/JSClosure.swift | 22 +++++++------------ Sources/_CJavaScriptKit/_CJavaScriptKit.c | 2 -- .../_CJavaScriptKit/include/_CJavaScriptKit.h | 5 ----- 3 files changed, 8 insertions(+), 21 deletions(-) diff --git a/Sources/JavaScriptKit/FundamentalObjects/JSClosure.swift b/Sources/JavaScriptKit/FundamentalObjects/JSClosure.swift index 5d367ba38..dafd4ce38 100644 --- a/Sources/JavaScriptKit/FundamentalObjects/JSClosure.swift +++ b/Sources/JavaScriptKit/FundamentalObjects/JSClosure.swift @@ -26,7 +26,7 @@ public class JSOneshotClosure: JSObject, JSClosureProtocol { } // 3. Retain the given body in static storage by `funcRef`. - JSClosure.sharedClosures[hostFuncRef] = (self, { + JSClosure.sharedClosures.wrappedValue[hostFuncRef] = (self, { defer { self.release() } return body($0) }) @@ -42,7 +42,7 @@ public class JSOneshotClosure: JSObject, JSClosureProtocol { /// 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 } } @@ -74,14 +74,8 @@ public class JSClosure: JSFunction, JSClosureProtocol { } // Note: Retain the closure object itself also to avoid funcRef conflicts - fileprivate static var sharedClosures: SharedJSClosure { - if let swjs_thread_local_closures { - return Unmanaged.fromOpaque(swjs_thread_local_closures).takeUnretainedValue() - } else { - let shared = SharedJSClosure() - swjs_thread_local_closures = Unmanaged.passRetained(shared).toOpaque() - return shared - } + fileprivate static let sharedClosures = LazyThreadLocal { + SharedJSClosure() } private var hostFuncRef: JavaScriptHostFuncRef = 0 @@ -110,7 +104,7 @@ public class JSClosure: JSFunction, JSClosureProtocol { } // 3. Retain the given body in static storage by `funcRef`. - Self.sharedClosures[hostFuncRef] = (self, body) + Self.sharedClosures.wrappedValue[hostFuncRef] = (self, body) } #if compiler(>=5.5) && !hasFeature(Embedded) @@ -192,7 +186,7 @@ func _call_host_function_impl( _ argv: UnsafePointer, _ argc: Int32, _ callbackFuncRef: JavaScriptObjectRef ) -> Bool { - guard let (_, hostFunc) = JSClosure.sharedClosures[hostFuncRef] else { + guard let (_, hostFunc) = JSClosure.sharedClosures.wrappedValue[hostFuncRef] else { return true } let arguments = UnsafeBufferPointer(start: argv, count: Int(argc)).map { $0.jsValue} @@ -232,7 +226,7 @@ extension JSClosure { @_cdecl("_free_host_function_impl") func _free_host_function_impl(_ hostFuncRef: JavaScriptHostFuncRef) { - JSClosure.sharedClosures[hostFuncRef] = nil + JSClosure.sharedClosures.wrappedValue[hostFuncRef] = nil } #endif @@ -251,4 +245,4 @@ public func _swjs_call_host_function( public func _swjs_free_host_function(_ hostFuncRef: JavaScriptHostFuncRef) { _free_host_function_impl(hostFuncRef) } -#endif \ No newline at end of file +#endif diff --git a/Sources/_CJavaScriptKit/_CJavaScriptKit.c b/Sources/_CJavaScriptKit/_CJavaScriptKit.c index 424e9081b..ea8b5b43d 100644 --- a/Sources/_CJavaScriptKit/_CJavaScriptKit.c +++ b/Sources/_CJavaScriptKit/_CJavaScriptKit.c @@ -61,5 +61,3 @@ int swjs_library_features(void) { } #endif #endif - -_Thread_local void *swjs_thread_local_closures; diff --git a/Sources/_CJavaScriptKit/include/_CJavaScriptKit.h b/Sources/_CJavaScriptKit/include/_CJavaScriptKit.h index aa0b978a2..5cb6e6037 100644 --- a/Sources/_CJavaScriptKit/include/_CJavaScriptKit.h +++ b/Sources/_CJavaScriptKit/include/_CJavaScriptKit.h @@ -308,9 +308,4 @@ IMPORT_JS_FUNCTION(swjs_terminate_worker_thread, void, (int tid)) IMPORT_JS_FUNCTION(swjs_get_worker_thread_id, int, (void)) -/// MARK: - thread local storage - -// TODO: Rewrite closure system without global storage -extern _Thread_local void * _Nullable swjs_thread_local_closures; - #endif /* _CJavaScriptKit_h */ From daa820960939fceef1aa243af9a1ac84dc724712 Mon Sep 17 00:00:00 2001 From: Yuta Saito Date: Wed, 5 Mar 2025 06:29:04 +0000 Subject: [PATCH 210/373] Concurrency: Remove `Error` conformance from `JSError` `Error` protocol now requires `Sendable` conformance, which is not possible for `JSError` because `JSObject` is not `Sendable`. --- Sources/JavaScriptKit/BasicObjects/JSError.swift | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/Sources/JavaScriptKit/BasicObjects/JSError.swift b/Sources/JavaScriptKit/BasicObjects/JSError.swift index 290838626..0f87d3c67 100644 --- a/Sources/JavaScriptKit/BasicObjects/JSError.swift +++ b/Sources/JavaScriptKit/BasicObjects/JSError.swift @@ -2,17 +2,13 @@ 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 { +public final class JSError: JSBridgedClass { /// The constructor function used to create new JavaScript `Error` objects. public static var constructor: JSFunction? { _constructor.wrappedValue } private static let _constructor = LazyThreadLocal(initialize: { JSObject.global.Error.function }) /// The underlying JavaScript `Error` object. - /// - /// NOTE: This property must be accessed from the thread that - /// the thrown `Error` object was created on. Otherwise, - /// it will result in a runtime assertion failure. - public nonisolated(unsafe) let jsObject: JSObject + public let jsObject: JSObject /// Creates a new instance of the JavaScript `Error` class with a given message. public init(message: String) { From 9f0197dc8f5c65ebe180712bbd753002cbb1c135 Mon Sep 17 00:00:00 2001 From: Yuta Saito Date: Wed, 5 Mar 2025 06:31:22 +0000 Subject: [PATCH 211/373] Concurrency: Isolate global executor installation by MainActor --- .../JavaScriptEventLoop/JavaScriptEventLoop.swift | 4 ++-- .../WebWorkerTaskExecutor.swift | 9 +++++---- .../JavaScriptEventLoopTestSupport.swift | 4 +++- .../include/_CJavaScriptEventLoop.h | 14 ++++++++------ 4 files changed, 18 insertions(+), 13 deletions(-) diff --git a/Sources/JavaScriptEventLoop/JavaScriptEventLoop.swift b/Sources/JavaScriptEventLoop/JavaScriptEventLoop.swift index 765746bb1..af8738ef8 100644 --- a/Sources/JavaScriptEventLoop/JavaScriptEventLoop.swift +++ b/Sources/JavaScriptEventLoop/JavaScriptEventLoop.swift @@ -102,14 +102,14 @@ public final class JavaScriptEventLoop: SerialExecutor, @unchecked Sendable { 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 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 public static func installGlobalExecutor() { guard !didInstallGlobalExecutor else { return } #if compiler(>=5.9) diff --git a/Sources/JavaScriptEventLoop/WebWorkerTaskExecutor.swift b/Sources/JavaScriptEventLoop/WebWorkerTaskExecutor.swift index 5110f60db..ac4769a82 100644 --- a/Sources/JavaScriptEventLoop/WebWorkerTaskExecutor.swift +++ b/Sources/JavaScriptEventLoop/WebWorkerTaskExecutor.swift @@ -426,14 +426,15 @@ public final class WebWorkerTaskExecutor: TaskExecutor { // MARK: Global Executor hack - private static var _mainThread: pthread_t? - private static var _swift_task_enqueueGlobal_hook_original: UnsafeMutableRawPointer? - private static var _swift_task_enqueueGlobalWithDelay_hook_original: UnsafeMutableRawPointer? - private static var _swift_task_enqueueGlobalWithDeadline_hook_original: UnsafeMutableRawPointer? + @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? /// Install a global executor that forwards jobs from Web Worker threads to the main thread. /// /// This function must be called once before using the Web Worker task executor. + @MainActor public static func installGlobalExecutor() { #if canImport(wasi_pthread) && compiler(>=6.1) && _runtime(_multithreaded) // Ensure this function is called only once. diff --git a/Sources/JavaScriptEventLoopTestSupport/JavaScriptEventLoopTestSupport.swift b/Sources/JavaScriptEventLoopTestSupport/JavaScriptEventLoopTestSupport.swift index 64e6776d4..4c441f3c4 100644 --- a/Sources/JavaScriptEventLoopTestSupport/JavaScriptEventLoopTestSupport.swift +++ b/Sources/JavaScriptEventLoopTestSupport/JavaScriptEventLoopTestSupport.swift @@ -25,7 +25,9 @@ import JavaScriptEventLoop @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() { - JavaScriptEventLoop.installGlobalExecutor() + MainActor.assumeIsolated { + JavaScriptEventLoop.installGlobalExecutor() + } } #endif diff --git a/Sources/_CJavaScriptEventLoop/include/_CJavaScriptEventLoop.h b/Sources/_CJavaScriptEventLoop/include/_CJavaScriptEventLoop.h index 4f1b9470c..08efcb948 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,13 +29,13 @@ typedef SWIFT_CC(swift) void (*swift_task_enqueueGlobal_original)( Job *_Nonnull job); SWIFT_EXPORT_FROM(swift_Concurrency) -extern 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) -extern void *_Nullable swift_task_enqueueGlobalWithDelay_hook; +extern void *_Nullable swift_task_enqueueGlobalWithDelay_hook SWIFT_NONISOLATED_UNSAFE; typedef SWIFT_CC(swift) void (*swift_task_enqueueGlobalWithDeadline_original)( long long sec, @@ -42,13 +44,13 @@ typedef SWIFT_CC(swift) void (*swift_task_enqueueGlobalWithDeadline_original)( long long tnsec, int clock, Job *_Nonnull job); SWIFT_EXPORT_FROM(swift_Concurrency) -extern void *_Nullable swift_task_enqueueGlobalWithDeadline_hook; +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) -extern 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. @@ -59,13 +61,13 @@ 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; +extern void *_Nullable swift_task_asyncMainDrainQueue_hook SWIFT_NONISOLATED_UNSAFE; /// MARK: - thread local storage extern _Thread_local void * _Nullable swjs_thread_local_event_loop; -extern _Thread_local void * _Nullable swjs_thread_local_task_executor_worker; +extern _Thread_local void * _Nullable swjs_thread_local_task_executor_worker SWIFT_NONISOLATED_UNSAFE; #endif From fa77908b7a9b5d6ac914bc886ee282ebb2403611 Mon Sep 17 00:00:00 2001 From: Yuta Saito Date: Wed, 5 Mar 2025 07:01:06 +0000 Subject: [PATCH 212/373] Concurrency: Remove `@Sendable` requirement from scheduling primitives They are accessed from a single thread, so there is no need to enforce `@Sendable` requirement on them. And also the following code is not working with `@Sendable` requirement because the captured `JSPromise` is not `Sendable`. ``` let promise = JSPromise(resolver: { resolver -> Void in resolver(.success(.undefined)) }) let setTimeout = JSObject.global.setTimeout.function! let eventLoop = JavaScriptEventLoop( queueTask: { job in // TODO(katei): Should prefer `queueMicrotask` if available? // We should measure if there is performance advantage. promise.then { _ in job() return JSValue.undefined } }, setTimeout: { delay, job in setTimeout(JSOneshotClosure { _ in job() return JSValue.undefined }, delay) } ) ``` --- Sources/JavaScriptEventLoop/JavaScriptEventLoop.swift | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Sources/JavaScriptEventLoop/JavaScriptEventLoop.swift b/Sources/JavaScriptEventLoop/JavaScriptEventLoop.swift index af8738ef8..867fb070a 100644 --- a/Sources/JavaScriptEventLoop/JavaScriptEventLoop.swift +++ b/Sources/JavaScriptEventLoop/JavaScriptEventLoop.swift @@ -40,17 +40,17 @@ 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 - public var queueMicrotask: @Sendable (@escaping () -> Void) -> Void + public var queueMicrotask: (@escaping () -> Void) -> Void /// A function that invokes a given closure after a specified number of milliseconds. - public var setTimeout: @Sendable (Double, @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 (Double, @escaping () -> Void) -> Void + queueTask: @escaping (@escaping () -> Void) -> Void, + setTimeout: @escaping (Double, @escaping () -> Void) -> Void ) { self.queueMicrotask = queueTask self.setTimeout = setTimeout From 97aad009327a645d2296b43160da4ce9f3f6b933 Mon Sep 17 00:00:00 2001 From: Yuta Saito Date: Wed, 5 Mar 2025 07:07:39 +0000 Subject: [PATCH 213/373] Concurrency: Fix sendability errors around `JSClosure.async` --- .../BasicObjects/JSPromise.swift | 14 +++---- .../FundamentalObjects/JSClosure.swift | 40 +++++++++++++------ 2 files changed, 34 insertions(+), 20 deletions(-) diff --git a/Sources/JavaScriptKit/BasicObjects/JSPromise.swift b/Sources/JavaScriptKit/BasicObjects/JSPromise.swift index a41a3e1ca..0580c23bb 100644 --- a/Sources/JavaScriptKit/BasicObjects/JSPromise.swift +++ b/Sources/JavaScriptKit/BasicObjects/JSPromise.swift @@ -90,7 +90,7 @@ public final class JSPromise: JSBridgedClass { /// 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) async throws -> ConvertibleToJSValue) -> JSPromise { + public func then(success: sending @escaping (sending JSValue) async throws -> ConvertibleToJSValue) -> JSPromise { let closure = JSOneshotClosure.async { try await success($0[0]).jsValue } @@ -101,8 +101,8 @@ public final class JSPromise: JSBridgedClass { /// Schedules the `success` closure to be invoked on successful completion of `self`. @discardableResult public func then( - success: @escaping (JSValue) -> ConvertibleToJSValue, - failure: @escaping (JSValue) -> ConvertibleToJSValue + success: @escaping (sending JSValue) -> ConvertibleToJSValue, + failure: @escaping (sending JSValue) -> ConvertibleToJSValue ) -> JSPromise { let successClosure = JSOneshotClosure { success($0[0]).jsValue @@ -117,8 +117,8 @@ public final class JSPromise: JSBridgedClass { /// 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) async throws -> ConvertibleToJSValue, - failure: @escaping (JSValue) async throws -> 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 @@ -132,7 +132,7 @@ public final class JSPromise: JSBridgedClass { /// Schedules the `failure` closure to be invoked on rejected completion of `self`. @discardableResult - public func `catch`(failure: @escaping (JSValue) -> ConvertibleToJSValue) -> JSPromise { + public func `catch`(failure: @escaping (sending JSValue) -> ConvertibleToJSValue) -> JSPromise { let closure = JSOneshotClosure { failure($0[0]).jsValue } @@ -143,7 +143,7 @@ public final class JSPromise: JSBridgedClass { /// 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: @escaping (JSValue) async throws -> ConvertibleToJSValue) -> JSPromise { + public func `catch`(failure: sending @escaping (sending JSValue) async throws -> ConvertibleToJSValue) -> JSPromise { let closure = JSOneshotClosure.async { try await failure($0[0]).jsValue } diff --git a/Sources/JavaScriptKit/FundamentalObjects/JSClosure.swift b/Sources/JavaScriptKit/FundamentalObjects/JSClosure.swift index dafd4ce38..81f2540b6 100644 --- a/Sources/JavaScriptKit/FundamentalObjects/JSClosure.swift +++ b/Sources/JavaScriptKit/FundamentalObjects/JSClosure.swift @@ -15,7 +15,7 @@ public protocol JSClosureProtocol: JSValueCompatible { public class JSOneshotClosure: JSObject, JSClosureProtocol { private var hostFuncRef: JavaScriptHostFuncRef = 0 - public init(_ body: @escaping ([JSValue]) -> JSValue, file: String = #fileID, line: UInt32 = #line) { + 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) @@ -34,7 +34,7 @@ public class JSOneshotClosure: JSObject, JSClosureProtocol { #if compiler(>=5.5) && !hasFeature(Embedded) @available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) - public static func async(_ body: @escaping ([JSValue]) async throws -> JSValue) -> JSOneshotClosure { + public static func async(_ body: sending @escaping (sending [JSValue]) async throws -> JSValue) -> JSOneshotClosure { JSOneshotClosure(makeAsyncClosure(body)) } #endif @@ -64,10 +64,10 @@ public class JSOneshotClosure: JSObject, JSClosureProtocol { public class JSClosure: JSFunction, JSClosureProtocol { class SharedJSClosure { - private var storage: [JavaScriptHostFuncRef: (object: JSObject, body: ([JSValue]) -> JSValue)] = [:] + private var storage: [JavaScriptHostFuncRef: (object: JSObject, body: (sending [JSValue]) -> JSValue)] = [:] init() {} - subscript(_ key: JavaScriptHostFuncRef) -> (object: JSObject, body: ([JSValue]) -> JSValue)? { + subscript(_ key: JavaScriptHostFuncRef) -> (object: JSObject, body: (sending [JSValue]) -> JSValue)? { get { storage[key] } set { storage[key] = newValue } } @@ -93,7 +93,7 @@ public class JSClosure: JSFunction, JSClosureProtocol { }) } - public init(_ body: @escaping ([JSValue]) -> JSValue, file: String = #fileID, line: UInt32 = #line) { + 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) @@ -109,7 +109,7 @@ public class JSClosure: JSFunction, JSClosureProtocol { #if compiler(>=5.5) && !hasFeature(Embedded) @available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) - public static func async(_ body: @escaping ([JSValue]) async throws -> JSValue) -> JSClosure { + public static func async(_ body: @Sendable @escaping (sending [JSValue]) async throws -> JSValue) -> JSClosure { JSClosure(makeAsyncClosure(body)) } #endif @@ -125,18 +125,29 @@ public class JSClosure: JSFunction, JSClosureProtocol { #if compiler(>=5.5) && !hasFeature(Embedded) @available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) -private func makeAsyncClosure(_ body: @escaping ([JSValue]) async throws -> JSValue) -> (([JSValue]) -> JSValue) { +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 body(arguments) - resolver(.success(result)) + let result = try await context.body(context.arguments) + context.resolver(.success(result)) } catch { if let jsError = error as? JSError { - resolver(.failure(jsError.jsValue)) + context.resolver(.failure(jsError.jsValue)) } else { - resolver(.failure(JSError(message: String(describing: error)).jsValue)) + context.resolver(.failure(JSError(message: String(describing: error)).jsValue)) } } } @@ -183,13 +194,16 @@ private func makeAsyncClosure(_ body: @escaping ([JSValue]) async throws -> JSVa @_cdecl("_call_host_function_impl") func _call_host_function_impl( _ hostFuncRef: JavaScriptHostFuncRef, - _ argv: UnsafePointer, _ argc: Int32, + _ argv: sending UnsafePointer, _ argc: Int32, _ callbackFuncRef: JavaScriptObjectRef ) -> 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.. Date: Wed, 5 Mar 2025 07:17:37 +0000 Subject: [PATCH 214/373] Concurrency: Introduce `JSException` and remove `Error` conformance from `JSValue` This is a breaking change. It introduces a new `JSException` type to represent exceptions thrown from JavaScript code. This change is necessary to remove `Sendable` conformance from `JSValue`, which is derived from `Error` conformance. --- .../JavaScriptEventLoop.swift | 6 ++-- .../BasicObjects/JSPromise.swift | 10 +++++- .../FundamentalObjects/JSClosure.swift | 4 +-- .../JSThrowingFunction.swift | 6 ++-- Sources/JavaScriptKit/JSException.swift | 34 +++++++++++++++++++ Sources/JavaScriptKit/JSValue.swift | 2 -- 6 files changed, 51 insertions(+), 11 deletions(-) create mode 100644 Sources/JavaScriptKit/JSException.swift diff --git a/Sources/JavaScriptEventLoop/JavaScriptEventLoop.swift b/Sources/JavaScriptEventLoop/JavaScriptEventLoop.swift index 867fb070a..b9e89a375 100644 --- a/Sources/JavaScriptEventLoop/JavaScriptEventLoop.swift +++ b/Sources/JavaScriptEventLoop/JavaScriptEventLoop.swift @@ -218,7 +218,7 @@ public extension JSPromise { return JSValue.undefined }, failure: { - continuation.resume(throwing: $0) + continuation.resume(throwing: JSException($0)) return JSValue.undefined } ) @@ -227,7 +227,7 @@ public extension JSPromise { } /// Wait for the promise to complete, returning its result or exception as a Result. - var result: Result { + var result: Swift.Result { get async { await withUnsafeContinuation { [self] continuation in self.then( @@ -236,7 +236,7 @@ public extension JSPromise { return JSValue.undefined }, failure: { - continuation.resume(returning: .failure($0)) + continuation.resume(returning: .failure(JSException($0))) return JSValue.undefined } ) diff --git a/Sources/JavaScriptKit/BasicObjects/JSPromise.swift b/Sources/JavaScriptKit/BasicObjects/JSPromise.swift index 0580c23bb..1aec5f4af 100644 --- a/Sources/JavaScriptKit/BasicObjects/JSPromise.swift +++ b/Sources/JavaScriptKit/BasicObjects/JSPromise.swift @@ -31,6 +31,14 @@ public final class JSPromise: JSBridgedClass { return Self(jsObject) } + /// The result of a promise. + public enum Result { + /// 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 @@ -38,7 +46,7 @@ public final class JSPromise: JSBridgedClass { /// 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) { + 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 diff --git a/Sources/JavaScriptKit/FundamentalObjects/JSClosure.swift b/Sources/JavaScriptKit/FundamentalObjects/JSClosure.swift index 81f2540b6..8c42d2ac4 100644 --- a/Sources/JavaScriptKit/FundamentalObjects/JSClosure.swift +++ b/Sources/JavaScriptKit/FundamentalObjects/JSClosure.swift @@ -144,8 +144,8 @@ private func makeAsyncClosure( let result = try await context.body(context.arguments) context.resolver(.success(result)) } catch { - if let jsError = error as? JSError { - context.resolver(.failure(jsError.jsValue)) + if let jsError = error as? JSException { + context.resolver(.failure(jsError.thrownValue)) } else { context.resolver(.failure(JSError(message: String(describing: error)).jsValue)) } diff --git a/Sources/JavaScriptKit/FundamentalObjects/JSThrowingFunction.swift b/Sources/JavaScriptKit/FundamentalObjects/JSThrowingFunction.swift index 8b4fc7cde..17b61090f 100644 --- a/Sources/JavaScriptKit/FundamentalObjects/JSThrowingFunction.swift +++ b/Sources/JavaScriptKit/FundamentalObjects/JSThrowingFunction.swift @@ -37,7 +37,7 @@ 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 @@ -52,7 +52,7 @@ public class JSThrowingFunction { let exceptionKind = JavaScriptValueKindAndFlags(bitPattern: exceptionRawKind) if exceptionKind.isException { let exception = RawJSValue(kind: exceptionKind.kind, payload1: exceptionPayload1, payload2: exceptionPayload2) - return .failure(exception.jsValue) + return .failure(JSException(exception.jsValue)) } return .success(JSObject(id: resultObj)) } @@ -92,7 +92,7 @@ private func invokeJSFunction(_ jsFunc: JSFunction, arguments: [ConvertibleToJSV } } if isException { - throw result + throw JSException(result) } return result } diff --git a/Sources/JavaScriptKit/JSException.swift b/Sources/JavaScriptKit/JSException.swift new file mode 100644 index 000000000..7f1959c70 --- /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 { + /// 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 ed44f50ea..1efffe484 100644 --- a/Sources/JavaScriptKit/JSValue.swift +++ b/Sources/JavaScriptKit/JSValue.swift @@ -124,8 +124,6 @@ public extension JSValue { } } -extension JSValue: Swift.Error {} - public extension JSValue { func fromJSValue() -> Type? where Type: ConstructibleFromJSValue { return Type.construct(from: self) From d1781a8c596bb14819a80116bc8d13870e316145 Mon Sep 17 00:00:00 2001 From: Yuta Saito Date: Wed, 5 Mar 2025 07:21:52 +0000 Subject: [PATCH 215/373] CI: Remove Xcode 15.2 (Swift 5.9) from the matrix --- .github/workflows/test.yml | 2 -- 1 file changed, 2 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index daac3c50f..f87d3c5f5 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -52,8 +52,6 @@ jobs: strategy: matrix: include: - - os: macos-14 - xcode: Xcode_15.2 - os: macos-15 xcode: Xcode_16 runs-on: ${{ matrix.os }} From 39c207b4e45ad92137ef149fe9ea83c92e9cad14 Mon Sep 17 00:00:00 2001 From: Yuta Saito Date: Wed, 5 Mar 2025 07:42:53 +0000 Subject: [PATCH 216/373] Fix `JAVASCRIPTKIT_WITHOUT_WEAKREFS` build --- Sources/JavaScriptKit/FundamentalObjects/JSClosure.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/JavaScriptKit/FundamentalObjects/JSClosure.swift b/Sources/JavaScriptKit/FundamentalObjects/JSClosure.swift index 8c42d2ac4..c1f0361da 100644 --- a/Sources/JavaScriptKit/FundamentalObjects/JSClosure.swift +++ b/Sources/JavaScriptKit/FundamentalObjects/JSClosure.swift @@ -221,7 +221,7 @@ func _call_host_function_impl( extension JSClosure { public func release() { isReleased = true - Self.sharedClosures[hostFuncRef] = nil + Self.sharedClosures.wrappedValue[hostFuncRef] = nil } } From 0fc7f41c573c3ad25d4367bf591d3c0008bcc303 Mon Sep 17 00:00:00 2001 From: Yuta Saito Date: Wed, 5 Mar 2025 07:43:19 +0000 Subject: [PATCH 217/373] Concurrency: Use `JSPromise.Result` instead of `Swift.Result` for `JSPromise.result` To reduce burden type casting, it's better to remove the wrapper from the API. --- Sources/JavaScriptEventLoop/JavaScriptEventLoop.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Sources/JavaScriptEventLoop/JavaScriptEventLoop.swift b/Sources/JavaScriptEventLoop/JavaScriptEventLoop.swift index b9e89a375..c0141cd63 100644 --- a/Sources/JavaScriptEventLoop/JavaScriptEventLoop.swift +++ b/Sources/JavaScriptEventLoop/JavaScriptEventLoop.swift @@ -227,7 +227,7 @@ public extension JSPromise { } /// Wait for the promise to complete, returning its result or exception as a Result. - var result: Swift.Result { + var result: JSPromise.Result { get async { await withUnsafeContinuation { [self] continuation in self.then( @@ -236,7 +236,7 @@ public extension JSPromise { return JSValue.undefined }, failure: { - continuation.resume(returning: .failure(JSException($0))) + continuation.resume(returning: .failure($0)) return JSValue.undefined } ) From 899fa637f04d34728401cba2984073e95b802c20 Mon Sep 17 00:00:00 2001 From: Yuta Saito Date: Wed, 5 Mar 2025 07:44:36 +0000 Subject: [PATCH 218/373] Add `Equatable` conformances to new types --- Sources/JavaScriptKit/BasicObjects/JSPromise.swift | 2 +- Sources/JavaScriptKit/JSException.swift | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Sources/JavaScriptKit/BasicObjects/JSPromise.swift b/Sources/JavaScriptKit/BasicObjects/JSPromise.swift index 1aec5f4af..cfe32d515 100644 --- a/Sources/JavaScriptKit/BasicObjects/JSPromise.swift +++ b/Sources/JavaScriptKit/BasicObjects/JSPromise.swift @@ -32,7 +32,7 @@ public final class JSPromise: JSBridgedClass { } /// The result of a promise. - public enum Result { + public enum Result: Equatable { /// The promise resolved with a value. case success(JSValue) /// The promise rejected with a value. diff --git a/Sources/JavaScriptKit/JSException.swift b/Sources/JavaScriptKit/JSException.swift index 7f1959c70..393ae9615 100644 --- a/Sources/JavaScriptKit/JSException.swift +++ b/Sources/JavaScriptKit/JSException.swift @@ -12,7 +12,7 @@ /// let jsErrorValue = error.thrownValue /// } /// ``` -public struct JSException: Error { +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 { From 042e26e8740fb084e52c58f3f34867b2795f25a4 Mon Sep 17 00:00:00 2001 From: Yuta Saito Date: Wed, 5 Mar 2025 07:45:20 +0000 Subject: [PATCH 219/373] Concurency: Remove `@MainActor` requirement from `JSEL.installGlobalExecutor` The installation of the global executor should be done before any job scheduling, so it should be able to be called at top-level immediately executed code. --- Sources/JavaScriptEventLoop/JavaScriptEventLoop.swift | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/Sources/JavaScriptEventLoop/JavaScriptEventLoop.swift b/Sources/JavaScriptEventLoop/JavaScriptEventLoop.swift index c0141cd63..07eec2cd2 100644 --- a/Sources/JavaScriptEventLoop/JavaScriptEventLoop.swift +++ b/Sources/JavaScriptEventLoop/JavaScriptEventLoop.swift @@ -109,7 +109,13 @@ public final class JavaScriptEventLoop: SerialExecutor, @unchecked Sendable { /// 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) - @MainActor public static func installGlobalExecutor() { + public static func installGlobalExecutor() { + MainActor.assumeIsolated { + Self.installGlobalExecutorIsolated() + } + } + + @MainActor private static func installGlobalExecutorIsolated() { guard !didInstallGlobalExecutor else { return } #if compiler(>=5.9) From 22572338eb7eed5624f7fcf76975dfa6f5c0d3e6 Mon Sep 17 00:00:00 2001 From: Yuta Saito Date: Wed, 5 Mar 2025 07:47:16 +0000 Subject: [PATCH 220/373] Concurrency: Adjust test cases for new exception handling --- .../Sources/ConcurrencyTests/main.swift | 4 +- .../Sources/PrimaryTests/UnitTestUtils.swift | 2 +- .../Sources/PrimaryTests/main.swift | 37 +++++++++---------- 3 files changed, 20 insertions(+), 23 deletions(-) diff --git a/IntegrationTests/TestSuites/Sources/ConcurrencyTests/main.swift b/IntegrationTests/TestSuites/Sources/ConcurrencyTests/main.swift index ece58b317..1f0764e14 100644 --- a/IntegrationTests/TestSuites/Sources/ConcurrencyTests/main.swift +++ b/IntegrationTests/TestSuites/Sources/ConcurrencyTests/main.swift @@ -48,7 +48,7 @@ func entrypoint() async throws { resolve(.failure(.number(3))) }) let error = try await expectAsyncThrow(await p.value) - let jsValue = try expectCast(error, to: JSValue.self) + let jsValue = try expectCast(error, to: JSException.self).thrownValue try expectEqual(jsValue, 3) try await expectEqual(p.result, .failure(.number(3))) } @@ -157,7 +157,7 @@ func entrypoint() async throws { ) } let promise2 = promise.then { _ in - throw JSError(message: "should not succeed") + throw MessageError("Should not be called", file: #file, line: #line, column: #column) } failure: { err in return err } diff --git a/IntegrationTests/TestSuites/Sources/PrimaryTests/UnitTestUtils.swift b/IntegrationTests/TestSuites/Sources/PrimaryTests/UnitTestUtils.swift index c4f9a9fb1..0d51c6ff5 100644 --- a/IntegrationTests/TestSuites/Sources/PrimaryTests/UnitTestUtils.swift +++ b/IntegrationTests/TestSuites/Sources/PrimaryTests/UnitTestUtils.swift @@ -110,7 +110,7 @@ func expectThrow(_ body: @autoclosure () throws -> T, file: StaticString = #f throw MessageError("Expect to throw an exception", file: file, line: line, column: column) } -func wrapUnsafeThrowableFunction(_ body: @escaping () -> Void, file: StaticString = #file, line: UInt = #line, column: UInt = #column) throws -> Error { +func wrapUnsafeThrowableFunction(_ body: @escaping () -> Void, file: StaticString = #file, line: UInt = #line, column: UInt = #column) throws -> JSValue { JSObject.global.callThrowingClosure.function!(JSClosure { _ in body() return .undefined diff --git a/IntegrationTests/TestSuites/Sources/PrimaryTests/main.swift b/IntegrationTests/TestSuites/Sources/PrimaryTests/main.swift index 67a51aa2e..12cc91cc9 100644 --- a/IntegrationTests/TestSuites/Sources/PrimaryTests/main.swift +++ b/IntegrationTests/TestSuites/Sources/PrimaryTests/main.swift @@ -263,8 +263,8 @@ try test("Closure Lifetime") { let c1Line = #line + 1 let c1 = JSClosure { $0[0] } c1.release() - let error = try expectThrow(try evalClosure.throws(c1, JSValue.number(42.0))) as! JSValue - try expect("Error message should contains definition location", error.description.hasSuffix("PrimaryTests/main.swift:\(c1Line)")) + let error = try expectThrow(try evalClosure.throws(c1, JSValue.number(42.0))) as! JSException + try expect("Error message should contains definition location", error.thrownValue.description.hasSuffix("PrimaryTests/main.swift:\(c1Line)")) } #endif @@ -275,8 +275,8 @@ try test("Closure Lifetime") { do { let c1 = JSClosure { _ in fatalError("Crash while closure evaluation") } - let error = try expectThrow(try evalClosure.throws(c1)) as! JSValue - try expectEqual(error.description, "RuntimeError: unreachable") + let error = try expectThrow(try evalClosure.throws(c1)) as! JSException + try expectEqual(error.thrownValue.description, "RuntimeError: unreachable") } } @@ -770,32 +770,32 @@ try test("Exception") { // 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 expectEqual(error1 is JSException, true) + let errorObject = JSError(from: (error1 as! JSException).thrownValue) 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(error2 is JSException, true) + let errorString = try expectString((error2 as! JSException).thrownValue) 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(error3 is JSException, true) + let errorNumber = try expectNumber((error3 as! JSException).thrownValue) 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 expectEqual(error4 is JSException, true) + let errorObject2 = JSError(from: (error4 as! JSException).thrownValue) 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 expectEqual(ageError is JSException, true) + let errorObject3 = JSError(from: (ageError as! JSException).thrownValue) try expectNotNil(errorObject3) } @@ -824,18 +824,15 @@ try test("Unhandled Exception") { // MARK: Throwing method calls let error1 = try wrapUnsafeThrowableFunction { _ = prop_9.object!.func1!() } - try expectEqual(error1 is JSValue, true) - let errorObject = JSError(from: error1 as! JSValue) + let errorObject = JSError(from: error1) try expectNotNil(errorObject) let error2 = try wrapUnsafeThrowableFunction { _ = prop_9.object!.func2!() } - try expectEqual(error2 is JSValue, true) - let errorString = try expectString(error2 as! JSValue) + let errorString = try expectString(error2) try expectEqual(errorString, "String Error") let error3 = try wrapUnsafeThrowableFunction { _ = prop_9.object!.func3!() } - try expectEqual(error3 is JSValue, true) - let errorNumber = try expectNumber(error3 as! JSValue) + let errorNumber = try expectNumber(error3) try expectEqual(errorNumber, 3.0) } From 0c43cbfd67ae8bf0969da51c9d15d181cbe13f7f Mon Sep 17 00:00:00 2001 From: Yuta Saito Date: Wed, 5 Mar 2025 07:58:09 +0000 Subject: [PATCH 221/373] CI: Update Swift toolchain to 2025-02-26-a Our new code htis assertion in 2024-10-30-a, but it's fixed in 2025-02-26-a. --- .github/workflows/test.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index f87d3c5f5..1c8dae632 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -15,11 +15,11 @@ jobs: wasi-backend: Node - os: ubuntu-22.04 toolchain: - download-url: https://download.swift.org/development/ubuntu2204/swift-DEVELOPMENT-SNAPSHOT-2024-10-30-a/swift-DEVELOPMENT-SNAPSHOT-2024-10-30-a-ubuntu22.04.tar.gz + 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 - os: ubuntu-22.04 toolchain: - download-url: https://download.swift.org/development/ubuntu2204/swift-DEVELOPMENT-SNAPSHOT-2024-10-30-a/swift-DEVELOPMENT-SNAPSHOT-2024-10-30-a-ubuntu22.04.tar.gz + 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 runs-on: ${{ matrix.entry.os }} @@ -69,7 +69,7 @@ jobs: entry: - os: ubuntu-22.04 toolchain: - download-url: https://download.swift.org/development/ubuntu2204/swift-DEVELOPMENT-SNAPSHOT-2024-10-30-a/swift-DEVELOPMENT-SNAPSHOT-2024-10-30-a-ubuntu22.04.tar.gz + 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 steps: - uses: actions/checkout@v4 - uses: ./.github/actions/install-swift From 7a7acb44ea71c58a9ccdb2a6e6f95059d8e624d1 Mon Sep 17 00:00:00 2001 From: Yuta Saito Date: Wed, 5 Mar 2025 08:02:02 +0000 Subject: [PATCH 222/373] Concurrency: Remove unnecessary `sending` keyword --- Sources/JavaScriptKit/FundamentalObjects/JSClosure.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/JavaScriptKit/FundamentalObjects/JSClosure.swift b/Sources/JavaScriptKit/FundamentalObjects/JSClosure.swift index c1f0361da..c075c63e5 100644 --- a/Sources/JavaScriptKit/FundamentalObjects/JSClosure.swift +++ b/Sources/JavaScriptKit/FundamentalObjects/JSClosure.swift @@ -194,7 +194,7 @@ private func makeAsyncClosure( @_cdecl("_call_host_function_impl") func _call_host_function_impl( _ hostFuncRef: JavaScriptHostFuncRef, - _ argv: sending UnsafePointer, _ argc: Int32, + _ argv: UnsafePointer, _ argc: Int32, _ callbackFuncRef: JavaScriptObjectRef ) -> Bool { guard let (_, hostFunc) = JSClosure.sharedClosures.wrappedValue[hostFuncRef] else { From 18ad4e3be8465167af62172b67d64da2fdaab3e2 Mon Sep 17 00:00:00 2001 From: Yuta Saito Date: Wed, 5 Mar 2025 08:13:18 +0000 Subject: [PATCH 223/373] Swift 6.1 and later uses .xctest for XCTest bundle --- Makefile | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/Makefile b/Makefile index 1b653315c..88f4e0795 100644 --- a/Makefile +++ b/Makefile @@ -21,11 +21,18 @@ test: CONFIGURATION=release SWIFT_BUILD_FLAGS="$(SWIFT_BUILD_FLAGS)" $(MAKE) test && \ CONFIGURATION=release SWIFT_BUILD_FLAGS="$(SWIFT_BUILD_FLAGS) -Xswiftc -DJAVASCRIPTKIT_WITHOUT_WEAKREFS" $(MAKE) test +TEST_RUNNER := node --experimental-wasi-unstable-preview1 scripts/test-harness.mjs .PHONY: unittest unittest: @echo Running unit tests swift build --build-tests -Xswiftc -Xclang-linker -Xswiftc -mexec-model=reactor -Xlinker --export-if-defined=main -Xlinker --export-if-defined=__main_argc_argv --static-swift-stdlib -Xswiftc -static-stdlib $(SWIFT_BUILD_FLAGS) - node --experimental-wasi-unstable-preview1 scripts/test-harness.mjs ./.build/debug/JavaScriptKitPackageTests.wasm +# Swift 6.1 and later uses .xctest for XCTest bundle but earliers used .wasm +# See https://github.com/swiftlang/swift-package-manager/pull/8254 + if [ -f .build/debug/JavaScriptKitPackageTests.xctest ]; then \ + $(TEST_RUNNER) .build/debug/JavaScriptKitPackageTests.xctest; \ + else \ + $(TEST_RUNNER) .build/debug/JavaScriptKitPackageTests.wasm; \ + fi .PHONY: benchmark_setup benchmark_setup: From 3f3b494adf034ec72b24c577f3bd3a11d7ae8a2b Mon Sep 17 00:00:00 2001 From: Yuta Saito Date: Wed, 5 Mar 2025 08:34:53 +0000 Subject: [PATCH 224/373] Concurrency: Explicitly mark `Sendable` conformance as unavailable for `JSValue` --- Sources/JavaScriptKit/JSValue.swift | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/Sources/JavaScriptKit/JSValue.swift b/Sources/JavaScriptKit/JSValue.swift index 1efffe484..2562daac8 100644 --- a/Sources/JavaScriptKit/JSValue.swift +++ b/Sources/JavaScriptKit/JSValue.swift @@ -100,6 +100,13 @@ 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 {} + public extension JSValue { #if !hasFeature(Embedded) /// An unsafe convenience method of `JSObject.subscript(_ name: String) -> ((ConvertibleToJSValue...) -> JSValue)?` From bf5861698f30bc241473ca4eda4409e2bee4ff04 Mon Sep 17 00:00:00 2001 From: Yuta Saito Date: Wed, 5 Mar 2025 08:54:29 +0000 Subject: [PATCH 225/373] Concurrency: Fix build for p1-threads target --- .../include/_CJavaScriptEventLoop.h | 2 +- .../WebWorkerTaskExecutorTests.swift | 36 +++++++++---------- 2 files changed, 18 insertions(+), 20 deletions(-) diff --git a/Sources/_CJavaScriptEventLoop/include/_CJavaScriptEventLoop.h b/Sources/_CJavaScriptEventLoop/include/_CJavaScriptEventLoop.h index 08efcb948..0fa08c9e7 100644 --- a/Sources/_CJavaScriptEventLoop/include/_CJavaScriptEventLoop.h +++ b/Sources/_CJavaScriptEventLoop/include/_CJavaScriptEventLoop.h @@ -66,7 +66,7 @@ extern void *_Nullable swift_task_asyncMainDrainQueue_hook SWIFT_NONISOLATED_UNS /// MARK: - thread local storage -extern _Thread_local void * _Nullable swjs_thread_local_event_loop; +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; diff --git a/Tests/JavaScriptEventLoopTests/WebWorkerTaskExecutorTests.swift b/Tests/JavaScriptEventLoopTests/WebWorkerTaskExecutorTests.swift index 726f4da75..3848ba4cc 100644 --- a/Tests/JavaScriptEventLoopTests/WebWorkerTaskExecutorTests.swift +++ b/Tests/JavaScriptEventLoopTests/WebWorkerTaskExecutorTests.swift @@ -8,8 +8,8 @@ import _CJavaScriptKit // For swjs_get_worker_thread_id func isMainThread() -> Bool final class WebWorkerTaskExecutorTests: XCTestCase { - override func setUp() { - WebWorkerTaskExecutor.installGlobalExecutor() + override func setUp() async { + await WebWorkerTaskExecutor.installGlobalExecutor() } func testTaskRunOnMainThread() async throws { @@ -152,48 +152,46 @@ final class WebWorkerTaskExecutorTests: XCTestCase { func testThreadLocalPerThreadValues() async throws { struct Check { - @ThreadLocal(boxing: ()) - static var value: Int? + static let value = ThreadLocal(boxing: ()) } let executor = try await WebWorkerTaskExecutor(numberOfThreads: 1) - XCTAssertNil(Check.value) - Check.value = 42 - XCTAssertEqual(Check.value, 42) + XCTAssertNil(Check.value.wrappedValue) + Check.value.wrappedValue = 42 + XCTAssertEqual(Check.value.wrappedValue, 42) let task = Task(executorPreference: executor) { - XCTAssertEqual(Check.value, nil) - Check.value = 100 - XCTAssertEqual(Check.value, 100) - return Check.value + 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, 42) + XCTAssertEqual(Check.value.wrappedValue, 42) executor.terminate() } func testLazyThreadLocalPerThreadInitialization() async throws { struct Check { - static var valueToInitialize = 42 - static var countOfInitialization = 0 - @LazyThreadLocal(initialize: { + nonisolated(unsafe) static var valueToInitialize = 42 + nonisolated(unsafe) static var countOfInitialization = 0 + static let value = LazyThreadLocal(initialize: { countOfInitialization += 1 return valueToInitialize }) - static var value: Int } let executor = try await WebWorkerTaskExecutor(numberOfThreads: 1) XCTAssertEqual(Check.countOfInitialization, 0) - XCTAssertEqual(Check.value, 42) + XCTAssertEqual(Check.value.wrappedValue, 42) XCTAssertEqual(Check.countOfInitialization, 1) Check.valueToInitialize = 100 let task = Task(executorPreference: executor) { XCTAssertEqual(Check.countOfInitialization, 1) - XCTAssertEqual(Check.value, 100) + XCTAssertEqual(Check.value.wrappedValue, 100) XCTAssertEqual(Check.countOfInitialization, 2) - return Check.value + return Check.value.wrappedValue } let result = await task.value XCTAssertEqual(result, 100) From 74a9070bcf6b6a544761288948cbc85b97287107 Mon Sep 17 00:00:00 2001 From: Yuta Saito Date: Wed, 5 Mar 2025 08:56:31 +0000 Subject: [PATCH 226/373] CI: Check p1-threads target --- .github/workflows/test.yml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 1c8dae632..62e2a8ac9 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -13,14 +13,17 @@ jobs: 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/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: @@ -33,6 +36,8 @@ jobs: download-url: ${{ matrix.entry.toolchain.download-url }} - uses: swiftwasm/setup-swiftwasm@v2 id: setup-swiftwasm + with: + target: ${{ matrix.entry.target }} - name: Configure Swift SDK run: echo "SWIFT_SDK_ID=${{ steps.setup-swiftwasm.outputs.swift-sdk-id }}" >> $GITHUB_ENV - run: make bootstrap From a732a0c45fe8dd6a7f5a1503926cac439f6f1015 Mon Sep 17 00:00:00 2001 From: Yuta Saito Date: Thu, 6 Mar 2025 17:58:13 +0900 Subject: [PATCH 227/373] Concurrency: Relax WebWorkerTaskExecutor.installGlobalExecutor() isolation requirement Avoid breaking existing code as much as possible just for the sake of trivial "safety". --- Sources/JavaScriptEventLoop/WebWorkerTaskExecutor.swift | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/Sources/JavaScriptEventLoop/WebWorkerTaskExecutor.swift b/Sources/JavaScriptEventLoop/WebWorkerTaskExecutor.swift index ac4769a82..14b13eee9 100644 --- a/Sources/JavaScriptEventLoop/WebWorkerTaskExecutor.swift +++ b/Sources/JavaScriptEventLoop/WebWorkerTaskExecutor.swift @@ -434,8 +434,14 @@ public final class WebWorkerTaskExecutor: TaskExecutor { /// Install a global executor that forwards jobs from Web Worker threads to the main thread. /// /// This function must be called once before using the Web Worker task executor. - @MainActor 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 } From 28d5ec060749d2ed386b554e282977a4ecee9a4a Mon Sep 17 00:00:00 2001 From: Yuta Saito Date: Mon, 10 Mar 2025 14:21:50 +0000 Subject: [PATCH 228/373] Add `JSObject.transfer` and `JSObject.receive` APIs These APIs allow transferring a `JSObject` between worker threads. The `JSObject.transfer` method creates a `JSObject.Transferring` instance that is `Sendable` and can be sent to another worker thread. The `JSObject.receive` method requests the object from the source worker thread and postMessage it to the destination worker thread. --- Runtime/src/index.ts | 147 ++++++++++++++++-- Runtime/src/types.ts | 8 + .../JSObject+Transferring.swift | 60 +++++++ .../FundamentalObjects/JSObject.swift | 16 +- Sources/JavaScriptKit/Runtime/index.js | 111 ++++++++++++- Sources/JavaScriptKit/Runtime/index.mjs | 111 ++++++++++++- Sources/_CJavaScriptKit/_CJavaScriptKit.c | 8 + .../_CJavaScriptKit/include/_CJavaScriptKit.h | 9 ++ 8 files changed, 436 insertions(+), 34 deletions(-) create mode 100644 Sources/JavaScriptEventLoop/JSObject+Transferring.swift diff --git a/Runtime/src/index.ts b/Runtime/src/index.ts index 73f56411a..25d6e92f5 100644 --- a/Runtime/src/index.ts +++ b/Runtime/src/index.ts @@ -6,18 +6,45 @@ import { pointer, TypedArray, ImportedFunctions, + MAIN_THREAD_TID, } from "./types.js"; import * as JSValue from "./js-value.js"; import { Memory } from "./memory.js"; +type TransferMessage = { + type: "transfer"; + data: { + object: any; + transferring: pointer; + destinationTid: number; + }; +}; + +type RequestTransferMessage = { + type: "requestTransfer"; + data: { + objectRef: ref; + objectSourceTid: number; + transferring: pointer; + destinationTid: number; + }; +}; + +type TransferErrorMessage = { + type: "transferError"; + data: { + error: string; + }; +}; + type MainToWorkerMessage = { type: "wake"; -}; +} | RequestTransferMessage | TransferMessage | TransferErrorMessage; type WorkerToMainMessage = { type: "job"; data: number; -}; +} | RequestTransferMessage | TransferMessage | TransferErrorMessage; /** * A thread channel is a set of functions that are used to communicate between @@ -60,8 +87,9 @@ 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) => void; + 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`. @@ -75,8 +103,9 @@ export type SwiftRuntimeThreadChannel = * 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) => void; + 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. @@ -610,8 +639,37 @@ export class SwiftRuntime { case "wake": this.exports.swjs_wake_worker_thread(); break; + case "requestTransfer": { + const object = this.memory.getObject(message.data.objectRef); + const messageToMainThread: TransferMessage = { + type: "transfer", + data: { + object, + destinationTid: message.data.destinationTid, + transferring: message.data.transferring, + }, + }; + try { + this.postMessageToMainThread(messageToMainThread, [object]); + } catch (error) { + this.postMessageToMainThread({ + type: "transferError", + data: { error: String(error) }, + }); + } + break; + } + case "transfer": { + const objectRef = this.memory.retain(message.data.object); + this.exports.swjs_receive_object(objectRef, message.data.transferring); + break; + } + case "transferError": { + console.error(message.data.error); // TODO: Handle the error + break; + } default: - const unknownMessage: never = message.type; + const unknownMessage: never = message; throw new Error(`Unknown message type: ${unknownMessage}`); } }); @@ -632,8 +690,57 @@ export class SwiftRuntime { case "job": this.exports.swjs_enqueue_main_job_from_worker(message.data); break; + case "requestTransfer": { + if (message.data.objectSourceTid == MAIN_THREAD_TID) { + const object = this.memory.getObject(message.data.objectRef); + if (message.data.destinationTid != tid) { + throw new Error("Invariant violation: The destination tid of the transfer request must be the same as the tid of the worker thread that received the request."); + } + this.postMessageToWorkerThread(message.data.destinationTid, { + type: "transfer", + data: { + object, + transferring: message.data.transferring, + destinationTid: message.data.destinationTid, + }, + }, [object]); + } else { + // Proxy the transfer request to the worker thread that owns the object + this.postMessageToWorkerThread(message.data.objectSourceTid, { + type: "requestTransfer", + data: { + objectRef: message.data.objectRef, + objectSourceTid: tid, + transferring: message.data.transferring, + destinationTid: message.data.destinationTid, + }, + }); + } + break; + } + case "transfer": { + if (message.data.destinationTid == MAIN_THREAD_TID) { + const objectRef = this.memory.retain(message.data.object); + this.exports.swjs_receive_object(objectRef, message.data.transferring); + } else { + // Proxy the transfer response to the destination worker thread + this.postMessageToWorkerThread(message.data.destinationTid, { + type: "transfer", + data: { + object: message.data.object, + transferring: message.data.transferring, + destinationTid: message.data.destinationTid, + }, + }, [message.data.object]); + } + break; + } + case "transferError": { + console.error(message.data.error); // TODO: Handle the error + break; + } default: - const unknownMessage: never = message.type; + const unknownMessage: never = message; throw new Error(`Unknown message type: ${unknownMessage}`); } }, @@ -649,27 +756,47 @@ export class SwiftRuntime { // Main thread's tid is always -1 return this.tid || -1; }, + swjs_request_transferring_object: ( + object_ref: ref, + object_source_tid: number, + transferring: pointer, + ) => { + if (this.tid == object_source_tid) { + // Fast path: The object is already in the same thread + this.exports.swjs_receive_object(object_ref, transferring); + return; + } + this.postMessageToMainThread({ + type: "requestTransfer", + data: { + objectRef: object_ref, + objectSourceTid: object_source_tid, + transferring, + destinationTid: this.tid ?? MAIN_THREAD_TID, + }, + }); + }, }; } - private postMessageToMainThread(message: WorkerToMainMessage) { + 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." ); } - threadChannel.postMessageToMainThread(message); + threadChannel.postMessageToMainThread(message, transfer); } - private postMessageToWorkerThread(tid: number, message: MainToWorkerMessage) { + 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." ); } - threadChannel.postMessageToWorkerThread(tid, message); + threadChannel.postMessageToWorkerThread(tid, message, transfer); } } diff --git a/Runtime/src/types.ts b/Runtime/src/types.ts index dd638acc5..4e311ef80 100644 --- a/Runtime/src/types.ts +++ b/Runtime/src/types.ts @@ -22,6 +22,7 @@ export interface ExportedFunctions { swjs_enqueue_main_job_from_worker(unowned_job: number): void; swjs_wake_worker_thread(): void; + swjs_receive_object(object: ref, transferring: pointer): void; } export interface ImportedFunctions { @@ -112,6 +113,11 @@ export interface ImportedFunctions { swjs_listen_message_from_worker_thread: (tid: number) => void; swjs_terminate_worker_thread: (tid: number) => void; swjs_get_worker_thread_id: () => number; + swjs_request_transferring_object: ( + object_ref: ref, + object_source_tid: number, + transferring: pointer, + ) => void; } export const enum LibraryFeatures { @@ -133,3 +139,5 @@ export type TypedArray = export function assertNever(x: never, message: string) { throw new Error(message); } + +export const MAIN_THREAD_TID = -1; diff --git a/Sources/JavaScriptEventLoop/JSObject+Transferring.swift b/Sources/JavaScriptEventLoop/JSObject+Transferring.swift new file mode 100644 index 000000000..dce32d7ec --- /dev/null +++ b/Sources/JavaScriptEventLoop/JSObject+Transferring.swift @@ -0,0 +1,60 @@ +@_spi(JSObject_id) import JavaScriptKit +import _CJavaScriptKit + +extension JSObject { + public class Transferring: @unchecked Sendable { + fileprivate let sourceTid: Int32 + fileprivate let idInSource: JavaScriptObjectRef + fileprivate var continuation: CheckedContinuation? = nil + + init(sourceTid: Int32, id: JavaScriptObjectRef) { + self.sourceTid = sourceTid + self.idInSource = id + } + + func receive(isolation: isolated (any Actor)?) async throws -> JSObject { + #if compiler(>=6.1) && _runtime(_multithreaded) + swjs_request_transferring_object( + idInSource, + sourceTid, + Unmanaged.passRetained(self).toOpaque() + ) + return try await withCheckedThrowingContinuation { continuation in + self.continuation = continuation + } + #else + return JSObject(id: idInSource) + #endif + } + } + + /// Transfers the ownership of a `JSObject` to be sent to another Worker. + /// + /// - Parameter object: The `JSObject` to be transferred. + /// - Returns: A `JSTransferring` instance that can be shared across worker threads. + /// - Note: The original `JSObject` should not be accessed after calling this method. + public static func transfer(_ object: JSObject) -> Transferring { + #if compiler(>=6.1) && _runtime(_multithreaded) + Transferring(sourceTid: object.ownerTid, id: object.id) + #else + Transferring(sourceTid: -1, id: object.id) + #endif + } + + /// Receives a transferred `JSObject` from a Worker. + /// + /// - Parameter transferring: The `JSTransferring` instance received from other worker threads. + /// - Returns: The reconstructed `JSObject` that can be used in the receiving Worker. + public static func receive(_ transferring: Transferring, isolation: isolated (any Actor)? = #isolation) async throws -> JSObject { + try await transferring.receive(isolation: isolation) + } +} + +#if compiler(>=6.1) // @_expose and @_extern are only available in Swift 6.1+ +@_expose(wasm, "swjs_receive_object") +@_cdecl("swjs_receive_object") +#endif +func _swjs_receive_object(_ object: JavaScriptObjectRef, _ transferring: UnsafeRawPointer) { + let transferring = Unmanaged.fromOpaque(transferring).takeRetainedValue() + transferring.continuation?.resume(returning: JSObject(id: object)) +} diff --git a/Sources/JavaScriptKit/FundamentalObjects/JSObject.swift b/Sources/JavaScriptKit/FundamentalObjects/JSObject.swift index f74b337d8..18c683682 100644 --- a/Sources/JavaScriptKit/FundamentalObjects/JSObject.swift +++ b/Sources/JavaScriptKit/FundamentalObjects/JSObject.swift @@ -1,13 +1,5 @@ import _CJavaScriptKit -#if arch(wasm32) - #if canImport(wasi_pthread) - import wasi_pthread - #endif -#else - import Foundation // for pthread_t on non-wasi platforms -#endif - /// `JSObject` represents an object in JavaScript and supports dynamic member lookup. /// Any member access like `object.foo` will dynamically request the JavaScript and Swift /// runtime bridge library for a member with the specified name in this object. @@ -31,14 +23,14 @@ public class JSObject: Equatable { public var id: JavaScriptObjectRef #if compiler(>=6.1) && _runtime(_multithreaded) - private let ownerThread: pthread_t + package let ownerTid: Int32 #endif @_spi(JSObject_id) public init(id: JavaScriptObjectRef) { self.id = id #if compiler(>=6.1) && _runtime(_multithreaded) - self.ownerThread = pthread_self() + self.ownerTid = swjs_get_worker_thread_id_cached() #endif } @@ -51,14 +43,14 @@ public class JSObject: Equatable { /// object spaces are not shared across threads backed by Web Workers. private func assertOnOwnerThread(hint: @autoclosure () -> String) { #if compiler(>=6.1) && _runtime(_multithreaded) - precondition(pthread_equal(ownerThread, pthread_self()) != 0, "JSObject is being accessed from a thread other than the owner thread: \(hint())") + precondition(ownerTid == swjs_get_worker_thread_id_cached(), "JSObject is being accessed from a thread other than the owner thread: \(hint())") #endif } /// Asserts that the two objects being compared are owned by the same thread. private static func assertSameOwnerThread(lhs: JSObject, rhs: JSObject, hint: @autoclosure () -> String) { #if compiler(>=6.1) && _runtime(_multithreaded) - precondition(pthread_equal(lhs.ownerThread, rhs.ownerThread) != 0, "JSObject is being accessed from a thread other than the owner thread: \(hint())") + precondition(lhs.ownerTid == rhs.ownerTid, "JSObject is being accessed from a thread other than the owner thread: \(hint())") #endif } diff --git a/Sources/JavaScriptKit/Runtime/index.js b/Sources/JavaScriptKit/Runtime/index.js index 223fed3e1..8027593e5 100644 --- a/Sources/JavaScriptKit/Runtime/index.js +++ b/Sources/JavaScriptKit/Runtime/index.js @@ -25,6 +25,7 @@ function assertNever(x, message) { throw new Error(message); } + const MAIN_THREAD_TID = -1; const decode = (kind, payload1, payload2, memory) => { switch (kind) { @@ -512,8 +513,38 @@ case "wake": this.exports.swjs_wake_worker_thread(); break; + case "requestTransfer": { + const object = this.memory.getObject(message.data.objectRef); + const messageToMainThread = { + type: "transfer", + data: { + object, + destinationTid: message.data.destinationTid, + transferring: message.data.transferring, + }, + }; + try { + this.postMessageToMainThread(messageToMainThread, [object]); + } + catch (error) { + this.postMessageToMainThread({ + type: "transferError", + data: { error: String(error) }, + }); + } + break; + } + case "transfer": { + const objectRef = this.memory.retain(message.data.object); + this.exports.swjs_receive_object(objectRef, message.data.transferring); + break; + } + case "transferError": { + console.error(message.data.error); // TODO: Handle the error + break; + } default: - const unknownMessage = message.type; + const unknownMessage = message; throw new Error(`Unknown message type: ${unknownMessage}`); } }); @@ -531,8 +562,59 @@ case "job": this.exports.swjs_enqueue_main_job_from_worker(message.data); break; + case "requestTransfer": { + if (message.data.objectSourceTid == MAIN_THREAD_TID) { + const object = this.memory.getObject(message.data.objectRef); + if (message.data.destinationTid != tid) { + throw new Error("Invariant violation: The destination tid of the transfer request must be the same as the tid of the worker thread that received the request."); + } + this.postMessageToWorkerThread(message.data.destinationTid, { + type: "transfer", + data: { + object, + transferring: message.data.transferring, + destinationTid: message.data.destinationTid, + }, + }, [object]); + } + else { + // Proxy the transfer request to the worker thread that owns the object + this.postMessageToWorkerThread(message.data.objectSourceTid, { + type: "requestTransfer", + data: { + objectRef: message.data.objectRef, + objectSourceTid: tid, + transferring: message.data.transferring, + destinationTid: message.data.destinationTid, + }, + }); + } + break; + } + case "transfer": { + if (message.data.destinationTid == MAIN_THREAD_TID) { + const objectRef = this.memory.retain(message.data.object); + this.exports.swjs_receive_object(objectRef, message.data.transferring); + } + else { + // Proxy the transfer response to the destination worker thread + this.postMessageToWorkerThread(message.data.destinationTid, { + type: "transfer", + data: { + object: message.data.object, + transferring: message.data.transferring, + destinationTid: message.data.destinationTid, + }, + }, [message.data.object]); + } + break; + } + case "transferError": { + console.error(message.data.error); // TODO: Handle the error + break; + } default: - const unknownMessage = message.type; + const unknownMessage = message; throw new Error(`Unknown message type: ${unknownMessage}`); } }); @@ -548,21 +630,38 @@ // Main thread's tid is always -1 return this.tid || -1; }, + swjs_request_transferring_object: (object_ref, object_source_tid, transferring) => { + var _a; + if (this.tid == object_source_tid) { + // Fast path: The object is already in the same thread + this.exports.swjs_receive_object(object_ref, transferring); + return; + } + this.postMessageToMainThread({ + type: "requestTransfer", + data: { + objectRef: object_ref, + objectSourceTid: object_source_tid, + transferring, + destinationTid: (_a = this.tid) !== null && _a !== void 0 ? _a : MAIN_THREAD_TID, + }, + }); + }, }; } - postMessageToMainThread(message) { + 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); + threadChannel.postMessageToMainThread(message, transfer); } - postMessageToWorkerThread(tid, message) { + 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); + threadChannel.postMessageToWorkerThread(tid, message, transfer); } } /// This error is thrown when yielding event loop control from `swift_task_asyncMainDrainQueue` diff --git a/Sources/JavaScriptKit/Runtime/index.mjs b/Sources/JavaScriptKit/Runtime/index.mjs index 34e4dd13f..6a3df7477 100644 --- a/Sources/JavaScriptKit/Runtime/index.mjs +++ b/Sources/JavaScriptKit/Runtime/index.mjs @@ -19,6 +19,7 @@ class SwiftClosureDeallocator { function assertNever(x, message) { throw new Error(message); } +const MAIN_THREAD_TID = -1; const decode = (kind, payload1, payload2, memory) => { switch (kind) { @@ -506,8 +507,38 @@ class SwiftRuntime { case "wake": this.exports.swjs_wake_worker_thread(); break; + case "requestTransfer": { + const object = this.memory.getObject(message.data.objectRef); + const messageToMainThread = { + type: "transfer", + data: { + object, + destinationTid: message.data.destinationTid, + transferring: message.data.transferring, + }, + }; + try { + this.postMessageToMainThread(messageToMainThread, [object]); + } + catch (error) { + this.postMessageToMainThread({ + type: "transferError", + data: { error: String(error) }, + }); + } + break; + } + case "transfer": { + const objectRef = this.memory.retain(message.data.object); + this.exports.swjs_receive_object(objectRef, message.data.transferring); + break; + } + case "transferError": { + console.error(message.data.error); // TODO: Handle the error + break; + } default: - const unknownMessage = message.type; + const unknownMessage = message; throw new Error(`Unknown message type: ${unknownMessage}`); } }); @@ -525,8 +556,59 @@ class SwiftRuntime { case "job": this.exports.swjs_enqueue_main_job_from_worker(message.data); break; + case "requestTransfer": { + if (message.data.objectSourceTid == MAIN_THREAD_TID) { + const object = this.memory.getObject(message.data.objectRef); + if (message.data.destinationTid != tid) { + throw new Error("Invariant violation: The destination tid of the transfer request must be the same as the tid of the worker thread that received the request."); + } + this.postMessageToWorkerThread(message.data.destinationTid, { + type: "transfer", + data: { + object, + transferring: message.data.transferring, + destinationTid: message.data.destinationTid, + }, + }, [object]); + } + else { + // Proxy the transfer request to the worker thread that owns the object + this.postMessageToWorkerThread(message.data.objectSourceTid, { + type: "requestTransfer", + data: { + objectRef: message.data.objectRef, + objectSourceTid: tid, + transferring: message.data.transferring, + destinationTid: message.data.destinationTid, + }, + }); + } + break; + } + case "transfer": { + if (message.data.destinationTid == MAIN_THREAD_TID) { + const objectRef = this.memory.retain(message.data.object); + this.exports.swjs_receive_object(objectRef, message.data.transferring); + } + else { + // Proxy the transfer response to the destination worker thread + this.postMessageToWorkerThread(message.data.destinationTid, { + type: "transfer", + data: { + object: message.data.object, + transferring: message.data.transferring, + destinationTid: message.data.destinationTid, + }, + }, [message.data.object]); + } + break; + } + case "transferError": { + console.error(message.data.error); // TODO: Handle the error + break; + } default: - const unknownMessage = message.type; + const unknownMessage = message; throw new Error(`Unknown message type: ${unknownMessage}`); } }); @@ -542,21 +624,38 @@ class SwiftRuntime { // Main thread's tid is always -1 return this.tid || -1; }, + swjs_request_transferring_object: (object_ref, object_source_tid, transferring) => { + var _a; + if (this.tid == object_source_tid) { + // Fast path: The object is already in the same thread + this.exports.swjs_receive_object(object_ref, transferring); + return; + } + this.postMessageToMainThread({ + type: "requestTransfer", + data: { + objectRef: object_ref, + objectSourceTid: object_source_tid, + transferring, + destinationTid: (_a = this.tid) !== null && _a !== void 0 ? _a : MAIN_THREAD_TID, + }, + }); + }, }; } - postMessageToMainThread(message) { + 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); + threadChannel.postMessageToMainThread(message, transfer); } - postMessageToWorkerThread(tid, message) { + 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); + threadChannel.postMessageToWorkerThread(tid, message, transfer); } } /// This error is thrown when yielding event loop control from `swift_task_asyncMainDrainQueue` diff --git a/Sources/_CJavaScriptKit/_CJavaScriptKit.c b/Sources/_CJavaScriptKit/_CJavaScriptKit.c index ea8b5b43d..ed8240ca1 100644 --- a/Sources/_CJavaScriptKit/_CJavaScriptKit.c +++ b/Sources/_CJavaScriptKit/_CJavaScriptKit.c @@ -59,5 +59,13 @@ __attribute__((export_name("swjs_library_features"))) int swjs_library_features(void) { return _library_features(); } + +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; +} #endif #endif diff --git a/Sources/_CJavaScriptKit/include/_CJavaScriptKit.h b/Sources/_CJavaScriptKit/include/_CJavaScriptKit.h index 5cb6e6037..575c0e6fd 100644 --- a/Sources/_CJavaScriptKit/include/_CJavaScriptKit.h +++ b/Sources/_CJavaScriptKit/include/_CJavaScriptKit.h @@ -308,4 +308,13 @@ IMPORT_JS_FUNCTION(swjs_terminate_worker_thread, void, (int tid)) IMPORT_JS_FUNCTION(swjs_get_worker_thread_id, int, (void)) +int swjs_get_worker_thread_id_cached(void); + +/// Requests transferring a JavaScript object to another worker thread. +/// +/// This must be called from the destination thread of the transfer. +IMPORT_JS_FUNCTION(swjs_request_transferring_object, void, (JavaScriptObjectRef object, + int object_source_tid, + void * _Nonnull transferring)) + #endif /* _CJavaScriptKit_h */ From e406cd3663255fe1761e8d8bb8287f7b75434bc8 Mon Sep 17 00:00:00 2001 From: Yuta Saito Date: Mon, 10 Mar 2025 14:23:56 +0000 Subject: [PATCH 229/373] Stop hardcoding the Swift toolchain version in the Multithreading example --- Examples/Multithreading/README.md | 16 ++++++++++++++-- Examples/Multithreading/build.sh | 2 +- 2 files changed, 15 insertions(+), 3 deletions(-) diff --git a/Examples/Multithreading/README.md b/Examples/Multithreading/README.md index c95df2a8b..346f8cc8b 100644 --- a/Examples/Multithreading/README.md +++ b/Examples/Multithreading/README.md @@ -1,9 +1,21 @@ # Multithreading 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: +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 -$ 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-wasip1-threads.artifactbundle.zip +$ ( + 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/build.sh b/Examples/Multithreading/build.sh index 7d903b1f4..0f8670db1 100755 --- a/Examples/Multithreading/build.sh +++ b/Examples/Multithreading/build.sh @@ -1 +1 @@ -swift build --swift-sdk DEVELOPMENT-SNAPSHOT-2024-07-09-a-wasm32-unknown-wasip1-threads -Xswiftc -Xclang-linker -Xswiftc -mexec-model=reactor -Xlinker --export=__main_argc_argv -c release -Xswiftc -g +swift build --swift-sdk "${SWIFT_SDK_ID:-wasm32-unknown-wasip1-threads}" -Xswiftc -Xclang-linker -Xswiftc -mexec-model=reactor -Xlinker --export=__main_argc_argv -c release -Xswiftc -g From cfa1b2ded3bf86b0fb6ca250a5674f2d2af9c5e6 Mon Sep 17 00:00:00 2001 From: Yuta Saito Date: Mon, 10 Mar 2025 14:24:53 +0000 Subject: [PATCH 230/373] Update Multithreading example to support transferable objects --- Examples/Multithreading/Sources/JavaScript/index.js | 4 ++-- Examples/Multithreading/Sources/JavaScript/worker.js | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/Examples/Multithreading/Sources/JavaScript/index.js b/Examples/Multithreading/Sources/JavaScript/index.js index cc0c7e4e4..3cfc01a43 100644 --- a/Examples/Multithreading/Sources/JavaScript/index.js +++ b/Examples/Multithreading/Sources/JavaScript/index.js @@ -27,9 +27,9 @@ class ThreadRegistry { }; } - postMessageToWorkerThread(tid, data) { + postMessageToWorkerThread(tid, data, transfer) { const worker = this.workers.get(tid); - worker.postMessage(data); + worker.postMessage(data, transfer); } terminateWorkerThread(tid) { diff --git a/Examples/Multithreading/Sources/JavaScript/worker.js b/Examples/Multithreading/Sources/JavaScript/worker.js index eadd42bef..703df4407 100644 --- a/Examples/Multithreading/Sources/JavaScript/worker.js +++ b/Examples/Multithreading/Sources/JavaScript/worker.js @@ -5,9 +5,9 @@ self.onmessage = async (event) => { const { instance, wasi, swiftRuntime } = await instantiate({ module, threadChannel: { - postMessageToMainThread: (message) => { + postMessageToMainThread: (message, transfer) => { // Send the job to the main thread - postMessage(message); + postMessage(message, transfer); }, listenMessageFromMainThread: (listener) => { self.onmessage = (event) => listener(event.data); From 9d335a88d2048abca1dfd96e80a21c2e56c7311d Mon Sep 17 00:00:00 2001 From: Yuta Saito Date: Mon, 10 Mar 2025 14:25:18 +0000 Subject: [PATCH 231/373] Add OffscreenCanvas example --- Examples/OffscrenCanvas/.gitignore | 8 + Examples/OffscrenCanvas/Package.swift | 20 ++ Examples/OffscrenCanvas/README.md | 21 +++ Examples/OffscrenCanvas/Sources/JavaScript | 1 + .../OffscrenCanvas/Sources/MyApp/main.swift | 139 ++++++++++++++ .../OffscrenCanvas/Sources/MyApp/render.swift | 174 ++++++++++++++++++ Examples/OffscrenCanvas/build.sh | 1 + Examples/OffscrenCanvas/index.html | 98 ++++++++++ Examples/OffscrenCanvas/serve.json | 1 + 9 files changed, 463 insertions(+) create mode 100644 Examples/OffscrenCanvas/.gitignore create mode 100644 Examples/OffscrenCanvas/Package.swift create mode 100644 Examples/OffscrenCanvas/README.md create mode 120000 Examples/OffscrenCanvas/Sources/JavaScript create mode 100644 Examples/OffscrenCanvas/Sources/MyApp/main.swift create mode 100644 Examples/OffscrenCanvas/Sources/MyApp/render.swift create mode 100755 Examples/OffscrenCanvas/build.sh create mode 100644 Examples/OffscrenCanvas/index.html create mode 120000 Examples/OffscrenCanvas/serve.json diff --git a/Examples/OffscrenCanvas/.gitignore b/Examples/OffscrenCanvas/.gitignore new file mode 100644 index 000000000..0023a5340 --- /dev/null +++ b/Examples/OffscrenCanvas/.gitignore @@ -0,0 +1,8 @@ +.DS_Store +/.build +/Packages +xcuserdata/ +DerivedData/ +.swiftpm/configuration/registries.json +.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata +.netrc diff --git a/Examples/OffscrenCanvas/Package.swift b/Examples/OffscrenCanvas/Package.swift new file mode 100644 index 000000000..7fc45ad1b --- /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/JavaScript b/Examples/OffscrenCanvas/Sources/JavaScript new file mode 120000 index 000000000..b24c2256e --- /dev/null +++ b/Examples/OffscrenCanvas/Sources/JavaScript @@ -0,0 +1 @@ +../../Multithreading/Sources/JavaScript \ No newline at end of file diff --git a/Examples/OffscrenCanvas/Sources/MyApp/main.swift b/Examples/OffscrenCanvas/Sources/MyApp/main.swift new file mode 100644 index 000000000..ba660c6b2 --- /dev/null +++ b/Examples/OffscrenCanvas/Sources/MyApp/main.swift @@ -0,0 +1,139 @@ +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 transferringCanvas = JSObject.transfer(canvas) + let renderingTask = Task(executorPreference: executor) { + let canvas = try await JSObject.receive(transferringCanvas) + 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..714cac184 --- /dev/null +++ b/Examples/OffscrenCanvas/Sources/MyApp/render.swift @@ -0,0 +1,174 @@ +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 120000 index 000000000..326719cd4 --- /dev/null +++ b/Examples/OffscrenCanvas/serve.json @@ -0,0 +1 @@ +../Multithreading/serve.json \ No newline at end of file From 98cec71bec7acf7e6fbd8ba282f9d6616fc4fc48 Mon Sep 17 00:00:00 2001 From: Yuta Saito Date: Mon, 10 Mar 2025 14:56:39 +0000 Subject: [PATCH 232/373] Rename `JSObject.receive` to `JSObject.Transferring.receive` --- .../OffscrenCanvas/Sources/MyApp/main.swift | 2 +- .../JSObject+Transferring.swift | 112 +++++++++++++----- 2 files changed, 85 insertions(+), 29 deletions(-) diff --git a/Examples/OffscrenCanvas/Sources/MyApp/main.swift b/Examples/OffscrenCanvas/Sources/MyApp/main.swift index ba660c6b2..9d169f39b 100644 --- a/Examples/OffscrenCanvas/Sources/MyApp/main.swift +++ b/Examples/OffscrenCanvas/Sources/MyApp/main.swift @@ -13,7 +13,7 @@ struct BackgroundRenderer: CanvasRenderer { let executor = try await WebWorkerTaskExecutor(numberOfThreads: 1) let transferringCanvas = JSObject.transfer(canvas) let renderingTask = Task(executorPreference: executor) { - let canvas = try await JSObject.receive(transferringCanvas) + let canvas = try await transferringCanvas.receive() try await renderAnimation(canvas: canvas, size: size) } await withTaskCancellationHandler { diff --git a/Sources/JavaScriptEventLoop/JSObject+Transferring.swift b/Sources/JavaScriptEventLoop/JSObject+Transferring.swift index dce32d7ec..c1be7185b 100644 --- a/Sources/JavaScriptEventLoop/JSObject+Transferring.swift +++ b/Sources/JavaScriptEventLoop/JSObject+Transferring.swift @@ -1,60 +1,116 @@ @_spi(JSObject_id) import JavaScriptKit import _CJavaScriptKit +#if canImport(Synchronization) + import Synchronization +#endif + extension JSObject { - public class Transferring: @unchecked Sendable { - fileprivate let sourceTid: Int32 - fileprivate let idInSource: JavaScriptObjectRef - fileprivate var continuation: CheckedContinuation? = nil - - init(sourceTid: Int32, id: JavaScriptObjectRef) { - self.sourceTid = sourceTid - self.idInSource = id + + /// A temporary object intended to transfer a ``JSObject`` from one thread to another. + /// + /// ``JSObject`` itself is not `Sendable`, but ``Transferring`` is `Sendable` because it's + /// intended to be shared across threads. + public struct Transferring: @unchecked Sendable { + fileprivate struct CriticalState { + var continuation: CheckedContinuation? + } + fileprivate class Storage { + let sourceTid: Int32 + let idInSource: JavaScriptObjectRef + #if compiler(>=6.1) && _runtime(_multithreaded) + let criticalState: Mutex = .init(CriticalState()) + #endif + + init(sourceTid: Int32, id: JavaScriptObjectRef) { + self.sourceTid = sourceTid + self.idInSource = id + } + } + + private let storage: Storage + + fileprivate init(sourceTid: Int32, id: JavaScriptObjectRef) { + self.init(storage: Storage(sourceTid: sourceTid, id: id)) } - func receive(isolation: isolated (any Actor)?) async throws -> JSObject { + fileprivate init(storage: Storage) { + self.storage = storage + } + + /// Receives a transferred ``JSObject`` from a thread. + /// + /// The original ``JSObject`` is ["Transferred"](https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API/Transferable_objects) + /// to the receiving thread. + /// + /// Note that this method should be called only once for each ``Transferring`` instance + /// on the receiving thread. + /// + /// ### Example + /// + /// ```swift + /// let canvas = JSObject.global.document.createElement("canvas").object! + /// let transferring = JSObject.transfer(canvas.transferControlToOffscreen().object!) + /// let executor = try await WebWorkerTaskExecutor(numberOfThreads: 1) + /// Task(executorPreference: executor) { + /// let canvas = try await transferring.receive() + /// } + /// ``` + public func receive(isolation: isolated (any Actor)? = #isolation, file: StaticString = #file, line: UInt = #line) async throws -> JSObject { #if compiler(>=6.1) && _runtime(_multithreaded) swjs_request_transferring_object( - idInSource, - sourceTid, - Unmanaged.passRetained(self).toOpaque() + self.storage.idInSource, + self.storage.sourceTid, + Unmanaged.passRetained(self.storage).toOpaque() ) return try await withCheckedThrowingContinuation { continuation in - self.continuation = continuation + self.storage.criticalState.withLock { criticalState in + guard criticalState.continuation == nil else { + // This is a programming error, `receive` should be called only once. + fatalError("JSObject.Transferring object is already received", file: file, line: line) + } + criticalState.continuation = continuation + } } #else - return JSObject(id: idInSource) + return JSObject(id: storage.idInSource) #endif } } - /// Transfers the ownership of a `JSObject` to be sent to another Worker. + /// Transfers the ownership of a `JSObject` to be sent to another thread. + /// + /// Note that the original ``JSObject`` should not be accessed after calling this method. /// - /// - Parameter object: The `JSObject` to be transferred. - /// - Returns: A `JSTransferring` instance that can be shared across worker threads. - /// - Note: The original `JSObject` should not be accessed after calling this method. + /// - Parameter object: The ``JSObject`` to be transferred. + /// - Returns: A ``Transferring`` instance that can be shared across threads. public static func transfer(_ object: JSObject) -> Transferring { #if compiler(>=6.1) && _runtime(_multithreaded) Transferring(sourceTid: object.ownerTid, id: object.id) #else + // On single-threaded runtime, source and destination threads are always the main thread (TID = -1). Transferring(sourceTid: -1, id: object.id) #endif } - - /// Receives a transferred `JSObject` from a Worker. - /// - /// - Parameter transferring: The `JSTransferring` instance received from other worker threads. - /// - Returns: The reconstructed `JSObject` that can be used in the receiving Worker. - public static func receive(_ transferring: Transferring, isolation: isolated (any Actor)? = #isolation) async throws -> JSObject { - try await transferring.receive(isolation: isolation) - } } + +/// 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. +/// - transferring: A pointer to the `Transferring.Storage` instance. #if compiler(>=6.1) // @_expose and @_extern are only available in Swift 6.1+ @_expose(wasm, "swjs_receive_object") @_cdecl("swjs_receive_object") #endif func _swjs_receive_object(_ object: JavaScriptObjectRef, _ transferring: UnsafeRawPointer) { - let transferring = Unmanaged.fromOpaque(transferring).takeRetainedValue() - transferring.continuation?.resume(returning: JSObject(id: object)) + #if compiler(>=6.1) && _runtime(_multithreaded) + let storage = Unmanaged.fromOpaque(transferring).takeRetainedValue() + storage.criticalState.withLock { criticalState in + assert(criticalState.continuation != nil, "JSObject.Transferring object is not yet received!?") + criticalState.continuation?.resume(returning: JSObject(id: object)) + } + #endif } From 9b84176c44c9b1ba7af222633bdf52ed8d8fb7a4 Mon Sep 17 00:00:00 2001 From: Yuta Saito Date: Mon, 10 Mar 2025 15:36:04 +0000 Subject: [PATCH 233/373] Update test harness to support transferring --- IntegrationTests/lib.js | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/IntegrationTests/lib.js b/IntegrationTests/lib.js index 0172250d4..a2f10e565 100644 --- a/IntegrationTests/lib.js +++ b/IntegrationTests/lib.js @@ -79,7 +79,9 @@ export async function startWasiChildThread(event) { const swift = new SwiftRuntime({ sharedMemory: true, threadChannel: { - postMessageToMainThread: parentPort.postMessage.bind(parentPort), + postMessageToMainThread: (message, transfer) => { + parentPort.postMessage(message, transfer); + }, listenMessageFromMainThread: (listener) => { parentPort.on("message", listener) } @@ -139,9 +141,9 @@ class ThreadRegistry { return this.workers.get(tid); } - wakeUpWorkerThread(tid, message) { + wakeUpWorkerThread(tid, message, transfer) { const worker = this.workers.get(tid); - worker.postMessage(message); + worker.postMessage(message, transfer); } } From c4816141c529318bfdff8fe71a8e4e4d44eef154 Mon Sep 17 00:00:00 2001 From: Yuta Saito Date: Mon, 10 Mar 2025 15:36:32 +0000 Subject: [PATCH 234/373] Fix JSObject lifetime issue while transferring --- .../JSObject+Transferring.swift | 42 ++++++++++++------- 1 file changed, 26 insertions(+), 16 deletions(-) diff --git a/Sources/JavaScriptEventLoop/JSObject+Transferring.swift b/Sources/JavaScriptEventLoop/JSObject+Transferring.swift index c1be7185b..c6d5b14cb 100644 --- a/Sources/JavaScriptEventLoop/JSObject+Transferring.swift +++ b/Sources/JavaScriptEventLoop/JSObject+Transferring.swift @@ -13,25 +13,39 @@ extension JSObject { /// intended to be shared across threads. public struct Transferring: @unchecked Sendable { fileprivate struct CriticalState { - var continuation: CheckedContinuation? + var continuation: CheckedContinuation? } fileprivate class Storage { - let sourceTid: Int32 - let idInSource: JavaScriptObjectRef + /// The original ``JSObject`` that is transferred. + /// + /// Retain it here to prevent it from being released before the transfer is complete. + let sourceObject: JSObject #if compiler(>=6.1) && _runtime(_multithreaded) let criticalState: Mutex = .init(CriticalState()) #endif - init(sourceTid: Int32, id: JavaScriptObjectRef) { - self.sourceTid = sourceTid - self.idInSource = id + var idInSource: JavaScriptObjectRef { + sourceObject.id + } + + var sourceTid: Int32 { + #if compiler(>=6.1) && _runtime(_multithreaded) + sourceObject.ownerTid + #else + // On single-threaded runtime, source and destination threads are always the main thread (TID = -1). + -1 + #endif + } + + init(sourceObject: JSObject) { + self.sourceObject = sourceObject } } private let storage: Storage - fileprivate init(sourceTid: Int32, id: JavaScriptObjectRef) { - self.init(storage: Storage(sourceTid: sourceTid, id: id)) + fileprivate init(sourceObject: JSObject) { + self.init(storage: Storage(sourceObject: sourceObject)) } fileprivate init(storage: Storage) { @@ -63,7 +77,7 @@ extension JSObject { self.storage.sourceTid, Unmanaged.passRetained(self.storage).toOpaque() ) - return try await withCheckedThrowingContinuation { continuation in + let idInDestination = try await withCheckedThrowingContinuation { continuation in self.storage.criticalState.withLock { criticalState in guard criticalState.continuation == nil else { // This is a programming error, `receive` should be called only once. @@ -72,6 +86,7 @@ extension JSObject { criticalState.continuation = continuation } } + return JSObject(id: idInDestination) #else return JSObject(id: storage.idInSource) #endif @@ -85,12 +100,7 @@ extension JSObject { /// - Parameter object: The ``JSObject`` to be transferred. /// - Returns: A ``Transferring`` instance that can be shared across threads. public static func transfer(_ object: JSObject) -> Transferring { - #if compiler(>=6.1) && _runtime(_multithreaded) - Transferring(sourceTid: object.ownerTid, id: object.id) - #else - // On single-threaded runtime, source and destination threads are always the main thread (TID = -1). - Transferring(sourceTid: -1, id: object.id) - #endif + return Transferring(sourceObject: object) } } @@ -110,7 +120,7 @@ func _swjs_receive_object(_ object: JavaScriptObjectRef, _ transferring: UnsafeR let storage = Unmanaged.fromOpaque(transferring).takeRetainedValue() storage.criticalState.withLock { criticalState in assert(criticalState.continuation != nil, "JSObject.Transferring object is not yet received!?") - criticalState.continuation?.resume(returning: JSObject(id: object)) + criticalState.continuation?.resume(returning: object) } #endif } From 65ddcd36b318aee5f973ac82ef6658f1c62d7520 Mon Sep 17 00:00:00 2001 From: Yuta Saito Date: Mon, 10 Mar 2025 15:36:46 +0000 Subject: [PATCH 235/373] Add basic tests for transferring objects between threads --- .../WebWorkerTaskExecutorTests.swift | 33 ++++++++++++++++++- 1 file changed, 32 insertions(+), 1 deletion(-) diff --git a/Tests/JavaScriptEventLoopTests/WebWorkerTaskExecutorTests.swift b/Tests/JavaScriptEventLoopTests/WebWorkerTaskExecutorTests.swift index 3848ba4cc..7d79c39fa 100644 --- a/Tests/JavaScriptEventLoopTests/WebWorkerTaskExecutorTests.swift +++ b/Tests/JavaScriptEventLoopTests/WebWorkerTaskExecutorTests.swift @@ -9,7 +9,7 @@ func isMainThread() -> Bool final class WebWorkerTaskExecutorTests: XCTestCase { override func setUp() async { - await WebWorkerTaskExecutor.installGlobalExecutor() + WebWorkerTaskExecutor.installGlobalExecutor() } func testTaskRunOnMainThread() async throws { @@ -264,6 +264,37 @@ final class WebWorkerTaskExecutorTests: XCTestCase { executor.terminate() } + func testTransfer() async throws { + let Uint8Array = JSObject.global.Uint8Array.function! + let buffer = Uint8Array.new(100).buffer.object! + let transferring = JSObject.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) + // Deinit the transferring object on the thread that was created + withExtendedLifetime(transferring) {} + } + + func testTransferNonTransferable() async throws { + let object = JSObject.global.Object.function!.new() + let transferring = JSObject.transfer(object) + let executor = try await WebWorkerTaskExecutor(numberOfThreads: 1) + let task = Task(executorPreference: executor) { + _ = try await transferring.receive() + return + } + do { + try await task.value + XCTFail("Should throw an error") + } catch {} + // Deinit the transferring object on the thread that was created + withExtendedLifetime(transferring) {} + } + /* func testDeinitJSObjectOnDifferentThread() async throws { let executor = try await WebWorkerTaskExecutor(numberOfThreads: 1) From f0bd60cd9315158f5f5a44750de9f1245457eefc Mon Sep 17 00:00:00 2001 From: Yuta Saito Date: Mon, 10 Mar 2025 15:38:14 +0000 Subject: [PATCH 236/373] Fix native build --- Sources/JavaScriptEventLoop/JSObject+Transferring.swift | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/Sources/JavaScriptEventLoop/JSObject+Transferring.swift b/Sources/JavaScriptEventLoop/JSObject+Transferring.swift index c6d5b14cb..0bab8bd0f 100644 --- a/Sources/JavaScriptEventLoop/JSObject+Transferring.swift +++ b/Sources/JavaScriptEventLoop/JSObject+Transferring.swift @@ -11,6 +11,7 @@ extension JSObject { /// /// ``JSObject`` itself is not `Sendable`, but ``Transferring`` is `Sendable` because it's /// intended to be shared across threads. + @available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) public struct Transferring: @unchecked Sendable { fileprivate struct CriticalState { var continuation: CheckedContinuation? @@ -70,6 +71,7 @@ extension JSObject { /// let canvas = try await transferring.receive() /// } /// ``` + @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 -> JSObject { #if compiler(>=6.1) && _runtime(_multithreaded) swjs_request_transferring_object( @@ -99,6 +101,7 @@ extension JSObject { /// /// - Parameter object: The ``JSObject`` to be transferred. /// - Returns: A ``Transferring`` 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) -> Transferring { return Transferring(sourceObject: object) } @@ -115,6 +118,7 @@ extension JSObject { @_expose(wasm, "swjs_receive_object") @_cdecl("swjs_receive_object") #endif +@available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) func _swjs_receive_object(_ object: JavaScriptObjectRef, _ transferring: UnsafeRawPointer) { #if compiler(>=6.1) && _runtime(_multithreaded) let storage = Unmanaged.fromOpaque(transferring).takeRetainedValue() From 8d4bba6188826ff5ab6059fb37cb96c3cd34de28 Mon Sep 17 00:00:00 2001 From: Yuta Saito Date: Mon, 10 Mar 2025 15:55:43 +0000 Subject: [PATCH 237/373] Add cautionary notes to the documentation of `JSObject.transfer()`. --- Sources/JavaScriptEventLoop/JSObject+Transferring.swift | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Sources/JavaScriptEventLoop/JSObject+Transferring.swift b/Sources/JavaScriptEventLoop/JSObject+Transferring.swift index 0bab8bd0f..859587f31 100644 --- a/Sources/JavaScriptEventLoop/JSObject+Transferring.swift +++ b/Sources/JavaScriptEventLoop/JSObject+Transferring.swift @@ -97,7 +97,9 @@ extension JSObject { /// Transfers the ownership of a `JSObject` to be sent to another thread. /// - /// Note that the original ``JSObject`` should not be accessed after calling this method. + /// - 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 ``Transferring`` instance that can be shared across threads. From 09d5311dcf5d6c3206f448b5eee4661ef85b24b9 Mon Sep 17 00:00:00 2001 From: Yuta Saito Date: Tue, 11 Mar 2025 02:26:23 +0000 Subject: [PATCH 238/373] Rename `JSObject.Transferring` to `JSTransferring` This change makes the transferring object to be used with a typed object like `JSDate` or something else in the future. --- .../OffscrenCanvas/Sources/MyApp/main.swift | 4 +- .../JSObject+Transferring.swift | 195 ++++++++++-------- .../WebWorkerTaskExecutorTests.swift | 4 +- 3 files changed, 115 insertions(+), 88 deletions(-) diff --git a/Examples/OffscrenCanvas/Sources/MyApp/main.swift b/Examples/OffscrenCanvas/Sources/MyApp/main.swift index 9d169f39b..b6e5b6df9 100644 --- a/Examples/OffscrenCanvas/Sources/MyApp/main.swift +++ b/Examples/OffscrenCanvas/Sources/MyApp/main.swift @@ -11,9 +11,9 @@ protocol CanvasRenderer { struct BackgroundRenderer: CanvasRenderer { func render(canvas: JSObject, size: Int) async throws { let executor = try await WebWorkerTaskExecutor(numberOfThreads: 1) - let transferringCanvas = JSObject.transfer(canvas) + let transfer = JSTransferring(canvas) let renderingTask = Task(executorPreference: executor) { - let canvas = try await transferringCanvas.receive() + let canvas = try await transfer.receive() try await renderAnimation(canvas: canvas, size: size) } await withTaskCancellationHandler { diff --git a/Sources/JavaScriptEventLoop/JSObject+Transferring.swift b/Sources/JavaScriptEventLoop/JSObject+Transferring.swift index 859587f31..58f9aaf5b 100644 --- a/Sources/JavaScriptEventLoop/JSObject+Transferring.swift +++ b/Sources/JavaScriptEventLoop/JSObject+Transferring.swift @@ -5,95 +5,109 @@ import _CJavaScriptKit import Synchronization #endif -extension JSObject { +/// A temporary object intended to transfer an object from one thread to another. +/// +/// ``JSTransferring`` is `Sendable` and it's intended to be shared across threads. +@available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) +public struct JSTransferring: @unchecked Sendable { + fileprivate struct Storage { + /// The original object that is transferred. + /// + /// Retain it here to prevent it from being released before the transfer is complete. + let sourceObject: T + /// A function that constructs an object from a JavaScript object reference. + let construct: (_ id: JavaScriptObjectRef) -> 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 - /// A temporary object intended to transfer a ``JSObject`` from one thread to another. - /// - /// ``JSObject`` itself is not `Sendable`, but ``Transferring`` is `Sendable` because it's - /// intended to be shared across threads. - @available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) - public struct Transferring: @unchecked Sendable { - fileprivate struct CriticalState { - var continuation: CheckedContinuation? - } - fileprivate class Storage { - /// The original ``JSObject`` that is transferred. - /// - /// Retain it here to prevent it from being released before the transfer is complete. - let sourceObject: JSObject - #if compiler(>=6.1) && _runtime(_multithreaded) - let criticalState: Mutex = .init(CriticalState()) - #endif + #if compiler(>=6.1) && _runtime(_multithreaded) + /// A shared context for transferring objects across threads. + let context: _JSTransferringContext = _JSTransferringContext() + #endif + } - var idInSource: JavaScriptObjectRef { - sourceObject.id - } + private let storage: Storage - var sourceTid: Int32 { - #if compiler(>=6.1) && _runtime(_multithreaded) - sourceObject.ownerTid - #else - // On single-threaded runtime, source and destination threads are always the main thread (TID = -1). - -1 - #endif - } + fileprivate init( + sourceObject: T, + construct: @escaping (_ id: JavaScriptObjectRef) -> T, + deconstruct: @escaping (_ object: T) -> JavaScriptObjectRef, + getSourceTid: @escaping (_ object: T) -> Int32 + ) { + self.storage = Storage( + sourceObject: sourceObject, + construct: construct, + idInSource: deconstruct(sourceObject), + sourceTid: getSourceTid(sourceObject) + ) + } - init(sourceObject: JSObject) { - self.sourceObject = sourceObject + /// Receives a transferred ``JSObject`` from a thread. + /// + /// The original ``JSObject`` is ["Transferred"](https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API/Transferable_objects) + /// to the receiving thread. + /// + /// Note that this method should be called only once for each ``Transferring`` instance + /// on the receiving thread. + /// + /// ### Example + /// + /// ```swift + /// let canvas = JSObject.global.document.createElement("canvas").object! + /// let transferring = JSObject.transfer(canvas.transferControlToOffscreen().object!) + /// let executor = try await WebWorkerTaskExecutor(numberOfThreads: 1) + /// Task(executorPreference: executor) { + /// let canvas = try await transferring.receive() + /// } + /// ``` + @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) + // The following sequence of events happens when a `JSObject` is transferred from + // the owner thread to the receiver thread: + // + // [Owner Thread] [Receiver Thread] + // <-----requestTransfer------ swjs_request_transferring_object + // ---------transfer---------> swjs_receive_object + let idInDestination = try await withCheckedThrowingContinuation { continuation in + self.storage.context.withLock { context in + guard context.continuation == nil else { + // This is a programming error, `receive` should be called only once. + fatalError("JSObject.Transferring object is already received", file: file, line: line) + } + // The continuation will be resumed by `swjs_receive_object`. + context.continuation = continuation } - } - - private let storage: Storage - - fileprivate init(sourceObject: JSObject) { - self.init(storage: Storage(sourceObject: sourceObject)) - } - - fileprivate init(storage: Storage) { - self.storage = storage - } - - /// Receives a transferred ``JSObject`` from a thread. - /// - /// The original ``JSObject`` is ["Transferred"](https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API/Transferable_objects) - /// to the receiving thread. - /// - /// Note that this method should be called only once for each ``Transferring`` instance - /// on the receiving thread. - /// - /// ### Example - /// - /// ```swift - /// let canvas = JSObject.global.document.createElement("canvas").object! - /// let transferring = JSObject.transfer(canvas.transferControlToOffscreen().object!) - /// let executor = try await WebWorkerTaskExecutor(numberOfThreads: 1) - /// Task(executorPreference: executor) { - /// let canvas = try await transferring.receive() - /// } - /// ``` - @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 -> JSObject { - #if compiler(>=6.1) && _runtime(_multithreaded) swjs_request_transferring_object( self.storage.idInSource, self.storage.sourceTid, - Unmanaged.passRetained(self.storage).toOpaque() + Unmanaged.passRetained(self.storage.context).toOpaque() ) - let idInDestination = try await withCheckedThrowingContinuation { continuation in - self.storage.criticalState.withLock { criticalState in - guard criticalState.continuation == nil else { - // This is a programming error, `receive` should be called only once. - fatalError("JSObject.Transferring object is already received", file: file, line: line) - } - criticalState.continuation = continuation - } - } - return JSObject(id: idInDestination) - #else - return JSObject(id: storage.idInSource) - #endif } + return storage.construct(idInDestination) + #else + return storage.construct(storage.idInSource) + #endif + } +} + +fileprivate final class _JSTransferringContext: Sendable { + struct State { + var continuation: CheckedContinuation? } + private let state: Mutex = .init(State()) + + func withLock(_ body: (inout State) -> R) -> R { + return state.withLock { state in + body(&state) + } + } +} + + +extension JSTransferring where T == JSObject { /// Transfers the ownership of a `JSObject` to be sent to another thread. /// @@ -104,8 +118,21 @@ extension JSObject { /// - Parameter object: The ``JSObject`` to be transferred. /// - Returns: A ``Transferring`` 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) -> Transferring { - return Transferring(sourceObject: object) + public init(_ object: JSObject) { + self.init( + sourceObject: object, + construct: { JSObject(id: $0) }, + deconstruct: { $0.id }, + 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 + } + ) } } @@ -123,10 +150,10 @@ extension JSObject { @available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) func _swjs_receive_object(_ object: JavaScriptObjectRef, _ transferring: UnsafeRawPointer) { #if compiler(>=6.1) && _runtime(_multithreaded) - let storage = Unmanaged.fromOpaque(transferring).takeRetainedValue() - storage.criticalState.withLock { criticalState in - assert(criticalState.continuation != nil, "JSObject.Transferring object is not yet received!?") - criticalState.continuation?.resume(returning: object) + let context = Unmanaged<_JSTransferringContext>.fromOpaque(transferring).takeRetainedValue() + context.withLock { state in + assert(state.continuation != nil, "JSObject.Transferring object is not yet received!?") + state.continuation?.resume(returning: object) } #endif } diff --git a/Tests/JavaScriptEventLoopTests/WebWorkerTaskExecutorTests.swift b/Tests/JavaScriptEventLoopTests/WebWorkerTaskExecutorTests.swift index 7d79c39fa..4892df591 100644 --- a/Tests/JavaScriptEventLoopTests/WebWorkerTaskExecutorTests.swift +++ b/Tests/JavaScriptEventLoopTests/WebWorkerTaskExecutorTests.swift @@ -267,7 +267,7 @@ final class WebWorkerTaskExecutorTests: XCTestCase { func testTransfer() async throws { let Uint8Array = JSObject.global.Uint8Array.function! let buffer = Uint8Array.new(100).buffer.object! - let transferring = JSObject.transfer(buffer) + let transferring = JSTransferring(buffer) let executor = try await WebWorkerTaskExecutor(numberOfThreads: 1) let task = Task(executorPreference: executor) { let buffer = try await transferring.receive() @@ -281,7 +281,7 @@ final class WebWorkerTaskExecutorTests: XCTestCase { func testTransferNonTransferable() async throws { let object = JSObject.global.Object.function!.new() - let transferring = JSObject.transfer(object) + let transferring = JSTransferring(object) let executor = try await WebWorkerTaskExecutor(numberOfThreads: 1) let task = Task(executorPreference: executor) { _ = try await transferring.receive() From f25bfec40071881d648038eb9fd41f5f99a57035 Mon Sep 17 00:00:00 2001 From: Yuta Saito Date: Tue, 11 Mar 2025 04:33:50 +0000 Subject: [PATCH 239/373] MessageBroker --- Runtime/src/index.ts | 266 +++++------------- Runtime/src/itc.ts | 235 ++++++++++++++++ Runtime/src/js-value.ts | 76 +++++ Runtime/src/types.ts | 3 +- .../JSObject+Transferring.swift | 27 +- Sources/JavaScriptKit/Runtime/index.js | 251 +++++++++++------ Sources/JavaScriptKit/Runtime/index.mjs | 251 +++++++++++------ .../WebWorkerTaskExecutorTests.swift | 17 +- 8 files changed, 752 insertions(+), 374 deletions(-) create mode 100644 Runtime/src/itc.ts diff --git a/Runtime/src/index.ts b/Runtime/src/index.ts index 25d6e92f5..5cb1acfc2 100644 --- a/Runtime/src/index.ts +++ b/Runtime/src/index.ts @@ -10,120 +10,7 @@ import { } from "./types.js"; import * as JSValue from "./js-value.js"; import { Memory } from "./memory.js"; - -type TransferMessage = { - type: "transfer"; - data: { - object: any; - transferring: pointer; - destinationTid: number; - }; -}; - -type RequestTransferMessage = { - type: "requestTransfer"; - data: { - objectRef: ref; - objectSourceTid: number; - transferring: pointer; - destinationTid: number; - }; -}; - -type TransferErrorMessage = { - type: "transferError"; - data: { - error: string; - }; -}; - -type MainToWorkerMessage = { - type: "wake"; -} | RequestTransferMessage | TransferMessage | TransferErrorMessage; - -type WorkerToMainMessage = { - type: "job"; - data: number; -} | RequestTransferMessage | TransferMessage | TransferErrorMessage; - -/** - * 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; - }; +import { deserializeError, MainToWorkerMessage, MessageBroker, ResponseMessage, ITCInterface, serializeError, SwiftRuntimeThreadChannel, WorkerToMainMessage } from "./itc.js"; export type SwiftRuntimeOptions = { /** @@ -294,6 +181,51 @@ export class SwiftRuntime { importObjects = () => this.wasmImports; get wasmImports(): ImportedFunctions { + 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 { + 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 response 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, @@ -634,38 +566,18 @@ export class SwiftRuntime { "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 "requestTransfer": { - const object = this.memory.getObject(message.data.objectRef); - const messageToMainThread: TransferMessage = { - type: "transfer", - data: { - object, - destinationTid: message.data.destinationTid, - transferring: message.data.transferring, - }, - }; - try { - this.postMessageToMainThread(messageToMainThread, [object]); - } catch (error) { - this.postMessageToMainThread({ - type: "transferError", - data: { error: String(error) }, - }); - } - break; - } - case "transfer": { - const objectRef = this.memory.retain(message.data.object); - this.exports.swjs_receive_object(objectRef, message.data.transferring); + case "request": { + broker.onReceivingRequest(message); break; } - case "transferError": { - console.error(message.data.error); // TODO: Handle the error + case "response": { + broker.onReceivingResponse(message); break; } default: @@ -684,59 +596,19 @@ export class SwiftRuntime { "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 "requestTransfer": { - if (message.data.objectSourceTid == MAIN_THREAD_TID) { - const object = this.memory.getObject(message.data.objectRef); - if (message.data.destinationTid != tid) { - throw new Error("Invariant violation: The destination tid of the transfer request must be the same as the tid of the worker thread that received the request."); - } - this.postMessageToWorkerThread(message.data.destinationTid, { - type: "transfer", - data: { - object, - transferring: message.data.transferring, - destinationTid: message.data.destinationTid, - }, - }, [object]); - } else { - // Proxy the transfer request to the worker thread that owns the object - this.postMessageToWorkerThread(message.data.objectSourceTid, { - type: "requestTransfer", - data: { - objectRef: message.data.objectRef, - objectSourceTid: tid, - transferring: message.data.transferring, - destinationTid: message.data.destinationTid, - }, - }); - } + case "request": { + broker.onReceivingRequest(message); break; } - case "transfer": { - if (message.data.destinationTid == MAIN_THREAD_TID) { - const objectRef = this.memory.retain(message.data.object); - this.exports.swjs_receive_object(objectRef, message.data.transferring); - } else { - // Proxy the transfer response to the destination worker thread - this.postMessageToWorkerThread(message.data.destinationTid, { - type: "transfer", - data: { - object: message.data.object, - transferring: message.data.transferring, - destinationTid: message.data.destinationTid, - }, - }, [message.data.object]); - } - break; - } - case "transferError": { - console.error(message.data.error); // TODO: Handle the error + case "response": { + broker.onReceivingResponse(message); break; } default: @@ -761,20 +633,22 @@ export class SwiftRuntime { object_source_tid: number, transferring: pointer, ) => { - if (this.tid == object_source_tid) { - // Fast path: The object is already in the same thread - this.exports.swjs_receive_object(object_ref, transferring); - return; + if (!this.options.threadChannel) { + throw new Error("threadChannel is not set in options given to SwiftRuntime. Please set it to request transferring objects."); } - this.postMessageToMainThread({ - type: "requestTransfer", + const broker = getMessageBroker(this.options.threadChannel); + broker.request({ + type: "request", data: { - objectRef: object_ref, - objectSourceTid: object_source_tid, - transferring, - destinationTid: this.tid ?? MAIN_THREAD_TID, - }, - }); + sourceTid: this.tid ?? MAIN_THREAD_TID, + targetTid: object_source_tid, + context: transferring, + request: { + method: "transfer", + parameters: [object_ref, transferring], + } + } + }) }, }; } diff --git a/Runtime/src/itc.ts b/Runtime/src/itc.ts new file mode 100644 index 000000000..44b37c7be --- /dev/null +++ b/Runtime/src/itc.ts @@ -0,0 +1,235 @@ +// 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) {} + + transfer(objectRef: ref, transferring: pointer): { object: any, transferring: pointer, transfer: Transferable[] } { + const object = this.memory.getObject(objectRef); + return { object, transferring, transfer: [object] }; + } +} + +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 1b142de05..29e4a42a4 100644 --- a/Runtime/src/js-value.ts +++ b/Runtime/src/js-value.ts @@ -92,6 +92,82 @@ export const write = ( memory.writeUint32(kind_ptr, kind); }; +export function decompose(value: any, memory: Memory): { + kind: JavaScriptValueKindAndFlags; + payload1: number; + payload2: number; +} { + if (value === null) { + return { + kind: Kind.Null, + payload1: 0, + payload2: 0, + } + } + const type = typeof value; + switch (type) { + case "boolean": { + return { + kind: Kind.Boolean, + payload1: value ? 1 : 0, + payload2: 0, + } + } + case "number": { + return { + kind: Kind.Number, + payload1: 0, + payload2: value, + } + } + case "string": { + return { + kind: Kind.String, + payload1: memory.retain(value), + payload2: 0, + } + } + case "undefined": { + return { + kind: Kind.Undefined, + payload1: 0, + payload2: 0, + } + } + case "object": { + return { + kind: Kind.Object, + payload1: memory.retain(value), + payload2: 0, + } + } + case "function": { + return { + kind: Kind.Function, + payload1: memory.retain(value), + payload2: 0, + } + } + case "symbol": { + return { + kind: Kind.Symbol, + payload1: memory.retain(value), + payload2: 0, + } + } + case "bigint": { + return { + kind: Kind.BigInt, + payload1: memory.retain(value), + payload2: 0, + } + } + default: + assertNever(type, `Type "${type}" is not supported yet`); + } + throw new Error("unreachable"); +} + export const writeAndReturnKindBits = ( value: any, payload1_ptr: pointer, diff --git a/Runtime/src/types.ts b/Runtime/src/types.ts index 4e311ef80..a83a74f0c 100644 --- a/Runtime/src/types.ts +++ b/Runtime/src/types.ts @@ -22,7 +22,8 @@ export interface ExportedFunctions { swjs_enqueue_main_job_from_worker(unowned_job: number): void; swjs_wake_worker_thread(): void; - swjs_receive_object(object: ref, transferring: pointer): void; + swjs_receive_response(object: ref, transferring: pointer): void; + swjs_receive_error(error: ref, context: number): void; } export interface ImportedFunctions { diff --git a/Sources/JavaScriptEventLoop/JSObject+Transferring.swift b/Sources/JavaScriptEventLoop/JSObject+Transferring.swift index 58f9aaf5b..6deee6598 100644 --- a/Sources/JavaScriptEventLoop/JSObject+Transferring.swift +++ b/Sources/JavaScriptEventLoop/JSObject+Transferring.swift @@ -144,11 +144,11 @@ extension JSTransferring where T == JSObject { /// - object: The `JSObject` to be received. /// - transferring: A pointer to the `Transferring.Storage` instance. #if compiler(>=6.1) // @_expose and @_extern are only available in Swift 6.1+ -@_expose(wasm, "swjs_receive_object") -@_cdecl("swjs_receive_object") +@_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_object(_ object: JavaScriptObjectRef, _ transferring: UnsafeRawPointer) { +func _swjs_receive_response(_ object: JavaScriptObjectRef, _ transferring: UnsafeRawPointer) { #if compiler(>=6.1) && _runtime(_multithreaded) let context = Unmanaged<_JSTransferringContext>.fromOpaque(transferring).takeRetainedValue() context.withLock { state in @@ -157,3 +157,24 @@ func _swjs_receive_object(_ object: JavaScriptObjectRef, _ transferring: UnsafeR } #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. +/// - transferring: A pointer to the `Transferring.Storage` instance. +#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, _ transferring: UnsafeRawPointer) { + #if compiler(>=6.1) && _runtime(_multithreaded) + let context = Unmanaged<_JSTransferringContext>.fromOpaque(transferring).takeRetainedValue() + context.withLock { state in + assert(state.continuation != nil, "JSObject.Transferring object is not yet received!?") + state.continuation?.resume(throwing: JSException(JSObject(id: error).jsValue)) + } + #endif +} diff --git a/Sources/JavaScriptKit/Runtime/index.js b/Sources/JavaScriptKit/Runtime/index.js index 8027593e5..206251a11 100644 --- a/Sources/JavaScriptKit/Runtime/index.js +++ b/Sources/JavaScriptKit/Runtime/index.js @@ -196,6 +196,100 @@ } } + class ITCInterface { + constructor(memory) { + this.memory = memory; + } + transfer(objectRef, transferring) { + const object = this.memory.getObject(objectRef); + return { object, transferring, transfer: [object] }; + } + } + 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; @@ -313,6 +407,56 @@ 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 { + 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 response 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; @@ -508,39 +652,18 @@ 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 "requestTransfer": { - const object = this.memory.getObject(message.data.objectRef); - const messageToMainThread = { - type: "transfer", - data: { - object, - destinationTid: message.data.destinationTid, - transferring: message.data.transferring, - }, - }; - try { - this.postMessageToMainThread(messageToMainThread, [object]); - } - catch (error) { - this.postMessageToMainThread({ - type: "transferError", - data: { error: String(error) }, - }); - } - break; - } - case "transfer": { - const objectRef = this.memory.retain(message.data.object); - this.exports.swjs_receive_object(objectRef, message.data.transferring); + case "request": { + broker.onReceivingRequest(message); break; } - case "transferError": { - console.error(message.data.error); // TODO: Handle the error + case "response": { + broker.onReceivingResponse(message); break; } default: @@ -557,60 +680,18 @@ 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 "requestTransfer": { - if (message.data.objectSourceTid == MAIN_THREAD_TID) { - const object = this.memory.getObject(message.data.objectRef); - if (message.data.destinationTid != tid) { - throw new Error("Invariant violation: The destination tid of the transfer request must be the same as the tid of the worker thread that received the request."); - } - this.postMessageToWorkerThread(message.data.destinationTid, { - type: "transfer", - data: { - object, - transferring: message.data.transferring, - destinationTid: message.data.destinationTid, - }, - }, [object]); - } - else { - // Proxy the transfer request to the worker thread that owns the object - this.postMessageToWorkerThread(message.data.objectSourceTid, { - type: "requestTransfer", - data: { - objectRef: message.data.objectRef, - objectSourceTid: tid, - transferring: message.data.transferring, - destinationTid: message.data.destinationTid, - }, - }); - } - break; - } - case "transfer": { - if (message.data.destinationTid == MAIN_THREAD_TID) { - const objectRef = this.memory.retain(message.data.object); - this.exports.swjs_receive_object(objectRef, message.data.transferring); - } - else { - // Proxy the transfer response to the destination worker thread - this.postMessageToWorkerThread(message.data.destinationTid, { - type: "transfer", - data: { - object: message.data.object, - transferring: message.data.transferring, - destinationTid: message.data.destinationTid, - }, - }, [message.data.object]); - } + case "request": { + broker.onReceivingRequest(message); break; } - case "transferError": { - console.error(message.data.error); // TODO: Handle the error + case "response": { + broker.onReceivingResponse(message); break; } default: @@ -632,19 +713,21 @@ }, swjs_request_transferring_object: (object_ref, object_source_tid, transferring) => { var _a; - if (this.tid == object_source_tid) { - // Fast path: The object is already in the same thread - this.exports.swjs_receive_object(object_ref, transferring); - return; + if (!this.options.threadChannel) { + throw new Error("threadChannel is not set in options given to SwiftRuntime. Please set it to request transferring objects."); } - this.postMessageToMainThread({ - type: "requestTransfer", + const broker = getMessageBroker(this.options.threadChannel); + broker.request({ + type: "request", data: { - objectRef: object_ref, - objectSourceTid: object_source_tid, - transferring, - destinationTid: (_a = this.tid) !== null && _a !== void 0 ? _a : MAIN_THREAD_TID, - }, + sourceTid: (_a = this.tid) !== null && _a !== void 0 ? _a : MAIN_THREAD_TID, + targetTid: object_source_tid, + context: transferring, + request: { + method: "transfer", + parameters: [object_ref, transferring], + } + } }); }, }; diff --git a/Sources/JavaScriptKit/Runtime/index.mjs b/Sources/JavaScriptKit/Runtime/index.mjs index 6a3df7477..62d9558ee 100644 --- a/Sources/JavaScriptKit/Runtime/index.mjs +++ b/Sources/JavaScriptKit/Runtime/index.mjs @@ -190,6 +190,100 @@ class Memory { } } +class ITCInterface { + constructor(memory) { + this.memory = memory; + } + transfer(objectRef, transferring) { + const object = this.memory.getObject(objectRef); + return { object, transferring, transfer: [object] }; + } +} +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; @@ -307,6 +401,56 @@ class SwiftRuntime { 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 { + 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 response 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; @@ -502,39 +646,18 @@ class SwiftRuntime { 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 "requestTransfer": { - const object = this.memory.getObject(message.data.objectRef); - const messageToMainThread = { - type: "transfer", - data: { - object, - destinationTid: message.data.destinationTid, - transferring: message.data.transferring, - }, - }; - try { - this.postMessageToMainThread(messageToMainThread, [object]); - } - catch (error) { - this.postMessageToMainThread({ - type: "transferError", - data: { error: String(error) }, - }); - } - break; - } - case "transfer": { - const objectRef = this.memory.retain(message.data.object); - this.exports.swjs_receive_object(objectRef, message.data.transferring); + case "request": { + broker.onReceivingRequest(message); break; } - case "transferError": { - console.error(message.data.error); // TODO: Handle the error + case "response": { + broker.onReceivingResponse(message); break; } default: @@ -551,60 +674,18 @@ class SwiftRuntime { 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 "requestTransfer": { - if (message.data.objectSourceTid == MAIN_THREAD_TID) { - const object = this.memory.getObject(message.data.objectRef); - if (message.data.destinationTid != tid) { - throw new Error("Invariant violation: The destination tid of the transfer request must be the same as the tid of the worker thread that received the request."); - } - this.postMessageToWorkerThread(message.data.destinationTid, { - type: "transfer", - data: { - object, - transferring: message.data.transferring, - destinationTid: message.data.destinationTid, - }, - }, [object]); - } - else { - // Proxy the transfer request to the worker thread that owns the object - this.postMessageToWorkerThread(message.data.objectSourceTid, { - type: "requestTransfer", - data: { - objectRef: message.data.objectRef, - objectSourceTid: tid, - transferring: message.data.transferring, - destinationTid: message.data.destinationTid, - }, - }); - } - break; - } - case "transfer": { - if (message.data.destinationTid == MAIN_THREAD_TID) { - const objectRef = this.memory.retain(message.data.object); - this.exports.swjs_receive_object(objectRef, message.data.transferring); - } - else { - // Proxy the transfer response to the destination worker thread - this.postMessageToWorkerThread(message.data.destinationTid, { - type: "transfer", - data: { - object: message.data.object, - transferring: message.data.transferring, - destinationTid: message.data.destinationTid, - }, - }, [message.data.object]); - } + case "request": { + broker.onReceivingRequest(message); break; } - case "transferError": { - console.error(message.data.error); // TODO: Handle the error + case "response": { + broker.onReceivingResponse(message); break; } default: @@ -626,19 +707,21 @@ class SwiftRuntime { }, swjs_request_transferring_object: (object_ref, object_source_tid, transferring) => { var _a; - if (this.tid == object_source_tid) { - // Fast path: The object is already in the same thread - this.exports.swjs_receive_object(object_ref, transferring); - return; + if (!this.options.threadChannel) { + throw new Error("threadChannel is not set in options given to SwiftRuntime. Please set it to request transferring objects."); } - this.postMessageToMainThread({ - type: "requestTransfer", + const broker = getMessageBroker(this.options.threadChannel); + broker.request({ + type: "request", data: { - objectRef: object_ref, - objectSourceTid: object_source_tid, - transferring, - destinationTid: (_a = this.tid) !== null && _a !== void 0 ? _a : MAIN_THREAD_TID, - }, + sourceTid: (_a = this.tid) !== null && _a !== void 0 ? _a : MAIN_THREAD_TID, + targetTid: object_source_tid, + context: transferring, + request: { + method: "transfer", + parameters: [object_ref, transferring], + } + } }); }, }; diff --git a/Tests/JavaScriptEventLoopTests/WebWorkerTaskExecutorTests.swift b/Tests/JavaScriptEventLoopTests/WebWorkerTaskExecutorTests.swift index 4892df591..c6cb2be36 100644 --- a/Tests/JavaScriptEventLoopTests/WebWorkerTaskExecutorTests.swift +++ b/Tests/JavaScriptEventLoopTests/WebWorkerTaskExecutorTests.swift @@ -283,14 +283,19 @@ final class WebWorkerTaskExecutorTests: XCTestCase { let object = JSObject.global.Object.function!.new() let transferring = JSTransferring(object) let executor = try await WebWorkerTaskExecutor(numberOfThreads: 1) - let task = Task(executorPreference: executor) { - _ = try await transferring.receive() - return + let task = Task(executorPreference: executor) { + do { + _ = try await transferring.receive() + return nil + } catch let error as JSException { + return error.thrownValue.description + } } - do { - try await task.value + guard let jsErrorMessage = try await task.value else { XCTFail("Should throw an error") - } catch {} + return + } + XCTAssertTrue(jsErrorMessage.contains("Failed to serialize response message")) // Deinit the transferring object on the thread that was created withExtendedLifetime(transferring) {} } From 58f91c35c6eecc5750c061972ac439dfd8dcbd49 Mon Sep 17 00:00:00 2001 From: Yuta Saito Date: Tue, 11 Mar 2025 05:53:01 +0000 Subject: [PATCH 240/373] Relax deinit requirement --- Runtime/src/index.ts | 20 ++++++++++ Runtime/src/itc.ts | 5 +++ Runtime/src/types.ts | 1 + .../JSObject+Transferring.swift | 14 +++---- .../FundamentalObjects/JSObject.swift | 8 +++- Sources/JavaScriptKit/Runtime/index.js | 24 ++++++++++++ Sources/JavaScriptKit/Runtime/index.mjs | 24 ++++++++++++ .../_CJavaScriptKit/include/_CJavaScriptKit.h | 6 +++ .../WebWorkerTaskExecutorTests.swift | 37 ++++++++++++++++--- 9 files changed, 124 insertions(+), 15 deletions(-) diff --git a/Runtime/src/index.ts b/Runtime/src/index.ts index 5cb1acfc2..67f478321 100644 --- a/Runtime/src/index.ts +++ b/Runtime/src/index.ts @@ -189,6 +189,7 @@ export class SwiftRuntime { 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) { @@ -526,6 +527,25 @@ export class SwiftRuntime { 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) diff --git a/Runtime/src/itc.ts b/Runtime/src/itc.ts index 44b37c7be..f7e951787 100644 --- a/Runtime/src/itc.ts +++ b/Runtime/src/itc.ts @@ -89,6 +89,11 @@ export class ITCInterface { const object = this.memory.getObject(objectRef); return { object, transferring, transfer: [object] }; } + + release(objectRef: ref): { object: undefined, transfer: Transferable[] } { + this.memory.release(objectRef); + return { object: undefined, transfer: [] }; + } } type AllRequests> = { diff --git a/Runtime/src/types.ts b/Runtime/src/types.ts index a83a74f0c..6cfc05d38 100644 --- a/Runtime/src/types.ts +++ b/Runtime/src/types.ts @@ -104,6 +104,7 @@ export interface ImportedFunctions { ): number; swjs_load_typed_array(ref: ref, buffer: pointer): void; swjs_release(ref: number): void; + swjs_release_remote(tid: number, ref: number): void; swjs_i64_to_bigint(value: bigint, signed: bool): ref; swjs_bigint_to_i64(ref: ref, signed: bool): bigint; swjs_i64_to_bigint_slow(lower: number, upper: number, signed: bool): ref; diff --git a/Sources/JavaScriptEventLoop/JSObject+Transferring.swift b/Sources/JavaScriptEventLoop/JSObject+Transferring.swift index 6deee6598..024d4250f 100644 --- a/Sources/JavaScriptEventLoop/JSObject+Transferring.swift +++ b/Sources/JavaScriptEventLoop/JSObject+Transferring.swift @@ -56,7 +56,7 @@ public struct JSTransferring: @unchecked Sendable { /// /// ```swift /// let canvas = JSObject.global.document.createElement("canvas").object! - /// let transferring = JSObject.transfer(canvas.transferControlToOffscreen().object!) + /// let transferring = JSTransferring(canvas.transferControlToOffscreen().object!) /// let executor = try await WebWorkerTaskExecutor(numberOfThreads: 1) /// Task(executorPreference: executor) { /// let canvas = try await transferring.receive() @@ -65,12 +65,6 @@ public struct JSTransferring: @unchecked Sendable { @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) - // The following sequence of events happens when a `JSObject` is transferred from - // the owner thread to the receiver thread: - // - // [Owner Thread] [Receiver Thread] - // <-----requestTransfer------ swjs_request_transferring_object - // ---------transfer---------> swjs_receive_object let idInDestination = try await withCheckedThrowingContinuation { continuation in self.storage.context.withLock { context in guard context.continuation == nil else { @@ -148,8 +142,9 @@ extension JSTransferring where T == JSObject { @_cdecl("swjs_receive_response") #endif @available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) -func _swjs_receive_response(_ object: JavaScriptObjectRef, _ transferring: UnsafeRawPointer) { +func _swjs_receive_response(_ object: JavaScriptObjectRef, _ transferring: UnsafeRawPointer?) { #if compiler(>=6.1) && _runtime(_multithreaded) + guard let transferring = transferring else { return } let context = Unmanaged<_JSTransferringContext>.fromOpaque(transferring).takeRetainedValue() context.withLock { state in assert(state.continuation != nil, "JSObject.Transferring object is not yet received!?") @@ -169,8 +164,9 @@ func _swjs_receive_response(_ object: JavaScriptObjectRef, _ transferring: Unsaf @_cdecl("swjs_receive_error") #endif @available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) -func _swjs_receive_error(_ error: JavaScriptObjectRef, _ transferring: UnsafeRawPointer) { +func _swjs_receive_error(_ error: JavaScriptObjectRef, _ transferring: UnsafeRawPointer?) { #if compiler(>=6.1) && _runtime(_multithreaded) + guard let transferring = transferring else { return } let context = Unmanaged<_JSTransferringContext>.fromOpaque(transferring).takeRetainedValue() context.withLock { state in assert(state.continuation != nil, "JSObject.Transferring object is not yet received!?") diff --git a/Sources/JavaScriptKit/FundamentalObjects/JSObject.swift b/Sources/JavaScriptKit/FundamentalObjects/JSObject.swift index 18c683682..0958b33f4 100644 --- a/Sources/JavaScriptKit/FundamentalObjects/JSObject.swift +++ b/Sources/JavaScriptKit/FundamentalObjects/JSObject.swift @@ -203,7 +203,13 @@ public class JSObject: Equatable { }) deinit { - assertOnOwnerThread(hint: "deinitializing") + #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) } diff --git a/Sources/JavaScriptKit/Runtime/index.js b/Sources/JavaScriptKit/Runtime/index.js index 206251a11..ede43514c 100644 --- a/Sources/JavaScriptKit/Runtime/index.js +++ b/Sources/JavaScriptKit/Runtime/index.js @@ -204,6 +204,10 @@ const object = this.memory.getObject(objectRef); return { object, transferring, transfer: [object] }; } + release(objectRef) { + this.memory.release(objectRef); + return { object: undefined, transfer: [] }; + } } class MessageBroker { constructor(selfTid, threadChannel, handlers) { @@ -417,6 +421,7 @@ onRequest: (message) => { let returnValue; try { + // @ts-ignore const result = itcInterface[message.data.request.method](...message.data.request.parameters); returnValue = { ok: true, value: result }; } @@ -618,6 +623,25 @@ 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)); }, diff --git a/Sources/JavaScriptKit/Runtime/index.mjs b/Sources/JavaScriptKit/Runtime/index.mjs index 62d9558ee..f95aee940 100644 --- a/Sources/JavaScriptKit/Runtime/index.mjs +++ b/Sources/JavaScriptKit/Runtime/index.mjs @@ -198,6 +198,10 @@ class ITCInterface { const object = this.memory.getObject(objectRef); return { object, transferring, transfer: [object] }; } + release(objectRef) { + this.memory.release(objectRef); + return { object: undefined, transfer: [] }; + } } class MessageBroker { constructor(selfTid, threadChannel, handlers) { @@ -411,6 +415,7 @@ class SwiftRuntime { onRequest: (message) => { let returnValue; try { + // @ts-ignore const result = itcInterface[message.data.request.method](...message.data.request.parameters); returnValue = { ok: true, value: result }; } @@ -612,6 +617,25 @@ class SwiftRuntime { 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)); }, diff --git a/Sources/_CJavaScriptKit/include/_CJavaScriptKit.h b/Sources/_CJavaScriptKit/include/_CJavaScriptKit.h index 575c0e6fd..12e07048a 100644 --- a/Sources/_CJavaScriptKit/include/_CJavaScriptKit.h +++ b/Sources/_CJavaScriptKit/include/_CJavaScriptKit.h @@ -290,6 +290,12 @@ IMPORT_JS_FUNCTION(swjs_load_typed_array, void, (const JavaScriptObjectRef ref, /// @param ref The target JavaScript object. IMPORT_JS_FUNCTION(swjs_release, void, (const JavaScriptObjectRef ref)) +/// 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 /// diff --git a/Tests/JavaScriptEventLoopTests/WebWorkerTaskExecutorTests.swift b/Tests/JavaScriptEventLoopTests/WebWorkerTaskExecutorTests.swift index c6cb2be36..8ed179f2a 100644 --- a/Tests/JavaScriptEventLoopTests/WebWorkerTaskExecutorTests.swift +++ b/Tests/JavaScriptEventLoopTests/WebWorkerTaskExecutorTests.swift @@ -264,7 +264,7 @@ final class WebWorkerTaskExecutorTests: XCTestCase { executor.terminate() } - func testTransfer() async throws { + func testTransferMainToWorker() async throws { let Uint8Array = JSObject.global.Uint8Array.function! let buffer = Uint8Array.new(100).buffer.object! let transferring = JSTransferring(buffer) @@ -275,8 +275,19 @@ final class WebWorkerTaskExecutorTests: XCTestCase { } let byteLength = try await task.value XCTAssertEqual(byteLength, 100) - // Deinit the transferring object on the thread that was created - withExtendedLifetime(transferring) {} + } + + 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 = JSTransferring(buffer) + return transferring + } + let transferring = await task.value + let buffer = try await transferring.receive() + XCTAssertEqual(buffer.byteLength.number!, 100) } func testTransferNonTransferable() async throws { @@ -296,8 +307,24 @@ final class WebWorkerTaskExecutorTests: XCTestCase { return } XCTAssertTrue(jsErrorMessage.contains("Failed to serialize response message")) - // Deinit the transferring object on the thread that was created - withExtendedLifetime(transferring) {} + } + + 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 = JSTransferring(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) } /* From 2a081de36a2b58718e092e3205f1ebb2f0c3b649 Mon Sep 17 00:00:00 2001 From: Yuta Saito Date: Tue, 11 Mar 2025 05:56:41 +0000 Subject: [PATCH 241/373] Remove dead code and fix error message --- Runtime/src/js-value.ts | 76 ------------------- .../JSObject+Transferring.swift | 6 +- 2 files changed, 3 insertions(+), 79 deletions(-) diff --git a/Runtime/src/js-value.ts b/Runtime/src/js-value.ts index 29e4a42a4..1b142de05 100644 --- a/Runtime/src/js-value.ts +++ b/Runtime/src/js-value.ts @@ -92,82 +92,6 @@ export const write = ( memory.writeUint32(kind_ptr, kind); }; -export function decompose(value: any, memory: Memory): { - kind: JavaScriptValueKindAndFlags; - payload1: number; - payload2: number; -} { - if (value === null) { - return { - kind: Kind.Null, - payload1: 0, - payload2: 0, - } - } - const type = typeof value; - switch (type) { - case "boolean": { - return { - kind: Kind.Boolean, - payload1: value ? 1 : 0, - payload2: 0, - } - } - case "number": { - return { - kind: Kind.Number, - payload1: 0, - payload2: value, - } - } - case "string": { - return { - kind: Kind.String, - payload1: memory.retain(value), - payload2: 0, - } - } - case "undefined": { - return { - kind: Kind.Undefined, - payload1: 0, - payload2: 0, - } - } - case "object": { - return { - kind: Kind.Object, - payload1: memory.retain(value), - payload2: 0, - } - } - case "function": { - return { - kind: Kind.Function, - payload1: memory.retain(value), - payload2: 0, - } - } - case "symbol": { - return { - kind: Kind.Symbol, - payload1: memory.retain(value), - payload2: 0, - } - } - case "bigint": { - return { - kind: Kind.BigInt, - payload1: memory.retain(value), - payload2: 0, - } - } - default: - assertNever(type, `Type "${type}" is not supported yet`); - } - throw new Error("unreachable"); -} - export const writeAndReturnKindBits = ( value: any, payload1_ptr: pointer, diff --git a/Sources/JavaScriptEventLoop/JSObject+Transferring.swift b/Sources/JavaScriptEventLoop/JSObject+Transferring.swift index 024d4250f..68e8c013c 100644 --- a/Sources/JavaScriptEventLoop/JSObject+Transferring.swift +++ b/Sources/JavaScriptEventLoop/JSObject+Transferring.swift @@ -69,7 +69,7 @@ public struct JSTransferring: @unchecked Sendable { self.storage.context.withLock { context in guard context.continuation == nil else { // This is a programming error, `receive` should be called only once. - fatalError("JSObject.Transferring object is already received", file: file, line: line) + fatalError("JSTransferring object is already received", file: file, line: line) } // The continuation will be resumed by `swjs_receive_object`. context.continuation = continuation @@ -147,7 +147,7 @@ func _swjs_receive_response(_ object: JavaScriptObjectRef, _ transferring: Unsaf guard let transferring = transferring else { return } let context = Unmanaged<_JSTransferringContext>.fromOpaque(transferring).takeRetainedValue() context.withLock { state in - assert(state.continuation != nil, "JSObject.Transferring object is not yet received!?") + assert(state.continuation != nil, "JSTransferring object is not yet received!?") state.continuation?.resume(returning: object) } #endif @@ -169,7 +169,7 @@ func _swjs_receive_error(_ error: JavaScriptObjectRef, _ transferring: UnsafeRaw guard let transferring = transferring else { return } let context = Unmanaged<_JSTransferringContext>.fromOpaque(transferring).takeRetainedValue() context.withLock { state in - assert(state.continuation != nil, "JSObject.Transferring object is not yet received!?") + assert(state.continuation != nil, "JSTransferring object is not yet received!?") state.continuation?.resume(throwing: JSException(JSObject(id: error).jsValue)) } #endif From 4fe37e7ae8b19d0242a01945e3e2be274ec8be6c Mon Sep 17 00:00:00 2001 From: Yuta Saito Date: Tue, 11 Mar 2025 06:23:09 +0000 Subject: [PATCH 242/373] Rename JSTransferring to JSSending --- .../JSObject+Transferring.swift | 14 +++++++------- .../WebWorkerTaskExecutorTests.swift | 8 ++++---- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/Sources/JavaScriptEventLoop/JSObject+Transferring.swift b/Sources/JavaScriptEventLoop/JSObject+Transferring.swift index 68e8c013c..b5c3a14bf 100644 --- a/Sources/JavaScriptEventLoop/JSObject+Transferring.swift +++ b/Sources/JavaScriptEventLoop/JSObject+Transferring.swift @@ -5,11 +5,11 @@ import _CJavaScriptKit import Synchronization #endif -/// A temporary object intended to transfer an object from one thread to another. +/// A temporary object intended to send an object from one thread to another. /// -/// ``JSTransferring`` is `Sendable` and it's intended to be shared across threads. +/// ``JSSending`` is `Sendable` and it's intended to be shared across threads. @available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) -public struct JSTransferring: @unchecked Sendable { +public struct JSSending: @unchecked Sendable { fileprivate struct Storage { /// The original object that is transferred. /// @@ -101,9 +101,9 @@ fileprivate final class _JSTransferringContext: Sendable { } -extension JSTransferring where T == JSObject { +extension JSSending where T == JSObject { - /// Transfers the ownership of a `JSObject` to be sent to another thread. + /// Sends a `JSObject` to another thread. /// /// - 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 @@ -112,8 +112,8 @@ extension JSTransferring where T == JSObject { /// - Parameter object: The ``JSObject`` to be transferred. /// - Returns: A ``Transferring`` 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( + public static func transfer(_ object: JSObject) -> JSSending { + JSSending( sourceObject: object, construct: { JSObject(id: $0) }, deconstruct: { $0.id }, diff --git a/Tests/JavaScriptEventLoopTests/WebWorkerTaskExecutorTests.swift b/Tests/JavaScriptEventLoopTests/WebWorkerTaskExecutorTests.swift index 8ed179f2a..1dd0f1dd1 100644 --- a/Tests/JavaScriptEventLoopTests/WebWorkerTaskExecutorTests.swift +++ b/Tests/JavaScriptEventLoopTests/WebWorkerTaskExecutorTests.swift @@ -267,7 +267,7 @@ final class WebWorkerTaskExecutorTests: XCTestCase { func testTransferMainToWorker() async throws { let Uint8Array = JSObject.global.Uint8Array.function! let buffer = Uint8Array.new(100).buffer.object! - let transferring = JSTransferring(buffer) + let transferring = JSSending.transfer(buffer) let executor = try await WebWorkerTaskExecutor(numberOfThreads: 1) let task = Task(executorPreference: executor) { let buffer = try await transferring.receive() @@ -282,7 +282,7 @@ final class WebWorkerTaskExecutorTests: XCTestCase { let task = Task(executorPreference: executor) { let Uint8Array = JSObject.global.Uint8Array.function! let buffer = Uint8Array.new(100).buffer.object! - let transferring = JSTransferring(buffer) + let transferring = JSSending.transfer(buffer) return transferring } let transferring = await task.value @@ -292,7 +292,7 @@ final class WebWorkerTaskExecutorTests: XCTestCase { func testTransferNonTransferable() async throws { let object = JSObject.global.Object.function!.new() - let transferring = JSTransferring(object) + let transferring = JSSending.transfer(object) let executor = try await WebWorkerTaskExecutor(numberOfThreads: 1) let task = Task(executorPreference: executor) { do { @@ -315,7 +315,7 @@ final class WebWorkerTaskExecutorTests: XCTestCase { let task = Task(executorPreference: executor1) { let Uint8Array = JSObject.global.Uint8Array.function! let buffer = Uint8Array.new(100).buffer.object! - let transferring = JSTransferring(buffer) + let transferring = JSSending.transfer(buffer) return transferring } let transferring = await task.value From eeff111bc7f1eceee8a1be8627d48fed6d5620e7 Mon Sep 17 00:00:00 2001 From: Yuta Saito Date: Tue, 11 Mar 2025 07:47:22 +0000 Subject: [PATCH 243/373] Add `JSSending.receive(...)` to receive multiple objects at once --- .../OffscrenCanvas/Sources/MyApp/main.swift | 2 +- Runtime/src/index.ts | 47 ++- Runtime/src/itc.ts | 13 +- Runtime/src/js-value.ts | 10 +- Runtime/src/types.ts | 16 +- .../JSObject+Transferring.swift | 364 +++++++++++++----- .../WebWorkerTaskExecutor.swift | 153 +++++++- Sources/JavaScriptKit/Runtime/index.js | 53 ++- Sources/JavaScriptKit/Runtime/index.mjs | 53 ++- .../_CJavaScriptKit/include/_CJavaScriptKit.h | 17 +- .../WebWorkerTaskExecutorTests.swift | 103 ++++- 11 files changed, 688 insertions(+), 143 deletions(-) diff --git a/Examples/OffscrenCanvas/Sources/MyApp/main.swift b/Examples/OffscrenCanvas/Sources/MyApp/main.swift index b6e5b6df9..67e087122 100644 --- a/Examples/OffscrenCanvas/Sources/MyApp/main.swift +++ b/Examples/OffscrenCanvas/Sources/MyApp/main.swift @@ -11,7 +11,7 @@ protocol CanvasRenderer { struct BackgroundRenderer: CanvasRenderer { func render(canvas: JSObject, size: Int) async throws { let executor = try await WebWorkerTaskExecutor(numberOfThreads: 1) - let transfer = JSTransferring(canvas) + let transfer = JSSending.transfer(canvas) let renderingTask = Task(executorPreference: executor) { let canvas = try await transfer.receive() try await renderAnimation(canvas: canvas, size: size) diff --git a/Runtime/src/index.ts b/Runtime/src/index.ts index 67f478321..3f23ed753 100644 --- a/Runtime/src/index.ts +++ b/Runtime/src/index.ts @@ -11,6 +11,7 @@ import { 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 type SwiftRuntimeOptions = { /** @@ -208,7 +209,7 @@ export class SwiftRuntime { } catch (error) { responseMessage.data.response = { ok: false, - error: serializeError(new TypeError(`Failed to serialize response message: ${error}`)) + error: serializeError(new TypeError(`Failed to serialize message: ${error}`)) }; newBroker.reply(responseMessage); } @@ -648,24 +649,56 @@ export class SwiftRuntime { // Main thread's tid is always -1 return this.tid || -1; }, - swjs_request_transferring_object: ( - object_ref: ref, + swjs_request_sending_object: ( + sending_object: ref, + transferring_objects: pointer, + transferring_objects_count: number, object_source_tid: number, - transferring: pointer, + 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: transferring, + context: sending_context, request: { - method: "transfer", - parameters: [object_ref, transferring], + method: "sendObjects", + parameters: [sendingObjects, transferringObjects, sending_context], } } }) diff --git a/Runtime/src/itc.ts b/Runtime/src/itc.ts index f7e951787..e2c93622a 100644 --- a/Runtime/src/itc.ts +++ b/Runtime/src/itc.ts @@ -85,9 +85,16 @@ export type SwiftRuntimeThreadChannel = export class ITCInterface { constructor(private memory: Memory) {} - transfer(objectRef: ref, transferring: pointer): { object: any, transferring: pointer, transfer: Transferable[] } { - const object = this.memory.getObject(objectRef); - return { object, transferring, transfer: [object] }; + 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[] } { diff --git a/Runtime/src/js-value.ts b/Runtime/src/js-value.ts index 1b142de05..dcc378f61 100644 --- a/Runtime/src/js-value.ts +++ b/Runtime/src/js-value.ts @@ -1,5 +1,5 @@ import { Memory } from "./memory.js"; -import { assertNever, JavaScriptValueKindAndFlags, pointer } from "./types.js"; +import { assertNever, JavaScriptValueKindAndFlags, pointer, ref } from "./types.js"; export const enum Kind { Boolean = 0, @@ -142,3 +142,11 @@ export const writeAndReturnKindBits = ( } 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/types.ts b/Runtime/src/types.ts index 6cfc05d38..587b60770 100644 --- a/Runtime/src/types.ts +++ b/Runtime/src/types.ts @@ -115,10 +115,20 @@ export interface ImportedFunctions { swjs_listen_message_from_worker_thread: (tid: number) => void; swjs_terminate_worker_thread: (tid: number) => void; swjs_get_worker_thread_id: () => number; - swjs_request_transferring_object: ( - object_ref: ref, + swjs_request_sending_object: ( + sending_object: ref, + transferring_objects: pointer, + transferring_objects_count: number, object_source_tid: number, - transferring: pointer, + sending_context: pointer, + ) => void; + swjs_request_sending_objects: ( + sending_objects: pointer, + sending_objects_count: number, + transferring_objects: pointer, + transferring_objects_count: number, + object_source_tid: number, + sending_context: pointer, ) => void; } diff --git a/Sources/JavaScriptEventLoop/JSObject+Transferring.swift b/Sources/JavaScriptEventLoop/JSObject+Transferring.swift index b5c3a14bf..c573939e9 100644 --- a/Sources/JavaScriptEventLoop/JSObject+Transferring.swift +++ b/Sources/JavaScriptEventLoop/JSObject+Transferring.swift @@ -5,131 +5,327 @@ import _CJavaScriptKit import Synchronization #endif -/// A temporary object intended to send an object from one thread to another. +/// A temporary object intended to send a JavaScript object from one thread to another. /// -/// ``JSSending`` is `Sendable` and it's intended to be shared across threads. +/// `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 { fileprivate struct Storage { - /// The original object that is transferred. + /// The original object that is sent. /// - /// Retain it here to prevent it from being released before the transfer is complete. - let sourceObject: T + /// 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: (_ id: JavaScriptObjectRef) -> T + 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 - - #if compiler(>=6.1) && _runtime(_multithreaded) - /// A shared context for transferring objects across threads. - let context: _JSTransferringContext = _JSTransferringContext() - #endif + /// Whether the object should be "transferred" or "cloned". + let transferring: Bool } private let storage: Storage fileprivate init( sourceObject: T, - construct: @escaping (_ id: JavaScriptObjectRef) -> T, - deconstruct: @escaping (_ object: T) -> JavaScriptObjectRef, - getSourceTid: @escaping (_ object: T) -> Int32 + 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: sourceObject, + sourceObject: object, construct: construct, - idInSource: deconstruct(sourceObject), - sourceTid: getSourceTid(sourceObject) + idInSource: object.id, + sourceTid: getSourceTid(sourceObject), + transferring: transferring + ) + } +} + +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 ) } - /// Receives a transferred ``JSObject`` from a thread. + /// 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. /// - /// The original ``JSObject`` is ["Transferred"](https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API/Transferable_objects) - /// to the receiving 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. /// - /// Note that this method should be called only once for each ``Transferring`` instance - /// on the receiving thread. + /// 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 + /// ## 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) + } +} + +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 = JSTransferring(canvas.transferControlToOffscreen().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 - self.storage.context.withLock { context in - guard context.continuation == nil else { - // This is a programming error, `receive` should be called only once. - fatalError("JSTransferring object is already received", file: file, line: line) - } - // The continuation will be resumed by `swjs_receive_object`. - context.continuation = continuation - } - swjs_request_transferring_object( - self.storage.idInSource, + 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(self.storage.context).toOpaque() + Unmanaged.passRetained(context).toOpaque() ) } - return storage.construct(idInDestination) + return storage.construct(JSObject(id: idInDestination)) #else - return storage.construct(storage.idInSource) + return storage.construct(storage.sourceObject) #endif } -} -fileprivate final class _JSTransferringContext: Sendable { - struct State { - var continuation: CheckedContinuation? - } - private let state: Mutex = .init(State()) - - func withLock(_ body: (inout State) -> R) -> R { - return state.withLock { state in - body(&state) + /// 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. + 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) { + 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)) } } +fileprivate final class _JSSendingContext: Sendable { + let continuation: CheckedContinuation -extension JSSending where T == JSObject { - - /// Sends a `JSObject` to another thread. - /// - /// - 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 ``Transferring`` 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( - sourceObject: object, - construct: { JSObject(id: $0) }, - deconstruct: { $0.id }, - 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 - } - ) + 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. @@ -142,14 +338,11 @@ extension JSSending where T == JSObject { @_cdecl("swjs_receive_response") #endif @available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) -func _swjs_receive_response(_ object: JavaScriptObjectRef, _ transferring: UnsafeRawPointer?) { +func _swjs_receive_response(_ object: JavaScriptObjectRef, _ contextPtr: UnsafeRawPointer?) { #if compiler(>=6.1) && _runtime(_multithreaded) - guard let transferring = transferring else { return } - let context = Unmanaged<_JSTransferringContext>.fromOpaque(transferring).takeRetainedValue() - context.withLock { state in - assert(state.continuation != nil, "JSTransferring object is not yet received!?") - state.continuation?.resume(returning: object) - } + guard let contextPtr = contextPtr else { return } + let context = Unmanaged<_JSSendingContext>.fromOpaque(contextPtr).takeRetainedValue() + context.continuation.resume(returning: object) #endif } @@ -164,13 +357,10 @@ func _swjs_receive_response(_ object: JavaScriptObjectRef, _ transferring: Unsaf @_cdecl("swjs_receive_error") #endif @available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) -func _swjs_receive_error(_ error: JavaScriptObjectRef, _ transferring: UnsafeRawPointer?) { +func _swjs_receive_error(_ error: JavaScriptObjectRef, _ contextPtr: UnsafeRawPointer?) { #if compiler(>=6.1) && _runtime(_multithreaded) - guard let transferring = transferring else { return } - let context = Unmanaged<_JSTransferringContext>.fromOpaque(transferring).takeRetainedValue() - context.withLock { state in - assert(state.continuation != nil, "JSTransferring object is not yet received!?") - state.continuation?.resume(throwing: JSException(JSObject(id: error).jsValue)) - } + 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/WebWorkerTaskExecutor.swift b/Sources/JavaScriptEventLoop/WebWorkerTaskExecutor.swift index 14b13eee9..7373b9604 100644 --- a/Sources/JavaScriptEventLoop/WebWorkerTaskExecutor.swift +++ b/Sources/JavaScriptEventLoop/WebWorkerTaskExecutor.swift @@ -16,6 +16,34 @@ import _CJavaScriptEventLoop /// 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) @@ -24,22 +52,40 @@ import _CJavaScriptEventLoop /// from spawned Web Workers, and forward the message to the main thread /// by calling `_swjs_enqueue_main_job_from_worker`. /// -/// ## Usage +/// ## Basic Usage /// /// ```swift -/// let executor = WebWorkerTaskExecutor(numberOfThreads: 4) +/// // 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 block runs on the Web Worker thread. -/// await withTaskGroup(of: Int.self) { group in +/// // 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 { -/// // Structured child works are executed on the Web Worker thread. -/// group.addTask { fibonacci(of: i) } +/// 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 /// @@ -359,36 +405,89 @@ public final class WebWorkerTaskExecutor: TaskExecutor { private let executor: Executor - /// Create a new Web Worker task 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 timeout to wait for all worker threads to be started. - /// - checkInterval: The interval to check if all worker threads are started. + /// - 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) } - /// Terminate child Web Worker threads. - /// Jobs enqueued to the executor after calling this method will be ignored. + /// 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 + /// ``` /// - /// NOTE: This method must be called after all tasks that prefer this executor are done. - /// Otherwise, the tasks may stuck forever. + /// - 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() } - /// The number of Web Worker threads. + /// 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 - /// Enqueue a job to the executor. + /// 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. /// - /// NOTE: Called from the Swift Concurrency runtime. + /// - Parameter job: The job to enqueue. public func enqueue(_ job: UnownedJob) { Self.traceStatsIncrement(\.enqueueExecutor) executor.enqueue(job) @@ -431,9 +530,23 @@ public final class WebWorkerTaskExecutor: TaskExecutor { @MainActor private static var _swift_task_enqueueGlobalWithDelay_hook_original: UnsafeMutableRawPointer? @MainActor private static var _swift_task_enqueueGlobalWithDeadline_hook_original: UnsafeMutableRawPointer? - /// Install a global executor that forwards jobs from Web Worker threads to the main thread. + /// 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) + /// ``` /// - /// This function must be called once before using the Web Worker task executor. + /// - Important: This method must be called from the main thread. public static func installGlobalExecutor() { MainActor.assumeIsolated { installGlobalExecutorIsolated() diff --git a/Sources/JavaScriptKit/Runtime/index.js b/Sources/JavaScriptKit/Runtime/index.js index ede43514c..25b6af3c9 100644 --- a/Sources/JavaScriptKit/Runtime/index.js +++ b/Sources/JavaScriptKit/Runtime/index.js @@ -122,6 +122,13 @@ } 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") { @@ -200,9 +207,15 @@ constructor(memory) { this.memory = memory; } - transfer(objectRef, transferring) { - const object = this.memory.getObject(objectRef); - return { object, transferring, transfer: [object] }; + 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); @@ -442,7 +455,7 @@ catch (error) { responseMessage.data.response = { ok: false, - error: serializeError(new TypeError(`Failed to serialize response message: ${error}`)) + error: serializeError(new TypeError(`Failed to serialize message: ${error}`)) }; newBroker.reply(responseMessage); } @@ -735,21 +748,45 @@ // Main thread's tid is always -1 return this.tid || -1; }, - swjs_request_transferring_object: (object_ref, object_source_tid, transferring) => { + 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: transferring, + context: sending_context, request: { - method: "transfer", - parameters: [object_ref, transferring], + method: "sendObjects", + parameters: [sendingObjects, transferringObjects, sending_context], } } }); diff --git a/Sources/JavaScriptKit/Runtime/index.mjs b/Sources/JavaScriptKit/Runtime/index.mjs index f95aee940..668368203 100644 --- a/Sources/JavaScriptKit/Runtime/index.mjs +++ b/Sources/JavaScriptKit/Runtime/index.mjs @@ -116,6 +116,13 @@ const writeAndReturnKindBits = (value, payload1_ptr, payload2_ptr, is_exception, } 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") { @@ -194,9 +201,15 @@ class ITCInterface { constructor(memory) { this.memory = memory; } - transfer(objectRef, transferring) { - const object = this.memory.getObject(objectRef); - return { object, transferring, transfer: [object] }; + 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); @@ -436,7 +449,7 @@ class SwiftRuntime { catch (error) { responseMessage.data.response = { ok: false, - error: serializeError(new TypeError(`Failed to serialize response message: ${error}`)) + error: serializeError(new TypeError(`Failed to serialize message: ${error}`)) }; newBroker.reply(responseMessage); } @@ -729,21 +742,45 @@ class SwiftRuntime { // Main thread's tid is always -1 return this.tid || -1; }, - swjs_request_transferring_object: (object_ref, object_source_tid, transferring) => { + 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: transferring, + context: sending_context, request: { - method: "transfer", - parameters: [object_ref, transferring], + method: "sendObjects", + parameters: [sendingObjects, transferringObjects, sending_context], } } }); diff --git a/Sources/_CJavaScriptKit/include/_CJavaScriptKit.h b/Sources/_CJavaScriptKit/include/_CJavaScriptKit.h index 12e07048a..2b96a81ea 100644 --- a/Sources/_CJavaScriptKit/include/_CJavaScriptKit.h +++ b/Sources/_CJavaScriptKit/include/_CJavaScriptKit.h @@ -316,11 +316,20 @@ IMPORT_JS_FUNCTION(swjs_get_worker_thread_id, int, (void)) int swjs_get_worker_thread_id_cached(void); -/// Requests transferring a JavaScript object to another worker thread. +/// 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_transferring_object, void, (JavaScriptObjectRef object, - int object_source_tid, - void * _Nonnull transferring)) +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/JavaScriptEventLoopTests/WebWorkerTaskExecutorTests.swift b/Tests/JavaScriptEventLoopTests/WebWorkerTaskExecutorTests.swift index 1dd0f1dd1..31d1593f3 100644 --- a/Tests/JavaScriptEventLoopTests/WebWorkerTaskExecutorTests.swift +++ b/Tests/JavaScriptEventLoopTests/WebWorkerTaskExecutorTests.swift @@ -264,6 +264,12 @@ final class WebWorkerTaskExecutorTests: XCTestCase { 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! @@ -275,6 +281,9 @@ final class WebWorkerTaskExecutorTests: XCTestCase { } let byteLength = try await task.value XCTAssertEqual(byteLength, 100) + + // Transferred Uint8Array should have 0 byteLength + XCTAssertEqual(buffer.byteLength.number!, 0) } func testTransferWorkerToMain() async throws { @@ -306,7 +315,50 @@ final class WebWorkerTaskExecutorTests: XCTestCase { XCTFail("Should throw an error") return } - XCTAssertTrue(jsErrorMessage.contains("Failed to serialize response message")) + XCTAssertTrue(jsErrorMessage.contains("Failed to serialize message"), jsErrorMessage) + } + + 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 { @@ -327,6 +379,55 @@ final class WebWorkerTaskExecutorTests: XCTestCase { 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) From 44a5dba7d3c8f929d49f9c2522a4a88c63beda26 Mon Sep 17 00:00:00 2001 From: Yuta Saito Date: Tue, 11 Mar 2025 09:42:50 +0000 Subject: [PATCH 244/373] Build fix --- .../JavaScriptEventLoop/JSObject+Transferring.swift | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/Sources/JavaScriptEventLoop/JSObject+Transferring.swift b/Sources/JavaScriptEventLoop/JSObject+Transferring.swift index c573939e9..615dadce6 100644 --- a/Sources/JavaScriptEventLoop/JSObject+Transferring.swift +++ b/Sources/JavaScriptEventLoop/JSObject+Transferring.swift @@ -79,6 +79,7 @@ public struct JSSending: @unchecked Sendable { } } +@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( @@ -165,6 +166,7 @@ extension JSSending where T == JSObject { } } +@available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) extension JSSending { /// Receives a sent `JSObject` from a thread. @@ -227,6 +229,8 @@ extension JSSending { #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 @@ -257,10 +261,12 @@ extension JSSending { /// - 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? @@ -302,9 +308,14 @@ extension JSSending { 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, *) fileprivate final class _JSSendingContext: Sendable { let continuation: CheckedContinuation From b678f71b632631ea8c7d782e08ed5a786cf962ee Mon Sep 17 00:00:00 2001 From: Yuta Saito Date: Tue, 11 Mar 2025 10:03:06 +0000 Subject: [PATCH 245/373] Skip multi-transfer tests --- .../WebWorkerTaskExecutorTests.swift | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/Tests/JavaScriptEventLoopTests/WebWorkerTaskExecutorTests.swift b/Tests/JavaScriptEventLoopTests/WebWorkerTaskExecutorTests.swift index 31d1593f3..16cfd6374 100644 --- a/Tests/JavaScriptEventLoopTests/WebWorkerTaskExecutorTests.swift +++ b/Tests/JavaScriptEventLoopTests/WebWorkerTaskExecutorTests.swift @@ -318,6 +318,10 @@ final class WebWorkerTaskExecutorTests: XCTestCase { XCTAssertTrue(jsErrorMessage.contains("Failed to serialize message"), jsErrorMessage) } + /* + // Node.js 20 and below doesn't throw exception when transferring the same ArrayBuffer + // multiple times. + // See https://github.com/nodejs/node/commit/38dee8a1c04237bd231a01410f42e9d172f4c162 func testTransferMultipleTimes() async throws { let executor = try await WebWorkerTaskExecutor(numberOfThreads: 1) let Uint8Array = JSObject.global.Uint8Array.function! @@ -344,6 +348,7 @@ final class WebWorkerTaskExecutorTests: XCTestCase { } XCTAssertTrue(jsErrorMessage.contains("Failed to serialize message")) } + */ func testCloneMultipleTimes() async throws { let executor = try await WebWorkerTaskExecutor(numberOfThreads: 1) From f5e3a95412cda11df093fe8485ca81a8c26487fb Mon Sep 17 00:00:00 2001 From: Yuta Saito Date: Tue, 11 Mar 2025 10:05:52 +0000 Subject: [PATCH 246/373] Rename JSObject+Transferring.swift to JSSending.swift --- .../{JSObject+Transferring.swift => JSSending.swift} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename Sources/JavaScriptEventLoop/{JSObject+Transferring.swift => JSSending.swift} (100%) diff --git a/Sources/JavaScriptEventLoop/JSObject+Transferring.swift b/Sources/JavaScriptEventLoop/JSSending.swift similarity index 100% rename from Sources/JavaScriptEventLoop/JSObject+Transferring.swift rename to Sources/JavaScriptEventLoop/JSSending.swift From 120a9f49d04f5b86538e92ec5332dca27563f3ba Mon Sep 17 00:00:00 2001 From: Yuta Saito Date: Tue, 11 Mar 2025 19:51:15 +0900 Subject: [PATCH 247/373] [skip ci] Fix the parameter name in the documentation --- Sources/JavaScriptEventLoop/JSSending.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Sources/JavaScriptEventLoop/JSSending.swift b/Sources/JavaScriptEventLoop/JSSending.swift index 615dadce6..4f89f7346 100644 --- a/Sources/JavaScriptEventLoop/JSSending.swift +++ b/Sources/JavaScriptEventLoop/JSSending.swift @@ -343,7 +343,7 @@ public struct JSSendingError: Error, CustomStringConvertible { /// /// - Parameters: /// - object: The `JSObject` to be received. -/// - transferring: A pointer to the `Transferring.Storage` instance. +/// - contextPtr: A pointer to the `_JSSendingContext` instance. #if compiler(>=6.1) // @_expose and @_extern are only available in Swift 6.1+ @_expose(wasm, "swjs_receive_response") @_cdecl("swjs_receive_response") @@ -362,7 +362,7 @@ func _swjs_receive_response(_ object: JavaScriptObjectRef, _ contextPtr: UnsafeR /// /// - Parameters: /// - error: The error to be received. -/// - transferring: A pointer to the `Transferring.Storage` instance. +/// - contextPtr: A pointer to the `_JSSendingContext` instance. #if compiler(>=6.1) // @_expose and @_extern are only available in Swift 6.1+ @_expose(wasm, "swjs_receive_error") @_cdecl("swjs_receive_error") From 20ecd3a6d9040a7baea1e039dcbf153c0b955f6d Mon Sep 17 00:00:00 2001 From: Yuta Saito Date: Tue, 11 Mar 2025 11:15:06 +0000 Subject: [PATCH 248/373] Fix build with older compilers --- Sources/JavaScriptEventLoop/JSSending.swift | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/Sources/JavaScriptEventLoop/JSSending.swift b/Sources/JavaScriptEventLoop/JSSending.swift index 4f89f7346..b4458d53a 100644 --- a/Sources/JavaScriptEventLoop/JSSending.swift +++ b/Sources/JavaScriptEventLoop/JSSending.swift @@ -44,7 +44,10 @@ import _CJavaScriptKit /// ``` @available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) public struct JSSending: @unchecked Sendable { - fileprivate struct Storage { + // 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. @@ -57,6 +60,20 @@ public struct JSSending: @unchecked Sendable { 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 From 97fc40fb961a1a7c4b35049c036af44dcd5a95e1 Mon Sep 17 00:00:00 2001 From: Yuta Saito Date: Thu, 13 Mar 2025 13:06:27 +0900 Subject: [PATCH 249/373] Workaround Swift 6.0 compiler crash --- .../JavaScriptKit/FundamentalObjects/JSClosure.swift | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/Sources/JavaScriptKit/FundamentalObjects/JSClosure.swift b/Sources/JavaScriptKit/FundamentalObjects/JSClosure.swift index c075c63e5..261b5b5cb 100644 --- a/Sources/JavaScriptKit/FundamentalObjects/JSClosure.swift +++ b/Sources/JavaScriptKit/FundamentalObjects/JSClosure.swift @@ -64,12 +64,18 @@ public class JSOneshotClosure: JSObject, JSClosureProtocol { public class JSClosure: JSFunction, JSClosureProtocol { class SharedJSClosure { - private var storage: [JavaScriptHostFuncRef: (object: JSObject, body: (sending [JSValue]) -> JSValue)] = [:] + // 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] } - set { storage[key] = newValue } + get { storage[key]?.item } + set { storage[key] = newValue.map { Entry(item: $0) } } } } From f84117445029ad302e99db61da708c7c1588cd6e Mon Sep 17 00:00:00 2001 From: Yuta Saito Date: Fri, 28 Feb 2025 12:58:06 +0000 Subject: [PATCH 250/373] Add initial packager plugin This is very much a work in progress. It's just a proof of concept at this point and just works for very simple examples. The plugin invocation is as follows: ``` swift package --swift-sdk wasm32-unknown-wasi js ``` --- .gitignore | 2 + Examples/Basic/Package.swift | 2 +- Examples/Basic/build.sh | 3 +- Examples/Basic/index.html | 5 +- Examples/Basic/index.js | 33 -- Examples/Embedded/_Runtime | 1 - Examples/Embedded/build.sh | 4 +- Examples/Embedded/index.html | 5 +- Examples/Embedded/index.js | 33 -- .../Sources/JavaScript/index.js | 74 --- .../Sources/JavaScript/instantiate.js | 29 -- .../Sources/JavaScript/worker.js | 28 -- Examples/Multithreading/build.sh | 4 +- Examples/Multithreading/index.html | 33 +- Examples/OffscrenCanvas/build.sh | 4 +- Examples/OffscrenCanvas/index.html | 5 +- Examples/OffscrenCanvas/serve.json | 15 +- Examples/Testing/.gitignore | 8 + Examples/Testing/Package.swift | 28 ++ .../Testing/Sources/Counter/Counter.swift | 7 + .../Tests/CounterTests/CounterTests.swift | 36 ++ Makefile | 10 +- Package.swift | 12 +- Plugins/PackageToJS/Package.swift | 12 + Plugins/PackageToJS/Sources/MiniMake.swift | 251 ++++++++++ Plugins/PackageToJS/Sources/PackageToJS.swift | 417 ++++++++++++++++ .../Sources/PackageToJSPlugin.swift | 471 ++++++++++++++++++ Plugins/PackageToJS/Sources/ParseWasm.swift | 312 ++++++++++++ Plugins/PackageToJS/Sources/Preprocess.swift | 367 ++++++++++++++ Plugins/PackageToJS/Templates/bin/test.js | 75 +++ Plugins/PackageToJS/Templates/index.d.ts | 29 ++ Plugins/PackageToJS/Templates/index.js | 14 + .../PackageToJS/Templates/instantiate.d.ts | 103 ++++ Plugins/PackageToJS/Templates/instantiate.js | 118 +++++ Plugins/PackageToJS/Templates/package.json | 16 + .../Templates/platforms/browser.d.ts | 15 + .../Templates/platforms/browser.js | 136 +++++ .../Templates/platforms/browser.worker.js | 18 + .../PackageToJS/Templates/platforms/node.d.ts | 13 + .../PackageToJS/Templates/platforms/node.js | 158 ++++++ .../PackageToJS/Templates/test.browser.html | 32 ++ Plugins/PackageToJS/Templates/test.d.ts | 12 + Plugins/PackageToJS/Templates/test.js | 188 +++++++ .../Tests/ExampleProjectTests.swift | 6 + Plugins/PackageToJS/Tests/MiniMakeTests.swift | 203 ++++++++ .../PackageToJS/Tests/PreprocessTests.swift | 137 +++++ .../Tests/TemporaryDirectory.swift | 24 + .../WebWorkerTaskExecutorTests.swift | 15 + Tests/prelude.mjs | 12 + scripts/test-harness.mjs | 17 - 50 files changed, 3302 insertions(+), 250 deletions(-) delete mode 100644 Examples/Basic/index.js delete mode 120000 Examples/Embedded/_Runtime delete mode 100644 Examples/Embedded/index.js delete mode 100644 Examples/Multithreading/Sources/JavaScript/index.js delete mode 100644 Examples/Multithreading/Sources/JavaScript/instantiate.js delete mode 100644 Examples/Multithreading/Sources/JavaScript/worker.js mode change 120000 => 100644 Examples/OffscrenCanvas/serve.json create mode 100644 Examples/Testing/.gitignore create mode 100644 Examples/Testing/Package.swift create mode 100644 Examples/Testing/Sources/Counter/Counter.swift create mode 100644 Examples/Testing/Tests/CounterTests/CounterTests.swift create mode 100644 Plugins/PackageToJS/Package.swift create mode 100644 Plugins/PackageToJS/Sources/MiniMake.swift create mode 100644 Plugins/PackageToJS/Sources/PackageToJS.swift create mode 100644 Plugins/PackageToJS/Sources/PackageToJSPlugin.swift create mode 100644 Plugins/PackageToJS/Sources/ParseWasm.swift create mode 100644 Plugins/PackageToJS/Sources/Preprocess.swift create mode 100644 Plugins/PackageToJS/Templates/bin/test.js create mode 100644 Plugins/PackageToJS/Templates/index.d.ts create mode 100644 Plugins/PackageToJS/Templates/index.js create mode 100644 Plugins/PackageToJS/Templates/instantiate.d.ts create mode 100644 Plugins/PackageToJS/Templates/instantiate.js create mode 100644 Plugins/PackageToJS/Templates/package.json create mode 100644 Plugins/PackageToJS/Templates/platforms/browser.d.ts create mode 100644 Plugins/PackageToJS/Templates/platforms/browser.js create mode 100644 Plugins/PackageToJS/Templates/platforms/browser.worker.js create mode 100644 Plugins/PackageToJS/Templates/platforms/node.d.ts create mode 100644 Plugins/PackageToJS/Templates/platforms/node.js create mode 100644 Plugins/PackageToJS/Templates/test.browser.html create mode 100644 Plugins/PackageToJS/Templates/test.d.ts create mode 100644 Plugins/PackageToJS/Templates/test.js create mode 100644 Plugins/PackageToJS/Tests/ExampleProjectTests.swift create mode 100644 Plugins/PackageToJS/Tests/MiniMakeTests.swift create mode 100644 Plugins/PackageToJS/Tests/PreprocessTests.swift create mode 100644 Plugins/PackageToJS/Tests/TemporaryDirectory.swift create mode 100644 Tests/prelude.mjs delete mode 100644 scripts/test-harness.mjs diff --git a/.gitignore b/.gitignore index 5102946ea..1d3cb87be 100644 --- a/.gitignore +++ b/.gitignore @@ -7,3 +7,5 @@ node_modules xcuserdata/ .swiftpm .vscode +Examples/*/Bundle +Examples/*/package-lock.json diff --git a/Examples/Basic/Package.swift b/Examples/Basic/Package.swift index ea70e6b20..f1a80aaaa 100644 --- a/Examples/Basic/Package.swift +++ b/Examples/Basic/Package.swift @@ -17,5 +17,5 @@ let package = Package( ] ) ], - swiftLanguageVersions: [.v5] + swiftLanguageModes: [.v5] ) diff --git a/Examples/Basic/build.sh b/Examples/Basic/build.sh index 0e5761ecf..826e90f81 100755 --- a/Examples/Basic/build.sh +++ b/Examples/Basic/build.sh @@ -1,2 +1,3 @@ #!/bin/bash -swift build --swift-sdk "${SWIFT_SDK_ID:-wasm32-unknown-wasi}" -Xswiftc -Xclang-linker -Xswiftc -mexec-model=reactor -Xlinker --export=__main_argc_argv +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 index d94796a09..a674baca1 100644 --- a/Examples/Basic/index.html +++ b/Examples/Basic/index.html @@ -6,7 +6,10 @@ - + diff --git a/Examples/Basic/index.js b/Examples/Basic/index.js deleted file mode 100644 index e90769aa5..000000000 --- a/Examples/Basic/index.js +++ /dev/null @@ -1,33 +0,0 @@ -import { WASI, File, OpenFile, ConsoleStdout, PreopenDirectory } from 'https://esm.run/@bjorn3/browser_wasi_shim@0.3.0'; - -async function main(configuration = "debug") { - // Fetch our Wasm File - const response = await fetch(`./.build/${configuration}/Basic.wasm`); - // Create a new WASI system instance - const wasi = new WASI(/* args */["main.wasm"], /* env */[], /* fd */[ - new OpenFile(new File([])), // stdin - ConsoleStdout.lineBuffered((stdout) => { - console.log(stdout); - }), - ConsoleStdout.lineBuffered((stderr) => { - console.error(stderr); - }), - new PreopenDirectory("/", new Map()), - ]) - const { SwiftRuntime } = await import(`./.build/${configuration}/JavaScriptKit_JavaScriptKit.resources/Runtime/index.mjs`); - // Create a new Swift Runtime instance to interact with JS and Swift - const swift = new SwiftRuntime(); - // Instantiate the WebAssembly file - const { instance } = await WebAssembly.instantiateStreaming(response, { - wasi_snapshot_preview1: wasi.wasiImport, - javascript_kit: swift.wasmImports, - }); - // Set the WebAssembly instance to the Swift Runtime - swift.setInstance(instance); - // Start the WebAssembly WASI reactor instance - wasi.initialize(instance); - // Start Swift main function - swift.main() -}; - -main(); diff --git a/Examples/Embedded/_Runtime b/Examples/Embedded/_Runtime deleted file mode 120000 index af934baa2..000000000 --- a/Examples/Embedded/_Runtime +++ /dev/null @@ -1 +0,0 @@ -../../Sources/JavaScriptKit/Runtime \ No newline at end of file diff --git a/Examples/Embedded/build.sh b/Examples/Embedded/build.sh index 1fde1fe91..f807cdbf5 100755 --- a/Examples/Embedded/build.sh +++ b/Examples/Embedded/build.sh @@ -1,5 +1,5 @@ #!/bin/bash package_dir="$(cd "$(dirname "$0")" && pwd)" JAVASCRIPTKIT_EXPERIMENTAL_EMBEDDED_WASM=true \ - swift build --package-path "$package_dir" --product EmbeddedApp \ - -c release --triple wasm32-unknown-none-wasm + 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 index d94796a09..a674baca1 100644 --- a/Examples/Embedded/index.html +++ b/Examples/Embedded/index.html @@ -6,7 +6,10 @@ - + diff --git a/Examples/Embedded/index.js b/Examples/Embedded/index.js deleted file mode 100644 index b95576135..000000000 --- a/Examples/Embedded/index.js +++ /dev/null @@ -1,33 +0,0 @@ -import { WASI, File, OpenFile, ConsoleStdout, PreopenDirectory } from 'https://esm.run/@bjorn3/browser_wasi_shim@0.3.0'; - -async function main(configuration = "release") { - // Fetch our Wasm File - const response = await fetch(`./.build/${configuration}/EmbeddedApp.wasm`); - // Create a new WASI system instance - const wasi = new WASI(/* args */["main.wasm"], /* env */[], /* fd */[ - new OpenFile(new File([])), // stdin - ConsoleStdout.lineBuffered((stdout) => { - console.log(stdout); - }), - ConsoleStdout.lineBuffered((stderr) => { - console.error(stderr); - }), - new PreopenDirectory("/", new Map()), - ]) - const { SwiftRuntime } = await import(`./_Runtime/index.mjs`); - // Create a new Swift Runtime instance to interact with JS and Swift - const swift = new SwiftRuntime(); - // Instantiate the WebAssembly file - const { instance } = await WebAssembly.instantiateStreaming(response, { - //wasi_snapshot_preview1: wasi.wasiImport, - javascript_kit: swift.wasmImports, - }); - // Set the WebAssembly instance to the Swift Runtime - swift.setInstance(instance); - // Start the WebAssembly WASI reactor instance - wasi.initialize(instance); - // Start Swift main function - swift.main() -}; - -main(); diff --git a/Examples/Multithreading/Sources/JavaScript/index.js b/Examples/Multithreading/Sources/JavaScript/index.js deleted file mode 100644 index 3cfc01a43..000000000 --- a/Examples/Multithreading/Sources/JavaScript/index.js +++ /dev/null @@ -1,74 +0,0 @@ -import { instantiate } from "./instantiate.js" -import * as WasmImportsParser from 'https://esm.run/wasm-imports-parser/polyfill.js'; - -// TODO: Remove this polyfill once the browser supports the WebAssembly Type Reflection JS API -// https://chromestatus.com/feature/5725002447978496 -globalThis.WebAssembly = WasmImportsParser.polyfill(globalThis.WebAssembly); - -class ThreadRegistry { - workers = new Map(); - nextTid = 1; - - constructor({ configuration }) { - this.configuration = configuration; - } - - spawnThread(worker, module, memory, startArg) { - const tid = this.nextTid++; - this.workers.set(tid, worker); - worker.postMessage({ module, memory, tid, startArg, configuration: this.configuration }); - return tid; - } - - listenMessageFromWorkerThread(tid, listener) { - const worker = this.workers.get(tid); - worker.onmessage = (event) => { - listener(event.data); - }; - } - - postMessageToWorkerThread(tid, data, transfer) { - const worker = this.workers.get(tid); - worker.postMessage(data, transfer); - } - - terminateWorkerThread(tid) { - const worker = this.workers.get(tid); - worker.terminate(); - this.workers.delete(tid); - } -} - -async function start(configuration = "release") { - const response = await fetch(`./.build/${configuration}/MyApp.wasm`); - const module = await WebAssembly.compileStreaming(response); - const memoryImport = WebAssembly.Module.imports(module).find(i => i.module === "env" && i.name === "memory"); - if (!memoryImport) { - throw new Error("Memory import not found"); - } - if (!memoryImport.type) { - throw new Error("Memory import type not found"); - } - const memoryType = memoryImport.type; - const memory = new WebAssembly.Memory({ initial: memoryType.minimum, maximum: memoryType.maximum, shared: true }); - const threads = new ThreadRegistry({ configuration }); - const { instance, swiftRuntime, wasi } = await instantiate({ - module, - threadChannel: threads, - addToImports(importObject) { - importObject["env"] = { memory } - importObject["wasi"] = { - "thread-spawn": (startArg) => { - const worker = new Worker("Sources/JavaScript/worker.js", { type: "module" }); - return threads.spawnThread(worker, module, memory, startArg); - } - }; - }, - configuration - }); - wasi.initialize(instance); - - swiftRuntime.main(); -} - -start(); diff --git a/Examples/Multithreading/Sources/JavaScript/instantiate.js b/Examples/Multithreading/Sources/JavaScript/instantiate.js deleted file mode 100644 index e7b60504c..000000000 --- a/Examples/Multithreading/Sources/JavaScript/instantiate.js +++ /dev/null @@ -1,29 +0,0 @@ -import { WASI, File, OpenFile, ConsoleStdout, PreopenDirectory } from 'https://esm.run/@bjorn3/browser_wasi_shim@0.3.0'; - -export async function instantiate({ module, addToImports, threadChannel, configuration }) { - const args = ["main.wasm"] - const env = [] - const fds = [ - new OpenFile(new File([])), // stdin - ConsoleStdout.lineBuffered((stdout) => { - console.log(stdout); - }), - ConsoleStdout.lineBuffered((stderr) => { - console.error(stderr); - }), - new PreopenDirectory("/", new Map()), - ]; - const wasi = new WASI(args, env, fds); - - const { SwiftRuntime } = await import(`/.build/${configuration}/JavaScriptKit_JavaScriptKit.resources/Runtime/index.mjs`); - const swiftRuntime = new SwiftRuntime({ sharedMemory: true, threadChannel }); - const importObject = { - wasi_snapshot_preview1: wasi.wasiImport, - javascript_kit: swiftRuntime.wasmImports, - }; - addToImports(importObject); - const instance = await WebAssembly.instantiate(module, importObject); - - swiftRuntime.setInstance(instance); - return { swiftRuntime, wasi, instance }; -} diff --git a/Examples/Multithreading/Sources/JavaScript/worker.js b/Examples/Multithreading/Sources/JavaScript/worker.js deleted file mode 100644 index 703df4407..000000000 --- a/Examples/Multithreading/Sources/JavaScript/worker.js +++ /dev/null @@ -1,28 +0,0 @@ -import { instantiate } from "./instantiate.js" - -self.onmessage = async (event) => { - const { module, memory, tid, startArg, configuration } = event.data; - const { instance, wasi, swiftRuntime } = await instantiate({ - module, - threadChannel: { - postMessageToMainThread: (message, transfer) => { - // Send the job to the main thread - postMessage(message, transfer); - }, - listenMessageFromMainThread: (listener) => { - self.onmessage = (event) => listener(event.data); - } - }, - addToImports(importObject) { - importObject["env"] = { memory } - importObject["wasi"] = { - "thread-spawn": () => { throw new Error("Cannot spawn a new thread from a worker thread"); } - }; - }, - configuration - }); - - swiftRuntime.setInstance(instance); - wasi.inst = instance; - swiftRuntime.startThread(tid, startArg); -} diff --git a/Examples/Multithreading/build.sh b/Examples/Multithreading/build.sh index 0f8670db1..c82a10c32 100755 --- a/Examples/Multithreading/build.sh +++ b/Examples/Multithreading/build.sh @@ -1 +1,3 @@ -swift build --swift-sdk "${SWIFT_SDK_ID:-wasm32-unknown-wasip1-threads}" -Xswiftc -Xclang-linker -Xswiftc -mexec-model=reactor -Xlinker --export=__main_argc_argv -c release -Xswiftc -g +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 index 6ed31039d..74ba8cfed 100644 --- a/Examples/Multithreading/index.html +++ b/Examples/Multithreading/index.html @@ -27,25 +27,28 @@ - +

Threading Example

-

- - -
-
- - -
-
- - - -
+
+ + +
+
+ + +
+
+ + + +

-

🧵
+
🧵

diff --git a/Examples/OffscrenCanvas/build.sh b/Examples/OffscrenCanvas/build.sh index 0f8670db1..c82a10c32 100755 --- a/Examples/OffscrenCanvas/build.sh +++ b/Examples/OffscrenCanvas/build.sh @@ -1 +1,3 @@ -swift build --swift-sdk "${SWIFT_SDK_ID:-wasm32-unknown-wasip1-threads}" -Xswiftc -Xclang-linker -Xswiftc -mexec-model=reactor -Xlinker --export=__main_argc_argv -c release -Xswiftc -g +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/OffscrenCanvas/index.html b/Examples/OffscrenCanvas/index.html index 5887c66cc..1202807a0 100644 --- a/Examples/OffscrenCanvas/index.html +++ b/Examples/OffscrenCanvas/index.html @@ -68,7 +68,10 @@ - +

OffscreenCanvas Example

diff --git a/Examples/OffscrenCanvas/serve.json b/Examples/OffscrenCanvas/serve.json deleted file mode 120000 index 326719cd4..000000000 --- a/Examples/OffscrenCanvas/serve.json +++ /dev/null @@ -1 +0,0 @@ -../Multithreading/serve.json \ No newline at end of file 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/.gitignore b/Examples/Testing/.gitignore new file mode 100644 index 000000000..0023a5340 --- /dev/null +++ b/Examples/Testing/.gitignore @@ -0,0 +1,8 @@ +.DS_Store +/.build +/Packages +xcuserdata/ +DerivedData/ +.swiftpm/configuration/registries.json +.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata +.netrc diff --git a/Examples/Testing/Package.swift b/Examples/Testing/Package.swift new file mode 100644 index 000000000..2e997652f --- /dev/null +++ b/Examples/Testing/Package.swift @@ -0,0 +1,28 @@ +// swift-tools-version: 6.0 +// The swift-tools-version declares the minimum version of Swift required to build this package. + +import PackageDescription + +let package = Package( + name: "Counter", + products: [ + // Products define the executables and libraries a package produces, making them visible to other packages. + .library( + name: "Counter", + targets: ["Counter"]), + ], + dependencies: [.package(name: "JavaScriptKit", path: "../../")], + targets: [ + // Targets are the basic building blocks of a package, defining a module or a test suite. + // Targets can depend on other targets in this package and products from dependencies. + .target( + name: "Counter", + dependencies: [ + .product(name: "JavaScriptKit", package: "JavaScriptKit") + ]), + .testTarget( + name: "CounterTests", + dependencies: ["Counter"] + ), + ] +) 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..4421c1223 --- /dev/null +++ b/Examples/Testing/Tests/CounterTests/CounterTests.swift @@ -0,0 +1,36 @@ +@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 + +import XCTest + +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/Makefile b/Makefile index 88f4e0795..ed0727ce8 100644 --- a/Makefile +++ b/Makefile @@ -21,18 +21,10 @@ test: CONFIGURATION=release SWIFT_BUILD_FLAGS="$(SWIFT_BUILD_FLAGS)" $(MAKE) test && \ CONFIGURATION=release SWIFT_BUILD_FLAGS="$(SWIFT_BUILD_FLAGS) -Xswiftc -DJAVASCRIPTKIT_WITHOUT_WEAKREFS" $(MAKE) test -TEST_RUNNER := node --experimental-wasi-unstable-preview1 scripts/test-harness.mjs .PHONY: unittest unittest: @echo Running unit tests - swift build --build-tests -Xswiftc -Xclang-linker -Xswiftc -mexec-model=reactor -Xlinker --export-if-defined=main -Xlinker --export-if-defined=__main_argc_argv --static-swift-stdlib -Xswiftc -static-stdlib $(SWIFT_BUILD_FLAGS) -# Swift 6.1 and later uses .xctest for XCTest bundle but earliers used .wasm -# See https://github.com/swiftlang/swift-package-manager/pull/8254 - if [ -f .build/debug/JavaScriptKitPackageTests.xctest ]; then \ - $(TEST_RUNNER) .build/debug/JavaScriptKitPackageTests.xctest; \ - else \ - $(TEST_RUNNER) .build/debug/JavaScriptKitPackageTests.wasm; \ - fi + swift package --swift-sdk "$(SWIFT_SDK_ID)" js test --prelude ./Tests/prelude.mjs .PHONY: benchmark_setup benchmark_setup: diff --git a/Package.swift b/Package.swift index 4d4634b88..7c49f0e33 100644 --- a/Package.swift +++ b/Package.swift @@ -4,6 +4,7 @@ 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 = shouldBuildForEmbedded || (Context.environment["JAVASCRIPTKIT_USE_LEGACY_RESOURCE_BUNDLING"].flatMap(Bool.init) ?? false) let package = Package( name: "JavaScriptKit", @@ -12,12 +13,14 @@ let package = Package( .library(name: "JavaScriptEventLoop", targets: ["JavaScriptEventLoop"]), .library(name: "JavaScriptBigIntSupport", targets: ["JavaScriptBigIntSupport"]), .library(name: "JavaScriptEventLoopTestSupport", targets: ["JavaScriptEventLoopTestSupport"]), + .plugin(name: "PackageToJS", targets: ["PackageToJS"]), ], targets: [ .target( name: "JavaScriptKit", dependencies: ["_CJavaScriptKit"], - resources: shouldBuildForEmbedded ? [] : [.copy("Runtime")], + exclude: useLegacyResourceBundling ? ["Runtime"] : [], + resources: useLegacyResourceBundling ? [] : [.copy("Runtime")], cSettings: shouldBuildForEmbedded ? [ .unsafeFlags(["-fdeclspec"]) ] : nil, @@ -71,5 +74,12 @@ let package = Package( "JavaScriptEventLoopTestSupport" ] ), + .plugin( + name: "PackageToJS", + capability: .command( + intent: .custom(verb: "js", description: "Convert a Swift package to a JavaScript package") + ), + sources: ["Sources"] + ), ] ) diff --git a/Plugins/PackageToJS/Package.swift b/Plugins/PackageToJS/Package.swift new file mode 100644 index 000000000..1cc9318bd --- /dev/null +++ b/Plugins/PackageToJS/Package.swift @@ -0,0 +1,12 @@ +// swift-tools-version: 6.0 + +import PackageDescription + +let package = Package( + name: "PackageToJS", + platforms: [.macOS(.v13)], + targets: [ + .target(name: "PackageToJS"), + .testTarget(name: "PackageToJSTests", dependencies: ["PackageToJS"]), + ] +) diff --git a/Plugins/PackageToJS/Sources/MiniMake.swift b/Plugins/PackageToJS/Sources/MiniMake.swift new file mode 100644 index 000000000..04e781690 --- /dev/null +++ b/Plugins/PackageToJS/Sources/MiniMake.swift @@ -0,0 +1,251 @@ +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: Codable { + /// Input tasks not yet built + let wants: [TaskKey] + /// Set of files that must be built before this task + let inputs: [String] + /// Output task name + let output: String + /// 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 not yet built + let wants: Set + /// Attributes of the task + let attributes: Set + /// Display name of the task + let displayName: String + /// Key of the task + let key: TaskKey + /// Build operation + let build: (Task) throws -> Void + /// Whether the task is done + var isDone: Bool + + var inputs: [String] { self.info.inputs } + var output: String { self.info.output } + } + + /// A task key + struct TaskKey: Codable, Hashable, Comparable, CustomStringConvertible { + let id: String + var description: String { self.id } + + fileprivate init(id: String) { + self.id = id + } + + static func < (lhs: TaskKey, rhs: TaskKey) -> Bool { lhs.id < rhs.id } + } + + /// All tasks in the build system + private var tasks: [TaskKey: Task] + /// Whether to explain why tasks are built + private var shouldExplain: Bool + /// Current working directory at the time the build started + private let buildCwd: String + /// Prints progress of the build + private var printProgress: ProgressPrinter.PrintProgress + + init( + explain: Bool = false, + printProgress: @escaping ProgressPrinter.PrintProgress + ) { + self.tasks = [:] + self.shouldExplain = explain + self.buildCwd = FileManager.default.currentDirectoryPath + self.printProgress = printProgress + } + + /// Adds a task to the build system + mutating func addTask( + inputFiles: [String] = [], inputTasks: [TaskKey] = [], output: String, + attributes: [TaskAttribute] = [], salt: (any Encodable)? = nil, + build: @escaping (Task) throws -> Void + ) -> TaskKey { + let displayName = + output.hasPrefix(self.buildCwd) + ? String(output.dropFirst(self.buildCwd.count + 1)) : output + let taskKey = TaskKey(id: output) + 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), + displayName: displayName, 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) throws -> Data { + let encoder = JSONEncoder() + encoder.outputFormatting = .sortedKeys + 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 { + typealias PrintProgress = (_ subject: Task, _ total: Int, _ built: Int, _ 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) { + self.print(task, "\(Self.green)building\(Self.reset)") + } + + mutating func skipped(_ task: Task) { + self.print(task, "\(Self.yellow)skipped\(Self.reset)") + } + + private mutating func print(_ task: Task, _ message: @autoclosure () -> String) { + guard !task.attributes.contains(.silent) else { return } + self.printProgress(task, self.total, self.built, 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() { + for task in self.tasks.values { + try? FileManager.default.removeItem(atPath: task.output) + } + } + + /// Starts building + func build(output: TaskKey) 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 = URL(fileURLWithPath: task.output) + if !FileManager.default.fileExists(atPath: task.output) { + 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 = URL(fileURLWithPath: input) + // Ignore directory modification times + var isDirectory: ObjCBool = false + let fileExists = FileManager.default.fileExists( + atPath: input, 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) + try task.build(task) + } else { + progressPrinter.skipped(task) + } + task.isDone = true + tasks[taskKey] = task + } + try runTask(taskKey: output) + } +} diff --git a/Plugins/PackageToJS/Sources/PackageToJS.swift b/Plugins/PackageToJS/Sources/PackageToJS.swift new file mode 100644 index 000000000..a575980d2 --- /dev/null +++ b/Plugins/PackageToJS/Sources/PackageToJS.swift @@ -0,0 +1,417 @@ +import Foundation + +struct PackageToJS { + struct PackageOptions { + /// Path to the output directory + var outputPath: String? + /// Name of the package (default: lowercased Package.swift name) + var packageName: String? + /// Whether to explain the build plan + var explain: Bool = false + /// Whether to use CDN for dependency packages + var useCDN: Bool + } + + struct BuildOptions { + /// Product to build (default: executable target if there's only one) + var product: String? + /// Whether to split debug information into a separate file (default: false) + var splitDebug: Bool + /// Whether to apply wasm-opt optimizations in release mode (default: true) + var noOptimize: Bool + /// 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 options for packaging + var packageOptions: PackageOptions + } +} + +struct PackageToJSError: Swift.Error, CustomStringConvertible { + let description: String + + init(_ message: String) { + self.description = "Error: " + message + } +} + +/// 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: URL + /// The path of this file itself, used to capture changes of planner code + let selfPath: String + /// The directory for the final output + let outputDir: URL + /// The directory for intermediate files + let intermediatesDir: URL + /// The filename of the .wasm file + let wasmFilename = "main.wasm" + /// The path to the .wasm product artifact + let wasmProductArtifact: URL + + init( + options: PackageToJS.PackageOptions, + packageId: String, + pluginWorkDirectoryURL: URL, + selfPackageDir: URL, + outputDir: URL, + wasmProductArtifact: URL + ) { + self.options = options + self.packageId = packageId + self.selfPackageDir = selfPackageDir + self.outputDir = outputDir + self.intermediatesDir = pluginWorkDirectoryURL.appending(path: outputDir.lastPathComponent + ".tmp") + self.selfPath = String(#filePath) + self.wasmProductArtifact = wasmProductArtifact + } + + // MARK: - Primitive build operations + + private static 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 + ) + } + + private static func createDirectory(atPath: String) throws { + guard !FileManager.default.fileExists(atPath: atPath) else { return } + try FileManager.default.createDirectory( + atPath: atPath, withIntermediateDirectories: true, attributes: nil + ) + } + + private static 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)") + } + } + + // 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, splitDebug: buildOptions.splitDebug, noOptimize: buildOptions.noOptimize + ) + return make.addTask( + inputTasks: allTasks, output: "all", attributes: [.phony, .silent] + ) { _ in } + } + + func deriveBuildConfiguration() -> (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 = self.wasmProductArtifact.resolvingSymlinksInPath() + let buildConfiguration = wasmProductArtifact.deletingLastPathComponent().lastPathComponent + let triple = wasmProductArtifact.deletingLastPathComponent().deletingLastPathComponent().lastPathComponent + return (buildConfiguration, triple) + } + + private func planBuildInternal( + make: inout MiniMake, + splitDebug: Bool, noOptimize: Bool + ) throws -> ( + allTasks: [MiniMake.TaskKey], + outputDirTask: MiniMake.TaskKey, + packageJsonTask: MiniMake.TaskKey + ) { + // Prepare output directory + let outputDirTask = make.addTask( + inputFiles: [selfPath], output: outputDir.path, attributes: [.silent] + ) { + try Self.createDirectory(atPath: $0.output) + } + + var packageInputs: [MiniMake.TaskKey] = [] + + // Guess the build configuration from the parent directory name of .wasm file + let (buildConfiguration, _) = deriveBuildConfiguration() + let wasm: MiniMake.TaskKey + + let shouldOptimize: Bool + let wasmOptPath = try? which("wasm-opt") + if buildConfiguration == "debug" { + shouldOptimize = false + } else { + if wasmOptPath != nil { + shouldOptimize = !noOptimize + } else { + print("Warning: wasm-opt not found in PATH, skipping optimizations") + shouldOptimize = false + } + } + + let intermediatesDirTask = make.addTask( + inputFiles: [selfPath], output: intermediatesDir.path, attributes: [.silent] + ) { + try Self.createDirectory(atPath: $0.output) + } + + let finalWasmPath = outputDir.appending(path: wasmFilename).path + + if let wasmOptPath = wasmOptPath, shouldOptimize { + // Optimize the wasm in release mode + // If splitDebug is true, we need to place the DWARF-stripped wasm file (but "name" section remains) + // in the output directory. + let stripWasmPath = (splitDebug ? outputDir : intermediatesDir).appending(path: wasmFilename + ".debug").path + + // First, strip DWARF sections as their existence enables DWARF preserving mode in wasm-opt + let stripWasm = make.addTask( + inputFiles: [selfPath, wasmProductArtifact.path], inputTasks: [outputDirTask, intermediatesDirTask], + output: stripWasmPath + ) { + print("Stripping DWARF debug info...") + try Self.runCommand(wasmOptPath, [wasmProductArtifact.path, "--strip-dwarf", "--debuginfo", "-o", $0.output]) + } + // Then, run wasm-opt with all optimizations + wasm = make.addTask( + inputFiles: [selfPath], inputTasks: [outputDirTask, stripWasm], + output: finalWasmPath + ) { + print("Optimizing the wasm file...") + try Self.runCommand(wasmOptPath, [stripWasmPath, "-Os", "-o", $0.output]) + } + } else { + // Copy the wasm product artifact + wasm = make.addTask( + inputFiles: [selfPath, wasmProductArtifact.path], inputTasks: [outputDirTask], + output: finalWasmPath + ) { + try Self.syncFile(from: wasmProductArtifact.path, to: $0.output) + } + } + packageInputs.append(wasm) + + let wasmImportsPath = intermediatesDir.appending(path: "wasm-imports.json") + let wasmImportsTask = make.addTask( + inputFiles: [selfPath, finalWasmPath], inputTasks: [outputDirTask, intermediatesDirTask, wasm], + output: wasmImportsPath.path + ) { + let metadata = try parseImports(moduleBytes: Array(try Data(contentsOf: URL(fileURLWithPath: finalWasmPath)))) + let jsonEncoder = JSONEncoder() + jsonEncoder.outputFormatting = .prettyPrinted + let jsonData = try jsonEncoder.encode(metadata) + try jsonData.write(to: URL(fileURLWithPath: $0.output)) + } + + packageInputs.append(wasmImportsTask) + + let platformsDir = outputDir.appending(path: "platforms") + let platformsDirTask = make.addTask( + inputFiles: [selfPath], output: platformsDir.path, attributes: [.silent] + ) { + try Self.createDirectory(atPath: $0.output) + } + + let packageJsonTask = planCopyTemplateFile( + make: &make, file: "Plugins/PackageToJS/Templates/package.json", output: "package.json", outputDirTask: outputDirTask, + inputFiles: [], inputTasks: [] + ) + + // 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"), + ] { + packageInputs.append(planCopyTemplateFile( + make: &make, file: file, output: output, outputDirTask: outputDirTask, + inputFiles: [wasmImportsPath.path], inputTasks: [platformsDirTask, wasmImportsTask], + wasmImportsPath: wasmImportsPath.path + )) + } + return (packageInputs, outputDirTask, packageJsonTask) + } + + /// Construct the test build plan and return the root task key + func planTestBuild( + make: inout MiniMake + ) throws -> (rootTask: MiniMake.TaskKey, binDir: URL) { + var (allTasks, outputDirTask, packageJsonTask) = try planBuildInternal( + make: &make, splitDebug: false, noOptimize: false + ) + + // Install npm dependencies used in the test harness + let npm = try which("npm") + allTasks.append(make.addTask( + inputFiles: [ + selfPath, + outputDir.appending(path: "package.json").path, + ], inputTasks: [outputDirTask, packageJsonTask], + output: intermediatesDir.appending(path: "npm-install.stamp").path + ) { + try Self.runCommand(npm, ["-C", outputDir.path, "install"]) + _ = FileManager.default.createFile(atPath: $0.output, contents: Data(), attributes: nil) + }) + + let binDir = outputDir.appending(path: "bin") + let binDirTask = make.addTask( + inputFiles: [selfPath], inputTasks: [outputDirTask], + output: binDir.path + ) { + try Self.createDirectory(atPath: $0.output) + } + 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: "all", attributes: [.phony, .silent] + ) { _ in } + return (rootTask, binDir) + } + + private func planCopyTemplateFile( + make: inout MiniMake, + file: String, + output: String, + outputDirTask: MiniMake.TaskKey, + inputFiles: [String], + inputTasks: [MiniMake.TaskKey], + wasmImportsPath: String? = nil + ) -> MiniMake.TaskKey { + + struct Salt: Encodable { + let conditions: [String: Bool] + let substitutions: [String: String] + } + + let inputPath = selfPackageDir.appending(path: file) + let (_, triple) = deriveBuildConfiguration() + let conditions = [ + "USE_SHARED_MEMORY": triple == "wasm32-unknown-wasip1-threads", + "IS_WASI": triple.hasPrefix("wasm32-unknown-wasi"), + "USE_WASI_CDN": options.useCDN, + ] + let constantSubstitutions = [ + "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.path] + inputFiles, inputTasks: [outputDirTask] + inputTasks, + output: outputDir.appending(path: output).path, salt: salt + ) { + var substitutions = constantSubstitutions + + if let wasmImportsPath = wasmImportsPath { + let importEntries = try JSONDecoder().decode([ImportEntry].self, from: Data(contentsOf: URL(fileURLWithPath: 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)" + substitutions["PACKAGE_TO_JS_MEMORY_MAXIMUM"] = "\(type.maximum ?? type.minimum)" + substitutions["PACKAGE_TO_JS_MEMORY_SHARED"] = "\(type.shared)" + } + } + + var content = try String(contentsOf: inputPath, encoding: .utf8) + let options = PreprocessOptions(conditions: conditions, substitutions: substitutions) + content = try preprocess(source: content, file: file, options: options) + try content.write(toFile: $0.output, atomically: true, encoding: .utf8) + } + } +} + +// MARK: - Utilities + +func which(_ executable: String) throws -> 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") +} + +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..7e12eb94f --- /dev/null +++ b/Plugins/PackageToJS/Sources/PackageToJSPlugin.swift @@ -0,0 +1,471 @@ +#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 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") + else { return nil } + let didYouMean = + [ + "swift", "package", "--swift-sdk", "wasm32-unknown-wasi", "js", + ] + arguments + return """ + Please pass the `--swift-sdk` option to the "swift package" command. + + Did you mean: + \(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 + """ + }), + ] + private func reportBuildFailure( + _ build: PackageManager.BuildResult, _ arguments: [String] + ) { + for diagnostic in Self.friendlyBuildDiagnostics { + if let message = diagnostic(build, arguments) { + printStderr("\n" + message) + } + } + } + + func performCommand(context: PluginContext, arguments: [String]) throws { + if arguments.first == "test" { + return try performTestCommand(context: context, arguments: Array(arguments.dropFirst())) + } + + return try performBuildCommand(context: context, arguments: arguments) + } + + 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 productName = try buildOptions.product ?? deriveDefaultProduct(package: context.package) + let build = try buildWasm( + productName: productName, context: context) + guard build.succeeded else { + reportBuildFailure(build, arguments) + exit(1) + } + let productArtifact = try build.findWasmArtifact(for: productName) + let outputDir = + if let outputPath = buildOptions.packageOptions.outputPath { + URL(fileURLWithPath: outputPath) + } else { + context.pluginWorkDirectoryURL.appending(path: "Package") + } + guard + let selfPackage = findPackageInDependencies( + package: context.package, id: Self.JAVASCRIPTKIT_PACKAGE_ID) + else { + throw PackageToJSError("Failed to find JavaScriptKit in dependencies!?") + } + var make = MiniMake( + explain: buildOptions.packageOptions.explain, + printProgress: self.printProgress + ) + let planner = PackagingPlanner( + options: buildOptions.packageOptions, context: context, selfPackage: selfPackage, + outputDir: outputDir, wasmProductArtifact: productArtifact) + let rootTask = try planner.planBuild( + make: &make, buildOptions: buildOptions) + cleanIfBuildGraphChanged(root: rootTask, make: make, context: context) + print("Packaging...") + try make.build(output: rootTask) + 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 productName = "\(context.package.displayName)PackageTests" + let build = try buildWasm( + productName: productName, context: context) + guard build.succeeded else { + reportBuildFailure(build, arguments) + exit(1) + } + + // 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 path = ".build/debug/\(productName).\(fileExtension)" + 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") + } + guard + let selfPackage = findPackageInDependencies( + package: context.package, id: Self.JAVASCRIPTKIT_PACKAGE_ID) + else { + throw PackageToJSError("Failed to find JavaScriptKit in dependencies!?") + } + var make = MiniMake( + explain: testOptions.packageOptions.explain, + printProgress: self.printProgress + ) + let planner = PackagingPlanner( + options: testOptions.packageOptions, context: context, selfPackage: selfPackage, + outputDir: outputDir, wasmProductArtifact: productArtifact) + let (rootTask, binDir) = try planner.planTestBuild( + make: &make) + cleanIfBuildGraphChanged(root: rootTask, make: make, context: context) + print("Packaging tests...") + try make.build(output: rootTask) + print("Packaging tests finished") + + let testRunner = binDir.appending(path: "test.js") + if !testOptions.buildOnly { + var testJsArguments: [String] = [] + var testFrameworkArguments: [String] = [] + if testOptions.listTests { + testFrameworkArguments += ["--list-tests"] + } + if let prelude = testOptions.prelude { + let preludeURL = URL(fileURLWithPath: prelude, relativeTo: URL(fileURLWithPath: FileManager.default.currentDirectoryPath)) + testJsArguments += ["--prelude", preludeURL.path] + } + if let environment = testOptions.environment { + testJsArguments += ["--environment", environment] + } + if testOptions.inspect { + testJsArguments += ["--inspect"] + } + try runTest( + testRunner: testRunner, context: context, + extraArguments: testJsArguments + ["--"] + testFrameworkArguments + testOptions.filter + ) + try runTest( + testRunner: testRunner, context: context, + extraArguments: testJsArguments + ["--", "--testing-library", "swift-testing"] + testFrameworkArguments + + testOptions.filter.flatMap { ["--filter", $0] } + ) + } + } + + private func runTest(testRunner: URL, context: PluginContext, extraArguments: [String]) throws { + let node = try which("node") + let arguments = ["--experimental-wasi-unstable-preview1", testRunner.path] + extraArguments + print("Running test...") + logCommandExecution(node.path, arguments) + + let task = Process() + task.executableURL = node + task.arguments = arguments + task.currentDirectoryURL = context.pluginWorkDirectoryURL + try task.forwardTerminationSignals { + try task.run() + task.waitUntilExit() + } + // swift-testing returns EX_UNAVAILABLE (which is 69 in wasi-libc) for "no tests found" + guard task.terminationStatus == 0 || task.terminationStatus == 69 else { + throw PackageToJSError("Test failed with status \(task.terminationStatus)") + } + } + + private func buildWasm(productName: String, context: PluginContext) throws + -> PackageManager.BuildResult + { + var parameters = PackageManager.BuildParameters( + configuration: .inherit, + logging: .concise + ) + parameters.echoLogs = true + let buildingForEmbedded = + ProcessInfo.processInfo.environment["JAVASCRIPTKIT_EXPERIMENTAL_EMBEDDED_WASM"].flatMap( + Bool.init) ?? false + if !buildingForEmbedded { + // 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" + ] + } + return try self.packageManager.build(.product(productName), parameters: parameters) + } + + /// 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 { + print("Build graph changed, cleaning...") + make.cleanEverything() + } + try? currentBuildFingerprint?.write(to: buildFingerprint) + } + + private func printProgress(task: MiniMake.Task, total: Int, built: Int, message: String) { + printStderr("[\(built + 1)/\(total)] \(task.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 packageName = extractor.extractOption(named: "package-name").last + let explain = extractor.extractFlag(named: "explain") + let useCDN = extractor.extractFlag(named: "use-cdn") + return PackageToJS.PackageOptions( + outputPath: outputPath, packageName: packageName, explain: explain != 0, useCDN: useCDN != 0 + ) + } +} + +extension PackageToJS.BuildOptions { + static func parse(from extractor: inout ArgumentExtractor) -> PackageToJS.BuildOptions { + let product = extractor.extractOption(named: "product").last + let splitDebug = extractor.extractFlag(named: "split-debug") + let noOptimize = extractor.extractFlag(named: "no-optimize") + let packageOptions = PackageToJS.PackageOptions.parse(from: &extractor) + return PackageToJS.BuildOptions(product: product, splitDebug: splitDebug != 0, noOptimize: noOptimize != 0, packageOptions: packageOptions) + } + + static func help() -> String { + return """ + OVERVIEW: Builds a JavaScript module from a Swift package. + + USAGE: swift package --swift-sdk [SwiftPM options] PackageToJS [options] [subcommand] + + OPTIONS: + --product Product to build (default: executable target if there's only one) + --output Path to the output directory (default: .build/plugins/PackageToJS/outputs/Package) + --package-name Name of the package (default: lowercased Package.swift name) + --explain Whether to explain the build plan + --split-debug Whether to split debug information into a separate .wasm.debug file (default: false) + --no-optimize Whether to disable wasm-opt optimization (default: false) + + SUBCOMMANDS: + test Builds and runs tests + + EXAMPLES: + $ swift package --swift-sdk wasm32-unknown-wasi plugin js + # Build a specific product + $ swift package --swift-sdk wasm32-unknown-wasi plugin js --product Example + # Build in release configuration + $ swift package --swift-sdk wasm32-unknown-wasi -c release plugin js + + # Run tests + $ swift package --swift-sdk wasm32-unknown-wasi plugin 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 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, 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] PackageToJS test [options] + + OPTIONS: + --build-only Whether to build only (default: false) + --prelude Path to the prelude script + --environment The environment to use for the tests + --inspect Whether to run tests in the browser with inspector enabled + + EXAMPLES: + $ swift package --swift-sdk wasm32-unknown-wasi plugin js test + $ swift package --swift-sdk wasm32-unknown-wasi plugin js test --environment browser + # Just build tests, don't run them + $ swift package --swift-sdk wasm32-unknown-wasi plugin js test --build-only + $ node .build/plugins/PackageToJS/outputs/PackageTests/bin/test.js + """ + } +} + +// MARK: - PackagePlugin helpers + +/// 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) +} + +extension PackagingPlanner { + init( + options: PackageToJS.PackageOptions, + context: PluginContext, + selfPackage: Package, + outputDir: URL, + wasmProductArtifact: URL + ) { + self.init( + options: options, + packageId: context.package.id, + pluginWorkDirectoryURL: context.pluginWorkDirectoryURL, + selfPackageDir: selfPackage.directoryURL, + outputDir: outputDir, + wasmProductArtifact: wasmProductArtifact + ) + } +} + +#endif diff --git a/Plugins/PackageToJS/Sources/ParseWasm.swift b/Plugins/PackageToJS/Sources/ParseWasm.swift new file mode 100644 index 000000000..1cec9e43f --- /dev/null +++ b/Plugins/PackageToJS/Sources/ParseWasm.swift @@ -0,0 +1,312 @@ +/// Represents the type of value in WebAssembly +enum ValueType: String, Codable { + case i32 + case i64 + case f32 + case f64 + case funcref + case externref + case v128 +} + +/// Represents a function type in WebAssembly +struct FunctionType: Codable { + let parameters: [ValueType] + let results: [ValueType] +} + +/// Represents a table type in WebAssembly +struct TableType: Codable { + let element: ElementType + let minimum: UInt32 + let maximum: UInt32? + + enum ElementType: String, Codable { + 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: Codable { + 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(type: FunctionType) + case table(type: TableType) + case memory(type: MemoryType) + case global(type: GlobalType) + } +} + +/// Parse state for WebAssembly parsing +private class ParseState { + private let moduleBytes: [UInt8] + private var offset: Int + + init(moduleBytes: [UInt8]) { + 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/Templates/bin/test.js b/Plugins/PackageToJS/Templates/bin/test.js new file mode 100644 index 000000000..5fed17359 --- /dev/null +++ b/Plugins/PackageToJS/Templates/bin/test.js @@ -0,0 +1,75 @@ +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" + +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" }, + }, +}) + +const harnesses = { + node: async ({ preludeScript }) => { + let options = await nodePlatform.defaultNodeSetup({ + args: testFrameworkArgs, + /* #if USE_SHARED_MEMORY */ + spawnWorker: nodePlatform.createDefaultWorkerFactory(preludeScript) + /* #endif */ + }) + if (preludeScript) { + const prelude = await import(preludeScript) + if (prelude.setupOptions) { + options = prelude.setupOptions(options, { isMainThread: true }) + } + } + try { + await instantiate(options) + } catch (e) { + if (e instanceof WebAssembly.CompileError) { + } + 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..4a1074c14 --- /dev/null +++ b/Plugins/PackageToJS/Templates/index.d.ts @@ -0,0 +1,29 @@ +import type { Import, Export } from './instantiate.js' + +export type Options = { + /** + * The CLI arguments to pass to the WebAssembly module + */ + args?: string[] +/* #if USE_SHARED_MEMORY */ + /** + * The WebAssembly memory to use (must be 'shared') + */ + memory: WebAssembly.Memory +/* #endif */ +} + +/** + * Initialize the given WebAssembly module + * + * This is a convenience function that creates an instantiator and instantiates the module. + * @param moduleSource - The WebAssembly module to instantiate + * @param imports - The imports to add + * @param options - The options + */ +export declare function init( + moduleSource: WebAssembly.Module | ArrayBufferView | ArrayBuffer | Response | PromiseLike +): Promise<{ + instance: WebAssembly.Instance, + exports: Export +}> diff --git a/Plugins/PackageToJS/Templates/index.js b/Plugins/PackageToJS/Templates/index.js new file mode 100644 index 000000000..d0d28569f --- /dev/null +++ b/Plugins/PackageToJS/Templates/index.js @@ -0,0 +1,14 @@ +// @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(moduleSource) { + const options = await defaultBrowserSetup({ + module: moduleSource, +/* #if USE_SHARED_MEMORY */ + spawnWorker: createDefaultWorkerFactory() +/* #endif */ + }) + return await instantiate(options); +} diff --git a/Plugins/PackageToJS/Templates/instantiate.d.ts b/Plugins/PackageToJS/Templates/instantiate.d.ts new file mode 100644 index 000000000..f813b5489 --- /dev/null +++ b/Plugins/PackageToJS/Templates/instantiate.d.ts @@ -0,0 +1,103 @@ +/* #if USE_SHARED_MEMORY */ +import type { SwiftRuntimeThreadChannel, SwiftRuntime } from "./runtime.js"; +/* #endif */ + +export type Import = { + // TODO: Generate type from imported .d.ts files +} +export type Export = { + // TODO: Generate type from .swift files +} + +/** + * 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 +} + +export type ModuleSource = WebAssembly.Module | ArrayBufferView | ArrayBuffer | Response | PromiseLike + +/** + * The options for instantiating a WebAssembly module + */ +export type InstantiateOptions = { + /** + * The WebAssembly module to instantiate + */ + module: ModuleSource, + /** + * The imports provided by the embedder + */ + imports: Import, +/* #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) => void +} + +/** + * Instantiate the given WebAssembly module + */ +export declare function instantiate(options: InstantiateOptions): Promise<{ + instance: WebAssembly.Instance, + swift: SwiftRuntime, + exports: Export +}> + +/** + * 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: Export +}> diff --git a/Plugins/PackageToJS/Templates/instantiate.js b/Plugins/PackageToJS/Templates/instantiate.js new file mode 100644 index 000000000..d786c31ef --- /dev/null +++ b/Plugins/PackageToJS/Templates/instantiate.js @@ -0,0 +1,118 @@ +// @ts-check +// @ts-ignore +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 */ + +/** + * @param {import('./instantiate.d').InstantiateOptions} options + */ +async function createInstantiator(options) { + return { + /** @param {WebAssembly.Imports} importObject */ + addImports: (importObject) => {}, + /** @param {WebAssembly.Instance} instance */ + createExports: (instance) => { + return {}; + }, + } +} +/** @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 moduleSource = options.module; +/* #if IS_WASI */ + const { wasi } = options; +/* #endif */ + const instantiator = await createInstantiator(options); + const swift = new SwiftRuntime({ +/* #if USE_SHARED_MEMORY */ + sharedMemory: true, + threadChannel: options.threadChannel, +/* #endif */ + }); + + /** @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); + + let module; + let instance; + 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); + + return { + instance, + swift, + exports: instantiator.createExports(instance), + } +} 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..5b27cc903 --- /dev/null +++ b/Plugins/PackageToJS/Templates/platforms/browser.d.ts @@ -0,0 +1,15 @@ +import type { InstantiateOptions, ModuleSource } from "../instantiate.js" + +export async function defaultBrowserSetup(options: { + module: ModuleSource, +/* #if IS_WASI */ + args?: string[], + onStdoutLine?: (line: string) => void, + onStderrLine?: (line: string) => void, +/* #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..672c274db --- /dev/null +++ b/Plugins/PackageToJS/Templates/platforms/browser.js @@ -0,0 +1,136 @@ +// @ts-check +import { MODULE_PATH /* #if USE_SHARED_MEMORY */, MEMORY_TYPE /* #endif */} from "../instantiate.js" +/* #if IS_WASI */ +/* #if USE_WASI_CDN */ +import { WASI, File, OpenFile, ConsoleStdout, PreopenDirectory } from 'https://cdn.jsdelivr.net/npm/@bjorn3/browser_wasi_shim@0.4.1/+esm'; +/* #else */ +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, + imports: {}, +/* #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..433f97ad6 --- /dev/null +++ b/Plugins/PackageToJS/Templates/platforms/node.d.ts @@ -0,0 +1,13 @@ +import type { InstantiateOptions } from "../instantiate.js" +import type { Worker } from "node:worker_threads" + +export async function defaultNodeSetup(options: { +/* #if IS_WASI */ + args?: string[], +/* #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/node.js b/Plugins/PackageToJS/Templates/platforms/node.js new file mode 100644 index 000000000..a8bb638bc --- /dev/null +++ b/Plugins/PackageToJS/Templates/platforms/node.js @@ -0,0 +1,158 @@ +// @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 } 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 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("/", new Map()), + ], { 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; + } + }), + addToCoreImports(importObject) { + importObject["wasi_snapshot_preview1"]["proc_exit"] = (code) => { + process.exit(code); + } + }, +/* #endif */ +/* #if USE_SHARED_MEMORY */ + memory, threadChannel, +/* #endif */ + } +} diff --git a/Plugins/PackageToJS/Templates/test.browser.html b/Plugins/PackageToJS/Templates/test.browser.html new file mode 100644 index 000000000..27bfd25fc --- /dev/null +++ b/Plugins/PackageToJS/Templates/test.browser.html @@ -0,0 +1,32 @@ + + + + + + + + diff --git a/Plugins/PackageToJS/Templates/test.d.ts b/Plugins/PackageToJS/Templates/test.d.ts new file mode 100644 index 000000000..b3bbe54dd --- /dev/null +++ b/Plugins/PackageToJS/Templates/test.d.ts @@ -0,0 +1,12 @@ +import type { InstantiateOptions, instantiate } from "./instantiate"; + +export async function testBrowser( + options: { + preludeScript?: string, + args?: string[], + } +): Promise + +export async 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/Tests/ExampleProjectTests.swift b/Plugins/PackageToJS/Tests/ExampleProjectTests.swift new file mode 100644 index 000000000..1bcc25d48 --- /dev/null +++ b/Plugins/PackageToJS/Tests/ExampleProjectTests.swift @@ -0,0 +1,6 @@ +import Testing + +@Suite struct ExampleProjectTests { + @Test func example() throws { + } +} diff --git a/Plugins/PackageToJS/Tests/MiniMakeTests.swift b/Plugins/PackageToJS/Tests/MiniMakeTests.swift new file mode 100644 index 000000000..bb097115c --- /dev/null +++ b/Plugins/PackageToJS/Tests/MiniMakeTests.swift @@ -0,0 +1,203 @@ +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 outputPath = tempDir.appendingPathComponent("output.txt").path + + let task = make.addTask(output: outputPath) { task in + try "Hello".write(toFile: task.output, atomically: true, encoding: .utf8) + } + + try make.build(output: task) + let content = try String(contentsOfFile: outputPath, 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 input = tempDir.appendingPathComponent("input.txt").path + let intermediate = tempDir.appendingPathComponent("intermediate.txt").path + let output = tempDir.appendingPathComponent("output.txt").path + + try "Input".write(toFile: input, atomically: true, encoding: .utf8) + + let intermediateTask = make.addTask(inputFiles: [input], output: intermediate) { task in + let content = try String(contentsOfFile: task.inputs[0], encoding: .utf8) + try (content + " processed").write( + toFile: task.output, atomically: true, encoding: .utf8) + } + + let finalTask = make.addTask( + inputFiles: [intermediate], inputTasks: [intermediateTask], output: output + ) { task in + let content = try String(contentsOfFile: task.inputs[0], encoding: .utf8) + try (content + " final").write( + toFile: task.output, atomically: true, encoding: .utf8) + } + + try make.build(output: finalTask) + let content = try String(contentsOfFile: output, 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 outputPath = tempDir.appendingPathComponent("phony.txt").path + try "Hello".write(toFile: outputPath, atomically: true, encoding: .utf8) + var buildCount = 0 + + let task = make.addTask(output: outputPath, attributes: [.phony]) { task in + buildCount += 1 + try String(buildCount).write(toFile: task.output, atomically: true, encoding: .utf8) + } + + try make.build(output: task) + try make.build(output: task) + + #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 = "output1.txt" + + 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 input = tempDir.appendingPathComponent("input.txt").path + let output = tempDir.appendingPathComponent("output.txt").path + var buildCount = 0 + + try "Initial".write(toFile: input, atomically: true, encoding: .utf8) + + let task = make.addTask(inputFiles: [input], output: output) { task in + buildCount += 1 + let content = try String(contentsOfFile: task.inputs[0], encoding: .utf8) + try content.write(toFile: task.output, atomically: true, encoding: .utf8) + } + + // First build + try make.build(output: task) + #expect(buildCount == 1, "First build should occur") + + // Second build without changes + try make.build(output: task) + #expect(buildCount == 1, "No rebuild should occur if input is not modified") + + // Modify input and rebuild + try "Modified".write(toFile: input, atomically: true, encoding: .utf8) + try make.build(output: task) + #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: { task, total, built, message in + messages.append((URL(fileURLWithPath: task.output).lastPathComponent, total, built, message)) + } + ) + let silentOutputPath = tempDir.appendingPathComponent("silent.txt").path + let silentTask = make.addTask(output: silentOutputPath, attributes: [.silent]) { task in + try "Silent".write(toFile: task.output, atomically: true, encoding: .utf8) + } + let finalOutputPath = tempDir.appendingPathComponent("output.txt").path + let task = make.addTask( + inputTasks: [silentTask], output: finalOutputPath + ) { task in + try "Hello".write(toFile: task.output, atomically: true, encoding: .utf8) + } + + try make.build(output: task) + #expect(FileManager.default.fileExists(atPath: silentOutputPath), "Silent task should still create output file") + #expect(FileManager.default.fileExists(atPath: finalOutputPath), "Final task should create output file") + try #require(messages.count == 1, "Should print progress for the final task") + #expect(messages[0] == ("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 output = tempDir.appendingPathComponent("error.txt").path + + let task = make.addTask(output: output) { task in + throw BuildError() + } + + #expect(throws: BuildError.self) { + try make.build(output: task) + } + } + } + + // Test that cleanup functionality works correctly + @Test func cleanup() throws { + try withTemporaryDirectory { tempDir in + var make = MiniMake(printProgress: { _, _, _, _ in }) + let outputs = [ + tempDir.appendingPathComponent("clean1.txt").path, + tempDir.appendingPathComponent("clean2.txt").path, + ] + + // Create tasks and build them + let tasks = outputs.map { output in + make.addTask(output: output) { task in + try "Content".write(toFile: task.output, atomically: true, encoding: .utf8) + } + } + + for task in tasks { + try make.build(output: task) + } + + // Verify files exist + for output in outputs { + #expect( + FileManager.default.fileExists(atPath: output), + "Output file should exist before cleanup") + } + + // Clean everything + make.cleanEverything() + + // Verify files are removed + for output in outputs { + #expect( + !FileManager.default.fileExists(atPath: output), + "Output file should not exist after cleanup") + } + } + } +} diff --git a/Plugins/PackageToJS/Tests/PreprocessTests.swift b/Plugins/PackageToJS/Tests/PreprocessTests.swift new file mode 100644 index 000000000..9ebb7a161 --- /dev/null +++ b/Plugins/PackageToJS/Tests/PreprocessTests.swift @@ -0,0 +1,137 @@ +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/TemporaryDirectory.swift b/Plugins/PackageToJS/Tests/TemporaryDirectory.swift new file mode 100644 index 000000000..4aa543bbf --- /dev/null +++ b/Plugins/PackageToJS/Tests/TemporaryDirectory.swift @@ -0,0 +1,24 @@ +import Foundation + +struct MakeTemporaryDirectoryError: Error { + let error: CInt +} + +internal func withTemporaryDirectory(body: (URL) 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)) + defer { + try? FileManager.default.removeItem(at: tempDir) + } + return try body(tempDir) + } +} \ No newline at end of file diff --git a/Tests/JavaScriptEventLoopTests/WebWorkerTaskExecutorTests.swift b/Tests/JavaScriptEventLoopTests/WebWorkerTaskExecutorTests.swift index 16cfd6374..0dfdac25f 100644 --- a/Tests/JavaScriptEventLoopTests/WebWorkerTaskExecutorTests.swift +++ b/Tests/JavaScriptEventLoopTests/WebWorkerTaskExecutorTests.swift @@ -7,6 +7,21 @@ import _CJavaScriptKit // For swjs_get_worker_thread_id @_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 { override func setUp() async { WebWorkerTaskExecutor.installGlobalExecutor() diff --git a/Tests/prelude.mjs b/Tests/prelude.mjs new file mode 100644 index 000000000..53073a850 --- /dev/null +++ b/Tests/prelude.mjs @@ -0,0 +1,12 @@ +/** @type {import('./../.build/plugins/PackageToJS/outputs/PackageTests/test.d.ts').Prelude["setupOptions"]} */ +export function setupOptions(options, context) { + return { + ...options, + addToCoreImports(importObject) { + options.addToCoreImports?.(importObject); + importObject["JavaScriptEventLoopTestSupportTests"] = { + "isMainThread": () => context.isMainThread, + } + } + } +} diff --git a/scripts/test-harness.mjs b/scripts/test-harness.mjs deleted file mode 100644 index 065d6d7da..000000000 --- a/scripts/test-harness.mjs +++ /dev/null @@ -1,17 +0,0 @@ -Error.stackTraceLimit = Infinity; - -import { startWasiTask } from "../IntegrationTests/lib.js"; - -if (process.env["JAVASCRIPTKIT_WASI_BACKEND"] === "MicroWASI") { - console.log("Skipping XCTest tests for MicroWASI because it is not supported yet."); - process.exit(0); -} - -const handleExitOrError = (error) => { - console.log(error); - process.exit(1); -} - -Error.stackTraceLimit = Infinity; - -startWasiTask(process.argv[2]).catch(handleExitOrError); From 1fdbdbcfb430f4fa6743f7084f0be1f24f5fc31d Mon Sep 17 00:00:00 2001 From: Yuta Saito Date: Thu, 13 Mar 2025 15:40:50 +0000 Subject: [PATCH 251/373] Remove dead symlink --- Examples/OffscrenCanvas/Sources/JavaScript | 1 - 1 file changed, 1 deletion(-) delete mode 120000 Examples/OffscrenCanvas/Sources/JavaScript diff --git a/Examples/OffscrenCanvas/Sources/JavaScript b/Examples/OffscrenCanvas/Sources/JavaScript deleted file mode 120000 index b24c2256e..000000000 --- a/Examples/OffscrenCanvas/Sources/JavaScript +++ /dev/null @@ -1 +0,0 @@ -../../Multithreading/Sources/JavaScript \ No newline at end of file From c3ca9b096574c6aab75046e9a2369df3a9d6b853 Mon Sep 17 00:00:00 2001 From: Yuta Saito Date: Thu, 13 Mar 2025 15:49:37 +0000 Subject: [PATCH 252/373] Fix missing package.json dependency for build (not test) --- Plugins/PackageToJS/Sources/PackageToJS.swift | 1 + 1 file changed, 1 insertion(+) diff --git a/Plugins/PackageToJS/Sources/PackageToJS.swift b/Plugins/PackageToJS/Sources/PackageToJS.swift index a575980d2..c34b6a57b 100644 --- a/Plugins/PackageToJS/Sources/PackageToJS.swift +++ b/Plugins/PackageToJS/Sources/PackageToJS.swift @@ -242,6 +242,7 @@ struct PackagingPlanner { make: &make, file: "Plugins/PackageToJS/Templates/package.json", output: "package.json", outputDirTask: outputDirTask, inputFiles: [], inputTasks: [] ) + packageInputs.append(packageJsonTask) // Copy the template files for (file, output) in [ From 2a8b343082cfb8e7c6d1b1d14f98da369d689ed4 Mon Sep 17 00:00:00 2001 From: Yuta Saito Date: Fri, 14 Mar 2025 01:09:07 +0900 Subject: [PATCH 253/373] [skip ci] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 5c5b76370..e7a9b63a5 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # 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) Swift framework to interact with JavaScript through WebAssembly. From ae38d0616edda5e980800287dd5cf07527731b57 Mon Sep 17 00:00:00 2001 From: Yuta Saito Date: Fri, 14 Mar 2025 10:46:31 +0900 Subject: [PATCH 254/373] Add Hello world tutorial --- .gitignore | 1 + Package.swift | 6 + README.md | 257 ++---------------- .../JavaScript-Environment-Requirements.md | 48 ++++ .../Documentation.docc/Documentation.md | 56 ++++ .../Hello-World/Hello-World.tutorial | 101 +++++++ .../hello-world-0-1-swift-version.txt | 7 + .../Resources/hello-world-0-2-select-sdk.txt | 9 + .../hello-world-1-1-init-package.txt | 6 + .../hello-world-1-2-add-dependency.txt | 9 + .../hello-world-1-3-add-target-dependency.txt | 12 + .../hello-world-2-1-main-swift.swift | 6 + .../Resources/hello-world-2-2-index-html.html | 14 + .../Resources/hello-world-3-1-build.txt | 6 + .../Resources/hello-world-3-2-server.txt | 8 + .../Resources/hello-world-3-3-app.png | Bin 0 -> 50233 bytes .../Resources/hello-world-3-3-open.txt | 9 + .../Tutorials/Resources/image.png | Bin 0 -> 308 bytes .../Tutorials/Table-of-Contents.tutorial | 9 + 19 files changed, 330 insertions(+), 234 deletions(-) create mode 100644 Sources/JavaScriptKit/Documentation.docc/Articles/JavaScript-Environment-Requirements.md create mode 100644 Sources/JavaScriptKit/Documentation.docc/Documentation.md create mode 100644 Sources/JavaScriptKit/Documentation.docc/Tutorials/Hello-World/Hello-World.tutorial create mode 100644 Sources/JavaScriptKit/Documentation.docc/Tutorials/Hello-World/Resources/hello-world-0-1-swift-version.txt create mode 100644 Sources/JavaScriptKit/Documentation.docc/Tutorials/Hello-World/Resources/hello-world-0-2-select-sdk.txt create mode 100644 Sources/JavaScriptKit/Documentation.docc/Tutorials/Hello-World/Resources/hello-world-1-1-init-package.txt create mode 100644 Sources/JavaScriptKit/Documentation.docc/Tutorials/Hello-World/Resources/hello-world-1-2-add-dependency.txt create mode 100644 Sources/JavaScriptKit/Documentation.docc/Tutorials/Hello-World/Resources/hello-world-1-3-add-target-dependency.txt create mode 100644 Sources/JavaScriptKit/Documentation.docc/Tutorials/Hello-World/Resources/hello-world-2-1-main-swift.swift create mode 100644 Sources/JavaScriptKit/Documentation.docc/Tutorials/Hello-World/Resources/hello-world-2-2-index-html.html create mode 100644 Sources/JavaScriptKit/Documentation.docc/Tutorials/Hello-World/Resources/hello-world-3-1-build.txt create mode 100644 Sources/JavaScriptKit/Documentation.docc/Tutorials/Hello-World/Resources/hello-world-3-2-server.txt create mode 100644 Sources/JavaScriptKit/Documentation.docc/Tutorials/Hello-World/Resources/hello-world-3-3-app.png create mode 100644 Sources/JavaScriptKit/Documentation.docc/Tutorials/Hello-World/Resources/hello-world-3-3-open.txt create mode 100644 Sources/JavaScriptKit/Documentation.docc/Tutorials/Resources/image.png create mode 100644 Sources/JavaScriptKit/Documentation.docc/Tutorials/Table-of-Contents.tutorial diff --git a/.gitignore b/.gitignore index 1d3cb87be..2fb37cb48 100644 --- a/.gitignore +++ b/.gitignore @@ -9,3 +9,4 @@ xcuserdata/ .vscode Examples/*/Bundle Examples/*/package-lock.json +/Package.resolved diff --git a/Package.swift b/Package.swift index 7c49f0e33..173add2dd 100644 --- a/Package.swift +++ b/Package.swift @@ -83,3 +83,9 @@ let package = Package( ), ] ) + +if Context.environment["JAVASCRIPTKIT_USE_DOCC_PLUGIN"] != nil { + package.dependencies.append( + .package(url: "https://github.com/apple/swift-docc-plugin", from: "1.4.0") + ) +} diff --git a/README.md b/README.md index e7a9b63a5..c03561587 100644 --- a/README.md +++ b/README.md @@ -4,262 +4,51 @@ 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/main/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 +- 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 +// Access global JavaScript objects 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: - -```swift -import JavaScriptKit -import JavaScriptEventLoop - -let alert = JSObject.global.alert.function! -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) -``` - -### `JavaScriptEventLoop` activation in XCTest suites - -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()` - - -## 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: - -- Edge 84+ -- Firefox 79+ -- Chrome 84+ -- Desktop Safari 14.1+ -- Mobile Safari 14.8+ - -If you need to support older browser versions, you'll have to build with -the `JAVASCRIPTKIT_WITHOUT_WEAKREFS` flag, passing `-Xswiftc -DJAVASCRIPTKIT_WITHOUT_WEAKREFS` flags -when compiling. This should lower browser requirements to these versions: - -- Edge 16+ -- Firefox 61+ -- Chrome 66+ -- (Mobile) Safari 12+ - -Not all of these versions are tested on regular basis though, compatibility reports are very welcome! - -## Usage in a browser application - -The easiest is to start with [Examples](/Examples) which has JavaScript glue runtime. - -Second option is to get started with JavaScriptKit in your browser app is with [the `carton` -bundler](https://carton.dev). Add carton to your swift package dependencies: - -```diff -dependencies: [ -+ .package(url: "https://github.com/swiftwasm/carton", from: "1.1.3"), -], -``` - -Now you can activate the package dependency through swift: - -``` -swift run carton dev +_ = document.body.appendChild(button) ``` -If you have multiple products in your package, you can also used the product flag: - -``` -swift run carton dev --product MyApp -``` +Check out the [examples](https://github.com/swiftwasm/JavaScriptKit/tree/main/Examples) for more detailed usage. -> [!WARNING] -> - If you already use `carton` before 0.x.x versions via Homebrew, you can remove it with `brew uninstall carton` and install the new version as a SwiftPM dependency. -> - Also please remove the old `.build` directory before using the new `carton` +## Contributing -
Legacy Installation - ---- - -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 -``` - ---- - -
- -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. +Contributions are welcome! See [CONTRIBUTING.md](CONTRIBUTING.md) for details on how to contribute to the project. ## Sponsoring [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. - 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..94d5ba3c5 --- /dev/null +++ b/Sources/JavaScriptKit/Documentation.docc/Documentation.md @@ -0,0 +1,56 @@ +# ``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 + +- + +### Core Types + +- +- +- 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..f5ede8f19 --- /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. + + @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..156ac0540 --- /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..84a3aa15e --- /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..569396481 --- /dev/null +++ b/Sources/JavaScriptKit/Documentation.docc/Tutorials/Hello-World/Resources/hello-world-3-2-server.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 +$ python3 -m http.server +Serving HTTP on 0.0.0.0 port 8000 (http://0.0.0.0:8000/) ... 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 0000000000000000000000000000000000000000..033cafbcd7fd227477f8a6735bf5ecfb727eebee GIT binary patch literal 50233 zcmeEuXIN8Pw>2OrMQk(yX%-M^0s*8qrAU+BgGvomdIv!Tl`2T@(tGa&LL4?quzqwf9+Bkqv?O_%UZhp#cb<2pgA~8H9vxZrP#u8+*+)ND23HqKr z@7ipTkJ#^C@OhBzU2pVx1@ZcOJ^nm-cWCeHm|r{ZXFY?+#J9KIt-YjQe;V|Z+b-TH z^&$%%>55Ml5Sr4SEcnHp{PR_JTt;qE%Rvn?MVZQF^@0-O4GcdVPq^V3NhfOFFIwrt z{_R|PA5P)HnMyAUUTPMT=%c;R*xQm(C-0~wr@kCaHk?+!nnWk@#Zu>;D|(hk{N)0X zXHlyZH8o;B>~t%kX#ZjS)iE>f5D4G8`jt<&B4yu;bt{Ur|6nU5F3XuO_Ma|WDUJ!4 zp`W=Y0RJZOqT$>{v;h2rYF|htzsAFR zj8FgX*BbbD|N0C89$qjEkMOV07=YinKat=EjQ;EQxu_t#3*cYZz|ZS+f`5IQ$RYjQ zzg`p4g7@&GHDwhQ!Ea4dCv$UqXDbJn;9PqxaDe26g1$2z9`zmE55A(t?QJ|ff+?7m zo{OG}vWTgJ9rrUchv(+p9(FHq=fM;65CJdk%w3)_d)V39JBxUT-~9av5%3y!nCB+* z?~l0Hh~LyxdBQB~;AGA$$bFyt{!Ix|W@ctFCo>BXjfZl74F~@cziH*-@=RO|~A0HR^1edd?y~{HXE_>%&e@^nR^E@)n3DuAzp&t6 z!~W-6|2S0tZ$tU`A%7qGx3~T_6gLSG4RdD)TUXpo)V7DYNbrgA{L|t8IZE%phDq>3 zAUwQ(4g34?|2ang{}}W4=+Lvz+<8oO<~+^-p6qSJXKN^Txct zU86EAqxm#;iiJMsyz4fo$NOA&Q`ZFIPVX@7G#*}eX<5c4k ze<_WayLTmj<<=Cc@n~bc=OR(kMG}0%D^hs>@E5Z#0smrw0Ob5Xy!ZR_%-5Jbx&vO~ z5fC%~!(a1HUe4HEawk=h`KObEW5j5ui~lqbcTAX)*(2#|icIuBe>FV(z}g$<{_(QR zFQqOK5u+Dh$ZR?^ZvtZe^2B;Q~uwN`ae(k zzpa-4EbRXsYyPvB{~iYa6Tbd;*#1w%{%<+IvS3MFtZkZ;qKj|DnD`*?NP z%kUq(rtR@dsovZoeyqyzPg&JcZ6!|jGv>C2r=B@cR|GD#2(E0Iw1Ck| zXDW!0ILxcxYCh`Ws-1i;aWEe(h&Y~OI=fGATPB^fz-iS&AbGqk>(XQ7y-~f0MLDPu zme0QDNIX3`O4_KMM&C_W!Xeb#_gz;BLH%@6c54ITLW<({Yk*g`!LNE;J!j<<<-E1d{O2 zoqtF}V;qLIE-dcJj)PiL8@-T5b|>)6?F#wPh#hZ#wb-6%UF;LxN{&6yZ8_9-`SI)5 zT!c_zNyBQ@g4mx-$*e{|=&*qvh)WsFlwY!F$wPj3@IOL*+c?+=YM<(})A^VO*<>Uch&hKTY24f) zZMxc2oY0g_`lNLG)g=)tgZ_1hplU!Am>P<+<#-21DNjqxJYYsJ?SBTZU&%@8v8$c3 z*la%9Of)Fn?aWECw;na|_pR*V>-LDrCW|BEEc;y5)Xm*k|{S;D&FMm5V=^5gz`K0@XFhw9=wn9{sLI z%xMIx=H64T$OH;Cv2vCE35Y?_Gefs2SKZ2u8WbG{Tl(>m#IKpWTTe>D2dPr|6;>&p@9^aLAfQg9n8 zX2fe#I(}?^ZfY0hkGTj*!3eP(1TmALD(#$mZBq1=LNeYpu1iR^T9*iinyc0MHWVXlkKz^|WkYlmOiN&+Sr$>YivVh!8TIh49O z{LLk)MMx8WI4$qg@?j%`FVMprOc-_wI7&ABWgGwY8k`jLHcA}00RczHC# zVM6HTrb6i@BbC!xI(gwsGHX3T(>{*l9t`+|iBF_xM9;R)PI>6=-6>pLYwWhI?A5^> zL+mH((o+-l?Z_~PBzx=Q!>|2>hT1%cjw&C68FsJDIxRoUODrlei10a^=zRBQ*@+PB zHC2a`-d`1TyVE08o}1GRt$Qzf}PVWcm6z|!Qk`2{aZg|(q! zhb&o&We}SZ)`lUjwO~a~Kf0x&k+U1s+@1NSkHLf)zn>UXfv z6IknW8nnShu1=i@aUp0$JrUTsh08G`nt-P#Y+mH=x#Ig18Rt)=qb7Oc;taN|yts3> zB|o zpWE|=jAy;^abs65RE%`QePaT#+>y_ELuuy~9~VTW<`W}vqO14%Sa~a4Wyt!%0;Ke% z-}+|VTsRdQ>u?<^hM=~sc+^u-_TB4np%OHj&>`gg@uPGB!o7sLb~+ov>60oX)cX-d zjSMe3#u@zpF`nSK_`u&8c$QaP3yPeiX(1T!>y<{dZMQm$g~ ziXkX-`*5w4W}Rk3&~O`MZJPD#sja7i#FIIkX++oywyog;2Jw96kCWEj)|&&#moL;l zI+{K|H1V+`aFnL*aJ7(wktd^L`DX`{NjWF-z^zvRY3uv6=1PFu?d>V|X{x2qaTlUt}IX1SJH~O(f`&H=pB?Yo6gXyv z3G$sge=#pVR0rxg_=qOVOSe$S-BY9V8_J<&stAsqSl~e{)O$VMUqy8H2rnhubs@+6 zRy*^skyqYQ3~YP}zTu?MX1hv46BRDJ7;hEGJncGS8i!k?w_be4zi@q;vAiKnpTgd{ zX>cLVMB=)%xC>9yw#@FqQkpzP3=z$C3)N5q1qbY;D6ZsX&^Cz*M{$+L@;!m2rCp)q z`}V{iAK$U?3^vLZEWd$+d+EMpSDMJj&U_E|cu7CAs(5d~Ygfj~O@|PgCZ#4VegZLy zGc~NuR%3xl4$bv7G8#tSYsIzULBk5=3h%9@@q3%08y%5-(JYEf4^?azlU!^BfW~7W zI|EjbAe}`U+c>)3VsClS0;B;8jG&93w-T1!tZUZ#=f#W9|-tI)LrvvHI z(p-{z1F~W5ZVoQ9yFy0CAd+7)=`4A+rKMEG$iea(yFK9nQc^&L=vI^S3|))av{N^i zzh~4dd64~r$HBI~R{K!gL9v>|DtSgs#jB!EWNoBIx@c;MAPr>i5u4&6IcuM#+v27I zr~4{GQ(F0Bux`YfJ<`}c1)F>Uq~#_8(gLev-?Xgq3Hi|LKbpM1S)M%h9HG#q$TIdn z!NgQNh*OKd-&&virk6c^>sg$U(>vX{7@?5HWss;Q#O?TCCGT#-O6nIMSJ18f!Cg;; za9_Df(SDdir~KPBB}%5n#jP%m(&Rj@)qo&2+PrKb?~Jx9s?h6h>^Ss7uczd9rn z0qC1(yd-MxE1oEmKufv~GXLP~W<)r@uj=^0xe4V^2W7LO~ejgg8C;v7X~GR7!GDruVcX1#M|Jsm3HwbUK_ zsaYc~*Oj5{iSF{!4FGNyaab8;TN#$%e|nI4EaHCY#)E-S2GLQ@+DWH!^GJMr!X2Lb ze#aipXwM~20WEj2HDanrSBe+y&6QUjP??a%8V)k+tihJ}4p!byz|A#^szR zA)kAyizXS7#xGilDC}xjOS?=BtTNtQ6HubC|1flQV4KQQaWAK@JN3JXDAfPNl|m>$ zo{r|G7j_+H-?&k8j6ZOb@QQ*8a~9H$>#Oz~K|R^JybJbQXt;_|zvxtWO1m&mRoHxK z>C0vsj+yw}kk0zU2u^?v1BDdVI*3;?R6hVa4rw^Us%32YH?fHp%0% zI?{O}(=*g`zAegLZ3y!`eqy29v!S8i&n_-F9Vr(wGa3KZamvyH=GaS zZA5#m35+P44qm1gu#5m48srlAs0{;J59qE#J+JLC5A_XNFr2S+FS z(@B0Ohn_7Rnq~!h9~W1MRp>0!KmY@=R4Ntd)8{wC5-e2(hrR(UL9M50TEUib%(g;^ z7dC#+S{g3^LNubenGwwRxKg1@1~0(v-VDXtdqcMw3JU7{xcno87q4sD$qZhWJT8l! z)u+Md39vXi7GKTF4CNk}XTL++5T07=Iq|BB2sc|PFjv%pW4eB9bXL~do%|5jD^kpc z0XVj%3%9C3q`MJb$$K;dATI`b0s`F6z9cs}<02*z7pu1Cl84Kg?@NCY5Wq+v!%L;@ zg@Cf`4o~cjbSYkYt8#5yrMQ*y&$M33zMLsO_vX>>Y|%n2w)6c~i`b8=xE1*7v!E3oiZo7X8`>T1{PN`^QR2exK446OIE>z9n5OC&!d0X9LRz`! zhH^XU?=!kmGkX|XK7EhNu!+V`=f8jGZ6>K=!Y6cMj?7}S85xI6rg7XUjBS>ZZlz!OT}kMejG6 z5fTk5AGVryRg=s)rI_skn*2|p&b?Aap>!V@tKFVEmNszJ<1EqtZ<-*+}!*d3OLH zLat#u1>q3o_oJn)-F`_2?3if;M5vzI{NxbXa_OEsK#=5JY8K$!;6~H2NNv|#?HUOH z7l!12jx+Ha&*I7*CuWWm3edMq5m1i~2haK8T6Vogi{HrvWn31UW0H*CODUoNG5=G) zfk>>~K{#>94+^0+DaIXX26L_Bf)GHsvMfQ41 zy{7?0*Q(dr=^Y?|b$Wbq9X5c0HDRSWBG2SIVnIuGML~(VG_A!NiKB`mM@{-eAPFu_ zjohY4_M}mj`*Zqhq*t0$h&!xtyUzQ{_qB))?9+ZAcvuqv4s9~4G6AFkmDo9d>#^56 zXm3ExA2*2?y_HIqVwCjHRZe&=IAL`wlZ7IY9kV?6wXq%u7FhR73DZl5={5SKFhFPYwUmYv&)0Z|}Zip%B zGdrZUcx~C$j9Xar#PhN{z)nKe0EBYC10vOKpVD75pu2P5IrH8pMnXES^mmLdN=Sg9 z%}#gI&f-0nQiKkLc6)AM!_YMPqe6ghWjd0Cm{L$jyvj1=v8dF?`iC*nJ#j{_l7Jw@5Dij8(c{4v2FB-=`Eksx~`=KlG5APJ)>MEU2&YB zK+AD@A1uU$wF|&k3kpAN9BqHS9EMI~QKVR&1n2)S4xp-~i&*n+pXe|ZOOm#Zew%Im zLaaJZ-wtXfw>x=8@h2SfjY2KHgVDQ3>}b8xZsI-&;PM^e?b;*tmOu^=1k_6_a3aw; zrdMcM%yC=CuxS4^_6LY=W;!7D1>HQx*0u=jPy73;xUN3`f)mFwO|uzc!3=JB4?cY4 zmP-M62$NT%lF-56o=ie+Uub7-Ic!#G$A znH?-=RQ*>$t}NV7W@RhHj%)TxEwutAA>SZ&1-dp_@4Q{kZ;T(##*&5%;jDO@Nl5(% z&qS8LJh>)KN?F#ZJYDzdl)+;rAfOt;dbC#B!dS)7wsG2Rayk@$UMIiJFM;!h|M5>E zZvpW^LCV^;^Uh>Al=K5oqGAoh6KIrqIhUiqu8A+^P+;EwPDgdd9>yQ&d#(;@)uguf z3!nN+QGxAA@9=d9+sIf7cJ-TKHA(o>S4oHh`&DFA^Oi9b#$AZ(t87>JHqO04f<*$##iUb?|t9z zVDi6cr!lpIf|?JWR^XQlE{e$cMr|+{*8yFObfN`u@g97%#s4gj{OysJwm`%Nk?{kN zFs}JCj;Cu%cSLU}8ds<4cnqFjq}bp}U3EPF-9T)31p^vdaswukD$1VsjcL8O~ZxTi<)-2)VG>wOni8YKe6XtB3Om* z_4}U=uLBe<2X|e66aQY^f}v-;N1{#5}kKSqSZFr(XGf%Ri#ut^3STr14Q0+GrbauxYG;2Z_hgUxl3v%=i$pa-y6%As?-oq|B&&7ZXnyTPp>+xt2vQZd`e9LdK5Ef*u# zP8Y=kD3c-6a0BDZT)gX39&T~q4bmVz=A*-#tPBsAC1azZsq2$rG`Zi_eq)^k;*lGt zcELnr3=6)Pn)tZX+G?h|s~;@RxwT^3e6Q^2!k^ha$-~Ndf0XJ6xb@6jy|fCKo6^xk zEJu~2O(58qju^JLWFPd6+lu=eC1OV>0=?qLnGB#EGTEuMTPe*FNLE_n|q&kCs6i8*`jmg&M7fQ#UmTfIku18W6 z(Qg-b*DJt}*9RogcChwP?9C$NQH>9iwpR(`t{0Pl(vUHM+a&r6z)sqguQsXc!Qvw= zZQ6PK1Dx&MohOV_^=83$6*hdccFUU6U`c!E;_RhQ5=$F)*f(Du*NK z>`FgMAI$-Y6v2#MR!WnjlSQqDYBo( zJ|`HY-vvkglG9aUUo==^sz{3Xq^YXBs=alQ%iE}LUSRx>n6Wj{6IV4IqS zx~Q#f0e#(|OnfvcNQLH|Z1cH~qT}VxHWQa#4dkn8{)d__s7ibHA;~GaJlX((fH2mUy3K~QP;wV7&aaq2 z*5*O3-Nd@Q!RSwQ6T_)-E7a}qeY9xoQxI1m0Jv4@KYw?pV6mDFYRW?#4@dz*3^Sss zy&Kz%qgC=0POzLkhNHfF=ud}0!||v)mJOHO<(aisfj_<4T2>INf2?hhG;1z zUF0R3hfleo+U6ghRd zCO;iuESK?BVcFaB`+d4@uG-|niNv@zUm-1`M1`ooC?_{jML467>4}?sJG7pwU$!4* z=50{>JgkuGLrmwGf3Dzstug%9%>r};nMPq0tW zR{M5)<%2xq&&@$YF^dthrdUHrbQOQ0t4z3>?$&75#^`Iud!<&;wdtXn{M>x(vtn~< zK>RYgW!!#`Qt{LXQu&8Bd+WIKVhiRprHLh27hnDn<8})ZtP1w0V9MFz% zXX;L?-)!_m=}Rw7t<|>N&aRP69f}yOq~uI3H8{gMjnHmw%So||wvD+-rjXurOO@P0d#5lE?yV+Wrc3c${ zd}_d}mQCXlCz~j4r`-=?i_J*R*UOSk8r6fZWk7wz0m1@+_LRklEb_cq=91t`V@J3( zPo7Y-mv>wah&@XRGm-#@Mr3!k?KZG}942)4R9&qv?Oc`7NQ{o?l{i{|=k>D>lqFbHR6o{E94NR? z{JO1Ttsl`(k=D=|&4ix@tzNyO{Ku2W97kaHrJnOGRhekTbDo!vO9yuaV_h3AzPfO^ z#Hz@#&WCj&zT|TS8y!c*P;)_*5xmRxenwH&L|V{ZkrfZ9*w9Ayd)TZ}**W-^`m&8( z!<8@sy?$yp1ru%I!pG8b>#fr6!-y z1^2oy4bE2AyN!`sPAzPx_fJBi(DD};Tq1Q-vhB+7zt5}+Tq^B)vL)^3kzMnXV)oaDA=XGY4uQK>4p!`vs z-qMz3IPU807)7!)B1m4Tap*T{N<{10?a#aN{AGXWNSl+3dIuZ(b$z4U2@}-U;oQ1( zv2zzL?G-du88@Ve-)4WC^~1dD6+5=?jZD;`5=!GxuQhI9KWRdT-O9wk?ZDLAUA`5& z>N>%G-D+$0E%`(&ee>~7cPl<0LuPSm^V#W9V85Bn!qMHm`oPnYK6j6KEy?9lp4;GACtKRZ342}t)r&!L0Z&mr3 z&Oh`1v*D#|NPr+vCOIeZ%ea)H3G-?+F2~wbJx91NFHlA?P>O<8c^h@T--xBP9DAk7 z_glF1n%CtR9#M>gqD zTKr`w5~rr{b7{^|UJM6&>lI&xvnHZTtnx(9%e<1|(@5tn{y6_!-%*n4>=G^C?eYi$ zQ^j^Mo>AjN-J=yi${#E50@m%#V3_T?+V@gNZS^4G_IK#&{E;50!I8;&mRD0$ooAp2 z?9;wL1?jkGljq}R6|1$z!&78X#ftbmmq6@ni79Gu{} zn1K42NjugLN)%S-J;KZl*vCnlpZ6=$HI8HNLa-e++0S#hkdC?IOT(&k^qQs{K{>!< zF=XY2g=h@@^wzHJJHmc?Kkm?Sa_y++!CdGfFh>-4YJytw4Sw6P`=FjdT6TI2vssoXb*S06Bb-rlasJc_yR^PuYZRC^UH{XU1B){9;wgU(jLP0 z7yZ-?sA)0Sofu+uKmtQY7=tB;yWum3t9>d->mCm6;TyW;;uzvl*z6}@pQ(GEQbJ?9 z#p68oEcg+{LLJmnpD!R1#72jbbi;~7R7o}yR%r5tEaa*IN_R@>^mM;Po}%w|J@X9r zhinAldT|v3T5_1U!D5Rmj23wAJ)ds&%}mb-bS2#xb2JX)zEM;&VLfMyxqP+(30WAL$nbE|lK{UZ8whuqE^RrA8Yi^Y(DiOjer@;(e z3I~r@G>c?-$NDD3*COi!rH=-)Ub}=Mg-ejC)q-YimFM|pAlm;>6Qk~!6| zY!Te~E-=O`txo`H1fw+6q)icZ7X+BOWpMvuT9)ECt7@`UgWJYx^b|*g#8=3(A_uSK zj9_K1r8#TNMJ9>Kg9@}HEEiY6+qlWTHW0^V4F)+sWKUyFg9hb~K0w4z25DVz+wI4&opEBgEFf1U4) z*4!=CdJ@IE?PB9o?TERDh*?@->9EWUQn~8|Yto-K&52j5eKdhSX}h_4sB8L}dM)fI z3Hz(JggivFU&nf`)6^2nUu;FG3vI9Kv`*I%)BosO8*&V?ApeqWOJ>4DCdnb ztED$Ku4ICnajoW;T5{iya~F$kE^)LTR!wBCs0z1^5MX&K9u-vT zNFI?%!nqbxU}t$?b1`Y=%upfv>%yIJQKNf|D7v(izi=Cr=(=F5AN?(Jo)^H)qzf+| z3xB0sD>uQ!*lG@V+f?gXnT$C;ra}D2Yl?f8cRe4;+CV-9fAk^=Ld)eF&be|%)7>fq z%F%h}Y5Z;H8sK^Fl$RUj{R(Dr#5OdLI3#892+(o`!ca?L&|ZSAfmJSLLhLFOTI(I1nr3 z;Lwe4F%pF%ToFMe>Jd+;vogqrROvo_6Uuil>My(Byg;=(1thEX@H-|xBA^$F70H3% zyYU!4F+#eqn7?FKYd>`_%36W`+8=o4Nd>(6Nb%wD33IlK1tQcLe&09&5^$wZbSj(# zG*Tal5?-q*7v>u4)7$Vp+71`p94jk+Fdr!!xKGI*6Ja_5A=f(rm z_E28fg?*~;mlTDjTr?HjN}Fmd3;97c`6Rh}Sa0kt*;iOqG+z{%7TJJ~HLXT9uR@2Nf>3bl@=(c*9b`5ggW6i~w#C2MjoHKaqwbKVn`&xpLNpUS?PrBJv}W|oNr=~UA8*GEfm+Eh z0-yGKs+p;Ed?S@3Lg~h^oBeen1`8NqmASwArtoKkthZ_~oz;!!#*k6(F|;5}gV((S zf%9$(JOiLE$s6Mp@&=Ue*y)gszr;2jst-tKM_u8kzy?R(`|3%m3yscOrGrDq{_3f$ zQR+OmrLgv@f5%WDoqJC4Z{!&}N{2_H6&1%UfCKTNxPuw#3PRU5 z&TD#t*C#}c;-t5`xDvryq*(P#poo(1<2L}=tn=~(bP#PuDrh%QpFsM)g4Cx3X?J}w zmi`u*LB z@7x&qOx-EAR#bW3PBAkui|+h4Ay^YiZXau49;5v!^-W^F#O;+p?FdG38J$jZwAd6K zJs;$C``LVrJq6rkgu(~Nw)b()s=@uYUfHveEN*=K!^?~)w=#I*|Fpi}eActdS1VPp{c z8<*OH>ZYKT?*7*F^hHLoI_c^I-Au=!Sg055j7p;~pGnR!|7xothK@vq)F;}S;uk zMmb_*LV*<6;a6yV9yVN@UZfT!o7jrN`2}((V4f~4I_??>uT-25Hu;FJli}PjUB@eB zq8-X6s@Hz|Rz=H0Su7M5sR?Oq*{ou6IIqw5IJ`8Cg9)aM5&;S6@Y?|hIjNd`UgF{V zm*Bk`6rE-tAUf7o+d3VBl7efEwUDAW4%Q0!!^f8dUB~jD4^D zNAq%Y)!AbHzO@3gClC~2nh;-4{=Ns9W_K==dkZd*M>@=S0(L`a;s{B%P z$T3iDo)+~ebaVhFm@itahrOxYbH2@d5f6%8!?XoUV&YtS}yxg41I{Cu>;0Oa$FdG+#JE>u6WG6+16irm8N8poc82#mb$ z`m+&QM9EE0VCjAC{HY3Lxn3nb?x@FWB!oWI;4oe^A} zM3Lu-z17k_SgPEK zteq*gu{8{=RdH1q)Wg*djY;p@$}~=H9E@3ok*r2X?Ro97*N%!cxIR#;ogQrv4}+e8 z+J?O|%Ai^>Z(sep-43Gb@lIkpJS@qmw8;o_vup0*n}v;ePcMSzrRwKlEb!I$YgriG zrBUD~=E$oX*q3_p!>r`qle?4!&3z$fR0FRc<|aEmhLltW`YqJT=l;klx1zeeIa+K; z*QU)S>&*J=(zEo&1c;6umori5b>2@oji2o6ew=p(@ltn+6&V65CNcHPpF$>@s(|=s zAtEH3biO=&1!SG40fMOX49b65#`J;zFLhrvq;>;Hc_C9@&RrI}A4dUi6HLBXPLNS+ zX2EdR1o(MPYxBjvf=y`9{C0_p8@OYBrE~S{)T#10A7v><2i8RN5S!gepF09Y?YGUc z#|QoXs@1e~GG+h)3G7d~n-nZGkPhF!l4Mtt@Dr}N0369B>>f!~NW1b4x=JjlU6L@; zHx5N!rrVDjnI!OB^ArU&uv&a$vk#9ZI8E{Y!W!3}j=dJ!2y+{yPGj!)_KpIT@c3wa z66Xf6GL^M3zkF|uf0wVDb1q>w&Xx|DFw#sY!^YR+H1-O#a7?)p1FS z)tug9LgX4`5H0ub7l8o?lsY)x1vTf(r85?+noqpVk}7v-0DL#htDIGl)J+n&>FspL zJzJK)*e-iu3$_EvxL=GGlE^9&XR(7a3tHfkeWL5Xd<)f}s9mfbiJ>4_#;T>I#$^OV zJ`8)7txPC$E=eEzgz`yL!rK+kq|S^O{p{5+n#2maj{31akZ7oxyiQJ*Gb5UIK9B^6 zk>Dcapjlw^*UP_Zd|OVi{EF!P<^`HvyQZJ*(liFZc^NxfAno5TwySJ;3K#gBfI#C%>L(XLNT|;DGBZD`cNnZViaY0a#|>9S~n%xM82U z6~T~(t{0vZOPpu9Lf6#mmE($;Ax)|3St59Ifl5E|A#;^by`T+`o^B_F4pm}}DT^^%=Zp$4$EkJGn^O6M?L?2l9ETa|i>&V>~)4T33T8myvog)NQ1-o5fTT37pn!Fz~aSGfOiE_%Ux$nAt zhJm&46`Qpv@NK2EF#%?kqnWFkBOk#iZSggT+&Naa`H6cTuBBihq?a|x_Bk~Oe1hHj z13mah0^)=BpPxpYH;A{yXV_1~vn_>V3+fYq zRV(LUN%BnJS=fiX6j-@YRY*|1>4HVfF*X5xW--yG#LTH(0oS z21UXAGvdI3&TNYW^H?lA8Z|!3M)EDF`SK9xmsqrqVs@R_$Ppv7rW$bc0e+T;6p09f z(OVH>8a7v;^IfF#w2i7<&b%4a~Q$>O+0?QvDWXwc_+95 ze-QMf)YCN18olz}a9gJ!+jP=xZM0Y*eVp{S5hWHljZj04LM`+{!^-@gLYu%*ztgAG zzIgRTFP0v82&!ii+)BnKms22i0QnTyc;6K>At-U&n>C1c-4@&nd|eYl)pW$mZ_0V2B1N3xQ2QLGm^R2rCRuSuPvrcZSm zyplR_&MpvE+$T6CXI`EQZ9llf@`NW~Xv>n&VJ%y^{x>m(;mj?X(G^WZK+v$?Y63o) zI^a$bjCg5NGhH{jHrmG-&1z4gm|eT@rbr&WNU3nG$rl*v`KHDOObx<-d8^muR=${K z0H}Z?aBhOZmJ3Wi_lIlh?&(Hu2hrQ#Z#n37Fyis%q8D~DlVxQZd9x*ZN%&i#O4s}< z!k~EvSMqMY>*WXR*@=KNl=a%+>|06ZG1Nw`2<=4x(Tevf>i33}Qc7|0zi)xlRy9Bj z+r$NAQhl=zi?6d0zbCTs7^I2DiK&C}u_4s)X@2BDt?PsTb%av8s0Qy}6Y1ndlwozK-*#?*uG20pJ_K!`eYP`UK$6Sg@Df zL*t|&8*3ymBkBzwHgjx&0(i52SP-`dum*B0o`ITTW@<;tMW9`}a0yN|y6-a%Ny&DC zs3G3x)9&@rS9ge}|?Q!`B8t1NOMep9uUA@;{$Gz9w8lVWc#5R$wmbigc_ zPy|LAC^2^yBlRydKqO2Q9`ivSDjdxB#=Z2~esliUT>I;yed-{_;p6Yc`;0L7PPc>N zr38h-4|H^Se)IqOe*yeyU9p+xVvz5*3<2U;z48_(v3QPbZGXp0v0|<=VAoZy4QPmI znJC^HHn-o7rciyr;usc2iY^EMXl&a)m(@H;%+o397_>vZ65j3MM{yzpaellw8X9|$ zPS6x_;@h9k<@kKh%-75n33eH1%V_pJqAvX55JEi9>tT3!VhhYc_E%wo=WqtP^cc`* zU$cYm{aLdTVzi>YRhI)gRuGQ*9`H`WC_)FSsg%tv+Z*{cTZl$*KsTxOCGi=zjCXpL|`mB;=qhm?U2*G7x=Qj`++it#$| zzD%-i0R?haY#-RXsdV%*R8EJ88oJYc_$r5x-ApFH_B_lr^n?`H=sI1x_a4rz2Ps>j za`33QE0fgJPi*--y~Nx#c4$@^A8fe3y`Q`LF-%)Db9J-*bLgNl60Y-ZKD3S z_*O401I5l9XOffCLOQuFcT9uMoU2vFi7{S7S&GtYSqZ`r55sP*I#^9o1sz-Z_>Al@ zC~eB<@qL1|obp5maEC9ig(0@y!p92P$u$Vk9L zDHJ6D?mh)gnC%kvtfl7f85ulGcbtY85{C9Fs67QVmGs^^0L7QPOtyM(!k@lrm_TT| zJscG7SB;I8VoN)1WQGAdVQP46eIRQevVka!Q{)TKwhqfqR|XGKXscnZM0uj%F)-U* z)7l3qh4y1w4{=?);^+e`@L<_+J4QiWY+SbBDQ**;D79$c)#uKll4dQDU>nAR?lS2&u#cqv~FV26q>J_P(7LX@}uklgnfwlB% zbyRsEL!9Z=J>u^^!zw}vvvx061L*1SNb(A@D^GDIIdn5HQUDWZl@7ogLQNq<@h`Km zZ0V>l*^a$t(7YAep4M{{4$5X|#A-4F-*}dLtC=kff6DwvLz~H5_u|h!Diz|`+jkOP z^DBzCxkB~^HXFB~K6FR5E}!a#RYiLf&OkNJmAka+zS(TWETMV!#$B$scU_MKJbqJY zoKsqvM{3*6>vF2Vlcxx6UGfsU<$;XWwJi7-5Nc5tIhMUW%A_=U0ILPIOX!fhqdjv; z;0Vzs^GyNJ>2YVDEIb?6YiY_JVqh#)t9pc$<1knz7yvG6sL#Ye%PP@QQXTUTjz`T- z_(;9ZRo_Cg3{NHC0#qEijTB_PclT*_ea$%QWFO~sQ7 zj(x84(rF~E2Rd3WfXrcQ-1ovldMtC4!p#4Ox6Cg;`-8pgblZ&e{!+3t-H9HtBFHjj zp1_GEtD`7T>O~TB!5Wk{z3nkdc}0!`f>SQTGu1d9XvWpr_y<^3LZpJfat2ZtniryY z&IlCS;cy^e@Q;__+^CJ{+p;nqG^*{f)Gshii1SRKJw#oqd|U|H+A+Ym53{Y5u#{<* zXV>!sJWsSx0yF|CFOOxdY8NZE`$S7@xt14*!H&^#;j~6j$^JDCz>!uB@w0z-FEI~X z!iCXSDalJJ$6uLFuS|*&sZ};0c0g;E?Xao&=a=}csL!M=#8%0T10NqbaH+&DCS4NW zdt$+ZZL@Cm0dz!vTL*SM7KFU>rMhdRzyy)3dllEi_^ip0=N_LhAw)QmgFTYQtFXB+ z84`zUPHSzuQy+b|E{c@nF5YI+M_u>Qk8DI~U@NTcv_bp3d8~8|VW7FGFAf-WX@JqW zCg?M`c_=#iFFJqqw`SnAqNI*>8oiRWuT*_!GyYx<|)oqVMde<&6K9TE~d!yAPg!QB)f6*uSIM^VG-ho!zq7N#WB{xX(_Fb(+z>1X7&1VMOiU?lqTT zy~M%e4IY~Vf4IMAYTZ)xWPnz8gX6|5=8zbtjpofXDGS~hvEuu)q9XZmO)#RjVP(7E z>yqFm)dCjtL#k{~GOh2g>9fNSW3bf#{jDrif^k3ykf6u!m%v00C*m^TAn7H794kWp zmi^I|39>k+1aY36Zr)(BsvB&U3tKgoQ_<6n8RXydXxA=_u9u}Jruv_H@>`GOrMp^p z`T%#PM=>thux-XHY2zmtK&5D`3$rgv{Q8XZX|fiReMPJmU=ue>8bCxVG>7dbvff>v zsJ2O32s7Hwl{{g0bj3M%aD7p;nCe;36>2$4uvbC~xuKLM;cLWWUcouf4=U8wP)UE+ zRv^r-HX0}-eGkEjf-j{iaZhv7RPj}cCpmhN<@!{ugUQzT`)+q6u;-a^11bu{cJUU# zlTpe~bU2SeFX>$%hlUZX`g(sMVIz#=1LF3KYgExTAh2G=HNAqU>nGRkvMHT1g3toP zTL+S72mQl5+a6eC%PHcP?uI^Otw}9i558kyC)O32t^}2Aw2>sBGYkxTwc2XT1+)5u z>&G>SZD_#)mPSkd>{^sHNpjGLCRZu9A9fW{Xh6MWlW1lK7?D#mb;T3eg4dyNLrzh% zipp6L%rX5AZS3BWv%=Bt;Q^}Nc`35@zL&|uo&4Z4DqW*zSYVf}1nqy z#ch0I_`R%)K|B|H!Nhj*w115&F+)=0y%8+jL;w(t1dtJm^<1`1rnuv=p@`Lu7odSN z@v&srDKOwB*iSVO)gr3D017j2kla}@z?&i)$m~d;^^9GH?cori6R8YB)EG2GL$#9u zfP)?+vTJ^i7>{I1gk67qqf~$Icu+5LVa}|>t~O|inASwNr0H>~3FtG3fhOA5ho1m3 z*RH`_G41twX_o7+BARQF+7SayRVYi;PO{}2DxJ4>X!hLX=eTyfF&|9Vc3jKxojfb! zA{21M1ditGS`ekn4F3pbvZws4a_dh0_e`>F&&Moj&saR8?M=ao06VQOT?MDMz1l%? z?BeXjvHBLYnHeH`<2k~RJ++8xTVQl{SdO&COsIA}!Vz0572Iw)as|Gm0Gnf>M%JKC z)5QDFWIkDh(F<`8dd27r#hng(Hiqfboq>uWjR3Bc+ACSr5lU+cadyd04{3*xCm7dP zrId8A1e}yMA9iU!wCpx1?pLlAsjC@=!5ToZFA4CU-n&ni z9^-nxX~jJLUprqO57is}UzRo>rIa;Wi6nfKRAfo3C6gs&&60f@!pO)TEkcn!OG#mj zeP$4$J`CA+W`szVvBpqD-)GXKqTlcT@Avv+Uf0}b?!C{s&vVZEobx^>i=6ylwGxV< zP4))2zUTSgvW@5aH_Qi%861R}RC)*-AIM<+GzYTv_i8h)cy^;$%B6{i--u=w^+h*x zP&6OQxTLu*=^qZ@^?LHf4xZ4~{?GW9QPHgTvE4VPFC_8ap{Q^XWXHdzI2jR79lQ0I zEDu^4py01StY1AwD)kzCb3q_Asgh^ z4av^t)YACrQu=L=#M9>WddM?zDo&f8O}$eoadFmYIXY}O2@+fjmco3?l4afwV$GF? z)nn;48&A;3j@K*!NOwGvAg295Xq@0>$T^Tnj(962@fK=Sr0jsFSDuWxQ*qngrN8{$>lGiUU2gGDEG#B4$%sH|Z9i02*` zmbp%iWj0$_TES_MdLyTGQ6MezwmQ{*#tFz#0eZ7%Zd8;?7Itr}9x z9x5QXDQ4#NQ<%H6YZZ>;e2j&IP#(q?=`o>}u@R7v6livp0B?E&34X{!s=J-gL{7ciEN2_ecF}+E+m_w|lmL`GNA4m;&YEyfUhr^- zOO*G*J%Fd6g*cb3$UU^xHe}3E-|$Fl2`aObv&fHV=JkPnG-R@cXzFLkxIiz5W1clT zY3uS4!_PDQWFm<=1=Cnl!pp6;J)ronB@0(@Ag6eR<{?mAUJHmh_A+qa_x*Ww<_MtD4X?5=)@rBu2aDezxw_ds62 z`Fs!FxJ0Gsn1y>k`^|3Fa@0E6o;p`R@>i00;($E

jHbibD8MNl&=B>2&lR9vMUSp?u7-c`| zjxb~K|FDO8Hmc@p;W9L*+dh#sNy9g7=2DB=91j;Gul-_%3pH|FVOek~l*POVd^-ZiQ!E7H;Dr18Ay z01Y!QrL?Q++5zS@{MSkNB3XX<*_c_x*Sz<>P$Xw!pU)+oSon6 z1AT4)%QkgZW9wq2%HG8MDxuh&ElCn&Diwd<(jl$+eT43(k@{KbEvHTHsFJHbO#!Zz z7BR1dmrPK#&eGmGzuqLo+a}a_r)@oGl@Sx8>~?Y_diT8FTlKA%sEq>LG`j1r+6h;u zqP`9rCORpJ3R95E^RMh~1;Hl7br-hD3?FsRsFvWAvDJtQf zk9CQbV(o*fMCC!Wt&0w-c56^5Qfmv)O->F5)bMEwsj-sIuE0GtAK~nCp5W9pp@}>n z?CX23{FBlW(bR&gDm+Eps5E`n!v%PGFzsuVDoC^Td#~J%ZHz<&^1uqyE+?BmGnR{* zUixA%qk3H+J@hK*I0O75y$ct-DDh$vq&YXVNn+;ai16=pbS@Ay#%(D;DF*>^_Wb(c zg_9FuFa_h$Tl$Q$f%f6VW-n#@?zD|Ip?vpSajJ503X^NxCouIC+$xg>OL(R1QM?e!(dVP2ZdE#T!V%qHf_l)Je zT)Kum(#Eza7jl>q76LJ3kc`?YBE26mG*+3+i)@NYEKIw~d&E}z>AE1|-fvRwogy5D zg>eEQWVctCkC7F2ptN{`6@jm|24$rds)Wrte!Uoil>r{SyFt&L0FD zWD9pmV7xN|4mU(@g@?pR7A5i>3nKC|5_d*ru{yT4&}+V|P-kg-fQ+1w2&xXB)eGB# z8`&5XS1ujiuX57q-3AQrUgahbBVgjf>nW&I;}xo$f|zWC{n4tKSKtYr{Qfvqd}mr@ z0bOZp6Ezq38Psv@aOZyI3+hx1Btye95z0!GW}I^L}Okr zA?Q#y$SXYUS8z@rXv&$^avw0i4*2e z275pCpQ?8$O4aqkGHq(b$<`;7v=TAXIj^h=)I!Cz5Fhg=>|2Q+P-0cb6$xjEV-8qa z_~i4Jn#B9F^f)8oTE*^MtNJdE4hhbR*&4O3co5gAqe9NR?J)N<+{dJ6RpEVR@CS_@ zn=S7%I|z&KwI~M>SgKc13tT4w3#_yoJVesmujB0DSkk(O*>WdIAC&EUf}=qgEeaG3 zcMUlL=11)g)q|2oQ@Go=vK5Bz7oRsB6*SOz`Mml;AL{~rZF1EKJB&lDCa2C)2gCYu=K<333WSBFCqOfI$ zbR)DQJNay>i84zn^AqP9-{^+ku1t7hiNT*@W4QEUme_699BhLnZP=J(j_S&HJexr8 zc#YbPA)9@!#w<{bO&c$oTh}bj!Q+7p0!DJ$>rw==|7()Qu!4TCZ1oc`D_*_ z#%;(*A#R4db=y!TXrB45J5KwJdgiiu=&Gpovxlj;Dw}<04YdRgcuu%so%UI>VRF4U zC`J`H6fJ&g3rri|eNLw5?b*YnAnwy8^2jh@&*-zLkD!1Pp_LzehWpA<;C)4AAlA5mOONt#V=dut1o-sY6~tJJ2Eu5;TT zv{svWfE21lJ8o&_ZBvqK55ox$qd@4wFm~~YL1I{hZZ#^y=`@=V3Nd*69Y|ThXG1Ea zk(6{#unCjm7vzfxcAc&RWFixzzrd-0oh`Dt(OO?_rChhSXcF{F#?6aAD8Ka(tNuv0 zkoif7QIA*5`{|=I#~LS6^k12eY|R4PcdYudfwp+7bvK<&ko zH;mC&-Dd-L4VBIx$)Mz`G;PIbg`iql-*(?ajDC>Qfb~CvqjB-5TSUH+?_K+-HMyXi zXind_&Ml)q@6P2htXVv2vG%Dew$5gR8E(~+(ZOBNHg;*El}g=}=}FZ!ju8o59FPf? zMUF;&v~-I+0!U)Gs#x|gY36DNpK;&)Fr*$`|A@_t_&}sJt2rS)1SLTdz>E-?+BQ76 z&!hBTN1|iPf<)k=?lrAyCyKUi{isIksd&uzhGwJV6qQ5m?j)BbkxVUk zA&8nEI<8Yqq6Otu0KfH#wjbfbzC;7fBfCLdxr>F-#e+Lj0%3qC#%9YMs0B3Ua&{?` z{N01iV~t!-mCD-muVGu^LtdZX@I58vRwD>?Qy-i2*meRc*tVxvGCWTQY-QDa`-Jal zA>0*|LrGD4{|u|1IGgyM4yo2)ZGFqEys+F5pM6_Ub}2nuZt;G=x(3E=%()K)cirY_ z`j@WU^mgc@ut68+>b%Ma;KWVv`MTV5FfqRQSq%$FOA-RDj?HR5(MNZkw;PbVE` zdHlv{c+XwmBCc*R%zHKv_a)pr$`v{c_LxHkC^mh;!5kOSdPdPINpHFL9B4ZTBI5h= zZh2Q6{|c^21Or``9J5A23SWDGS9~v z)-_6E)@26U@!(}LT-3-3!8USu)~eI}pQW=>wfz;-OF^#LY9<=6Jc=Qd`mHa+-s;t3^ImF@NBS%QW%oYnwI%RnnwEqd zV*NdFF2v0xPv@S9bJYOSdgH>sB(_C7GLBtJ!oQ9adFcI+ljg)jPRl5PZM8|v%dy^L zAVnZ^`G?pdK$OX@;$Zs{eHiH{C^)FMEnfAY?n(@zY{io`*;kJ zQgH_lp8oC2mFLW%QlK#k*yRB7?{}t(Kpl7z>TGPwJt=hbPStV z_!WN~MA7w)SvU&P{{8tqI8YSj@5U{)ZC)9?a`s=w|43DR=l{GKb%2kKVi9~1!@4qf z<>FVi+`)&HKm6kvmPcZ|Z;<8@9$6kcTaU~ygI9(<_r4)}dH0&) zY5?=bl(?+uYkU7V8{Jh042Z_5_5C&f4m0LhBZWHw4R;?mv$E{Wx(2n~>SNu>Z zkg8mzw;>z%i@Et>Lm?Zt+TcRqWEE0^z{zT041p5}oX}PUh^j|3vOg;j zS5x&?LY)veStTwAoUCXVn$Cf;11nnwN~A)G)IaPF1Wss*Vnua8;N*`*0)dm&i-cbh z0w*h5h9)1V1H)=-3$@PpiRWntP>^{gdPdtsP(F?pF!|}%K;VR?S80m`BE+s*BoH`R zy+|N%vKqlMZ2CV7PJH+fCJVm5_RPvCeyNMuv+2#cQORBfqP57gobzH4I>vtd+wEtB z)X-S=D@loajQymEcdv-(rR?uW@&Cs4pnu_va5{mqt&j7eRc~!g-8G~%?2Dw@FQzPN zS4!U)p;NhiE%CGPH|Qqb!=pVE%0|{c|F7u9a#DM4SjL*2zZ4TQz5C!MUg)*^p=pC_ z!_G}at=WKWozFG8OGO?Ha2KXEf9+kGqPPpe7u+qWOv5O@CotYIq}_1GlSbHte*gZ* zM0_-Ss+?usMaxQrVv=Bgdxiog`*<}WMD7TtAk+TjDx~;bwS9h~$P;^UtSPb%_Ll+d z3__Ic$7M|23kg--g>Aox{})A&m|04!BkhixluvNXn0TSLc6&C480x6P%5f5Wczj>r z-Lq>?o`;bbjgQS)m_7BX-{R2NXSs;KNMTbtJ3@6YV;A>uU5nn{wI$_AM@j+L+0dGcXmXArjm_Xl=RjIx@OTkVMx0Upx$7SCmv`53|7BqWY(DWeRz2C;y|~!X zc@X~c@?!h#$U}z8@$HZQA};Pd7mu_zS2blh7$BAg>PV;*?C9%v52qTREk$>}e^yt~ zy7oKkonsv7AjVb=e9*s#o2c-lgxtfwVSo1ErD*%@P`@f43$GlB&ahE+whbNuTx;1u zh9!3&*cT>~eHgFksoW3B*A)uFy41^NFU%+4>W`kCQH~ntZCVHZl+UQ2&QQAI`#(wU B>vaGC literal 0 HcmV?d00001 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..f4df8ec2f --- /dev/null +++ b/Sources/JavaScriptKit/Documentation.docc/Tutorials/Hello-World/Resources/hello-world-3-3-open.txt @@ -0,0 +1,9 @@ +$ 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 +$ python3 -m http.server +Serving HTTP on 0.0.0.0 port 8000 (http://0.0.0.0:8000/) ... +$ open http://localhost:8000 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 0000000000000000000000000000000000000000..5b24016a92caa5d22aa4a593d6b007094ebfb6a1 GIT binary patch literal 308 zcmeAS@N?(olHy`uVBq!ia0vp^j35jm7|ip2ssJg4WRD z45bDP46hOx7_4S6Fo+k-*%fF5lwc|e@(X5QD4TrN0>n%5c6VW5yxS$b1ju7A@$_|N zf62@%X3Z+2);0+!#O3MY7{YNqIRVIKVqkovxW^dCQY~?fC`m~yNwrEYN(E93Mg~Tv zx(3F&hQ=XAMpmYlRtBcp1_o9J1|q2&AERi<%}>cptHiA#)q*n~s6hj6LrG?CYH>+o XZUJsRM!FgeKs^keu6{1-oD!M<3V%pt literal 0 HcmV?d00001 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") + } +} From adbed6f56af28abf6da60d04ff3042126485512e Mon Sep 17 00:00:00 2001 From: Yuta Saito Date: Fri, 14 Mar 2025 10:55:11 +0900 Subject: [PATCH 255/373] Migrate BigInt support tests to XCTest --- .../TestSuites/Sources/PrimaryTests/I64.swift | 39 --------------- .../Sources/PrimaryTests/main.swift | 1 - Package.swift | 4 ++ .../JavaScriptBigIntSupportTests.swift | 50 +++++++++++++++++++ 4 files changed, 54 insertions(+), 40 deletions(-) delete mode 100644 IntegrationTests/TestSuites/Sources/PrimaryTests/I64.swift create mode 100644 Tests/JavaScriptBigIntSupportTests/JavaScriptBigIntSupportTests.swift diff --git a/IntegrationTests/TestSuites/Sources/PrimaryTests/I64.swift b/IntegrationTests/TestSuites/Sources/PrimaryTests/I64.swift deleted file mode 100644 index 8d8dda331..000000000 --- a/IntegrationTests/TestSuites/Sources/PrimaryTests/I64.swift +++ /dev/null @@ -1,39 +0,0 @@ -import JavaScriptBigIntSupport -import JavaScriptKit - -func testI64() throws { - try test("BigInt") { - func expectPassesThrough(signed value: Int64) throws { - let bigInt = JSBigInt(value) - try expectEqual(bigInt.description, value.description) - let bigInt2 = JSBigInt(_slowBridge: value) - try expectEqual(bigInt2.description, value.description) - } - - func expectPassesThrough(unsigned value: UInt64) throws { - let bigInt = JSBigInt(unsigned: value) - try expectEqual(bigInt.description, value.description) - let bigInt2 = JSBigInt(_slowBridge: value) - try expectEqual(bigInt2.description, value.description) - } - - try expectPassesThrough(signed: 0) - try expectPassesThrough(signed: 1 << 62) - try expectPassesThrough(signed: -2305) - for _ in 0 ..< 100 { - try expectPassesThrough(signed: .random(in: .min ... .max)) - } - try expectPassesThrough(signed: .min) - try expectPassesThrough(signed: .max) - - try expectPassesThrough(unsigned: 0) - try expectPassesThrough(unsigned: 1 << 62) - try expectPassesThrough(unsigned: 1 << 63) - try expectPassesThrough(unsigned: .min) - try expectPassesThrough(unsigned: .max) - try expectPassesThrough(unsigned: ~0) - for _ in 0 ..< 100 { - try expectPassesThrough(unsigned: .random(in: .min ... .max)) - } - } -} diff --git a/IntegrationTests/TestSuites/Sources/PrimaryTests/main.swift b/IntegrationTests/TestSuites/Sources/PrimaryTests/main.swift index 12cc91cc9..d042f5fae 100644 --- a/IntegrationTests/TestSuites/Sources/PrimaryTests/main.swift +++ b/IntegrationTests/TestSuites/Sources/PrimaryTests/main.swift @@ -914,5 +914,4 @@ try test("JSValueDecoder") { try expectEqual(decodedTama.isCat, true) } -try testI64() Expectation.wait(expectations) diff --git a/Package.swift b/Package.swift index 173add2dd..cc7165546 100644 --- a/Package.swift +++ b/Package.swift @@ -42,6 +42,10 @@ let package = Package( dependencies: ["_CJavaScriptBigIntSupport", "JavaScriptKit"] ), .target(name: "_CJavaScriptBigIntSupport", dependencies: ["_CJavaScriptKit"]), + .testTarget( + name: "JavaScriptBigIntSupportTests", + dependencies: ["JavaScriptBigIntSupport", "JavaScriptKit"] + ), .target( name: "JavaScriptEventLoop", diff --git a/Tests/JavaScriptBigIntSupportTests/JavaScriptBigIntSupportTests.swift b/Tests/JavaScriptBigIntSupportTests/JavaScriptBigIntSupportTests.swift new file mode 100644 index 000000000..e1fb8a96f --- /dev/null +++ b/Tests/JavaScriptBigIntSupportTests/JavaScriptBigIntSupportTests.swift @@ -0,0 +1,50 @@ +import XCTest +import JavaScriptBigIntSupport +import JavaScriptKit + +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)) + } + } +} From 3530396d47d9b2e9ef7a5826f09e78749d8e0047 Mon Sep 17 00:00:00 2001 From: Yuta Saito Date: Fri, 14 Mar 2025 11:46:40 +0900 Subject: [PATCH 256/373] Migrate rest of primary tests to XCTest --- IntegrationTests/TestSuites/Package.swift | 8 - .../Sources/PrimaryTests/UnitTestUtils.swift | 161 --- .../Sources/PrimaryTests/main.swift | 917 ------------------ Makefile | 2 +- Package.swift | 5 +- .../JSPromiseTests.swift | 96 ++ .../JSTimerTests.swift | 56 ++ .../JSTypedArrayTests.swift | 84 +- .../JavaScriptKitTests.swift | 674 +++++++++++++ Tests/prelude.mjs | 105 ++ 10 files changed, 1019 insertions(+), 1089 deletions(-) delete mode 100644 IntegrationTests/TestSuites/Sources/PrimaryTests/UnitTestUtils.swift delete mode 100644 IntegrationTests/TestSuites/Sources/PrimaryTests/main.swift create mode 100644 Tests/JavaScriptEventLoopTests/JSPromiseTests.swift create mode 100644 Tests/JavaScriptEventLoopTests/JSTimerTests.swift create mode 100644 Tests/JavaScriptKitTests/JavaScriptKitTests.swift diff --git a/IntegrationTests/TestSuites/Package.swift b/IntegrationTests/TestSuites/Package.swift index 95b47f94c..63a78b2cd 100644 --- a/IntegrationTests/TestSuites/Package.swift +++ b/IntegrationTests/TestSuites/Package.swift @@ -11,9 +11,6 @@ let package = Package( .macOS("12.0"), ], products: [ - .executable( - name: "PrimaryTests", targets: ["PrimaryTests"] - ), .executable( name: "ConcurrencyTests", targets: ["ConcurrencyTests"] ), @@ -24,11 +21,6 @@ let package = Package( dependencies: [.package(name: "JavaScriptKit", path: "../../")], targets: [ .target(name: "CHelpers"), - .executableTarget(name: "PrimaryTests", dependencies: [ - .product(name: "JavaScriptBigIntSupport", package: "JavaScriptKit"), - "JavaScriptKit", - "CHelpers", - ]), .executableTarget( name: "ConcurrencyTests", dependencies: [ diff --git a/IntegrationTests/TestSuites/Sources/PrimaryTests/UnitTestUtils.swift b/IntegrationTests/TestSuites/Sources/PrimaryTests/UnitTestUtils.swift deleted file mode 100644 index 0d51c6ff5..000000000 --- a/IntegrationTests/TestSuites/Sources/PrimaryTests/UnitTestUtils.swift +++ /dev/null @@ -1,161 +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 - } - print("✅ \(name)") -} - -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 expect(_ description: String, _ result: Bool, file: StaticString = #file, line: UInt = #line, column: UInt = #column) throws { - if !result { - throw MessageError(description, 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 wrapUnsafeThrowableFunction(_ body: @escaping () -> Void, file: StaticString = #file, line: UInt = #line, column: UInt = #column) throws -> JSValue { - JSObject.global.callThrowingClosure.function!(JSClosure { _ in - body() - return .undefined - }) -} -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) - } -} -func expectNil(_ value: T?, file: StaticString = #file, line: UInt = #line, column: UInt = #column) throws { - switch value { - case .some: - throw MessageError("Expect an nil", file: file, line: line, column: column) - case .none: return - } -} - -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 d042f5fae..000000000 --- a/IntegrationTests/TestSuites/Sources/PrimaryTests/main.swift +++ /dev/null @@ -1,917 +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) - - for source: JSValue in [ - .number(.infinity), .number(.nan), - .number(Double(UInt64.max).nextUp), .number(Double(Int64.min).nextDown) - ] { - try expectNil(Int.construct(from: source)) - try expectNil(Int8.construct(from: source)) - try expectNil(Int16.construct(from: source)) - try expectNil(Int32.construct(from: source)) - try expectNil(Int64.construct(from: source)) - try expectNil(UInt.construct(from: source)) - try expectNil(UInt8.construct(from: source)) - try expectNil(UInt16.construct(from: source)) - try expectNil(UInt32.construct(from: source)) - try expectNil(UInt64.construct(from: source)) - } -} - -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 - -#if JAVASCRIPTKIT_WITHOUT_WEAKREFS - // Check diagnostics of use-after-free - do { - let c1Line = #line + 1 - let c1 = JSClosure { $0[0] } - c1.release() - let error = try expectThrow(try evalClosure.throws(c1, JSValue.number(42.0))) as! JSException - try expect("Error message should contains definition location", error.thrownValue.description.hasSuffix("PrimaryTests/main.swift:\(c1Line)")) - } -#endif - - do { - let c1 = JSClosure { _ in .number(4) } - try expectEqual(c1(), .number(4)) - } - - do { - let c1 = JSClosure { _ in fatalError("Crash while closure evaluation") } - let error = try expectThrow(try evalClosure.throws(c1)) as! JSException - try expectEqual(error.thrownValue.description, "RuntimeError: unreachable") - } -} - -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("Object Decoding") { - /* - ```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 - - try expectNotNil(JSObject.construct(from: object)) - try expectEqual(JSObject.construct(from: function).map { $0 is JSFunction }, .some(true)) - try expectEqual(JSObject.construct(from: symbol).map { $0 is JSSymbol }, .some(true)) - try expectEqual(JSObject.construct(from: bigInt).map { $0 is JSBigInt }, .some(true)) - - try expectNil(JSFunction.construct(from: object)) - try expectNotNil(JSFunction.construct(from: function)) - try expectNil(JSFunction.construct(from: symbol)) - try expectNil(JSFunction.construct(from: bigInt)) - - try expectNil(JSSymbol.construct(from: object)) - try expectNil(JSSymbol.construct(from: function)) - try expectNotNil(JSSymbol.construct(from: symbol)) - try expectNil(JSSymbol.construct(from: bigInt)) - - try expectNil(JSBigInt.construct(from: object)) - try expectNil(JSBigInt.construct(from: function)) - try expectNil(JSBigInt.construct(from: symbol)) - try expectNotNil(JSBigInt.construct(from: bigInt)) -} - -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 expectThrow(try getIsCat.throws()) - - // 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: JSValue] = [ - "prop1": 1.jsValue, - "prop2": "foo".jsValue, - ] - 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 -} - -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 JSException, true) - let errorObject = JSError(from: (error1 as! JSException).thrownValue) - try expectNotNil(errorObject) - - let error2 = try expectThrow(try prop_9.object!.throwing.func2!()) - try expectEqual(error2 is JSException, true) - let errorString = try expectString((error2 as! JSException).thrownValue) - try expectEqual(errorString, "String Error") - - let error3 = try expectThrow(try prop_9.object!.throwing.func3!()) - try expectEqual(error3 is JSException, true) - let errorNumber = try expectNumber((error3 as! JSException).thrownValue) - try expectEqual(errorNumber, 3.0) - - // MARK: Simple function calls - let error4 = try expectThrow(try prop_9.func1.function!.throws()) - try expectEqual(error4 is JSException, true) - let errorObject2 = JSError(from: (error4 as! JSException).thrownValue) - 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 JSException, true) - let errorObject3 = JSError(from: (ageError as! JSException).thrownValue) - try expectNotNil(errorObject3) -} - -try test("Unhandled 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 wrapUnsafeThrowableFunction { _ = prop_9.object!.func1!() } - let errorObject = JSError(from: error1) - try expectNotNil(errorObject) - - let error2 = try wrapUnsafeThrowableFunction { _ = prop_9.object!.func2!() } - let errorString = try expectString(error2) - try expectEqual(errorString, "String Error") - - let error3 = try wrapUnsafeThrowableFunction { _ = prop_9.object!.func3!() } - let errorNumber = try expectNumber(error3) - try expectEqual(errorNumber, 3.0) -} - -/// 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) -} - -try test("Symbols") { - let symbol1 = JSSymbol("abc") - let symbol2 = JSSymbol("abc") - try expectNotEqual(symbol1, symbol2) - try expectEqual(symbol1.name, symbol2.name) - try expectEqual(symbol1.name, "abc") - - try expectEqual(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 - ) - try expectEqual(hasInstanceClass[JSSymbol.hasInstance].function!().boolean, true) - try expectEqual(JSObject.global.Object.isInstanceOf(hasInstanceClass), true) -} - -struct AnimalStruct: Decodable { - let name: String - let age: Int - let isCat: Bool -} - -try test("JSValueDecoder") { - 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) - - try expectEqual(decodedTama.name, tama.name.string) - try expectEqual(decodedTama.name, "Tama") - - try expectEqual(decodedTama.age, tama.age.number.map(Int.init)) - try expectEqual(decodedTama.age, 3) - - try expectEqual(decodedTama.isCat, tama.isCat.boolean) - try expectEqual(decodedTama.isCat, true) -} - -Expectation.wait(expectations) diff --git a/Makefile b/Makefile index ed0727ce8..93db9e51a 100644 --- a/Makefile +++ b/Makefile @@ -24,7 +24,7 @@ test: .PHONY: unittest unittest: @echo Running unit tests - swift package --swift-sdk "$(SWIFT_SDK_ID)" js test --prelude ./Tests/prelude.mjs + swift package --disable-sandbox --swift-sdk "$(SWIFT_SDK_ID)" js test --prelude ./Tests/prelude.mjs .PHONY: benchmark_setup benchmark_setup: diff --git a/Package.swift b/Package.swift index cc7165546..9b8e1ca38 100644 --- a/Package.swift +++ b/Package.swift @@ -34,7 +34,10 @@ let package = Package( .target(name: "_CJavaScriptKit"), .testTarget( name: "JavaScriptKitTests", - dependencies: ["JavaScriptKit"] + dependencies: ["JavaScriptKit"], + swiftSettings: [ + .enableExperimentalFeature("Extern") + ] ), .target( diff --git a/Tests/JavaScriptEventLoopTests/JSPromiseTests.swift b/Tests/JavaScriptEventLoopTests/JSPromiseTests.swift new file mode 100644 index 000000000..11ecdad91 --- /dev/null +++ b/Tests/JavaScriptEventLoopTests/JSPromiseTests.swift @@ -0,0 +1,96 @@ +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 + } + } + } +} diff --git a/Tests/JavaScriptEventLoopTests/JSTimerTests.swift b/Tests/JavaScriptEventLoopTests/JSTimerTests.swift new file mode 100644 index 000000000..2ee92cebd --- /dev/null +++ b/Tests/JavaScriptEventLoopTests/JSTimerTests.swift @@ -0,0 +1,56 @@ +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 start = JSDate().valueOf() + 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/JavaScriptKitTests/JSTypedArrayTests.swift b/Tests/JavaScriptKitTests/JSTypedArrayTests.swift index 87b81ae16..8e2556f8d 100644 --- a/Tests/JavaScriptKitTests/JSTypedArrayTests.swift +++ b/Tests/JavaScriptKitTests/JSTypedArrayTests.swift @@ -1,5 +1,5 @@ -import XCTest import JavaScriptKit +import XCTest final class JSTypedArrayTests: XCTestCase { func testEmptyArray() { @@ -15,4 +15,86 @@ final class JSTypedArrayTests: XCTestCase { _ = JSTypedArray([Float32]()) _ = JSTypedArray([Float64]()) } + + func testTypedArray() { + func checkArray(_ array: [T]) where T: TypedArrayElement & Equatable { + 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 { + 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))) + } } diff --git a/Tests/JavaScriptKitTests/JavaScriptKitTests.swift b/Tests/JavaScriptKitTests/JavaScriptKitTests.swift new file mode 100644 index 000000000..6c90afead --- /dev/null +++ b/Tests/JavaScriptKitTests/JavaScriptKitTests.swift @@ -0,0 +1,674 @@ +import XCTest +import JavaScriptKit + +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 let (.number(lhs), .number(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() { + var 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/prelude.mjs b/Tests/prelude.mjs index 53073a850..0cf7a3577 100644 --- a/Tests/prelude.mjs +++ b/Tests/prelude.mjs @@ -1,5 +1,6 @@ /** @type {import('./../.build/plugins/PackageToJS/outputs/PackageTests/test.d.ts').Prelude["setupOptions"]} */ export function setupOptions(options, context) { + setupTestGlobals(globalThis); return { ...options, addToCoreImports(importObject) { @@ -10,3 +11,107 @@ export function setupOptions(options, context) { } } } + +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) + }; +} From 31016991dae3f06aef95301f36c9a9936873c369 Mon Sep 17 00:00:00 2001 From: Yuta Saito Date: Fri, 14 Mar 2025 12:05:13 +0900 Subject: [PATCH 257/373] Migrate Concurrency test suite to JavaScriptEventLoopTests.swift --- IntegrationTests/TestSuites/Package.swift | 9 - .../ConcurrencyTests/UnitTestUtils.swift | 141 -------- .../Sources/ConcurrencyTests/main.swift | 221 ------------- Makefile | 8 +- .../JSPromiseTests.swift | 1 + .../JavaScriptEventLoopTests.swift | 304 ++++++++++++++++++ Tests/prelude.mjs | 1 + Tests/toolset.json | 10 + 8 files changed, 323 insertions(+), 372 deletions(-) delete mode 100644 IntegrationTests/TestSuites/Sources/ConcurrencyTests/UnitTestUtils.swift delete mode 100644 IntegrationTests/TestSuites/Sources/ConcurrencyTests/main.swift create mode 100644 Tests/JavaScriptEventLoopTests/JavaScriptEventLoopTests.swift create mode 100644 Tests/toolset.json diff --git a/IntegrationTests/TestSuites/Package.swift b/IntegrationTests/TestSuites/Package.swift index 63a78b2cd..3d583d082 100644 --- a/IntegrationTests/TestSuites/Package.swift +++ b/IntegrationTests/TestSuites/Package.swift @@ -11,9 +11,6 @@ let package = Package( .macOS("12.0"), ], products: [ - .executable( - name: "ConcurrencyTests", targets: ["ConcurrencyTests"] - ), .executable( name: "BenchmarkTests", targets: ["BenchmarkTests"] ), @@ -21,12 +18,6 @@ let package = Package( dependencies: [.package(name: "JavaScriptKit", path: "../../")], targets: [ .target(name: "CHelpers"), - .executableTarget( - name: "ConcurrencyTests", - dependencies: [ - .product(name: "JavaScriptEventLoop", package: "JavaScriptKit"), - ] - ), .executableTarget(name: "BenchmarkTests", dependencies: ["JavaScriptKit", "CHelpers"]), ] ) diff --git a/IntegrationTests/TestSuites/Sources/ConcurrencyTests/UnitTestUtils.swift b/IntegrationTests/TestSuites/Sources/ConcurrencyTests/UnitTestUtils.swift deleted file mode 100644 index acd81e6d9..000000000 --- a/IntegrationTests/TestSuites/Sources/ConcurrencyTests/UnitTestUtils.swift +++ /dev/null @@ -1,141 +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 - } - print("✅ \(name)") -} - -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 - } - print("✅ \(name)") -} - -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 expectGTE( - _ lhs: T, _ rhs: T, - file: StaticString = #file, line: UInt = #line, column: UInt = #column -) throws { - if lhs < rhs { - throw MessageError( - "Expected \(lhs) to be greater than or equal to \(rhs)", - file: file, line: line, 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 1f0764e14..000000000 --- a/IntegrationTests/TestSuites/Sources/ConcurrencyTests/main.swift +++ /dev/null @@ -1,221 +0,0 @@ -import JavaScriptEventLoop -import JavaScriptKit -#if canImport(WASILibc) -import WASILibc -#elseif canImport(Darwin) -import Darwin -#endif - -func performanceNow() -> Double { - return JSObject.global.performance.now().number! -} - -func measure(_ block: () async throws -> Void) async rethrows -> Double { - let start = performanceNow() - try await block() - return performanceNow() - start -} - -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 expectEqual(p.result, .success(.number(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: JSException.self).thrownValue - try expectEqual(jsValue, 3) - try await expectEqual(p.result, .failure(.number(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 diff = try await measure { - try await Task.sleep(nanoseconds: 200_000_000) - } - try expectGTE(diff, 200) - } - - 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"]) - } - - try await asyncTest("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 diff = try await measure { - let promise = JSPromise(from: delayObject.closure!()) - try expectNotNil(promise) - let result = try await promise!.value - try expectEqual(result, .number(3)) - } - try expectGTE(diff, 200) - } - - try await asyncTest("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 diff = try await measure { - let result = try await promise2.value - try expectEqual(result, .string("3.0")) - } - try expectGTE(diff, 200) - } - - try await asyncTest("Async JSPromise: then(success:failure:)") { - let promise = JSPromise { resolve in - _ = JSObject.global.setTimeout!( - JSClosure { _ in - resolve(.failure(JSError(message: "test").jsValue)) - return .undefined - }.jsValue, - 100 - ) - } - let promise2 = promise.then { _ in - throw MessageError("Should not be called", file: #file, line: #line, column: #column) - } failure: { err in - return err - } - let result = try await promise2.value - try expectEqual(result.object?.message, .string("test")) - } - - try await asyncTest("Async JSPromise: catch") { - let promise = JSPromise { resolve in - _ = JSObject.global.setTimeout!( - JSClosure { _ in - resolve(.failure(JSError(message: "test").jsValue)) - return .undefined - }.jsValue, - 100 - ) - } - let promise2 = promise.catch { err in - try await Task.sleep(nanoseconds: 100_000_000) - return err - } - let diff = try await measure { - let result = try await promise2.value - try expectEqual(result.object?.message, .string("test")) - } - try expectGTE(diff, 200) - } - - try await asyncTest("Task.sleep(nanoseconds:)") { - let diff = try await measure { - try await Task.sleep(nanoseconds: 100_000_000) - } - try expectGTE(diff, 100) - } - - #if compiler(>=5.7) - try await asyncTest("ContinuousClock.sleep") { - let diff = try await measure { - let c = ContinuousClock() - try await c.sleep(until: .now + .milliseconds(100)) - } - try expectGTE(diff, 99) - } - try await asyncTest("SuspendingClock.sleep") { - let diff = try await measure { - let c = SuspendingClock() - try await c.sleep(until: .now + .milliseconds(100)) - } - try expectGTE(diff, 99) - } - #endif -} - -JavaScriptEventLoop.installGlobalExecutor() -Task { - do { - try await entrypoint() - } catch { - print(error) - } -} diff --git a/Makefile b/Makefile index 93db9e51a..9ffee54b5 100644 --- a/Makefile +++ b/Makefile @@ -24,7 +24,13 @@ test: .PHONY: unittest unittest: @echo Running unit tests - swift package --disable-sandbox --swift-sdk "$(SWIFT_SDK_ID)" js test --prelude ./Tests/prelude.mjs + 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: benchmark_setup benchmark_setup: diff --git a/Tests/JavaScriptEventLoopTests/JSPromiseTests.swift b/Tests/JavaScriptEventLoopTests/JSPromiseTests.swift index 11ecdad91..e19d356e5 100644 --- a/Tests/JavaScriptEventLoopTests/JSPromiseTests.swift +++ b/Tests/JavaScriptEventLoopTests/JSPromiseTests.swift @@ -92,5 +92,6 @@ final class JSPromiseTests: XCTestCase { return JSValue.undefined } } + withExtendedLifetime(timer) {} } } diff --git a/Tests/JavaScriptEventLoopTests/JavaScriptEventLoopTests.swift b/Tests/JavaScriptEventLoopTests/JavaScriptEventLoopTests.swift new file mode 100644 index 000000000..826a5dfd8 --- /dev/null +++ b/Tests/JavaScriptEventLoopTests/JavaScriptEventLoopTests.swift @@ -0,0 +1,304 @@ +import JavaScriptEventLoop +import JavaScriptKit +import XCTest + +// 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 expectGTE( + _ lhs: T, _ rhs: T, + file: StaticString = #file, line: UInt = #line, column: UInt = #column +) throws { + if lhs < rhs { + throw MessageError( + "Expected \(lhs) to be greater than or equal to \(rhs)", + file: file, line: line, 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 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) + } +} + +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 +} + +final class JavaScriptEventLoopTests: XCTestCase { + + // 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 expectCast(error, to: E.self) + 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, 200) + + // Test shorter sleep duration + let shortSleepDiff = try await measureTime { + try await Task.sleep(nanoseconds: 100_000_000) + } + XCTAssertGreaterThanOrEqual(shortSleepDiff, 100) + } + + 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(await rejectedPromise.value) + let jsValue = try expectCast(promiseError, to: JSException.self).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, 200) + } + + // 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 expectCast(continuationError, to: E.self) + 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, 200) + } + + // 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, 99) + + // Test SuspendingClock.sleep + let suspendingClockDiff = try await measureTime { + let c = SuspendingClock() + try await c.sleep(until: .now + .milliseconds(100)) + } + XCTAssertGreaterThanOrEqual(suspendingClockDiff, 99) + } + #endif +} diff --git a/Tests/prelude.mjs b/Tests/prelude.mjs index 0cf7a3577..ab5723587 100644 --- a/Tests/prelude.mjs +++ b/Tests/prelude.mjs @@ -1,5 +1,6 @@ /** @type {import('./../.build/plugins/PackageToJS/outputs/PackageTests/test.d.ts').Prelude["setupOptions"]} */ export function setupOptions(options, context) { + Error.stackTraceLimit = 100; setupTestGlobals(globalThis); return { ...options, diff --git a/Tests/toolset.json b/Tests/toolset.json new file mode 100644 index 000000000..567fd7e53 --- /dev/null +++ b/Tests/toolset.json @@ -0,0 +1,10 @@ +{ + "schemaVersion" : "1.0", + "linker" : { + "extraCLIOptions" : [ + "--stack-first", + "-z", "stack-size=524288", + "--global-base=524288" + ] + } + } From c9ea2fd95af91a4cb55cadc50986120a4a7eb3ff Mon Sep 17 00:00:00 2001 From: Yuta Saito Date: Fri, 14 Mar 2025 12:09:56 +0900 Subject: [PATCH 258/373] Remove IntegrationTests setup as it's now a part of unittest --- IntegrationTests/bin/concurrency-tests.js | 8 -- IntegrationTests/bin/primary-tests.js | 110 ----------------- IntegrationTests/lib.js | 114 +---------------- Makefile | 9 -- .../JavaScriptEventLoopTests.swift | 116 ++++++------------ 5 files changed, 37 insertions(+), 320 deletions(-) delete mode 100644 IntegrationTests/bin/concurrency-tests.js delete mode 100644 IntegrationTests/bin/primary-tests.js diff --git a/IntegrationTests/bin/concurrency-tests.js b/IntegrationTests/bin/concurrency-tests.js deleted file mode 100644 index 02489c959..000000000 --- a/IntegrationTests/bin/concurrency-tests.js +++ /dev/null @@ -1,8 +0,0 @@ -import { startWasiTask } from "../lib.js"; - -Error.stackTraceLimit = Infinity; - -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 36ac65812..000000000 --- a/IntegrationTests/bin/primary-tests.js +++ /dev/null @@ -1,110 +0,0 @@ -Error.stackTraceLimit = Infinity; - -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) -}; - -import { startWasiTask } from "../lib.js"; - -startWasiTask("./dist/PrimaryTests.wasm").catch((err) => { - console.log(err); - process.exit(1); -}); diff --git a/IntegrationTests/lib.js b/IntegrationTests/lib.js index a2f10e565..d9c424f0e 100644 --- a/IntegrationTests/lib.js +++ b/IntegrationTests/lib.js @@ -3,7 +3,6 @@ import { WASI as NodeWASI } from "wasi" import { WASI as MicroWASI, useAll } from "uwasi" import * as fs from "fs/promises" import path from "path"; -import { Worker, parentPort } from "node:worker_threads"; const WASI = { MicroWASI: ({ args }) => { @@ -53,16 +52,6 @@ const selectWASIBackend = () => { return "Node" }; -function isUsingSharedMemory(module) { - const imports = WebAssembly.Module.imports(module); - for (const entry of imports) { - if (entry.module === "env" && entry.name === "memory" && entry.kind == "memory") { - return true; - } - } - return false; -} - function constructBaseImportObject(wasi, swift) { return { wasi_snapshot_preview1: wasi.wasiImport, @@ -74,79 +63,6 @@ function constructBaseImportObject(wasi, swift) { } } -export async function startWasiChildThread(event) { - const { module, programName, memory, tid, startArg } = event; - const swift = new SwiftRuntime({ - sharedMemory: true, - threadChannel: { - postMessageToMainThread: (message, transfer) => { - parentPort.postMessage(message, transfer); - }, - listenMessageFromMainThread: (listener) => { - parentPort.on("message", listener) - } - } - }); - // Use uwasi for child threads because Node.js WASI cannot be used without calling - // `WASI.start` or `WASI.initialize`, which is already called in the main thread and - // will cause an error if called again. - const wasi = WASI.MicroWASI({ programName }); - - const importObject = constructBaseImportObject(wasi, swift); - - importObject["wasi"] = { - "thread-spawn": () => { - throw new Error("Cannot spawn a new thread from a worker thread") - } - }; - importObject["env"] = { memory }; - importObject["JavaScriptEventLoopTestSupportTests"] = { - "isMainThread": () => false, - } - - const instance = await WebAssembly.instantiate(module, importObject); - swift.setInstance(instance); - wasi.setInstance(instance); - swift.startThread(tid, startArg); -} - -class ThreadRegistry { - workers = new Map(); - nextTid = 1; - - spawnThread(module, programName, memory, startArg) { - const tid = this.nextTid++; - const selfFilePath = new URL(import.meta.url).pathname; - const worker = new Worker(` - const { parentPort } = require('node:worker_threads'); - - Error.stackTraceLimit = 100; - parentPort.once("message", async (event) => { - const { selfFilePath } = event; - const { startWasiChildThread } = await import(selfFilePath); - await startWasiChildThread(event); - }) - `, { type: "module", eval: true }) - - worker.on("error", (error) => { - console.error(`Worker thread ${tid} error:`, error); - throw error; - }); - this.workers.set(tid, worker); - worker.postMessage({ selfFilePath, module, programName, memory, tid, startArg }); - return tid; - } - - worker(tid) { - return this.workers.get(tid); - } - - wakeUpWorkerThread(tid, message, transfer) { - const worker = this.workers.get(tid); - worker.postMessage(message, transfer); - } -} - export const startWasiTask = async (wasmPath, wasiConstructorKey = selectWASIBackend()) => { // Fetch our Wasm File const wasmBinary = await fs.readFile(wasmPath); @@ -157,38 +73,10 @@ export const startWasiTask = async (wasmPath, wasiConstructorKey = selectWASIBac const module = await WebAssembly.compile(wasmBinary); - const sharedMemory = isUsingSharedMemory(module); - const threadRegistry = new ThreadRegistry(); - const swift = new SwiftRuntime({ - sharedMemory, - threadChannel: { - postMessageToWorkerThread: threadRegistry.wakeUpWorkerThread.bind(threadRegistry), - listenMessageFromWorkerThread: (tid, listener) => { - const worker = threadRegistry.worker(tid); - worker.on("message", listener); - } - } - }); + const swift = new SwiftRuntime(); const importObject = constructBaseImportObject(wasi, swift); - importObject["JavaScriptEventLoopTestSupportTests"] = { - "isMainThread": () => true, - } - - if (sharedMemory) { - // We don't have JS API to get memory descriptor of imported memory - // at this moment, so we assume 256 pages (16MB) memory is enough - // large for initial memory size. - const memory = new WebAssembly.Memory({ initial: 1024, maximum: 16384, shared: true }) - importObject["env"] = { memory }; - importObject["wasi"] = { - "thread-spawn": (startArg) => { - return threadRegistry.spawnThread(module, programName, memory, startArg); - } - } - } - // Instantiate the WebAssembly file const instance = await WebAssembly.instantiate(module, importObject); diff --git a/Makefile b/Makefile index 9ffee54b5..c8b79b4ab 100644 --- a/Makefile +++ b/Makefile @@ -12,15 +12,6 @@ build: swift build --triple wasm32-unknown-wasi npm run build -.PHONY: test -test: - @echo Running integration tests - cd IntegrationTests && \ - CONFIGURATION=debug SWIFT_BUILD_FLAGS="$(SWIFT_BUILD_FLAGS)" $(MAKE) test && \ - CONFIGURATION=debug SWIFT_BUILD_FLAGS="$(SWIFT_BUILD_FLAGS) -Xswiftc -DJAVASCRIPTKIT_WITHOUT_WEAKREFS" $(MAKE) test && \ - CONFIGURATION=release SWIFT_BUILD_FLAGS="$(SWIFT_BUILD_FLAGS)" $(MAKE) test && \ - CONFIGURATION=release SWIFT_BUILD_FLAGS="$(SWIFT_BUILD_FLAGS) -Xswiftc -DJAVASCRIPTKIT_WITHOUT_WEAKREFS" $(MAKE) test - .PHONY: unittest unittest: @echo Running unit tests diff --git a/Tests/JavaScriptEventLoopTests/JavaScriptEventLoopTests.swift b/Tests/JavaScriptEventLoopTests/JavaScriptEventLoopTests.swift index 826a5dfd8..40eb96af0 100644 --- a/Tests/JavaScriptEventLoopTests/JavaScriptEventLoopTests.swift +++ b/Tests/JavaScriptEventLoopTests/JavaScriptEventLoopTests.swift @@ -2,91 +2,47 @@ import JavaScriptEventLoop import JavaScriptKit import XCTest -// 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 expectGTE( - _ lhs: T, _ rhs: T, - file: StaticString = #file, line: UInt = #line, column: UInt = #column -) throws { - if lhs < rhs { - throw MessageError( - "Expected \(lhs) to be greater than or equal to \(rhs)", - file: file, line: line, column: column - ) +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 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 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 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) + func performanceNow() -> Double { + return JSObject.global.performance.now().number! } - return value -} -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 + func measureTime(_ block: () async throws -> Void) async rethrows -> Double { + let start = performanceNow() + try await block() + return performanceNow() - start } - 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) + // Error type used in tests + struct E: Error, Equatable { + let value: Int } -} - -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 -} - -final class JavaScriptEventLoopTests: XCTestCase { // MARK: - Task Tests @@ -103,7 +59,7 @@ final class JavaScriptEventLoopTests: XCTestCase { throw E(value: 2) } let error = try await expectAsyncThrow(await throwingHandle.value) - let e = try expectCast(error, to: E.self) + let e = try XCTUnwrap(error as? E) XCTAssertEqual(e, E(value: 2)) } @@ -173,8 +129,8 @@ final class JavaScriptEventLoopTests: XCTestCase { let rejectedPromise = JSPromise(resolver: { resolve in resolve(.failure(.number(3))) }) - let promiseError = try await expectAsyncThrow(await rejectedPromise.value) - let jsValue = try expectCast(promiseError, to: JSException.self).thrownValue + 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))) @@ -258,7 +214,7 @@ final class JavaScriptEventLoopTests: XCTestCase { cont.resume(throwing: E(value: 2)) } ) - let errorValue = try expectCast(continuationError, to: E.self) + let errorValue = try XCTUnwrap(continuationError as? E) XCTAssertEqual(errorValue.value, 2) } From 0371513908bd7b78e6392b776aa4adfcb53b61d5 Mon Sep 17 00:00:00 2001 From: Yuta Saito Date: Fri, 14 Mar 2025 12:26:16 +0900 Subject: [PATCH 259/373] Fix wrong resource bundling logic in Package.swift --- Package.swift | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Package.swift b/Package.swift index 173add2dd..d42bca6ba 100644 --- a/Package.swift +++ b/Package.swift @@ -4,7 +4,7 @@ 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 = shouldBuildForEmbedded || (Context.environment["JAVASCRIPTKIT_USE_LEGACY_RESOURCE_BUNDLING"].flatMap(Bool.init) ?? false) +let useLegacyResourceBundling = Context.environment["JAVASCRIPTKIT_USE_LEGACY_RESOURCE_BUNDLING"].flatMap(Bool.init) ?? false let package = Package( name: "JavaScriptKit", @@ -19,8 +19,8 @@ let package = Package( .target( name: "JavaScriptKit", dependencies: ["_CJavaScriptKit"], - exclude: useLegacyResourceBundling ? ["Runtime"] : [], - resources: useLegacyResourceBundling ? [] : [.copy("Runtime")], + exclude: useLegacyResourceBundling ? [] : ["Runtime"], + resources: useLegacyResourceBundling ? [.copy("Runtime")] : [], cSettings: shouldBuildForEmbedded ? [ .unsafeFlags(["-fdeclspec"]) ] : nil, From 8d1eadcb9b5cb0df2b5f2dc7b07e7c3648ec8226 Mon Sep 17 00:00:00 2001 From: Yuta Saito Date: Fri, 14 Mar 2025 12:28:19 +0900 Subject: [PATCH 260/373] Remove remaining references to `make test` --- .github/workflows/test.yml | 1 - CONTRIBUTING.md | 12 ++++-------- 2 files changed, 4 insertions(+), 9 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 62e2a8ac9..c50de248a 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -41,7 +41,6 @@ jobs: - name: Configure Swift SDK run: echo "SWIFT_SDK_ID=${{ steps.setup-swiftwasm.outputs.swift-sdk-id }}" >> $GITHUB_ENV - run: make bootstrap - - run: make test - run: make unittest # Skip unit tests with uwasi because its proc_exit throws # unhandled promise rejection. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 2526556c6..38454374a 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -58,14 +58,10 @@ Thank you for considering contributing to JavaScriptKit! We welcome contribution ``` ### Running Tests -- Run unit tests: - ```bash - make unittest SWIFT_SDK_ID=wasm32-unknown-wasi - ``` -- Run integration tests: - ```bash - make test SWIFT_SDK_ID=wasm32-unknown-wasi - ``` + +```bash +make unittest SWIFT_SDK_ID=wasm32-unknown-wasi +``` ### Editing `./Runtime` directory From 52a2221d071b19c3989cb1bca8237d0a5c30d66b Mon Sep 17 00:00:00 2001 From: Yuta Saito Date: Fri, 14 Mar 2025 12:29:11 +0900 Subject: [PATCH 261/373] Remove unused toolset.json --- Tests/toolset.json | 10 ---------- 1 file changed, 10 deletions(-) delete mode 100644 Tests/toolset.json diff --git a/Tests/toolset.json b/Tests/toolset.json deleted file mode 100644 index 567fd7e53..000000000 --- a/Tests/toolset.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "schemaVersion" : "1.0", - "linker" : { - "extraCLIOptions" : [ - "--stack-first", - "-z", "stack-size=524288", - "--global-base=524288" - ] - } - } From dd010777568d8a534ef4b2f7d41754d9deccd85a Mon Sep 17 00:00:00 2001 From: Yuta Saito Date: Fri, 14 Mar 2025 12:41:03 +0900 Subject: [PATCH 262/373] Remove the old test targets from the Makefile --- IntegrationTests/Makefile | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/IntegrationTests/Makefile b/IntegrationTests/Makefile index 30ffef297..54a656fd1 100644 --- a/IntegrationTests/Makefile +++ b/IntegrationTests/Makefile @@ -34,14 +34,3 @@ run_benchmark: .PHONY: benchmark benchmark: benchmark_setup run_benchmark - -.PHONY: primary_test -primary_test: build_rt dist/PrimaryTests.wasm - $(NODEJS) bin/primary-tests.js - -.PHONY: concurrency_test -concurrency_test: build_rt dist/ConcurrencyTests.wasm - $(NODEJS) bin/concurrency-tests.js - -.PHONY: test -test: concurrency_test primary_test From 218022e28b6037b876209ec1929ef169a0ad3754 Mon Sep 17 00:00:00 2001 From: Yuta Saito Date: Fri, 14 Mar 2025 07:56:09 +0000 Subject: [PATCH 263/373] PackageToJS: Fix the missing dependency on the stripWasm task --- Plugins/PackageToJS/Sources/PackageToJS.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Plugins/PackageToJS/Sources/PackageToJS.swift b/Plugins/PackageToJS/Sources/PackageToJS.swift index c34b6a57b..44aaf532d 100644 --- a/Plugins/PackageToJS/Sources/PackageToJS.swift +++ b/Plugins/PackageToJS/Sources/PackageToJS.swift @@ -200,7 +200,7 @@ struct PackagingPlanner { } // Then, run wasm-opt with all optimizations wasm = make.addTask( - inputFiles: [selfPath], inputTasks: [outputDirTask, stripWasm], + inputFiles: [selfPath, stripWasmPath], inputTasks: [outputDirTask, stripWasm], output: finalWasmPath ) { print("Optimizing the wasm file...") From 7790846a4c3b19f5be8c249b8dcb3c0b46bd53d3 Mon Sep 17 00:00:00 2001 From: Yuta Saito Date: Fri, 14 Mar 2025 11:21:28 +0000 Subject: [PATCH 264/373] Add `WebWorkerDedicatedExecutor` to run actors on a dedicated web worker thread --- .../WebWorkerDedicatedExecutor.swift | 60 +++++++++++++++++++ 1 file changed, 60 insertions(+) create mode 100644 Sources/JavaScriptEventLoop/WebWorkerDedicatedExecutor.swift diff --git a/Sources/JavaScriptEventLoop/WebWorkerDedicatedExecutor.swift b/Sources/JavaScriptEventLoop/WebWorkerDedicatedExecutor.swift new file mode 100644 index 000000000..d1a3c048e --- /dev/null +++ b/Sources/JavaScriptEventLoop/WebWorkerDedicatedExecutor.swift @@ -0,0 +1,60 @@ +import JavaScriptKit +import _CJavaScriptEventLoop + +#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 14.0, iOS 17.0, watchOS 10.0, tvOS 17.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) + } +} From 15af3c878c3a5054ca34c90342e4fbe922022d3b Mon Sep 17 00:00:00 2001 From: Yuta Saito Date: Fri, 14 Mar 2025 11:37:43 +0000 Subject: [PATCH 265/373] Add `JSPromise.value()` method to wto avoid switching isolation domains Otherwise, we don7t have a way to wait for a promise created on a worker to complete. --- .../JavaScriptEventLoop.swift | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/Sources/JavaScriptEventLoop/JavaScriptEventLoop.swift b/Sources/JavaScriptEventLoop/JavaScriptEventLoop.swift index 07eec2cd2..ce4fb1047 100644 --- a/Sources/JavaScriptEventLoop/JavaScriptEventLoop.swift +++ b/Sources/JavaScriptEventLoop/JavaScriptEventLoop.swift @@ -232,6 +232,24 @@ public extension JSPromise { } } + /// Wait for the promise to complete, returning its result or exception as a Result. + /// + /// - Note: Calling this function does not switch from the caller's isolation domain. + func value(isolation: isolated (any Actor)? = #isolation) async throws -> JSValue { + try await withUnsafeThrowingContinuation(isolation: isolation) { [self] continuation in + self.then( + success: { + continuation.resume(returning: $0) + return JSValue.undefined + }, + failure: { + continuation.resume(throwing: JSException($0)) + return JSValue.undefined + } + ) + } + } + /// Wait for the promise to complete, returning its result or exception as a Result. var result: JSPromise.Result { get async { From 640065157445bd51a45a6ecadbcb5d6ea126cee5 Mon Sep 17 00:00:00 2001 From: Yuta Saito Date: Fri, 14 Mar 2025 11:39:57 +0000 Subject: [PATCH 266/373] Add ActorOnWebWorker example --- Examples/ActorOnWebWorker/Package.swift | 20 ++ Examples/ActorOnWebWorker/README.md | 21 ++ Examples/ActorOnWebWorker/Sources/MyApp.swift | 262 ++++++++++++++++++ Examples/ActorOnWebWorker/build.sh | 3 + Examples/ActorOnWebWorker/index.html | 31 +++ Examples/ActorOnWebWorker/serve.json | 14 + 6 files changed, 351 insertions(+) create mode 100644 Examples/ActorOnWebWorker/Package.swift create mode 100644 Examples/ActorOnWebWorker/README.md create mode 100644 Examples/ActorOnWebWorker/Sources/MyApp.swift create mode 100755 Examples/ActorOnWebWorker/build.sh create mode 100644 Examples/ActorOnWebWorker/index.html create mode 100644 Examples/ActorOnWebWorker/serve.json diff --git a/Examples/ActorOnWebWorker/Package.swift b/Examples/ActorOnWebWorker/Package.swift new file mode 100644 index 000000000..711bf6461 --- /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..7d362d13e --- /dev/null +++ b/Examples/ActorOnWebWorker/Sources/MyApp.swift @@ -0,0 +1,262 @@ +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: any SerialExecutor + + // 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.asUnownedSerialExecutor() + } + + init(serialExecutor: any SerialExecutor) { + 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 var container: JSValue + private var urlInput: JSValue + private var indexButton: JSValue + private var searchInput: JSValue + private var searchButton: JSValue + private var statusElement: JSValue + private var resultsElement: JSValue + + // 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 { + var noResults = document.createElement("p") + noResults.innerText = .string("No results found.") + _ = resultsElement.appendChild(noResults) + } else { + // Display up to 10 results + for (index, result) in results.prefix(10).enumerated() { + var resultItem = document.createElement("div") + 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) + } + } + } +} + +@main struct Main { + @MainActor static var app: App? + + static func main() { + JavaScriptEventLoop.installGlobalExecutor() + WebWorkerTaskExecutor.installGlobalExecutor() + + Task { + // Create dedicated worker and search service + let dedicatedWorker = try await WebWorkerDedicatedExecutor() + let service = SearchService(serialExecutor: dedicatedWorker) + 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..2797702e1 --- /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" + } + ] + }] +} From 9c38b8f2f1b3184cae69d8f81e315d6f18ef1782 Mon Sep 17 00:00:00 2001 From: Yuta Saito Date: Fri, 14 Mar 2025 11:51:31 +0000 Subject: [PATCH 267/373] Add test cases for `WebWorkerDedicatedExecutor` --- .../JavaScriptEventLoopTestSupport.swift | 5 +++ .../WebWorkerDedicatedExecutorTests.swift | 32 +++++++++++++++++++ .../WebWorkerTaskExecutorTests.swift | 4 --- 3 files changed, 37 insertions(+), 4 deletions(-) create mode 100644 Tests/JavaScriptEventLoopTests/WebWorkerDedicatedExecutorTests.swift diff --git a/Sources/JavaScriptEventLoopTestSupport/JavaScriptEventLoopTestSupport.swift b/Sources/JavaScriptEventLoopTestSupport/JavaScriptEventLoopTestSupport.swift index 4c441f3c4..0582fe8c4 100644 --- a/Sources/JavaScriptEventLoopTestSupport/JavaScriptEventLoopTestSupport.swift +++ b/Sources/JavaScriptEventLoopTestSupport/JavaScriptEventLoopTestSupport.swift @@ -27,6 +27,11 @@ import JavaScriptEventLoop func swift_javascriptkit_activate_js_executor_impl() { MainActor.assumeIsolated { JavaScriptEventLoop.installGlobalExecutor() + #if canImport(wasi_pthread) && compiler(>=6.1) && _runtime(_multithreaded) + if #available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) { + WebWorkerTaskExecutor.installGlobalExecutor() + } + #endif } } diff --git a/Tests/JavaScriptEventLoopTests/WebWorkerDedicatedExecutorTests.swift b/Tests/JavaScriptEventLoopTests/WebWorkerDedicatedExecutorTests.swift new file mode 100644 index 000000000..c05740117 --- /dev/null +++ b/Tests/JavaScriptEventLoopTests/WebWorkerDedicatedExecutorTests.swift @@ -0,0 +1,32 @@ +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()) + } +} diff --git a/Tests/JavaScriptEventLoopTests/WebWorkerTaskExecutorTests.swift b/Tests/JavaScriptEventLoopTests/WebWorkerTaskExecutorTests.swift index 0dfdac25f..1696224df 100644 --- a/Tests/JavaScriptEventLoopTests/WebWorkerTaskExecutorTests.swift +++ b/Tests/JavaScriptEventLoopTests/WebWorkerTaskExecutorTests.swift @@ -23,10 +23,6 @@ func pthread_mutex_lock(_ mutex: UnsafeMutablePointer) -> Int32 #endif final class WebWorkerTaskExecutorTests: XCTestCase { - override func setUp() async { - WebWorkerTaskExecutor.installGlobalExecutor() - } - func testTaskRunOnMainThread() async throws { let executor = try await WebWorkerTaskExecutor(numberOfThreads: 1) From 1751cba6b77adf35e7e53bb70683ed01b616c3fa Mon Sep 17 00:00:00 2001 From: Yuta Saito Date: Fri, 14 Mar 2025 11:52:36 +0000 Subject: [PATCH 268/373] Fix native build --- Sources/JavaScriptEventLoop/WebWorkerDedicatedExecutor.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/JavaScriptEventLoop/WebWorkerDedicatedExecutor.swift b/Sources/JavaScriptEventLoop/WebWorkerDedicatedExecutor.swift index d1a3c048e..695eb9c61 100644 --- a/Sources/JavaScriptEventLoop/WebWorkerDedicatedExecutor.swift +++ b/Sources/JavaScriptEventLoop/WebWorkerDedicatedExecutor.swift @@ -31,7 +31,7 @@ import _CJavaScriptEventLoop /// ``` /// /// - SeeAlso: ``WebWorkerTaskExecutor`` -@available(macOS 14.0, iOS 17.0, watchOS 10.0, tvOS 17.0, *) +@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 From f638d147f877fbb0a10c0f8bb57a8d79005852c2 Mon Sep 17 00:00:00 2001 From: Yuta Saito Date: Fri, 14 Mar 2025 11:54:57 +0000 Subject: [PATCH 269/373] Fix test build --- .../WebWorkerDedicatedExecutorTests.swift | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Tests/JavaScriptEventLoopTests/WebWorkerDedicatedExecutorTests.swift b/Tests/JavaScriptEventLoopTests/WebWorkerDedicatedExecutorTests.swift index c05740117..b6c2bd8db 100644 --- a/Tests/JavaScriptEventLoopTests/WebWorkerDedicatedExecutorTests.swift +++ b/Tests/JavaScriptEventLoopTests/WebWorkerDedicatedExecutorTests.swift @@ -1,3 +1,4 @@ +#if compiler(>=6.1) && _runtime(_multithreaded) import XCTest @testable import JavaScriptEventLoop @@ -30,3 +31,4 @@ final class WebWorkerDedicatedExecutorTests: XCTestCase { XCTAssertTrue(isMainThread()) } } +#endif From 22fb0a716812f57bf0208d4b6bf89bc86d4d7891 Mon Sep 17 00:00:00 2001 From: Yuta Saito Date: Fri, 14 Mar 2025 14:18:39 +0000 Subject: [PATCH 270/373] Remove reference to swift-docc-plugin from Package.swift It seems like the conditional dependency broke swiftpackageindex's documentation publishing. --- Package.swift | 6 ------ 1 file changed, 6 deletions(-) diff --git a/Package.swift b/Package.swift index d62965759..86c533d1c 100644 --- a/Package.swift +++ b/Package.swift @@ -90,9 +90,3 @@ let package = Package( ), ] ) - -if Context.environment["JAVASCRIPTKIT_USE_DOCC_PLUGIN"] != nil { - package.dependencies.append( - .package(url: "https://github.com/apple/swift-docc-plugin", from: "1.4.0") - ) -} From 9cb46194f575a3a86aba6ef5d40b30da5a9dbb93 Mon Sep 17 00:00:00 2001 From: Yuta Saito Date: Fri, 14 Mar 2025 14:27:29 +0000 Subject: [PATCH 271/373] Remove unused variable in JSTimerTests.swift --- Tests/JavaScriptEventLoopTests/JSTimerTests.swift | 1 - 1 file changed, 1 deletion(-) diff --git a/Tests/JavaScriptEventLoopTests/JSTimerTests.swift b/Tests/JavaScriptEventLoopTests/JSTimerTests.swift index 2ee92cebd..1d3fec036 100644 --- a/Tests/JavaScriptEventLoopTests/JSTimerTests.swift +++ b/Tests/JavaScriptEventLoopTests/JSTimerTests.swift @@ -43,7 +43,6 @@ final class JSTimerTests: XCTestCase { } func testTimer() async throws { - let start = JSDate().valueOf() let timeoutMilliseconds = 5.0 var timeout: JSTimer! await withCheckedContinuation { continuation in From 9d1b014482bbb3b371569cd0e1c6ea2a8a308737 Mon Sep 17 00:00:00 2001 From: Yuta Saito Date: Fri, 14 Mar 2025 14:27:45 +0000 Subject: [PATCH 272/373] Relax the timing constraints in JavaScriptEventLoopTests.swift --- Tests/JavaScriptEventLoopTests/JavaScriptEventLoopTests.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Tests/JavaScriptEventLoopTests/JavaScriptEventLoopTests.swift b/Tests/JavaScriptEventLoopTests/JavaScriptEventLoopTests.swift index 40eb96af0..029876904 100644 --- a/Tests/JavaScriptEventLoopTests/JavaScriptEventLoopTests.swift +++ b/Tests/JavaScriptEventLoopTests/JavaScriptEventLoopTests.swift @@ -247,14 +247,14 @@ final class JavaScriptEventLoopTests: XCTestCase { let c = ContinuousClock() try await c.sleep(until: .now + .milliseconds(100)) } - XCTAssertGreaterThanOrEqual(continuousClockDiff, 99) + XCTAssertGreaterThanOrEqual(continuousClockDiff, 50) // Test SuspendingClock.sleep let suspendingClockDiff = try await measureTime { let c = SuspendingClock() try await c.sleep(until: .now + .milliseconds(100)) } - XCTAssertGreaterThanOrEqual(suspendingClockDiff, 99) + XCTAssertGreaterThanOrEqual(suspendingClockDiff, 50) } #endif } From eae4d1150e4bd3c42581d100bbd8f123c2e87023 Mon Sep 17 00:00:00 2001 From: Yuta Saito Date: Sat, 15 Mar 2025 03:08:28 +0000 Subject: [PATCH 273/373] Setup unit test infrastructure for PackageToJS --- Plugins/PackageToJS/Package.swift | 6 +- Plugins/PackageToJS/Sources/MiniMake.swift | 155 ++++++-- Plugins/PackageToJS/Sources/PackageToJS.swift | 302 ++++++++------ .../Sources/PackageToJSPlugin.swift | 60 ++- Plugins/PackageToJS/Sources/ParseWasm.swift | 8 +- Plugins/PackageToJS/Tests/MiniMakeTests.swift | 167 ++++---- .../Tests/PackagingPlannerTests.swift | 92 +++++ .../PackageToJS/Tests/SnapshotTesting.swift | 34 ++ .../planBuild_debug.json | 275 +++++++++++++ .../planBuild_release.json | 290 ++++++++++++++ .../planBuild_release_no_optimize.json | 275 +++++++++++++ .../planBuild_release_split_debug.json | 290 ++++++++++++++ .../PackagingPlannerTests/planTestBuild.json | 367 ++++++++++++++++++ 13 files changed, 2049 insertions(+), 272 deletions(-) create mode 100644 Plugins/PackageToJS/Tests/PackagingPlannerTests.swift create mode 100644 Plugins/PackageToJS/Tests/SnapshotTesting.swift create mode 100644 Plugins/PackageToJS/Tests/__Snapshots__/PackagingPlannerTests/planBuild_debug.json create mode 100644 Plugins/PackageToJS/Tests/__Snapshots__/PackagingPlannerTests/planBuild_release.json create mode 100644 Plugins/PackageToJS/Tests/__Snapshots__/PackagingPlannerTests/planBuild_release_no_optimize.json create mode 100644 Plugins/PackageToJS/Tests/__Snapshots__/PackagingPlannerTests/planBuild_release_split_debug.json create mode 100644 Plugins/PackageToJS/Tests/__Snapshots__/PackagingPlannerTests/planTestBuild.json diff --git a/Plugins/PackageToJS/Package.swift b/Plugins/PackageToJS/Package.swift index 1cc9318bd..57ccf3cf9 100644 --- a/Plugins/PackageToJS/Package.swift +++ b/Plugins/PackageToJS/Package.swift @@ -7,6 +7,10 @@ let package = Package( platforms: [.macOS(.v13)], targets: [ .target(name: "PackageToJS"), - .testTarget(name: "PackageToJSTests", dependencies: ["PackageToJS"]), + .testTarget( + name: "PackageToJSTests", + dependencies: ["PackageToJS"], + exclude: ["__Snapshots__"] + ), ] ) diff --git a/Plugins/PackageToJS/Sources/MiniMake.swift b/Plugins/PackageToJS/Sources/MiniMake.swift index 04e781690..3544a80f3 100644 --- a/Plugins/PackageToJS/Sources/MiniMake.swift +++ b/Plugins/PackageToJS/Sources/MiniMake.swift @@ -14,13 +14,13 @@ struct MiniMake { /// Information about a task enough to capture build /// graph changes - struct TaskInfo: Codable { + struct TaskInfo: Encodable { /// Input tasks not yet built let wants: [TaskKey] - /// Set of files that must be built before this task - let inputs: [String] - /// Output task name - let output: String + /// 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 @@ -30,25 +30,23 @@ struct MiniMake { /// A task to build struct Task { let info: TaskInfo - /// Input tasks not yet built + /// Input tasks (files and phony tasks) not yet built let wants: Set /// Attributes of the task let attributes: Set - /// Display name of the task - let displayName: String /// Key of the task let key: TaskKey /// Build operation - let build: (Task) throws -> Void + let build: (_ task: Task, _ scope: VariableScope) throws -> Void /// Whether the task is done var isDone: Bool - var inputs: [String] { self.info.inputs } - var output: String { self.info.output } + var inputs: [BuildPath] { self.info.inputs } + var output: BuildPath { self.info.output } } /// A task key - struct TaskKey: Codable, Hashable, Comparable, CustomStringConvertible { + struct TaskKey: Encodable, Hashable, Comparable, CustomStringConvertible { let id: String var description: String { self.id } @@ -56,15 +54,45 @@ struct MiniMake { 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 - /// Current working directory at the time the build started - private let buildCwd: String /// Prints progress of the build private var printProgress: ProgressPrinter.PrintProgress @@ -74,20 +102,16 @@ struct MiniMake { ) { self.tasks = [:] self.shouldExplain = explain - self.buildCwd = FileManager.default.currentDirectoryPath self.printProgress = printProgress } /// Adds a task to the build system mutating func addTask( - inputFiles: [String] = [], inputTasks: [TaskKey] = [], output: String, + inputFiles: [BuildPath] = [], inputTasks: [TaskKey] = [], output: BuildPath, attributes: [TaskAttribute] = [], salt: (any Encodable)? = nil, - build: @escaping (Task) throws -> Void + build: @escaping (_ task: Task, _ scope: VariableScope) throws -> Void ) -> TaskKey { - let displayName = - output.hasPrefix(self.buildCwd) - ? String(output.dropFirst(self.buildCwd.count + 1)) : output - let taskKey = TaskKey(id: output) + let taskKey = TaskKey(id: output.description) let saltData = try! salt.map { let encoder = JSONEncoder() encoder.outputFormatting = .sortedKeys @@ -99,7 +123,7 @@ struct MiniMake { ) self.tasks[taskKey] = Task( info: info, wants: Set(inputTasks), attributes: Set(attributes), - displayName: displayName, key: taskKey, build: build, isDone: false) + key: taskKey, build: build, isDone: false) return taskKey } @@ -107,9 +131,12 @@ struct MiniMake { /// /// This fingerprint must be stable across builds and must change /// if the build graph changes in any way. - func computeFingerprint(root: TaskKey) throws -> Data { + 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) } @@ -126,7 +153,13 @@ struct MiniMake { /// Prints progress of the build struct ProgressPrinter { - typealias PrintProgress = (_ subject: Task, _ total: Int, _ built: Int, _ message: String) -> Void + 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 @@ -145,17 +178,17 @@ struct MiniMake { private static var yellow: String { "\u{001B}[33m" } private static var reset: String { "\u{001B}[0m" } - mutating func started(_ task: Task) { - self.print(task, "\(Self.green)building\(Self.reset)") + mutating func started(_ task: Task, scope: VariableScope) { + self.print(task, scope, "\(Self.green)building\(Self.reset)") } - mutating func skipped(_ task: Task) { - self.print(task, "\(Self.yellow)skipped\(Self.reset)") + mutating func skipped(_ task: Task, scope: VariableScope) { + self.print(task, scope, "\(Self.yellow)skipped\(Self.reset)") } - private mutating func print(_ task: Task, _ message: @autoclosure () -> String) { + private mutating func print(_ task: Task, _ scope: VariableScope, _ message: @autoclosure () -> String) { guard !task.attributes.contains(.silent) else { return } - self.printProgress(task, self.total, self.built, message()) + self.printProgress(Context(subject: task, total: self.total, built: self.built, scope: scope), message()) self.built += 1 } } @@ -176,32 +209,32 @@ struct MiniMake { } /// Cleans all outputs of all tasks - func cleanEverything() { + func cleanEverything(scope: VariableScope) { for task in self.tasks.values { - try? FileManager.default.removeItem(atPath: task.output) + try? FileManager.default.removeItem(at: scope.resolve(path: task.output)) } } /// Starts building - func build(output: TaskKey) throws { + 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 = URL(fileURLWithPath: task.output) - if !FileManager.default.fileExists(atPath: task.output) { + 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 = URL(fileURLWithPath: input) + let inputURL = scope.resolve(path: input) // Ignore directory modification times var isDirectory: ObjCBool = false let fileExists = FileManager.default.fileExists( - atPath: input, isDirectory: &isDirectory) + atPath: inputURL.path, isDirectory: &isDirectory) if fileExists && isDirectory.boolValue { return false } @@ -238,10 +271,10 @@ struct MiniMake { } if shouldBuild(task: task) { - progressPrinter.started(task) - try task.build(task) + progressPrinter.started(task, scope: scope) + try task.build(task, scope) } else { - progressPrinter.skipped(task) + progressPrinter.skipped(task, scope: scope) } task.isDone = true tasks[taskKey] = task @@ -249,3 +282,45 @@ struct MiniMake { 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 index 44aaf532d..bd70660f3 100644 --- a/Plugins/PackageToJS/Sources/PackageToJS.swift +++ b/Plugins/PackageToJS/Sources/PackageToJS.swift @@ -9,7 +9,7 @@ struct PackageToJS { /// Whether to explain the build plan var explain: Bool = false /// Whether to use CDN for dependency packages - var useCDN: Bool + var useCDN: Bool = false } struct BuildOptions { @@ -39,6 +39,37 @@ struct PackageToJS { /// 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, extraArguments: [String]) throws { + let node = try which("node") + let arguments = ["--experimental-wasi-unstable-preview1", testRunner.path] + extraArguments + print("Running test...") + logCommandExecution(node.path, arguments) + + let task = Process() + task.executableURL = node + task.arguments = arguments + task.currentDirectoryURL = currentDirectoryURL + try task.forwardTerminationSignals { + try task.run() + task.waitUntilExit() + } + // swift-testing returns EX_UNAVAILABLE (which is 69 in wasi-libc) for "no tests found" + guard task.terminationStatus == 0 || task.terminationStatus == 69 else { + throw PackageToJSError("Test failed with status \(task.terminationStatus)") + } + } } struct PackageToJSError: Swift.Error, CustomStringConvertible { @@ -49,45 +80,24 @@ struct PackageToJSError: Swift.Error, CustomStringConvertible { } } -/// 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: URL - /// The path of this file itself, used to capture changes of planner code - let selfPath: String - /// The directory for the final output - let outputDir: URL - /// The directory for intermediate files - let intermediatesDir: URL - /// The filename of the .wasm file - let wasmFilename = "main.wasm" - /// The path to the .wasm product artifact - let wasmProductArtifact: URL +protocol PackagingSystem { + func createDirectory(atPath: String) throws + func syncFile(from: String, to: String) throws + func writeFile(atPath: String, content: Data) throws - init( - options: PackageToJS.PackageOptions, - packageId: String, - pluginWorkDirectoryURL: URL, - selfPackageDir: URL, - outputDir: URL, - wasmProductArtifact: URL - ) { - self.options = options - self.packageId = packageId - self.selfPackageDir = selfPackageDir - self.outputDir = outputDir - self.intermediatesDir = pluginWorkDirectoryURL.appending(path: outputDir.lastPathComponent + ".tmp") - self.selfPath = String(#filePath) - self.wasmProductArtifact = wasmProductArtifact - } + func wasmOpt(_ arguments: [String], input: String, output: String) throws + func npmInstall(packageDir: String) throws +} - // MARK: - Primitive build operations +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 + ) + } - private static func syncFile(from: String, to: String) throws { + func syncFile(from: String, to: String) throws { if FileManager.default.fileExists(atPath: to) { try FileManager.default.removeItem(atPath: to) } @@ -97,14 +107,25 @@ struct PackagingPlanner { ) } - private static func createDirectory(atPath: String) throws { - guard !FileManager.default.fileExists(atPath: atPath) else { return } - try FileManager.default.createDirectory( - atPath: atPath, withIntermediateDirectories: true, attributes: nil - ) + 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 { + func npmInstall(packageDir: String) throws { + try runCommand(try which("npm"), ["-C", packageDir, "install"]) + } + + func wasmOpt(_ arguments: [String], input: String, output: String) throws { + try runCommand(try which("wasm-opt"), arguments + ["-o", output, input]) } - private static func runCommand(_ command: URL, _ arguments: [String]) throws { + private func runCommand(_ command: URL, _ arguments: [String]) throws { let task = Process() task.executableURL = command task.arguments = arguments @@ -115,6 +136,73 @@ struct PackagingPlanner { throw PackageToJSError("Command failed with status \(task.terminationStatus)") } } +} + +private func which(_ executable: String) throws -> 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") +} + +/// 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 directory for the final output + let outputDir: BuildPath + /// The directory for intermediate files + let intermediatesDir: BuildPath + /// The filename of the .wasm file + let wasmFilename = "main.wasm" + /// 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, + outputDir: BuildPath, + wasmProductArtifact: BuildPath, + configuration: String, + triple: String, + selfPath: BuildPath = BuildPath(absolute: #filePath), + system: any PackagingSystem = DefaultPackagingSystem() + ) { + self.options = options + self.packageId = packageId + self.selfPackageDir = selfPackageDir + self.outputDir = outputDir + self.intermediatesDir = intermediatesDir + self.selfPath = selfPath + self.wasmProductArtifact = wasmProductArtifact + self.configuration = configuration + self.triple = triple + self.system = system + } // MARK: - Build plans @@ -123,23 +211,12 @@ struct PackagingPlanner { make: inout MiniMake, buildOptions: PackageToJS.BuildOptions ) throws -> MiniMake.TaskKey { - let (allTasks, _, _) = try planBuildInternal( + let (allTasks, _, _, _) = try planBuildInternal( make: &make, splitDebug: buildOptions.splitDebug, noOptimize: buildOptions.noOptimize ) return make.addTask( - inputTasks: allTasks, output: "all", attributes: [.phony, .silent] - ) { _ in } - } - - func deriveBuildConfiguration() -> (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 = self.wasmProductArtifact.resolvingSymlinksInPath() - let buildConfiguration = wasmProductArtifact.deletingLastPathComponent().lastPathComponent - let triple = wasmProductArtifact.deletingLastPathComponent().deletingLastPathComponent().lastPathComponent - return (buildConfiguration, triple) + inputTasks: allTasks, output: BuildPath(phony: "all"), attributes: [.phony, .silent] + ) { _, _ in } } private func planBuildInternal( @@ -148,55 +225,49 @@ struct PackagingPlanner { ) throws -> ( allTasks: [MiniMake.TaskKey], outputDirTask: MiniMake.TaskKey, + intermediatesDirTask: MiniMake.TaskKey, packageJsonTask: MiniMake.TaskKey ) { // Prepare output directory let outputDirTask = make.addTask( - inputFiles: [selfPath], output: outputDir.path, attributes: [.silent] + inputFiles: [selfPath], output: outputDir, attributes: [.silent] ) { - try Self.createDirectory(atPath: $0.output) + 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 (buildConfiguration, _) = deriveBuildConfiguration() let wasm: MiniMake.TaskKey let shouldOptimize: Bool - let wasmOptPath = try? which("wasm-opt") - if buildConfiguration == "debug" { + if self.configuration == "debug" { shouldOptimize = false } else { - if wasmOptPath != nil { - shouldOptimize = !noOptimize - } else { - print("Warning: wasm-opt not found in PATH, skipping optimizations") - shouldOptimize = false - } + shouldOptimize = !noOptimize } let intermediatesDirTask = make.addTask( - inputFiles: [selfPath], output: intermediatesDir.path, attributes: [.silent] + inputFiles: [selfPath], output: intermediatesDir, attributes: [.silent] ) { - try Self.createDirectory(atPath: $0.output) + try system.createDirectory(atPath: $1.resolve(path: $0.output).path) } - let finalWasmPath = outputDir.appending(path: wasmFilename).path + let finalWasmPath = outputDir.appending(path: wasmFilename) - if let wasmOptPath = wasmOptPath, shouldOptimize { + if shouldOptimize { // Optimize the wasm in release mode // If splitDebug is true, we need to place the DWARF-stripped wasm file (but "name" section remains) // in the output directory. - let stripWasmPath = (splitDebug ? outputDir : intermediatesDir).appending(path: wasmFilename + ".debug").path + let stripWasmPath = (splitDebug ? outputDir : intermediatesDir).appending(path: wasmFilename + ".debug") // First, strip DWARF sections as their existence enables DWARF preserving mode in wasm-opt let stripWasm = make.addTask( - inputFiles: [selfPath, wasmProductArtifact.path], inputTasks: [outputDirTask, intermediatesDirTask], + inputFiles: [selfPath, wasmProductArtifact], inputTasks: [outputDirTask, intermediatesDirTask], output: stripWasmPath ) { print("Stripping DWARF debug info...") - try Self.runCommand(wasmOptPath, [wasmProductArtifact.path, "--strip-dwarf", "--debuginfo", "-o", $0.output]) + 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( @@ -204,15 +275,15 @@ struct PackagingPlanner { output: finalWasmPath ) { print("Optimizing the wasm file...") - try Self.runCommand(wasmOptPath, [stripWasmPath, "-Os", "-o", $0.output]) + try system.wasmOpt(["-Os"], input: $1.resolve(path: stripWasmPath).path, output: $1.resolve(path: $0.output).path) } } else { // Copy the wasm product artifact wasm = make.addTask( - inputFiles: [selfPath, wasmProductArtifact.path], inputTasks: [outputDirTask], + inputFiles: [selfPath, wasmProductArtifact], inputTasks: [outputDirTask], output: finalWasmPath ) { - try Self.syncFile(from: wasmProductArtifact.path, to: $0.output) + try system.syncFile(from: $1.resolve(path: wasmProductArtifact).path, to: $1.resolve(path: $0.output).path) } } packageInputs.append(wasm) @@ -220,22 +291,24 @@ struct PackagingPlanner { let wasmImportsPath = intermediatesDir.appending(path: "wasm-imports.json") let wasmImportsTask = make.addTask( inputFiles: [selfPath, finalWasmPath], inputTasks: [outputDirTask, intermediatesDirTask, wasm], - output: wasmImportsPath.path + output: wasmImportsPath ) { - let metadata = try parseImports(moduleBytes: Array(try Data(contentsOf: URL(fileURLWithPath: finalWasmPath)))) + 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 jsonData.write(to: URL(fileURLWithPath: $0.output)) + 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.path, attributes: [.silent] + inputFiles: [selfPath], output: platformsDir, attributes: [.silent] ) { - try Self.createDirectory(atPath: $0.output) + try system.createDirectory(atPath: $1.resolve(path: $0.output).path) } let packageJsonTask = planCopyTemplateFile( @@ -259,40 +332,39 @@ struct PackagingPlanner { ] { packageInputs.append(planCopyTemplateFile( make: &make, file: file, output: output, outputDirTask: outputDirTask, - inputFiles: [wasmImportsPath.path], inputTasks: [platformsDirTask, wasmImportsTask], - wasmImportsPath: wasmImportsPath.path + inputFiles: [wasmImportsPath], inputTasks: [platformsDirTask, wasmImportsTask], + wasmImportsPath: wasmImportsPath )) } - return (packageInputs, outputDirTask, packageJsonTask) + 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: URL) { - var (allTasks, outputDirTask, packageJsonTask) = try planBuildInternal( + ) throws -> (rootTask: MiniMake.TaskKey, binDir: BuildPath) { + var (allTasks, outputDirTask, intermediatesDirTask, packageJsonTask) = try planBuildInternal( make: &make, splitDebug: false, noOptimize: false ) // Install npm dependencies used in the test harness - let npm = try which("npm") allTasks.append(make.addTask( inputFiles: [ selfPath, - outputDir.appending(path: "package.json").path, - ], inputTasks: [outputDirTask, packageJsonTask], - output: intermediatesDir.appending(path: "npm-install.stamp").path + outputDir.appending(path: "package.json"), + ], inputTasks: [intermediatesDirTask, packageJsonTask], + output: intermediatesDir.appending(path: "npm-install.stamp") ) { - try Self.runCommand(npm, ["-C", outputDir.path, "install"]) - _ = FileManager.default.createFile(atPath: $0.output, contents: Data(), attributes: nil) + 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.path + output: binDir ) { - try Self.createDirectory(atPath: $0.output) + try system.createDirectory(atPath: $1.resolve(path: $0.output).path) } allTasks.append(binDirTask) @@ -309,8 +381,8 @@ struct PackagingPlanner { )) } let rootTask = make.addTask( - inputTasks: allTasks, output: "all", attributes: [.phony, .silent] - ) { _ in } + inputTasks: allTasks, output: BuildPath(phony: "all"), attributes: [.phony, .silent] + ) { _, _ in } return (rootTask, binDir) } @@ -319,9 +391,9 @@ struct PackagingPlanner { file: String, output: String, outputDirTask: MiniMake.TaskKey, - inputFiles: [String], + inputFiles: [BuildPath], inputTasks: [MiniMake.TaskKey], - wasmImportsPath: String? = nil + wasmImportsPath: BuildPath? = nil ) -> MiniMake.TaskKey { struct Salt: Encodable { @@ -330,7 +402,6 @@ struct PackagingPlanner { } let inputPath = selfPackageDir.appending(path: file) - let (_, triple) = deriveBuildConfiguration() let conditions = [ "USE_SHARED_MEMORY": triple == "wasm32-unknown-wasip1-threads", "IS_WASI": triple.hasPrefix("wasm32-unknown-wasi"), @@ -343,13 +414,14 @@ struct PackagingPlanner { let salt = Salt(conditions: conditions, substitutions: constantSubstitutions) return make.addTask( - inputFiles: [selfPath, inputPath.path] + inputFiles, inputTasks: [outputDirTask] + inputTasks, - output: outputDir.appending(path: output).path, salt: salt + inputFiles: [selfPath, inputPath] + inputFiles, inputTasks: [outputDirTask] + inputTasks, + output: outputDir.appending(path: output), salt: salt ) { var substitutions = constantSubstitutions if let wasmImportsPath = wasmImportsPath { - let importEntries = try JSONDecoder().decode([ImportEntry].self, from: Data(contentsOf: URL(fileURLWithPath: 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)" @@ -358,33 +430,17 @@ struct PackagingPlanner { } } + 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: file, options: options) - try content.write(toFile: $0.output, atomically: true, encoding: .utf8) + 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 which(_ executable: String) throws -> 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") -} - func logCommandExecution(_ command: String, _ arguments: [String]) { var fullArguments = [command] fullArguments.append(contentsOf: arguments) diff --git a/Plugins/PackageToJS/Sources/PackageToJSPlugin.swift b/Plugins/PackageToJS/Sources/PackageToJSPlugin.swift index 7e12eb94f..727356443 100644 --- a/Plugins/PackageToJS/Sources/PackageToJSPlugin.swift +++ b/Plugins/PackageToJS/Sources/PackageToJSPlugin.swift @@ -122,7 +122,8 @@ struct PackageToJSPlugin: CommandPlugin { make: &make, buildOptions: buildOptions) cleanIfBuildGraphChanged(root: rootTask, make: make, context: context) print("Packaging...") - try make.build(output: rootTask) + let scope = MiniMake.VariableScope(variables: [:]) + try make.build(output: rootTask, scope: scope) print("Packaging finished") } @@ -192,10 +193,11 @@ struct PackageToJSPlugin: CommandPlugin { make: &make) cleanIfBuildGraphChanged(root: rootTask, make: make, context: context) print("Packaging tests...") - try make.build(output: rootTask) + let scope = MiniMake.VariableScope(variables: [:]) + try make.build(output: rootTask, scope: scope) print("Packaging tests finished") - let testRunner = binDir.appending(path: "test.js") + let testRunner = scope.resolve(path: binDir.appending(path: "test.js")) if !testOptions.buildOnly { var testJsArguments: [String] = [] var testFrameworkArguments: [String] = [] @@ -212,38 +214,18 @@ struct PackageToJSPlugin: CommandPlugin { if testOptions.inspect { testJsArguments += ["--inspect"] } - try runTest( - testRunner: testRunner, context: context, + try PackageToJS.runTest( + testRunner: testRunner, currentDirectoryURL: context.pluginWorkDirectoryURL, extraArguments: testJsArguments + ["--"] + testFrameworkArguments + testOptions.filter ) - try runTest( - testRunner: testRunner, context: context, + try PackageToJS.runTest( + testRunner: testRunner, currentDirectoryURL: context.pluginWorkDirectoryURL, extraArguments: testJsArguments + ["--", "--testing-library", "swift-testing"] + testFrameworkArguments + testOptions.filter.flatMap { ["--filter", $0] } ) } } - private func runTest(testRunner: URL, context: PluginContext, extraArguments: [String]) throws { - let node = try which("node") - let arguments = ["--experimental-wasi-unstable-preview1", testRunner.path] + extraArguments - print("Running test...") - logCommandExecution(node.path, arguments) - - let task = Process() - task.executableURL = node - task.arguments = arguments - task.currentDirectoryURL = context.pluginWorkDirectoryURL - try task.forwardTerminationSignals { - try task.run() - task.waitUntilExit() - } - // swift-testing returns EX_UNAVAILABLE (which is 69 in wasi-libc) for "no tests found" - guard task.terminationStatus == 0 || task.terminationStatus == 69 else { - throw PackageToJSError("Test failed with status \(task.terminationStatus)") - } - } - private func buildWasm(productName: String, context: PluginContext) throws -> PackageManager.BuildResult { @@ -282,14 +264,18 @@ struct PackageToJSPlugin: CommandPlugin { let lastBuildFingerprint = try? Data(contentsOf: buildFingerprint) let currentBuildFingerprint = try? make.computeFingerprint(root: root) if lastBuildFingerprint != currentBuildFingerprint { - print("Build graph changed, cleaning...") - make.cleanEverything() + printStderr("Build graph changed, cleaning...") + make.cleanEverything(scope: MiniMake.VariableScope(variables: [:])) } try? currentBuildFingerprint?.write(to: buildFingerprint) } - private func printProgress(task: MiniMake.Task, total: Int, built: Int, message: String) { - printStderr("[\(built + 1)/\(total)] \(task.displayName): \(message)") + 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)") } } @@ -457,13 +443,17 @@ extension PackagingPlanner { outputDir: URL, wasmProductArtifact: URL ) { + let outputBaseName = outputDir.lastPathComponent + let (configuration, triple) = PackageToJS.deriveBuildConfiguration(wasmProductArtifact: wasmProductArtifact) self.init( options: options, packageId: context.package.id, - pluginWorkDirectoryURL: context.pluginWorkDirectoryURL, - selfPackageDir: selfPackage.directoryURL, - outputDir: outputDir, - wasmProductArtifact: wasmProductArtifact + intermediatesDir: BuildPath(absolute: context.pluginWorkDirectoryURL.appending(path: outputBaseName + ".tmp").path), + selfPackageDir: BuildPath(absolute: selfPackage.directoryURL.path), + outputDir: BuildPath(absolute: outputDir.path), + wasmProductArtifact: BuildPath(absolute: wasmProductArtifact.path), + configuration: configuration, + triple: triple ) } } diff --git a/Plugins/PackageToJS/Sources/ParseWasm.swift b/Plugins/PackageToJS/Sources/ParseWasm.swift index 1cec9e43f..a35b69561 100644 --- a/Plugins/PackageToJS/Sources/ParseWasm.swift +++ b/Plugins/PackageToJS/Sources/ParseWasm.swift @@ -1,3 +1,5 @@ +import struct Foundation.Data + /// Represents the type of value in WebAssembly enum ValueType: String, Codable { case i32 @@ -62,10 +64,10 @@ struct ImportEntry: Codable { /// Parse state for WebAssembly parsing private class ParseState { - private let moduleBytes: [UInt8] + private let moduleBytes: Data private var offset: Int - init(moduleBytes: [UInt8]) { + init(moduleBytes: Data) { self.moduleBytes = moduleBytes self.offset = 0 } @@ -158,7 +160,7 @@ enum ParseError: Error { /// - Parameter moduleBytes: The WebAssembly module bytes /// - Returns: Array of import entries /// - Throws: ParseError if the module bytes are invalid -func parseImports(moduleBytes: [UInt8]) throws -> [ImportEntry] { +func parseImports(moduleBytes: Data) throws -> [ImportEntry] { let parseState = ParseState(moduleBytes: moduleBytes) try parseMagicNumber(parseState) try parseVersion(parseState) diff --git a/Plugins/PackageToJS/Tests/MiniMakeTests.swift b/Plugins/PackageToJS/Tests/MiniMakeTests.swift index bb097115c..f76af298e 100644 --- a/Plugins/PackageToJS/Tests/MiniMakeTests.swift +++ b/Plugins/PackageToJS/Tests/MiniMakeTests.swift @@ -7,15 +7,18 @@ import Testing // Test basic task management functionality @Test func basicTaskManagement() throws { try withTemporaryDirectory { tempDir in - var make = MiniMake(printProgress: { _, _, _, _ in }) - let outputPath = tempDir.appendingPathComponent("output.txt").path + var make = MiniMake(printProgress: { _, _ in }) + let outDir = BuildPath(prefix: "OUTPUT") - let task = make.addTask(output: outputPath) { task in - try "Hello".write(toFile: task.output, atomically: true, encoding: .utf8) + let task = make.addTask(output: outDir.appending(path: "output.txt")) { + print($0.output, $1.resolve(path: $0.output).path) + try "Hello".write(toFile: $1.resolve(path: $0.output).path, atomically: true, encoding: .utf8) } - try make.build(output: task) - let content = try String(contentsOfFile: outputPath, 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") } } @@ -23,29 +26,33 @@ import Testing // Test that task dependencies are handled correctly @Test func taskDependencies() throws { try withTemporaryDirectory { tempDir in - var make = MiniMake(printProgress: { _, _, _, _ in }) - let input = tempDir.appendingPathComponent("input.txt").path - let intermediate = tempDir.appendingPathComponent("intermediate.txt").path - let output = tempDir.appendingPathComponent("output.txt").path - - try "Input".write(toFile: input, atomically: true, encoding: .utf8) - - let intermediateTask = make.addTask(inputFiles: [input], output: intermediate) { task in - let content = try String(contentsOfFile: task.inputs[0], encoding: .utf8) + 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: task.output, atomically: true, encoding: .utf8) + toFile: scope.resolve(path: task.output).path, atomically: true, encoding: .utf8) } let finalTask = make.addTask( inputFiles: [intermediate], inputTasks: [intermediateTask], output: output - ) { task in - let content = try String(contentsOfFile: task.inputs[0], encoding: .utf8) + ) { task, scope in + let content = try String(contentsOfFile: scope.resolve(path: task.inputs[0]).path, encoding: .utf8) try (content + " final").write( - toFile: task.output, atomically: true, encoding: .utf8) + toFile: scope.resolve(path: task.output).path, atomically: true, encoding: .utf8) } - try make.build(output: finalTask) - let content = try String(contentsOfFile: output, 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") } } @@ -53,18 +60,22 @@ import Testing // Test that phony tasks are always rebuilt @Test func phonyTask() throws { try withTemporaryDirectory { tempDir in - var make = MiniMake(printProgress: { _, _, _, _ in }) - let outputPath = tempDir.appendingPathComponent("phony.txt").path - try "Hello".write(toFile: outputPath, atomically: true, encoding: .utf8) + 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 in + let task = make.addTask(output: outputPath, attributes: [.phony]) { task, scope in buildCount += 1 - try String(buildCount).write(toFile: task.output, atomically: true, encoding: .utf8) + try String(buildCount).write(toFile: scope.resolve(path: task.output).path, atomically: true, encoding: .utf8) } - try make.build(output: task) - try make.build(output: task) + 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") } @@ -72,13 +83,13 @@ import Testing // Test that the same build graph produces stable fingerprints @Test func fingerprintStability() throws { - var make1 = MiniMake(printProgress: { _, _, _, _ in }) - var make2 = MiniMake(printProgress: { _, _, _, _ in }) + var make1 = MiniMake(printProgress: { _, _ in }) + var make2 = MiniMake(printProgress: { _, _ in }) - let output1 = "output1.txt" + let output1 = BuildPath(prefix: "OUTPUT") - let task1 = make1.addTask(output: output1) { _ in } - let task2 = make2.addTask(output: output1) { _ in } + 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) @@ -89,30 +100,34 @@ import Testing // Test that rebuilds are controlled by timestamps @Test func timestampBasedRebuild() throws { try withTemporaryDirectory { tempDir in - var make = MiniMake(printProgress: { _, _, _, _ in }) - let input = tempDir.appendingPathComponent("input.txt").path - let output = tempDir.appendingPathComponent("output.txt").path + 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: input, atomically: true, encoding: .utf8) + try "Initial".write(toFile: scope.resolve(path: input).path, atomically: true, encoding: .utf8) - let task = make.addTask(inputFiles: [input], output: output) { task in + let task = make.addTask(inputFiles: [input], output: output) { task, scope in buildCount += 1 - let content = try String(contentsOfFile: task.inputs[0], encoding: .utf8) - try content.write(toFile: task.output, atomically: true, encoding: .utf8) + 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) + try make.build(output: task, scope: scope) #expect(buildCount == 1, "First build should occur") // Second build without changes - try make.build(output: task) + 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: input, atomically: true, encoding: .utf8) - try make.build(output: task) + 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") } } @@ -122,26 +137,30 @@ import Testing try withTemporaryDirectory { tempDir in var messages: [(String, Int, Int, String)] = [] var make = MiniMake( - printProgress: { task, total, built, message in - messages.append((URL(fileURLWithPath: task.output).lastPathComponent, total, built, message)) + printProgress: { ctx, message in + messages.append((ctx.subject.output.description, ctx.total, ctx.built, message)) } ) - let silentOutputPath = tempDir.appendingPathComponent("silent.txt").path - let silentTask = make.addTask(output: silentOutputPath, attributes: [.silent]) { task in - try "Silent".write(toFile: task.output, atomically: true, encoding: .utf8) + 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 = tempDir.appendingPathComponent("output.txt").path + let finalOutputPath = prefix.appending(path: "output.txt") let task = make.addTask( inputTasks: [silentTask], output: finalOutputPath - ) { task in - try "Hello".write(toFile: task.output, atomically: true, encoding: .utf8) + ) { task, scope in + try "Hello".write(toFile: scope.resolve(path: task.output).path, atomically: true, encoding: .utf8) } - try make.build(output: task) - #expect(FileManager.default.fileExists(atPath: silentOutputPath), "Silent task should still create output file") - #expect(FileManager.default.fileExists(atPath: finalOutputPath), "Final task should create output file") + 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] == ("output.txt", 1, 0, "\u{1B}[32mbuilding\u{1B}[0m")) + #expect(messages[0] == ("$PREFIX/output.txt", 1, 0, "\u{1B}[32mbuilding\u{1B}[0m")) } } @@ -149,15 +168,19 @@ import Testing @Test func errorWhileBuilding() throws { struct BuildError: Error {} try withTemporaryDirectory { tempDir in - var make = MiniMake(printProgress: { _, _, _, _ in }) - let output = tempDir.appendingPathComponent("error.txt").path - - let task = make.addTask(output: output) { task 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) + try make.build(output: task, scope: scope) } } } @@ -165,37 +188,41 @@ import Testing // Test that cleanup functionality works correctly @Test func cleanup() throws { try withTemporaryDirectory { tempDir in - var make = MiniMake(printProgress: { _, _, _, _ in }) + var make = MiniMake(printProgress: { _, _ in }) + let prefix = BuildPath(prefix: "PREFIX") + let scope = MiniMake.VariableScope(variables: [ + "PREFIX": tempDir.path, + ]) let outputs = [ - tempDir.appendingPathComponent("clean1.txt").path, - tempDir.appendingPathComponent("clean2.txt").path, + 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 in - try "Content".write(toFile: task.output, atomically: true, encoding: .utf8) + 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) + try make.build(output: task, scope: scope) } // Verify files exist for output in outputs { #expect( - FileManager.default.fileExists(atPath: output), + FileManager.default.fileExists(atPath: scope.resolve(path: output).path), "Output file should exist before cleanup") } // Clean everything - make.cleanEverything() + make.cleanEverything(scope: scope) // Verify files are removed for output in outputs { #expect( - !FileManager.default.fileExists(atPath: output), + !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..7269bea2d --- /dev/null +++ b/Plugins/PackageToJS/Tests/PackagingPlannerTests.swift @@ -0,0 +1,92 @@ +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 + ) + } + + @Test(arguments: [ + (variant: "debug", configuration: "debug", splitDebug: false, noOptimize: false), + (variant: "release", configuration: "release", splitDebug: false, noOptimize: false), + (variant: "release_split_debug", configuration: "release", splitDebug: true, noOptimize: false), + (variant: "release_no_optimize", configuration: "release", splitDebug: false, noOptimize: true), + ]) + func planBuild(variant: String, configuration: String, splitDebug: Bool, noOptimize: Bool) throws { + let options = PackageToJS.PackageOptions() + let system = TestPackagingSystem() + let planner = PackagingPlanner( + options: options, + packageId: "test", + intermediatesDir: BuildPath(prefix: "INTERMEDIATES"), + selfPackageDir: BuildPath(prefix: "SELF_PACKAGE"), + outputDir: BuildPath(prefix: "OUTPUT"), + wasmProductArtifact: BuildPath(prefix: "WASM_PRODUCT_ARTIFACT"), + 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", + splitDebug: splitDebug, + noOptimize: noOptimize, + 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"), + outputDir: BuildPath(prefix: "OUTPUT"), + wasmProductArtifact: BuildPath(prefix: "WASM_PRODUCT_ARTIFACT"), + 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/SnapshotTesting.swift b/Plugins/PackageToJS/Tests/SnapshotTesting.swift new file mode 100644 index 000000000..8e556357b --- /dev/null +++ b/Plugins/PackageToJS/Tests/SnapshotTesting.swift @@ -0,0 +1,34 @@ +import Testing +import Foundation + +func assertSnapshot( + filePath: String = #filePath, function: String = #function, + sourceLocation: SourceLocation = #_sourceLocation, + variant: String? = nil, + input: Data +) 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/__Snapshots__/PackagingPlannerTests/planBuild_debug.json b/Plugins/PackageToJS/Tests/__Snapshots__/PackagingPlannerTests/planBuild_debug.json new file mode 100644 index 000000000..0b1b2ac80 --- /dev/null +++ b/Plugins/PackageToJS/Tests/__Snapshots__/PackagingPlannerTests/planBuild_debug.json @@ -0,0 +1,275 @@ +[ + { + "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" : "eyJjb25kaXRpb25zIjp7IklTX1dBU0kiOnRydWUsIlVTRV9TSEFSRURfTUVNT1JZIjpmYWxzZSwiVVNFX1dBU0lfQ0ROIjpmYWxzZX0sInN1YnN0aXR1dGlvbnMiOnsiUEFDS0FHRV9UT19KU19NT0RVTEVfUEFUSCI6Im1haW4ud2FzbSIsIlBBQ0tBR0VfVE9fSlNfUEFDS0FHRV9OQU1FIjoidGVzdCJ9fQ==", + "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" : "eyJjb25kaXRpb25zIjp7IklTX1dBU0kiOnRydWUsIlVTRV9TSEFSRURfTUVNT1JZIjpmYWxzZSwiVVNFX1dBU0lfQ0ROIjpmYWxzZX0sInN1YnN0aXR1dGlvbnMiOnsiUEFDS0FHRV9UT19KU19NT0RVTEVfUEFUSCI6Im1haW4ud2FzbSIsIlBBQ0tBR0VfVE9fSlNfUEFDS0FHRV9OQU1FIjoidGVzdCJ9fQ==", + "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" : "eyJjb25kaXRpb25zIjp7IklTX1dBU0kiOnRydWUsIlVTRV9TSEFSRURfTUVNT1JZIjpmYWxzZSwiVVNFX1dBU0lfQ0ROIjpmYWxzZX0sInN1YnN0aXR1dGlvbnMiOnsiUEFDS0FHRV9UT19KU19NT0RVTEVfUEFUSCI6Im1haW4ud2FzbSIsIlBBQ0tBR0VfVE9fSlNfUEFDS0FHRV9OQU1FIjoidGVzdCJ9fQ==", + "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" : "eyJjb25kaXRpb25zIjp7IklTX1dBU0kiOnRydWUsIlVTRV9TSEFSRURfTUVNT1JZIjpmYWxzZSwiVVNFX1dBU0lfQ0ROIjpmYWxzZX0sInN1YnN0aXR1dGlvbnMiOnsiUEFDS0FHRV9UT19KU19NT0RVTEVfUEFUSCI6Im1haW4ud2FzbSIsIlBBQ0tBR0VfVE9fSlNfUEFDS0FHRV9OQU1FIjoidGVzdCJ9fQ==", + "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" : "eyJjb25kaXRpb25zIjp7IklTX1dBU0kiOnRydWUsIlVTRV9TSEFSRURfTUVNT1JZIjpmYWxzZSwiVVNFX1dBU0lfQ0ROIjpmYWxzZX0sInN1YnN0aXR1dGlvbnMiOnsiUEFDS0FHRV9UT19KU19NT0RVTEVfUEFUSCI6Im1haW4ud2FzbSIsIlBBQ0tBR0VfVE9fSlNfUEFDS0FHRV9OQU1FIjoidGVzdCJ9fQ==", + "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" : "eyJjb25kaXRpb25zIjp7IklTX1dBU0kiOnRydWUsIlVTRV9TSEFSRURfTUVNT1JZIjpmYWxzZSwiVVNFX1dBU0lfQ0ROIjpmYWxzZX0sInN1YnN0aXR1dGlvbnMiOnsiUEFDS0FHRV9UT19KU19NT0RVTEVfUEFUSCI6Im1haW4ud2FzbSIsIlBBQ0tBR0VfVE9fSlNfUEFDS0FHRV9OQU1FIjoidGVzdCJ9fQ==", + "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" : "eyJjb25kaXRpb25zIjp7IklTX1dBU0kiOnRydWUsIlVTRV9TSEFSRURfTUVNT1JZIjpmYWxzZSwiVVNFX1dBU0lfQ0ROIjpmYWxzZX0sInN1YnN0aXR1dGlvbnMiOnsiUEFDS0FHRV9UT19KU19NT0RVTEVfUEFUSCI6Im1haW4ud2FzbSIsIlBBQ0tBR0VfVE9fSlNfUEFDS0FHRV9OQU1FIjoidGVzdCJ9fQ==", + "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" : "eyJjb25kaXRpb25zIjp7IklTX1dBU0kiOnRydWUsIlVTRV9TSEFSRURfTUVNT1JZIjpmYWxzZSwiVVNFX1dBU0lfQ0ROIjpmYWxzZX0sInN1YnN0aXR1dGlvbnMiOnsiUEFDS0FHRV9UT19KU19NT0RVTEVfUEFUSCI6Im1haW4ud2FzbSIsIlBBQ0tBR0VfVE9fSlNfUEFDS0FHRV9OQU1FIjoidGVzdCJ9fQ==", + "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" : "eyJjb25kaXRpb25zIjp7IklTX1dBU0kiOnRydWUsIlVTRV9TSEFSRURfTUVNT1JZIjpmYWxzZSwiVVNFX1dBU0lfQ0ROIjpmYWxzZX0sInN1YnN0aXR1dGlvbnMiOnsiUEFDS0FHRV9UT19KU19NT0RVTEVfUEFUSCI6Im1haW4ud2FzbSIsIlBBQ0tBR0VfVE9fSlNfUEFDS0FHRV9OQU1FIjoidGVzdCJ9fQ==", + "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" : "eyJjb25kaXRpb25zIjp7IklTX1dBU0kiOnRydWUsIlVTRV9TSEFSRURfTUVNT1JZIjpmYWxzZSwiVVNFX1dBU0lfQ0ROIjpmYWxzZX0sInN1YnN0aXR1dGlvbnMiOnsiUEFDS0FHRV9UT19KU19NT0RVTEVfUEFUSCI6Im1haW4ud2FzbSIsIlBBQ0tBR0VfVE9fSlNfUEFDS0FHRV9OQU1FIjoidGVzdCJ9fQ==", + "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" : "eyJjb25kaXRpb25zIjp7IklTX1dBU0kiOnRydWUsIlVTRV9TSEFSRURfTUVNT1JZIjpmYWxzZSwiVVNFX1dBU0lfQ0ROIjpmYWxzZX0sInN1YnN0aXR1dGlvbnMiOnsiUEFDS0FHRV9UT19KU19NT0RVTEVfUEFUSCI6Im1haW4ud2FzbSIsIlBBQ0tBR0VfVE9fSlNfUEFDS0FHRV9OQU1FIjoidGVzdCJ9fQ==", + "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" + ] + } +] \ 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..bb2c3f74b --- /dev/null +++ b/Plugins/PackageToJS/Tests/__Snapshots__/PackagingPlannerTests/planBuild_release.json @@ -0,0 +1,290 @@ +[ + { + "attributes" : [ + "silent" + ], + "inputs" : [ + "$PLANNER_SOURCE_PATH" + ], + "output" : "$INTERMEDIATES", + "wants" : [ + + ] + }, + { + "attributes" : [ + + ], + "inputs" : [ + "$PLANNER_SOURCE_PATH", + "$WASM_PRODUCT_ARTIFACT" + ], + "output" : "$INTERMEDIATES\/main.wasm.debug", + "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" : "eyJjb25kaXRpb25zIjp7IklTX1dBU0kiOnRydWUsIlVTRV9TSEFSRURfTUVNT1JZIjpmYWxzZSwiVVNFX1dBU0lfQ0ROIjpmYWxzZX0sInN1YnN0aXR1dGlvbnMiOnsiUEFDS0FHRV9UT19KU19NT0RVTEVfUEFUSCI6Im1haW4ud2FzbSIsIlBBQ0tBR0VfVE9fSlNfUEFDS0FHRV9OQU1FIjoidGVzdCJ9fQ==", + "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" : "eyJjb25kaXRpb25zIjp7IklTX1dBU0kiOnRydWUsIlVTRV9TSEFSRURfTUVNT1JZIjpmYWxzZSwiVVNFX1dBU0lfQ0ROIjpmYWxzZX0sInN1YnN0aXR1dGlvbnMiOnsiUEFDS0FHRV9UT19KU19NT0RVTEVfUEFUSCI6Im1haW4ud2FzbSIsIlBBQ0tBR0VfVE9fSlNfUEFDS0FHRV9OQU1FIjoidGVzdCJ9fQ==", + "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" : "eyJjb25kaXRpb25zIjp7IklTX1dBU0kiOnRydWUsIlVTRV9TSEFSRURfTUVNT1JZIjpmYWxzZSwiVVNFX1dBU0lfQ0ROIjpmYWxzZX0sInN1YnN0aXR1dGlvbnMiOnsiUEFDS0FHRV9UT19KU19NT0RVTEVfUEFUSCI6Im1haW4ud2FzbSIsIlBBQ0tBR0VfVE9fSlNfUEFDS0FHRV9OQU1FIjoidGVzdCJ9fQ==", + "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" : "eyJjb25kaXRpb25zIjp7IklTX1dBU0kiOnRydWUsIlVTRV9TSEFSRURfTUVNT1JZIjpmYWxzZSwiVVNFX1dBU0lfQ0ROIjpmYWxzZX0sInN1YnN0aXR1dGlvbnMiOnsiUEFDS0FHRV9UT19KU19NT0RVTEVfUEFUSCI6Im1haW4ud2FzbSIsIlBBQ0tBR0VfVE9fSlNfUEFDS0FHRV9OQU1FIjoidGVzdCJ9fQ==", + "wants" : [ + "$OUTPUT", + "$OUTPUT\/platforms", + "$INTERMEDIATES\/wasm-imports.json" + ] + }, + { + "attributes" : [ + + ], + "inputs" : [ + "$PLANNER_SOURCE_PATH", + "$INTERMEDIATES\/main.wasm.debug" + ], + "output" : "$OUTPUT\/main.wasm", + "wants" : [ + "$OUTPUT", + "$INTERMEDIATES\/main.wasm.debug" + ] + }, + { + "attributes" : [ + + ], + "inputs" : [ + "$PLANNER_SOURCE_PATH", + "$SELF_PACKAGE\/Plugins\/PackageToJS\/Templates\/package.json" + ], + "output" : "$OUTPUT\/package.json", + "salt" : "eyJjb25kaXRpb25zIjp7IklTX1dBU0kiOnRydWUsIlVTRV9TSEFSRURfTUVNT1JZIjpmYWxzZSwiVVNFX1dBU0lfQ0ROIjpmYWxzZX0sInN1YnN0aXR1dGlvbnMiOnsiUEFDS0FHRV9UT19KU19NT0RVTEVfUEFUSCI6Im1haW4ud2FzbSIsIlBBQ0tBR0VfVE9fSlNfUEFDS0FHRV9OQU1FIjoidGVzdCJ9fQ==", + "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" : "eyJjb25kaXRpb25zIjp7IklTX1dBU0kiOnRydWUsIlVTRV9TSEFSRURfTUVNT1JZIjpmYWxzZSwiVVNFX1dBU0lfQ0ROIjpmYWxzZX0sInN1YnN0aXR1dGlvbnMiOnsiUEFDS0FHRV9UT19KU19NT0RVTEVfUEFUSCI6Im1haW4ud2FzbSIsIlBBQ0tBR0VfVE9fSlNfUEFDS0FHRV9OQU1FIjoidGVzdCJ9fQ==", + "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" : "eyJjb25kaXRpb25zIjp7IklTX1dBU0kiOnRydWUsIlVTRV9TSEFSRURfTUVNT1JZIjpmYWxzZSwiVVNFX1dBU0lfQ0ROIjpmYWxzZX0sInN1YnN0aXR1dGlvbnMiOnsiUEFDS0FHRV9UT19KU19NT0RVTEVfUEFUSCI6Im1haW4ud2FzbSIsIlBBQ0tBR0VfVE9fSlNfUEFDS0FHRV9OQU1FIjoidGVzdCJ9fQ==", + "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" : "eyJjb25kaXRpb25zIjp7IklTX1dBU0kiOnRydWUsIlVTRV9TSEFSRURfTUVNT1JZIjpmYWxzZSwiVVNFX1dBU0lfQ0ROIjpmYWxzZX0sInN1YnN0aXR1dGlvbnMiOnsiUEFDS0FHRV9UT19KU19NT0RVTEVfUEFUSCI6Im1haW4ud2FzbSIsIlBBQ0tBR0VfVE9fSlNfUEFDS0FHRV9OQU1FIjoidGVzdCJ9fQ==", + "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" : "eyJjb25kaXRpb25zIjp7IklTX1dBU0kiOnRydWUsIlVTRV9TSEFSRURfTUVNT1JZIjpmYWxzZSwiVVNFX1dBU0lfQ0ROIjpmYWxzZX0sInN1YnN0aXR1dGlvbnMiOnsiUEFDS0FHRV9UT19KU19NT0RVTEVfUEFUSCI6Im1haW4ud2FzbSIsIlBBQ0tBR0VfVE9fSlNfUEFDS0FHRV9OQU1FIjoidGVzdCJ9fQ==", + "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" : "eyJjb25kaXRpb25zIjp7IklTX1dBU0kiOnRydWUsIlVTRV9TSEFSRURfTUVNT1JZIjpmYWxzZSwiVVNFX1dBU0lfQ0ROIjpmYWxzZX0sInN1YnN0aXR1dGlvbnMiOnsiUEFDS0FHRV9UT19KU19NT0RVTEVfUEFUSCI6Im1haW4ud2FzbSIsIlBBQ0tBR0VfVE9fSlNfUEFDS0FHRV9OQU1FIjoidGVzdCJ9fQ==", + "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" : "eyJjb25kaXRpb25zIjp7IklTX1dBU0kiOnRydWUsIlVTRV9TSEFSRURfTUVNT1JZIjpmYWxzZSwiVVNFX1dBU0lfQ0ROIjpmYWxzZX0sInN1YnN0aXR1dGlvbnMiOnsiUEFDS0FHRV9UT19KU19NT0RVTEVfUEFUSCI6Im1haW4ud2FzbSIsIlBBQ0tBR0VfVE9fSlNfUEFDS0FHRV9OQU1FIjoidGVzdCJ9fQ==", + "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" + ] + } +] \ 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..0b1b2ac80 --- /dev/null +++ b/Plugins/PackageToJS/Tests/__Snapshots__/PackagingPlannerTests/planBuild_release_no_optimize.json @@ -0,0 +1,275 @@ +[ + { + "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" : "eyJjb25kaXRpb25zIjp7IklTX1dBU0kiOnRydWUsIlVTRV9TSEFSRURfTUVNT1JZIjpmYWxzZSwiVVNFX1dBU0lfQ0ROIjpmYWxzZX0sInN1YnN0aXR1dGlvbnMiOnsiUEFDS0FHRV9UT19KU19NT0RVTEVfUEFUSCI6Im1haW4ud2FzbSIsIlBBQ0tBR0VfVE9fSlNfUEFDS0FHRV9OQU1FIjoidGVzdCJ9fQ==", + "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" : "eyJjb25kaXRpb25zIjp7IklTX1dBU0kiOnRydWUsIlVTRV9TSEFSRURfTUVNT1JZIjpmYWxzZSwiVVNFX1dBU0lfQ0ROIjpmYWxzZX0sInN1YnN0aXR1dGlvbnMiOnsiUEFDS0FHRV9UT19KU19NT0RVTEVfUEFUSCI6Im1haW4ud2FzbSIsIlBBQ0tBR0VfVE9fSlNfUEFDS0FHRV9OQU1FIjoidGVzdCJ9fQ==", + "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" : "eyJjb25kaXRpb25zIjp7IklTX1dBU0kiOnRydWUsIlVTRV9TSEFSRURfTUVNT1JZIjpmYWxzZSwiVVNFX1dBU0lfQ0ROIjpmYWxzZX0sInN1YnN0aXR1dGlvbnMiOnsiUEFDS0FHRV9UT19KU19NT0RVTEVfUEFUSCI6Im1haW4ud2FzbSIsIlBBQ0tBR0VfVE9fSlNfUEFDS0FHRV9OQU1FIjoidGVzdCJ9fQ==", + "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" : "eyJjb25kaXRpb25zIjp7IklTX1dBU0kiOnRydWUsIlVTRV9TSEFSRURfTUVNT1JZIjpmYWxzZSwiVVNFX1dBU0lfQ0ROIjpmYWxzZX0sInN1YnN0aXR1dGlvbnMiOnsiUEFDS0FHRV9UT19KU19NT0RVTEVfUEFUSCI6Im1haW4ud2FzbSIsIlBBQ0tBR0VfVE9fSlNfUEFDS0FHRV9OQU1FIjoidGVzdCJ9fQ==", + "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" : "eyJjb25kaXRpb25zIjp7IklTX1dBU0kiOnRydWUsIlVTRV9TSEFSRURfTUVNT1JZIjpmYWxzZSwiVVNFX1dBU0lfQ0ROIjpmYWxzZX0sInN1YnN0aXR1dGlvbnMiOnsiUEFDS0FHRV9UT19KU19NT0RVTEVfUEFUSCI6Im1haW4ud2FzbSIsIlBBQ0tBR0VfVE9fSlNfUEFDS0FHRV9OQU1FIjoidGVzdCJ9fQ==", + "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" : "eyJjb25kaXRpb25zIjp7IklTX1dBU0kiOnRydWUsIlVTRV9TSEFSRURfTUVNT1JZIjpmYWxzZSwiVVNFX1dBU0lfQ0ROIjpmYWxzZX0sInN1YnN0aXR1dGlvbnMiOnsiUEFDS0FHRV9UT19KU19NT0RVTEVfUEFUSCI6Im1haW4ud2FzbSIsIlBBQ0tBR0VfVE9fSlNfUEFDS0FHRV9OQU1FIjoidGVzdCJ9fQ==", + "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" : "eyJjb25kaXRpb25zIjp7IklTX1dBU0kiOnRydWUsIlVTRV9TSEFSRURfTUVNT1JZIjpmYWxzZSwiVVNFX1dBU0lfQ0ROIjpmYWxzZX0sInN1YnN0aXR1dGlvbnMiOnsiUEFDS0FHRV9UT19KU19NT0RVTEVfUEFUSCI6Im1haW4ud2FzbSIsIlBBQ0tBR0VfVE9fSlNfUEFDS0FHRV9OQU1FIjoidGVzdCJ9fQ==", + "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" : "eyJjb25kaXRpb25zIjp7IklTX1dBU0kiOnRydWUsIlVTRV9TSEFSRURfTUVNT1JZIjpmYWxzZSwiVVNFX1dBU0lfQ0ROIjpmYWxzZX0sInN1YnN0aXR1dGlvbnMiOnsiUEFDS0FHRV9UT19KU19NT0RVTEVfUEFUSCI6Im1haW4ud2FzbSIsIlBBQ0tBR0VfVE9fSlNfUEFDS0FHRV9OQU1FIjoidGVzdCJ9fQ==", + "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" : "eyJjb25kaXRpb25zIjp7IklTX1dBU0kiOnRydWUsIlVTRV9TSEFSRURfTUVNT1JZIjpmYWxzZSwiVVNFX1dBU0lfQ0ROIjpmYWxzZX0sInN1YnN0aXR1dGlvbnMiOnsiUEFDS0FHRV9UT19KU19NT0RVTEVfUEFUSCI6Im1haW4ud2FzbSIsIlBBQ0tBR0VfVE9fSlNfUEFDS0FHRV9OQU1FIjoidGVzdCJ9fQ==", + "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" : "eyJjb25kaXRpb25zIjp7IklTX1dBU0kiOnRydWUsIlVTRV9TSEFSRURfTUVNT1JZIjpmYWxzZSwiVVNFX1dBU0lfQ0ROIjpmYWxzZX0sInN1YnN0aXR1dGlvbnMiOnsiUEFDS0FHRV9UT19KU19NT0RVTEVfUEFUSCI6Im1haW4ud2FzbSIsIlBBQ0tBR0VfVE9fSlNfUEFDS0FHRV9OQU1FIjoidGVzdCJ9fQ==", + "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" : "eyJjb25kaXRpb25zIjp7IklTX1dBU0kiOnRydWUsIlVTRV9TSEFSRURfTUVNT1JZIjpmYWxzZSwiVVNFX1dBU0lfQ0ROIjpmYWxzZX0sInN1YnN0aXR1dGlvbnMiOnsiUEFDS0FHRV9UT19KU19NT0RVTEVfUEFUSCI6Im1haW4ud2FzbSIsIlBBQ0tBR0VfVE9fSlNfUEFDS0FHRV9OQU1FIjoidGVzdCJ9fQ==", + "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" + ] + } +] \ No newline at end of file diff --git a/Plugins/PackageToJS/Tests/__Snapshots__/PackagingPlannerTests/planBuild_release_split_debug.json b/Plugins/PackageToJS/Tests/__Snapshots__/PackagingPlannerTests/planBuild_release_split_debug.json new file mode 100644 index 000000000..b18680f8d --- /dev/null +++ b/Plugins/PackageToJS/Tests/__Snapshots__/PackagingPlannerTests/planBuild_release_split_debug.json @@ -0,0 +1,290 @@ +[ + { + "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" : "eyJjb25kaXRpb25zIjp7IklTX1dBU0kiOnRydWUsIlVTRV9TSEFSRURfTUVNT1JZIjpmYWxzZSwiVVNFX1dBU0lfQ0ROIjpmYWxzZX0sInN1YnN0aXR1dGlvbnMiOnsiUEFDS0FHRV9UT19KU19NT0RVTEVfUEFUSCI6Im1haW4ud2FzbSIsIlBBQ0tBR0VfVE9fSlNfUEFDS0FHRV9OQU1FIjoidGVzdCJ9fQ==", + "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" : "eyJjb25kaXRpb25zIjp7IklTX1dBU0kiOnRydWUsIlVTRV9TSEFSRURfTUVNT1JZIjpmYWxzZSwiVVNFX1dBU0lfQ0ROIjpmYWxzZX0sInN1YnN0aXR1dGlvbnMiOnsiUEFDS0FHRV9UT19KU19NT0RVTEVfUEFUSCI6Im1haW4ud2FzbSIsIlBBQ0tBR0VfVE9fSlNfUEFDS0FHRV9OQU1FIjoidGVzdCJ9fQ==", + "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" : "eyJjb25kaXRpb25zIjp7IklTX1dBU0kiOnRydWUsIlVTRV9TSEFSRURfTUVNT1JZIjpmYWxzZSwiVVNFX1dBU0lfQ0ROIjpmYWxzZX0sInN1YnN0aXR1dGlvbnMiOnsiUEFDS0FHRV9UT19KU19NT0RVTEVfUEFUSCI6Im1haW4ud2FzbSIsIlBBQ0tBR0VfVE9fSlNfUEFDS0FHRV9OQU1FIjoidGVzdCJ9fQ==", + "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" : "eyJjb25kaXRpb25zIjp7IklTX1dBU0kiOnRydWUsIlVTRV9TSEFSRURfTUVNT1JZIjpmYWxzZSwiVVNFX1dBU0lfQ0ROIjpmYWxzZX0sInN1YnN0aXR1dGlvbnMiOnsiUEFDS0FHRV9UT19KU19NT0RVTEVfUEFUSCI6Im1haW4ud2FzbSIsIlBBQ0tBR0VfVE9fSlNfUEFDS0FHRV9OQU1FIjoidGVzdCJ9fQ==", + "wants" : [ + "$OUTPUT", + "$OUTPUT\/platforms", + "$INTERMEDIATES\/wasm-imports.json" + ] + }, + { + "attributes" : [ + + ], + "inputs" : [ + "$PLANNER_SOURCE_PATH", + "$OUTPUT\/main.wasm.debug" + ], + "output" : "$OUTPUT\/main.wasm", + "wants" : [ + "$OUTPUT", + "$OUTPUT\/main.wasm.debug" + ] + }, + { + "attributes" : [ + + ], + "inputs" : [ + "$PLANNER_SOURCE_PATH", + "$WASM_PRODUCT_ARTIFACT" + ], + "output" : "$OUTPUT\/main.wasm.debug", + "wants" : [ + "$OUTPUT", + "$INTERMEDIATES" + ] + }, + { + "attributes" : [ + + ], + "inputs" : [ + "$PLANNER_SOURCE_PATH", + "$SELF_PACKAGE\/Plugins\/PackageToJS\/Templates\/package.json" + ], + "output" : "$OUTPUT\/package.json", + "salt" : "eyJjb25kaXRpb25zIjp7IklTX1dBU0kiOnRydWUsIlVTRV9TSEFSRURfTUVNT1JZIjpmYWxzZSwiVVNFX1dBU0lfQ0ROIjpmYWxzZX0sInN1YnN0aXR1dGlvbnMiOnsiUEFDS0FHRV9UT19KU19NT0RVTEVfUEFUSCI6Im1haW4ud2FzbSIsIlBBQ0tBR0VfVE9fSlNfUEFDS0FHRV9OQU1FIjoidGVzdCJ9fQ==", + "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" : "eyJjb25kaXRpb25zIjp7IklTX1dBU0kiOnRydWUsIlVTRV9TSEFSRURfTUVNT1JZIjpmYWxzZSwiVVNFX1dBU0lfQ0ROIjpmYWxzZX0sInN1YnN0aXR1dGlvbnMiOnsiUEFDS0FHRV9UT19KU19NT0RVTEVfUEFUSCI6Im1haW4ud2FzbSIsIlBBQ0tBR0VfVE9fSlNfUEFDS0FHRV9OQU1FIjoidGVzdCJ9fQ==", + "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" : "eyJjb25kaXRpb25zIjp7IklTX1dBU0kiOnRydWUsIlVTRV9TSEFSRURfTUVNT1JZIjpmYWxzZSwiVVNFX1dBU0lfQ0ROIjpmYWxzZX0sInN1YnN0aXR1dGlvbnMiOnsiUEFDS0FHRV9UT19KU19NT0RVTEVfUEFUSCI6Im1haW4ud2FzbSIsIlBBQ0tBR0VfVE9fSlNfUEFDS0FHRV9OQU1FIjoidGVzdCJ9fQ==", + "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" : "eyJjb25kaXRpb25zIjp7IklTX1dBU0kiOnRydWUsIlVTRV9TSEFSRURfTUVNT1JZIjpmYWxzZSwiVVNFX1dBU0lfQ0ROIjpmYWxzZX0sInN1YnN0aXR1dGlvbnMiOnsiUEFDS0FHRV9UT19KU19NT0RVTEVfUEFUSCI6Im1haW4ud2FzbSIsIlBBQ0tBR0VfVE9fSlNfUEFDS0FHRV9OQU1FIjoidGVzdCJ9fQ==", + "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" : "eyJjb25kaXRpb25zIjp7IklTX1dBU0kiOnRydWUsIlVTRV9TSEFSRURfTUVNT1JZIjpmYWxzZSwiVVNFX1dBU0lfQ0ROIjpmYWxzZX0sInN1YnN0aXR1dGlvbnMiOnsiUEFDS0FHRV9UT19KU19NT0RVTEVfUEFUSCI6Im1haW4ud2FzbSIsIlBBQ0tBR0VfVE9fSlNfUEFDS0FHRV9OQU1FIjoidGVzdCJ9fQ==", + "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" : "eyJjb25kaXRpb25zIjp7IklTX1dBU0kiOnRydWUsIlVTRV9TSEFSRURfTUVNT1JZIjpmYWxzZSwiVVNFX1dBU0lfQ0ROIjpmYWxzZX0sInN1YnN0aXR1dGlvbnMiOnsiUEFDS0FHRV9UT19KU19NT0RVTEVfUEFUSCI6Im1haW4ud2FzbSIsIlBBQ0tBR0VfVE9fSlNfUEFDS0FHRV9OQU1FIjoidGVzdCJ9fQ==", + "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" : "eyJjb25kaXRpb25zIjp7IklTX1dBU0kiOnRydWUsIlVTRV9TSEFSRURfTUVNT1JZIjpmYWxzZSwiVVNFX1dBU0lfQ0ROIjpmYWxzZX0sInN1YnN0aXR1dGlvbnMiOnsiUEFDS0FHRV9UT19KU19NT0RVTEVfUEFUSCI6Im1haW4ud2FzbSIsIlBBQ0tBR0VfVE9fSlNfUEFDS0FHRV9OQU1FIjoidGVzdCJ9fQ==", + "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" + ] + } +] \ 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..59e5bb4ad --- /dev/null +++ b/Plugins/PackageToJS/Tests/__Snapshots__/PackagingPlannerTests/planTestBuild.json @@ -0,0 +1,367 @@ +[ + { + "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" : "eyJjb25kaXRpb25zIjp7IklTX1dBU0kiOnRydWUsIlVTRV9TSEFSRURfTUVNT1JZIjpmYWxzZSwiVVNFX1dBU0lfQ0ROIjpmYWxzZX0sInN1YnN0aXR1dGlvbnMiOnsiUEFDS0FHRV9UT19KU19NT0RVTEVfUEFUSCI6Im1haW4ud2FzbSIsIlBBQ0tBR0VfVE9fSlNfUEFDS0FHRV9OQU1FIjoidGVzdCJ9fQ==", + "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" : "eyJjb25kaXRpb25zIjp7IklTX1dBU0kiOnRydWUsIlVTRV9TSEFSRURfTUVNT1JZIjpmYWxzZSwiVVNFX1dBU0lfQ0ROIjpmYWxzZX0sInN1YnN0aXR1dGlvbnMiOnsiUEFDS0FHRV9UT19KU19NT0RVTEVfUEFUSCI6Im1haW4ud2FzbSIsIlBBQ0tBR0VfVE9fSlNfUEFDS0FHRV9OQU1FIjoidGVzdCJ9fQ==", + "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" : "eyJjb25kaXRpb25zIjp7IklTX1dBU0kiOnRydWUsIlVTRV9TSEFSRURfTUVNT1JZIjpmYWxzZSwiVVNFX1dBU0lfQ0ROIjpmYWxzZX0sInN1YnN0aXR1dGlvbnMiOnsiUEFDS0FHRV9UT19KU19NT0RVTEVfUEFUSCI6Im1haW4ud2FzbSIsIlBBQ0tBR0VfVE9fSlNfUEFDS0FHRV9OQU1FIjoidGVzdCJ9fQ==", + "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" : "eyJjb25kaXRpb25zIjp7IklTX1dBU0kiOnRydWUsIlVTRV9TSEFSRURfTUVNT1JZIjpmYWxzZSwiVVNFX1dBU0lfQ0ROIjpmYWxzZX0sInN1YnN0aXR1dGlvbnMiOnsiUEFDS0FHRV9UT19KU19NT0RVTEVfUEFUSCI6Im1haW4ud2FzbSIsIlBBQ0tBR0VfVE9fSlNfUEFDS0FHRV9OQU1FIjoidGVzdCJ9fQ==", + "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" : "eyJjb25kaXRpb25zIjp7IklTX1dBU0kiOnRydWUsIlVTRV9TSEFSRURfTUVNT1JZIjpmYWxzZSwiVVNFX1dBU0lfQ0ROIjpmYWxzZX0sInN1YnN0aXR1dGlvbnMiOnsiUEFDS0FHRV9UT19KU19NT0RVTEVfUEFUSCI6Im1haW4ud2FzbSIsIlBBQ0tBR0VfVE9fSlNfUEFDS0FHRV9OQU1FIjoidGVzdCJ9fQ==", + "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" : "eyJjb25kaXRpb25zIjp7IklTX1dBU0kiOnRydWUsIlVTRV9TSEFSRURfTUVNT1JZIjpmYWxzZSwiVVNFX1dBU0lfQ0ROIjpmYWxzZX0sInN1YnN0aXR1dGlvbnMiOnsiUEFDS0FHRV9UT19KU19NT0RVTEVfUEFUSCI6Im1haW4ud2FzbSIsIlBBQ0tBR0VfVE9fSlNfUEFDS0FHRV9OQU1FIjoidGVzdCJ9fQ==", + "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" : "eyJjb25kaXRpb25zIjp7IklTX1dBU0kiOnRydWUsIlVTRV9TSEFSRURfTUVNT1JZIjpmYWxzZSwiVVNFX1dBU0lfQ0ROIjpmYWxzZX0sInN1YnN0aXR1dGlvbnMiOnsiUEFDS0FHRV9UT19KU19NT0RVTEVfUEFUSCI6Im1haW4ud2FzbSIsIlBBQ0tBR0VfVE9fSlNfUEFDS0FHRV9OQU1FIjoidGVzdCJ9fQ==", + "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" : "eyJjb25kaXRpb25zIjp7IklTX1dBU0kiOnRydWUsIlVTRV9TSEFSRURfTUVNT1JZIjpmYWxzZSwiVVNFX1dBU0lfQ0ROIjpmYWxzZX0sInN1YnN0aXR1dGlvbnMiOnsiUEFDS0FHRV9UT19KU19NT0RVTEVfUEFUSCI6Im1haW4ud2FzbSIsIlBBQ0tBR0VfVE9fSlNfUEFDS0FHRV9OQU1FIjoidGVzdCJ9fQ==", + "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" : "eyJjb25kaXRpb25zIjp7IklTX1dBU0kiOnRydWUsIlVTRV9TSEFSRURfTUVNT1JZIjpmYWxzZSwiVVNFX1dBU0lfQ0ROIjpmYWxzZX0sInN1YnN0aXR1dGlvbnMiOnsiUEFDS0FHRV9UT19KU19NT0RVTEVfUEFUSCI6Im1haW4ud2FzbSIsIlBBQ0tBR0VfVE9fSlNfUEFDS0FHRV9OQU1FIjoidGVzdCJ9fQ==", + "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" : "eyJjb25kaXRpb25zIjp7IklTX1dBU0kiOnRydWUsIlVTRV9TSEFSRURfTUVNT1JZIjpmYWxzZSwiVVNFX1dBU0lfQ0ROIjpmYWxzZX0sInN1YnN0aXR1dGlvbnMiOnsiUEFDS0FHRV9UT19KU19NT0RVTEVfUEFUSCI6Im1haW4ud2FzbSIsIlBBQ0tBR0VfVE9fSlNfUEFDS0FHRV9OQU1FIjoidGVzdCJ9fQ==", + "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" : "eyJjb25kaXRpb25zIjp7IklTX1dBU0kiOnRydWUsIlVTRV9TSEFSRURfTUVNT1JZIjpmYWxzZSwiVVNFX1dBU0lfQ0ROIjpmYWxzZX0sInN1YnN0aXR1dGlvbnMiOnsiUEFDS0FHRV9UT19KU19NT0RVTEVfUEFUSCI6Im1haW4ud2FzbSIsIlBBQ0tBR0VfVE9fSlNfUEFDS0FHRV9OQU1FIjoidGVzdCJ9fQ==", + "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" : "eyJjb25kaXRpb25zIjp7IklTX1dBU0kiOnRydWUsIlVTRV9TSEFSRURfTUVNT1JZIjpmYWxzZSwiVVNFX1dBU0lfQ0ROIjpmYWxzZX0sInN1YnN0aXR1dGlvbnMiOnsiUEFDS0FHRV9UT19KU19NT0RVTEVfUEFUSCI6Im1haW4ud2FzbSIsIlBBQ0tBR0VfVE9fSlNfUEFDS0FHRV9OQU1FIjoidGVzdCJ9fQ==", + "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" : "eyJjb25kaXRpb25zIjp7IklTX1dBU0kiOnRydWUsIlVTRV9TSEFSRURfTUVNT1JZIjpmYWxzZSwiVVNFX1dBU0lfQ0ROIjpmYWxzZX0sInN1YnN0aXR1dGlvbnMiOnsiUEFDS0FHRV9UT19KU19NT0RVTEVfUEFUSCI6Im1haW4ud2FzbSIsIlBBQ0tBR0VfVE9fSlNfUEFDS0FHRV9OQU1FIjoidGVzdCJ9fQ==", + "wants" : [ + "$OUTPUT", + "$OUTPUT\/bin" + ] + }, + { + "attributes" : [ + + ], + "inputs" : [ + "$PLANNER_SOURCE_PATH", + "$SELF_PACKAGE\/Plugins\/PackageToJS\/Templates\/test.d.ts" + ], + "output" : "$OUTPUT\/test.d.ts", + "salt" : "eyJjb25kaXRpb25zIjp7IklTX1dBU0kiOnRydWUsIlVTRV9TSEFSRURfTUVNT1JZIjpmYWxzZSwiVVNFX1dBU0lfQ0ROIjpmYWxzZX0sInN1YnN0aXR1dGlvbnMiOnsiUEFDS0FHRV9UT19KU19NT0RVTEVfUEFUSCI6Im1haW4ud2FzbSIsIlBBQ0tBR0VfVE9fSlNfUEFDS0FHRV9OQU1FIjoidGVzdCJ9fQ==", + "wants" : [ + "$OUTPUT", + "$OUTPUT\/bin" + ] + }, + { + "attributes" : [ + + ], + "inputs" : [ + "$PLANNER_SOURCE_PATH", + "$SELF_PACKAGE\/Plugins\/PackageToJS\/Templates\/test.js" + ], + "output" : "$OUTPUT\/test.js", + "salt" : "eyJjb25kaXRpb25zIjp7IklTX1dBU0kiOnRydWUsIlVTRV9TSEFSRURfTUVNT1JZIjpmYWxzZSwiVVNFX1dBU0lfQ0ROIjpmYWxzZX0sInN1YnN0aXR1dGlvbnMiOnsiUEFDS0FHRV9UT19KU19NT0RVTEVfUEFUSCI6Im1haW4ud2FzbSIsIlBBQ0tBR0VfVE9fSlNfUEFDS0FHRV9OQU1FIjoidGVzdCJ9fQ==", + "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", + "$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 From e5c37a9be3863db84bfcf366788fb385a98cfeb8 Mon Sep 17 00:00:00 2001 From: Yuta Saito Date: Sat, 15 Mar 2025 06:45:52 +0000 Subject: [PATCH 274/373] Add test suite to ensure examples build correctly --- Plugins/PackageToJS/Sources/PackageToJS.swift | 2 +- .../Sources/PackageToJSPlugin.swift | 3 +- .../Tests/ExampleProjectTests.swift | 6 - Plugins/PackageToJS/Tests/ExampleTests.swift | 184 ++++++++++++++++++ Plugins/PackageToJS/Tests/MiniMakeTests.swift | 14 +- .../Tests/TemporaryDirectory.swift | 11 +- 6 files changed, 201 insertions(+), 19 deletions(-) delete mode 100644 Plugins/PackageToJS/Tests/ExampleProjectTests.swift create mode 100644 Plugins/PackageToJS/Tests/ExampleTests.swift diff --git a/Plugins/PackageToJS/Sources/PackageToJS.swift b/Plugins/PackageToJS/Sources/PackageToJS.swift index bd70660f3..5727f6385 100644 --- a/Plugins/PackageToJS/Sources/PackageToJS.swift +++ b/Plugins/PackageToJS/Sources/PackageToJS.swift @@ -138,7 +138,7 @@ final class DefaultPackagingSystem: PackagingSystem { } } -private func which(_ executable: String) throws -> URL { +internal func which(_ executable: String) throws -> URL { let pathSeparator: Character #if os(Windows) pathSeparator = ";" diff --git a/Plugins/PackageToJS/Sources/PackageToJSPlugin.swift b/Plugins/PackageToJS/Sources/PackageToJSPlugin.swift index 727356443..c22bc2949 100644 --- a/Plugins/PackageToJS/Sources/PackageToJSPlugin.swift +++ b/Plugins/PackageToJS/Sources/PackageToJSPlugin.swift @@ -160,7 +160,8 @@ struct PackageToJSPlugin: CommandPlugin { // not worth the overhead) var productArtifact: URL? for fileExtension in ["wasm", "xctest"] { - let path = ".build/debug/\(productName).\(fileExtension)" + 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 diff --git a/Plugins/PackageToJS/Tests/ExampleProjectTests.swift b/Plugins/PackageToJS/Tests/ExampleProjectTests.swift deleted file mode 100644 index 1bcc25d48..000000000 --- a/Plugins/PackageToJS/Tests/ExampleProjectTests.swift +++ /dev/null @@ -1,6 +0,0 @@ -import Testing - -@Suite struct ExampleProjectTests { - @Test func example() throws { - } -} diff --git a/Plugins/PackageToJS/Tests/ExampleTests.swift b/Plugins/PackageToJS/Tests/ExampleTests.swift new file mode 100644 index 000000000..f1be33b5b --- /dev/null +++ b/Plugins/PackageToJS/Tests/ExampleTests.swift @@ -0,0 +1,184 @@ +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 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 + } + // 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: ProcessInfo.processInfo.environment["SWIFT_PATH"]!)) + 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"], [:]) + } + } + + @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 runSwift(["package", "--swift-sdk", swiftSDKID, "js", "test", "--environment", "browser"], [:]) + } + } + + @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", "-c", "release", "js"], + [ + "JAVASCRIPTKIT_EXPERIMENTAL_EMBEDDED_WASM": "true" + ] + ) + } + } +} diff --git a/Plugins/PackageToJS/Tests/MiniMakeTests.swift b/Plugins/PackageToJS/Tests/MiniMakeTests.swift index f76af298e..0870cde45 100644 --- a/Plugins/PackageToJS/Tests/MiniMakeTests.swift +++ b/Plugins/PackageToJS/Tests/MiniMakeTests.swift @@ -6,7 +6,7 @@ import Testing @Suite struct MiniMakeTests { // Test basic task management functionality @Test func basicTaskManagement() throws { - try withTemporaryDirectory { tempDir in + try withTemporaryDirectory { tempDir, _ in var make = MiniMake(printProgress: { _, _ in }) let outDir = BuildPath(prefix: "OUTPUT") @@ -25,7 +25,7 @@ import Testing // Test that task dependencies are handled correctly @Test func taskDependencies() throws { - try withTemporaryDirectory { tempDir in + try withTemporaryDirectory { tempDir, _ in var make = MiniMake(printProgress: { _, _ in }) let prefix = BuildPath(prefix: "PREFIX") let scope = MiniMake.VariableScope(variables: [ @@ -59,7 +59,7 @@ import Testing // Test that phony tasks are always rebuilt @Test func phonyTask() throws { - try withTemporaryDirectory { tempDir in + try withTemporaryDirectory { tempDir, _ in var make = MiniMake(printProgress: { _, _ in }) let phonyName = "phony.txt" let outputPath = BuildPath(prefix: "OUTPUT").appending(path: phonyName) @@ -99,7 +99,7 @@ import Testing // Test that rebuilds are controlled by timestamps @Test func timestampBasedRebuild() throws { - try withTemporaryDirectory { tempDir in + try withTemporaryDirectory { tempDir, _ in var make = MiniMake(printProgress: { _, _ in }) let prefix = BuildPath(prefix: "PREFIX") let scope = MiniMake.VariableScope(variables: [ @@ -134,7 +134,7 @@ import Testing // Test that silent tasks execute without output @Test func silentTask() throws { - try withTemporaryDirectory { tempDir in + try withTemporaryDirectory { tempDir, _ in var messages: [(String, Int, Int, String)] = [] var make = MiniMake( printProgress: { ctx, message in @@ -167,7 +167,7 @@ import Testing // Test that error cases are handled appropriately @Test func errorWhileBuilding() throws { struct BuildError: Error {} - try withTemporaryDirectory { tempDir in + try withTemporaryDirectory { tempDir, _ in var make = MiniMake(printProgress: { _, _ in }) let prefix = BuildPath(prefix: "PREFIX") let scope = MiniMake.VariableScope(variables: [ @@ -187,7 +187,7 @@ import Testing // Test that cleanup functionality works correctly @Test func cleanup() throws { - try withTemporaryDirectory { tempDir in + try withTemporaryDirectory { tempDir, _ in var make = MiniMake(printProgress: { _, _ in }) let prefix = BuildPath(prefix: "PREFIX") let scope = MiniMake.VariableScope(variables: [ diff --git a/Plugins/PackageToJS/Tests/TemporaryDirectory.swift b/Plugins/PackageToJS/Tests/TemporaryDirectory.swift index 4aa543bbf..199380fac 100644 --- a/Plugins/PackageToJS/Tests/TemporaryDirectory.swift +++ b/Plugins/PackageToJS/Tests/TemporaryDirectory.swift @@ -4,7 +4,7 @@ struct MakeTemporaryDirectoryError: Error { let error: CInt } -internal func withTemporaryDirectory(body: (URL) throws -> T) throws -> T { +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 @@ -16,9 +16,12 @@ internal func withTemporaryDirectory(body: (URL) throws -> T) throws -> T { throw MakeTemporaryDirectoryError(error: errno) } let tempDir = URL(fileURLWithPath: String(cString: result)) + var retain = false defer { - try? FileManager.default.removeItem(at: tempDir) + if !retain { + try? FileManager.default.removeItem(at: tempDir) + } } - return try body(tempDir) + return try body(tempDir, &retain) } -} \ No newline at end of file +} From efd097c821366e03dec62abb6621aa8ca1e9bc0b Mon Sep 17 00:00:00 2001 From: Yuta Saito Date: Sat, 15 Mar 2025 07:21:10 +0000 Subject: [PATCH 275/373] CI: Check all examples build --- .github/workflows/compatibility.yml | 20 ----------------- .github/workflows/test.yml | 23 +++++--------------- Plugins/PackageToJS/Tests/ExampleTests.swift | 1 + 3 files changed, 6 insertions(+), 38 deletions(-) delete mode 100644 .github/workflows/compatibility.yml diff --git a/.github/workflows/compatibility.yml b/.github/workflows/compatibility.yml deleted file mode 100644 index 8994b624b..000000000 --- a/.github/workflows/compatibility.yml +++ /dev/null @@ -1,20 +0,0 @@ -name: Check compatibility -on: - pull_request: - push: - branches: [main] -jobs: - test: - name: Check source code compatibility - runs-on: ubuntu-latest - container: swift:6.0.3 - steps: - - name: Checkout - uses: actions/checkout@v4 - - uses: swiftwasm/setup-swiftwasm@v2 - - name: Run Test - run: | - set -eux - cd Examples/Basic - swift build --swift-sdk wasm32-unknown-wasi --static-swift-stdlib - swift build --swift-sdk wasm32-unknown-wasi -Xswiftc -DJAVASCRIPTKIT_WITHOUT_WEAKREFS --static-swift-stdlib diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index c50de248a..486f7b6bf 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -38,8 +38,10 @@ jobs: id: setup-swiftwasm with: target: ${{ matrix.entry.target }} - - name: Configure Swift SDK - run: echo "SWIFT_SDK_ID=${{ steps.setup-swiftwasm.outputs.swift-sdk-id }}" >> $GITHUB_ENV + - name: Configure environment variables + run: | + 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 @@ -49,6 +51,7 @@ jobs: run: | make regenerate_swiftpm_resources git diff --exit-code Sources/JavaScriptKit/Runtime + - run: swift test --package-path ./Plugins/PackageToJS native-build: # Check native build to make it easy to develop applications by Xcode @@ -64,19 +67,3 @@ jobs: - run: swift build env: DEVELOPER_DIR: /Applications/${{ matrix.xcode }}.app/Contents/Developer/ - - embedded-build: - name: Build for embedded target - runs-on: ubuntu-22.04 - strategy: - matrix: - entry: - - 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 - steps: - - uses: actions/checkout@v4 - - uses: ./.github/actions/install-swift - with: - download-url: ${{ matrix.entry.toolchain.download-url }} - - run: ./Examples/Embedded/build.sh diff --git a/Plugins/PackageToJS/Tests/ExampleTests.swift b/Plugins/PackageToJS/Tests/ExampleTests.swift index f1be33b5b..be5d8e60b 100644 --- a/Plugins/PackageToJS/Tests/ExampleTests.swift +++ b/Plugins/PackageToJS/Tests/ExampleTests.swift @@ -135,6 +135,7 @@ extension Trait where Self == ConditionTrait { 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, "-Xswiftc", "-DJAVASCRIPTKIT_WITHOUT_WEAKREFS", "js"], [:]) } } From 1694e78b3559b94806e8f1bb4b7d6b6a64435258 Mon Sep 17 00:00:00 2001 From: Yuta Saito Date: Sat, 15 Mar 2025 07:30:00 +0000 Subject: [PATCH 276/373] CI: npx playwright install before running tests --- Makefile | 1 + Plugins/PackageToJS/Tests/MiniMakeTests.swift | 1 - package-lock.json | 49 +++++++++++++++++++ package.json | 1 + 4 files changed, 51 insertions(+), 1 deletion(-) diff --git a/Makefile b/Makefile index c8b79b4ab..a2ad1526a 100644 --- a/Makefile +++ b/Makefile @@ -6,6 +6,7 @@ SWIFT_BUILD_FLAGS := --swift-sdk $(SWIFT_SDK_ID) .PHONY: bootstrap bootstrap: npm ci + npx playwright install .PHONY: build build: diff --git a/Plugins/PackageToJS/Tests/MiniMakeTests.swift b/Plugins/PackageToJS/Tests/MiniMakeTests.swift index 0870cde45..b15a87607 100644 --- a/Plugins/PackageToJS/Tests/MiniMakeTests.swift +++ b/Plugins/PackageToJS/Tests/MiniMakeTests.swift @@ -11,7 +11,6 @@ import Testing let outDir = BuildPath(prefix: "OUTPUT") let task = make.addTask(output: outDir.appending(path: "output.txt")) { - print($0.output, $1.resolve(path: $0.output).path) try "Hello".write(toFile: $1.resolve(path: $0.output).path, atomically: true, encoding: .utf8) } diff --git a/package-lock.json b/package-lock.json index 18415649f..bb5718d1b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,6 +10,7 @@ "license": "MIT", "devDependencies": { "@rollup/plugin-typescript": "^8.3.1", + "playwright": "^1.51.0", "prettier": "2.6.1", "rollup": "^2.70.0", "tslib": "^2.3.1", @@ -125,6 +126,38 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, + "node_modules/playwright": { + "version": "1.51.0", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.51.0.tgz", + "integrity": "sha512-442pTfGM0xxfCYxuBa/Pu6B2OqxqqaYq39JS8QDMGThUvIOCd6s0ANDog3uwA0cHavVlnTQzGCN7Id2YekDSXA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "playwright-core": "1.51.0" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "fsevents": "2.3.2" + } + }, + "node_modules/playwright-core": { + "version": "1.51.0", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.51.0.tgz", + "integrity": "sha512-x47yPE3Zwhlil7wlNU/iktF7t2r/URR3VLbH6EknJd/04Qc/PSJ0EY3CMXipmglLG+zyRxW6HNo2EGbKLHPWMg==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "playwright-core": "cli.js" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/prettier": { "version": "2.6.1", "resolved": "https://registry.npmjs.org/prettier/-/prettier-2.6.1.tgz", @@ -281,6 +314,22 @@ "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", "dev": true }, + "playwright": { + "version": "1.51.0", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.51.0.tgz", + "integrity": "sha512-442pTfGM0xxfCYxuBa/Pu6B2OqxqqaYq39JS8QDMGThUvIOCd6s0ANDog3uwA0cHavVlnTQzGCN7Id2YekDSXA==", + "dev": true, + "requires": { + "fsevents": "2.3.2", + "playwright-core": "1.51.0" + } + }, + "playwright-core": { + "version": "1.51.0", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.51.0.tgz", + "integrity": "sha512-x47yPE3Zwhlil7wlNU/iktF7t2r/URR3VLbH6EknJd/04Qc/PSJ0EY3CMXipmglLG+zyRxW6HNo2EGbKLHPWMg==", + "dev": true + }, "prettier": { "version": "2.6.1", "resolved": "https://registry.npmjs.org/prettier/-/prettier-2.6.1.tgz", diff --git a/package.json b/package.json index e25d0a17b..0c67b2705 100644 --- a/package.json +++ b/package.json @@ -35,6 +35,7 @@ "license": "MIT", "devDependencies": { "@rollup/plugin-typescript": "^8.3.1", + "playwright": "^1.51.0", "prettier": "2.6.1", "rollup": "^2.70.0", "tslib": "^2.3.1", From 3aa9cb8ebb7690225d095e057394246d09f54c4e Mon Sep 17 00:00:00 2001 From: Yuta Saito Date: Sat, 15 Mar 2025 07:33:32 +0000 Subject: [PATCH 277/373] test: Relax the timing constraint for the JavaScriptEventLoopTests --- Tests/JavaScriptEventLoopTests/JavaScriptEventLoopTests.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Tests/JavaScriptEventLoopTests/JavaScriptEventLoopTests.swift b/Tests/JavaScriptEventLoopTests/JavaScriptEventLoopTests.swift index 029876904..5d610aa48 100644 --- a/Tests/JavaScriptEventLoopTests/JavaScriptEventLoopTests.swift +++ b/Tests/JavaScriptEventLoopTests/JavaScriptEventLoopTests.swift @@ -197,7 +197,7 @@ final class JavaScriptEventLoopTests: XCTestCase { let result = try await catchPromise2.value XCTAssertEqual(result.object?.message, .string("test")) } - XCTAssertGreaterThanOrEqual(catchDiff, 200) + XCTAssertGreaterThanOrEqual(catchDiff, 150) } // MARK: - Continuation Tests From ae2cc40f2a2c25bea0b309b08560e786d6ae29cc Mon Sep 17 00:00:00 2001 From: Yuta Saito Date: Sat, 15 Mar 2025 07:47:55 +0000 Subject: [PATCH 278/373] Fallback to simple copy if wasm-opt is not installed --- Plugins/PackageToJS/Sources/PackageToJS.swift | 20 +++++++++++++++++-- .../Sources/PackageToJSPlugin.swift | 4 +++- 2 files changed, 21 insertions(+), 3 deletions(-) diff --git a/Plugins/PackageToJS/Sources/PackageToJS.swift b/Plugins/PackageToJS/Sources/PackageToJS.swift index 5727f6385..e54a2a910 100644 --- a/Plugins/PackageToJS/Sources/PackageToJS.swift +++ b/Plugins/PackageToJS/Sources/PackageToJS.swift @@ -117,12 +117,28 @@ extension PackagingSystem { } 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 { - try runCommand(try which("wasm-opt"), arguments + ["-o", output, input]) + 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]) } private func runCommand(_ command: URL, _ arguments: [String]) throws { @@ -190,7 +206,7 @@ struct PackagingPlanner { configuration: String, triple: String, selfPath: BuildPath = BuildPath(absolute: #filePath), - system: any PackagingSystem = DefaultPackagingSystem() + system: any PackagingSystem ) { self.options = options self.packageId = packageId diff --git a/Plugins/PackageToJS/Sources/PackageToJSPlugin.swift b/Plugins/PackageToJS/Sources/PackageToJSPlugin.swift index c22bc2949..4074a8218 100644 --- a/Plugins/PackageToJS/Sources/PackageToJSPlugin.swift +++ b/Plugins/PackageToJS/Sources/PackageToJSPlugin.swift @@ -446,6 +446,7 @@ extension PackagingPlanner { ) { let outputBaseName = outputDir.lastPathComponent let (configuration, triple) = PackageToJS.deriveBuildConfiguration(wasmProductArtifact: wasmProductArtifact) + let system = DefaultPackagingSystem(printWarning: printStderr) self.init( options: options, packageId: context.package.id, @@ -454,7 +455,8 @@ extension PackagingPlanner { outputDir: BuildPath(absolute: outputDir.path), wasmProductArtifact: BuildPath(absolute: wasmProductArtifact.path), configuration: configuration, - triple: triple + triple: triple, + system: system ) } } From 96d73a2fde2f96b5afc426c3092f5aa94a714e21 Mon Sep 17 00:00:00 2001 From: Yuta Saito Date: Sat, 15 Mar 2025 08:11:18 +0000 Subject: [PATCH 279/373] [skip ci] Add Plugins/PackageToJS/README.md --- CONTRIBUTING.md | 8 +++++++ Plugins/PackageToJS/README.md | 43 +++++++++++++++++++++++++++++++++++ 2 files changed, 51 insertions(+) create mode 100644 Plugins/PackageToJS/README.md diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 38454374a..f71ca83ae 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -59,10 +59,18 @@ Thank you for considering contributing to JavaScriptKit! We welcome contribution ### 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. diff --git a/Plugins/PackageToJS/README.md b/Plugins/PackageToJS/README.md new file mode 100644 index 000000000..0681024b4 --- /dev/null +++ b/Plugins/PackageToJS/README.md @@ -0,0 +1,43 @@ +# 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 Swift packages for WebAssembly targets +- Generate JavaScript wrapper code for Swift WebAssembly modules +- Support for testing Swift WebAssembly code +- Diagnostic helpers for common build issues +- Options for optimization and debug information management + +## Requirements + +- Swift 6.0 or later +- A compatible WebAssembly SDK + +## 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 + From 6f2ba042c186019bd3f0792a49b33d20a7a10b52 Mon Sep 17 00:00:00 2001 From: Yuta Saito Date: Sun, 16 Mar 2025 04:58:19 +0000 Subject: [PATCH 280/373] Add `--enable-code-coverage` --- Examples/Testing/.gitignore | 8 -- Examples/Testing/Package.swift | 4 - Examples/Testing/README.md | 33 +++++ Makefile | 10 +- Plugins/PackageToJS/Sources/PackageToJS.swift | 116 +++++++++++++++--- .../Sources/PackageToJSPlugin.swift | 71 +++++------ Plugins/PackageToJS/Templates/bin/test.js | 19 +++ .../PackageToJS/Templates/instantiate.d.ts | 7 ++ .../PackageToJS/Templates/platforms/node.d.ts | 1 + .../PackageToJS/Templates/platforms/node.js | 44 ++++++- Plugins/PackageToJS/Tests/ExampleTests.swift | 30 ++++- 11 files changed, 271 insertions(+), 72 deletions(-) delete mode 100644 Examples/Testing/.gitignore create mode 100644 Examples/Testing/README.md diff --git a/Examples/Testing/.gitignore b/Examples/Testing/.gitignore deleted file mode 100644 index 0023a5340..000000000 --- a/Examples/Testing/.gitignore +++ /dev/null @@ -1,8 +0,0 @@ -.DS_Store -/.build -/Packages -xcuserdata/ -DerivedData/ -.swiftpm/configuration/registries.json -.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata -.netrc diff --git a/Examples/Testing/Package.swift b/Examples/Testing/Package.swift index 2e997652f..6dd492cd1 100644 --- a/Examples/Testing/Package.swift +++ b/Examples/Testing/Package.swift @@ -1,20 +1,16 @@ // swift-tools-version: 6.0 -// The swift-tools-version declares the minimum version of Swift required to build this package. import PackageDescription let package = Package( name: "Counter", products: [ - // Products define the executables and libraries a package produces, making them visible to other packages. .library( name: "Counter", targets: ["Counter"]), ], dependencies: [.package(name: "JavaScriptKit", path: "../../")], targets: [ - // Targets are the basic building blocks of a package, defining a module or a test suite. - // Targets can depend on other targets in this package and products from dependencies. .target( name: "Counter", dependencies: [ 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/Makefile b/Makefile index a2ad1526a..3764ed06a 100644 --- a/Makefile +++ b/Makefile @@ -18,11 +18,11 @@ unittest: @echo Running unit tests 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 + -Xlinker --stack-first \ + -Xlinker --global-base=524288 \ + -Xlinker -z \ + -Xlinker stack-size=524288 \ + js test --prelude ./Tests/prelude.mjs .PHONY: benchmark_setup benchmark_setup: diff --git a/Plugins/PackageToJS/Sources/PackageToJS.swift b/Plugins/PackageToJS/Sources/PackageToJS.swift index e54a2a910..1949527dc 100644 --- a/Plugins/PackageToJS/Sources/PackageToJS.swift +++ b/Plugins/PackageToJS/Sources/PackageToJS.swift @@ -6,10 +6,12 @@ struct PackageToJS { var outputPath: String? /// Name of the package (default: lowercased Package.swift name) var packageName: String? - /// Whether to explain the build plan + /// Whether to explain the build plan (default: false) var explain: Bool = false - /// Whether to use CDN for dependency packages + /// 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 } struct BuildOptions { @@ -51,7 +53,69 @@ struct PackageToJS { return (buildConfiguration, triple) } - static func runTest(testRunner: URL, currentDirectoryURL: URL, extraArguments: [String]) throws { + static func runTest(testRunner: URL, currentDirectoryURL: URL, outputDir: URL, testOptions: TestOptions) throws { + var testJsArguments: [String] = [] + var testLibraryArguments: [String] = [] + if testOptions.listTests { + testLibraryArguments += ["--list-tests"] + } + if let prelude = testOptions.prelude { + let preludeURL = URL(fileURLWithPath: prelude, relativeTo: URL(fileURLWithPath: FileManager.default.currentDirectoryPath)) + testJsArguments += ["--prelude", preludeURL.path] + } + if let environment = testOptions.environment { + testJsArguments += ["--environment", environment] + } + if testOptions.inspect { + testJsArguments += ["--inspect"] + } + + let xctestCoverageFile = outputDir.appending(path: "XCTest.profraw") + do { + var extraArguments = testJsArguments + if testOptions.packageOptions.enableCodeCoverage { + extraArguments += ["--coverage-file", xctestCoverageFile.path] + } + extraArguments += ["--"] + extraArguments += testLibraryArguments + extraArguments += testOptions.filter + + try PackageToJS.runSingleTestingLibrary( + testRunner: testRunner, currentDirectoryURL: currentDirectoryURL, + extraArguments: extraArguments + ) + } + let swiftTestingCoverageFile = outputDir.appending(path: "SwiftTesting.profraw") + do { + var extraArguments = testJsArguments + if testOptions.packageOptions.enableCodeCoverage { + extraArguments += ["--coverage-file", swiftTestingCoverageFile.path] + } + extraArguments += ["--", "--testing-library", "swift-testing"] + extraArguments += testLibraryArguments + extraArguments += testOptions.filter.flatMap { ["--filter", $0] } + + try PackageToJS.runSingleTestingLibrary( + testRunner: testRunner, currentDirectoryURL: currentDirectoryURL, + extraArguments: extraArguments + ) + } + + if testOptions.packageOptions.enableCodeCoverage { + let profrawFiles = [xctestCoverageFile, swiftTestingCoverageFile].filter { FileManager.default.fileExists(atPath: $0.path) } + 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] + ) throws { let node = try which("node") let arguments = ["--experimental-wasi-unstable-preview1", testRunner.path] + extraArguments print("Running test...") @@ -70,6 +134,18 @@ struct PackageToJS { throw PackageToJSError("Test failed with status \(task.terminationStatus)") } } + + static func postProcessCoverageFiles(outputDir: URL, profrawFiles: [URL]) throws { + let mergedCoverageFile = outputDir.appending(path: "default.profdata") + do { + // Merge the coverage files by llvm-profdata + let arguments = ["merge", "-sparse", "-output", mergedCoverageFile.path] + profrawFiles.map { $0.path } + let llvmProfdata = try which("llvm-profdata") + logCommandExecution(llvmProfdata.path, arguments) + try runCommand(llvmProfdata, arguments) + print("Saved profile data to \(mergedCoverageFile.path)") + } + } } struct PackageToJSError: Swift.Error, CustomStringConvertible { @@ -140,21 +216,19 @@ final class DefaultPackagingSystem: PackagingSystem { } try runCommand(wasmOpt, arguments + ["-o", output, input]) } - - 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)") - } - } } 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 = ";" @@ -171,6 +245,18 @@ internal func which(_ executable: String) throws -> 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 diff --git a/Plugins/PackageToJS/Sources/PackageToJSPlugin.swift b/Plugins/PackageToJS/Sources/PackageToJSPlugin.swift index 4074a8218..853ea5020 100644 --- a/Plugins/PackageToJS/Sources/PackageToJSPlugin.swift +++ b/Plugins/PackageToJS/Sources/PackageToJSPlugin.swift @@ -93,7 +93,9 @@ struct PackageToJSPlugin: CommandPlugin { // Build products let productName = try buildOptions.product ?? deriveDefaultProduct(package: context.package) let build = try buildWasm( - productName: productName, context: context) + productName: productName, context: context, + enableCodeCoverage: buildOptions.packageOptions.enableCodeCoverage + ) guard build.succeeded else { reportBuildFailure(build, arguments) exit(1) @@ -145,7 +147,9 @@ struct PackageToJSPlugin: CommandPlugin { let productName = "\(context.package.displayName)PackageTests" let build = try buildWasm( - productName: productName, context: context) + productName: productName, context: context, + enableCodeCoverage: testOptions.packageOptions.enableCodeCoverage + ) guard build.succeeded else { reportBuildFailure(build, arguments) exit(1) @@ -198,36 +202,18 @@ struct PackageToJSPlugin: CommandPlugin { try make.build(output: rootTask, scope: scope) print("Packaging tests finished") - let testRunner = scope.resolve(path: binDir.appending(path: "test.js")) if !testOptions.buildOnly { - var testJsArguments: [String] = [] - var testFrameworkArguments: [String] = [] - if testOptions.listTests { - testFrameworkArguments += ["--list-tests"] - } - if let prelude = testOptions.prelude { - let preludeURL = URL(fileURLWithPath: prelude, relativeTo: URL(fileURLWithPath: FileManager.default.currentDirectoryPath)) - testJsArguments += ["--prelude", preludeURL.path] - } - if let environment = testOptions.environment { - testJsArguments += ["--environment", environment] - } - if testOptions.inspect { - testJsArguments += ["--inspect"] - } - try PackageToJS.runTest( - testRunner: testRunner, currentDirectoryURL: context.pluginWorkDirectoryURL, - extraArguments: testJsArguments + ["--"] + testFrameworkArguments + testOptions.filter - ) + let testRunner = scope.resolve(path: binDir.appending(path: "test.js")) try PackageToJS.runTest( - testRunner: testRunner, currentDirectoryURL: context.pluginWorkDirectoryURL, - extraArguments: testJsArguments + ["--", "--testing-library", "swift-testing"] + testFrameworkArguments - + testOptions.filter.flatMap { ["--filter", $0] } + testRunner: testRunner, + currentDirectoryURL: context.pluginWorkDirectoryURL, + outputDir: outputDir, + testOptions: testOptions ) } } - private func buildWasm(productName: String, context: PluginContext) throws + private func buildWasm(productName: String, context: PluginContext, enableCodeCoverage: Bool) throws -> PackageManager.BuildResult { var parameters = PackageManager.BuildParameters( @@ -248,6 +234,12 @@ struct PackageToJSPlugin: CommandPlugin { parameters.otherLinkerFlags = [ "--export-if-defined=__main_argc_argv" ] + + // Enable code coverage options if requested + if enableCodeCoverage { + parameters.otherSwiftcFlags += ["-profile-coverage-mapping", "-profile-generate"] + parameters.otherCFlags += ["-fprofile-instr-generate", "-fcoverage-mapping"] + } } return try self.packageManager.build(.product(productName), parameters: parameters) } @@ -292,8 +284,9 @@ extension PackageToJS.PackageOptions { let packageName = extractor.extractOption(named: "package-name").last let explain = extractor.extractFlag(named: "explain") let useCDN = extractor.extractFlag(named: "use-cdn") + let enableCodeCoverage = extractor.extractFlag(named: "enable-code-coverage") return PackageToJS.PackageOptions( - outputPath: outputPath, packageName: packageName, explain: explain != 0, useCDN: useCDN != 0 + outputPath: outputPath, packageName: packageName, explain: explain != 0, useCDN: useCDN != 0, enableCodeCoverage: enableCodeCoverage != 0 ) } } @@ -314,12 +307,14 @@ extension PackageToJS.BuildOptions { USAGE: swift package --swift-sdk [SwiftPM options] PackageToJS [options] [subcommand] OPTIONS: - --product Product to build (default: executable target if there's only one) - --output Path to the output directory (default: .build/plugins/PackageToJS/outputs/Package) - --package-name Name of the package (default: lowercased Package.swift name) - --explain Whether to explain the build plan - --split-debug Whether to split debug information into a separate .wasm.debug file (default: false) - --no-optimize Whether to disable wasm-opt optimization (default: false) + --product Product to build (default: executable target if there's only one) + --output Path to the output directory (default: .build/plugins/PackageToJS/outputs/Package) + --package-name Name of the package (default: lowercased Package.swift name) + --explain Whether to explain the build plan (default: false) + --split-debug Whether to split debug information into a separate .wasm.debug file (default: false) + --no-optimize Whether to disable wasm-opt optimization (default: false) + --use-cdn Whether to use CDN for dependency packages (default: false) + --enable-code-coverage Whether to enable code coverage collection (default: false) SUBCOMMANDS: test Builds and runs tests @@ -365,10 +360,12 @@ extension PackageToJS.TestOptions { USAGE: swift package --swift-sdk [SwiftPM options] PackageToJS test [options] OPTIONS: - --build-only Whether to build only (default: false) - --prelude Path to the prelude script - --environment The environment to use for the tests - --inspect Whether to run tests in the browser with inspector enabled + --build-only Whether to build only (default: false) + --prelude Path to the prelude script + --environment The environment to use for the tests + --inspect Whether to run tests in the browser with inspector enabled + --use-cdn Whether to use CDN for dependency packages (default: false) + --enable-code-coverage Whether to enable code coverage collection (default: false) EXAMPLES: $ swift package --swift-sdk wasm32-unknown-wasi plugin js test diff --git a/Plugins/PackageToJS/Templates/bin/test.js b/Plugins/PackageToJS/Templates/bin/test.js index 5fed17359..b31d82086 100644 --- a/Plugins/PackageToJS/Templates/bin/test.js +++ b/Plugins/PackageToJS/Templates/bin/test.js @@ -3,6 +3,7 @@ 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 "--" @@ -31,6 +32,7 @@ const args = parseArgs({ prelude: { type: "string" }, environment: { type: "string" }, inspect: { type: "boolean" }, + "coverage-file": { type: "string" }, }, }) @@ -38,6 +40,17 @@ const harnesses = { node: async ({ preludeScript }) => { let options = await nodePlatform.defaultNodeSetup({ args: testFrameworkArgs, + onExit: (code) => { + if (code !== 0) { return } + // Extract the coverage file from the wasm module + const filePath = "default.profraw" + const destinationPath = args.values["coverage-file"] ?? filePath + const profraw = options.wasi.extractFile?.(filePath) + if (profraw) { + console.log(`Saved ${filePath} to ${destinationPath}`); + writeFileSync(destinationPath, profraw); + } + }, /* #if USE_SHARED_MEMORY */ spawnWorker: nodePlatform.createDefaultWorkerFactory(preludeScript) /* #endif */ @@ -52,6 +65,12 @@ const harnesses = { await instantiate(options) } catch (e) { if (e instanceof WebAssembly.CompileError) { + // Check Node.js major version + const nodeVersion = process.version.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 } diff --git a/Plugins/PackageToJS/Templates/instantiate.d.ts b/Plugins/PackageToJS/Templates/instantiate.d.ts index f813b5489..424d35175 100644 --- a/Plugins/PackageToJS/Templates/instantiate.d.ts +++ b/Plugins/PackageToJS/Templates/instantiate.d.ts @@ -42,6 +42,13 @@ export interface WASI { * @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 diff --git a/Plugins/PackageToJS/Templates/platforms/node.d.ts b/Plugins/PackageToJS/Templates/platforms/node.d.ts index 433f97ad6..636ad0eea 100644 --- a/Plugins/PackageToJS/Templates/platforms/node.d.ts +++ b/Plugins/PackageToJS/Templates/platforms/node.d.ts @@ -5,6 +5,7 @@ export async 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 */ diff --git a/Plugins/PackageToJS/Templates/platforms/node.js b/Plugins/PackageToJS/Templates/platforms/node.js index a8bb638bc..c45bdf354 100644 --- a/Plugins/PackageToJS/Templates/platforms/node.js +++ b/Plugins/PackageToJS/Templates/platforms/node.js @@ -3,7 +3,7 @@ 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 } from '@bjorn3/browser_wasi_shim'; +import { WASI, File, OpenFile, ConsoleStdout, PreopenDirectory, Directory, Inode } from '@bjorn3/browser_wasi_shim'; /* #endif */ /* #if USE_SHARED_MEMORY */ @@ -119,6 +119,7 @@ export async function defaultNodeSetup(options) { 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) => { @@ -127,7 +128,7 @@ export async function defaultNodeSetup(options) { ConsoleStdout.lineBuffered((stderr) => { console.error(stderr); }), - new PreopenDirectory("/", new Map()), + 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))) @@ -143,10 +144,49 @@ export async function defaultNodeSetup(options) { 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); } }, diff --git a/Plugins/PackageToJS/Tests/ExampleTests.swift b/Plugins/PackageToJS/Tests/ExampleTests.swift index be5d8e60b..743504e3e 100644 --- a/Plugins/PackageToJS/Tests/ExampleTests.swift +++ b/Plugins/PackageToJS/Tests/ExampleTests.swift @@ -42,6 +42,10 @@ extension Trait where Self == ConditionTrait { ProcessInfo.processInfo.environment["SWIFT_SDK_ID"] } + static func getSwiftPath() -> String? { + ProcessInfo.processInfo.environment["SWIFT_PATH"] + } + static let repoPath = URL(fileURLWithPath: #filePath) .deletingLastPathComponent() .deletingLastPathComponent() @@ -97,7 +101,7 @@ extension Trait where Self == ConditionTrait { process.executableURL = URL( fileURLWithPath: "swift", relativeTo: URL( - fileURLWithPath: ProcessInfo.processInfo.environment["SWIFT_PATH"]!)) + fileURLWithPath: try #require(Self.getSwiftPath()))) process.arguments = args process.currentDirectoryURL = destination.appending(path: path) process.environment = ProcessInfo.processInfo.environment.merging(env) { _, new in @@ -148,6 +152,30 @@ extension Trait where Self == ConditionTrait { } } + #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/main.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()) From 2ce221bd9daa19382e0882568841dd1a2663ddd3 Mon Sep 17 00:00:00 2001 From: Yuta Saito Date: Sun, 16 Mar 2025 06:04:11 +0000 Subject: [PATCH 281/373] test: Relax sleep duration checks --- Tests/JavaScriptEventLoopTests/JavaScriptEventLoopTests.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Tests/JavaScriptEventLoopTests/JavaScriptEventLoopTests.swift b/Tests/JavaScriptEventLoopTests/JavaScriptEventLoopTests.swift index 5d610aa48..0609232a0 100644 --- a/Tests/JavaScriptEventLoopTests/JavaScriptEventLoopTests.swift +++ b/Tests/JavaScriptEventLoopTests/JavaScriptEventLoopTests.swift @@ -68,13 +68,13 @@ final class JavaScriptEventLoopTests: XCTestCase { let sleepDiff = try await measureTime { try await Task.sleep(nanoseconds: 200_000_000) } - XCTAssertGreaterThanOrEqual(sleepDiff, 200) + XCTAssertGreaterThanOrEqual(sleepDiff, 150) // Test shorter sleep duration let shortSleepDiff = try await measureTime { try await Task.sleep(nanoseconds: 100_000_000) } - XCTAssertGreaterThanOrEqual(shortSleepDiff, 100) + XCTAssertGreaterThanOrEqual(shortSleepDiff, 50) } func testTaskPriority() async throws { From 62367111efacda7ee8f2a55bc7461c9726edc95b Mon Sep 17 00:00:00 2001 From: Yuta Saito Date: Sun, 16 Mar 2025 06:17:56 +0000 Subject: [PATCH 282/373] [skip ci] Fix display name of output path --- Plugins/PackageToJS/Sources/PackageToJSPlugin.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Plugins/PackageToJS/Sources/PackageToJSPlugin.swift b/Plugins/PackageToJS/Sources/PackageToJSPlugin.swift index 853ea5020..49e60fce3 100644 --- a/Plugins/PackageToJS/Sources/PackageToJSPlugin.swift +++ b/Plugins/PackageToJS/Sources/PackageToJSPlugin.swift @@ -266,7 +266,7 @@ struct PackageToJSPlugin: CommandPlugin { private func printProgress(context: MiniMake.ProgressPrinter.Context, message: String) { let buildCwd = FileManager.default.currentDirectoryPath let outputPath = context.scope.resolve(path: context.subject.output).path - let displayName = outputPath.hasPrefix(buildCwd) + let displayName = outputPath.hasPrefix(buildCwd + "/") ? String(outputPath.dropFirst(buildCwd.count + 1)) : outputPath printStderr("[\(context.built + 1)/\(context.total)] \(displayName): \(message)") } From c4a8bae5205ebeabbf0e7405d3a229907e5560bd Mon Sep 17 00:00:00 2001 From: Yuta Saito Date: Sun, 16 Mar 2025 06:38:41 +0000 Subject: [PATCH 283/373] Add `-Xnode` option to pass extra arguments to node --- Plugins/PackageToJS/Sources/PackageToJS.swift | 13 +++++-- .../Sources/PackageToJSPlugin.swift | 38 ++++++++++++++++++- Plugins/PackageToJS/Tests/ExampleTests.swift | 14 +++++++ 3 files changed, 60 insertions(+), 5 deletions(-) diff --git a/Plugins/PackageToJS/Sources/PackageToJS.swift b/Plugins/PackageToJS/Sources/PackageToJS.swift index 1949527dc..80934c248 100644 --- a/Plugins/PackageToJS/Sources/PackageToJS.swift +++ b/Plugins/PackageToJS/Sources/PackageToJS.swift @@ -38,6 +38,8 @@ struct PackageToJS { 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 } @@ -82,7 +84,8 @@ struct PackageToJS { try PackageToJS.runSingleTestingLibrary( testRunner: testRunner, currentDirectoryURL: currentDirectoryURL, - extraArguments: extraArguments + extraArguments: extraArguments, + testOptions: testOptions ) } let swiftTestingCoverageFile = outputDir.appending(path: "SwiftTesting.profraw") @@ -97,7 +100,8 @@ struct PackageToJS { try PackageToJS.runSingleTestingLibrary( testRunner: testRunner, currentDirectoryURL: currentDirectoryURL, - extraArguments: extraArguments + extraArguments: extraArguments, + testOptions: testOptions ) } @@ -114,10 +118,11 @@ struct PackageToJS { static func runSingleTestingLibrary( testRunner: URL, currentDirectoryURL: URL, - extraArguments: [String] + extraArguments: [String], + testOptions: TestOptions ) throws { let node = try which("node") - let arguments = ["--experimental-wasi-unstable-preview1", testRunner.path] + extraArguments + let arguments = ["--experimental-wasi-unstable-preview1"] + testOptions.extraNodeArguments + [testRunner.path] + extraArguments print("Running test...") logCommandExecution(node.path, arguments) diff --git a/Plugins/PackageToJS/Sources/PackageToJSPlugin.swift b/Plugins/PackageToJS/Sources/PackageToJSPlugin.swift index 49e60fce3..96102376d 100644 --- a/Plugins/PackageToJS/Sources/PackageToJSPlugin.swift +++ b/Plugins/PackageToJS/Sources/PackageToJSPlugin.swift @@ -340,10 +340,13 @@ extension PackageToJS.TestOptions { 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, packageOptions: packageOptions + filter: filter, prelude: prelude, environment: environment, inspect: inspect != 0, + extraNodeArguments: extraNodeArguments, + packageOptions: packageOptions ) if !options.buildOnly, !options.packageOptions.useCDN { @@ -379,6 +382,39 @@ extension PackageToJS.TestOptions { // MARK: - PackagePlugin helpers +extension ArgumentExtractor { + fileprivate mutating func extractSingleDashOption(named name: String) -> [String] { + let parts = remainingArguments.split(separator: "--", maxSplits: 1, omittingEmptySubsequences: false) + var args = Array(parts[0]) + let literals = Array(parts.count == 2 ? parts[1] : []) + + var values: [String] = [] + var idx = 0 + while idx < args.count { + var arg = args[idx] + if arg == "-\(name)" { + args.remove(at: idx) + if idx < args.count { + let val = args[idx] + values.append(val) + args.remove(at: idx) + } + } + 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 diff --git a/Plugins/PackageToJS/Tests/ExampleTests.swift b/Plugins/PackageToJS/Tests/ExampleTests.swift index 743504e3e..53048e000 100644 --- a/Plugins/PackageToJS/Tests/ExampleTests.swift +++ b/Plugins/PackageToJS/Tests/ExampleTests.swift @@ -148,6 +148,20 @@ extension Trait where Self == ConditionTrait { 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"], [:]) } } From faa935932da9722b7a29e8915f2319aae38dc688 Mon Sep 17 00:00:00 2001 From: Yuta Saito Date: Sun, 16 Mar 2025 08:32:32 +0000 Subject: [PATCH 284/373] PackageToJS: Bring XCTest output formatter from carton --- Examples/Testing/Package.swift | 6 +- Plugins/PackageToJS/Sources/PackageToJS.swift | 65 +++++ .../Sources/PackageToJSPlugin.swift | 4 + Plugins/PackageToJS/Sources/TestsParser.swift | 259 ++++++++++++++++++ .../PackageToJS/Tests/SnapshotTesting.swift | 4 +- .../PackageToJS/Tests/TestParserTests.swift | 137 +++++++++ .../TestParserTests/testAllPassed.txt | 9 + .../TestParserTests/testAssertFailure.txt | 14 + .../TestParserTests/testCrash.txt | 22 ++ .../TestParserTests/testSkipped.txt | 10 + .../TestParserTests/testThrowFailure.txt | 14 + 11 files changed, 541 insertions(+), 3 deletions(-) create mode 100644 Plugins/PackageToJS/Sources/TestsParser.swift create mode 100644 Plugins/PackageToJS/Tests/TestParserTests.swift create mode 100644 Plugins/PackageToJS/Tests/__Snapshots__/TestParserTests/testAllPassed.txt create mode 100644 Plugins/PackageToJS/Tests/__Snapshots__/TestParserTests/testAssertFailure.txt create mode 100644 Plugins/PackageToJS/Tests/__Snapshots__/TestParserTests/testCrash.txt create mode 100644 Plugins/PackageToJS/Tests/__Snapshots__/TestParserTests/testSkipped.txt create mode 100644 Plugins/PackageToJS/Tests/__Snapshots__/TestParserTests/testThrowFailure.txt diff --git a/Examples/Testing/Package.swift b/Examples/Testing/Package.swift index 6dd492cd1..d9d1719f0 100644 --- a/Examples/Testing/Package.swift +++ b/Examples/Testing/Package.swift @@ -18,7 +18,11 @@ let package = Package( ]), .testTarget( name: "CounterTests", - dependencies: ["Counter"] + dependencies: [ + "Counter", + // This is needed to run the tests in the JavaScript event loop + .product(name: "JavaScriptEventLoopTestSupport", package: "JavaScriptKit") + ] ), ] ) diff --git a/Plugins/PackageToJS/Sources/PackageToJS.swift b/Plugins/PackageToJS/Sources/PackageToJS.swift index 80934c248..4d5e44ee0 100644 --- a/Plugins/PackageToJS/Sources/PackageToJS.swift +++ b/Plugins/PackageToJS/Sources/PackageToJS.swift @@ -40,6 +40,8 @@ struct PackageToJS { var inspect: Bool /// The extra arguments to pass to node var extraNodeArguments: [String] + /// Whether to print verbose output + var verbose: Bool /// The options for packaging var packageOptions: PackageOptions } @@ -85,6 +87,7 @@ struct PackageToJS { try PackageToJS.runSingleTestingLibrary( testRunner: testRunner, currentDirectoryURL: currentDirectoryURL, extraArguments: extraArguments, + testParser: testOptions.verbose ? nil : FancyTestsParser(), testOptions: testOptions ) } @@ -119,6 +122,7 @@ struct PackageToJS { testRunner: URL, currentDirectoryURL: URL, extraArguments: [String], + testParser: (any TestsParser)? = nil, testOptions: TestOptions ) throws { let node = try which("node") @@ -129,11 +133,39 @@ struct PackageToJS { let task = Process() task.executableURL = node task.arguments = arguments + + var finalize: () -> Void = {} + if let testParser = testParser { + class Writer: InteractiveWriter { + func write(_ string: String) { + print(string, terminator: "") + } + } + + let writer = Writer() + let stdoutBuffer = LineBuffer { line in + testParser.onLine(line, writer) + } + 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(writer) + } + } + 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 task.terminationStatus == 0 || task.terminationStatus == 69 else { throw PackageToJSError("Test failed with status \(task.terminationStatus)") @@ -151,6 +183,39 @@ struct PackageToJS { 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 += 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 { diff --git a/Plugins/PackageToJS/Sources/PackageToJSPlugin.swift b/Plugins/PackageToJS/Sources/PackageToJSPlugin.swift index 96102376d..9013b26e6 100644 --- a/Plugins/PackageToJS/Sources/PackageToJSPlugin.swift +++ b/Plugins/PackageToJS/Sources/PackageToJSPlugin.swift @@ -340,12 +340,14 @@ extension PackageToJS.TestOptions { let prelude = extractor.extractOption(named: "prelude").last let environment = extractor.extractOption(named: "environment").last let inspect = extractor.extractFlag(named: "inspect") + let verbose = extractor.extractFlag(named: "verbose") 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, + verbose: verbose != 0, packageOptions: packageOptions ) @@ -369,6 +371,8 @@ extension PackageToJS.TestOptions { --inspect Whether to run tests in the browser with inspector enabled --use-cdn Whether to use CDN for dependency packages (default: false) --enable-code-coverage Whether to enable code coverage collection (default: false) + --verbose Whether to print verbose output (default: false) + -Xnode Extra arguments to pass to Node.js EXAMPLES: $ swift package --swift-sdk wasm32-unknown-wasi plugin js test diff --git a/Plugins/PackageToJS/Sources/TestsParser.swift b/Plugins/PackageToJS/Sources/TestsParser.swift new file mode 100644 index 000000000..d222dd2e7 --- /dev/null +++ b/Plugins/PackageToJS/Sources/TestsParser.swift @@ -0,0 +1,259 @@ +/// 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 + +protocol InteractiveWriter { + func write(_ string: String) +} + +protocol TestsParser { + /// Parse the output of a test process, format it, then output in the `InteractiveWriter`. + func onLine(_ line: String, _ terminal: InteractiveWriter) + func finalize(_ terminal: InteractiveWriter) +} + +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: TestsParser { + init() {} + + enum Status: Equatable { + 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) + } + } + } + + 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? + } + } + + var suites = [Suite]() + + let swiftIdentifier = #/[_\p{L}\p{Nl}][_\p{L}\p{Nl}\p{Mn}\p{Nd}\p{Pc}]*/# + let timestamp = #/\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}\.\d{3}/# + lazy var suiteStarted = Regex { + "Test Suite '" + Capture { + OneOrMore(CharacterClass.anyOf("'").inverted) + } + "' started at " + Capture { self.timestamp } + } + lazy var suiteStatus = Regex { + "Test Suite '" + Capture { OneOrMore(CharacterClass.anyOf("'").inverted) } + "' " + Capture { + ChoiceOf { + "failed" + "passed" + } + } + " at " + Capture { self.timestamp } + } + lazy var testCaseStarted = Regex { + "Test Case '" + Capture { self.swiftIdentifier } + "." + Capture { self.swiftIdentifier } + "' started" + } + lazy var testCaseStatus = Regex { + "Test Case '" + Capture { self.swiftIdentifier } + "." + Capture { self.swiftIdentifier } + "' " + Capture { + ChoiceOf { + "failed" + "passed" + "skipped" + } + } + " (" + Capture { + OneOrMore(.digit) + "." + OneOrMore(.digit) + } + " seconds)" + } + + 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, _ terminal: InteractiveWriter) { + 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], terminal) + } + } 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 { + terminal.write(line + "\n") + } + } + } + + func finalize(_ terminal: InteractiveWriter) { + terminal.write("\n") + flushSummary(of: suites, terminal) + } + + private func flushSingleSuite(_ suite: Suite, _ terminal: InteractiveWriter) { + terminal.write(suite.statusLabel) + terminal.write(" \(suite.name)\n") + for testCase in suite.cases { + terminal.write(" \(testCase.statusMark) ") + if let duration = testCase.duration { + terminal + .write( + "\(testCase.name) \("(\(Int(Double(duration)! * 1000))ms)", color: "[90m")\n" + ) // gray + } + } + } + + private func flushSummary(of suites: [Suite], _ terminal: InteractiveWriter) { + let suitesWithCases = suites.filter { $0.cases.count > 0 } + + terminal.write("Test Suites: ") + let suitesPassed = suitesWithCases.filter { $0.status == .passed }.count + if suitesPassed > 0 { + terminal.write("\("\(suitesPassed) passed", color: "[32m"), ") + } + let suitesSkipped = suitesWithCases.filter { $0.status == .skipped }.count + if suitesSkipped > 0 { + terminal.write("\("\(suitesSkipped) skipped", color: "[97m"), ") + } + let suitesFailed = suitesWithCases.filter { $0.status == .failed }.count + if suitesFailed > 0 { + terminal.write("\("\(suitesFailed) failed", color: "[31m"), ") + } + let suitesUnknown = suitesWithCases.filter { $0.status == .unknown(nil) }.count + if suitesUnknown > 0 { + terminal.write("\("\(suitesUnknown) unknown", color: "[31m"), ") + } + terminal.write("\(suitesWithCases.count) total\n") + + terminal.write("Tests: ") + let allTests = suitesWithCases.map(\.cases).reduce([], +) + let testsPassed = allTests.filter { $0.status == .passed }.count + if testsPassed > 0 { + terminal.write("\("\(testsPassed) passed", color: "[32m"), ") + } + let testsSkipped = allTests.filter { $0.status == .skipped }.count + if testsSkipped > 0 { + terminal.write("\("\(testsSkipped) skipped", color: "[97m"), ") + } + let testsFailed = allTests.filter { $0.status == .failed }.count + if testsFailed > 0 { + terminal.write("\("\(testsFailed) failed", color: "[31m"), ") + } + let testsUnknown = allTests.filter { $0.status == .unknown(nil) }.count + if testsUnknown > 0 { + terminal.write("\("\(testsUnknown) unknown", color: "[31m"), ") + } + terminal.write("\(allTests.count) total\n") + + if suites.contains(where: { $0.name == "All tests" }) { + terminal.write("\("Ran all test suites.", color: "[90m")\n") // gray + } + + if suites.contains(where: { $0.status.isNegative }) { + print(suites.filter({ $0.status.isNegative })) + terminal.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 }) { + terminal.write(" \(testCase.statusMark) \(suite.name).\(testCase.name)\n") + } + } + + terminal.write( + "\n\("Some tests failed. Use --verbose for raw test output.", color: "[33m")\n" + ) + } + } +} diff --git a/Plugins/PackageToJS/Tests/SnapshotTesting.swift b/Plugins/PackageToJS/Tests/SnapshotTesting.swift index 8e556357b..4732cfce8 100644 --- a/Plugins/PackageToJS/Tests/SnapshotTesting.swift +++ b/Plugins/PackageToJS/Tests/SnapshotTesting.swift @@ -5,7 +5,7 @@ func assertSnapshot( filePath: String = #filePath, function: String = #function, sourceLocation: SourceLocation = #_sourceLocation, variant: String? = nil, - input: Data + input: Data, fileExtension: String = "json" ) throws { let testFileName = URL(fileURLWithPath: filePath).deletingPathExtension().lastPathComponent let snapshotDir = URL(fileURLWithPath: filePath) @@ -13,7 +13,7 @@ func assertSnapshot( .appendingPathComponent("__Snapshots__") .appendingPathComponent(testFileName) try FileManager.default.createDirectory(at: snapshotDir, withIntermediateDirectories: true) - let snapshotFileName: String = "\(function[..: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__/TestParserTests/testAllPassed.txt b/Plugins/PackageToJS/Tests/__Snapshots__/TestParserTests/testAllPassed.txt new file mode 100644 index 000000000..7c1d56a6c --- /dev/null +++ b/Plugins/PackageToJS/Tests/__Snapshots__/TestParserTests/testAllPassed.txt @@ -0,0 +1,9 @@ + PASSED  CounterTests + ✔ testIncrement (2ms) + ✔ testIncrementTwice (1ms) + 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__/TestParserTests/testAssertFailure.txt b/Plugins/PackageToJS/Tests/__Snapshots__/TestParserTests/testAssertFailure.txt new file mode 100644 index 000000000..2adb698cb --- /dev/null +++ b/Plugins/PackageToJS/Tests/__Snapshots__/TestParserTests/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 (1ms) + 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__/TestParserTests/testCrash.txt b/Plugins/PackageToJS/Tests/__Snapshots__/TestParserTests/testCrash.txt new file mode 100644 index 000000000..ada55fb0d --- /dev/null +++ b/Plugins/PackageToJS/Tests/__Snapshots__/TestParserTests/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__/TestParserTests/testSkipped.txt b/Plugins/PackageToJS/Tests/__Snapshots__/TestParserTests/testSkipped.txt new file mode 100644 index 000000000..eb945cc90 --- /dev/null +++ b/Plugins/PackageToJS/Tests/__Snapshots__/TestParserTests/testSkipped.txt @@ -0,0 +1,10 @@ +/tmp/Tests/CounterTests/CounterTests.swift:25: CounterTests.testIncrement : Test skipped - Skip it + PASSED  CounterTests + ➜ testIncrement (6ms) + ✔ testIncrementTwice (0ms) + 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__/TestParserTests/testThrowFailure.txt b/Plugins/PackageToJS/Tests/__Snapshots__/TestParserTests/testThrowFailure.txt new file mode 100644 index 000000000..ec5115e4a --- /dev/null +++ b/Plugins/PackageToJS/Tests/__Snapshots__/TestParserTests/testThrowFailure.txt @@ -0,0 +1,14 @@ +:0: error: CounterTests.testThrowFailure : threw error "TestError()" + FAILED  CounterTests + ✘ testThrowFailure (2ms) + 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. From 6bf418e82174e7e4112b778c6ac61f2c7ca855cb Mon Sep 17 00:00:00 2001 From: Yuta Saito Date: Sun, 16 Mar 2025 12:30:47 +0000 Subject: [PATCH 285/373] Optimize compile-time --- Plugins/PackageToJS/Sources/PackageToJS.swift | 15 +- Plugins/PackageToJS/Sources/TestsParser.swift | 153 +++++++++--------- ...rserTests.swift => TestsParserTests.swift} | 19 +-- .../TestParserTests/testAllPassed.txt | 4 +- .../TestParserTests/testAssertFailure.txt | 4 +- .../TestParserTests/testCrash.txt | 4 +- .../TestParserTests/testSkipped.txt | 4 +- .../TestParserTests/testThrowFailure.txt | 4 +- 8 files changed, 94 insertions(+), 113 deletions(-) rename Plugins/PackageToJS/Tests/{TestParserTests.swift => TestsParserTests.swift} (94%) diff --git a/Plugins/PackageToJS/Sources/PackageToJS.swift b/Plugins/PackageToJS/Sources/PackageToJS.swift index 4d5e44ee0..1f7c1e189 100644 --- a/Plugins/PackageToJS/Sources/PackageToJS.swift +++ b/Plugins/PackageToJS/Sources/PackageToJS.swift @@ -87,7 +87,7 @@ struct PackageToJS { try PackageToJS.runSingleTestingLibrary( testRunner: testRunner, currentDirectoryURL: currentDirectoryURL, extraArguments: extraArguments, - testParser: testOptions.verbose ? nil : FancyTestsParser(), + testParser: testOptions.verbose ? nil : FancyTestsParser(write: { print($0, terminator: "") }), testOptions: testOptions ) } @@ -122,7 +122,7 @@ struct PackageToJS { testRunner: URL, currentDirectoryURL: URL, extraArguments: [String], - testParser: (any TestsParser)? = nil, + testParser: FancyTestsParser? = nil, testOptions: TestOptions ) throws { let node = try which("node") @@ -136,15 +136,8 @@ struct PackageToJS { var finalize: () -> Void = {} if let testParser = testParser { - class Writer: InteractiveWriter { - func write(_ string: String) { - print(string, terminator: "") - } - } - - let writer = Writer() let stdoutBuffer = LineBuffer { line in - testParser.onLine(line, writer) + testParser.onLine(line) } let stdoutPipe = Pipe() stdoutPipe.fileHandleForReading.readabilityHandler = { handle in @@ -156,7 +149,7 @@ struct PackageToJS { stdoutBuffer.append(data) } stdoutBuffer.flush() - testParser.finalize(writer) + testParser.finalize() } } diff --git a/Plugins/PackageToJS/Sources/TestsParser.swift b/Plugins/PackageToJS/Sources/TestsParser.swift index d222dd2e7..efd757124 100644 --- a/Plugins/PackageToJS/Sources/TestsParser.swift +++ b/Plugins/PackageToJS/Sources/TestsParser.swift @@ -4,16 +4,6 @@ import Foundation import RegexBuilder -protocol InteractiveWriter { - func write(_ string: String) -} - -protocol TestsParser { - /// Parse the output of a test process, format it, then output in the `InteractiveWriter`. - func onLine(_ line: String, _ terminal: InteractiveWriter) - func finalize(_ terminal: InteractiveWriter) -} - extension String.StringInterpolation { /// Display `value` with the specified ANSI-escaped `color` values, then apply the reset. fileprivate mutating func appendInterpolation(_ value: T, color: String...) { @@ -21,10 +11,14 @@ extension String.StringInterpolation { } } -class FancyTestsParser: TestsParser { - init() {} +class FancyTestsParser { + let write: (String) -> Void - enum Status: Equatable { + init(write: @escaping (String) -> Void) { + self.write = write + } + + private enum Status: Equatable { case passed, failed, skipped case unknown(String.SubSequence?) @@ -45,7 +39,7 @@ class FancyTestsParser: TestsParser { } } - struct Suite { + private struct Suite { let name: String.SubSequence var status: Status = .unknown(nil) @@ -76,11 +70,11 @@ class FancyTestsParser: TestsParser { } } - var suites = [Suite]() + private var suites = [Suite]() - let swiftIdentifier = #/[_\p{L}\p{Nl}][_\p{L}\p{Nl}\p{Mn}\p{Nd}\p{Pc}]*/# - let timestamp = #/\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}\.\d{3}/# - lazy var suiteStarted = Regex { + 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) @@ -88,7 +82,7 @@ class FancyTestsParser: TestsParser { "' started at " Capture { self.timestamp } } - lazy var suiteStatus = Regex { + private lazy var suiteStatus = Regex { "Test Suite '" Capture { OneOrMore(CharacterClass.anyOf("'").inverted) } "' " @@ -101,14 +95,14 @@ class FancyTestsParser: TestsParser { " at " Capture { self.timestamp } } - lazy var testCaseStarted = Regex { + private lazy var testCaseStarted = Regex { "Test Case '" Capture { self.swiftIdentifier } "." Capture { self.swiftIdentifier } "' started" } - lazy var testCaseStatus = Regex { + private lazy var testCaseStatus = Regex { "Test Case '" Capture { self.swiftIdentifier } "." @@ -130,10 +124,10 @@ class FancyTestsParser: TestsParser { " seconds)" } - let testSummary = + 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, _ terminal: InteractiveWriter) { + func onLine(_ line: String) { if let match = line.firstMatch( of: suiteStarted ) { @@ -145,7 +139,7 @@ class FancyTestsParser: TestsParser { let (_, suite, status, _) = match.output if let suiteIdx = suites.firstIndex(where: { $0.name == suite }) { suites[suiteIdx].status = Status(rawValue: status) - flushSingleSuite(suites[suiteIdx], terminal) + flushSingleSuite(suites[suiteIdx]) } } else if let match = line.firstMatch( of: testCaseStarted @@ -172,86 +166,87 @@ class FancyTestsParser: TestsParser { // do nothing } else { if !line.isEmpty { - terminal.write(line + "\n") + write(line + "\n") } } } - func finalize(_ terminal: InteractiveWriter) { - terminal.write("\n") - flushSummary(of: suites, terminal) - } - - private func flushSingleSuite(_ suite: Suite, _ terminal: InteractiveWriter) { - terminal.write(suite.statusLabel) - terminal.write(" \(suite.name)\n") + private func flushSingleSuite(_ suite: Suite) { + write(suite.statusLabel) + write(" \(suite.name)\n") for testCase in suite.cases { - terminal.write(" \(testCase.statusMark) ") + write(" \(testCase.statusMark) ") if let duration = testCase.duration { - terminal - .write( + write( "\(testCase.name) \("(\(Int(Double(duration)! * 1000))ms)", color: "[90m")\n" ) // gray } } } - private func flushSummary(of suites: [Suite], _ terminal: InteractiveWriter) { - let suitesWithCases = suites.filter { $0.cases.count > 0 } - - terminal.write("Test Suites: ") - let suitesPassed = suitesWithCases.filter { $0.status == .passed }.count - if suitesPassed > 0 { - terminal.write("\("\(suitesPassed) passed", color: "[32m"), ") - } - let suitesSkipped = suitesWithCases.filter { $0.status == .skipped }.count - if suitesSkipped > 0 { - terminal.write("\("\(suitesSkipped) skipped", color: "[97m"), ") - } - let suitesFailed = suitesWithCases.filter { $0.status == .failed }.count - if suitesFailed > 0 { - terminal.write("\("\(suitesFailed) failed", color: "[31m"), ") - } - let suitesUnknown = suitesWithCases.filter { $0.status == .unknown(nil) }.count - if suitesUnknown > 0 { - terminal.write("\("\(suitesUnknown) unknown", color: "[31m"), ") + 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 } - terminal.write("\(suitesWithCases.count) total\n") - terminal.write("Tests: ") - let allTests = suitesWithCases.map(\.cases).reduce([], +) - let testsPassed = allTests.filter { $0.status == .passed }.count - if testsPassed > 0 { - terminal.write("\("\(testsPassed) passed", color: "[32m"), ") - } - let testsSkipped = allTests.filter { $0.status == .skipped }.count - if testsSkipped > 0 { - terminal.write("\("\(testsSkipped) skipped", color: "[97m"), ") - } - let testsFailed = allTests.filter { $0.status == .failed }.count - if testsFailed > 0 { - terminal.write("\("\(testsFailed) failed", color: "[31m"), ") - } - let testsUnknown = allTests.filter { $0.status == .unknown(nil) }.count - if testsUnknown > 0 { - terminal.write("\("\(testsUnknown) unknown", color: "[31m"), ") + 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 } } - terminal.write("\(allTests.count) total\n") + write( + formatCategory( + label: "Tests: ", statuses: allCaseStatuses + ) + ) if suites.contains(where: { $0.name == "All tests" }) { - terminal.write("\("Ran all test suites.", color: "[90m")\n") // gray + write("\("Ran all test suites.", color: "[90m")\n") // gray } if suites.contains(where: { $0.status.isNegative }) { - print(suites.filter({ $0.status.isNegative })) - terminal.write("\n\("Failed test cases:", color: "[31m")\n") + 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 }) { - terminal.write(" \(testCase.statusMark) \(suite.name).\(testCase.name)\n") + write(" \(testCase.statusMark) \(suite.name).\(testCase.name)\n") } } - terminal.write( + write( "\n\("Some tests failed. Use --verbose for raw test output.", color: "[33m")\n" ) } diff --git a/Plugins/PackageToJS/Tests/TestParserTests.swift b/Plugins/PackageToJS/Tests/TestsParserTests.swift similarity index 94% rename from Plugins/PackageToJS/Tests/TestParserTests.swift rename to Plugins/PackageToJS/Tests/TestsParserTests.swift index a42c86997..099febf13 100644 --- a/Plugins/PackageToJS/Tests/TestParserTests.swift +++ b/Plugins/PackageToJS/Tests/TestsParserTests.swift @@ -3,30 +3,23 @@ import Testing @testable import PackageToJS -@Suite struct TestParserTests { +@Suite struct TestsParserTests { func assertFancyFormatSnapshot( _ input: String, filePath: String = #filePath, function: String = #function, sourceLocation: SourceLocation = #_sourceLocation ) throws { - let parser = FancyTestsParser() + var output = "" + let parser = FancyTestsParser(write: { output += $0 }) let lines = input.split(separator: "\n", omittingEmptySubsequences: false) - class Writer: InteractiveWriter { - var output = "" - func write(_ string: String) { - output += string - } - } - - let writer = Writer() for line in lines { - parser.onLine(String(line), writer) + parser.onLine(String(line)) } - parser.finalize(writer) + parser.finalize() try assertSnapshot( filePath: filePath, function: function, sourceLocation: sourceLocation, - input: Data(writer.output.utf8), fileExtension: "txt", + input: Data(output.utf8), fileExtension: "txt", ) } diff --git a/Plugins/PackageToJS/Tests/__Snapshots__/TestParserTests/testAllPassed.txt b/Plugins/PackageToJS/Tests/__Snapshots__/TestParserTests/testAllPassed.txt index 7c1d56a6c..121c05199 100644 --- a/Plugins/PackageToJS/Tests/__Snapshots__/TestParserTests/testAllPassed.txt +++ b/Plugins/PackageToJS/Tests/__Snapshots__/TestParserTests/testAllPassed.txt @@ -4,6 +4,6 @@  PASSED  /.xctest  PASSED  All tests -Test Suites: 1 passed, 1 total -Tests: 2 passed, 2 total +Test Suites: 1 passed, 1 total +Tests: 2 passed, 2 total Ran all test suites. diff --git a/Plugins/PackageToJS/Tests/__Snapshots__/TestParserTests/testAssertFailure.txt b/Plugins/PackageToJS/Tests/__Snapshots__/TestParserTests/testAssertFailure.txt index 2adb698cb..75dc7a9af 100644 --- a/Plugins/PackageToJS/Tests/__Snapshots__/TestParserTests/testAssertFailure.txt +++ b/Plugins/PackageToJS/Tests/__Snapshots__/TestParserTests/testAssertFailure.txt @@ -4,8 +4,8 @@  FAILED  /.xctest  FAILED  All tests -Test Suites: 1 failed, 1 total -Tests: 1 failed, 1 total +Test Suites: 1 failed, 1 total +Tests: 1 failed, 1 total Ran all test suites. Failed test cases: diff --git a/Plugins/PackageToJS/Tests/__Snapshots__/TestParserTests/testCrash.txt b/Plugins/PackageToJS/Tests/__Snapshots__/TestParserTests/testCrash.txt index ada55fb0d..02977cc1d 100644 --- a/Plugins/PackageToJS/Tests/__Snapshots__/TestParserTests/testCrash.txt +++ b/Plugins/PackageToJS/Tests/__Snapshots__/TestParserTests/testCrash.txt @@ -12,8 +12,8 @@ RuntimeError: unreachable 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 +Test Suites: 1 unknown, 1 total +Tests: 1 unknown, 1 total Ran all test suites. Failed test cases: diff --git a/Plugins/PackageToJS/Tests/__Snapshots__/TestParserTests/testSkipped.txt b/Plugins/PackageToJS/Tests/__Snapshots__/TestParserTests/testSkipped.txt index eb945cc90..7d10905f9 100644 --- a/Plugins/PackageToJS/Tests/__Snapshots__/TestParserTests/testSkipped.txt +++ b/Plugins/PackageToJS/Tests/__Snapshots__/TestParserTests/testSkipped.txt @@ -5,6 +5,6 @@  PASSED  /.xctest  PASSED  All tests -Test Suites: 1 passed, 1 total -Tests: 1 passed, 1 skipped, 2 total +Test Suites: 1 passed, 1 total +Tests: 1 passed, 1 skipped, 2 total Ran all test suites. diff --git a/Plugins/PackageToJS/Tests/__Snapshots__/TestParserTests/testThrowFailure.txt b/Plugins/PackageToJS/Tests/__Snapshots__/TestParserTests/testThrowFailure.txt index ec5115e4a..d33db731c 100644 --- a/Plugins/PackageToJS/Tests/__Snapshots__/TestParserTests/testThrowFailure.txt +++ b/Plugins/PackageToJS/Tests/__Snapshots__/TestParserTests/testThrowFailure.txt @@ -4,8 +4,8 @@  FAILED  /.xctest  FAILED  All tests -Test Suites: 1 failed, 1 total -Tests: 1 failed, 1 total +Test Suites: 1 failed, 1 total +Tests: 1 failed, 1 total Ran all test suites. Failed test cases: From 3002a2a52f895a3857c8830efa2f44d76288f382 Mon Sep 17 00:00:00 2001 From: Yuta Saito Date: Sun, 16 Mar 2025 12:38:41 +0000 Subject: [PATCH 286/373] test: Relax the timing constraint --- Tests/JavaScriptEventLoopTests/JavaScriptEventLoopTests.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Tests/JavaScriptEventLoopTests/JavaScriptEventLoopTests.swift b/Tests/JavaScriptEventLoopTests/JavaScriptEventLoopTests.swift index 0609232a0..1cd628338 100644 --- a/Tests/JavaScriptEventLoopTests/JavaScriptEventLoopTests.swift +++ b/Tests/JavaScriptEventLoopTests/JavaScriptEventLoopTests.swift @@ -235,7 +235,7 @@ final class JavaScriptEventLoopTests: XCTestCase { let result = try await promise!.value XCTAssertEqual(result, .number(3)) } - XCTAssertGreaterThanOrEqual(closureDiff, 200) + XCTAssertGreaterThanOrEqual(closureDiff, 150) } // MARK: - Clock Tests From 5d8f43eb685ff453e7af360272dae7921105e756 Mon Sep 17 00:00:00 2001 From: Yuta Saito Date: Sun, 16 Mar 2025 12:42:55 +0000 Subject: [PATCH 287/373] Fix 6.0 build of PackageToJS --- Plugins/PackageToJS/Tests/TestsParserTests.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Plugins/PackageToJS/Tests/TestsParserTests.swift b/Plugins/PackageToJS/Tests/TestsParserTests.swift index 099febf13..cb0f7d202 100644 --- a/Plugins/PackageToJS/Tests/TestsParserTests.swift +++ b/Plugins/PackageToJS/Tests/TestsParserTests.swift @@ -19,7 +19,7 @@ import Testing parser.finalize() try assertSnapshot( filePath: filePath, function: function, sourceLocation: sourceLocation, - input: Data(output.utf8), fileExtension: "txt", + input: Data(output.utf8), fileExtension: "txt" ) } From e5210527127a2eae4f7a213726be9ebacd18471c Mon Sep 17 00:00:00 2001 From: Yuta Saito Date: Sun, 16 Mar 2025 13:25:12 +0000 Subject: [PATCH 288/373] Optimize compile-time --- Plugins/PackageToJS/Sources/PackageToJS.swift | 63 +++++++---- Plugins/PackageToJS/Sources/ParseWasm.swift | 105 +++++++++--------- Plugins/PackageToJS/Sources/TestsParser.swift | 12 +- .../testAllPassed.txt | 4 +- .../testAssertFailure.txt | 2 +- .../testCrash.txt | 0 .../testSkipped.txt | 4 +- .../testThrowFailure.txt | 2 +- 8 files changed, 101 insertions(+), 91 deletions(-) rename Plugins/PackageToJS/Tests/__Snapshots__/{TestParserTests => TestsParserTests}/testAllPassed.txt (70%) rename Plugins/PackageToJS/Tests/__Snapshots__/{TestParserTests => TestsParserTests}/testAssertFailure.txt (91%) rename Plugins/PackageToJS/Tests/__Snapshots__/{TestParserTests => TestsParserTests}/testCrash.txt (100%) rename Plugins/PackageToJS/Tests/__Snapshots__/{TestParserTests => TestsParserTests}/testSkipped.txt (78%) rename Plugins/PackageToJS/Tests/__Snapshots__/{TestParserTests => TestsParserTests}/testThrowFailure.txt (89%) diff --git a/Plugins/PackageToJS/Sources/PackageToJS.swift b/Plugins/PackageToJS/Sources/PackageToJS.swift index 1f7c1e189..03af2c73b 100644 --- a/Plugins/PackageToJS/Sources/PackageToJS.swift +++ b/Plugins/PackageToJS/Sources/PackageToJS.swift @@ -61,28 +61,31 @@ struct PackageToJS { var testJsArguments: [String] = [] var testLibraryArguments: [String] = [] if testOptions.listTests { - testLibraryArguments += ["--list-tests"] + testLibraryArguments.append("--list-tests") } if let prelude = testOptions.prelude { let preludeURL = URL(fileURLWithPath: prelude, relativeTo: URL(fileURLWithPath: FileManager.default.currentDirectoryPath)) - testJsArguments += ["--prelude", preludeURL.path] + testJsArguments.append("--prelude") + testJsArguments.append(preludeURL.path) } if let environment = testOptions.environment { - testJsArguments += ["--environment", environment] + testJsArguments.append("--environment") + testJsArguments.append(environment) } if testOptions.inspect { - testJsArguments += ["--inspect"] + testJsArguments.append("--inspect") } let xctestCoverageFile = outputDir.appending(path: "XCTest.profraw") do { var extraArguments = testJsArguments if testOptions.packageOptions.enableCodeCoverage { - extraArguments += ["--coverage-file", xctestCoverageFile.path] + extraArguments.append("--coverage-file") + extraArguments.append(xctestCoverageFile.path) } - extraArguments += ["--"] - extraArguments += testLibraryArguments - extraArguments += testOptions.filter + extraArguments.append("--") + extraArguments.append(contentsOf: testLibraryArguments) + extraArguments.append(contentsOf: testOptions.filter) try PackageToJS.runSingleTestingLibrary( testRunner: testRunner, currentDirectoryURL: currentDirectoryURL, @@ -95,11 +98,17 @@ struct PackageToJS { do { var extraArguments = testJsArguments if testOptions.packageOptions.enableCodeCoverage { - extraArguments += ["--coverage-file", swiftTestingCoverageFile.path] + 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) } - extraArguments += ["--", "--testing-library", "swift-testing"] - extraArguments += testLibraryArguments - extraArguments += testOptions.filter.flatMap { ["--filter", $0] } try PackageToJS.runSingleTestingLibrary( testRunner: testRunner, currentDirectoryURL: currentDirectoryURL, @@ -109,7 +118,7 @@ struct PackageToJS { } if testOptions.packageOptions.enableCodeCoverage { - let profrawFiles = [xctestCoverageFile, swiftTestingCoverageFile].filter { FileManager.default.fileExists(atPath: $0.path) } + let profrawFiles = [xctestCoverageFile.path, swiftTestingCoverageFile.path].filter { FileManager.default.fileExists(atPath: $0) } do { try PackageToJS.postProcessCoverageFiles(outputDir: outputDir, profrawFiles: profrawFiles) } catch { @@ -126,7 +135,11 @@ struct PackageToJS { testOptions: TestOptions ) throws { let node = try which("node") - let arguments = ["--experimental-wasi-unstable-preview1"] + testOptions.extraNodeArguments + [testRunner.path] + extraArguments + 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) @@ -160,16 +173,16 @@ struct PackageToJS { } finalize() // swift-testing returns EX_UNAVAILABLE (which is 69 in wasi-libc) for "no tests found" - guard task.terminationStatus == 0 || task.terminationStatus == 69 else { + guard [0, 69].contains(task.terminationStatus) else { throw PackageToJSError("Test failed with status \(task.terminationStatus)") } } - static func postProcessCoverageFiles(outputDir: URL, profrawFiles: [URL]) throws { + 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.map { $0.path } + let arguments = ["merge", "-sparse", "-output", mergedCoverageFile.path] + profrawFiles let llvmProfdata = try which("llvm-profdata") logCommandExecution(llvmProfdata.path, arguments) try runCommand(llvmProfdata, arguments) @@ -194,7 +207,7 @@ struct PackageToJS { func append(_ data: String) { lock.lock() defer { lock.unlock() } - buffer += data + buffer.append(data) let lines = buffer.split(separator: "\n", omittingEmptySubsequences: false) for line in lines.dropLast() { handler(String(line)) @@ -567,12 +580,12 @@ struct PackagingPlanner { } let inputPath = selfPackageDir.appending(path: file) - let conditions = [ + let conditions: [String: Bool] = [ "USE_SHARED_MEMORY": triple == "wasm32-unknown-wasip1-threads", "IS_WASI": triple.hasPrefix("wasm32-unknown-wasi"), "USE_WASI_CDN": options.useCDN, ] - let constantSubstitutions = [ + let constantSubstitutions: [String: String] = [ "PACKAGE_TO_JS_MODULE_PATH": wasmFilename, "PACKAGE_TO_JS_PACKAGE_NAME": options.packageName ?? packageId.lowercased(), ] @@ -587,11 +600,13 @@ struct PackagingPlanner { 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" } + 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)" - substitutions["PACKAGE_TO_JS_MEMORY_MAXIMUM"] = "\(type.maximum ?? type.minimum)" - substitutions["PACKAGE_TO_JS_MEMORY_SHARED"] = "\(type.shared)" + 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 } } diff --git a/Plugins/PackageToJS/Sources/ParseWasm.swift b/Plugins/PackageToJS/Sources/ParseWasm.swift index a35b69561..8cfb6c66c 100644 --- a/Plugins/PackageToJS/Sources/ParseWasm.swift +++ b/Plugins/PackageToJS/Sources/ParseWasm.swift @@ -1,7 +1,7 @@ import struct Foundation.Data /// Represents the type of value in WebAssembly -enum ValueType: String, Codable { +enum ValueType { case i32 case i64 case f32 @@ -12,18 +12,18 @@ enum ValueType: String, Codable { } /// Represents a function type in WebAssembly -struct FunctionType: Codable { +struct FunctionType { let parameters: [ValueType] let results: [ValueType] } /// Represents a table type in WebAssembly -struct TableType: Codable { +struct TableType { let element: ElementType let minimum: UInt32 let maximum: UInt32? - - enum ElementType: String, Codable { + + enum ElementType: String { case funcref case externref } @@ -35,7 +35,7 @@ struct MemoryType: Codable { let maximum: UInt32? let shared: Bool let index: IndexType - + enum IndexType: String, Codable { case i32 case i64 @@ -43,7 +43,7 @@ struct MemoryType: Codable { } /// Represents a global type in WebAssembly -struct GlobalType: Codable { +struct GlobalType { let value: ValueType let mutable: Bool } @@ -53,12 +53,12 @@ struct ImportEntry: Codable { let module: String let name: String let kind: ImportKind - + enum ImportKind: Codable { - case function(type: FunctionType) - case table(type: TableType) + case function + case table case memory(type: MemoryType) - case global(type: GlobalType) + case global } } @@ -66,16 +66,16 @@ struct ImportEntry: Codable { 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 @@ -84,7 +84,7 @@ private class ParseState { offset += 1 return byte } - + func skipBytes(_ count: Int) throws { guard offset + count <= moduleBytes.count else { throw ParseError.unexpectedEndOfData @@ -97,7 +97,7 @@ private class ParseState { var result: UInt32 = 0 var shift: UInt32 = 0 var byte: UInt8 - + repeat { byte = try readByte() result |= UInt32(byte & 0x7F) << shift @@ -106,39 +106,39 @@ private class ParseState { 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 + 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: @@ -243,7 +240,7 @@ private func parseTableType(_ parseState: ParseState) throws -> TableType { default: throw ParseError.unknownTableElementType(elementType) } - + let limits = try parseLimits(parseState) return TableType(element: element, minimum: limits.minimum, maximum: limits.maximum) } @@ -255,7 +252,7 @@ private func parseLimits(_ parseState: ParseState) throws -> MemoryType { 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) @@ -297,18 +294,18 @@ private func parseFunctionType(_ parseState: ParseState) throws -> FunctionType if form != 0x60 { throw ParseError.invalidFunctionTypeForm(form) } - + var parameters: [ValueType] = [] let parameterCount = try parseState.readUnsignedLEB128() for _ in 0..:0: error: CounterTests.testThrowFailure : threw error "TestError()"  FAILED  CounterTests - ✘ testThrowFailure (2ms) + ✘ testThrowFailure (0.002s)  FAILED  /.xctest  FAILED  All tests From 7238f897ec96315eda961e3556ee077a498d5daa Mon Sep 17 00:00:00 2001 From: Yuta Saito Date: Sun, 16 Mar 2025 13:59:17 +0000 Subject: [PATCH 289/373] Minor compile-time optimization --- Plugins/PackageToJS/Sources/MiniMake.swift | 2 +- Plugins/PackageToJS/Sources/PackageToJS.swift | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Plugins/PackageToJS/Sources/MiniMake.swift b/Plugins/PackageToJS/Sources/MiniMake.swift index 3544a80f3..7c8a320ae 100644 --- a/Plugins/PackageToJS/Sources/MiniMake.swift +++ b/Plugins/PackageToJS/Sources/MiniMake.swift @@ -109,7 +109,7 @@ struct MiniMake { mutating func addTask( inputFiles: [BuildPath] = [], inputTasks: [TaskKey] = [], output: BuildPath, attributes: [TaskAttribute] = [], salt: (any Encodable)? = nil, - build: @escaping (_ task: Task, _ scope: VariableScope) throws -> Void + build: @escaping (_ task: Task, _ scope: VariableScope) throws -> Void = { _, _ in } ) -> TaskKey { let taskKey = TaskKey(id: output.description) let saltData = try! salt.map { diff --git a/Plugins/PackageToJS/Sources/PackageToJS.swift b/Plugins/PackageToJS/Sources/PackageToJS.swift index 03af2c73b..59d2f0c29 100644 --- a/Plugins/PackageToJS/Sources/PackageToJS.swift +++ b/Plugins/PackageToJS/Sources/PackageToJS.swift @@ -394,7 +394,7 @@ struct PackagingPlanner { ) return make.addTask( inputTasks: allTasks, output: BuildPath(phony: "all"), attributes: [.phony, .silent] - ) { _, _ in } + ) } private func planBuildInternal( @@ -560,7 +560,7 @@ struct PackagingPlanner { } let rootTask = make.addTask( inputTasks: allTasks, output: BuildPath(phony: "all"), attributes: [.phony, .silent] - ) { _, _ in } + ) return (rootTask, binDir) } From e52cb5d6f60dbf3046e6c3816f5ed7b5792071f0 Mon Sep 17 00:00:00 2001 From: Yuta Saito Date: Sun, 16 Mar 2025 23:17:07 +0900 Subject: [PATCH 290/373] [skip ci] Update README.md --- Plugins/PackageToJS/README.md | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/Plugins/PackageToJS/README.md b/Plugins/PackageToJS/README.md index 0681024b4..8b1821187 100644 --- a/Plugins/PackageToJS/README.md +++ b/Plugins/PackageToJS/README.md @@ -8,17 +8,19 @@ PackageToJS is a command plugin for Swift Package Manager that simplifies the pr ## Features -- Build Swift packages for WebAssembly targets -- Generate JavaScript wrapper code for Swift WebAssembly modules -- Support for testing Swift WebAssembly code -- Diagnostic helpers for common build issues -- Options for optimization and debug information management +- 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: From 80077f539e85cc0d348b55c696bac2bde1a05f97 Mon Sep 17 00:00:00 2001 From: Yuta Saito Date: Sun, 16 Mar 2025 14:47:54 +0000 Subject: [PATCH 291/373] Remove `--split-debug` mode The split-out DWARF can't be used anyway because wasm-opt invalidates it during optimization. --- Plugins/PackageToJS/Sources/PackageToJS.swift | 20 +- .../Sources/PackageToJSPlugin.swift | 4 +- .../Tests/PackagingPlannerTests.swift | 10 +- .../planBuild_release.json | 6 +- .../planBuild_release_split_debug.json | 290 ------------------ 5 files changed, 16 insertions(+), 314 deletions(-) delete mode 100644 Plugins/PackageToJS/Tests/__Snapshots__/PackagingPlannerTests/planBuild_release_split_debug.json diff --git a/Plugins/PackageToJS/Sources/PackageToJS.swift b/Plugins/PackageToJS/Sources/PackageToJS.swift index 59d2f0c29..b778cbce1 100644 --- a/Plugins/PackageToJS/Sources/PackageToJS.swift +++ b/Plugins/PackageToJS/Sources/PackageToJS.swift @@ -17,8 +17,6 @@ struct PackageToJS { struct BuildOptions { /// Product to build (default: executable target if there's only one) var product: String? - /// Whether to split debug information into a separate file (default: false) - var splitDebug: Bool /// Whether to apply wasm-opt optimizations in release mode (default: true) var noOptimize: Bool /// The options for packaging @@ -390,7 +388,7 @@ struct PackagingPlanner { buildOptions: PackageToJS.BuildOptions ) throws -> MiniMake.TaskKey { let (allTasks, _, _, _) = try planBuildInternal( - make: &make, splitDebug: buildOptions.splitDebug, noOptimize: buildOptions.noOptimize + make: &make, noOptimize: buildOptions.noOptimize ) return make.addTask( inputTasks: allTasks, output: BuildPath(phony: "all"), attributes: [.phony, .silent] @@ -399,7 +397,7 @@ struct PackagingPlanner { private func planBuildInternal( make: inout MiniMake, - splitDebug: Bool, noOptimize: Bool + noOptimize: Bool ) throws -> ( allTasks: [MiniMake.TaskKey], outputDirTask: MiniMake.TaskKey, @@ -435,25 +433,23 @@ struct PackagingPlanner { if shouldOptimize { // Optimize the wasm in release mode - // If splitDebug is true, we need to place the DWARF-stripped wasm file (but "name" section remains) - // in the output directory. - let stripWasmPath = (splitDebug ? outputDir : intermediatesDir).appending(path: wasmFilename + ".debug") + let wasmWithoutDwarfPath = intermediatesDir.appending(path: wasmFilename + ".no-dwarf") // First, strip DWARF sections as their existence enables DWARF preserving mode in wasm-opt - let stripWasm = make.addTask( + let wasmWithoutDwarf = make.addTask( inputFiles: [selfPath, wasmProductArtifact], inputTasks: [outputDirTask, intermediatesDirTask], - output: stripWasmPath + output: wasmWithoutDwarfPath ) { 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, stripWasmPath], inputTasks: [outputDirTask, stripWasm], + inputFiles: [selfPath, wasmWithoutDwarfPath], inputTasks: [outputDirTask, wasmWithoutDwarf], output: finalWasmPath ) { print("Optimizing the wasm file...") - try system.wasmOpt(["-Os"], input: $1.resolve(path: stripWasmPath).path, output: $1.resolve(path: $0.output).path) + try system.wasmOpt(["-Os", "--debuginfo"], input: $1.resolve(path: wasmWithoutDwarfPath).path, output: $1.resolve(path: $0.output).path) } } else { // Copy the wasm product artifact @@ -522,7 +518,7 @@ struct PackagingPlanner { make: inout MiniMake ) throws -> (rootTask: MiniMake.TaskKey, binDir: BuildPath) { var (allTasks, outputDirTask, intermediatesDirTask, packageJsonTask) = try planBuildInternal( - make: &make, splitDebug: false, noOptimize: false + make: &make, noOptimize: false ) // Install npm dependencies used in the test harness diff --git a/Plugins/PackageToJS/Sources/PackageToJSPlugin.swift b/Plugins/PackageToJS/Sources/PackageToJSPlugin.swift index 9013b26e6..fc6f6ad90 100644 --- a/Plugins/PackageToJS/Sources/PackageToJSPlugin.swift +++ b/Plugins/PackageToJS/Sources/PackageToJSPlugin.swift @@ -294,10 +294,9 @@ extension PackageToJS.PackageOptions { extension PackageToJS.BuildOptions { static func parse(from extractor: inout ArgumentExtractor) -> PackageToJS.BuildOptions { let product = extractor.extractOption(named: "product").last - let splitDebug = extractor.extractFlag(named: "split-debug") let noOptimize = extractor.extractFlag(named: "no-optimize") let packageOptions = PackageToJS.PackageOptions.parse(from: &extractor) - return PackageToJS.BuildOptions(product: product, splitDebug: splitDebug != 0, noOptimize: noOptimize != 0, packageOptions: packageOptions) + return PackageToJS.BuildOptions(product: product, noOptimize: noOptimize != 0, packageOptions: packageOptions) } static func help() -> String { @@ -311,7 +310,6 @@ extension PackageToJS.BuildOptions { --output Path to the output directory (default: .build/plugins/PackageToJS/outputs/Package) --package-name Name of the package (default: lowercased Package.swift name) --explain Whether to explain the build plan (default: false) - --split-debug Whether to split debug information into a separate .wasm.debug file (default: false) --no-optimize Whether to disable wasm-opt optimization (default: false) --use-cdn Whether to use CDN for dependency packages (default: false) --enable-code-coverage Whether to enable code coverage collection (default: false) diff --git a/Plugins/PackageToJS/Tests/PackagingPlannerTests.swift b/Plugins/PackageToJS/Tests/PackagingPlannerTests.swift index 7269bea2d..047eb50b7 100644 --- a/Plugins/PackageToJS/Tests/PackagingPlannerTests.swift +++ b/Plugins/PackageToJS/Tests/PackagingPlannerTests.swift @@ -35,12 +35,11 @@ import Testing } @Test(arguments: [ - (variant: "debug", configuration: "debug", splitDebug: false, noOptimize: false), - (variant: "release", configuration: "release", splitDebug: false, noOptimize: false), - (variant: "release_split_debug", configuration: "release", splitDebug: true, noOptimize: false), - (variant: "release_no_optimize", configuration: "release", splitDebug: false, noOptimize: true), + (variant: "debug", configuration: "debug", noOptimize: false), + (variant: "release", configuration: "release", noOptimize: false), + (variant: "release_no_optimize", configuration: "release", noOptimize: true), ]) - func planBuild(variant: String, configuration: String, splitDebug: Bool, noOptimize: Bool) throws { + func planBuild(variant: String, configuration: String, noOptimize: Bool) throws { let options = PackageToJS.PackageOptions() let system = TestPackagingSystem() let planner = PackagingPlanner( @@ -60,7 +59,6 @@ import Testing make: &make, buildOptions: PackageToJS.BuildOptions( product: "test", - splitDebug: splitDebug, noOptimize: noOptimize, packageOptions: options ) diff --git a/Plugins/PackageToJS/Tests/__Snapshots__/PackagingPlannerTests/planBuild_release.json b/Plugins/PackageToJS/Tests/__Snapshots__/PackagingPlannerTests/planBuild_release.json index bb2c3f74b..889789bd9 100644 --- a/Plugins/PackageToJS/Tests/__Snapshots__/PackagingPlannerTests/planBuild_release.json +++ b/Plugins/PackageToJS/Tests/__Snapshots__/PackagingPlannerTests/planBuild_release.json @@ -19,7 +19,7 @@ "$PLANNER_SOURCE_PATH", "$WASM_PRODUCT_ARTIFACT" ], - "output" : "$INTERMEDIATES\/main.wasm.debug", + "output" : "$INTERMEDIATES\/main.wasm.no-dwarf", "wants" : [ "$OUTPUT", "$INTERMEDIATES" @@ -126,12 +126,12 @@ ], "inputs" : [ "$PLANNER_SOURCE_PATH", - "$INTERMEDIATES\/main.wasm.debug" + "$INTERMEDIATES\/main.wasm.no-dwarf" ], "output" : "$OUTPUT\/main.wasm", "wants" : [ "$OUTPUT", - "$INTERMEDIATES\/main.wasm.debug" + "$INTERMEDIATES\/main.wasm.no-dwarf" ] }, { diff --git a/Plugins/PackageToJS/Tests/__Snapshots__/PackagingPlannerTests/planBuild_release_split_debug.json b/Plugins/PackageToJS/Tests/__Snapshots__/PackagingPlannerTests/planBuild_release_split_debug.json deleted file mode 100644 index b18680f8d..000000000 --- a/Plugins/PackageToJS/Tests/__Snapshots__/PackagingPlannerTests/planBuild_release_split_debug.json +++ /dev/null @@ -1,290 +0,0 @@ -[ - { - "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" : "eyJjb25kaXRpb25zIjp7IklTX1dBU0kiOnRydWUsIlVTRV9TSEFSRURfTUVNT1JZIjpmYWxzZSwiVVNFX1dBU0lfQ0ROIjpmYWxzZX0sInN1YnN0aXR1dGlvbnMiOnsiUEFDS0FHRV9UT19KU19NT0RVTEVfUEFUSCI6Im1haW4ud2FzbSIsIlBBQ0tBR0VfVE9fSlNfUEFDS0FHRV9OQU1FIjoidGVzdCJ9fQ==", - "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" : "eyJjb25kaXRpb25zIjp7IklTX1dBU0kiOnRydWUsIlVTRV9TSEFSRURfTUVNT1JZIjpmYWxzZSwiVVNFX1dBU0lfQ0ROIjpmYWxzZX0sInN1YnN0aXR1dGlvbnMiOnsiUEFDS0FHRV9UT19KU19NT0RVTEVfUEFUSCI6Im1haW4ud2FzbSIsIlBBQ0tBR0VfVE9fSlNfUEFDS0FHRV9OQU1FIjoidGVzdCJ9fQ==", - "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" : "eyJjb25kaXRpb25zIjp7IklTX1dBU0kiOnRydWUsIlVTRV9TSEFSRURfTUVNT1JZIjpmYWxzZSwiVVNFX1dBU0lfQ0ROIjpmYWxzZX0sInN1YnN0aXR1dGlvbnMiOnsiUEFDS0FHRV9UT19KU19NT0RVTEVfUEFUSCI6Im1haW4ud2FzbSIsIlBBQ0tBR0VfVE9fSlNfUEFDS0FHRV9OQU1FIjoidGVzdCJ9fQ==", - "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" : "eyJjb25kaXRpb25zIjp7IklTX1dBU0kiOnRydWUsIlVTRV9TSEFSRURfTUVNT1JZIjpmYWxzZSwiVVNFX1dBU0lfQ0ROIjpmYWxzZX0sInN1YnN0aXR1dGlvbnMiOnsiUEFDS0FHRV9UT19KU19NT0RVTEVfUEFUSCI6Im1haW4ud2FzbSIsIlBBQ0tBR0VfVE9fSlNfUEFDS0FHRV9OQU1FIjoidGVzdCJ9fQ==", - "wants" : [ - "$OUTPUT", - "$OUTPUT\/platforms", - "$INTERMEDIATES\/wasm-imports.json" - ] - }, - { - "attributes" : [ - - ], - "inputs" : [ - "$PLANNER_SOURCE_PATH", - "$OUTPUT\/main.wasm.debug" - ], - "output" : "$OUTPUT\/main.wasm", - "wants" : [ - "$OUTPUT", - "$OUTPUT\/main.wasm.debug" - ] - }, - { - "attributes" : [ - - ], - "inputs" : [ - "$PLANNER_SOURCE_PATH", - "$WASM_PRODUCT_ARTIFACT" - ], - "output" : "$OUTPUT\/main.wasm.debug", - "wants" : [ - "$OUTPUT", - "$INTERMEDIATES" - ] - }, - { - "attributes" : [ - - ], - "inputs" : [ - "$PLANNER_SOURCE_PATH", - "$SELF_PACKAGE\/Plugins\/PackageToJS\/Templates\/package.json" - ], - "output" : "$OUTPUT\/package.json", - "salt" : "eyJjb25kaXRpb25zIjp7IklTX1dBU0kiOnRydWUsIlVTRV9TSEFSRURfTUVNT1JZIjpmYWxzZSwiVVNFX1dBU0lfQ0ROIjpmYWxzZX0sInN1YnN0aXR1dGlvbnMiOnsiUEFDS0FHRV9UT19KU19NT0RVTEVfUEFUSCI6Im1haW4ud2FzbSIsIlBBQ0tBR0VfVE9fSlNfUEFDS0FHRV9OQU1FIjoidGVzdCJ9fQ==", - "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" : "eyJjb25kaXRpb25zIjp7IklTX1dBU0kiOnRydWUsIlVTRV9TSEFSRURfTUVNT1JZIjpmYWxzZSwiVVNFX1dBU0lfQ0ROIjpmYWxzZX0sInN1YnN0aXR1dGlvbnMiOnsiUEFDS0FHRV9UT19KU19NT0RVTEVfUEFUSCI6Im1haW4ud2FzbSIsIlBBQ0tBR0VfVE9fSlNfUEFDS0FHRV9OQU1FIjoidGVzdCJ9fQ==", - "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" : "eyJjb25kaXRpb25zIjp7IklTX1dBU0kiOnRydWUsIlVTRV9TSEFSRURfTUVNT1JZIjpmYWxzZSwiVVNFX1dBU0lfQ0ROIjpmYWxzZX0sInN1YnN0aXR1dGlvbnMiOnsiUEFDS0FHRV9UT19KU19NT0RVTEVfUEFUSCI6Im1haW4ud2FzbSIsIlBBQ0tBR0VfVE9fSlNfUEFDS0FHRV9OQU1FIjoidGVzdCJ9fQ==", - "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" : "eyJjb25kaXRpb25zIjp7IklTX1dBU0kiOnRydWUsIlVTRV9TSEFSRURfTUVNT1JZIjpmYWxzZSwiVVNFX1dBU0lfQ0ROIjpmYWxzZX0sInN1YnN0aXR1dGlvbnMiOnsiUEFDS0FHRV9UT19KU19NT0RVTEVfUEFUSCI6Im1haW4ud2FzbSIsIlBBQ0tBR0VfVE9fSlNfUEFDS0FHRV9OQU1FIjoidGVzdCJ9fQ==", - "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" : "eyJjb25kaXRpb25zIjp7IklTX1dBU0kiOnRydWUsIlVTRV9TSEFSRURfTUVNT1JZIjpmYWxzZSwiVVNFX1dBU0lfQ0ROIjpmYWxzZX0sInN1YnN0aXR1dGlvbnMiOnsiUEFDS0FHRV9UT19KU19NT0RVTEVfUEFUSCI6Im1haW4ud2FzbSIsIlBBQ0tBR0VfVE9fSlNfUEFDS0FHRV9OQU1FIjoidGVzdCJ9fQ==", - "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" : "eyJjb25kaXRpb25zIjp7IklTX1dBU0kiOnRydWUsIlVTRV9TSEFSRURfTUVNT1JZIjpmYWxzZSwiVVNFX1dBU0lfQ0ROIjpmYWxzZX0sInN1YnN0aXR1dGlvbnMiOnsiUEFDS0FHRV9UT19KU19NT0RVTEVfUEFUSCI6Im1haW4ud2FzbSIsIlBBQ0tBR0VfVE9fSlNfUEFDS0FHRV9OQU1FIjoidGVzdCJ9fQ==", - "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" : "eyJjb25kaXRpb25zIjp7IklTX1dBU0kiOnRydWUsIlVTRV9TSEFSRURfTUVNT1JZIjpmYWxzZSwiVVNFX1dBU0lfQ0ROIjpmYWxzZX0sInN1YnN0aXR1dGlvbnMiOnsiUEFDS0FHRV9UT19KU19NT0RVTEVfUEFUSCI6Im1haW4ud2FzbSIsIlBBQ0tBR0VfVE9fSlNfUEFDS0FHRV9OQU1FIjoidGVzdCJ9fQ==", - "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" - ] - } -] \ No newline at end of file From 2c515b5fd3b8f3e62237194b1b42834ae3c0e41f Mon Sep 17 00:00:00 2001 From: Yuta Saito Date: Mon, 17 Mar 2025 05:17:02 +0000 Subject: [PATCH 292/373] PackageToJS: Add `--debug-info-format` option --- Plugins/PackageToJS/Sources/PackageToJS.swift | 50 ++- .../Sources/PackageToJSPlugin.swift | 11 +- Plugins/PackageToJS/Tests/ExampleTests.swift | 2 + .../Tests/PackagingPlannerTests.swift | 13 +- .../planBuild_release_dwarf.json | 275 +++++++++++++++++ .../planBuild_release_name.json | 290 ++++++++++++++++++ 6 files changed, 621 insertions(+), 20 deletions(-) create mode 100644 Plugins/PackageToJS/Tests/__Snapshots__/PackagingPlannerTests/planBuild_release_dwarf.json create mode 100644 Plugins/PackageToJS/Tests/__Snapshots__/PackagingPlannerTests/planBuild_release_name.json diff --git a/Plugins/PackageToJS/Sources/PackageToJS.swift b/Plugins/PackageToJS/Sources/PackageToJS.swift index b778cbce1..cc0c02182 100644 --- a/Plugins/PackageToJS/Sources/PackageToJS.swift +++ b/Plugins/PackageToJS/Sources/PackageToJS.swift @@ -14,11 +14,22 @@ struct PackageToJS { 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 } @@ -388,7 +399,7 @@ struct PackagingPlanner { buildOptions: PackageToJS.BuildOptions ) throws -> MiniMake.TaskKey { let (allTasks, _, _, _) = try planBuildInternal( - make: &make, noOptimize: buildOptions.noOptimize + make: &make, noOptimize: buildOptions.noOptimize, debugInfoFormat: buildOptions.debugInfoFormat ) return make.addTask( inputTasks: allTasks, output: BuildPath(phony: "all"), attributes: [.phony, .silent] @@ -397,7 +408,8 @@ struct PackagingPlanner { private func planBuildInternal( make: inout MiniMake, - noOptimize: Bool + noOptimize: Bool, + debugInfoFormat: PackageToJS.DebugInfoFormat ) throws -> ( allTasks: [MiniMake.TaskKey], outputDirTask: MiniMake.TaskKey, @@ -432,24 +444,32 @@ struct PackagingPlanner { let finalWasmPath = outputDir.appending(path: wasmFilename) if shouldOptimize { - // Optimize the wasm in release mode - let wasmWithoutDwarfPath = intermediatesDir.appending(path: wasmFilename + ".no-dwarf") - - // First, strip DWARF sections as their existence enables DWARF preserving mode in wasm-opt - let wasmWithoutDwarf = make.addTask( - inputFiles: [selfPath, wasmProductArtifact], inputTasks: [outputDirTask, intermediatesDirTask], - output: wasmWithoutDwarfPath - ) { - print("Stripping DWARF debug info...") - try system.wasmOpt(["--strip-dwarf", "--debuginfo"], input: $1.resolve(path: wasmProductArtifact).path, output: $1.resolve(path: $0.output).path) + 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, wasmWithoutDwarfPath], inputTasks: [outputDirTask, wasmWithoutDwarf], + inputFiles: [selfPath, wasmOptInputFile], inputTasks: [outputDirTask] + (wasmOptInputTask.map { [$0] } ?? []), output: finalWasmPath ) { print("Optimizing the wasm file...") - try system.wasmOpt(["-Os", "--debuginfo"], input: $1.resolve(path: wasmWithoutDwarfPath).path, output: $1.resolve(path: $0.output).path) + try system.wasmOpt(["-Os"] + (debugInfoFormat != .none ? ["--debuginfo"] : []), input: $1.resolve(path: wasmOptInputFile).path, output: $1.resolve(path: $0.output).path) } } else { // Copy the wasm product artifact @@ -518,7 +538,7 @@ struct PackagingPlanner { make: inout MiniMake ) throws -> (rootTask: MiniMake.TaskKey, binDir: BuildPath) { var (allTasks, outputDirTask, intermediatesDirTask, packageJsonTask) = try planBuildInternal( - make: &make, noOptimize: false + make: &make, noOptimize: false, debugInfoFormat: .dwarf ) // Install npm dependencies used in the test harness diff --git a/Plugins/PackageToJS/Sources/PackageToJSPlugin.swift b/Plugins/PackageToJS/Sources/PackageToJSPlugin.swift index fc6f6ad90..4bf6a1106 100644 --- a/Plugins/PackageToJS/Sources/PackageToJSPlugin.swift +++ b/Plugins/PackageToJS/Sources/PackageToJSPlugin.swift @@ -295,8 +295,16 @@ 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, packageOptions: packageOptions) + return PackageToJS.BuildOptions(product: product, noOptimize: noOptimize != 0, debugInfoFormat: debugInfoFormat, packageOptions: packageOptions) } static func help() -> String { @@ -313,6 +321,7 @@ extension PackageToJS.BuildOptions { --no-optimize Whether to disable wasm-opt optimization (default: false) --use-cdn Whether to use CDN for dependency packages (default: false) --enable-code-coverage Whether to enable code coverage collection (default: false) + --debug-info-format The format of debug info to keep in the final wasm file (values: none, dwarf, name; default: none) SUBCOMMANDS: test Builds and runs tests diff --git a/Plugins/PackageToJS/Tests/ExampleTests.swift b/Plugins/PackageToJS/Tests/ExampleTests.swift index 53048e000..90a20e5a4 100644 --- a/Plugins/PackageToJS/Tests/ExampleTests.swift +++ b/Plugins/PackageToJS/Tests/ExampleTests.swift @@ -139,6 +139,8 @@ extension Trait where Self == ConditionTrait { 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"], [:]) } } diff --git a/Plugins/PackageToJS/Tests/PackagingPlannerTests.swift b/Plugins/PackageToJS/Tests/PackagingPlannerTests.swift index 047eb50b7..1b1eb1abf 100644 --- a/Plugins/PackageToJS/Tests/PackagingPlannerTests.swift +++ b/Plugins/PackageToJS/Tests/PackagingPlannerTests.swift @@ -34,12 +34,16 @@ import Testing ) } + typealias DebugInfoFormat = PackageToJS.DebugInfoFormat + @Test(arguments: [ - (variant: "debug", configuration: "debug", noOptimize: false), - (variant: "release", configuration: "release", noOptimize: false), - (variant: "release_no_optimize", configuration: "release", noOptimize: true), + (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) throws { + func planBuild(variant: String, configuration: String, noOptimize: Bool, debugInfoFormat: PackageToJS.DebugInfoFormat) throws { let options = PackageToJS.PackageOptions() let system = TestPackagingSystem() let planner = PackagingPlanner( @@ -60,6 +64,7 @@ import Testing buildOptions: PackageToJS.BuildOptions( product: "test", noOptimize: noOptimize, + debugInfoFormat: debugInfoFormat, packageOptions: options ) ) 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..0b1b2ac80 --- /dev/null +++ b/Plugins/PackageToJS/Tests/__Snapshots__/PackagingPlannerTests/planBuild_release_dwarf.json @@ -0,0 +1,275 @@ +[ + { + "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" : "eyJjb25kaXRpb25zIjp7IklTX1dBU0kiOnRydWUsIlVTRV9TSEFSRURfTUVNT1JZIjpmYWxzZSwiVVNFX1dBU0lfQ0ROIjpmYWxzZX0sInN1YnN0aXR1dGlvbnMiOnsiUEFDS0FHRV9UT19KU19NT0RVTEVfUEFUSCI6Im1haW4ud2FzbSIsIlBBQ0tBR0VfVE9fSlNfUEFDS0FHRV9OQU1FIjoidGVzdCJ9fQ==", + "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" : "eyJjb25kaXRpb25zIjp7IklTX1dBU0kiOnRydWUsIlVTRV9TSEFSRURfTUVNT1JZIjpmYWxzZSwiVVNFX1dBU0lfQ0ROIjpmYWxzZX0sInN1YnN0aXR1dGlvbnMiOnsiUEFDS0FHRV9UT19KU19NT0RVTEVfUEFUSCI6Im1haW4ud2FzbSIsIlBBQ0tBR0VfVE9fSlNfUEFDS0FHRV9OQU1FIjoidGVzdCJ9fQ==", + "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" : "eyJjb25kaXRpb25zIjp7IklTX1dBU0kiOnRydWUsIlVTRV9TSEFSRURfTUVNT1JZIjpmYWxzZSwiVVNFX1dBU0lfQ0ROIjpmYWxzZX0sInN1YnN0aXR1dGlvbnMiOnsiUEFDS0FHRV9UT19KU19NT0RVTEVfUEFUSCI6Im1haW4ud2FzbSIsIlBBQ0tBR0VfVE9fSlNfUEFDS0FHRV9OQU1FIjoidGVzdCJ9fQ==", + "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" : "eyJjb25kaXRpb25zIjp7IklTX1dBU0kiOnRydWUsIlVTRV9TSEFSRURfTUVNT1JZIjpmYWxzZSwiVVNFX1dBU0lfQ0ROIjpmYWxzZX0sInN1YnN0aXR1dGlvbnMiOnsiUEFDS0FHRV9UT19KU19NT0RVTEVfUEFUSCI6Im1haW4ud2FzbSIsIlBBQ0tBR0VfVE9fSlNfUEFDS0FHRV9OQU1FIjoidGVzdCJ9fQ==", + "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" : "eyJjb25kaXRpb25zIjp7IklTX1dBU0kiOnRydWUsIlVTRV9TSEFSRURfTUVNT1JZIjpmYWxzZSwiVVNFX1dBU0lfQ0ROIjpmYWxzZX0sInN1YnN0aXR1dGlvbnMiOnsiUEFDS0FHRV9UT19KU19NT0RVTEVfUEFUSCI6Im1haW4ud2FzbSIsIlBBQ0tBR0VfVE9fSlNfUEFDS0FHRV9OQU1FIjoidGVzdCJ9fQ==", + "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" : "eyJjb25kaXRpb25zIjp7IklTX1dBU0kiOnRydWUsIlVTRV9TSEFSRURfTUVNT1JZIjpmYWxzZSwiVVNFX1dBU0lfQ0ROIjpmYWxzZX0sInN1YnN0aXR1dGlvbnMiOnsiUEFDS0FHRV9UT19KU19NT0RVTEVfUEFUSCI6Im1haW4ud2FzbSIsIlBBQ0tBR0VfVE9fSlNfUEFDS0FHRV9OQU1FIjoidGVzdCJ9fQ==", + "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" : "eyJjb25kaXRpb25zIjp7IklTX1dBU0kiOnRydWUsIlVTRV9TSEFSRURfTUVNT1JZIjpmYWxzZSwiVVNFX1dBU0lfQ0ROIjpmYWxzZX0sInN1YnN0aXR1dGlvbnMiOnsiUEFDS0FHRV9UT19KU19NT0RVTEVfUEFUSCI6Im1haW4ud2FzbSIsIlBBQ0tBR0VfVE9fSlNfUEFDS0FHRV9OQU1FIjoidGVzdCJ9fQ==", + "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" : "eyJjb25kaXRpb25zIjp7IklTX1dBU0kiOnRydWUsIlVTRV9TSEFSRURfTUVNT1JZIjpmYWxzZSwiVVNFX1dBU0lfQ0ROIjpmYWxzZX0sInN1YnN0aXR1dGlvbnMiOnsiUEFDS0FHRV9UT19KU19NT0RVTEVfUEFUSCI6Im1haW4ud2FzbSIsIlBBQ0tBR0VfVE9fSlNfUEFDS0FHRV9OQU1FIjoidGVzdCJ9fQ==", + "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" : "eyJjb25kaXRpb25zIjp7IklTX1dBU0kiOnRydWUsIlVTRV9TSEFSRURfTUVNT1JZIjpmYWxzZSwiVVNFX1dBU0lfQ0ROIjpmYWxzZX0sInN1YnN0aXR1dGlvbnMiOnsiUEFDS0FHRV9UT19KU19NT0RVTEVfUEFUSCI6Im1haW4ud2FzbSIsIlBBQ0tBR0VfVE9fSlNfUEFDS0FHRV9OQU1FIjoidGVzdCJ9fQ==", + "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" : "eyJjb25kaXRpb25zIjp7IklTX1dBU0kiOnRydWUsIlVTRV9TSEFSRURfTUVNT1JZIjpmYWxzZSwiVVNFX1dBU0lfQ0ROIjpmYWxzZX0sInN1YnN0aXR1dGlvbnMiOnsiUEFDS0FHRV9UT19KU19NT0RVTEVfUEFUSCI6Im1haW4ud2FzbSIsIlBBQ0tBR0VfVE9fSlNfUEFDS0FHRV9OQU1FIjoidGVzdCJ9fQ==", + "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" : "eyJjb25kaXRpb25zIjp7IklTX1dBU0kiOnRydWUsIlVTRV9TSEFSRURfTUVNT1JZIjpmYWxzZSwiVVNFX1dBU0lfQ0ROIjpmYWxzZX0sInN1YnN0aXR1dGlvbnMiOnsiUEFDS0FHRV9UT19KU19NT0RVTEVfUEFUSCI6Im1haW4ud2FzbSIsIlBBQ0tBR0VfVE9fSlNfUEFDS0FHRV9OQU1FIjoidGVzdCJ9fQ==", + "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" + ] + } +] \ 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..889789bd9 --- /dev/null +++ b/Plugins/PackageToJS/Tests/__Snapshots__/PackagingPlannerTests/planBuild_release_name.json @@ -0,0 +1,290 @@ +[ + { + "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" : "eyJjb25kaXRpb25zIjp7IklTX1dBU0kiOnRydWUsIlVTRV9TSEFSRURfTUVNT1JZIjpmYWxzZSwiVVNFX1dBU0lfQ0ROIjpmYWxzZX0sInN1YnN0aXR1dGlvbnMiOnsiUEFDS0FHRV9UT19KU19NT0RVTEVfUEFUSCI6Im1haW4ud2FzbSIsIlBBQ0tBR0VfVE9fSlNfUEFDS0FHRV9OQU1FIjoidGVzdCJ9fQ==", + "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" : "eyJjb25kaXRpb25zIjp7IklTX1dBU0kiOnRydWUsIlVTRV9TSEFSRURfTUVNT1JZIjpmYWxzZSwiVVNFX1dBU0lfQ0ROIjpmYWxzZX0sInN1YnN0aXR1dGlvbnMiOnsiUEFDS0FHRV9UT19KU19NT0RVTEVfUEFUSCI6Im1haW4ud2FzbSIsIlBBQ0tBR0VfVE9fSlNfUEFDS0FHRV9OQU1FIjoidGVzdCJ9fQ==", + "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" : "eyJjb25kaXRpb25zIjp7IklTX1dBU0kiOnRydWUsIlVTRV9TSEFSRURfTUVNT1JZIjpmYWxzZSwiVVNFX1dBU0lfQ0ROIjpmYWxzZX0sInN1YnN0aXR1dGlvbnMiOnsiUEFDS0FHRV9UT19KU19NT0RVTEVfUEFUSCI6Im1haW4ud2FzbSIsIlBBQ0tBR0VfVE9fSlNfUEFDS0FHRV9OQU1FIjoidGVzdCJ9fQ==", + "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" : "eyJjb25kaXRpb25zIjp7IklTX1dBU0kiOnRydWUsIlVTRV9TSEFSRURfTUVNT1JZIjpmYWxzZSwiVVNFX1dBU0lfQ0ROIjpmYWxzZX0sInN1YnN0aXR1dGlvbnMiOnsiUEFDS0FHRV9UT19KU19NT0RVTEVfUEFUSCI6Im1haW4ud2FzbSIsIlBBQ0tBR0VfVE9fSlNfUEFDS0FHRV9OQU1FIjoidGVzdCJ9fQ==", + "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" : "eyJjb25kaXRpb25zIjp7IklTX1dBU0kiOnRydWUsIlVTRV9TSEFSRURfTUVNT1JZIjpmYWxzZSwiVVNFX1dBU0lfQ0ROIjpmYWxzZX0sInN1YnN0aXR1dGlvbnMiOnsiUEFDS0FHRV9UT19KU19NT0RVTEVfUEFUSCI6Im1haW4ud2FzbSIsIlBBQ0tBR0VfVE9fSlNfUEFDS0FHRV9OQU1FIjoidGVzdCJ9fQ==", + "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" : "eyJjb25kaXRpb25zIjp7IklTX1dBU0kiOnRydWUsIlVTRV9TSEFSRURfTUVNT1JZIjpmYWxzZSwiVVNFX1dBU0lfQ0ROIjpmYWxzZX0sInN1YnN0aXR1dGlvbnMiOnsiUEFDS0FHRV9UT19KU19NT0RVTEVfUEFUSCI6Im1haW4ud2FzbSIsIlBBQ0tBR0VfVE9fSlNfUEFDS0FHRV9OQU1FIjoidGVzdCJ9fQ==", + "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" : "eyJjb25kaXRpb25zIjp7IklTX1dBU0kiOnRydWUsIlVTRV9TSEFSRURfTUVNT1JZIjpmYWxzZSwiVVNFX1dBU0lfQ0ROIjpmYWxzZX0sInN1YnN0aXR1dGlvbnMiOnsiUEFDS0FHRV9UT19KU19NT0RVTEVfUEFUSCI6Im1haW4ud2FzbSIsIlBBQ0tBR0VfVE9fSlNfUEFDS0FHRV9OQU1FIjoidGVzdCJ9fQ==", + "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" : "eyJjb25kaXRpb25zIjp7IklTX1dBU0kiOnRydWUsIlVTRV9TSEFSRURfTUVNT1JZIjpmYWxzZSwiVVNFX1dBU0lfQ0ROIjpmYWxzZX0sInN1YnN0aXR1dGlvbnMiOnsiUEFDS0FHRV9UT19KU19NT0RVTEVfUEFUSCI6Im1haW4ud2FzbSIsIlBBQ0tBR0VfVE9fSlNfUEFDS0FHRV9OQU1FIjoidGVzdCJ9fQ==", + "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" : "eyJjb25kaXRpb25zIjp7IklTX1dBU0kiOnRydWUsIlVTRV9TSEFSRURfTUVNT1JZIjpmYWxzZSwiVVNFX1dBU0lfQ0ROIjpmYWxzZX0sInN1YnN0aXR1dGlvbnMiOnsiUEFDS0FHRV9UT19KU19NT0RVTEVfUEFUSCI6Im1haW4ud2FzbSIsIlBBQ0tBR0VfVE9fSlNfUEFDS0FHRV9OQU1FIjoidGVzdCJ9fQ==", + "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" : "eyJjb25kaXRpb25zIjp7IklTX1dBU0kiOnRydWUsIlVTRV9TSEFSRURfTUVNT1JZIjpmYWxzZSwiVVNFX1dBU0lfQ0ROIjpmYWxzZX0sInN1YnN0aXR1dGlvbnMiOnsiUEFDS0FHRV9UT19KU19NT0RVTEVfUEFUSCI6Im1haW4ud2FzbSIsIlBBQ0tBR0VfVE9fSlNfUEFDS0FHRV9OQU1FIjoidGVzdCJ9fQ==", + "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" : "eyJjb25kaXRpb25zIjp7IklTX1dBU0kiOnRydWUsIlVTRV9TSEFSRURfTUVNT1JZIjpmYWxzZSwiVVNFX1dBU0lfQ0ROIjpmYWxzZX0sInN1YnN0aXR1dGlvbnMiOnsiUEFDS0FHRV9UT19KU19NT0RVTEVfUEFUSCI6Im1haW4ud2FzbSIsIlBBQ0tBR0VfVE9fSlNfUEFDS0FHRV9OQU1FIjoidGVzdCJ9fQ==", + "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" + ] + } +] \ No newline at end of file From ec9470c122bf7ffed0834a13e5c848340858ec36 Mon Sep 17 00:00:00 2001 From: Yuta Saito Date: Mon, 17 Mar 2025 07:21:42 +0000 Subject: [PATCH 293/373] PackageToJS: Fetch module from the default location if `init` is called without options --- .gitignore | 2 +- Examples/ActorOnWebWorker/index.html | 2 +- Examples/Basic/.gitignore | 5 ---- Examples/Basic/index.html | 2 +- Examples/Embedded/.gitignore | 6 ----- Examples/Embedded/index.html | 2 +- Examples/Multithreading/.gitignore | 8 ------ Examples/Multithreading/index.html | 2 +- Examples/OffscrenCanvas/.gitignore | 8 ------ Examples/OffscrenCanvas/index.html | 2 +- Plugins/PackageToJS/Templates/index.d.ts | 26 +++++++------------ Plugins/PackageToJS/Templates/index.js | 12 ++++++--- .../Resources/hello-world-2-2-index-html.html | 2 +- 13 files changed, 24 insertions(+), 55 deletions(-) delete mode 100644 Examples/Basic/.gitignore delete mode 100644 Examples/Embedded/.gitignore delete mode 100644 Examples/Multithreading/.gitignore delete mode 100644 Examples/OffscrenCanvas/.gitignore diff --git a/.gitignore b/.gitignore index 2fb37cb48..232ea1145 100644 --- a/.gitignore +++ b/.gitignore @@ -9,4 +9,4 @@ xcuserdata/ .vscode Examples/*/Bundle Examples/*/package-lock.json -/Package.resolved +Package.resolved diff --git a/Examples/ActorOnWebWorker/index.html b/Examples/ActorOnWebWorker/index.html index 2797702e1..4a16f16a0 100644 --- a/Examples/ActorOnWebWorker/index.html +++ b/Examples/ActorOnWebWorker/index.html @@ -8,7 +8,7 @@

Full-text Search with Actor on Web Worker

diff --git a/Examples/Basic/.gitignore b/Examples/Basic/.gitignore deleted file mode 100644 index 95c432091..000000000 --- a/Examples/Basic/.gitignore +++ /dev/null @@ -1,5 +0,0 @@ -.DS_Store -/.build -/Packages -/*.xcodeproj -xcuserdata/ diff --git a/Examples/Basic/index.html b/Examples/Basic/index.html index a674baca1..93868214d 100644 --- a/Examples/Basic/index.html +++ b/Examples/Basic/index.html @@ -8,7 +8,7 @@ diff --git a/Examples/Embedded/.gitignore b/Examples/Embedded/.gitignore deleted file mode 100644 index 31492b35d..000000000 --- a/Examples/Embedded/.gitignore +++ /dev/null @@ -1,6 +0,0 @@ -.DS_Store -/.build -/Packages -/*.xcodeproj -xcuserdata/ -Package.resolved \ No newline at end of file diff --git a/Examples/Embedded/index.html b/Examples/Embedded/index.html index a674baca1..93868214d 100644 --- a/Examples/Embedded/index.html +++ b/Examples/Embedded/index.html @@ -8,7 +8,7 @@ diff --git a/Examples/Multithreading/.gitignore b/Examples/Multithreading/.gitignore deleted file mode 100644 index 0023a5340..000000000 --- a/Examples/Multithreading/.gitignore +++ /dev/null @@ -1,8 +0,0 @@ -.DS_Store -/.build -/Packages -xcuserdata/ -DerivedData/ -.swiftpm/configuration/registries.json -.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata -.netrc diff --git a/Examples/Multithreading/index.html b/Examples/Multithreading/index.html index 74ba8cfed..20696d83a 100644 --- a/Examples/Multithreading/index.html +++ b/Examples/Multithreading/index.html @@ -29,7 +29,7 @@

Threading Example

diff --git a/Examples/OffscrenCanvas/.gitignore b/Examples/OffscrenCanvas/.gitignore deleted file mode 100644 index 0023a5340..000000000 --- a/Examples/OffscrenCanvas/.gitignore +++ /dev/null @@ -1,8 +0,0 @@ -.DS_Store -/.build -/Packages -xcuserdata/ -DerivedData/ -.swiftpm/configuration/registries.json -.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata -.netrc diff --git a/Examples/OffscrenCanvas/index.html b/Examples/OffscrenCanvas/index.html index 1202807a0..dd101b765 100644 --- a/Examples/OffscrenCanvas/index.html +++ b/Examples/OffscrenCanvas/index.html @@ -70,7 +70,7 @@

OffscreenCanvas Example

diff --git a/Plugins/PackageToJS/Templates/index.d.ts b/Plugins/PackageToJS/Templates/index.d.ts index 4a1074c14..11d5908c2 100644 --- a/Plugins/PackageToJS/Templates/index.d.ts +++ b/Plugins/PackageToJS/Templates/index.d.ts @@ -1,29 +1,21 @@ -import type { Import, Export } from './instantiate.js' +import type { Export, ModuleSource } from './instantiate.js' export type Options = { /** - * The CLI arguments to pass to the WebAssembly module + * The WebAssembly module to instantiate + * + * If not provided, the module will be fetched from the default path. */ - args?: string[] -/* #if USE_SHARED_MEMORY */ - /** - * The WebAssembly memory to use (must be 'shared') - */ - memory: WebAssembly.Memory -/* #endif */ + module?: ModuleSource } /** - * Initialize the given WebAssembly module + * Instantiate and initialize the module * - * This is a convenience function that creates an instantiator and instantiates the module. - * @param moduleSource - The WebAssembly module to instantiate - * @param imports - The imports to add - * @param options - The options + * This is a convenience function for browser environments. + * If you need a more flexible API, see `instantiate`. */ -export declare function init( - moduleSource: WebAssembly.Module | ArrayBufferView | ArrayBuffer | Response | PromiseLike -): Promise<{ +export declare function init(options?: Options): Promise<{ instance: WebAssembly.Instance, exports: Export }> diff --git a/Plugins/PackageToJS/Templates/index.js b/Plugins/PackageToJS/Templates/index.js index d0d28569f..4b8d90f6b 100644 --- a/Plugins/PackageToJS/Templates/index.js +++ b/Plugins/PackageToJS/Templates/index.js @@ -3,12 +3,16 @@ 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(moduleSource) { - const options = await defaultBrowserSetup({ - module: moduleSource, +export async function init(options = {}) { + let module = options.module; + if (!module) { + module = fetch(new URL("@PACKAGE_TO_JS_MODULE_PATH@", import.meta.url)) + } + const instantiateOptions = await defaultBrowserSetup({ + module, /* #if USE_SHARED_MEMORY */ spawnWorker: createDefaultWorkerFactory() /* #endif */ }) - return await instantiate(options); + return await instantiate(instantiateOptions); } 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 index 84a3aa15e..c75dd927a 100644 --- 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 @@ -5,7 +5,7 @@ Swift Web App From 53d1a470c54be990cfee1e6c9f32e963ce6c719c Mon Sep 17 00:00:00 2001 From: Yuta Saito Date: Mon, 17 Mar 2025 07:38:22 +0000 Subject: [PATCH 294/373] PackageToJS: Use the actual wasm filename in the final product --- Plugins/PackageToJS/Sources/PackageToJS.swift | 4 +++- Plugins/PackageToJS/Sources/PackageToJSPlugin.swift | 1 + Plugins/PackageToJS/Tests/PackagingPlannerTests.swift | 2 ++ 3 files changed, 6 insertions(+), 1 deletion(-) diff --git a/Plugins/PackageToJS/Sources/PackageToJS.swift b/Plugins/PackageToJS/Sources/PackageToJS.swift index cc0c02182..c766995d2 100644 --- a/Plugins/PackageToJS/Sources/PackageToJS.swift +++ b/Plugins/PackageToJS/Sources/PackageToJS.swift @@ -357,7 +357,7 @@ struct PackagingPlanner { /// The directory for intermediate files let intermediatesDir: BuildPath /// The filename of the .wasm file - let wasmFilename = "main.wasm" + let wasmFilename: String /// The path to the .wasm product artifact let wasmProductArtifact: BuildPath /// The build configuration @@ -374,6 +374,7 @@ struct PackagingPlanner { selfPackageDir: BuildPath, outputDir: BuildPath, wasmProductArtifact: BuildPath, + wasmFilename: String, configuration: String, triple: String, selfPath: BuildPath = BuildPath(absolute: #filePath), @@ -384,6 +385,7 @@ struct PackagingPlanner { self.selfPackageDir = selfPackageDir self.outputDir = outputDir self.intermediatesDir = intermediatesDir + self.wasmFilename = wasmFilename self.selfPath = selfPath self.wasmProductArtifact = wasmProductArtifact self.configuration = configuration diff --git a/Plugins/PackageToJS/Sources/PackageToJSPlugin.swift b/Plugins/PackageToJS/Sources/PackageToJSPlugin.swift index 4bf6a1106..a4fd58d29 100644 --- a/Plugins/PackageToJS/Sources/PackageToJSPlugin.swift +++ b/Plugins/PackageToJS/Sources/PackageToJSPlugin.swift @@ -498,6 +498,7 @@ extension PackagingPlanner { selfPackageDir: BuildPath(absolute: selfPackage.directoryURL.path), outputDir: BuildPath(absolute: outputDir.path), wasmProductArtifact: BuildPath(absolute: wasmProductArtifact.path), + wasmFilename: wasmProductArtifact.lastPathComponent, configuration: configuration, triple: triple, system: system diff --git a/Plugins/PackageToJS/Tests/PackagingPlannerTests.swift b/Plugins/PackageToJS/Tests/PackagingPlannerTests.swift index 1b1eb1abf..6392ca664 100644 --- a/Plugins/PackageToJS/Tests/PackagingPlannerTests.swift +++ b/Plugins/PackageToJS/Tests/PackagingPlannerTests.swift @@ -53,6 +53,7 @@ import Testing selfPackageDir: BuildPath(prefix: "SELF_PACKAGE"), 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"), @@ -81,6 +82,7 @@ import Testing selfPackageDir: BuildPath(prefix: "SELF_PACKAGE"), 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"), From 81fe6c8033030cb9aaaaf203c40a8b85c235d1de Mon Sep 17 00:00:00 2001 From: Yuta Saito Date: Mon, 17 Mar 2025 07:55:26 +0000 Subject: [PATCH 295/373] PackageToJS: Fix browser tests with non-.wasm product --- .../Sources/PackageToJSPlugin.swift | 18 ++++++++++++++---- .../PackageToJS/Templates/test.browser.html | 3 ++- Plugins/PackageToJS/Tests/ExampleTests.swift | 2 +- 3 files changed, 17 insertions(+), 6 deletions(-) diff --git a/Plugins/PackageToJS/Sources/PackageToJSPlugin.swift b/Plugins/PackageToJS/Sources/PackageToJSPlugin.swift index a4fd58d29..2844d52ec 100644 --- a/Plugins/PackageToJS/Sources/PackageToJSPlugin.swift +++ b/Plugins/PackageToJS/Sources/PackageToJSPlugin.swift @@ -119,7 +119,9 @@ struct PackageToJSPlugin: CommandPlugin { ) let planner = PackagingPlanner( options: buildOptions.packageOptions, context: context, selfPackage: selfPackage, - outputDir: outputDir, wasmProductArtifact: productArtifact) + outputDir: outputDir, wasmProductArtifact: productArtifact, + wasmFilename: productArtifact.lastPathComponent + ) let rootTask = try planner.planBuild( make: &make, buildOptions: buildOptions) cleanIfBuildGraphChanged(root: rootTask, make: make, context: context) @@ -193,7 +195,14 @@ struct PackageToJSPlugin: CommandPlugin { ) let planner = PackagingPlanner( options: testOptions.packageOptions, context: context, selfPackage: selfPackage, - outputDir: outputDir, wasmProductArtifact: productArtifact) + 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) @@ -486,7 +495,8 @@ extension PackagingPlanner { context: PluginContext, selfPackage: Package, outputDir: URL, - wasmProductArtifact: URL + wasmProductArtifact: URL, + wasmFilename: String ) { let outputBaseName = outputDir.lastPathComponent let (configuration, triple) = PackageToJS.deriveBuildConfiguration(wasmProductArtifact: wasmProductArtifact) @@ -498,7 +508,7 @@ extension PackagingPlanner { selfPackageDir: BuildPath(absolute: selfPackage.directoryURL.path), outputDir: BuildPath(absolute: outputDir.path), wasmProductArtifact: BuildPath(absolute: wasmProductArtifact.path), - wasmFilename: wasmProductArtifact.lastPathComponent, + wasmFilename: wasmFilename, configuration: configuration, triple: triple, system: system diff --git a/Plugins/PackageToJS/Templates/test.browser.html b/Plugins/PackageToJS/Templates/test.browser.html index 27bfd25fc..35a37c943 100644 --- a/Plugins/PackageToJS/Templates/test.browser.html +++ b/Plugins/PackageToJS/Templates/test.browser.html @@ -4,6 +4,7 @@ + + +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/Tutorials/Hello-World/Hello-World.tutorial b/Sources/JavaScriptKit/Documentation.docc/Tutorials/Hello-World/Hello-World.tutorial index f5ede8f19..c054e3a48 100644 --- a/Sources/JavaScriptKit/Documentation.docc/Tutorials/Hello-World/Hello-World.tutorial +++ b/Sources/JavaScriptKit/Documentation.docc/Tutorials/Hello-World/Hello-World.tutorial @@ -84,7 +84,7 @@ @Step { Start a local web server to serve your application: This starts a simple HTTP server that serves files from your current directory. - + > Note: If you are building your app with `wasm32-unknown-wasip1-threads` target, you need to enable [Cross-Origin Isolation](https://developer.mozilla.org/en-US/docs/Web/API/Window/crossOriginIsolated) for `SharedArrayBuffer`. See "Cross-Origin Isolation Requirements" in @Code(name: "Console", file: "hello-world-3-2-server.txt") } diff --git a/Sources/JavaScriptKit/Documentation.docc/Tutorials/Hello-World/Resources/hello-world-3-2-server.txt b/Sources/JavaScriptKit/Documentation.docc/Tutorials/Hello-World/Resources/hello-world-3-2-server.txt index 569396481..ad560a635 100644 --- a/Sources/JavaScriptKit/Documentation.docc/Tutorials/Hello-World/Resources/hello-world-3-2-server.txt +++ b/Sources/JavaScriptKit/Documentation.docc/Tutorials/Hello-World/Resources/hello-world-3-2-server.txt @@ -4,5 +4,4 @@ Build of product 'Hello' complete! (5.16s) Packaging... ... Packaging finished -$ python3 -m http.server -Serving HTTP on 0.0.0.0 port 8000 (http://0.0.0.0:8000/) ... +$ npx serve diff --git a/Sources/JavaScriptKit/Documentation.docc/Tutorials/Hello-World/Resources/hello-world-3-3-open.txt b/Sources/JavaScriptKit/Documentation.docc/Tutorials/Hello-World/Resources/hello-world-3-3-open.txt index f4df8ec2f..8abe30b7c 100644 --- a/Sources/JavaScriptKit/Documentation.docc/Tutorials/Hello-World/Resources/hello-world-3-3-open.txt +++ b/Sources/JavaScriptKit/Documentation.docc/Tutorials/Hello-World/Resources/hello-world-3-3-open.txt @@ -4,6 +4,5 @@ Build of product 'Hello' complete! (5.16s) Packaging... ... Packaging finished -$ python3 -m http.server -Serving HTTP on 0.0.0.0 port 8000 (http://0.0.0.0:8000/) ... -$ open http://localhost:8000 +$ npx serve +$ open http://localhost:3000 From fccfd971c3c5f4b8f82713e4327d9de4ee120684 Mon Sep 17 00:00:00 2001 From: Yuta Saito Date: Mon, 31 Mar 2025 22:43:41 +0000 Subject: [PATCH 355/373] Fix node version diagnostic handling on test harness The CompileError usually happens during `defaultNodeSetup`, so we should catch it there. Also `process.version` is a string with a `v` prefix, so we should use `process.versions.node`, which doesn't have the prefix instead. --- Plugins/PackageToJS/Templates/bin/test.js | 46 +++++++++++------------ 1 file changed, 23 insertions(+), 23 deletions(-) diff --git a/Plugins/PackageToJS/Templates/bin/test.js b/Plugins/PackageToJS/Templates/bin/test.js index b31d82086..f888b9d1c 100644 --- a/Plugins/PackageToJS/Templates/bin/test.js +++ b/Plugins/PackageToJS/Templates/bin/test.js @@ -38,35 +38,35 @@ const args = parseArgs({ const harnesses = { node: async ({ preludeScript }) => { - let options = await nodePlatform.defaultNodeSetup({ - args: testFrameworkArgs, - onExit: (code) => { - if (code !== 0) { return } - // Extract the coverage file from the wasm module - const filePath = "default.profraw" - const destinationPath = args.values["coverage-file"] ?? filePath - const profraw = options.wasi.extractFile?.(filePath) - if (profraw) { - console.log(`Saved ${filePath} to ${destinationPath}`); - writeFileSync(destinationPath, profraw); + try { + let options = await nodePlatform.defaultNodeSetup({ + args: testFrameworkArgs, + onExit: (code) => { + 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 }) } - }, - /* #if USE_SHARED_MEMORY */ - spawnWorker: nodePlatform.createDefaultWorkerFactory(preludeScript) - /* #endif */ - }) - if (preludeScript) { - const prelude = await import(preludeScript) - if (prelude.setupOptions) { - options = prelude.setupOptions(options, { isMainThread: true }) } - } - try { await instantiate(options) } catch (e) { if (e instanceof WebAssembly.CompileError) { // Check Node.js major version - const nodeVersion = process.version.split(".")[0] + const nodeVersion = process.versions.node.split(".")[0] const minNodeVersion = 20 if (nodeVersion < minNodeVersion) { console.error(`Hint: Node.js version ${nodeVersion} is not supported, please use version ${minNodeVersion} or later.`) From c80eed35c2f838c7fcc258ccab682f52000ebcb5 Mon Sep 17 00:00:00 2001 From: Yuta Saito Date: Tue, 1 Apr 2025 13:46:53 +0000 Subject: [PATCH 356/373] build: Fix native build for missing symbol ``` $s13JavaScriptKit8JSObjectC2idACs6UInt32V_tcfc: error: undefined reference to 'swjs_get_worker_thread_id_cached' ``` --- Sources/_CJavaScriptKit/_CJavaScriptKit.c | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/Sources/_CJavaScriptKit/_CJavaScriptKit.c b/Sources/_CJavaScriptKit/_CJavaScriptKit.c index ed8240ca1..a32881804 100644 --- a/Sources/_CJavaScriptKit/_CJavaScriptKit.c +++ b/Sources/_CJavaScriptKit/_CJavaScriptKit.c @@ -1,18 +1,18 @@ #include "_CJavaScriptKit.h" #if __wasm32__ -#ifndef __wasi__ -#if __has_include("malloc.h") -#include -#endif +# ifndef __wasi__ +# if __has_include("malloc.h") +# include +# endif extern void *malloc(size_t size); extern void free(void *ptr); extern void *memset (void *, int, size_t); extern void *memcpy (void *__restrict, const void *__restrict, size_t); -#else -#include -#include +# else +# include +# include -#endif +# endif /// The compatibility runtime library version. /// Notes: If you change any interface of runtime library, please increment /// this and `SwiftRuntime.version` in `./Runtime/src/index.ts`. @@ -34,7 +34,7 @@ void swjs_cleanup_host_function_call(void *argv_buffer) { // NOTE: This __wasi__ check is a hack for Embedded compatibility (assuming that if __wasi__ is defined, we are not building for Embedded) // cdecls don't work in Embedded, but @_expose(wasm) can be used with Swift >=6.0 // the previously used `#if __Embedded` did not play well with SwiftPM (defines needed to be on every target up the chain) -#ifdef __wasi__ +# ifdef __wasi__ bool _call_host_function_impl(const JavaScriptHostFuncRef host_func_ref, const RawJSValue *argv, const int argc, const JavaScriptObjectRef callback_func); @@ -59,6 +59,8 @@ __attribute__((export_name("swjs_library_features"))) int swjs_library_features(void) { return _library_features(); } +# endif +#endif int swjs_get_worker_thread_id_cached(void) { _Thread_local static int tid = 0; @@ -67,5 +69,3 @@ int swjs_get_worker_thread_id_cached(void) { } return tid; } -#endif -#endif From 4709005e1b82b8112f5e2b5da4e15bbc0467d0ec Mon Sep 17 00:00:00 2001 From: Yuta Saito Date: Tue, 1 Apr 2025 13:48:29 +0000 Subject: [PATCH 357/373] CI: Ensure that linking works correctly for native targets --- .github/workflows/test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 174b873ef..35405eaf6 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -64,7 +64,7 @@ jobs: runs-on: ${{ matrix.os }} steps: - uses: actions/checkout@v4 - - run: swift build + - run: swift build --package-path ./Examples/Basic env: DEVELOPER_DIR: /Applications/${{ matrix.xcode }}.app/Contents/Developer/ From d65706936b1b5e2abb2964f74fd1bbc1faf75757 Mon Sep 17 00:00:00 2001 From: Yuta Saito Date: Sun, 9 Mar 2025 19:03:50 +0900 Subject: [PATCH 358/373] Introduce BridgeJS, a declarative JS interop system --- .github/workflows/test.yml | 1 + .gitignore | 1 + Examples/ExportSwift/Package.swift | 25 + Examples/ExportSwift/Sources/main.swift | 34 + Examples/ExportSwift/index.html | 12 + Examples/ExportSwift/index.js | 14 + Examples/ImportTS/Package.swift | 29 + Examples/ImportTS/Sources/bridge.d.ts | 24 + Examples/ImportTS/Sources/main.swift | 26 + Examples/ImportTS/index.html | 16 + Examples/ImportTS/index.js | 13 + Examples/Multithreading/Package.resolved | 11 +- Package.swift | 48 +- Plugins/BridgeJS/Package.swift | 29 + Plugins/BridgeJS/README.md | 133 ++++ .../BridgeJSBuildPlugin.swift | 71 +++ .../BridgeJSCommandPlugin.swift | 182 ++++++ .../Sources/BridgeJSLink/BridgeJSLink.swift | 561 ++++++++++++++++ .../Sources/BridgeJSLink/BridgeJSSkeleton | 1 + .../BridgeJSSkeleton/BridgeJSSkeleton.swift | 96 +++ .../Sources/BridgeJSTool/BridgeJSSkeleton | 1 + .../Sources/BridgeJSTool/BridgeJSTool.swift | 341 ++++++++++ .../BridgeJSTool/DiagnosticError.swift | 23 + .../Sources/BridgeJSTool/ExportSwift.swift | 599 ++++++++++++++++++ .../Sources/BridgeJSTool/ImportTS.swift | 533 ++++++++++++++++ .../BridgeJSTool/TypeDeclResolver.swift | 112 ++++ Plugins/BridgeJS/Sources/JavaScript/README.md | 3 + .../Sources/JavaScript/bin/ts2skeleton.js | 14 + .../BridgeJS/Sources/JavaScript/package.json | 9 + .../BridgeJS/Sources/JavaScript/src/cli.js | 139 ++++ .../Sources/JavaScript/src/index.d.ts | 44 ++ .../Sources/JavaScript/src/processor.js | 414 ++++++++++++ .../BridgeJSToolTests/BridgeJSLinkTests.swift | 61 ++ .../BridgeJSToolTests/ExportSwiftTests.swift | 57 ++ .../BridgeJSToolTests/ImportTSTests.swift | 32 + .../Inputs/ArrayParameter.d.ts | 3 + .../BridgeJSToolTests/Inputs/Interface.d.ts | 6 + .../Inputs/PrimitiveParameters.d.ts | 1 + .../Inputs/PrimitiveParameters.swift | 1 + .../Inputs/PrimitiveReturn.d.ts | 2 + .../Inputs/PrimitiveReturn.swift | 4 + .../Inputs/StringParameter.d.ts | 2 + .../Inputs/StringParameter.swift | 1 + .../Inputs/StringReturn.d.ts | 1 + .../Inputs/StringReturn.swift | 1 + .../BridgeJSToolTests/Inputs/SwiftClass.swift | 17 + .../BridgeJSToolTests/Inputs/TypeAlias.d.ts | 3 + .../Inputs/TypeScriptClass.d.ts | 5 + .../Inputs/VoidParameterVoidReturn.d.ts | 1 + .../Inputs/VoidParameterVoidReturn.swift | 1 + .../BridgeJSToolTests/SnapshotTesting.swift | 42 ++ .../TemporaryDirectory.swift | 27 + .../PrimitiveParameters.d.ts | 18 + .../BridgeJSLinkTests/PrimitiveParameters.js | 55 ++ .../BridgeJSLinkTests/PrimitiveReturn.d.ts | 21 + .../BridgeJSLinkTests/PrimitiveReturn.js | 68 ++ .../BridgeJSLinkTests/StringParameter.d.ts | 18 + .../BridgeJSLinkTests/StringParameter.js | 58 ++ .../BridgeJSLinkTests/StringReturn.d.ts | 18 + .../BridgeJSLinkTests/StringReturn.js | 58 ++ .../BridgeJSLinkTests/SwiftClass.d.ts | 32 + .../BridgeJSLinkTests/SwiftClass.js | 92 +++ .../VoidParameterVoidReturn.d.ts | 18 + .../VoidParameterVoidReturn.js | 55 ++ .../ExportSwiftTests/PrimitiveParameters.json | 54 ++ .../PrimitiveParameters.swift | 15 + .../ExportSwiftTests/PrimitiveReturn.json | 55 ++ .../ExportSwiftTests/PrimitiveReturn.swift | 37 ++ .../ExportSwiftTests/StringParameter.json | 27 + .../ExportSwiftTests/StringParameter.swift | 19 + .../ExportSwiftTests/StringReturn.json | 19 + .../ExportSwiftTests/StringReturn.swift | 18 + .../ExportSwiftTests/SwiftClass.json | 77 +++ .../ExportSwiftTests/SwiftClass.swift | 51 ++ .../VoidParameterVoidReturn.json | 19 + .../VoidParameterVoidReturn.swift | 15 + .../ImportTSTests/ArrayParameter.swift | 34 + .../ImportTSTests/Interface.swift | 50 ++ .../ImportTSTests/PrimitiveParameters.swift | 22 + .../ImportTSTests/PrimitiveReturn.swift | 30 + .../ImportTSTests/StringParameter.swift | 36 ++ .../ImportTSTests/StringReturn.swift | 26 + .../ImportTSTests/TypeAlias.swift | 22 + .../ImportTSTests/TypeScriptClass.swift | 60 ++ .../VoidParameterVoidReturn.swift | 22 + Plugins/PackageToJS/Sources/BridgeJSLink | 1 + Plugins/PackageToJS/Sources/PackageToJS.swift | 34 + .../Sources/PackageToJSPlugin.swift | 97 +++ Plugins/PackageToJS/Templates/index.d.ts | 10 +- Plugins/PackageToJS/Templates/index.js | 12 +- .../PackageToJS/Templates/instantiate.d.ts | 27 +- Plugins/PackageToJS/Templates/instantiate.js | 20 +- .../Templates/platforms/browser.d.ts | 5 +- .../Templates/platforms/browser.js | 4 +- Plugins/PackageToJS/Tests/ExampleTests.swift | 19 + .../Tests/PackagingPlannerTests.swift | 4 + .../planBuild_debug.json | 24 +- .../planBuild_release.json | 24 +- .../planBuild_release_dwarf.json | 24 +- .../planBuild_release_name.json | 24 +- .../planBuild_release_no_optimize.json | 24 +- .../PackagingPlannerTests/planTestBuild.json | 32 +- .../Articles/Ahead-of-Time-Code-Generation.md | 169 +++++ .../Articles/Exporting-Swift-to-JavaScript.md | 164 +++++ .../Importing-TypeScript-into-Swift.md | 172 +++++ .../Documentation.docc/Documentation.md | 16 +- Sources/JavaScriptKit/Macros.swift | 35 + .../BridgeJSRuntimeTests/ExportAPITests.swift | 61 ++ .../Generated/ExportSwift.swift | 98 +++ .../Generated/JavaScript/ExportSwift.json | 206 ++++++ Tests/prelude.mjs | 58 +- Utilities/format.swift | 1 + 112 files changed, 6309 insertions(+), 102 deletions(-) create mode 100644 Examples/ExportSwift/Package.swift create mode 100644 Examples/ExportSwift/Sources/main.swift create mode 100644 Examples/ExportSwift/index.html create mode 100644 Examples/ExportSwift/index.js create mode 100644 Examples/ImportTS/Package.swift create mode 100644 Examples/ImportTS/Sources/bridge.d.ts create mode 100644 Examples/ImportTS/Sources/main.swift create mode 100644 Examples/ImportTS/index.html create mode 100644 Examples/ImportTS/index.js create mode 100644 Plugins/BridgeJS/Package.swift create mode 100644 Plugins/BridgeJS/README.md create mode 100644 Plugins/BridgeJS/Sources/BridgeJSBuildPlugin/BridgeJSBuildPlugin.swift create mode 100644 Plugins/BridgeJS/Sources/BridgeJSCommandPlugin/BridgeJSCommandPlugin.swift create mode 100644 Plugins/BridgeJS/Sources/BridgeJSLink/BridgeJSLink.swift create mode 120000 Plugins/BridgeJS/Sources/BridgeJSLink/BridgeJSSkeleton create mode 100644 Plugins/BridgeJS/Sources/BridgeJSSkeleton/BridgeJSSkeleton.swift create mode 120000 Plugins/BridgeJS/Sources/BridgeJSTool/BridgeJSSkeleton create mode 100644 Plugins/BridgeJS/Sources/BridgeJSTool/BridgeJSTool.swift create mode 100644 Plugins/BridgeJS/Sources/BridgeJSTool/DiagnosticError.swift create mode 100644 Plugins/BridgeJS/Sources/BridgeJSTool/ExportSwift.swift create mode 100644 Plugins/BridgeJS/Sources/BridgeJSTool/ImportTS.swift create mode 100644 Plugins/BridgeJS/Sources/BridgeJSTool/TypeDeclResolver.swift create mode 100644 Plugins/BridgeJS/Sources/JavaScript/README.md create mode 100755 Plugins/BridgeJS/Sources/JavaScript/bin/ts2skeleton.js create mode 100644 Plugins/BridgeJS/Sources/JavaScript/package.json create mode 100644 Plugins/BridgeJS/Sources/JavaScript/src/cli.js create mode 100644 Plugins/BridgeJS/Sources/JavaScript/src/index.d.ts create mode 100644 Plugins/BridgeJS/Sources/JavaScript/src/processor.js create mode 100644 Plugins/BridgeJS/Tests/BridgeJSToolTests/BridgeJSLinkTests.swift create mode 100644 Plugins/BridgeJS/Tests/BridgeJSToolTests/ExportSwiftTests.swift create mode 100644 Plugins/BridgeJS/Tests/BridgeJSToolTests/ImportTSTests.swift create mode 100644 Plugins/BridgeJS/Tests/BridgeJSToolTests/Inputs/ArrayParameter.d.ts create mode 100644 Plugins/BridgeJS/Tests/BridgeJSToolTests/Inputs/Interface.d.ts create mode 100644 Plugins/BridgeJS/Tests/BridgeJSToolTests/Inputs/PrimitiveParameters.d.ts create mode 100644 Plugins/BridgeJS/Tests/BridgeJSToolTests/Inputs/PrimitiveParameters.swift create mode 100644 Plugins/BridgeJS/Tests/BridgeJSToolTests/Inputs/PrimitiveReturn.d.ts create mode 100644 Plugins/BridgeJS/Tests/BridgeJSToolTests/Inputs/PrimitiveReturn.swift create mode 100644 Plugins/BridgeJS/Tests/BridgeJSToolTests/Inputs/StringParameter.d.ts create mode 100644 Plugins/BridgeJS/Tests/BridgeJSToolTests/Inputs/StringParameter.swift create mode 100644 Plugins/BridgeJS/Tests/BridgeJSToolTests/Inputs/StringReturn.d.ts create mode 100644 Plugins/BridgeJS/Tests/BridgeJSToolTests/Inputs/StringReturn.swift create mode 100644 Plugins/BridgeJS/Tests/BridgeJSToolTests/Inputs/SwiftClass.swift create mode 100644 Plugins/BridgeJS/Tests/BridgeJSToolTests/Inputs/TypeAlias.d.ts create mode 100644 Plugins/BridgeJS/Tests/BridgeJSToolTests/Inputs/TypeScriptClass.d.ts create mode 100644 Plugins/BridgeJS/Tests/BridgeJSToolTests/Inputs/VoidParameterVoidReturn.d.ts create mode 100644 Plugins/BridgeJS/Tests/BridgeJSToolTests/Inputs/VoidParameterVoidReturn.swift create mode 100644 Plugins/BridgeJS/Tests/BridgeJSToolTests/SnapshotTesting.swift create mode 100644 Plugins/BridgeJS/Tests/BridgeJSToolTests/TemporaryDirectory.swift create mode 100644 Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/PrimitiveParameters.d.ts create mode 100644 Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/PrimitiveParameters.js create mode 100644 Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/PrimitiveReturn.d.ts create mode 100644 Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/PrimitiveReturn.js create mode 100644 Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/StringParameter.d.ts create mode 100644 Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/StringParameter.js create mode 100644 Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/StringReturn.d.ts create mode 100644 Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/StringReturn.js create mode 100644 Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/SwiftClass.d.ts create mode 100644 Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/SwiftClass.js create mode 100644 Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/VoidParameterVoidReturn.d.ts create mode 100644 Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/VoidParameterVoidReturn.js create mode 100644 Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/ExportSwiftTests/PrimitiveParameters.json create mode 100644 Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/ExportSwiftTests/PrimitiveParameters.swift create mode 100644 Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/ExportSwiftTests/PrimitiveReturn.json create mode 100644 Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/ExportSwiftTests/PrimitiveReturn.swift create mode 100644 Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/ExportSwiftTests/StringParameter.json create mode 100644 Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/ExportSwiftTests/StringParameter.swift create mode 100644 Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/ExportSwiftTests/StringReturn.json create mode 100644 Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/ExportSwiftTests/StringReturn.swift create mode 100644 Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/ExportSwiftTests/SwiftClass.json create mode 100644 Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/ExportSwiftTests/SwiftClass.swift create mode 100644 Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/ExportSwiftTests/VoidParameterVoidReturn.json create mode 100644 Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/ExportSwiftTests/VoidParameterVoidReturn.swift create mode 100644 Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/ImportTSTests/ArrayParameter.swift create mode 100644 Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/ImportTSTests/Interface.swift create mode 100644 Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/ImportTSTests/PrimitiveParameters.swift create mode 100644 Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/ImportTSTests/PrimitiveReturn.swift create mode 100644 Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/ImportTSTests/StringParameter.swift create mode 100644 Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/ImportTSTests/StringReturn.swift create mode 100644 Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/ImportTSTests/TypeAlias.swift create mode 100644 Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/ImportTSTests/TypeScriptClass.swift create mode 100644 Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/ImportTSTests/VoidParameterVoidReturn.swift create mode 120000 Plugins/PackageToJS/Sources/BridgeJSLink create mode 100644 Sources/JavaScriptKit/Documentation.docc/Articles/Ahead-of-Time-Code-Generation.md create mode 100644 Sources/JavaScriptKit/Documentation.docc/Articles/Exporting-Swift-to-JavaScript.md create mode 100644 Sources/JavaScriptKit/Documentation.docc/Articles/Importing-TypeScript-into-Swift.md create mode 100644 Sources/JavaScriptKit/Macros.swift create mode 100644 Tests/BridgeJSRuntimeTests/ExportAPITests.swift create mode 100644 Tests/BridgeJSRuntimeTests/Generated/ExportSwift.swift create mode 100644 Tests/BridgeJSRuntimeTests/Generated/JavaScript/ExportSwift.json diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 35405eaf6..ffb7fefb7 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -52,6 +52,7 @@ jobs: make regenerate_swiftpm_resources git diff --exit-code Sources/JavaScriptKit/Runtime - run: swift test --package-path ./Plugins/PackageToJS + - run: swift test --package-path ./Plugins/BridgeJS native-build: # Check native build to make it easy to develop applications by Xcode diff --git a/.gitignore b/.gitignore index 232ea1145..5aac0048c 100644 --- a/.gitignore +++ b/.gitignore @@ -10,3 +10,4 @@ xcuserdata/ Examples/*/Bundle Examples/*/package-lock.json Package.resolved +Plugins/BridgeJS/Sources/JavaScript/package-lock.json diff --git a/Examples/ExportSwift/Package.swift b/Examples/ExportSwift/Package.swift new file mode 100644 index 000000000..191278fda --- /dev/null +++ b/Examples/ExportSwift/Package.swift @@ -0,0 +1,25 @@ +// swift-tools-version:6.0 + +import PackageDescription + +let package = Package( + name: "MyApp", + platforms: [ + .macOS(.v14) + ], + dependencies: [.package(name: "JavaScriptKit", path: "../../")], + targets: [ + .executableTarget( + name: "MyApp", + dependencies: [ + "JavaScriptKit" + ], + swiftSettings: [ + .enableExperimentalFeature("Extern") + ], + plugins: [ + .plugin(name: "BridgeJS", package: "JavaScriptKit") + ] + ) + ] +) diff --git a/Examples/ExportSwift/Sources/main.swift b/Examples/ExportSwift/Sources/main.swift new file mode 100644 index 000000000..449155214 --- /dev/null +++ b/Examples/ExportSwift/Sources/main.swift @@ -0,0 +1,34 @@ +import JavaScriptKit + +// Mark functions you want to export to JavaScript with the @JS attribute +// This function will be available as `renderCircleSVG(size)` in JavaScript +@JS public func renderCircleSVG(size: Int) -> String { + let strokeWidth = 3 + let strokeColor = "black" + let fillColor = "red" + let cx = size / 2 + let cy = size / 2 + let r = (size / 2) - strokeWidth + var svg = "" + svg += + "" + svg += "" + return svg +} + +// Classes can also be exported using the @JS attribute +// This class will be available as a constructor in JavaScript: new Greeter("name") +@JS class Greeter { + var name: String + + // Use @JS for initializers you want to expose + @JS init(name: String) { + self.name = name + } + + // Methods need the @JS attribute to be accessible from JavaScript + // This method will be available as greeter.greet() in JavaScript + @JS public func greet() -> String { + "Hello, \(name)!" + } +} diff --git a/Examples/ExportSwift/index.html b/Examples/ExportSwift/index.html new file mode 100644 index 000000000..ef3d190ac --- /dev/null +++ b/Examples/ExportSwift/index.html @@ -0,0 +1,12 @@ + + + + + Getting Started + + + + + + + diff --git a/Examples/ExportSwift/index.js b/Examples/ExportSwift/index.js new file mode 100644 index 000000000..4c5576b25 --- /dev/null +++ b/Examples/ExportSwift/index.js @@ -0,0 +1,14 @@ +import { init } from "./.build/plugins/PackageToJS/outputs/Package/index.js"; +const { exports } = await init({}); + +const Greeter = exports.Greeter; +const greeter = new Greeter("World"); +const circle = exports.renderCircleSVG(100); + +// Display the results +const textOutput = document.createElement("div"); +textOutput.innerText = greeter.greet() +document.body.appendChild(textOutput); +const circleOutput = document.createElement("div"); +circleOutput.innerHTML = circle; +document.body.appendChild(circleOutput); diff --git a/Examples/ImportTS/Package.swift b/Examples/ImportTS/Package.swift new file mode 100644 index 000000000..4809ec006 --- /dev/null +++ b/Examples/ImportTS/Package.swift @@ -0,0 +1,29 @@ +// swift-tools-version:6.0 + +import PackageDescription + +let package = Package( + name: "MyApp", + platforms: [ + .macOS(.v10_15), + .iOS(.v13), + .tvOS(.v13), + .watchOS(.v6), + .macCatalyst(.v13), + ], + dependencies: [.package(name: "JavaScriptKit", path: "../../")], + targets: [ + .executableTarget( + name: "MyApp", + dependencies: [ + "JavaScriptKit" + ], + swiftSettings: [ + .enableExperimentalFeature("Extern") + ], + plugins: [ + .plugin(name: "BridgeJS", package: "JavaScriptKit") + ] + ) + ] +) diff --git a/Examples/ImportTS/Sources/bridge.d.ts b/Examples/ImportTS/Sources/bridge.d.ts new file mode 100644 index 000000000..856bba9c4 --- /dev/null +++ b/Examples/ImportTS/Sources/bridge.d.ts @@ -0,0 +1,24 @@ +// Function definition to expose console.log to Swift +// Will be imported as a Swift function: consoleLog(message: String) +export function consoleLog(message: string): void + +// TypeScript interface types are converted to Swift structs +// This defines a subset of the browser's HTMLElement interface +type HTMLElement = Pick & { + // Methods with object parameters are properly handled + appendChild(child: HTMLElement): void +} + +// TypeScript object type with read-only properties +// Properties will become Swift properties with appropriate access level +type Document = { + // Regular property - will be read/write in Swift + title: string + // Read-only property - will be read-only in Swift + readonly body: HTMLElement + // Method returning an object - will become a Swift method returning an HTMLElement + createElement(tagName: string): HTMLElement +} +// Function returning a complex object +// Will be imported as a Swift function: getDocument() -> Document +export function getDocument(): Document diff --git a/Examples/ImportTS/Sources/main.swift b/Examples/ImportTS/Sources/main.swift new file mode 100644 index 000000000..4328b0a3b --- /dev/null +++ b/Examples/ImportTS/Sources/main.swift @@ -0,0 +1,26 @@ +import JavaScriptKit + +// This function is automatically generated by the @JS plugin +// It demonstrates how to use TypeScript functions and types imported from bridge.d.ts +@JS public func run() { + // Call the imported consoleLog function defined in bridge.d.ts + consoleLog("Hello, World!") + + // Get the document object - this comes from the imported getDocument() function + let document = getDocument() + + // Access and modify properties - the title property is read/write + document.title = "Hello, World!" + + // Access read-only properties - body is defined as readonly in TypeScript + let body = document.body + + // Create a new element using the document.createElement method + let h1 = document.createElement("h1") + + // Set properties on the created element + h1.innerText = "Hello, World!" + + // Call methods on objects - appendChild is defined in the HTMLElement interface + body.appendChild(h1) +} diff --git a/Examples/ImportTS/index.html b/Examples/ImportTS/index.html new file mode 100644 index 000000000..31881c499 --- /dev/null +++ b/Examples/ImportTS/index.html @@ -0,0 +1,16 @@ + + + + + Getting Started + + + + + +

+
+

+
+
+
diff --git a/Examples/ImportTS/index.js b/Examples/ImportTS/index.js
new file mode 100644
index 000000000..9452b7ec7
--- /dev/null
+++ b/Examples/ImportTS/index.js
@@ -0,0 +1,13 @@
+import { init } from "./.build/plugins/PackageToJS/outputs/Package/index.js";
+const { exports } = await init({
+    imports: {
+        consoleLog: (message) => {
+            console.log(message);
+        },
+        getDocument: () => {
+            return document;
+        },
+    }
+});
+
+exports.run()
diff --git a/Examples/Multithreading/Package.resolved b/Examples/Multithreading/Package.resolved
index 1354cc039..f55b8400a 100644
--- a/Examples/Multithreading/Package.resolved
+++ b/Examples/Multithreading/Package.resolved
@@ -1,5 +1,5 @@
 {
-  "originHash" : "e66f4c272838a860049b7e3528f1db03ee6ae99c2b21c3b6ea58a293be4db41b",
+  "originHash" : "072d03a6e24e01bd372682a6090adb80cf29dea39421e065de6ff8853de704c9",
   "pins" : [
     {
       "identity" : "chibi-ray",
@@ -8,6 +8,15 @@
       "state" : {
         "revision" : "c8cab621a3338dd2f8e817d3785362409d3b8cf1"
       }
+    },
+    {
+      "identity" : "swift-syntax",
+      "kind" : "remoteSourceControl",
+      "location" : "https://github.com/swiftlang/swift-syntax",
+      "state" : {
+        "revision" : "0687f71944021d616d34d922343dcef086855920",
+        "version" : "600.0.1"
+      }
     }
   ],
   "version" : 3
diff --git a/Package.swift b/Package.swift
index fcf40524a..3657bfa99 100644
--- a/Package.swift
+++ b/Package.swift
@@ -1,5 +1,6 @@
 // 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
@@ -9,12 +10,24 @@ let useLegacyResourceBundling =
 
 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(
@@ -98,7 +111,40 @@ let package = Package(
             capability: .command(
                 intent: .custom(verb: "js", description: "Convert a Swift package to a JavaScript package")
             ),
-            sources: ["Sources"]
+            path: "Plugins/PackageToJS/Sources"
+        ),
+        .plugin(
+            name: "BridgeJS",
+            capability: .buildTool(),
+            dependencies: ["BridgeJSTool"],
+            path: "Plugins/BridgeJS/Sources/BridgeJSBuildPlugin"
+        ),
+        .plugin(
+            name: "BridgeJSCommandPlugin",
+            capability: .command(
+                intent: .custom(verb: "bridge-js", description: "Generate bridging code"),
+                permissions: [.writeToPackageDirectory(reason: "Generate bridging code")]
+            ),
+            dependencies: ["BridgeJSTool"],
+            path: "Plugins/BridgeJS/Sources/BridgeJSCommandPlugin"
+        ),
+        .executableTarget(
+            name: "BridgeJSTool",
+            dependencies: [
+                .product(name: "SwiftParser", package: "swift-syntax"),
+                .product(name: "SwiftSyntax", package: "swift-syntax"),
+                .product(name: "SwiftBasicFormat", package: "swift-syntax"),
+                .product(name: "SwiftSyntaxBuilder", package: "swift-syntax"),
+            ],
+            path: "Plugins/BridgeJS/Sources/BridgeJSTool"
+        ),
+        .testTarget(
+            name: "BridgeJSRuntimeTests",
+            dependencies: ["JavaScriptKit"],
+            exclude: ["Generated/JavaScript"],
+            swiftSettings: [
+                .enableExperimentalFeature("Extern")
+            ]
         ),
     ]
 )
diff --git a/Plugins/BridgeJS/Package.swift b/Plugins/BridgeJS/Package.swift
new file mode 100644
index 000000000..ab8b475cb
--- /dev/null
+++ b/Plugins/BridgeJS/Package.swift
@@ -0,0 +1,29 @@
+// swift-tools-version: 6.0
+
+import PackageDescription
+
+let package = Package(
+    name: "BridgeJS",
+    platforms: [.macOS(.v13)],
+    dependencies: [
+        .package(url: "https://github.com/swiftlang/swift-syntax", from: "600.0.1")
+    ],
+    targets: [
+        .target(name: "BridgeJSBuildPlugin"),
+        .target(name: "BridgeJSLink"),
+        .executableTarget(
+            name: "BridgeJSTool",
+            dependencies: [
+                .product(name: "SwiftParser", package: "swift-syntax"),
+                .product(name: "SwiftSyntax", package: "swift-syntax"),
+                .product(name: "SwiftBasicFormat", package: "swift-syntax"),
+                .product(name: "SwiftSyntaxBuilder", package: "swift-syntax"),
+            ]
+        ),
+        .testTarget(
+            name: "BridgeJSToolTests",
+            dependencies: ["BridgeJSTool", "BridgeJSLink"],
+            exclude: ["__Snapshots__", "Inputs"]
+        ),
+    ]
+)
diff --git a/Plugins/BridgeJS/README.md b/Plugins/BridgeJS/README.md
new file mode 100644
index 000000000..a62072539
--- /dev/null
+++ b/Plugins/BridgeJS/README.md
@@ -0,0 +1,133 @@
+# BridgeJS
+
+> 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..9ea500b8c
--- /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..0405f2393
--- /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
+    let 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..c8ff8df67
--- /dev/null
+++ b/Plugins/BridgeJS/Sources/BridgeJSTool/BridgeJSTool.swift
@@ -0,0 +1,341 @@
+@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 outputSkeletons = ImportedModuleSkeleton(moduleName: importer.moduleName, children: importer.skeletons)
+            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(outputSkeletons).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..c6e4729ea
--- /dev/null
+++ b/Plugins/BridgeJS/Sources/BridgeJSTool/ImportTS.swift
@@ -0,0 +1,533 @@
+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
+    let moduleName: String
+    private(set) var skeletons: [ImportedFileSkeleton] = []
+
+    init(progress: ProgressReporting, moduleName: String) {
+        self.progress = progress
+        self.moduleName = moduleName
+    }
+
+    /// Adds a skeleton to the importer's state
+    mutating func addSkeleton(_ skeleton: ImportedFileSkeleton) {
+        self.skeletons.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.skeletons {
+            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..5edb1b367
--- /dev/null
+++ b/Plugins/BridgeJS/Tests/BridgeJSToolTests/BridgeJSLinkTests.swift
@@ -0,0 +1,61 @@
+import Foundation
+import SwiftSyntax
+import SwiftParser
+import Testing
+
+@testable import BridgeJSLink
+@testable import BridgeJSTool
+
+@Suite struct BridgeJSLinkTests {
+    private func snapshot(
+        swiftAPI: ExportSwift,
+        name: String? = nil,
+        filePath: String = #filePath,
+        function: String = #function,
+        sourceLocation: Testing.SourceLocation = #_sourceLocation
+    ) throws {
+        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)
+        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() -> [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 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
+        try snapshot(swiftAPI: swiftAPI, name: name)
+    }
+}
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/PrimitiveParameters.d.ts b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/PrimitiveParameters.d.ts
new file mode 100644
index 000000000..a9c37f378
--- /dev/null
+++ b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/PrimitiveParameters.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.js b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/PrimitiveParameters.js
new file mode 100644
index 000000000..2d9ee4b10
--- /dev/null
+++ b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/PrimitiveParameters.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/PrimitiveReturn.d.ts b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/PrimitiveReturn.d.ts
new file mode 100644
index 000000000..da7f59772
--- /dev/null
+++ b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/PrimitiveReturn.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.js b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/PrimitiveReturn.js
new file mode 100644
index 000000000..8a66f0412
--- /dev/null
+++ b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/PrimitiveReturn.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/StringParameter.d.ts b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/StringParameter.d.ts
new file mode 100644
index 000000000..a83fca6f5
--- /dev/null
+++ b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/StringParameter.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.js b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/StringParameter.js
new file mode 100644
index 000000000..c13cd3585
--- /dev/null
+++ b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/StringParameter.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/StringReturn.d.ts b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/StringReturn.d.ts
new file mode 100644
index 000000000..c6a9f65a4
--- /dev/null
+++ b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/StringReturn.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.js b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/StringReturn.js
new file mode 100644
index 000000000..0208d8cea
--- /dev/null
+++ b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/StringReturn.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/SwiftClass.d.ts b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/SwiftClass.d.ts
new file mode 100644
index 000000000..fd376d57b
--- /dev/null
+++ b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/SwiftClass.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.js b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/SwiftClass.js
new file mode 100644
index 000000000..971b9d69d
--- /dev/null
+++ b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/SwiftClass.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/VoidParameterVoidReturn.d.ts b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/VoidParameterVoidReturn.d.ts
new file mode 100644
index 000000000..be85a00fd
--- /dev/null
+++ b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/VoidParameterVoidReturn.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.js b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/VoidParameterVoidReturn.js
new file mode 100644
index 000000000..a3dae190f
--- /dev/null
+++ b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/VoidParameterVoidReturn.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__/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/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/PackageToJS.swift b/Plugins/PackageToJS/Sources/PackageToJS.swift
index da29164ba..89db66551 100644
--- a/Plugins/PackageToJS/Sources/PackageToJS.swift
+++ b/Plugins/PackageToJS/Sources/PackageToJS.swift
@@ -365,6 +365,10 @@ struct PackagingPlanner {
     let selfPackageDir: BuildPath
     /// The path of this file itself, used to capture changes of planner code
     let selfPath: BuildPath
+    /// The exported API skeletons source files
+    let exportedSkeletons: [BuildPath]
+    /// The imported API skeletons source files
+    let importedSkeletons: [BuildPath]
     /// The directory for the final output
     let outputDir: BuildPath
     /// The directory for intermediate files
@@ -385,6 +389,8 @@ struct PackagingPlanner {
         packageId: String,
         intermediatesDir: BuildPath,
         selfPackageDir: BuildPath,
+        exportedSkeletons: [BuildPath],
+        importedSkeletons: [BuildPath],
         outputDir: BuildPath,
         wasmProductArtifact: BuildPath,
         wasmFilename: String,
@@ -396,6 +402,8 @@ struct PackagingPlanner {
         self.options = options
         self.packageId = packageId
         self.selfPackageDir = selfPackageDir
+        self.exportedSkeletons = exportedSkeletons
+        self.importedSkeletons = importedSkeletons
         self.outputDir = outputDir
         self.intermediatesDir = intermediatesDir
         self.wasmFilename = wasmFilename
@@ -555,6 +563,30 @@ struct PackagingPlanner {
         )
         packageInputs.append(packageJsonTask)
 
+        if exportedSkeletons.count > 0 || importedSkeletons.count > 0 {
+            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"),
@@ -665,6 +697,8 @@ struct PackagingPlanner {
             "USE_SHARED_MEMORY": triple == "wasm32-unknown-wasip1-threads",
             "IS_WASI": triple.hasPrefix("wasm32-unknown-wasi"),
             "USE_WASI_CDN": options.useCDN,
+            "HAS_BRIDGE": exportedSkeletons.count > 0 || importedSkeletons.count > 0,
+            "HAS_IMPORTS": importedSkeletons.count > 0,
         ]
         let constantSubstitutions: [String: String] = [
             "PACKAGE_TO_JS_MODULE_PATH": wasmFilename,
diff --git a/Plugins/PackageToJS/Sources/PackageToJSPlugin.swift b/Plugins/PackageToJS/Sources/PackageToJSPlugin.swift
index 5eb26cdf1..e7f74e974 100644
--- a/Plugins/PackageToJS/Sources/PackageToJSPlugin.swift
+++ b/Plugins/PackageToJS/Sources/PackageToJSPlugin.swift
@@ -173,6 +173,8 @@ struct PackageToJSPlugin: CommandPlugin {
             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 {
@@ -188,6 +190,8 @@ struct PackageToJSPlugin: CommandPlugin {
             options: buildOptions.packageOptions,
             context: context,
             selfPackage: selfPackage,
+            exportedSkeletons: exportedSkeletons,
+            importedSkeletons: importedSkeletons,
             outputDir: outputDir,
             wasmProductArtifact: productArtifact,
             wasmFilename: productArtifact.lastPathComponent
@@ -233,6 +237,9 @@ struct PackageToJSPlugin: CommandPlugin {
             exit(1)
         }
 
+        let skeletonCollector = SkeletonCollector(context: context)
+        let (exportedSkeletons, importedSkeletons) = skeletonCollector.collectFromTests()
+
         // NOTE: Find the product artifact from the default build directory
         //       because PackageManager.BuildResult doesn't include the
         //       product artifact for tests.
@@ -268,6 +275,8 @@ struct PackageToJSPlugin: CommandPlugin {
             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
@@ -631,11 +640,97 @@ private func findPackageInDependencies(package: Package, id: Package.ID) -> Pack
     return visit(package: package)
 }
 
+class SkeletonCollector {
+    private var visitedProducts: Set = []
+    private var visitedTargets: Set = []
+
+    var exportedSkeletons: [URL] = []
+    var importedSkeletons: [URL] = []
+    let exportedSkeletonFile = "ExportSwift.json"
+    let importedSkeletonFile = "ImportTS.json"
+    let context: PluginContext
+
+    init(context: PluginContext) {
+        self.context = context
+    }
+
+    func collectFromProduct(name: String) -> (exportedSkeletons: [URL], importedSkeletons: [URL]) {
+        guard let product = context.package.products.first(where: { $0.name == name }) else {
+            return ([], [])
+        }
+        visit(product: product, package: context.package)
+        return (exportedSkeletons, importedSkeletons)
+    }
+
+    func collectFromTests() -> (exportedSkeletons: [URL], importedSkeletons: [URL]) {
+        let tests = context.package.targets.filter {
+            guard let target = $0 as? SwiftSourceModuleTarget else { return false }
+            return target.kind == .test
+        }
+        for test in tests {
+            visit(target: test, package: context.package)
+        }
+        return (exportedSkeletons, importedSkeletons)
+    }
+
+    private func visit(product: Product, package: Package) {
+        if visitedProducts.contains(product.id) { return }
+        visitedProducts.insert(product.id)
+        for target in product.targets {
+            visit(target: target, package: package)
+        }
+    }
+
+    private func visit(target: Target, package: Package) {
+        if visitedTargets.contains(target.id) { return }
+        visitedTargets.insert(target.id)
+        if let target = target as? SwiftSourceModuleTarget {
+            let directories = [
+                target.directoryURL.appending(path: "Generated/JavaScript"),
+                // context.pluginWorkDirectoryURL: ".build/plugins/PackageToJS/outputs/"
+                // .build/plugins/outputs/exportswift/MyApp/destination/BridgeJS/ExportSwift.json
+                context.pluginWorkDirectoryURL.deletingLastPathComponent().deletingLastPathComponent()
+                    .appending(path: "outputs/\(package.id)/\(target.name)/destination/BridgeJS"),
+            ]
+            for directory in directories {
+                let exportedSkeletonURL = directory.appending(path: exportedSkeletonFile)
+                let importedSkeletonURL = directory.appending(path: importedSkeletonFile)
+                if FileManager.default.fileExists(atPath: exportedSkeletonURL.path) {
+                    exportedSkeletons.append(exportedSkeletonURL)
+                }
+                if FileManager.default.fileExists(atPath: importedSkeletonURL.path) {
+                    importedSkeletons.append(importedSkeletonURL)
+                }
+            }
+        }
+
+        var packageByProduct: [Product.ID: Package] = [:]
+        for packageDependency in package.dependencies {
+            for product in packageDependency.package.products {
+                packageByProduct[product.id] = packageDependency.package
+            }
+        }
+
+        for dependency in target.dependencies {
+            switch dependency {
+            case .product(let product):
+                visit(product: product, package: packageByProduct[product.id]!)
+            case .target(let target):
+                visit(target: target, package: package)
+            @unknown default:
+                continue
+            }
+        }
+    }
+}
+
 extension PackagingPlanner {
     init(
         options: PackageToJS.PackageOptions,
         context: PluginContext,
         selfPackage: Package,
+        exportedSkeletons: [URL],
+        importedSkeletons: [URL],
         outputDir: URL,
         wasmProductArtifact: URL,
         wasmFilename: String
@@ -650,6 +745,8 @@ extension PackagingPlanner {
                 absolute: context.pluginWorkDirectoryURL.appending(path: outputBaseName + ".tmp").path
             ),
             selfPackageDir: BuildPath(absolute: selfPackage.directoryURL.path),
+            exportedSkeletons: exportedSkeletons.map { BuildPath(absolute: $0.path) },
+            importedSkeletons: importedSkeletons.map { BuildPath(absolute: $0.path) },
             outputDir: BuildPath(absolute: outputDir.path),
             wasmProductArtifact: BuildPath(absolute: wasmProductArtifact.path),
             wasmFilename: wasmFilename,
diff --git a/Plugins/PackageToJS/Templates/index.d.ts b/Plugins/PackageToJS/Templates/index.d.ts
index 11d5908c2..77d68efd9 100644
--- a/Plugins/PackageToJS/Templates/index.d.ts
+++ b/Plugins/PackageToJS/Templates/index.d.ts
@@ -1,4 +1,4 @@
-import type { Export, ModuleSource } from './instantiate.js'
+import type { Exports, Imports, ModuleSource } from './instantiate.js'
 
 export type Options = {
     /**
@@ -7,6 +7,12 @@ export type Options = {
      * If not provided, the module will be fetched from the default path.
      */
     module?: ModuleSource
+/* #if HAS_IMPORTS */
+    /**
+     * The imports to use for the module
+     */
+    imports: Imports
+/* #endif */
 }
 
 /**
@@ -17,5 +23,5 @@ export type Options = {
  */
 export declare function init(options?: Options): Promise<{
     instance: WebAssembly.Instance,
-    exports: Export
+    exports: Exports
 }>
diff --git a/Plugins/PackageToJS/Templates/index.js b/Plugins/PackageToJS/Templates/index.js
index 4b8d90f6b..76721511a 100644
--- a/Plugins/PackageToJS/Templates/index.js
+++ b/Plugins/PackageToJS/Templates/index.js
@@ -3,13 +3,23 @@ import { instantiate } from './instantiate.js';
 import { defaultBrowserSetup /* #if USE_SHARED_MEMORY */, createDefaultWorkerFactory /* #endif */} from './platforms/browser.js';
 
 /** @type {import('./index.d').init} */
-export async function init(options = {}) {
+export async function init(_options) {
+    /** @type {import('./index.d').Options} */
+    const options = _options || {
+/* #if HAS_IMPORTS */
+        /** @returns {import('./instantiate.d').Imports} */
+        get imports() { (() => { throw new Error("No imports provided") })() }
+/* #endif */
+    };
     let module = options.module;
     if (!module) {
         module = fetch(new URL("@PACKAGE_TO_JS_MODULE_PATH@", import.meta.url))
     }
     const instantiateOptions = await defaultBrowserSetup({
         module,
+/* #if HAS_IMPORTS */
+        imports: options.imports,
+/* #endif */
 /* #if USE_SHARED_MEMORY */
         spawnWorker: createDefaultWorkerFactory()
 /* #endif */
diff --git a/Plugins/PackageToJS/Templates/instantiate.d.ts b/Plugins/PackageToJS/Templates/instantiate.d.ts
index 3a88b12d0..6c71d1dae 100644
--- a/Plugins/PackageToJS/Templates/instantiate.d.ts
+++ b/Plugins/PackageToJS/Templates/instantiate.d.ts
@@ -1,11 +1,12 @@
 import type { /* #if USE_SHARED_MEMORY */SwiftRuntimeThreadChannel, /* #endif */SwiftRuntime } from "./runtime.js";
 
-export type Import = {
-    // TODO: Generate type from imported .d.ts files
-}
-export type Export = {
-    // TODO: Generate type from .swift files
-}
+/* #if HAS_BRIDGE */
+// @ts-ignore
+export type { Imports, Exports } from "./bridge.js";
+/* #else */
+export type Imports = {}
+export type Exports = {}
+/* #endif */
 
 /**
  * The path to the WebAssembly module relative to the root of the package
@@ -59,10 +60,12 @@ export type InstantiateOptions = {
      * The WebAssembly module to instantiate
      */
     module: ModuleSource,
+/* #if HAS_IMPORTS */
     /**
      * The imports provided by the embedder
      */
-    imports: Import,
+    imports: Imports,
+/* #endif */
 /* #if IS_WASI */
     /**
      * The WASI implementation to use
@@ -86,7 +89,11 @@ export type InstantiateOptions = {
      * Add imports to the WebAssembly import object
      * @param imports - The imports to add
      */
-    addToCoreImports?: (imports: WebAssembly.Imports) => void
+    addToCoreImports?: (
+        imports: WebAssembly.Imports,
+        getInstance: () => WebAssembly.Instance | null,
+        getExports: () => Exports | null,
+    ) => void
 }
 
 /**
@@ -95,7 +102,7 @@ export type InstantiateOptions = {
 export declare function instantiate(options: InstantiateOptions): Promise<{
     instance: WebAssembly.Instance,
     swift: SwiftRuntime,
-    exports: Export
+    exports: Exports
 }>
 
 /**
@@ -104,5 +111,5 @@ export declare function instantiate(options: InstantiateOptions): Promise<{
 export declare function instantiateForThread(tid: number, startArg: number, options: InstantiateOptions): Promise<{
     instance: WebAssembly.Instance,
     swift: SwiftRuntime,
-    exports: Export
+    exports: Exports
 }>
diff --git a/Plugins/PackageToJS/Templates/instantiate.js b/Plugins/PackageToJS/Templates/instantiate.js
index a239a79c9..2a41d48c9 100644
--- a/Plugins/PackageToJS/Templates/instantiate.js
+++ b/Plugins/PackageToJS/Templates/instantiate.js
@@ -13,19 +13,28 @@ export const MEMORY_TYPE = {
 }
 /* #endif */
 
+/* #if HAS_BRIDGE */
+// @ts-ignore
+import { createInstantiator } from "./bridge.js"
+/* #else */
 /**
  * @param {import('./instantiate.d').InstantiateOptions} options
+ * @param {any} swift
  */
-async function createInstantiator(options) {
+async function createInstantiator(options, swift) {
     return {
         /** @param {WebAssembly.Imports} importObject */
         addImports: (importObject) => {},
         /** @param {WebAssembly.Instance} instance */
+        setInstance: (instance) => {},
+        /** @param {WebAssembly.Instance} instance */
         createExports: (instance) => {
             return {};
         },
     }
 }
+/* #endif */
+
 /** @type {import('./instantiate.d').instantiate} */
 export async function instantiate(
     options
@@ -58,13 +67,13 @@ async function _instantiate(
 /* #if IS_WASI */
     const { wasi } = options;
 /* #endif */
-    const instantiator = await createInstantiator(options);
     const swift = new SwiftRuntime({
 /* #if USE_SHARED_MEMORY */
         sharedMemory: true,
         threadChannel: options.threadChannel,
 /* #endif */
     });
+    const instantiator = await createInstantiator(options, swift);
 
     /** @type {WebAssembly.Imports} */
     const importObject = {
@@ -84,10 +93,11 @@ async function _instantiate(
 /* #endif */
     };
     instantiator.addImports(importObject);
-    options.addToCoreImports?.(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);
@@ -108,10 +118,12 @@ async function _instantiate(
     }
 
     swift.setInstance(instance);
+    instantiator.setInstance(instance);
+    exports = instantiator.createExports(instance);
 
     return {
         instance,
         swift,
-        exports: instantiator.createExports(instance),
+        exports,
     }
 }
diff --git a/Plugins/PackageToJS/Templates/platforms/browser.d.ts b/Plugins/PackageToJS/Templates/platforms/browser.d.ts
index a8089f8af..b851c2283 100644
--- a/Plugins/PackageToJS/Templates/platforms/browser.d.ts
+++ b/Plugins/PackageToJS/Templates/platforms/browser.d.ts
@@ -1,4 +1,4 @@
-import type { InstantiateOptions, ModuleSource } from "../instantiate.js"
+import type { InstantiateOptions, ModuleSource/* #if HAS_IMPORTS */, Imports/* #endif */ } from "../instantiate.js"
 
 export function defaultBrowserSetup(options: {
     module: ModuleSource,
@@ -7,6 +7,9 @@ export function defaultBrowserSetup(options: {
     onStdoutLine?: (line: string) => void,
     onStderrLine?: (line: string) => void,
 /* #endif */
+/* #if HAS_IMPORTS */
+    imports: Imports,
+/* #endif */
 /* #if USE_SHARED_MEMORY */
     spawnWorker: (module: WebAssembly.Module, memory: WebAssembly.Memory, startArg: any) => Worker,
 /* #endif */
diff --git a/Plugins/PackageToJS/Templates/platforms/browser.js b/Plugins/PackageToJS/Templates/platforms/browser.js
index b1e469fb0..9afd5c94a 100644
--- a/Plugins/PackageToJS/Templates/platforms/browser.js
+++ b/Plugins/PackageToJS/Templates/platforms/browser.js
@@ -123,7 +123,9 @@ export async function defaultBrowserSetup(options) {
 
     return {
         module: options.module,
-        imports: {},
+/* #if HAS_IMPORTS */
+        imports: options.imports,
+/* #endif */
 /* #if IS_WASI */
         wasi: Object.assign(wasi, {
             setInstance(instance) {
diff --git a/Plugins/PackageToJS/Tests/ExampleTests.swift b/Plugins/PackageToJS/Tests/ExampleTests.swift
index c51cbfa96..7c41cf3bf 100644
--- a/Plugins/PackageToJS/Tests/ExampleTests.swift
+++ b/Plugins/PackageToJS/Tests/ExampleTests.swift
@@ -73,6 +73,25 @@ extension Trait where Self == ConditionTrait {
                 enumerator.skipDescendants()
                 continue
             }
+
+            // Copy symbolic links
+            if let resourceValues = try? sourcePath.resourceValues(forKeys: [.isSymbolicLinkKey]),
+                resourceValues.isSymbolicLink == true
+            {
+                try FileManager.default.createDirectory(
+                    at: destinationPath.deletingLastPathComponent(),
+                    withIntermediateDirectories: true,
+                    attributes: nil
+                )
+                let linkDestination = try! FileManager.default.destinationOfSymbolicLink(atPath: sourcePath.path)
+                try FileManager.default.createSymbolicLink(
+                    atPath: destinationPath.path,
+                    withDestinationPath: linkDestination
+                )
+                enumerator.skipDescendants()
+                continue
+            }
+
             // Skip directories
             var isDirectory: ObjCBool = false
             if FileManager.default.fileExists(atPath: sourcePath.path, isDirectory: &isDirectory) {
diff --git a/Plugins/PackageToJS/Tests/PackagingPlannerTests.swift b/Plugins/PackageToJS/Tests/PackagingPlannerTests.swift
index c69dcb66f..03fc4c9cc 100644
--- a/Plugins/PackageToJS/Tests/PackagingPlannerTests.swift
+++ b/Plugins/PackageToJS/Tests/PackagingPlannerTests.swift
@@ -65,6 +65,8 @@ import Testing
             packageId: "test",
             intermediatesDir: BuildPath(prefix: "INTERMEDIATES"),
             selfPackageDir: BuildPath(prefix: "SELF_PACKAGE"),
+            exportedSkeletons: [],
+            importedSkeletons: [],
             outputDir: BuildPath(prefix: "OUTPUT"),
             wasmProductArtifact: BuildPath(prefix: "WASM_PRODUCT_ARTIFACT"),
             wasmFilename: "main.wasm",
@@ -94,6 +96,8 @@ import Testing
             packageId: "test",
             intermediatesDir: BuildPath(prefix: "INTERMEDIATES"),
             selfPackageDir: BuildPath(prefix: "SELF_PACKAGE"),
+            exportedSkeletons: [],
+            importedSkeletons: [],
             outputDir: BuildPath(prefix: "OUTPUT"),
             wasmProductArtifact: BuildPath(prefix: "WASM_PRODUCT_ARTIFACT"),
             wasmFilename: "main.wasm",
diff --git a/Plugins/PackageToJS/Tests/__Snapshots__/PackagingPlannerTests/planBuild_debug.json b/Plugins/PackageToJS/Tests/__Snapshots__/PackagingPlannerTests/planBuild_debug.json
index e525d1347..13768da75 100644
--- a/Plugins/PackageToJS/Tests/__Snapshots__/PackagingPlannerTests/planBuild_debug.json
+++ b/Plugins/PackageToJS/Tests/__Snapshots__/PackagingPlannerTests/planBuild_debug.json
@@ -48,7 +48,7 @@
       "$INTERMEDIATES\/wasm-imports.json"
     ],
     "output" : "$OUTPUT\/index.d.ts",
-    "salt" : "eyJjb25kaXRpb25zIjp7IklTX1dBU0kiOnRydWUsIlVTRV9TSEFSRURfTUVNT1JZIjpmYWxzZSwiVVNFX1dBU0lfQ0ROIjpmYWxzZX0sInN1YnN0aXR1dGlvbnMiOnsiUEFDS0FHRV9UT19KU19NT0RVTEVfUEFUSCI6Im1haW4ud2FzbSIsIlBBQ0tBR0VfVE9fSlNfUEFDS0FHRV9OQU1FIjoidGVzdCJ9fQ==",
+    "salt" : "eyJjb25kaXRpb25zIjp7IkhBU19CUklER0UiOmZhbHNlLCJIQVNfSU1QT1JUUyI6ZmFsc2UsIklTX1dBU0kiOnRydWUsIlVTRV9TSEFSRURfTUVNT1JZIjpmYWxzZSwiVVNFX1dBU0lfQ0ROIjpmYWxzZX0sInN1YnN0aXR1dGlvbnMiOnsiUEFDS0FHRV9UT19KU19NT0RVTEVfUEFUSCI6Im1haW4ud2FzbSIsIlBBQ0tBR0VfVE9fSlNfUEFDS0FHRV9OQU1FIjoidGVzdCJ9fQ==",
     "wants" : [
       "$OUTPUT",
       "$OUTPUT\/platforms",
@@ -65,7 +65,7 @@
       "$INTERMEDIATES\/wasm-imports.json"
     ],
     "output" : "$OUTPUT\/index.js",
-    "salt" : "eyJjb25kaXRpb25zIjp7IklTX1dBU0kiOnRydWUsIlVTRV9TSEFSRURfTUVNT1JZIjpmYWxzZSwiVVNFX1dBU0lfQ0ROIjpmYWxzZX0sInN1YnN0aXR1dGlvbnMiOnsiUEFDS0FHRV9UT19KU19NT0RVTEVfUEFUSCI6Im1haW4ud2FzbSIsIlBBQ0tBR0VfVE9fSlNfUEFDS0FHRV9OQU1FIjoidGVzdCJ9fQ==",
+    "salt" : "eyJjb25kaXRpb25zIjp7IkhBU19CUklER0UiOmZhbHNlLCJIQVNfSU1QT1JUUyI6ZmFsc2UsIklTX1dBU0kiOnRydWUsIlVTRV9TSEFSRURfTUVNT1JZIjpmYWxzZSwiVVNFX1dBU0lfQ0ROIjpmYWxzZX0sInN1YnN0aXR1dGlvbnMiOnsiUEFDS0FHRV9UT19KU19NT0RVTEVfUEFUSCI6Im1haW4ud2FzbSIsIlBBQ0tBR0VfVE9fSlNfUEFDS0FHRV9OQU1FIjoidGVzdCJ9fQ==",
     "wants" : [
       "$OUTPUT",
       "$OUTPUT\/platforms",
@@ -82,7 +82,7 @@
       "$INTERMEDIATES\/wasm-imports.json"
     ],
     "output" : "$OUTPUT\/instantiate.d.ts",
-    "salt" : "eyJjb25kaXRpb25zIjp7IklTX1dBU0kiOnRydWUsIlVTRV9TSEFSRURfTUVNT1JZIjpmYWxzZSwiVVNFX1dBU0lfQ0ROIjpmYWxzZX0sInN1YnN0aXR1dGlvbnMiOnsiUEFDS0FHRV9UT19KU19NT0RVTEVfUEFUSCI6Im1haW4ud2FzbSIsIlBBQ0tBR0VfVE9fSlNfUEFDS0FHRV9OQU1FIjoidGVzdCJ9fQ==",
+    "salt" : "eyJjb25kaXRpb25zIjp7IkhBU19CUklER0UiOmZhbHNlLCJIQVNfSU1QT1JUUyI6ZmFsc2UsIklTX1dBU0kiOnRydWUsIlVTRV9TSEFSRURfTUVNT1JZIjpmYWxzZSwiVVNFX1dBU0lfQ0ROIjpmYWxzZX0sInN1YnN0aXR1dGlvbnMiOnsiUEFDS0FHRV9UT19KU19NT0RVTEVfUEFUSCI6Im1haW4ud2FzbSIsIlBBQ0tBR0VfVE9fSlNfUEFDS0FHRV9OQU1FIjoidGVzdCJ9fQ==",
     "wants" : [
       "$OUTPUT",
       "$OUTPUT\/platforms",
@@ -99,7 +99,7 @@
       "$INTERMEDIATES\/wasm-imports.json"
     ],
     "output" : "$OUTPUT\/instantiate.js",
-    "salt" : "eyJjb25kaXRpb25zIjp7IklTX1dBU0kiOnRydWUsIlVTRV9TSEFSRURfTUVNT1JZIjpmYWxzZSwiVVNFX1dBU0lfQ0ROIjpmYWxzZX0sInN1YnN0aXR1dGlvbnMiOnsiUEFDS0FHRV9UT19KU19NT0RVTEVfUEFUSCI6Im1haW4ud2FzbSIsIlBBQ0tBR0VfVE9fSlNfUEFDS0FHRV9OQU1FIjoidGVzdCJ9fQ==",
+    "salt" : "eyJjb25kaXRpb25zIjp7IkhBU19CUklER0UiOmZhbHNlLCJIQVNfSU1QT1JUUyI6ZmFsc2UsIklTX1dBU0kiOnRydWUsIlVTRV9TSEFSRURfTUVNT1JZIjpmYWxzZSwiVVNFX1dBU0lfQ0ROIjpmYWxzZX0sInN1YnN0aXR1dGlvbnMiOnsiUEFDS0FHRV9UT19KU19NT0RVTEVfUEFUSCI6Im1haW4ud2FzbSIsIlBBQ0tBR0VfVE9fSlNfUEFDS0FHRV9OQU1FIjoidGVzdCJ9fQ==",
     "wants" : [
       "$OUTPUT",
       "$OUTPUT\/platforms",
@@ -128,7 +128,7 @@
       "$SELF_PACKAGE\/Plugins\/PackageToJS\/Templates\/package.json"
     ],
     "output" : "$OUTPUT\/package.json",
-    "salt" : "eyJjb25kaXRpb25zIjp7IklTX1dBU0kiOnRydWUsIlVTRV9TSEFSRURfTUVNT1JZIjpmYWxzZSwiVVNFX1dBU0lfQ0ROIjpmYWxzZX0sInN1YnN0aXR1dGlvbnMiOnsiUEFDS0FHRV9UT19KU19NT0RVTEVfUEFUSCI6Im1haW4ud2FzbSIsIlBBQ0tBR0VfVE9fSlNfUEFDS0FHRV9OQU1FIjoidGVzdCJ9fQ==",
+    "salt" : "eyJjb25kaXRpb25zIjp7IkhBU19CUklER0UiOmZhbHNlLCJIQVNfSU1QT1JUUyI6ZmFsc2UsIklTX1dBU0kiOnRydWUsIlVTRV9TSEFSRURfTUVNT1JZIjpmYWxzZSwiVVNFX1dBU0lfQ0ROIjpmYWxzZX0sInN1YnN0aXR1dGlvbnMiOnsiUEFDS0FHRV9UT19KU19NT0RVTEVfUEFUSCI6Im1haW4ud2FzbSIsIlBBQ0tBR0VfVE9fSlNfUEFDS0FHRV9OQU1FIjoidGVzdCJ9fQ==",
     "wants" : [
       "$OUTPUT"
     ]
@@ -155,7 +155,7 @@
       "$INTERMEDIATES\/wasm-imports.json"
     ],
     "output" : "$OUTPUT\/platforms\/browser.d.ts",
-    "salt" : "eyJjb25kaXRpb25zIjp7IklTX1dBU0kiOnRydWUsIlVTRV9TSEFSRURfTUVNT1JZIjpmYWxzZSwiVVNFX1dBU0lfQ0ROIjpmYWxzZX0sInN1YnN0aXR1dGlvbnMiOnsiUEFDS0FHRV9UT19KU19NT0RVTEVfUEFUSCI6Im1haW4ud2FzbSIsIlBBQ0tBR0VfVE9fSlNfUEFDS0FHRV9OQU1FIjoidGVzdCJ9fQ==",
+    "salt" : "eyJjb25kaXRpb25zIjp7IkhBU19CUklER0UiOmZhbHNlLCJIQVNfSU1QT1JUUyI6ZmFsc2UsIklTX1dBU0kiOnRydWUsIlVTRV9TSEFSRURfTUVNT1JZIjpmYWxzZSwiVVNFX1dBU0lfQ0ROIjpmYWxzZX0sInN1YnN0aXR1dGlvbnMiOnsiUEFDS0FHRV9UT19KU19NT0RVTEVfUEFUSCI6Im1haW4ud2FzbSIsIlBBQ0tBR0VfVE9fSlNfUEFDS0FHRV9OQU1FIjoidGVzdCJ9fQ==",
     "wants" : [
       "$OUTPUT",
       "$OUTPUT\/platforms",
@@ -172,7 +172,7 @@
       "$INTERMEDIATES\/wasm-imports.json"
     ],
     "output" : "$OUTPUT\/platforms\/browser.js",
-    "salt" : "eyJjb25kaXRpb25zIjp7IklTX1dBU0kiOnRydWUsIlVTRV9TSEFSRURfTUVNT1JZIjpmYWxzZSwiVVNFX1dBU0lfQ0ROIjpmYWxzZX0sInN1YnN0aXR1dGlvbnMiOnsiUEFDS0FHRV9UT19KU19NT0RVTEVfUEFUSCI6Im1haW4ud2FzbSIsIlBBQ0tBR0VfVE9fSlNfUEFDS0FHRV9OQU1FIjoidGVzdCJ9fQ==",
+    "salt" : "eyJjb25kaXRpb25zIjp7IkhBU19CUklER0UiOmZhbHNlLCJIQVNfSU1QT1JUUyI6ZmFsc2UsIklTX1dBU0kiOnRydWUsIlVTRV9TSEFSRURfTUVNT1JZIjpmYWxzZSwiVVNFX1dBU0lfQ0ROIjpmYWxzZX0sInN1YnN0aXR1dGlvbnMiOnsiUEFDS0FHRV9UT19KU19NT0RVTEVfUEFUSCI6Im1haW4ud2FzbSIsIlBBQ0tBR0VfVE9fSlNfUEFDS0FHRV9OQU1FIjoidGVzdCJ9fQ==",
     "wants" : [
       "$OUTPUT",
       "$OUTPUT\/platforms",
@@ -189,7 +189,7 @@
       "$INTERMEDIATES\/wasm-imports.json"
     ],
     "output" : "$OUTPUT\/platforms\/browser.worker.js",
-    "salt" : "eyJjb25kaXRpb25zIjp7IklTX1dBU0kiOnRydWUsIlVTRV9TSEFSRURfTUVNT1JZIjpmYWxzZSwiVVNFX1dBU0lfQ0ROIjpmYWxzZX0sInN1YnN0aXR1dGlvbnMiOnsiUEFDS0FHRV9UT19KU19NT0RVTEVfUEFUSCI6Im1haW4ud2FzbSIsIlBBQ0tBR0VfVE9fSlNfUEFDS0FHRV9OQU1FIjoidGVzdCJ9fQ==",
+    "salt" : "eyJjb25kaXRpb25zIjp7IkhBU19CUklER0UiOmZhbHNlLCJIQVNfSU1QT1JUUyI6ZmFsc2UsIklTX1dBU0kiOnRydWUsIlVTRV9TSEFSRURfTUVNT1JZIjpmYWxzZSwiVVNFX1dBU0lfQ0ROIjpmYWxzZX0sInN1YnN0aXR1dGlvbnMiOnsiUEFDS0FHRV9UT19KU19NT0RVTEVfUEFUSCI6Im1haW4ud2FzbSIsIlBBQ0tBR0VfVE9fSlNfUEFDS0FHRV9OQU1FIjoidGVzdCJ9fQ==",
     "wants" : [
       "$OUTPUT",
       "$OUTPUT\/platforms",
@@ -206,7 +206,7 @@
       "$INTERMEDIATES\/wasm-imports.json"
     ],
     "output" : "$OUTPUT\/platforms\/node.d.ts",
-    "salt" : "eyJjb25kaXRpb25zIjp7IklTX1dBU0kiOnRydWUsIlVTRV9TSEFSRURfTUVNT1JZIjpmYWxzZSwiVVNFX1dBU0lfQ0ROIjpmYWxzZX0sInN1YnN0aXR1dGlvbnMiOnsiUEFDS0FHRV9UT19KU19NT0RVTEVfUEFUSCI6Im1haW4ud2FzbSIsIlBBQ0tBR0VfVE9fSlNfUEFDS0FHRV9OQU1FIjoidGVzdCJ9fQ==",
+    "salt" : "eyJjb25kaXRpb25zIjp7IkhBU19CUklER0UiOmZhbHNlLCJIQVNfSU1QT1JUUyI6ZmFsc2UsIklTX1dBU0kiOnRydWUsIlVTRV9TSEFSRURfTUVNT1JZIjpmYWxzZSwiVVNFX1dBU0lfQ0ROIjpmYWxzZX0sInN1YnN0aXR1dGlvbnMiOnsiUEFDS0FHRV9UT19KU19NT0RVTEVfUEFUSCI6Im1haW4ud2FzbSIsIlBBQ0tBR0VfVE9fSlNfUEFDS0FHRV9OQU1FIjoidGVzdCJ9fQ==",
     "wants" : [
       "$OUTPUT",
       "$OUTPUT\/platforms",
@@ -223,7 +223,7 @@
       "$INTERMEDIATES\/wasm-imports.json"
     ],
     "output" : "$OUTPUT\/platforms\/node.js",
-    "salt" : "eyJjb25kaXRpb25zIjp7IklTX1dBU0kiOnRydWUsIlVTRV9TSEFSRURfTUVNT1JZIjpmYWxzZSwiVVNFX1dBU0lfQ0ROIjpmYWxzZX0sInN1YnN0aXR1dGlvbnMiOnsiUEFDS0FHRV9UT19KU19NT0RVTEVfUEFUSCI6Im1haW4ud2FzbSIsIlBBQ0tBR0VfVE9fSlNfUEFDS0FHRV9OQU1FIjoidGVzdCJ9fQ==",
+    "salt" : "eyJjb25kaXRpb25zIjp7IkhBU19CUklER0UiOmZhbHNlLCJIQVNfSU1QT1JUUyI6ZmFsc2UsIklTX1dBU0kiOnRydWUsIlVTRV9TSEFSRURfTUVNT1JZIjpmYWxzZSwiVVNFX1dBU0lfQ0ROIjpmYWxzZX0sInN1YnN0aXR1dGlvbnMiOnsiUEFDS0FHRV9UT19KU19NT0RVTEVfUEFUSCI6Im1haW4ud2FzbSIsIlBBQ0tBR0VfVE9fSlNfUEFDS0FHRV9OQU1FIjoidGVzdCJ9fQ==",
     "wants" : [
       "$OUTPUT",
       "$OUTPUT\/platforms",
@@ -240,7 +240,7 @@
       "$INTERMEDIATES\/wasm-imports.json"
     ],
     "output" : "$OUTPUT\/runtime.d.ts",
-    "salt" : "eyJjb25kaXRpb25zIjp7IklTX1dBU0kiOnRydWUsIlVTRV9TSEFSRURfTUVNT1JZIjpmYWxzZSwiVVNFX1dBU0lfQ0ROIjpmYWxzZX0sInN1YnN0aXR1dGlvbnMiOnsiUEFDS0FHRV9UT19KU19NT0RVTEVfUEFUSCI6Im1haW4ud2FzbSIsIlBBQ0tBR0VfVE9fSlNfUEFDS0FHRV9OQU1FIjoidGVzdCJ9fQ==",
+    "salt" : "eyJjb25kaXRpb25zIjp7IkhBU19CUklER0UiOmZhbHNlLCJIQVNfSU1QT1JUUyI6ZmFsc2UsIklTX1dBU0kiOnRydWUsIlVTRV9TSEFSRURfTUVNT1JZIjpmYWxzZSwiVVNFX1dBU0lfQ0ROIjpmYWxzZX0sInN1YnN0aXR1dGlvbnMiOnsiUEFDS0FHRV9UT19KU19NT0RVTEVfUEFUSCI6Im1haW4ud2FzbSIsIlBBQ0tBR0VfVE9fSlNfUEFDS0FHRV9OQU1FIjoidGVzdCJ9fQ==",
     "wants" : [
       "$OUTPUT",
       "$OUTPUT\/platforms",
@@ -257,7 +257,7 @@
       "$INTERMEDIATES\/wasm-imports.json"
     ],
     "output" : "$OUTPUT\/runtime.js",
-    "salt" : "eyJjb25kaXRpb25zIjp7IklTX1dBU0kiOnRydWUsIlVTRV9TSEFSRURfTUVNT1JZIjpmYWxzZSwiVVNFX1dBU0lfQ0ROIjpmYWxzZX0sInN1YnN0aXR1dGlvbnMiOnsiUEFDS0FHRV9UT19KU19NT0RVTEVfUEFUSCI6Im1haW4ud2FzbSIsIlBBQ0tBR0VfVE9fSlNfUEFDS0FHRV9OQU1FIjoidGVzdCJ9fQ==",
+    "salt" : "eyJjb25kaXRpb25zIjp7IkhBU19CUklER0UiOmZhbHNlLCJIQVNfSU1QT1JUUyI6ZmFsc2UsIklTX1dBU0kiOnRydWUsIlVTRV9TSEFSRURfTUVNT1JZIjpmYWxzZSwiVVNFX1dBU0lfQ0ROIjpmYWxzZX0sInN1YnN0aXR1dGlvbnMiOnsiUEFDS0FHRV9UT19KU19NT0RVTEVfUEFUSCI6Im1haW4ud2FzbSIsIlBBQ0tBR0VfVE9fSlNfUEFDS0FHRV9OQU1FIjoidGVzdCJ9fQ==",
     "wants" : [
       "$OUTPUT",
       "$OUTPUT\/platforms",
diff --git a/Plugins/PackageToJS/Tests/__Snapshots__/PackagingPlannerTests/planBuild_release.json b/Plugins/PackageToJS/Tests/__Snapshots__/PackagingPlannerTests/planBuild_release.json
index 6e3480c59..ccfbc35cc 100644
--- a/Plugins/PackageToJS/Tests/__Snapshots__/PackagingPlannerTests/planBuild_release.json
+++ b/Plugins/PackageToJS/Tests/__Snapshots__/PackagingPlannerTests/planBuild_release.json
@@ -62,7 +62,7 @@
       "$INTERMEDIATES\/wasm-imports.json"
     ],
     "output" : "$OUTPUT\/index.d.ts",
-    "salt" : "eyJjb25kaXRpb25zIjp7IklTX1dBU0kiOnRydWUsIlVTRV9TSEFSRURfTUVNT1JZIjpmYWxzZSwiVVNFX1dBU0lfQ0ROIjpmYWxzZX0sInN1YnN0aXR1dGlvbnMiOnsiUEFDS0FHRV9UT19KU19NT0RVTEVfUEFUSCI6Im1haW4ud2FzbSIsIlBBQ0tBR0VfVE9fSlNfUEFDS0FHRV9OQU1FIjoidGVzdCJ9fQ==",
+    "salt" : "eyJjb25kaXRpb25zIjp7IkhBU19CUklER0UiOmZhbHNlLCJIQVNfSU1QT1JUUyI6ZmFsc2UsIklTX1dBU0kiOnRydWUsIlVTRV9TSEFSRURfTUVNT1JZIjpmYWxzZSwiVVNFX1dBU0lfQ0ROIjpmYWxzZX0sInN1YnN0aXR1dGlvbnMiOnsiUEFDS0FHRV9UT19KU19NT0RVTEVfUEFUSCI6Im1haW4ud2FzbSIsIlBBQ0tBR0VfVE9fSlNfUEFDS0FHRV9OQU1FIjoidGVzdCJ9fQ==",
     "wants" : [
       "$OUTPUT",
       "$OUTPUT\/platforms",
@@ -79,7 +79,7 @@
       "$INTERMEDIATES\/wasm-imports.json"
     ],
     "output" : "$OUTPUT\/index.js",
-    "salt" : "eyJjb25kaXRpb25zIjp7IklTX1dBU0kiOnRydWUsIlVTRV9TSEFSRURfTUVNT1JZIjpmYWxzZSwiVVNFX1dBU0lfQ0ROIjpmYWxzZX0sInN1YnN0aXR1dGlvbnMiOnsiUEFDS0FHRV9UT19KU19NT0RVTEVfUEFUSCI6Im1haW4ud2FzbSIsIlBBQ0tBR0VfVE9fSlNfUEFDS0FHRV9OQU1FIjoidGVzdCJ9fQ==",
+    "salt" : "eyJjb25kaXRpb25zIjp7IkhBU19CUklER0UiOmZhbHNlLCJIQVNfSU1QT1JUUyI6ZmFsc2UsIklTX1dBU0kiOnRydWUsIlVTRV9TSEFSRURfTUVNT1JZIjpmYWxzZSwiVVNFX1dBU0lfQ0ROIjpmYWxzZX0sInN1YnN0aXR1dGlvbnMiOnsiUEFDS0FHRV9UT19KU19NT0RVTEVfUEFUSCI6Im1haW4ud2FzbSIsIlBBQ0tBR0VfVE9fSlNfUEFDS0FHRV9OQU1FIjoidGVzdCJ9fQ==",
     "wants" : [
       "$OUTPUT",
       "$OUTPUT\/platforms",
@@ -96,7 +96,7 @@
       "$INTERMEDIATES\/wasm-imports.json"
     ],
     "output" : "$OUTPUT\/instantiate.d.ts",
-    "salt" : "eyJjb25kaXRpb25zIjp7IklTX1dBU0kiOnRydWUsIlVTRV9TSEFSRURfTUVNT1JZIjpmYWxzZSwiVVNFX1dBU0lfQ0ROIjpmYWxzZX0sInN1YnN0aXR1dGlvbnMiOnsiUEFDS0FHRV9UT19KU19NT0RVTEVfUEFUSCI6Im1haW4ud2FzbSIsIlBBQ0tBR0VfVE9fSlNfUEFDS0FHRV9OQU1FIjoidGVzdCJ9fQ==",
+    "salt" : "eyJjb25kaXRpb25zIjp7IkhBU19CUklER0UiOmZhbHNlLCJIQVNfSU1QT1JUUyI6ZmFsc2UsIklTX1dBU0kiOnRydWUsIlVTRV9TSEFSRURfTUVNT1JZIjpmYWxzZSwiVVNFX1dBU0lfQ0ROIjpmYWxzZX0sInN1YnN0aXR1dGlvbnMiOnsiUEFDS0FHRV9UT19KU19NT0RVTEVfUEFUSCI6Im1haW4ud2FzbSIsIlBBQ0tBR0VfVE9fSlNfUEFDS0FHRV9OQU1FIjoidGVzdCJ9fQ==",
     "wants" : [
       "$OUTPUT",
       "$OUTPUT\/platforms",
@@ -113,7 +113,7 @@
       "$INTERMEDIATES\/wasm-imports.json"
     ],
     "output" : "$OUTPUT\/instantiate.js",
-    "salt" : "eyJjb25kaXRpb25zIjp7IklTX1dBU0kiOnRydWUsIlVTRV9TSEFSRURfTUVNT1JZIjpmYWxzZSwiVVNFX1dBU0lfQ0ROIjpmYWxzZX0sInN1YnN0aXR1dGlvbnMiOnsiUEFDS0FHRV9UT19KU19NT0RVTEVfUEFUSCI6Im1haW4ud2FzbSIsIlBBQ0tBR0VfVE9fSlNfUEFDS0FHRV9OQU1FIjoidGVzdCJ9fQ==",
+    "salt" : "eyJjb25kaXRpb25zIjp7IkhBU19CUklER0UiOmZhbHNlLCJIQVNfSU1QT1JUUyI6ZmFsc2UsIklTX1dBU0kiOnRydWUsIlVTRV9TSEFSRURfTUVNT1JZIjpmYWxzZSwiVVNFX1dBU0lfQ0ROIjpmYWxzZX0sInN1YnN0aXR1dGlvbnMiOnsiUEFDS0FHRV9UT19KU19NT0RVTEVfUEFUSCI6Im1haW4ud2FzbSIsIlBBQ0tBR0VfVE9fSlNfUEFDS0FHRV9OQU1FIjoidGVzdCJ9fQ==",
     "wants" : [
       "$OUTPUT",
       "$OUTPUT\/platforms",
@@ -143,7 +143,7 @@
       "$SELF_PACKAGE\/Plugins\/PackageToJS\/Templates\/package.json"
     ],
     "output" : "$OUTPUT\/package.json",
-    "salt" : "eyJjb25kaXRpb25zIjp7IklTX1dBU0kiOnRydWUsIlVTRV9TSEFSRURfTUVNT1JZIjpmYWxzZSwiVVNFX1dBU0lfQ0ROIjpmYWxzZX0sInN1YnN0aXR1dGlvbnMiOnsiUEFDS0FHRV9UT19KU19NT0RVTEVfUEFUSCI6Im1haW4ud2FzbSIsIlBBQ0tBR0VfVE9fSlNfUEFDS0FHRV9OQU1FIjoidGVzdCJ9fQ==",
+    "salt" : "eyJjb25kaXRpb25zIjp7IkhBU19CUklER0UiOmZhbHNlLCJIQVNfSU1QT1JUUyI6ZmFsc2UsIklTX1dBU0kiOnRydWUsIlVTRV9TSEFSRURfTUVNT1JZIjpmYWxzZSwiVVNFX1dBU0lfQ0ROIjpmYWxzZX0sInN1YnN0aXR1dGlvbnMiOnsiUEFDS0FHRV9UT19KU19NT0RVTEVfUEFUSCI6Im1haW4ud2FzbSIsIlBBQ0tBR0VfVE9fSlNfUEFDS0FHRV9OQU1FIjoidGVzdCJ9fQ==",
     "wants" : [
       "$OUTPUT"
     ]
@@ -170,7 +170,7 @@
       "$INTERMEDIATES\/wasm-imports.json"
     ],
     "output" : "$OUTPUT\/platforms\/browser.d.ts",
-    "salt" : "eyJjb25kaXRpb25zIjp7IklTX1dBU0kiOnRydWUsIlVTRV9TSEFSRURfTUVNT1JZIjpmYWxzZSwiVVNFX1dBU0lfQ0ROIjpmYWxzZX0sInN1YnN0aXR1dGlvbnMiOnsiUEFDS0FHRV9UT19KU19NT0RVTEVfUEFUSCI6Im1haW4ud2FzbSIsIlBBQ0tBR0VfVE9fSlNfUEFDS0FHRV9OQU1FIjoidGVzdCJ9fQ==",
+    "salt" : "eyJjb25kaXRpb25zIjp7IkhBU19CUklER0UiOmZhbHNlLCJIQVNfSU1QT1JUUyI6ZmFsc2UsIklTX1dBU0kiOnRydWUsIlVTRV9TSEFSRURfTUVNT1JZIjpmYWxzZSwiVVNFX1dBU0lfQ0ROIjpmYWxzZX0sInN1YnN0aXR1dGlvbnMiOnsiUEFDS0FHRV9UT19KU19NT0RVTEVfUEFUSCI6Im1haW4ud2FzbSIsIlBBQ0tBR0VfVE9fSlNfUEFDS0FHRV9OQU1FIjoidGVzdCJ9fQ==",
     "wants" : [
       "$OUTPUT",
       "$OUTPUT\/platforms",
@@ -187,7 +187,7 @@
       "$INTERMEDIATES\/wasm-imports.json"
     ],
     "output" : "$OUTPUT\/platforms\/browser.js",
-    "salt" : "eyJjb25kaXRpb25zIjp7IklTX1dBU0kiOnRydWUsIlVTRV9TSEFSRURfTUVNT1JZIjpmYWxzZSwiVVNFX1dBU0lfQ0ROIjpmYWxzZX0sInN1YnN0aXR1dGlvbnMiOnsiUEFDS0FHRV9UT19KU19NT0RVTEVfUEFUSCI6Im1haW4ud2FzbSIsIlBBQ0tBR0VfVE9fSlNfUEFDS0FHRV9OQU1FIjoidGVzdCJ9fQ==",
+    "salt" : "eyJjb25kaXRpb25zIjp7IkhBU19CUklER0UiOmZhbHNlLCJIQVNfSU1QT1JUUyI6ZmFsc2UsIklTX1dBU0kiOnRydWUsIlVTRV9TSEFSRURfTUVNT1JZIjpmYWxzZSwiVVNFX1dBU0lfQ0ROIjpmYWxzZX0sInN1YnN0aXR1dGlvbnMiOnsiUEFDS0FHRV9UT19KU19NT0RVTEVfUEFUSCI6Im1haW4ud2FzbSIsIlBBQ0tBR0VfVE9fSlNfUEFDS0FHRV9OQU1FIjoidGVzdCJ9fQ==",
     "wants" : [
       "$OUTPUT",
       "$OUTPUT\/platforms",
@@ -204,7 +204,7 @@
       "$INTERMEDIATES\/wasm-imports.json"
     ],
     "output" : "$OUTPUT\/platforms\/browser.worker.js",
-    "salt" : "eyJjb25kaXRpb25zIjp7IklTX1dBU0kiOnRydWUsIlVTRV9TSEFSRURfTUVNT1JZIjpmYWxzZSwiVVNFX1dBU0lfQ0ROIjpmYWxzZX0sInN1YnN0aXR1dGlvbnMiOnsiUEFDS0FHRV9UT19KU19NT0RVTEVfUEFUSCI6Im1haW4ud2FzbSIsIlBBQ0tBR0VfVE9fSlNfUEFDS0FHRV9OQU1FIjoidGVzdCJ9fQ==",
+    "salt" : "eyJjb25kaXRpb25zIjp7IkhBU19CUklER0UiOmZhbHNlLCJIQVNfSU1QT1JUUyI6ZmFsc2UsIklTX1dBU0kiOnRydWUsIlVTRV9TSEFSRURfTUVNT1JZIjpmYWxzZSwiVVNFX1dBU0lfQ0ROIjpmYWxzZX0sInN1YnN0aXR1dGlvbnMiOnsiUEFDS0FHRV9UT19KU19NT0RVTEVfUEFUSCI6Im1haW4ud2FzbSIsIlBBQ0tBR0VfVE9fSlNfUEFDS0FHRV9OQU1FIjoidGVzdCJ9fQ==",
     "wants" : [
       "$OUTPUT",
       "$OUTPUT\/platforms",
@@ -221,7 +221,7 @@
       "$INTERMEDIATES\/wasm-imports.json"
     ],
     "output" : "$OUTPUT\/platforms\/node.d.ts",
-    "salt" : "eyJjb25kaXRpb25zIjp7IklTX1dBU0kiOnRydWUsIlVTRV9TSEFSRURfTUVNT1JZIjpmYWxzZSwiVVNFX1dBU0lfQ0ROIjpmYWxzZX0sInN1YnN0aXR1dGlvbnMiOnsiUEFDS0FHRV9UT19KU19NT0RVTEVfUEFUSCI6Im1haW4ud2FzbSIsIlBBQ0tBR0VfVE9fSlNfUEFDS0FHRV9OQU1FIjoidGVzdCJ9fQ==",
+    "salt" : "eyJjb25kaXRpb25zIjp7IkhBU19CUklER0UiOmZhbHNlLCJIQVNfSU1QT1JUUyI6ZmFsc2UsIklTX1dBU0kiOnRydWUsIlVTRV9TSEFSRURfTUVNT1JZIjpmYWxzZSwiVVNFX1dBU0lfQ0ROIjpmYWxzZX0sInN1YnN0aXR1dGlvbnMiOnsiUEFDS0FHRV9UT19KU19NT0RVTEVfUEFUSCI6Im1haW4ud2FzbSIsIlBBQ0tBR0VfVE9fSlNfUEFDS0FHRV9OQU1FIjoidGVzdCJ9fQ==",
     "wants" : [
       "$OUTPUT",
       "$OUTPUT\/platforms",
@@ -238,7 +238,7 @@
       "$INTERMEDIATES\/wasm-imports.json"
     ],
     "output" : "$OUTPUT\/platforms\/node.js",
-    "salt" : "eyJjb25kaXRpb25zIjp7IklTX1dBU0kiOnRydWUsIlVTRV9TSEFSRURfTUVNT1JZIjpmYWxzZSwiVVNFX1dBU0lfQ0ROIjpmYWxzZX0sInN1YnN0aXR1dGlvbnMiOnsiUEFDS0FHRV9UT19KU19NT0RVTEVfUEFUSCI6Im1haW4ud2FzbSIsIlBBQ0tBR0VfVE9fSlNfUEFDS0FHRV9OQU1FIjoidGVzdCJ9fQ==",
+    "salt" : "eyJjb25kaXRpb25zIjp7IkhBU19CUklER0UiOmZhbHNlLCJIQVNfSU1QT1JUUyI6ZmFsc2UsIklTX1dBU0kiOnRydWUsIlVTRV9TSEFSRURfTUVNT1JZIjpmYWxzZSwiVVNFX1dBU0lfQ0ROIjpmYWxzZX0sInN1YnN0aXR1dGlvbnMiOnsiUEFDS0FHRV9UT19KU19NT0RVTEVfUEFUSCI6Im1haW4ud2FzbSIsIlBBQ0tBR0VfVE9fSlNfUEFDS0FHRV9OQU1FIjoidGVzdCJ9fQ==",
     "wants" : [
       "$OUTPUT",
       "$OUTPUT\/platforms",
@@ -255,7 +255,7 @@
       "$INTERMEDIATES\/wasm-imports.json"
     ],
     "output" : "$OUTPUT\/runtime.d.ts",
-    "salt" : "eyJjb25kaXRpb25zIjp7IklTX1dBU0kiOnRydWUsIlVTRV9TSEFSRURfTUVNT1JZIjpmYWxzZSwiVVNFX1dBU0lfQ0ROIjpmYWxzZX0sInN1YnN0aXR1dGlvbnMiOnsiUEFDS0FHRV9UT19KU19NT0RVTEVfUEFUSCI6Im1haW4ud2FzbSIsIlBBQ0tBR0VfVE9fSlNfUEFDS0FHRV9OQU1FIjoidGVzdCJ9fQ==",
+    "salt" : "eyJjb25kaXRpb25zIjp7IkhBU19CUklER0UiOmZhbHNlLCJIQVNfSU1QT1JUUyI6ZmFsc2UsIklTX1dBU0kiOnRydWUsIlVTRV9TSEFSRURfTUVNT1JZIjpmYWxzZSwiVVNFX1dBU0lfQ0ROIjpmYWxzZX0sInN1YnN0aXR1dGlvbnMiOnsiUEFDS0FHRV9UT19KU19NT0RVTEVfUEFUSCI6Im1haW4ud2FzbSIsIlBBQ0tBR0VfVE9fSlNfUEFDS0FHRV9OQU1FIjoidGVzdCJ9fQ==",
     "wants" : [
       "$OUTPUT",
       "$OUTPUT\/platforms",
@@ -272,7 +272,7 @@
       "$INTERMEDIATES\/wasm-imports.json"
     ],
     "output" : "$OUTPUT\/runtime.js",
-    "salt" : "eyJjb25kaXRpb25zIjp7IklTX1dBU0kiOnRydWUsIlVTRV9TSEFSRURfTUVNT1JZIjpmYWxzZSwiVVNFX1dBU0lfQ0ROIjpmYWxzZX0sInN1YnN0aXR1dGlvbnMiOnsiUEFDS0FHRV9UT19KU19NT0RVTEVfUEFUSCI6Im1haW4ud2FzbSIsIlBBQ0tBR0VfVE9fSlNfUEFDS0FHRV9OQU1FIjoidGVzdCJ9fQ==",
+    "salt" : "eyJjb25kaXRpb25zIjp7IkhBU19CUklER0UiOmZhbHNlLCJIQVNfSU1QT1JUUyI6ZmFsc2UsIklTX1dBU0kiOnRydWUsIlVTRV9TSEFSRURfTUVNT1JZIjpmYWxzZSwiVVNFX1dBU0lfQ0ROIjpmYWxzZX0sInN1YnN0aXR1dGlvbnMiOnsiUEFDS0FHRV9UT19KU19NT0RVTEVfUEFUSCI6Im1haW4ud2FzbSIsIlBBQ0tBR0VfVE9fSlNfUEFDS0FHRV9OQU1FIjoidGVzdCJ9fQ==",
     "wants" : [
       "$OUTPUT",
       "$OUTPUT\/platforms",
diff --git a/Plugins/PackageToJS/Tests/__Snapshots__/PackagingPlannerTests/planBuild_release_dwarf.json b/Plugins/PackageToJS/Tests/__Snapshots__/PackagingPlannerTests/planBuild_release_dwarf.json
index e525d1347..13768da75 100644
--- a/Plugins/PackageToJS/Tests/__Snapshots__/PackagingPlannerTests/planBuild_release_dwarf.json
+++ b/Plugins/PackageToJS/Tests/__Snapshots__/PackagingPlannerTests/planBuild_release_dwarf.json
@@ -48,7 +48,7 @@
       "$INTERMEDIATES\/wasm-imports.json"
     ],
     "output" : "$OUTPUT\/index.d.ts",
-    "salt" : "eyJjb25kaXRpb25zIjp7IklTX1dBU0kiOnRydWUsIlVTRV9TSEFSRURfTUVNT1JZIjpmYWxzZSwiVVNFX1dBU0lfQ0ROIjpmYWxzZX0sInN1YnN0aXR1dGlvbnMiOnsiUEFDS0FHRV9UT19KU19NT0RVTEVfUEFUSCI6Im1haW4ud2FzbSIsIlBBQ0tBR0VfVE9fSlNfUEFDS0FHRV9OQU1FIjoidGVzdCJ9fQ==",
+    "salt" : "eyJjb25kaXRpb25zIjp7IkhBU19CUklER0UiOmZhbHNlLCJIQVNfSU1QT1JUUyI6ZmFsc2UsIklTX1dBU0kiOnRydWUsIlVTRV9TSEFSRURfTUVNT1JZIjpmYWxzZSwiVVNFX1dBU0lfQ0ROIjpmYWxzZX0sInN1YnN0aXR1dGlvbnMiOnsiUEFDS0FHRV9UT19KU19NT0RVTEVfUEFUSCI6Im1haW4ud2FzbSIsIlBBQ0tBR0VfVE9fSlNfUEFDS0FHRV9OQU1FIjoidGVzdCJ9fQ==",
     "wants" : [
       "$OUTPUT",
       "$OUTPUT\/platforms",
@@ -65,7 +65,7 @@
       "$INTERMEDIATES\/wasm-imports.json"
     ],
     "output" : "$OUTPUT\/index.js",
-    "salt" : "eyJjb25kaXRpb25zIjp7IklTX1dBU0kiOnRydWUsIlVTRV9TSEFSRURfTUVNT1JZIjpmYWxzZSwiVVNFX1dBU0lfQ0ROIjpmYWxzZX0sInN1YnN0aXR1dGlvbnMiOnsiUEFDS0FHRV9UT19KU19NT0RVTEVfUEFUSCI6Im1haW4ud2FzbSIsIlBBQ0tBR0VfVE9fSlNfUEFDS0FHRV9OQU1FIjoidGVzdCJ9fQ==",
+    "salt" : "eyJjb25kaXRpb25zIjp7IkhBU19CUklER0UiOmZhbHNlLCJIQVNfSU1QT1JUUyI6ZmFsc2UsIklTX1dBU0kiOnRydWUsIlVTRV9TSEFSRURfTUVNT1JZIjpmYWxzZSwiVVNFX1dBU0lfQ0ROIjpmYWxzZX0sInN1YnN0aXR1dGlvbnMiOnsiUEFDS0FHRV9UT19KU19NT0RVTEVfUEFUSCI6Im1haW4ud2FzbSIsIlBBQ0tBR0VfVE9fSlNfUEFDS0FHRV9OQU1FIjoidGVzdCJ9fQ==",
     "wants" : [
       "$OUTPUT",
       "$OUTPUT\/platforms",
@@ -82,7 +82,7 @@
       "$INTERMEDIATES\/wasm-imports.json"
     ],
     "output" : "$OUTPUT\/instantiate.d.ts",
-    "salt" : "eyJjb25kaXRpb25zIjp7IklTX1dBU0kiOnRydWUsIlVTRV9TSEFSRURfTUVNT1JZIjpmYWxzZSwiVVNFX1dBU0lfQ0ROIjpmYWxzZX0sInN1YnN0aXR1dGlvbnMiOnsiUEFDS0FHRV9UT19KU19NT0RVTEVfUEFUSCI6Im1haW4ud2FzbSIsIlBBQ0tBR0VfVE9fSlNfUEFDS0FHRV9OQU1FIjoidGVzdCJ9fQ==",
+    "salt" : "eyJjb25kaXRpb25zIjp7IkhBU19CUklER0UiOmZhbHNlLCJIQVNfSU1QT1JUUyI6ZmFsc2UsIklTX1dBU0kiOnRydWUsIlVTRV9TSEFSRURfTUVNT1JZIjpmYWxzZSwiVVNFX1dBU0lfQ0ROIjpmYWxzZX0sInN1YnN0aXR1dGlvbnMiOnsiUEFDS0FHRV9UT19KU19NT0RVTEVfUEFUSCI6Im1haW4ud2FzbSIsIlBBQ0tBR0VfVE9fSlNfUEFDS0FHRV9OQU1FIjoidGVzdCJ9fQ==",
     "wants" : [
       "$OUTPUT",
       "$OUTPUT\/platforms",
@@ -99,7 +99,7 @@
       "$INTERMEDIATES\/wasm-imports.json"
     ],
     "output" : "$OUTPUT\/instantiate.js",
-    "salt" : "eyJjb25kaXRpb25zIjp7IklTX1dBU0kiOnRydWUsIlVTRV9TSEFSRURfTUVNT1JZIjpmYWxzZSwiVVNFX1dBU0lfQ0ROIjpmYWxzZX0sInN1YnN0aXR1dGlvbnMiOnsiUEFDS0FHRV9UT19KU19NT0RVTEVfUEFUSCI6Im1haW4ud2FzbSIsIlBBQ0tBR0VfVE9fSlNfUEFDS0FHRV9OQU1FIjoidGVzdCJ9fQ==",
+    "salt" : "eyJjb25kaXRpb25zIjp7IkhBU19CUklER0UiOmZhbHNlLCJIQVNfSU1QT1JUUyI6ZmFsc2UsIklTX1dBU0kiOnRydWUsIlVTRV9TSEFSRURfTUVNT1JZIjpmYWxzZSwiVVNFX1dBU0lfQ0ROIjpmYWxzZX0sInN1YnN0aXR1dGlvbnMiOnsiUEFDS0FHRV9UT19KU19NT0RVTEVfUEFUSCI6Im1haW4ud2FzbSIsIlBBQ0tBR0VfVE9fSlNfUEFDS0FHRV9OQU1FIjoidGVzdCJ9fQ==",
     "wants" : [
       "$OUTPUT",
       "$OUTPUT\/platforms",
@@ -128,7 +128,7 @@
       "$SELF_PACKAGE\/Plugins\/PackageToJS\/Templates\/package.json"
     ],
     "output" : "$OUTPUT\/package.json",
-    "salt" : "eyJjb25kaXRpb25zIjp7IklTX1dBU0kiOnRydWUsIlVTRV9TSEFSRURfTUVNT1JZIjpmYWxzZSwiVVNFX1dBU0lfQ0ROIjpmYWxzZX0sInN1YnN0aXR1dGlvbnMiOnsiUEFDS0FHRV9UT19KU19NT0RVTEVfUEFUSCI6Im1haW4ud2FzbSIsIlBBQ0tBR0VfVE9fSlNfUEFDS0FHRV9OQU1FIjoidGVzdCJ9fQ==",
+    "salt" : "eyJjb25kaXRpb25zIjp7IkhBU19CUklER0UiOmZhbHNlLCJIQVNfSU1QT1JUUyI6ZmFsc2UsIklTX1dBU0kiOnRydWUsIlVTRV9TSEFSRURfTUVNT1JZIjpmYWxzZSwiVVNFX1dBU0lfQ0ROIjpmYWxzZX0sInN1YnN0aXR1dGlvbnMiOnsiUEFDS0FHRV9UT19KU19NT0RVTEVfUEFUSCI6Im1haW4ud2FzbSIsIlBBQ0tBR0VfVE9fSlNfUEFDS0FHRV9OQU1FIjoidGVzdCJ9fQ==",
     "wants" : [
       "$OUTPUT"
     ]
@@ -155,7 +155,7 @@
       "$INTERMEDIATES\/wasm-imports.json"
     ],
     "output" : "$OUTPUT\/platforms\/browser.d.ts",
-    "salt" : "eyJjb25kaXRpb25zIjp7IklTX1dBU0kiOnRydWUsIlVTRV9TSEFSRURfTUVNT1JZIjpmYWxzZSwiVVNFX1dBU0lfQ0ROIjpmYWxzZX0sInN1YnN0aXR1dGlvbnMiOnsiUEFDS0FHRV9UT19KU19NT0RVTEVfUEFUSCI6Im1haW4ud2FzbSIsIlBBQ0tBR0VfVE9fSlNfUEFDS0FHRV9OQU1FIjoidGVzdCJ9fQ==",
+    "salt" : "eyJjb25kaXRpb25zIjp7IkhBU19CUklER0UiOmZhbHNlLCJIQVNfSU1QT1JUUyI6ZmFsc2UsIklTX1dBU0kiOnRydWUsIlVTRV9TSEFSRURfTUVNT1JZIjpmYWxzZSwiVVNFX1dBU0lfQ0ROIjpmYWxzZX0sInN1YnN0aXR1dGlvbnMiOnsiUEFDS0FHRV9UT19KU19NT0RVTEVfUEFUSCI6Im1haW4ud2FzbSIsIlBBQ0tBR0VfVE9fSlNfUEFDS0FHRV9OQU1FIjoidGVzdCJ9fQ==",
     "wants" : [
       "$OUTPUT",
       "$OUTPUT\/platforms",
@@ -172,7 +172,7 @@
       "$INTERMEDIATES\/wasm-imports.json"
     ],
     "output" : "$OUTPUT\/platforms\/browser.js",
-    "salt" : "eyJjb25kaXRpb25zIjp7IklTX1dBU0kiOnRydWUsIlVTRV9TSEFSRURfTUVNT1JZIjpmYWxzZSwiVVNFX1dBU0lfQ0ROIjpmYWxzZX0sInN1YnN0aXR1dGlvbnMiOnsiUEFDS0FHRV9UT19KU19NT0RVTEVfUEFUSCI6Im1haW4ud2FzbSIsIlBBQ0tBR0VfVE9fSlNfUEFDS0FHRV9OQU1FIjoidGVzdCJ9fQ==",
+    "salt" : "eyJjb25kaXRpb25zIjp7IkhBU19CUklER0UiOmZhbHNlLCJIQVNfSU1QT1JUUyI6ZmFsc2UsIklTX1dBU0kiOnRydWUsIlVTRV9TSEFSRURfTUVNT1JZIjpmYWxzZSwiVVNFX1dBU0lfQ0ROIjpmYWxzZX0sInN1YnN0aXR1dGlvbnMiOnsiUEFDS0FHRV9UT19KU19NT0RVTEVfUEFUSCI6Im1haW4ud2FzbSIsIlBBQ0tBR0VfVE9fSlNfUEFDS0FHRV9OQU1FIjoidGVzdCJ9fQ==",
     "wants" : [
       "$OUTPUT",
       "$OUTPUT\/platforms",
@@ -189,7 +189,7 @@
       "$INTERMEDIATES\/wasm-imports.json"
     ],
     "output" : "$OUTPUT\/platforms\/browser.worker.js",
-    "salt" : "eyJjb25kaXRpb25zIjp7IklTX1dBU0kiOnRydWUsIlVTRV9TSEFSRURfTUVNT1JZIjpmYWxzZSwiVVNFX1dBU0lfQ0ROIjpmYWxzZX0sInN1YnN0aXR1dGlvbnMiOnsiUEFDS0FHRV9UT19KU19NT0RVTEVfUEFUSCI6Im1haW4ud2FzbSIsIlBBQ0tBR0VfVE9fSlNfUEFDS0FHRV9OQU1FIjoidGVzdCJ9fQ==",
+    "salt" : "eyJjb25kaXRpb25zIjp7IkhBU19CUklER0UiOmZhbHNlLCJIQVNfSU1QT1JUUyI6ZmFsc2UsIklTX1dBU0kiOnRydWUsIlVTRV9TSEFSRURfTUVNT1JZIjpmYWxzZSwiVVNFX1dBU0lfQ0ROIjpmYWxzZX0sInN1YnN0aXR1dGlvbnMiOnsiUEFDS0FHRV9UT19KU19NT0RVTEVfUEFUSCI6Im1haW4ud2FzbSIsIlBBQ0tBR0VfVE9fSlNfUEFDS0FHRV9OQU1FIjoidGVzdCJ9fQ==",
     "wants" : [
       "$OUTPUT",
       "$OUTPUT\/platforms",
@@ -206,7 +206,7 @@
       "$INTERMEDIATES\/wasm-imports.json"
     ],
     "output" : "$OUTPUT\/platforms\/node.d.ts",
-    "salt" : "eyJjb25kaXRpb25zIjp7IklTX1dBU0kiOnRydWUsIlVTRV9TSEFSRURfTUVNT1JZIjpmYWxzZSwiVVNFX1dBU0lfQ0ROIjpmYWxzZX0sInN1YnN0aXR1dGlvbnMiOnsiUEFDS0FHRV9UT19KU19NT0RVTEVfUEFUSCI6Im1haW4ud2FzbSIsIlBBQ0tBR0VfVE9fSlNfUEFDS0FHRV9OQU1FIjoidGVzdCJ9fQ==",
+    "salt" : "eyJjb25kaXRpb25zIjp7IkhBU19CUklER0UiOmZhbHNlLCJIQVNfSU1QT1JUUyI6ZmFsc2UsIklTX1dBU0kiOnRydWUsIlVTRV9TSEFSRURfTUVNT1JZIjpmYWxzZSwiVVNFX1dBU0lfQ0ROIjpmYWxzZX0sInN1YnN0aXR1dGlvbnMiOnsiUEFDS0FHRV9UT19KU19NT0RVTEVfUEFUSCI6Im1haW4ud2FzbSIsIlBBQ0tBR0VfVE9fSlNfUEFDS0FHRV9OQU1FIjoidGVzdCJ9fQ==",
     "wants" : [
       "$OUTPUT",
       "$OUTPUT\/platforms",
@@ -223,7 +223,7 @@
       "$INTERMEDIATES\/wasm-imports.json"
     ],
     "output" : "$OUTPUT\/platforms\/node.js",
-    "salt" : "eyJjb25kaXRpb25zIjp7IklTX1dBU0kiOnRydWUsIlVTRV9TSEFSRURfTUVNT1JZIjpmYWxzZSwiVVNFX1dBU0lfQ0ROIjpmYWxzZX0sInN1YnN0aXR1dGlvbnMiOnsiUEFDS0FHRV9UT19KU19NT0RVTEVfUEFUSCI6Im1haW4ud2FzbSIsIlBBQ0tBR0VfVE9fSlNfUEFDS0FHRV9OQU1FIjoidGVzdCJ9fQ==",
+    "salt" : "eyJjb25kaXRpb25zIjp7IkhBU19CUklER0UiOmZhbHNlLCJIQVNfSU1QT1JUUyI6ZmFsc2UsIklTX1dBU0kiOnRydWUsIlVTRV9TSEFSRURfTUVNT1JZIjpmYWxzZSwiVVNFX1dBU0lfQ0ROIjpmYWxzZX0sInN1YnN0aXR1dGlvbnMiOnsiUEFDS0FHRV9UT19KU19NT0RVTEVfUEFUSCI6Im1haW4ud2FzbSIsIlBBQ0tBR0VfVE9fSlNfUEFDS0FHRV9OQU1FIjoidGVzdCJ9fQ==",
     "wants" : [
       "$OUTPUT",
       "$OUTPUT\/platforms",
@@ -240,7 +240,7 @@
       "$INTERMEDIATES\/wasm-imports.json"
     ],
     "output" : "$OUTPUT\/runtime.d.ts",
-    "salt" : "eyJjb25kaXRpb25zIjp7IklTX1dBU0kiOnRydWUsIlVTRV9TSEFSRURfTUVNT1JZIjpmYWxzZSwiVVNFX1dBU0lfQ0ROIjpmYWxzZX0sInN1YnN0aXR1dGlvbnMiOnsiUEFDS0FHRV9UT19KU19NT0RVTEVfUEFUSCI6Im1haW4ud2FzbSIsIlBBQ0tBR0VfVE9fSlNfUEFDS0FHRV9OQU1FIjoidGVzdCJ9fQ==",
+    "salt" : "eyJjb25kaXRpb25zIjp7IkhBU19CUklER0UiOmZhbHNlLCJIQVNfSU1QT1JUUyI6ZmFsc2UsIklTX1dBU0kiOnRydWUsIlVTRV9TSEFSRURfTUVNT1JZIjpmYWxzZSwiVVNFX1dBU0lfQ0ROIjpmYWxzZX0sInN1YnN0aXR1dGlvbnMiOnsiUEFDS0FHRV9UT19KU19NT0RVTEVfUEFUSCI6Im1haW4ud2FzbSIsIlBBQ0tBR0VfVE9fSlNfUEFDS0FHRV9OQU1FIjoidGVzdCJ9fQ==",
     "wants" : [
       "$OUTPUT",
       "$OUTPUT\/platforms",
@@ -257,7 +257,7 @@
       "$INTERMEDIATES\/wasm-imports.json"
     ],
     "output" : "$OUTPUT\/runtime.js",
-    "salt" : "eyJjb25kaXRpb25zIjp7IklTX1dBU0kiOnRydWUsIlVTRV9TSEFSRURfTUVNT1JZIjpmYWxzZSwiVVNFX1dBU0lfQ0ROIjpmYWxzZX0sInN1YnN0aXR1dGlvbnMiOnsiUEFDS0FHRV9UT19KU19NT0RVTEVfUEFUSCI6Im1haW4ud2FzbSIsIlBBQ0tBR0VfVE9fSlNfUEFDS0FHRV9OQU1FIjoidGVzdCJ9fQ==",
+    "salt" : "eyJjb25kaXRpb25zIjp7IkhBU19CUklER0UiOmZhbHNlLCJIQVNfSU1QT1JUUyI6ZmFsc2UsIklTX1dBU0kiOnRydWUsIlVTRV9TSEFSRURfTUVNT1JZIjpmYWxzZSwiVVNFX1dBU0lfQ0ROIjpmYWxzZX0sInN1YnN0aXR1dGlvbnMiOnsiUEFDS0FHRV9UT19KU19NT0RVTEVfUEFUSCI6Im1haW4ud2FzbSIsIlBBQ0tBR0VfVE9fSlNfUEFDS0FHRV9OQU1FIjoidGVzdCJ9fQ==",
     "wants" : [
       "$OUTPUT",
       "$OUTPUT\/platforms",
diff --git a/Plugins/PackageToJS/Tests/__Snapshots__/PackagingPlannerTests/planBuild_release_name.json b/Plugins/PackageToJS/Tests/__Snapshots__/PackagingPlannerTests/planBuild_release_name.json
index 6e3480c59..ccfbc35cc 100644
--- a/Plugins/PackageToJS/Tests/__Snapshots__/PackagingPlannerTests/planBuild_release_name.json
+++ b/Plugins/PackageToJS/Tests/__Snapshots__/PackagingPlannerTests/planBuild_release_name.json
@@ -62,7 +62,7 @@
       "$INTERMEDIATES\/wasm-imports.json"
     ],
     "output" : "$OUTPUT\/index.d.ts",
-    "salt" : "eyJjb25kaXRpb25zIjp7IklTX1dBU0kiOnRydWUsIlVTRV9TSEFSRURfTUVNT1JZIjpmYWxzZSwiVVNFX1dBU0lfQ0ROIjpmYWxzZX0sInN1YnN0aXR1dGlvbnMiOnsiUEFDS0FHRV9UT19KU19NT0RVTEVfUEFUSCI6Im1haW4ud2FzbSIsIlBBQ0tBR0VfVE9fSlNfUEFDS0FHRV9OQU1FIjoidGVzdCJ9fQ==",
+    "salt" : "eyJjb25kaXRpb25zIjp7IkhBU19CUklER0UiOmZhbHNlLCJIQVNfSU1QT1JUUyI6ZmFsc2UsIklTX1dBU0kiOnRydWUsIlVTRV9TSEFSRURfTUVNT1JZIjpmYWxzZSwiVVNFX1dBU0lfQ0ROIjpmYWxzZX0sInN1YnN0aXR1dGlvbnMiOnsiUEFDS0FHRV9UT19KU19NT0RVTEVfUEFUSCI6Im1haW4ud2FzbSIsIlBBQ0tBR0VfVE9fSlNfUEFDS0FHRV9OQU1FIjoidGVzdCJ9fQ==",
     "wants" : [
       "$OUTPUT",
       "$OUTPUT\/platforms",
@@ -79,7 +79,7 @@
       "$INTERMEDIATES\/wasm-imports.json"
     ],
     "output" : "$OUTPUT\/index.js",
-    "salt" : "eyJjb25kaXRpb25zIjp7IklTX1dBU0kiOnRydWUsIlVTRV9TSEFSRURfTUVNT1JZIjpmYWxzZSwiVVNFX1dBU0lfQ0ROIjpmYWxzZX0sInN1YnN0aXR1dGlvbnMiOnsiUEFDS0FHRV9UT19KU19NT0RVTEVfUEFUSCI6Im1haW4ud2FzbSIsIlBBQ0tBR0VfVE9fSlNfUEFDS0FHRV9OQU1FIjoidGVzdCJ9fQ==",
+    "salt" : "eyJjb25kaXRpb25zIjp7IkhBU19CUklER0UiOmZhbHNlLCJIQVNfSU1QT1JUUyI6ZmFsc2UsIklTX1dBU0kiOnRydWUsIlVTRV9TSEFSRURfTUVNT1JZIjpmYWxzZSwiVVNFX1dBU0lfQ0ROIjpmYWxzZX0sInN1YnN0aXR1dGlvbnMiOnsiUEFDS0FHRV9UT19KU19NT0RVTEVfUEFUSCI6Im1haW4ud2FzbSIsIlBBQ0tBR0VfVE9fSlNfUEFDS0FHRV9OQU1FIjoidGVzdCJ9fQ==",
     "wants" : [
       "$OUTPUT",
       "$OUTPUT\/platforms",
@@ -96,7 +96,7 @@
       "$INTERMEDIATES\/wasm-imports.json"
     ],
     "output" : "$OUTPUT\/instantiate.d.ts",
-    "salt" : "eyJjb25kaXRpb25zIjp7IklTX1dBU0kiOnRydWUsIlVTRV9TSEFSRURfTUVNT1JZIjpmYWxzZSwiVVNFX1dBU0lfQ0ROIjpmYWxzZX0sInN1YnN0aXR1dGlvbnMiOnsiUEFDS0FHRV9UT19KU19NT0RVTEVfUEFUSCI6Im1haW4ud2FzbSIsIlBBQ0tBR0VfVE9fSlNfUEFDS0FHRV9OQU1FIjoidGVzdCJ9fQ==",
+    "salt" : "eyJjb25kaXRpb25zIjp7IkhBU19CUklER0UiOmZhbHNlLCJIQVNfSU1QT1JUUyI6ZmFsc2UsIklTX1dBU0kiOnRydWUsIlVTRV9TSEFSRURfTUVNT1JZIjpmYWxzZSwiVVNFX1dBU0lfQ0ROIjpmYWxzZX0sInN1YnN0aXR1dGlvbnMiOnsiUEFDS0FHRV9UT19KU19NT0RVTEVfUEFUSCI6Im1haW4ud2FzbSIsIlBBQ0tBR0VfVE9fSlNfUEFDS0FHRV9OQU1FIjoidGVzdCJ9fQ==",
     "wants" : [
       "$OUTPUT",
       "$OUTPUT\/platforms",
@@ -113,7 +113,7 @@
       "$INTERMEDIATES\/wasm-imports.json"
     ],
     "output" : "$OUTPUT\/instantiate.js",
-    "salt" : "eyJjb25kaXRpb25zIjp7IklTX1dBU0kiOnRydWUsIlVTRV9TSEFSRURfTUVNT1JZIjpmYWxzZSwiVVNFX1dBU0lfQ0ROIjpmYWxzZX0sInN1YnN0aXR1dGlvbnMiOnsiUEFDS0FHRV9UT19KU19NT0RVTEVfUEFUSCI6Im1haW4ud2FzbSIsIlBBQ0tBR0VfVE9fSlNfUEFDS0FHRV9OQU1FIjoidGVzdCJ9fQ==",
+    "salt" : "eyJjb25kaXRpb25zIjp7IkhBU19CUklER0UiOmZhbHNlLCJIQVNfSU1QT1JUUyI6ZmFsc2UsIklTX1dBU0kiOnRydWUsIlVTRV9TSEFSRURfTUVNT1JZIjpmYWxzZSwiVVNFX1dBU0lfQ0ROIjpmYWxzZX0sInN1YnN0aXR1dGlvbnMiOnsiUEFDS0FHRV9UT19KU19NT0RVTEVfUEFUSCI6Im1haW4ud2FzbSIsIlBBQ0tBR0VfVE9fSlNfUEFDS0FHRV9OQU1FIjoidGVzdCJ9fQ==",
     "wants" : [
       "$OUTPUT",
       "$OUTPUT\/platforms",
@@ -143,7 +143,7 @@
       "$SELF_PACKAGE\/Plugins\/PackageToJS\/Templates\/package.json"
     ],
     "output" : "$OUTPUT\/package.json",
-    "salt" : "eyJjb25kaXRpb25zIjp7IklTX1dBU0kiOnRydWUsIlVTRV9TSEFSRURfTUVNT1JZIjpmYWxzZSwiVVNFX1dBU0lfQ0ROIjpmYWxzZX0sInN1YnN0aXR1dGlvbnMiOnsiUEFDS0FHRV9UT19KU19NT0RVTEVfUEFUSCI6Im1haW4ud2FzbSIsIlBBQ0tBR0VfVE9fSlNfUEFDS0FHRV9OQU1FIjoidGVzdCJ9fQ==",
+    "salt" : "eyJjb25kaXRpb25zIjp7IkhBU19CUklER0UiOmZhbHNlLCJIQVNfSU1QT1JUUyI6ZmFsc2UsIklTX1dBU0kiOnRydWUsIlVTRV9TSEFSRURfTUVNT1JZIjpmYWxzZSwiVVNFX1dBU0lfQ0ROIjpmYWxzZX0sInN1YnN0aXR1dGlvbnMiOnsiUEFDS0FHRV9UT19KU19NT0RVTEVfUEFUSCI6Im1haW4ud2FzbSIsIlBBQ0tBR0VfVE9fSlNfUEFDS0FHRV9OQU1FIjoidGVzdCJ9fQ==",
     "wants" : [
       "$OUTPUT"
     ]
@@ -170,7 +170,7 @@
       "$INTERMEDIATES\/wasm-imports.json"
     ],
     "output" : "$OUTPUT\/platforms\/browser.d.ts",
-    "salt" : "eyJjb25kaXRpb25zIjp7IklTX1dBU0kiOnRydWUsIlVTRV9TSEFSRURfTUVNT1JZIjpmYWxzZSwiVVNFX1dBU0lfQ0ROIjpmYWxzZX0sInN1YnN0aXR1dGlvbnMiOnsiUEFDS0FHRV9UT19KU19NT0RVTEVfUEFUSCI6Im1haW4ud2FzbSIsIlBBQ0tBR0VfVE9fSlNfUEFDS0FHRV9OQU1FIjoidGVzdCJ9fQ==",
+    "salt" : "eyJjb25kaXRpb25zIjp7IkhBU19CUklER0UiOmZhbHNlLCJIQVNfSU1QT1JUUyI6ZmFsc2UsIklTX1dBU0kiOnRydWUsIlVTRV9TSEFSRURfTUVNT1JZIjpmYWxzZSwiVVNFX1dBU0lfQ0ROIjpmYWxzZX0sInN1YnN0aXR1dGlvbnMiOnsiUEFDS0FHRV9UT19KU19NT0RVTEVfUEFUSCI6Im1haW4ud2FzbSIsIlBBQ0tBR0VfVE9fSlNfUEFDS0FHRV9OQU1FIjoidGVzdCJ9fQ==",
     "wants" : [
       "$OUTPUT",
       "$OUTPUT\/platforms",
@@ -187,7 +187,7 @@
       "$INTERMEDIATES\/wasm-imports.json"
     ],
     "output" : "$OUTPUT\/platforms\/browser.js",
-    "salt" : "eyJjb25kaXRpb25zIjp7IklTX1dBU0kiOnRydWUsIlVTRV9TSEFSRURfTUVNT1JZIjpmYWxzZSwiVVNFX1dBU0lfQ0ROIjpmYWxzZX0sInN1YnN0aXR1dGlvbnMiOnsiUEFDS0FHRV9UT19KU19NT0RVTEVfUEFUSCI6Im1haW4ud2FzbSIsIlBBQ0tBR0VfVE9fSlNfUEFDS0FHRV9OQU1FIjoidGVzdCJ9fQ==",
+    "salt" : "eyJjb25kaXRpb25zIjp7IkhBU19CUklER0UiOmZhbHNlLCJIQVNfSU1QT1JUUyI6ZmFsc2UsIklTX1dBU0kiOnRydWUsIlVTRV9TSEFSRURfTUVNT1JZIjpmYWxzZSwiVVNFX1dBU0lfQ0ROIjpmYWxzZX0sInN1YnN0aXR1dGlvbnMiOnsiUEFDS0FHRV9UT19KU19NT0RVTEVfUEFUSCI6Im1haW4ud2FzbSIsIlBBQ0tBR0VfVE9fSlNfUEFDS0FHRV9OQU1FIjoidGVzdCJ9fQ==",
     "wants" : [
       "$OUTPUT",
       "$OUTPUT\/platforms",
@@ -204,7 +204,7 @@
       "$INTERMEDIATES\/wasm-imports.json"
     ],
     "output" : "$OUTPUT\/platforms\/browser.worker.js",
-    "salt" : "eyJjb25kaXRpb25zIjp7IklTX1dBU0kiOnRydWUsIlVTRV9TSEFSRURfTUVNT1JZIjpmYWxzZSwiVVNFX1dBU0lfQ0ROIjpmYWxzZX0sInN1YnN0aXR1dGlvbnMiOnsiUEFDS0FHRV9UT19KU19NT0RVTEVfUEFUSCI6Im1haW4ud2FzbSIsIlBBQ0tBR0VfVE9fSlNfUEFDS0FHRV9OQU1FIjoidGVzdCJ9fQ==",
+    "salt" : "eyJjb25kaXRpb25zIjp7IkhBU19CUklER0UiOmZhbHNlLCJIQVNfSU1QT1JUUyI6ZmFsc2UsIklTX1dBU0kiOnRydWUsIlVTRV9TSEFSRURfTUVNT1JZIjpmYWxzZSwiVVNFX1dBU0lfQ0ROIjpmYWxzZX0sInN1YnN0aXR1dGlvbnMiOnsiUEFDS0FHRV9UT19KU19NT0RVTEVfUEFUSCI6Im1haW4ud2FzbSIsIlBBQ0tBR0VfVE9fSlNfUEFDS0FHRV9OQU1FIjoidGVzdCJ9fQ==",
     "wants" : [
       "$OUTPUT",
       "$OUTPUT\/platforms",
@@ -221,7 +221,7 @@
       "$INTERMEDIATES\/wasm-imports.json"
     ],
     "output" : "$OUTPUT\/platforms\/node.d.ts",
-    "salt" : "eyJjb25kaXRpb25zIjp7IklTX1dBU0kiOnRydWUsIlVTRV9TSEFSRURfTUVNT1JZIjpmYWxzZSwiVVNFX1dBU0lfQ0ROIjpmYWxzZX0sInN1YnN0aXR1dGlvbnMiOnsiUEFDS0FHRV9UT19KU19NT0RVTEVfUEFUSCI6Im1haW4ud2FzbSIsIlBBQ0tBR0VfVE9fSlNfUEFDS0FHRV9OQU1FIjoidGVzdCJ9fQ==",
+    "salt" : "eyJjb25kaXRpb25zIjp7IkhBU19CUklER0UiOmZhbHNlLCJIQVNfSU1QT1JUUyI6ZmFsc2UsIklTX1dBU0kiOnRydWUsIlVTRV9TSEFSRURfTUVNT1JZIjpmYWxzZSwiVVNFX1dBU0lfQ0ROIjpmYWxzZX0sInN1YnN0aXR1dGlvbnMiOnsiUEFDS0FHRV9UT19KU19NT0RVTEVfUEFUSCI6Im1haW4ud2FzbSIsIlBBQ0tBR0VfVE9fSlNfUEFDS0FHRV9OQU1FIjoidGVzdCJ9fQ==",
     "wants" : [
       "$OUTPUT",
       "$OUTPUT\/platforms",
@@ -238,7 +238,7 @@
       "$INTERMEDIATES\/wasm-imports.json"
     ],
     "output" : "$OUTPUT\/platforms\/node.js",
-    "salt" : "eyJjb25kaXRpb25zIjp7IklTX1dBU0kiOnRydWUsIlVTRV9TSEFSRURfTUVNT1JZIjpmYWxzZSwiVVNFX1dBU0lfQ0ROIjpmYWxzZX0sInN1YnN0aXR1dGlvbnMiOnsiUEFDS0FHRV9UT19KU19NT0RVTEVfUEFUSCI6Im1haW4ud2FzbSIsIlBBQ0tBR0VfVE9fSlNfUEFDS0FHRV9OQU1FIjoidGVzdCJ9fQ==",
+    "salt" : "eyJjb25kaXRpb25zIjp7IkhBU19CUklER0UiOmZhbHNlLCJIQVNfSU1QT1JUUyI6ZmFsc2UsIklTX1dBU0kiOnRydWUsIlVTRV9TSEFSRURfTUVNT1JZIjpmYWxzZSwiVVNFX1dBU0lfQ0ROIjpmYWxzZX0sInN1YnN0aXR1dGlvbnMiOnsiUEFDS0FHRV9UT19KU19NT0RVTEVfUEFUSCI6Im1haW4ud2FzbSIsIlBBQ0tBR0VfVE9fSlNfUEFDS0FHRV9OQU1FIjoidGVzdCJ9fQ==",
     "wants" : [
       "$OUTPUT",
       "$OUTPUT\/platforms",
@@ -255,7 +255,7 @@
       "$INTERMEDIATES\/wasm-imports.json"
     ],
     "output" : "$OUTPUT\/runtime.d.ts",
-    "salt" : "eyJjb25kaXRpb25zIjp7IklTX1dBU0kiOnRydWUsIlVTRV9TSEFSRURfTUVNT1JZIjpmYWxzZSwiVVNFX1dBU0lfQ0ROIjpmYWxzZX0sInN1YnN0aXR1dGlvbnMiOnsiUEFDS0FHRV9UT19KU19NT0RVTEVfUEFUSCI6Im1haW4ud2FzbSIsIlBBQ0tBR0VfVE9fSlNfUEFDS0FHRV9OQU1FIjoidGVzdCJ9fQ==",
+    "salt" : "eyJjb25kaXRpb25zIjp7IkhBU19CUklER0UiOmZhbHNlLCJIQVNfSU1QT1JUUyI6ZmFsc2UsIklTX1dBU0kiOnRydWUsIlVTRV9TSEFSRURfTUVNT1JZIjpmYWxzZSwiVVNFX1dBU0lfQ0ROIjpmYWxzZX0sInN1YnN0aXR1dGlvbnMiOnsiUEFDS0FHRV9UT19KU19NT0RVTEVfUEFUSCI6Im1haW4ud2FzbSIsIlBBQ0tBR0VfVE9fSlNfUEFDS0FHRV9OQU1FIjoidGVzdCJ9fQ==",
     "wants" : [
       "$OUTPUT",
       "$OUTPUT\/platforms",
@@ -272,7 +272,7 @@
       "$INTERMEDIATES\/wasm-imports.json"
     ],
     "output" : "$OUTPUT\/runtime.js",
-    "salt" : "eyJjb25kaXRpb25zIjp7IklTX1dBU0kiOnRydWUsIlVTRV9TSEFSRURfTUVNT1JZIjpmYWxzZSwiVVNFX1dBU0lfQ0ROIjpmYWxzZX0sInN1YnN0aXR1dGlvbnMiOnsiUEFDS0FHRV9UT19KU19NT0RVTEVfUEFUSCI6Im1haW4ud2FzbSIsIlBBQ0tBR0VfVE9fSlNfUEFDS0FHRV9OQU1FIjoidGVzdCJ9fQ==",
+    "salt" : "eyJjb25kaXRpb25zIjp7IkhBU19CUklER0UiOmZhbHNlLCJIQVNfSU1QT1JUUyI6ZmFsc2UsIklTX1dBU0kiOnRydWUsIlVTRV9TSEFSRURfTUVNT1JZIjpmYWxzZSwiVVNFX1dBU0lfQ0ROIjpmYWxzZX0sInN1YnN0aXR1dGlvbnMiOnsiUEFDS0FHRV9UT19KU19NT0RVTEVfUEFUSCI6Im1haW4ud2FzbSIsIlBBQ0tBR0VfVE9fSlNfUEFDS0FHRV9OQU1FIjoidGVzdCJ9fQ==",
     "wants" : [
       "$OUTPUT",
       "$OUTPUT\/platforms",
diff --git a/Plugins/PackageToJS/Tests/__Snapshots__/PackagingPlannerTests/planBuild_release_no_optimize.json b/Plugins/PackageToJS/Tests/__Snapshots__/PackagingPlannerTests/planBuild_release_no_optimize.json
index e525d1347..13768da75 100644
--- a/Plugins/PackageToJS/Tests/__Snapshots__/PackagingPlannerTests/planBuild_release_no_optimize.json
+++ b/Plugins/PackageToJS/Tests/__Snapshots__/PackagingPlannerTests/planBuild_release_no_optimize.json
@@ -48,7 +48,7 @@
       "$INTERMEDIATES\/wasm-imports.json"
     ],
     "output" : "$OUTPUT\/index.d.ts",
-    "salt" : "eyJjb25kaXRpb25zIjp7IklTX1dBU0kiOnRydWUsIlVTRV9TSEFSRURfTUVNT1JZIjpmYWxzZSwiVVNFX1dBU0lfQ0ROIjpmYWxzZX0sInN1YnN0aXR1dGlvbnMiOnsiUEFDS0FHRV9UT19KU19NT0RVTEVfUEFUSCI6Im1haW4ud2FzbSIsIlBBQ0tBR0VfVE9fSlNfUEFDS0FHRV9OQU1FIjoidGVzdCJ9fQ==",
+    "salt" : "eyJjb25kaXRpb25zIjp7IkhBU19CUklER0UiOmZhbHNlLCJIQVNfSU1QT1JUUyI6ZmFsc2UsIklTX1dBU0kiOnRydWUsIlVTRV9TSEFSRURfTUVNT1JZIjpmYWxzZSwiVVNFX1dBU0lfQ0ROIjpmYWxzZX0sInN1YnN0aXR1dGlvbnMiOnsiUEFDS0FHRV9UT19KU19NT0RVTEVfUEFUSCI6Im1haW4ud2FzbSIsIlBBQ0tBR0VfVE9fSlNfUEFDS0FHRV9OQU1FIjoidGVzdCJ9fQ==",
     "wants" : [
       "$OUTPUT",
       "$OUTPUT\/platforms",
@@ -65,7 +65,7 @@
       "$INTERMEDIATES\/wasm-imports.json"
     ],
     "output" : "$OUTPUT\/index.js",
-    "salt" : "eyJjb25kaXRpb25zIjp7IklTX1dBU0kiOnRydWUsIlVTRV9TSEFSRURfTUVNT1JZIjpmYWxzZSwiVVNFX1dBU0lfQ0ROIjpmYWxzZX0sInN1YnN0aXR1dGlvbnMiOnsiUEFDS0FHRV9UT19KU19NT0RVTEVfUEFUSCI6Im1haW4ud2FzbSIsIlBBQ0tBR0VfVE9fSlNfUEFDS0FHRV9OQU1FIjoidGVzdCJ9fQ==",
+    "salt" : "eyJjb25kaXRpb25zIjp7IkhBU19CUklER0UiOmZhbHNlLCJIQVNfSU1QT1JUUyI6ZmFsc2UsIklTX1dBU0kiOnRydWUsIlVTRV9TSEFSRURfTUVNT1JZIjpmYWxzZSwiVVNFX1dBU0lfQ0ROIjpmYWxzZX0sInN1YnN0aXR1dGlvbnMiOnsiUEFDS0FHRV9UT19KU19NT0RVTEVfUEFUSCI6Im1haW4ud2FzbSIsIlBBQ0tBR0VfVE9fSlNfUEFDS0FHRV9OQU1FIjoidGVzdCJ9fQ==",
     "wants" : [
       "$OUTPUT",
       "$OUTPUT\/platforms",
@@ -82,7 +82,7 @@
       "$INTERMEDIATES\/wasm-imports.json"
     ],
     "output" : "$OUTPUT\/instantiate.d.ts",
-    "salt" : "eyJjb25kaXRpb25zIjp7IklTX1dBU0kiOnRydWUsIlVTRV9TSEFSRURfTUVNT1JZIjpmYWxzZSwiVVNFX1dBU0lfQ0ROIjpmYWxzZX0sInN1YnN0aXR1dGlvbnMiOnsiUEFDS0FHRV9UT19KU19NT0RVTEVfUEFUSCI6Im1haW4ud2FzbSIsIlBBQ0tBR0VfVE9fSlNfUEFDS0FHRV9OQU1FIjoidGVzdCJ9fQ==",
+    "salt" : "eyJjb25kaXRpb25zIjp7IkhBU19CUklER0UiOmZhbHNlLCJIQVNfSU1QT1JUUyI6ZmFsc2UsIklTX1dBU0kiOnRydWUsIlVTRV9TSEFSRURfTUVNT1JZIjpmYWxzZSwiVVNFX1dBU0lfQ0ROIjpmYWxzZX0sInN1YnN0aXR1dGlvbnMiOnsiUEFDS0FHRV9UT19KU19NT0RVTEVfUEFUSCI6Im1haW4ud2FzbSIsIlBBQ0tBR0VfVE9fSlNfUEFDS0FHRV9OQU1FIjoidGVzdCJ9fQ==",
     "wants" : [
       "$OUTPUT",
       "$OUTPUT\/platforms",
@@ -99,7 +99,7 @@
       "$INTERMEDIATES\/wasm-imports.json"
     ],
     "output" : "$OUTPUT\/instantiate.js",
-    "salt" : "eyJjb25kaXRpb25zIjp7IklTX1dBU0kiOnRydWUsIlVTRV9TSEFSRURfTUVNT1JZIjpmYWxzZSwiVVNFX1dBU0lfQ0ROIjpmYWxzZX0sInN1YnN0aXR1dGlvbnMiOnsiUEFDS0FHRV9UT19KU19NT0RVTEVfUEFUSCI6Im1haW4ud2FzbSIsIlBBQ0tBR0VfVE9fSlNfUEFDS0FHRV9OQU1FIjoidGVzdCJ9fQ==",
+    "salt" : "eyJjb25kaXRpb25zIjp7IkhBU19CUklER0UiOmZhbHNlLCJIQVNfSU1QT1JUUyI6ZmFsc2UsIklTX1dBU0kiOnRydWUsIlVTRV9TSEFSRURfTUVNT1JZIjpmYWxzZSwiVVNFX1dBU0lfQ0ROIjpmYWxzZX0sInN1YnN0aXR1dGlvbnMiOnsiUEFDS0FHRV9UT19KU19NT0RVTEVfUEFUSCI6Im1haW4ud2FzbSIsIlBBQ0tBR0VfVE9fSlNfUEFDS0FHRV9OQU1FIjoidGVzdCJ9fQ==",
     "wants" : [
       "$OUTPUT",
       "$OUTPUT\/platforms",
@@ -128,7 +128,7 @@
       "$SELF_PACKAGE\/Plugins\/PackageToJS\/Templates\/package.json"
     ],
     "output" : "$OUTPUT\/package.json",
-    "salt" : "eyJjb25kaXRpb25zIjp7IklTX1dBU0kiOnRydWUsIlVTRV9TSEFSRURfTUVNT1JZIjpmYWxzZSwiVVNFX1dBU0lfQ0ROIjpmYWxzZX0sInN1YnN0aXR1dGlvbnMiOnsiUEFDS0FHRV9UT19KU19NT0RVTEVfUEFUSCI6Im1haW4ud2FzbSIsIlBBQ0tBR0VfVE9fSlNfUEFDS0FHRV9OQU1FIjoidGVzdCJ9fQ==",
+    "salt" : "eyJjb25kaXRpb25zIjp7IkhBU19CUklER0UiOmZhbHNlLCJIQVNfSU1QT1JUUyI6ZmFsc2UsIklTX1dBU0kiOnRydWUsIlVTRV9TSEFSRURfTUVNT1JZIjpmYWxzZSwiVVNFX1dBU0lfQ0ROIjpmYWxzZX0sInN1YnN0aXR1dGlvbnMiOnsiUEFDS0FHRV9UT19KU19NT0RVTEVfUEFUSCI6Im1haW4ud2FzbSIsIlBBQ0tBR0VfVE9fSlNfUEFDS0FHRV9OQU1FIjoidGVzdCJ9fQ==",
     "wants" : [
       "$OUTPUT"
     ]
@@ -155,7 +155,7 @@
       "$INTERMEDIATES\/wasm-imports.json"
     ],
     "output" : "$OUTPUT\/platforms\/browser.d.ts",
-    "salt" : "eyJjb25kaXRpb25zIjp7IklTX1dBU0kiOnRydWUsIlVTRV9TSEFSRURfTUVNT1JZIjpmYWxzZSwiVVNFX1dBU0lfQ0ROIjpmYWxzZX0sInN1YnN0aXR1dGlvbnMiOnsiUEFDS0FHRV9UT19KU19NT0RVTEVfUEFUSCI6Im1haW4ud2FzbSIsIlBBQ0tBR0VfVE9fSlNfUEFDS0FHRV9OQU1FIjoidGVzdCJ9fQ==",
+    "salt" : "eyJjb25kaXRpb25zIjp7IkhBU19CUklER0UiOmZhbHNlLCJIQVNfSU1QT1JUUyI6ZmFsc2UsIklTX1dBU0kiOnRydWUsIlVTRV9TSEFSRURfTUVNT1JZIjpmYWxzZSwiVVNFX1dBU0lfQ0ROIjpmYWxzZX0sInN1YnN0aXR1dGlvbnMiOnsiUEFDS0FHRV9UT19KU19NT0RVTEVfUEFUSCI6Im1haW4ud2FzbSIsIlBBQ0tBR0VfVE9fSlNfUEFDS0FHRV9OQU1FIjoidGVzdCJ9fQ==",
     "wants" : [
       "$OUTPUT",
       "$OUTPUT\/platforms",
@@ -172,7 +172,7 @@
       "$INTERMEDIATES\/wasm-imports.json"
     ],
     "output" : "$OUTPUT\/platforms\/browser.js",
-    "salt" : "eyJjb25kaXRpb25zIjp7IklTX1dBU0kiOnRydWUsIlVTRV9TSEFSRURfTUVNT1JZIjpmYWxzZSwiVVNFX1dBU0lfQ0ROIjpmYWxzZX0sInN1YnN0aXR1dGlvbnMiOnsiUEFDS0FHRV9UT19KU19NT0RVTEVfUEFUSCI6Im1haW4ud2FzbSIsIlBBQ0tBR0VfVE9fSlNfUEFDS0FHRV9OQU1FIjoidGVzdCJ9fQ==",
+    "salt" : "eyJjb25kaXRpb25zIjp7IkhBU19CUklER0UiOmZhbHNlLCJIQVNfSU1QT1JUUyI6ZmFsc2UsIklTX1dBU0kiOnRydWUsIlVTRV9TSEFSRURfTUVNT1JZIjpmYWxzZSwiVVNFX1dBU0lfQ0ROIjpmYWxzZX0sInN1YnN0aXR1dGlvbnMiOnsiUEFDS0FHRV9UT19KU19NT0RVTEVfUEFUSCI6Im1haW4ud2FzbSIsIlBBQ0tBR0VfVE9fSlNfUEFDS0FHRV9OQU1FIjoidGVzdCJ9fQ==",
     "wants" : [
       "$OUTPUT",
       "$OUTPUT\/platforms",
@@ -189,7 +189,7 @@
       "$INTERMEDIATES\/wasm-imports.json"
     ],
     "output" : "$OUTPUT\/platforms\/browser.worker.js",
-    "salt" : "eyJjb25kaXRpb25zIjp7IklTX1dBU0kiOnRydWUsIlVTRV9TSEFSRURfTUVNT1JZIjpmYWxzZSwiVVNFX1dBU0lfQ0ROIjpmYWxzZX0sInN1YnN0aXR1dGlvbnMiOnsiUEFDS0FHRV9UT19KU19NT0RVTEVfUEFUSCI6Im1haW4ud2FzbSIsIlBBQ0tBR0VfVE9fSlNfUEFDS0FHRV9OQU1FIjoidGVzdCJ9fQ==",
+    "salt" : "eyJjb25kaXRpb25zIjp7IkhBU19CUklER0UiOmZhbHNlLCJIQVNfSU1QT1JUUyI6ZmFsc2UsIklTX1dBU0kiOnRydWUsIlVTRV9TSEFSRURfTUVNT1JZIjpmYWxzZSwiVVNFX1dBU0lfQ0ROIjpmYWxzZX0sInN1YnN0aXR1dGlvbnMiOnsiUEFDS0FHRV9UT19KU19NT0RVTEVfUEFUSCI6Im1haW4ud2FzbSIsIlBBQ0tBR0VfVE9fSlNfUEFDS0FHRV9OQU1FIjoidGVzdCJ9fQ==",
     "wants" : [
       "$OUTPUT",
       "$OUTPUT\/platforms",
@@ -206,7 +206,7 @@
       "$INTERMEDIATES\/wasm-imports.json"
     ],
     "output" : "$OUTPUT\/platforms\/node.d.ts",
-    "salt" : "eyJjb25kaXRpb25zIjp7IklTX1dBU0kiOnRydWUsIlVTRV9TSEFSRURfTUVNT1JZIjpmYWxzZSwiVVNFX1dBU0lfQ0ROIjpmYWxzZX0sInN1YnN0aXR1dGlvbnMiOnsiUEFDS0FHRV9UT19KU19NT0RVTEVfUEFUSCI6Im1haW4ud2FzbSIsIlBBQ0tBR0VfVE9fSlNfUEFDS0FHRV9OQU1FIjoidGVzdCJ9fQ==",
+    "salt" : "eyJjb25kaXRpb25zIjp7IkhBU19CUklER0UiOmZhbHNlLCJIQVNfSU1QT1JUUyI6ZmFsc2UsIklTX1dBU0kiOnRydWUsIlVTRV9TSEFSRURfTUVNT1JZIjpmYWxzZSwiVVNFX1dBU0lfQ0ROIjpmYWxzZX0sInN1YnN0aXR1dGlvbnMiOnsiUEFDS0FHRV9UT19KU19NT0RVTEVfUEFUSCI6Im1haW4ud2FzbSIsIlBBQ0tBR0VfVE9fSlNfUEFDS0FHRV9OQU1FIjoidGVzdCJ9fQ==",
     "wants" : [
       "$OUTPUT",
       "$OUTPUT\/platforms",
@@ -223,7 +223,7 @@
       "$INTERMEDIATES\/wasm-imports.json"
     ],
     "output" : "$OUTPUT\/platforms\/node.js",
-    "salt" : "eyJjb25kaXRpb25zIjp7IklTX1dBU0kiOnRydWUsIlVTRV9TSEFSRURfTUVNT1JZIjpmYWxzZSwiVVNFX1dBU0lfQ0ROIjpmYWxzZX0sInN1YnN0aXR1dGlvbnMiOnsiUEFDS0FHRV9UT19KU19NT0RVTEVfUEFUSCI6Im1haW4ud2FzbSIsIlBBQ0tBR0VfVE9fSlNfUEFDS0FHRV9OQU1FIjoidGVzdCJ9fQ==",
+    "salt" : "eyJjb25kaXRpb25zIjp7IkhBU19CUklER0UiOmZhbHNlLCJIQVNfSU1QT1JUUyI6ZmFsc2UsIklTX1dBU0kiOnRydWUsIlVTRV9TSEFSRURfTUVNT1JZIjpmYWxzZSwiVVNFX1dBU0lfQ0ROIjpmYWxzZX0sInN1YnN0aXR1dGlvbnMiOnsiUEFDS0FHRV9UT19KU19NT0RVTEVfUEFUSCI6Im1haW4ud2FzbSIsIlBBQ0tBR0VfVE9fSlNfUEFDS0FHRV9OQU1FIjoidGVzdCJ9fQ==",
     "wants" : [
       "$OUTPUT",
       "$OUTPUT\/platforms",
@@ -240,7 +240,7 @@
       "$INTERMEDIATES\/wasm-imports.json"
     ],
     "output" : "$OUTPUT\/runtime.d.ts",
-    "salt" : "eyJjb25kaXRpb25zIjp7IklTX1dBU0kiOnRydWUsIlVTRV9TSEFSRURfTUVNT1JZIjpmYWxzZSwiVVNFX1dBU0lfQ0ROIjpmYWxzZX0sInN1YnN0aXR1dGlvbnMiOnsiUEFDS0FHRV9UT19KU19NT0RVTEVfUEFUSCI6Im1haW4ud2FzbSIsIlBBQ0tBR0VfVE9fSlNfUEFDS0FHRV9OQU1FIjoidGVzdCJ9fQ==",
+    "salt" : "eyJjb25kaXRpb25zIjp7IkhBU19CUklER0UiOmZhbHNlLCJIQVNfSU1QT1JUUyI6ZmFsc2UsIklTX1dBU0kiOnRydWUsIlVTRV9TSEFSRURfTUVNT1JZIjpmYWxzZSwiVVNFX1dBU0lfQ0ROIjpmYWxzZX0sInN1YnN0aXR1dGlvbnMiOnsiUEFDS0FHRV9UT19KU19NT0RVTEVfUEFUSCI6Im1haW4ud2FzbSIsIlBBQ0tBR0VfVE9fSlNfUEFDS0FHRV9OQU1FIjoidGVzdCJ9fQ==",
     "wants" : [
       "$OUTPUT",
       "$OUTPUT\/platforms",
@@ -257,7 +257,7 @@
       "$INTERMEDIATES\/wasm-imports.json"
     ],
     "output" : "$OUTPUT\/runtime.js",
-    "salt" : "eyJjb25kaXRpb25zIjp7IklTX1dBU0kiOnRydWUsIlVTRV9TSEFSRURfTUVNT1JZIjpmYWxzZSwiVVNFX1dBU0lfQ0ROIjpmYWxzZX0sInN1YnN0aXR1dGlvbnMiOnsiUEFDS0FHRV9UT19KU19NT0RVTEVfUEFUSCI6Im1haW4ud2FzbSIsIlBBQ0tBR0VfVE9fSlNfUEFDS0FHRV9OQU1FIjoidGVzdCJ9fQ==",
+    "salt" : "eyJjb25kaXRpb25zIjp7IkhBU19CUklER0UiOmZhbHNlLCJIQVNfSU1QT1JUUyI6ZmFsc2UsIklTX1dBU0kiOnRydWUsIlVTRV9TSEFSRURfTUVNT1JZIjpmYWxzZSwiVVNFX1dBU0lfQ0ROIjpmYWxzZX0sInN1YnN0aXR1dGlvbnMiOnsiUEFDS0FHRV9UT19KU19NT0RVTEVfUEFUSCI6Im1haW4ud2FzbSIsIlBBQ0tBR0VfVE9fSlNfUEFDS0FHRV9OQU1FIjoidGVzdCJ9fQ==",
     "wants" : [
       "$OUTPUT",
       "$OUTPUT\/platforms",
diff --git a/Plugins/PackageToJS/Tests/__Snapshots__/PackagingPlannerTests/planTestBuild.json b/Plugins/PackageToJS/Tests/__Snapshots__/PackagingPlannerTests/planTestBuild.json
index 2be6ce1d6..89425dc83 100644
--- a/Plugins/PackageToJS/Tests/__Snapshots__/PackagingPlannerTests/planTestBuild.json
+++ b/Plugins/PackageToJS/Tests/__Snapshots__/PackagingPlannerTests/planTestBuild.json
@@ -73,7 +73,7 @@
       "$SELF_PACKAGE\/Plugins\/PackageToJS\/Templates\/bin\/test.js"
     ],
     "output" : "$OUTPUT\/bin\/test.js",
-    "salt" : "eyJjb25kaXRpb25zIjp7IklTX1dBU0kiOnRydWUsIlVTRV9TSEFSRURfTUVNT1JZIjpmYWxzZSwiVVNFX1dBU0lfQ0ROIjpmYWxzZX0sInN1YnN0aXR1dGlvbnMiOnsiUEFDS0FHRV9UT19KU19NT0RVTEVfUEFUSCI6Im1haW4ud2FzbSIsIlBBQ0tBR0VfVE9fSlNfUEFDS0FHRV9OQU1FIjoidGVzdCJ9fQ==",
+    "salt" : "eyJjb25kaXRpb25zIjp7IkhBU19CUklER0UiOmZhbHNlLCJIQVNfSU1QT1JUUyI6ZmFsc2UsIklTX1dBU0kiOnRydWUsIlVTRV9TSEFSRURfTUVNT1JZIjpmYWxzZSwiVVNFX1dBU0lfQ0ROIjpmYWxzZX0sInN1YnN0aXR1dGlvbnMiOnsiUEFDS0FHRV9UT19KU19NT0RVTEVfUEFUSCI6Im1haW4ud2FzbSIsIlBBQ0tBR0VfVE9fSlNfUEFDS0FHRV9OQU1FIjoidGVzdCJ9fQ==",
     "wants" : [
       "$OUTPUT",
       "$OUTPUT\/bin"
@@ -89,7 +89,7 @@
       "$INTERMEDIATES\/wasm-imports.json"
     ],
     "output" : "$OUTPUT\/index.d.ts",
-    "salt" : "eyJjb25kaXRpb25zIjp7IklTX1dBU0kiOnRydWUsIlVTRV9TSEFSRURfTUVNT1JZIjpmYWxzZSwiVVNFX1dBU0lfQ0ROIjpmYWxzZX0sInN1YnN0aXR1dGlvbnMiOnsiUEFDS0FHRV9UT19KU19NT0RVTEVfUEFUSCI6Im1haW4ud2FzbSIsIlBBQ0tBR0VfVE9fSlNfUEFDS0FHRV9OQU1FIjoidGVzdCJ9fQ==",
+    "salt" : "eyJjb25kaXRpb25zIjp7IkhBU19CUklER0UiOmZhbHNlLCJIQVNfSU1QT1JUUyI6ZmFsc2UsIklTX1dBU0kiOnRydWUsIlVTRV9TSEFSRURfTUVNT1JZIjpmYWxzZSwiVVNFX1dBU0lfQ0ROIjpmYWxzZX0sInN1YnN0aXR1dGlvbnMiOnsiUEFDS0FHRV9UT19KU19NT0RVTEVfUEFUSCI6Im1haW4ud2FzbSIsIlBBQ0tBR0VfVE9fSlNfUEFDS0FHRV9OQU1FIjoidGVzdCJ9fQ==",
     "wants" : [
       "$OUTPUT",
       "$OUTPUT\/platforms",
@@ -106,7 +106,7 @@
       "$INTERMEDIATES\/wasm-imports.json"
     ],
     "output" : "$OUTPUT\/index.js",
-    "salt" : "eyJjb25kaXRpb25zIjp7IklTX1dBU0kiOnRydWUsIlVTRV9TSEFSRURfTUVNT1JZIjpmYWxzZSwiVVNFX1dBU0lfQ0ROIjpmYWxzZX0sInN1YnN0aXR1dGlvbnMiOnsiUEFDS0FHRV9UT19KU19NT0RVTEVfUEFUSCI6Im1haW4ud2FzbSIsIlBBQ0tBR0VfVE9fSlNfUEFDS0FHRV9OQU1FIjoidGVzdCJ9fQ==",
+    "salt" : "eyJjb25kaXRpb25zIjp7IkhBU19CUklER0UiOmZhbHNlLCJIQVNfSU1QT1JUUyI6ZmFsc2UsIklTX1dBU0kiOnRydWUsIlVTRV9TSEFSRURfTUVNT1JZIjpmYWxzZSwiVVNFX1dBU0lfQ0ROIjpmYWxzZX0sInN1YnN0aXR1dGlvbnMiOnsiUEFDS0FHRV9UT19KU19NT0RVTEVfUEFUSCI6Im1haW4ud2FzbSIsIlBBQ0tBR0VfVE9fSlNfUEFDS0FHRV9OQU1FIjoidGVzdCJ9fQ==",
     "wants" : [
       "$OUTPUT",
       "$OUTPUT\/platforms",
@@ -123,7 +123,7 @@
       "$INTERMEDIATES\/wasm-imports.json"
     ],
     "output" : "$OUTPUT\/instantiate.d.ts",
-    "salt" : "eyJjb25kaXRpb25zIjp7IklTX1dBU0kiOnRydWUsIlVTRV9TSEFSRURfTUVNT1JZIjpmYWxzZSwiVVNFX1dBU0lfQ0ROIjpmYWxzZX0sInN1YnN0aXR1dGlvbnMiOnsiUEFDS0FHRV9UT19KU19NT0RVTEVfUEFUSCI6Im1haW4ud2FzbSIsIlBBQ0tBR0VfVE9fSlNfUEFDS0FHRV9OQU1FIjoidGVzdCJ9fQ==",
+    "salt" : "eyJjb25kaXRpb25zIjp7IkhBU19CUklER0UiOmZhbHNlLCJIQVNfSU1QT1JUUyI6ZmFsc2UsIklTX1dBU0kiOnRydWUsIlVTRV9TSEFSRURfTUVNT1JZIjpmYWxzZSwiVVNFX1dBU0lfQ0ROIjpmYWxzZX0sInN1YnN0aXR1dGlvbnMiOnsiUEFDS0FHRV9UT19KU19NT0RVTEVfUEFUSCI6Im1haW4ud2FzbSIsIlBBQ0tBR0VfVE9fSlNfUEFDS0FHRV9OQU1FIjoidGVzdCJ9fQ==",
     "wants" : [
       "$OUTPUT",
       "$OUTPUT\/platforms",
@@ -140,7 +140,7 @@
       "$INTERMEDIATES\/wasm-imports.json"
     ],
     "output" : "$OUTPUT\/instantiate.js",
-    "salt" : "eyJjb25kaXRpb25zIjp7IklTX1dBU0kiOnRydWUsIlVTRV9TSEFSRURfTUVNT1JZIjpmYWxzZSwiVVNFX1dBU0lfQ0ROIjpmYWxzZX0sInN1YnN0aXR1dGlvbnMiOnsiUEFDS0FHRV9UT19KU19NT0RVTEVfUEFUSCI6Im1haW4ud2FzbSIsIlBBQ0tBR0VfVE9fSlNfUEFDS0FHRV9OQU1FIjoidGVzdCJ9fQ==",
+    "salt" : "eyJjb25kaXRpb25zIjp7IkhBU19CUklER0UiOmZhbHNlLCJIQVNfSU1QT1JUUyI6ZmFsc2UsIklTX1dBU0kiOnRydWUsIlVTRV9TSEFSRURfTUVNT1JZIjpmYWxzZSwiVVNFX1dBU0lfQ0ROIjpmYWxzZX0sInN1YnN0aXR1dGlvbnMiOnsiUEFDS0FHRV9UT19KU19NT0RVTEVfUEFUSCI6Im1haW4ud2FzbSIsIlBBQ0tBR0VfVE9fSlNfUEFDS0FHRV9OQU1FIjoidGVzdCJ9fQ==",
     "wants" : [
       "$OUTPUT",
       "$OUTPUT\/platforms",
@@ -169,7 +169,7 @@
       "$SELF_PACKAGE\/Plugins\/PackageToJS\/Templates\/package.json"
     ],
     "output" : "$OUTPUT\/package.json",
-    "salt" : "eyJjb25kaXRpb25zIjp7IklTX1dBU0kiOnRydWUsIlVTRV9TSEFSRURfTUVNT1JZIjpmYWxzZSwiVVNFX1dBU0lfQ0ROIjpmYWxzZX0sInN1YnN0aXR1dGlvbnMiOnsiUEFDS0FHRV9UT19KU19NT0RVTEVfUEFUSCI6Im1haW4ud2FzbSIsIlBBQ0tBR0VfVE9fSlNfUEFDS0FHRV9OQU1FIjoidGVzdCJ9fQ==",
+    "salt" : "eyJjb25kaXRpb25zIjp7IkhBU19CUklER0UiOmZhbHNlLCJIQVNfSU1QT1JUUyI6ZmFsc2UsIklTX1dBU0kiOnRydWUsIlVTRV9TSEFSRURfTUVNT1JZIjpmYWxzZSwiVVNFX1dBU0lfQ0ROIjpmYWxzZX0sInN1YnN0aXR1dGlvbnMiOnsiUEFDS0FHRV9UT19KU19NT0RVTEVfUEFUSCI6Im1haW4ud2FzbSIsIlBBQ0tBR0VfVE9fSlNfUEFDS0FHRV9OQU1FIjoidGVzdCJ9fQ==",
     "wants" : [
       "$OUTPUT"
     ]
@@ -196,7 +196,7 @@
       "$INTERMEDIATES\/wasm-imports.json"
     ],
     "output" : "$OUTPUT\/platforms\/browser.d.ts",
-    "salt" : "eyJjb25kaXRpb25zIjp7IklTX1dBU0kiOnRydWUsIlVTRV9TSEFSRURfTUVNT1JZIjpmYWxzZSwiVVNFX1dBU0lfQ0ROIjpmYWxzZX0sInN1YnN0aXR1dGlvbnMiOnsiUEFDS0FHRV9UT19KU19NT0RVTEVfUEFUSCI6Im1haW4ud2FzbSIsIlBBQ0tBR0VfVE9fSlNfUEFDS0FHRV9OQU1FIjoidGVzdCJ9fQ==",
+    "salt" : "eyJjb25kaXRpb25zIjp7IkhBU19CUklER0UiOmZhbHNlLCJIQVNfSU1QT1JUUyI6ZmFsc2UsIklTX1dBU0kiOnRydWUsIlVTRV9TSEFSRURfTUVNT1JZIjpmYWxzZSwiVVNFX1dBU0lfQ0ROIjpmYWxzZX0sInN1YnN0aXR1dGlvbnMiOnsiUEFDS0FHRV9UT19KU19NT0RVTEVfUEFUSCI6Im1haW4ud2FzbSIsIlBBQ0tBR0VfVE9fSlNfUEFDS0FHRV9OQU1FIjoidGVzdCJ9fQ==",
     "wants" : [
       "$OUTPUT",
       "$OUTPUT\/platforms",
@@ -213,7 +213,7 @@
       "$INTERMEDIATES\/wasm-imports.json"
     ],
     "output" : "$OUTPUT\/platforms\/browser.js",
-    "salt" : "eyJjb25kaXRpb25zIjp7IklTX1dBU0kiOnRydWUsIlVTRV9TSEFSRURfTUVNT1JZIjpmYWxzZSwiVVNFX1dBU0lfQ0ROIjpmYWxzZX0sInN1YnN0aXR1dGlvbnMiOnsiUEFDS0FHRV9UT19KU19NT0RVTEVfUEFUSCI6Im1haW4ud2FzbSIsIlBBQ0tBR0VfVE9fSlNfUEFDS0FHRV9OQU1FIjoidGVzdCJ9fQ==",
+    "salt" : "eyJjb25kaXRpb25zIjp7IkhBU19CUklER0UiOmZhbHNlLCJIQVNfSU1QT1JUUyI6ZmFsc2UsIklTX1dBU0kiOnRydWUsIlVTRV9TSEFSRURfTUVNT1JZIjpmYWxzZSwiVVNFX1dBU0lfQ0ROIjpmYWxzZX0sInN1YnN0aXR1dGlvbnMiOnsiUEFDS0FHRV9UT19KU19NT0RVTEVfUEFUSCI6Im1haW4ud2FzbSIsIlBBQ0tBR0VfVE9fSlNfUEFDS0FHRV9OQU1FIjoidGVzdCJ9fQ==",
     "wants" : [
       "$OUTPUT",
       "$OUTPUT\/platforms",
@@ -230,7 +230,7 @@
       "$INTERMEDIATES\/wasm-imports.json"
     ],
     "output" : "$OUTPUT\/platforms\/browser.worker.js",
-    "salt" : "eyJjb25kaXRpb25zIjp7IklTX1dBU0kiOnRydWUsIlVTRV9TSEFSRURfTUVNT1JZIjpmYWxzZSwiVVNFX1dBU0lfQ0ROIjpmYWxzZX0sInN1YnN0aXR1dGlvbnMiOnsiUEFDS0FHRV9UT19KU19NT0RVTEVfUEFUSCI6Im1haW4ud2FzbSIsIlBBQ0tBR0VfVE9fSlNfUEFDS0FHRV9OQU1FIjoidGVzdCJ9fQ==",
+    "salt" : "eyJjb25kaXRpb25zIjp7IkhBU19CUklER0UiOmZhbHNlLCJIQVNfSU1QT1JUUyI6ZmFsc2UsIklTX1dBU0kiOnRydWUsIlVTRV9TSEFSRURfTUVNT1JZIjpmYWxzZSwiVVNFX1dBU0lfQ0ROIjpmYWxzZX0sInN1YnN0aXR1dGlvbnMiOnsiUEFDS0FHRV9UT19KU19NT0RVTEVfUEFUSCI6Im1haW4ud2FzbSIsIlBBQ0tBR0VfVE9fSlNfUEFDS0FHRV9OQU1FIjoidGVzdCJ9fQ==",
     "wants" : [
       "$OUTPUT",
       "$OUTPUT\/platforms",
@@ -247,7 +247,7 @@
       "$INTERMEDIATES\/wasm-imports.json"
     ],
     "output" : "$OUTPUT\/platforms\/node.d.ts",
-    "salt" : "eyJjb25kaXRpb25zIjp7IklTX1dBU0kiOnRydWUsIlVTRV9TSEFSRURfTUVNT1JZIjpmYWxzZSwiVVNFX1dBU0lfQ0ROIjpmYWxzZX0sInN1YnN0aXR1dGlvbnMiOnsiUEFDS0FHRV9UT19KU19NT0RVTEVfUEFUSCI6Im1haW4ud2FzbSIsIlBBQ0tBR0VfVE9fSlNfUEFDS0FHRV9OQU1FIjoidGVzdCJ9fQ==",
+    "salt" : "eyJjb25kaXRpb25zIjp7IkhBU19CUklER0UiOmZhbHNlLCJIQVNfSU1QT1JUUyI6ZmFsc2UsIklTX1dBU0kiOnRydWUsIlVTRV9TSEFSRURfTUVNT1JZIjpmYWxzZSwiVVNFX1dBU0lfQ0ROIjpmYWxzZX0sInN1YnN0aXR1dGlvbnMiOnsiUEFDS0FHRV9UT19KU19NT0RVTEVfUEFUSCI6Im1haW4ud2FzbSIsIlBBQ0tBR0VfVE9fSlNfUEFDS0FHRV9OQU1FIjoidGVzdCJ9fQ==",
     "wants" : [
       "$OUTPUT",
       "$OUTPUT\/platforms",
@@ -264,7 +264,7 @@
       "$INTERMEDIATES\/wasm-imports.json"
     ],
     "output" : "$OUTPUT\/platforms\/node.js",
-    "salt" : "eyJjb25kaXRpb25zIjp7IklTX1dBU0kiOnRydWUsIlVTRV9TSEFSRURfTUVNT1JZIjpmYWxzZSwiVVNFX1dBU0lfQ0ROIjpmYWxzZX0sInN1YnN0aXR1dGlvbnMiOnsiUEFDS0FHRV9UT19KU19NT0RVTEVfUEFUSCI6Im1haW4ud2FzbSIsIlBBQ0tBR0VfVE9fSlNfUEFDS0FHRV9OQU1FIjoidGVzdCJ9fQ==",
+    "salt" : "eyJjb25kaXRpb25zIjp7IkhBU19CUklER0UiOmZhbHNlLCJIQVNfSU1QT1JUUyI6ZmFsc2UsIklTX1dBU0kiOnRydWUsIlVTRV9TSEFSRURfTUVNT1JZIjpmYWxzZSwiVVNFX1dBU0lfQ0ROIjpmYWxzZX0sInN1YnN0aXR1dGlvbnMiOnsiUEFDS0FHRV9UT19KU19NT0RVTEVfUEFUSCI6Im1haW4ud2FzbSIsIlBBQ0tBR0VfVE9fSlNfUEFDS0FHRV9OQU1FIjoidGVzdCJ9fQ==",
     "wants" : [
       "$OUTPUT",
       "$OUTPUT\/platforms",
@@ -281,7 +281,7 @@
       "$INTERMEDIATES\/wasm-imports.json"
     ],
     "output" : "$OUTPUT\/runtime.d.ts",
-    "salt" : "eyJjb25kaXRpb25zIjp7IklTX1dBU0kiOnRydWUsIlVTRV9TSEFSRURfTUVNT1JZIjpmYWxzZSwiVVNFX1dBU0lfQ0ROIjpmYWxzZX0sInN1YnN0aXR1dGlvbnMiOnsiUEFDS0FHRV9UT19KU19NT0RVTEVfUEFUSCI6Im1haW4ud2FzbSIsIlBBQ0tBR0VfVE9fSlNfUEFDS0FHRV9OQU1FIjoidGVzdCJ9fQ==",
+    "salt" : "eyJjb25kaXRpb25zIjp7IkhBU19CUklER0UiOmZhbHNlLCJIQVNfSU1QT1JUUyI6ZmFsc2UsIklTX1dBU0kiOnRydWUsIlVTRV9TSEFSRURfTUVNT1JZIjpmYWxzZSwiVVNFX1dBU0lfQ0ROIjpmYWxzZX0sInN1YnN0aXR1dGlvbnMiOnsiUEFDS0FHRV9UT19KU19NT0RVTEVfUEFUSCI6Im1haW4ud2FzbSIsIlBBQ0tBR0VfVE9fSlNfUEFDS0FHRV9OQU1FIjoidGVzdCJ9fQ==",
     "wants" : [
       "$OUTPUT",
       "$OUTPUT\/platforms",
@@ -298,7 +298,7 @@
       "$INTERMEDIATES\/wasm-imports.json"
     ],
     "output" : "$OUTPUT\/runtime.js",
-    "salt" : "eyJjb25kaXRpb25zIjp7IklTX1dBU0kiOnRydWUsIlVTRV9TSEFSRURfTUVNT1JZIjpmYWxzZSwiVVNFX1dBU0lfQ0ROIjpmYWxzZX0sInN1YnN0aXR1dGlvbnMiOnsiUEFDS0FHRV9UT19KU19NT0RVTEVfUEFUSCI6Im1haW4ud2FzbSIsIlBBQ0tBR0VfVE9fSlNfUEFDS0FHRV9OQU1FIjoidGVzdCJ9fQ==",
+    "salt" : "eyJjb25kaXRpb25zIjp7IkhBU19CUklER0UiOmZhbHNlLCJIQVNfSU1QT1JUUyI6ZmFsc2UsIklTX1dBU0kiOnRydWUsIlVTRV9TSEFSRURfTUVNT1JZIjpmYWxzZSwiVVNFX1dBU0lfQ0ROIjpmYWxzZX0sInN1YnN0aXR1dGlvbnMiOnsiUEFDS0FHRV9UT19KU19NT0RVTEVfUEFUSCI6Im1haW4ud2FzbSIsIlBBQ0tBR0VfVE9fSlNfUEFDS0FHRV9OQU1FIjoidGVzdCJ9fQ==",
     "wants" : [
       "$OUTPUT",
       "$OUTPUT\/platforms",
@@ -314,7 +314,7 @@
       "$SELF_PACKAGE\/Plugins\/PackageToJS\/Templates\/test.browser.html"
     ],
     "output" : "$OUTPUT\/test.browser.html",
-    "salt" : "eyJjb25kaXRpb25zIjp7IklTX1dBU0kiOnRydWUsIlVTRV9TSEFSRURfTUVNT1JZIjpmYWxzZSwiVVNFX1dBU0lfQ0ROIjpmYWxzZX0sInN1YnN0aXR1dGlvbnMiOnsiUEFDS0FHRV9UT19KU19NT0RVTEVfUEFUSCI6Im1haW4ud2FzbSIsIlBBQ0tBR0VfVE9fSlNfUEFDS0FHRV9OQU1FIjoidGVzdCJ9fQ==",
+    "salt" : "eyJjb25kaXRpb25zIjp7IkhBU19CUklER0UiOmZhbHNlLCJIQVNfSU1QT1JUUyI6ZmFsc2UsIklTX1dBU0kiOnRydWUsIlVTRV9TSEFSRURfTUVNT1JZIjpmYWxzZSwiVVNFX1dBU0lfQ0ROIjpmYWxzZX0sInN1YnN0aXR1dGlvbnMiOnsiUEFDS0FHRV9UT19KU19NT0RVTEVfUEFUSCI6Im1haW4ud2FzbSIsIlBBQ0tBR0VfVE9fSlNfUEFDS0FHRV9OQU1FIjoidGVzdCJ9fQ==",
     "wants" : [
       "$OUTPUT",
       "$OUTPUT\/bin"
@@ -329,7 +329,7 @@
       "$SELF_PACKAGE\/Plugins\/PackageToJS\/Templates\/test.d.ts"
     ],
     "output" : "$OUTPUT\/test.d.ts",
-    "salt" : "eyJjb25kaXRpb25zIjp7IklTX1dBU0kiOnRydWUsIlVTRV9TSEFSRURfTUVNT1JZIjpmYWxzZSwiVVNFX1dBU0lfQ0ROIjpmYWxzZX0sInN1YnN0aXR1dGlvbnMiOnsiUEFDS0FHRV9UT19KU19NT0RVTEVfUEFUSCI6Im1haW4ud2FzbSIsIlBBQ0tBR0VfVE9fSlNfUEFDS0FHRV9OQU1FIjoidGVzdCJ9fQ==",
+    "salt" : "eyJjb25kaXRpb25zIjp7IkhBU19CUklER0UiOmZhbHNlLCJIQVNfSU1QT1JUUyI6ZmFsc2UsIklTX1dBU0kiOnRydWUsIlVTRV9TSEFSRURfTUVNT1JZIjpmYWxzZSwiVVNFX1dBU0lfQ0ROIjpmYWxzZX0sInN1YnN0aXR1dGlvbnMiOnsiUEFDS0FHRV9UT19KU19NT0RVTEVfUEFUSCI6Im1haW4ud2FzbSIsIlBBQ0tBR0VfVE9fSlNfUEFDS0FHRV9OQU1FIjoidGVzdCJ9fQ==",
     "wants" : [
       "$OUTPUT",
       "$OUTPUT\/bin"
@@ -344,7 +344,7 @@
       "$SELF_PACKAGE\/Plugins\/PackageToJS\/Templates\/test.js"
     ],
     "output" : "$OUTPUT\/test.js",
-    "salt" : "eyJjb25kaXRpb25zIjp7IklTX1dBU0kiOnRydWUsIlVTRV9TSEFSRURfTUVNT1JZIjpmYWxzZSwiVVNFX1dBU0lfQ0ROIjpmYWxzZX0sInN1YnN0aXR1dGlvbnMiOnsiUEFDS0FHRV9UT19KU19NT0RVTEVfUEFUSCI6Im1haW4ud2FzbSIsIlBBQ0tBR0VfVE9fSlNfUEFDS0FHRV9OQU1FIjoidGVzdCJ9fQ==",
+    "salt" : "eyJjb25kaXRpb25zIjp7IkhBU19CUklER0UiOmZhbHNlLCJIQVNfSU1QT1JUUyI6ZmFsc2UsIklTX1dBU0kiOnRydWUsIlVTRV9TSEFSRURfTUVNT1JZIjpmYWxzZSwiVVNFX1dBU0lfQ0ROIjpmYWxzZX0sInN1YnN0aXR1dGlvbnMiOnsiUEFDS0FHRV9UT19KU19NT0RVTEVfUEFUSCI6Im1haW4ud2FzbSIsIlBBQ0tBR0VfVE9fSlNfUEFDS0FHRV9OQU1FIjoidGVzdCJ9fQ==",
     "wants" : [
       "$OUTPUT",
       "$OUTPUT\/bin"
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/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..e61664960
--- /dev/null
+++ b/Sources/JavaScriptKit/Documentation.docc/Articles/Importing-TypeScript-into-Swift.md
@@ -0,0 +1,172 @@
+# 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 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
+
+## Getting Started
+
+### Step 1: Configure Your Package
+
+First, add the BridgeJS plugin to your Swift package by modifying your `Package.swift` file:
+
+```swift
+// swift-tools-version:6.0
+
+import PackageDescription
+
+let package = Package(
+    name: "MyApp",
+    dependencies: [
+        .package(url: "https://github.com/swiftwasm/JavaScriptKit.git", branch: "main")
+    ],
+    targets: [
+        .executableTarget(
+            name: "MyApp",
+            dependencies: ["JavaScriptKit"],
+            swiftSettings: [
+                // This is required because the generated code depends on @_extern(wasm)
+                .enableExperimentalFeature("Extern")
+            ],
+            plugins: [
+                // Add build plugin for processing @JS and generate Swift glue code
+                .plugin(name: "BridgeJS", package: "JavaScriptKit")
+            ]
+        )
+    ]
+)
+```
+
+### Step 2: Create TypeScript Definitions
+
+Create a file named `bridge.d.ts` in your target source directory (e.g. `Sources//bridge.d.ts`). This file defines the JavaScript APIs you want to use in Swift:
+
+```typescript
+// Simple function
+export function consoleLog(message: string): void;
+
+// Define a subset of DOM API you want to use
+interface Document {
+    // Properties
+    title: string;
+    readonly body: HTMLElement;
+ 
+    // Methods
+    getElementById(id: string): HTMLElement;
+    createElement(tagName: string): HTMLElement;
+}
+
+// You can use type-level operations like `Pick` to reuse
+// type definitions provided by `lib.dom.d.ts`.
+interface HTMLElement extends Pick {
+    appendChild(child: HTMLElement): void;
+    // TODO: Function types on function signatures are not supported yet.
+    // addEventListener(event: string, handler: (event: any) => void): void;
+}
+
+// Provide access to `document`
+export function getDocument(): Document;
+```
+
+BridgeJS will generate Swift code that matches these TypeScript declarations. For example:
+
+```swift
+func consoleLog(message: String)
+
+struct Document {
+    var title: String { get set }
+    var body: HTMLElement { get }
+
+    func getElementById(_ id: String) -> HTMLElement
+    func createElement(_ tagName: String) -> HTMLElement
+}
+
+struct HTMLElement {
+    var innerText: String { get set }
+    var className: String { get set }
+    
+    func appendChild(_ child: HTMLElement)
+}
+
+func getDocument() -> Document
+```
+
+### Step 3: Build Your Package
+
+Build your package with the following command:
+
+```bash
+swift package --swift-sdk $SWIFT_SDK_ID js
+```
+
+This command:
+1. Processes your TypeScript definition files
+2. Generates corresponding Swift bindings
+3. Compiles your Swift code to WebAssembly
+4. Produces JavaScript glue code in `.build/plugins/PackageToJS/outputs/`
+
+> Note: For larger projects, you may want to generate the BridgeJS code ahead of time to improve build performance. See  for more information.
+
+### Step 4: Use the Generated Swift Bindings
+
+The BridgeJS plugin automatically generates Swift bindings that match your TypeScript definitions. You can now use these APIs directly in your Swift code:
+
+```swift
+import JavaScriptKit
+
+@JS func run() {
+    // Simple function call
+    consoleLog("Hello from Swift!")
+
+    // Get `document`
+    let document = getDocument()
+
+    // Property access
+    document.title = "My Swift App"
+
+    // Method calls
+    let button = document.createElement("button")
+    button.innerText = "Click Me"
+
+    // TODO: Function types on function signatures are not supported yet.
+    // buttion.addEventListener("click") { _ in
+    //     print("On click!")
+    // }
+
+    // DOM manipulation
+    let container = document.getElementById("app")
+    container.appendChild(button)
+}
+```
+
+### Step 5: Inject JavaScript Implementations
+
+The final step is to provide the actual JavaScript implementations for the TypeScript declarations you defined. You need to create a JavaScript file that initializes your WebAssembly module with the appropriate implementations:
+
+```javascript
+// index.js
+import { init } from "./.build/plugins/PackageToJS/outputs/Package/index.js";
+
+// Initialize the WebAssembly module with JavaScript implementations
+const { exports } = await init({
+    imports: {
+        consoleLog: (message) => {
+            console.log(message);
+        },
+        getDocument: () => document,
+    }
+});
+
+// Call the entry point of your Swift application
+exports.run();
+```
diff --git a/Sources/JavaScriptKit/Documentation.docc/Documentation.md b/Sources/JavaScriptKit/Documentation.docc/Documentation.md
index 94d5ba3c5..ffc168431 100644
--- a/Sources/JavaScriptKit/Documentation.docc/Documentation.md
+++ b/Sources/JavaScriptKit/Documentation.docc/Documentation.md
@@ -49,8 +49,16 @@ Check out the [examples](https://github.com/swiftwasm/JavaScriptKit/tree/main/Ex
 
 - 
 
-### Core Types
+### Articles
 
-- 
-- 
-- 
+- 
+- 
+- 
+- 
+- 
+
+### Core APIs
+
+- ``JSValue``
+- ``JSObject``
+- ``JS()``
diff --git a/Sources/JavaScriptKit/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/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/prelude.mjs b/Tests/prelude.mjs
index ab5723587..1e12d3755 100644
--- a/Tests/prelude.mjs
+++ b/Tests/prelude.mjs
@@ -4,15 +4,71 @@ export function setupOptions(options, context) {
     setupTestGlobals(globalThis);
     return {
         ...options,
-        addToCoreImports(importObject) {
+        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: {
diff --git a/Utilities/format.swift b/Utilities/format.swift
index be6e70858..9df282ad7 100755
--- a/Utilities/format.swift
+++ b/Utilities/format.swift
@@ -63,6 +63,7 @@ let excluded: Set = [
     ".index-build",
     "node_modules",
     "__Snapshots__",
+    "Generated",
     // Exclude the script itself to avoid changing its file mode.
     URL(fileURLWithPath: #filePath).lastPathComponent,
 ]

From 7309d97d63f87a9dce2e8d62aa5b4ae5a71eda3f Mon Sep 17 00:00:00 2001
From: Yuta Saito 
Date: Wed, 2 Apr 2025 12:10:33 +0000
Subject: [PATCH 359/373] [skip ci] Mention `@dynamicMemberLookup`-based APIs

It's still up to the user to decide which approach to use.
---
 .../Articles/Importing-TypeScript-into-Swift.md               | 4 +++-
 1 file changed, 3 insertions(+), 1 deletion(-)

diff --git a/Sources/JavaScriptKit/Documentation.docc/Articles/Importing-TypeScript-into-Swift.md b/Sources/JavaScriptKit/Documentation.docc/Articles/Importing-TypeScript-into-Swift.md
index e61664960..5f9bb4a12 100644
--- a/Sources/JavaScriptKit/Documentation.docc/Articles/Importing-TypeScript-into-Swift.md
+++ b/Sources/JavaScriptKit/Documentation.docc/Articles/Importing-TypeScript-into-Swift.md
@@ -8,12 +8,14 @@ Learn how to leverage TypeScript definitions to create type-safe bindings for Ja
 
 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 include:
+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

From 5c596cb6c0b0e5ab73e192b4888a3e8492fe1677 Mon Sep 17 00:00:00 2001
From: Yuta Saito 
Date: Thu, 3 Apr 2025 09:55:48 +0000
Subject: [PATCH 360/373] Add snapshot tests for JS glue for importing TS

---
 .../BridgeJSSkeleton/BridgeJSSkeleton.swift   |  2 +-
 .../Sources/BridgeJSTool/BridgeJSTool.swift   |  3 +-
 .../Sources/BridgeJSTool/ImportTS.swift       | 12 ++--
 .../BridgeJSToolTests/BridgeJSLinkTests.swift | 43 ++++++++----
 .../ArrayParameter.Import.d.ts                | 20 ++++++
 .../ArrayParameter.Import.js                  | 62 ++++++++++++++++++
 .../BridgeJSLinkTests/Interface.Import.d.ts   | 18 +++++
 .../BridgeJSLinkTests/Interface.Import.js     | 65 +++++++++++++++++++
 ...s.d.ts => PrimitiveParameters.Export.d.ts} |  0
 ...eters.js => PrimitiveParameters.Export.js} |  0
 .../PrimitiveParameters.Import.d.ts           | 18 +++++
 .../PrimitiveParameters.Import.js             | 56 ++++++++++++++++
 ...eturn.d.ts => PrimitiveReturn.Export.d.ts} |  0
 ...iveReturn.js => PrimitiveReturn.Export.js} |  0
 .../PrimitiveReturn.Import.d.ts               | 19 ++++++
 .../PrimitiveReturn.Import.js                 | 61 +++++++++++++++++
 ...meter.d.ts => StringParameter.Export.d.ts} |  0
 ...Parameter.js => StringParameter.Export.js} |  0
 .../StringParameter.Import.d.ts               | 19 ++++++
 .../StringParameter.Import.js                 | 63 ++++++++++++++++++
 ...ngReturn.d.ts => StringReturn.Export.d.ts} |  0
 ...StringReturn.js => StringReturn.Export.js} |  0
 .../StringReturn.Import.d.ts                  | 18 +++++
 .../BridgeJSLinkTests/StringReturn.Import.js  | 58 +++++++++++++++++
 ...SwiftClass.d.ts => SwiftClass.Export.d.ts} |  0
 .../{SwiftClass.js => SwiftClass.Export.js}   |  0
 .../BridgeJSLinkTests/TypeAlias.Import.d.ts   | 18 +++++
 .../BridgeJSLinkTests/TypeAlias.Import.js     | 56 ++++++++++++++++
 .../TypeScriptClass.Import.d.ts               | 17 +++++
 .../TypeScriptClass.Import.js                 | 63 ++++++++++++++++++
 ...ts => VoidParameterVoidReturn.Export.d.ts} |  0
 ...n.js => VoidParameterVoidReturn.Export.js} |  0
 .../VoidParameterVoidReturn.Import.d.ts       | 18 +++++
 .../VoidParameterVoidReturn.Import.js         | 56 ++++++++++++++++
 34 files changed, 745 insertions(+), 20 deletions(-)
 create mode 100644 Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/ArrayParameter.Import.d.ts
 create mode 100644 Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/ArrayParameter.Import.js
 create mode 100644 Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/Interface.Import.d.ts
 create mode 100644 Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/Interface.Import.js
 rename Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/{PrimitiveParameters.d.ts => PrimitiveParameters.Export.d.ts} (100%)
 rename Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/{PrimitiveParameters.js => PrimitiveParameters.Export.js} (100%)
 create mode 100644 Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/PrimitiveParameters.Import.d.ts
 create mode 100644 Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/PrimitiveParameters.Import.js
 rename Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/{PrimitiveReturn.d.ts => PrimitiveReturn.Export.d.ts} (100%)
 rename Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/{PrimitiveReturn.js => PrimitiveReturn.Export.js} (100%)
 create mode 100644 Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/PrimitiveReturn.Import.d.ts
 create mode 100644 Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/PrimitiveReturn.Import.js
 rename Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/{StringParameter.d.ts => StringParameter.Export.d.ts} (100%)
 rename Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/{StringParameter.js => StringParameter.Export.js} (100%)
 create mode 100644 Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/StringParameter.Import.d.ts
 create mode 100644 Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/StringParameter.Import.js
 rename Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/{StringReturn.d.ts => StringReturn.Export.d.ts} (100%)
 rename Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/{StringReturn.js => StringReturn.Export.js} (100%)
 create mode 100644 Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/StringReturn.Import.d.ts
 create mode 100644 Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/StringReturn.Import.js
 rename Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/{SwiftClass.d.ts => SwiftClass.Export.d.ts} (100%)
 rename Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/{SwiftClass.js => SwiftClass.Export.js} (100%)
 create mode 100644 Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/TypeAlias.Import.d.ts
 create mode 100644 Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/TypeAlias.Import.js
 create mode 100644 Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/TypeScriptClass.Import.d.ts
 create mode 100644 Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/TypeScriptClass.Import.js
 rename Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/{VoidParameterVoidReturn.d.ts => VoidParameterVoidReturn.Export.d.ts} (100%)
 rename Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/{VoidParameterVoidReturn.js => VoidParameterVoidReturn.Export.js} (100%)
 create mode 100644 Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/VoidParameterVoidReturn.Import.d.ts
 create mode 100644 Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/VoidParameterVoidReturn.Import.js

diff --git a/Plugins/BridgeJS/Sources/BridgeJSSkeleton/BridgeJSSkeleton.swift b/Plugins/BridgeJS/Sources/BridgeJSSkeleton/BridgeJSSkeleton.swift
index 0405f2393..34492682f 100644
--- a/Plugins/BridgeJS/Sources/BridgeJSSkeleton/BridgeJSSkeleton.swift
+++ b/Plugins/BridgeJS/Sources/BridgeJSSkeleton/BridgeJSSkeleton.swift
@@ -92,5 +92,5 @@ struct ImportedFileSkeleton: Codable {
 
 struct ImportedModuleSkeleton: Codable {
     let moduleName: String
-    let children: [ImportedFileSkeleton]
+    var children: [ImportedFileSkeleton]
 }
diff --git a/Plugins/BridgeJS/Sources/BridgeJSTool/BridgeJSTool.swift b/Plugins/BridgeJS/Sources/BridgeJSTool/BridgeJSTool.swift
index c8ff8df67..a6bd5ff52 100644
--- a/Plugins/BridgeJS/Sources/BridgeJSTool/BridgeJSTool.swift
+++ b/Plugins/BridgeJS/Sources/BridgeJSTool/BridgeJSTool.swift
@@ -115,7 +115,6 @@ import SwiftParser
             )
             try (outputSwift ?? "").write(to: outputSwiftURL, atomically: true, encoding: .utf8)
 
-            let outputSkeletons = ImportedModuleSkeleton(moduleName: importer.moduleName, children: importer.skeletons)
             let outputSkeletonsURL = URL(fileURLWithPath: doubleDashOptions["output-skeleton"]!)
             try FileManager.default.createDirectory(
                 at: outputSkeletonsURL.deletingLastPathComponent(),
@@ -124,7 +123,7 @@ import SwiftParser
             )
             let encoder = JSONEncoder()
             encoder.outputFormatting = [.prettyPrinted, .sortedKeys]
-            try encoder.encode(outputSkeletons).write(to: outputSkeletonsURL)
+            try encoder.encode(importer.skeleton).write(to: outputSkeletonsURL)
 
             progress.print(
                 """
diff --git a/Plugins/BridgeJS/Sources/BridgeJSTool/ImportTS.swift b/Plugins/BridgeJS/Sources/BridgeJSTool/ImportTS.swift
index c6e4729ea..a97550bd1 100644
--- a/Plugins/BridgeJS/Sources/BridgeJSTool/ImportTS.swift
+++ b/Plugins/BridgeJS/Sources/BridgeJSTool/ImportTS.swift
@@ -13,17 +13,19 @@ import Foundation
 /// JavaScript glue code and TypeScript definitions.
 struct ImportTS {
     let progress: ProgressReporting
-    let moduleName: String
-    private(set) var skeletons: [ImportedFileSkeleton] = []
+    private(set) var skeleton: ImportedModuleSkeleton
+    private var moduleName: String {
+        skeleton.moduleName
+    }
 
     init(progress: ProgressReporting, moduleName: String) {
         self.progress = progress
-        self.moduleName = moduleName
+        self.skeleton = ImportedModuleSkeleton(moduleName: moduleName, children: [])
     }
 
     /// Adds a skeleton to the importer's state
     mutating func addSkeleton(_ skeleton: ImportedFileSkeleton) {
-        self.skeletons.append(skeleton)
+        self.skeleton.children.append(skeleton)
     }
 
     /// Processes a TypeScript definition file and extracts its API information
@@ -69,7 +71,7 @@ struct ImportTS {
     /// Finalizes the import process and generates Swift code
     func finalize() throws -> String? {
         var decls: [DeclSyntax] = []
-        for skeleton in self.skeletons {
+        for skeleton in self.skeleton.children {
             for function in skeleton.functions {
                 let thunkDecls = try renderSwiftThunk(function, topLevelDecls: &decls)
                 decls.append(contentsOf: thunkDecls)
diff --git a/Plugins/BridgeJS/Tests/BridgeJSToolTests/BridgeJSLinkTests.swift b/Plugins/BridgeJS/Tests/BridgeJSToolTests/BridgeJSLinkTests.swift
index 5edb1b367..e052ed427 100644
--- a/Plugins/BridgeJS/Tests/BridgeJSToolTests/BridgeJSLinkTests.swift
+++ b/Plugins/BridgeJS/Tests/BridgeJSToolTests/BridgeJSLinkTests.swift
@@ -8,18 +8,12 @@ import Testing
 
 @Suite struct BridgeJSLinkTests {
     private func snapshot(
-        swiftAPI: ExportSwift,
+        bridgeJSLink: BridgeJSLink,
         name: String? = nil,
         filePath: String = #filePath,
         function: String = #function,
         sourceLocation: Testing.SourceLocation = #_sourceLocation
     ) throws {
-        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)
         let (outputJs, outputDts) = try bridgeJSLink.link()
         try assertSnapshot(
             name: name,
@@ -43,19 +37,44 @@ import Testing
         "Inputs"
     )
 
-    static func collectInputs() -> [String] {
+    static func collectInputs(extension: String) -> [String] {
         let fileManager = FileManager.default
         let inputs = try! fileManager.contentsOfDirectory(atPath: Self.inputsDirectory.path)
-        return inputs.filter { $0.hasSuffix(".swift") }
+        return inputs.filter { $0.hasSuffix(`extension`) }
     }
 
-    @Test(arguments: collectInputs())
-    func snapshot(input: String) throws {
+    @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
-        try snapshot(swiftAPI: swiftAPI, name: name)
+
+        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/__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.d.ts b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/PrimitiveParameters.Export.d.ts
similarity index 100%
rename from Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/PrimitiveParameters.d.ts
rename to Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/PrimitiveParameters.Export.d.ts
diff --git a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/PrimitiveParameters.js b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/PrimitiveParameters.Export.js
similarity index 100%
rename from Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/PrimitiveParameters.js
rename to Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/PrimitiveParameters.Export.js
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.d.ts b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/PrimitiveReturn.Export.d.ts
similarity index 100%
rename from Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/PrimitiveReturn.d.ts
rename to Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/PrimitiveReturn.Export.d.ts
diff --git a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/PrimitiveReturn.js b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/PrimitiveReturn.Export.js
similarity index 100%
rename from Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/PrimitiveReturn.js
rename to Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/PrimitiveReturn.Export.js
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.d.ts b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/StringParameter.Export.d.ts
similarity index 100%
rename from Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/StringParameter.d.ts
rename to Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/StringParameter.Export.d.ts
diff --git a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/StringParameter.js b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/StringParameter.Export.js
similarity index 100%
rename from Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/StringParameter.js
rename to Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/StringParameter.Export.js
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.d.ts b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/StringReturn.Export.d.ts
similarity index 100%
rename from Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/StringReturn.d.ts
rename to Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/StringReturn.Export.d.ts
diff --git a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/StringReturn.js b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/StringReturn.Export.js
similarity index 100%
rename from Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/StringReturn.js
rename to Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/StringReturn.Export.js
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.d.ts b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/SwiftClass.Export.d.ts
similarity index 100%
rename from Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/SwiftClass.d.ts
rename to Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/SwiftClass.Export.d.ts
diff --git a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/SwiftClass.js b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/SwiftClass.Export.js
similarity index 100%
rename from Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/SwiftClass.js
rename to Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/SwiftClass.Export.js
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.d.ts b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/VoidParameterVoidReturn.Export.d.ts
similarity index 100%
rename from Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/VoidParameterVoidReturn.d.ts
rename to Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/VoidParameterVoidReturn.Export.d.ts
diff --git a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/VoidParameterVoidReturn.js b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/VoidParameterVoidReturn.Export.js
similarity index 100%
rename from Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/VoidParameterVoidReturn.js
rename to Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/VoidParameterVoidReturn.Export.js
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

From 3123dcb2e72550adb1c5550a1e917d299e5f4622 Mon Sep 17 00:00:00 2001
From: Yuta Saito 
Date: Sun, 6 Apr 2025 14:05:24 +0000
Subject: [PATCH 361/373] Add CI matrix for Swift 6.1

---
 .github/workflows/test.yml | 5 +++++
 1 file changed, 5 insertions(+)

diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml
index 35405eaf6..a7dfcd578 100644
--- a/.github/workflows/test.yml
+++ b/.github/workflows/test.yml
@@ -14,6 +14,11 @@ jobs:
               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

From 71e16e7dde395f24154f1e698fa8d245fefafc6a Mon Sep 17 00:00:00 2001
From: Yuta Saito 
Date: Mon, 7 Apr 2025 07:20:56 +0000
Subject: [PATCH 362/373] Throw error if the worker thread creation fails

use-sites still can fallback to other task executors, so it should be a
recoverable error rather than a fatal error.
---
 .../WebWorkerTaskExecutor.swift               | 33 ++++++++++++++++---
 1 file changed, 29 insertions(+), 4 deletions(-)

diff --git a/Sources/JavaScriptEventLoop/WebWorkerTaskExecutor.swift b/Sources/JavaScriptEventLoop/WebWorkerTaskExecutor.swift
index f47cb1b9c..a1962eb77 100644
--- a/Sources/JavaScriptEventLoop/WebWorkerTaskExecutor.swift
+++ b/Sources/JavaScriptEventLoop/WebWorkerTaskExecutor.swift
@@ -110,6 +110,16 @@ import WASILibc
 @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
@@ -348,20 +358,30 @@ public final class WebWorkerTaskExecutor: TaskExecutor {
                 }
             }
             trace("Executor.start")
+
+            // Hold over-retained contexts until all worker threads are started.
+            var contexts: [Unmanaged] = []
+            defer {
+                for context in contexts {
+                    context.release()
+                }
+            }
             // Start worker threads via pthread_create.
             for worker in workers {
                 // NOTE: The context must be allocated on the heap because
                 // `pthread_create` on WASI does not guarantee the thread is started
                 // immediately. The context must be retained until the thread is started.
                 let context = Context(executor: self, worker: worker)
-                let ptr = Unmanaged.passRetained(context).toOpaque()
+                let 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!).takeRetainedValue()
+                        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.
@@ -370,7 +390,10 @@ public final class WebWorkerTaskExecutor: TaskExecutor {
                     },
                     ptr
                 )
-                precondition(ret == 0, "Failed to create a thread")
+                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.
@@ -380,7 +403,9 @@ public final class WebWorkerTaskExecutor: TaskExecutor {
                 var tid: pid_t
                 repeat {
                     if workerInitStarted.duration(to: .now) > timeout {
-                        fatalError("Worker thread initialization timeout exceeded (\(timeout))")
+                        throw SpawnError(
+                            reason: "Worker thread initialization timeout exceeded (\(timeout))"
+                        )
                     }
                     tid = worker.tid.load(ordering: .sequentiallyConsistent)
                     try await clock.sleep(for: checkInterval)

From 0575dd1ccde777655a90d0202be34dd6f566b362 Mon Sep 17 00:00:00 2001
From: Yuta Saito 
Date: Tue, 8 Apr 2025 10:17:17 +0000
Subject: [PATCH 363/373] [BridgeJS] Hide it behind an experimental feature
 flag

---
 Plugins/BridgeJS/README.md                                   | 3 +++
 .../BridgeJSCommandPlugin/BridgeJSCommandPlugin.swift        | 2 +-
 Plugins/PackageToJS/Sources/PackageToJS.swift                | 5 +++++
 3 files changed, 9 insertions(+), 1 deletion(-)

diff --git a/Plugins/BridgeJS/README.md b/Plugins/BridgeJS/README.md
index a62072539..7ed16ed8b 100644
--- a/Plugins/BridgeJS/README.md
+++ b/Plugins/BridgeJS/README.md
@@ -1,5 +1,8 @@
 # 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
diff --git a/Plugins/BridgeJS/Sources/BridgeJSCommandPlugin/BridgeJSCommandPlugin.swift b/Plugins/BridgeJS/Sources/BridgeJSCommandPlugin/BridgeJSCommandPlugin.swift
index 9ea500b8c..286b052d5 100644
--- a/Plugins/BridgeJS/Sources/BridgeJSCommandPlugin/BridgeJSCommandPlugin.swift
+++ b/Plugins/BridgeJS/Sources/BridgeJSCommandPlugin/BridgeJSCommandPlugin.swift
@@ -27,7 +27,7 @@ struct BridgeJSCommandPlugin: CommandPlugin {
                 Generated code will be placed in the target's 'Generated' directory.
 
                 OPTIONS:
-                    --target  Specify target(s) to generate bridge code for. If omitted, 
+                    --target  Specify target(s) to generate bridge code for. If omitted,
                                       generates for all targets with JavaScriptKit dependency.
                 """
         }
diff --git a/Plugins/PackageToJS/Sources/PackageToJS.swift b/Plugins/PackageToJS/Sources/PackageToJS.swift
index 89db66551..2b8b4458a 100644
--- a/Plugins/PackageToJS/Sources/PackageToJS.swift
+++ b/Plugins/PackageToJS/Sources/PackageToJS.swift
@@ -564,6 +564,11 @@ struct PackagingPlanner {
         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(

From 0ff3ebf52057a2b24f58c8a473a17d4c76326ae0 Mon Sep 17 00:00:00 2001
From: Yuta Saito 
Date: Tue, 8 Apr 2025 19:21:24 +0900
Subject: [PATCH 364/373] Update README.md

---
 Plugins/BridgeJS/README.md | 7 ++++---
 1 file changed, 4 insertions(+), 3 deletions(-)

diff --git a/Plugins/BridgeJS/README.md b/Plugins/BridgeJS/README.md
index 7ed16ed8b..9cbd04011 100644
--- a/Plugins/BridgeJS/README.md
+++ b/Plugins/BridgeJS/README.md
@@ -1,9 +1,10 @@
 # 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.
+> [!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.
+> [!NOTE]
+> This documentation is intended for JavaScriptKit developers, not JavaScriptKit users.
 
 ## Overview
 

From 9752c5ad82de4ce0e3d47db2784d24f97cc90ad7 Mon Sep 17 00:00:00 2001
From: Yuta Saito 
Date: Tue, 8 Apr 2025 10:24:23 +0000
Subject: [PATCH 365/373] Explicitly enable
 `JAVASCRIPTKIT_EXPERIMENTAL_BRIDGEJS` for unittest

---
 Makefile | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/Makefile b/Makefile
index 93d7400e1..761010bd9 100644
--- a/Makefile
+++ b/Makefile
@@ -16,7 +16,7 @@ build:
 .PHONY: unittest
 unittest:
 	@echo Running unit tests
-	swift package --swift-sdk "$(SWIFT_SDK_ID)" \
+	env JAVASCRIPTKIT_EXPERIMENTAL_BRIDGEJS=1 swift package --swift-sdk "$(SWIFT_SDK_ID)" \
 	    --disable-sandbox \
 	    -Xlinker --stack-first \
 	    -Xlinker --global-base=524288 \

From c3ec45657ebf9e8393d0deadf583912f1228bca6 Mon Sep 17 00:00:00 2001
From: Yuta Saito 
Date: Wed, 9 Apr 2025 18:22:19 +0900
Subject: [PATCH 366/373] Export `UnsafeEventLoopYield` error type

Some apps wrap instance.exports and monitor
exceptions thrown during execution of Wasm program
but `UnsafeEventLoopYield` is not something they
want to report, so they need to be able to filter them
out. However, they cannot use `UnsafeEventLoopYield` type
name because some bundlers can rename it.
We should export it to allow them not to depend on the
constructor name
---
 Runtime/src/index.ts | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/Runtime/src/index.ts b/Runtime/src/index.ts
index ee12e5be0..b70bed3aa 100644
--- a/Runtime/src/index.ts
+++ b/Runtime/src/index.ts
@@ -749,4 +749,4 @@ export class SwiftRuntime {
 /// 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 class UnsafeEventLoopYield extends Error {}

From 0229735c268ddaf6b65af0cc30d8d5956444510e Mon Sep 17 00:00:00 2001
From: Yuta Saito 
Date: Thu, 10 Apr 2025 00:57:08 +0900
Subject: [PATCH 367/373] Expose UnsafeEventLoopYield by property

---
 Plugins/PackageToJS/Templates/runtime.d.ts | 9 ++++++---
 Plugins/PackageToJS/Templates/runtime.js   | 1 +
 Plugins/PackageToJS/Templates/runtime.mjs  | 1 +
 Runtime/src/index.ts                       | 4 +++-
 4 files changed, 11 insertions(+), 4 deletions(-)

diff --git a/Plugins/PackageToJS/Templates/runtime.d.ts b/Plugins/PackageToJS/Templates/runtime.d.ts
index 98e1f1cc1..9613004cc 100644
--- a/Plugins/PackageToJS/Templates/runtime.d.ts
+++ b/Plugins/PackageToJS/Templates/runtime.d.ts
@@ -1,3 +1,6 @@
+type ref = number;
+type pointer = number;
+
 declare class Memory {
     readonly rawMemory: WebAssembly.Memory;
     private readonly heap;
@@ -18,9 +21,6 @@ declare class Memory {
     writeFloat64: (ptr: pointer, value: number) => void;
 }
 
-type ref = number;
-type pointer = number;
-
 /**
  * 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
@@ -189,6 +189,7 @@ declare class SwiftRuntime {
     private textEncoder;
     /** The thread ID of the current thread. */
     private tid;
+    UnsafeEventLoopYield: typeof UnsafeEventLoopYield;
     constructor(options?: SwiftRuntimeOptions);
     setInstance(instance: WebAssembly.Instance): void;
     main(): void;
@@ -209,6 +210,8 @@ declare class SwiftRuntime {
     private postMessageToMainThread;
     private postMessageToWorkerThread;
 }
+declare class UnsafeEventLoopYield extends Error {
+}
 
 export { SwiftRuntime };
 export type { SwiftRuntimeOptions, SwiftRuntimeThreadChannel };
diff --git a/Plugins/PackageToJS/Templates/runtime.js b/Plugins/PackageToJS/Templates/runtime.js
index 1e45e9b08..da27a1524 100644
--- a/Plugins/PackageToJS/Templates/runtime.js
+++ b/Plugins/PackageToJS/Templates/runtime.js
@@ -312,6 +312,7 @@
             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;
diff --git a/Plugins/PackageToJS/Templates/runtime.mjs b/Plugins/PackageToJS/Templates/runtime.mjs
index ef1f57e74..71f7f9a30 100644
--- a/Plugins/PackageToJS/Templates/runtime.mjs
+++ b/Plugins/PackageToJS/Templates/runtime.mjs
@@ -306,6 +306,7 @@ class SwiftRuntime {
         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;
diff --git a/Runtime/src/index.ts b/Runtime/src/index.ts
index b70bed3aa..05c2964f4 100644
--- a/Runtime/src/index.ts
+++ b/Runtime/src/index.ts
@@ -38,6 +38,8 @@ export class SwiftRuntime {
     /** The thread ID of the current thread. */
     private tid: number | null;
 
+    UnsafeEventLoopYield = UnsafeEventLoopYield;
+
     constructor(options?: SwiftRuntimeOptions) {
         this._instance = null;
         this._memory = null;
@@ -749,4 +751,4 @@ export class SwiftRuntime {
 /// 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.
-export class UnsafeEventLoopYield extends Error {}
+class UnsafeEventLoopYield extends Error {}

From 4a2728554af66c5f7d7ecddafdd531b691b53cee Mon Sep 17 00:00:00 2001
From: Yuta Saito 
Date: Fri, 11 Apr 2025 05:42:56 +0000
Subject: [PATCH 368/373] PackageToJS: Add WebAssembly namespace option to
 instantiate

---
 Plugins/PackageToJS/Templates/instantiate.d.ts |  5 +++++
 Plugins/PackageToJS/Templates/instantiate.js   | 17 +++++++++--------
 2 files changed, 14 insertions(+), 8 deletions(-)

diff --git a/Plugins/PackageToJS/Templates/instantiate.d.ts b/Plugins/PackageToJS/Templates/instantiate.d.ts
index 6c71d1dae..11837aba8 100644
--- a/Plugins/PackageToJS/Templates/instantiate.d.ts
+++ b/Plugins/PackageToJS/Templates/instantiate.d.ts
@@ -56,6 +56,11 @@ export type ModuleSource = WebAssembly.Module | ArrayBufferView | ArrayBuffer |
  * The options for instantiating a WebAssembly module
  */
 export type InstantiateOptions = {
+    /**
+     * The WebAssembly namespace to use for instantiation.
+     * Defaults to the globalThis.WebAssembly object.
+     */
+    WebAssembly?: typeof globalThis.WebAssembly,
     /**
      * The WebAssembly module to instantiate
      */
diff --git a/Plugins/PackageToJS/Templates/instantiate.js b/Plugins/PackageToJS/Templates/instantiate.js
index 2a41d48c9..08351e67e 100644
--- a/Plugins/PackageToJS/Templates/instantiate.js
+++ b/Plugins/PackageToJS/Templates/instantiate.js
@@ -63,6 +63,7 @@ export async function instantiateForThread(
 async function _instantiate(
     options
 ) {
+    const _WebAssembly = options.WebAssembly || WebAssembly;
     const moduleSource = options.module;
 /* #if IS_WASI */
     const { wasi } = options;
@@ -98,23 +99,23 @@ async function _instantiate(
     let module;
     let instance;
     let exports;
-    if (moduleSource instanceof WebAssembly.Module) {
+    if (moduleSource instanceof _WebAssembly.Module) {
         module = moduleSource;
-        instance = await WebAssembly.instantiate(module, importObject);
+        instance = await _WebAssembly.instantiate(module, importObject);
     } else if (typeof Response === "function" && (moduleSource instanceof Response || moduleSource instanceof Promise)) {
-        if (typeof WebAssembly.instantiateStreaming === "function") {
-            const result = await WebAssembly.instantiateStreaming(moduleSource, importObject);
+        if (typeof _WebAssembly.instantiateStreaming === "function") {
+            const result = await _WebAssembly.instantiateStreaming(moduleSource, importObject);
             module = result.module;
             instance = result.instance;
         } else {
             const moduleBytes = await (await moduleSource).arrayBuffer();
-            module = await WebAssembly.compile(moduleBytes);
-            instance = await WebAssembly.instantiate(module, importObject);
+            module = await _WebAssembly.compile(moduleBytes);
+            instance = await _WebAssembly.instantiate(module, importObject);
         }
     } else {
         // @ts-expect-error: Type 'Response' is not assignable to type 'BufferSource'
-        module = await WebAssembly.compile(moduleSource);
-        instance = await WebAssembly.instantiate(module, importObject);
+        module = await _WebAssembly.compile(moduleSource);
+        instance = await _WebAssembly.instantiate(module, importObject);
     }
 
     swift.setInstance(instance);

From 539fd441533a2e129d9f791d18ff88340d530907 Mon Sep 17 00:00:00 2001
From: Yuta Saito 
Date: Sat, 12 Apr 2025 01:51:42 +0000
Subject: [PATCH 369/373] Build benchmarks with PackageToJS

---
 .github/workflows/perf.yml                    |  21 -
 Benchmarks/Package.swift                      |  20 +
 Benchmarks/README.md                          |  30 +
 Benchmarks/Sources/Benchmarks.swift           |  78 ++
 .../Sources/Generated/ExportSwift.swift       |  15 +
 Benchmarks/Sources/Generated/ImportTS.swift   |  38 +
 .../Generated/JavaScript/ExportSwift.json     |  19 +
 .../Generated/JavaScript/ImportTS.json        |  67 ++
 Benchmarks/Sources/bridge.d.ts                |   3 +
 Benchmarks/package.json                       |   1 +
 Benchmarks/run.js                             | 449 +++++++++
 IntegrationTests/Makefile                     |  36 -
 IntegrationTests/TestSuites/.gitignore        |   5 -
 IntegrationTests/TestSuites/Package.swift     |  24 -
 .../Sources/BenchmarkTests/Benchmark.swift    |  19 -
 .../Sources/BenchmarkTests/main.swift         |  85 --
 .../TestSuites/Sources/CHelpers/helpers.c     |   4 -
 .../Sources/CHelpers/include/helpers.h        |  10 -
 .../Sources/CHelpers/include/module.modulemap |   4 -
 IntegrationTests/bin/benchmark-tests.js       |  70 --
 IntegrationTests/lib.js                       |  86 --
 IntegrationTests/package-lock.json            |  86 --
 IntegrationTests/package.json                 |   8 -
 Makefile                                      |  17 -
 ci/perf-tester/package-lock.json              | 924 ------------------
 ci/perf-tester/package.json                   |   9 -
 ci/perf-tester/src/index.js                   | 212 ----
 ci/perf-tester/src/utils.js                   | 221 -----
 28 files changed, 720 insertions(+), 1841 deletions(-)
 delete mode 100644 .github/workflows/perf.yml
 create mode 100644 Benchmarks/Package.swift
 create mode 100644 Benchmarks/README.md
 create mode 100644 Benchmarks/Sources/Benchmarks.swift
 create mode 100644 Benchmarks/Sources/Generated/ExportSwift.swift
 create mode 100644 Benchmarks/Sources/Generated/ImportTS.swift
 create mode 100644 Benchmarks/Sources/Generated/JavaScript/ExportSwift.json
 create mode 100644 Benchmarks/Sources/Generated/JavaScript/ImportTS.json
 create mode 100644 Benchmarks/Sources/bridge.d.ts
 create mode 100644 Benchmarks/package.json
 create mode 100644 Benchmarks/run.js
 delete mode 100644 IntegrationTests/Makefile
 delete mode 100644 IntegrationTests/TestSuites/.gitignore
 delete mode 100644 IntegrationTests/TestSuites/Package.swift
 delete mode 100644 IntegrationTests/TestSuites/Sources/BenchmarkTests/Benchmark.swift
 delete mode 100644 IntegrationTests/TestSuites/Sources/BenchmarkTests/main.swift
 delete mode 100644 IntegrationTests/TestSuites/Sources/CHelpers/helpers.c
 delete mode 100644 IntegrationTests/TestSuites/Sources/CHelpers/include/helpers.h
 delete mode 100644 IntegrationTests/TestSuites/Sources/CHelpers/include/module.modulemap
 delete mode 100644 IntegrationTests/bin/benchmark-tests.js
 delete mode 100644 IntegrationTests/lib.js
 delete mode 100644 IntegrationTests/package-lock.json
 delete mode 100644 IntegrationTests/package.json
 delete mode 100644 ci/perf-tester/package-lock.json
 delete mode 100644 ci/perf-tester/package.json
 delete mode 100644 ci/perf-tester/src/index.js
 delete mode 100644 ci/perf-tester/src/utils.js

diff --git a/.github/workflows/perf.yml b/.github/workflows/perf.yml
deleted file mode 100644
index 501b16099..000000000
--- a/.github/workflows/perf.yml
+++ /dev/null
@@ -1,21 +0,0 @@
-name: Performance
-
-on: [pull_request]
-
-jobs:
-  perf:
-    runs-on: ubuntu-24.04
-    steps:
-      - name: Checkout
-        uses: actions/checkout@v4
-      - uses: ./.github/actions/install-swift
-        with:
-          download-url: https://download.swift.org/swift-6.0.3-release/ubuntu2404/swift-6.0.3-RELEASE/swift-6.0.3-RELEASE-ubuntu24.04.tar.gz
-      - uses: swiftwasm/setup-swiftwasm@v2
-      - name: Run Benchmark
-        run: |
-          make bootstrap
-          make perf-tester
-          node ci/perf-tester
-        env:
-          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
diff --git a/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/IntegrationTests/Makefile b/IntegrationTests/Makefile
deleted file mode 100644
index 54a656fd1..000000000
--- a/IntegrationTests/Makefile
+++ /dev/null
@@ -1,36 +0,0 @@
-CONFIGURATION ?= debug
-SWIFT_BUILD_FLAGS ?=
-NODEJS_FLAGS ?=
-
-NODEJS = node --experimental-wasi-unstable-preview1 $(NODEJS_FLAGS)
-
-FORCE:
-TestSuites/.build/$(CONFIGURATION)/%.wasm: FORCE
-	swift build --package-path TestSuites \
-	            --product $(basename $(notdir $@)) \
-	            --configuration $(CONFIGURATION) \
-	            -Xswiftc -Xclang-linker -Xswiftc -mexec-model=reactor \
-	            -Xlinker --export-if-defined=main -Xlinker --export-if-defined=__main_argc_argv \
-		    --static-swift-stdlib -Xswiftc -static-stdlib \
-		    $(SWIFT_BUILD_FLAGS)
-
-dist/%.wasm: TestSuites/.build/$(CONFIGURATION)/%.wasm
-	mkdir -p dist
-	cp $< $@
-
-node_modules: package-lock.json
-	npm ci
-
-.PHONY: build_rt
-build_rt: node_modules
-	cd .. && npm run build
-
-.PHONY: benchmark_setup
-benchmark_setup: build_rt dist/BenchmarkTests.wasm
-
-.PHONY: run_benchmark
-run_benchmark:
-	$(NODEJS) bin/benchmark-tests.js
-
-.PHONY: benchmark
-benchmark: benchmark_setup run_benchmark
diff --git a/IntegrationTests/TestSuites/.gitignore b/IntegrationTests/TestSuites/.gitignore
deleted file mode 100644
index 95c432091..000000000
--- a/IntegrationTests/TestSuites/.gitignore
+++ /dev/null
@@ -1,5 +0,0 @@
-.DS_Store
-/.build
-/Packages
-/*.xcodeproj
-xcuserdata/
diff --git a/IntegrationTests/TestSuites/Package.swift b/IntegrationTests/TestSuites/Package.swift
deleted file mode 100644
index 1ae22dfa5..000000000
--- a/IntegrationTests/TestSuites/Package.swift
+++ /dev/null
@@ -1,24 +0,0 @@
-// swift-tools-version:5.7
-
-import PackageDescription
-
-let package = Package(
-    name: "TestSuites",
-    platforms: [
-        // This package doesn't work on macOS host, but should be able to be built for it
-        // for developing on Xcode. This minimum version requirement is to prevent availability
-        // errors for Concurrency API, whose runtime support is shipped from macOS 12.0
-        .macOS("12.0")
-    ],
-    products: [
-        .executable(
-            name: "BenchmarkTests",
-            targets: ["BenchmarkTests"]
-        )
-    ],
-    dependencies: [.package(name: "JavaScriptKit", path: "../../")],
-    targets: [
-        .target(name: "CHelpers"),
-        .executableTarget(name: "BenchmarkTests", dependencies: ["JavaScriptKit", "CHelpers"]),
-    ]
-)
diff --git a/IntegrationTests/TestSuites/Sources/BenchmarkTests/Benchmark.swift b/IntegrationTests/TestSuites/Sources/BenchmarkTests/Benchmark.swift
deleted file mode 100644
index 4562898fb..000000000
--- a/IntegrationTests/TestSuites/Sources/BenchmarkTests/Benchmark.swift
+++ /dev/null
@@ -1,19 +0,0 @@
-import JavaScriptKit
-
-class Benchmark {
-    init(_ title: String) {
-        self.title = title
-    }
-
-    let title: String
-    let runner = JSObject.global.benchmarkRunner.function!
-
-    func testSuite(_ name: String, _ body: @escaping (Int) -> Void) {
-        let jsBody = JSClosure { arguments -> JSValue in
-            let iteration = Int(arguments[0].number!)
-            body(iteration)
-            return .undefined
-        }
-        runner("\(title)/\(name)", jsBody)
-    }
-}
diff --git a/IntegrationTests/TestSuites/Sources/BenchmarkTests/main.swift b/IntegrationTests/TestSuites/Sources/BenchmarkTests/main.swift
deleted file mode 100644
index 6bd10835b..000000000
--- a/IntegrationTests/TestSuites/Sources/BenchmarkTests/main.swift
+++ /dev/null
@@ -1,85 +0,0 @@
-import CHelpers
-import JavaScriptKit
-
-let serialization = Benchmark("Serialization")
-
-let noopFunction = JSObject.global.noopFunction.function!
-
-serialization.testSuite("JavaScript function call through Wasm import") { n in
-    for _ in 0.. {
-            body(iteration);
-        });
-    }
-}
-
-const serialization = new JSBenchmark("Serialization");
-serialization.testSuite("Call JavaScript function directly", (n) => {
-    for (let idx = 0; idx < n; idx++) {
-        global.noopFunction()
-    }
-});
-
-serialization.testSuite("Assign JavaScript number directly", (n) => {
-    const jsNumber = 42;
-    const object = global;
-    const key = "numberValue"
-    for (let idx = 0; idx < n; idx++) {
-        object[key] = jsNumber;
-    }
-});
-
-serialization.testSuite("Call with JavaScript number directly", (n) => {
-    const jsNumber = 42;
-    for (let idx = 0; idx < n; idx++) {
-        global.noopFunction(jsNumber)
-    }
-});
-
-serialization.testSuite("Write JavaScript string directly", (n) => {
-    const jsString = "Hello, world";
-    const object = global;
-    const key = "stringValue"
-    for (let idx = 0; idx < n; idx++) {
-        object[key] = jsString;
-    }
-});
-
-serialization.testSuite("Call with JavaScript string directly", (n) => {
-    const jsString = "Hello, world";
-    for (let idx = 0; idx < n; idx++) {
-        global.noopFunction(jsString)
-    }
-});
-
-startWasiTask("./dist/BenchmarkTests.wasm").catch((err) => {
-    console.log(err);
-});
diff --git a/IntegrationTests/lib.js b/IntegrationTests/lib.js
deleted file mode 100644
index d9c424f0e..000000000
--- a/IntegrationTests/lib.js
+++ /dev/null
@@ -1,86 +0,0 @@
-import { SwiftRuntime } from "javascript-kit-swift"
-import { WASI as NodeWASI } from "wasi"
-import { WASI as MicroWASI, useAll } from "uwasi"
-import * as fs from "fs/promises"
-import path from "path";
-
-const WASI = {
-    MicroWASI: ({ args }) => {
-        const wasi = new MicroWASI({
-            args: args,
-            env: {},
-            features: [useAll()],
-        })
-
-        return {
-            wasiImport: wasi.wasiImport,
-            setInstance(instance) {
-                wasi.instance = instance;
-            },
-            start(instance, swift) {
-                wasi.initialize(instance);
-                swift.main();
-            }
-        }
-    },
-    Node: ({ args }) => {
-        const wasi = new NodeWASI({
-            args: args,
-            env: {},
-            preopens: {
-              "/": "./",
-            },
-            returnOnExit: false,
-            version: "preview1",
-        })
-
-        return {
-            wasiImport: wasi.wasiImport,
-            start(instance, swift) {
-                wasi.initialize(instance);
-                swift.main();
-            }
-        }
-    },
-};
-
-const selectWASIBackend = () => {
-    const value = process.env["JAVASCRIPTKIT_WASI_BACKEND"]
-    if (value) {
-        return value;
-    }
-    return "Node"
-};
-
-function constructBaseImportObject(wasi, swift) {
-    return {
-        wasi_snapshot_preview1: wasi.wasiImport,
-        javascript_kit: swift.wasmImports,
-        benchmark_helper: {
-            noop: () => {},
-            noop_with_int: (_) => {},
-        },
-    }
-}
-
-export const startWasiTask = async (wasmPath, wasiConstructorKey = selectWASIBackend()) => {
-    // Fetch our Wasm File
-    const wasmBinary = await fs.readFile(wasmPath);
-    const programName = wasmPath;
-    const args = [path.basename(programName)];
-    args.push(...process.argv.slice(3));
-    const wasi = WASI[wasiConstructorKey]({ args });
-
-    const module = await WebAssembly.compile(wasmBinary);
-
-    const swift = new SwiftRuntime();
-
-    const importObject = constructBaseImportObject(wasi, swift);
-
-    // Instantiate the WebAssembly file
-    const instance = await WebAssembly.instantiate(module, importObject);
-
-    swift.setInstance(instance);
-    // Start the WebAssembly WASI instance!
-    wasi.start(instance, swift);
-};
diff --git a/IntegrationTests/package-lock.json b/IntegrationTests/package-lock.json
deleted file mode 100644
index 9ea81b961..000000000
--- a/IntegrationTests/package-lock.json
+++ /dev/null
@@ -1,86 +0,0 @@
-{
-  "name": "IntegrationTests",
-  "lockfileVersion": 2,
-  "requires": true,
-  "packages": {
-    "": {
-      "dependencies": {
-        "javascript-kit-swift": "file:..",
-        "uwasi": "^1.2.0"
-      }
-    },
-    "..": {
-      "name": "javascript-kit-swift",
-      "version": "0.0.0",
-      "license": "MIT",
-      "devDependencies": {
-        "@rollup/plugin-typescript": "^8.3.1",
-        "prettier": "2.6.1",
-        "rollup": "^2.70.0",
-        "tslib": "^2.3.1",
-        "typescript": "^4.6.3"
-      }
-    },
-    "../node_modules/prettier": {
-      "version": "2.1.2",
-      "integrity": "sha512-16c7K+x4qVlJg9rEbXl7HEGmQyZlG4R9AgP+oHKRMsMsuk8s+ATStlf1NpDqyBI1HpVyfjLOeMhH2LvuNvV5Vg==",
-      "dev": true,
-      "bin": {
-        "prettier": "bin-prettier.js"
-      },
-      "engines": {
-        "node": ">=10.13.0"
-      }
-    },
-    "../node_modules/typescript": {
-      "version": "4.4.2",
-      "integrity": "sha512-gzP+t5W4hdy4c+68bfcv0t400HVJMMd2+H9B7gae1nQlBzCqvrXX+6GL/b3GAgyTH966pzrZ70/fRjwAtZksSQ==",
-      "dev": true,
-      "bin": {
-        "tsc": "bin/tsc",
-        "tsserver": "bin/tsserver"
-      },
-      "engines": {
-        "node": ">=4.2.0"
-      }
-    },
-    "node_modules/javascript-kit-swift": {
-      "resolved": "..",
-      "link": true
-    },
-    "node_modules/uwasi": {
-      "version": "1.2.0",
-      "resolved": "https://registry.npmjs.org/uwasi/-/uwasi-1.2.0.tgz",
-      "integrity": "sha512-+U3ajjQgx/Xh1/ZNrgH0EzM5qI2czr94oz3DPDwTvUIlM4SFpDjTqJzDA3xcqlTmpp2YGpxApmjwZfablMUoOg=="
-    }
-  },
-  "dependencies": {
-    "javascript-kit-swift": {
-      "version": "file:..",
-      "requires": {
-        "@rollup/plugin-typescript": "^8.3.1",
-        "prettier": "2.6.1",
-        "rollup": "^2.70.0",
-        "tslib": "^2.3.1",
-        "typescript": "^4.6.3"
-      },
-      "dependencies": {
-        "prettier": {
-          "version": "2.1.2",
-          "integrity": "sha512-16c7K+x4qVlJg9rEbXl7HEGmQyZlG4R9AgP+oHKRMsMsuk8s+ATStlf1NpDqyBI1HpVyfjLOeMhH2LvuNvV5Vg==",
-          "dev": true
-        },
-        "typescript": {
-          "version": "4.4.2",
-          "integrity": "sha512-gzP+t5W4hdy4c+68bfcv0t400HVJMMd2+H9B7gae1nQlBzCqvrXX+6GL/b3GAgyTH966pzrZ70/fRjwAtZksSQ==",
-          "dev": true
-        }
-      }
-    },
-    "uwasi": {
-      "version": "1.2.0",
-      "resolved": "https://registry.npmjs.org/uwasi/-/uwasi-1.2.0.tgz",
-      "integrity": "sha512-+U3ajjQgx/Xh1/ZNrgH0EzM5qI2czr94oz3DPDwTvUIlM4SFpDjTqJzDA3xcqlTmpp2YGpxApmjwZfablMUoOg=="
-    }
-  }
-}
diff --git a/IntegrationTests/package.json b/IntegrationTests/package.json
deleted file mode 100644
index 8491e91fb..000000000
--- a/IntegrationTests/package.json
+++ /dev/null
@@ -1,8 +0,0 @@
-{
-  "private": true,
-  "type": "module",
-  "dependencies": {
-    "uwasi": "^1.2.0",
-    "javascript-kit-swift": "file:.."
-  }
-}
diff --git a/Makefile b/Makefile
index 761010bd9..1524ba1ba 100644
--- a/Makefile
+++ b/Makefile
@@ -8,11 +8,6 @@ bootstrap:
 	npm ci
 	npx playwright install
 
-.PHONY: build
-build:
-	swift build --triple wasm32-unknown-wasi
-	npm run build
-
 .PHONY: unittest
 unittest:
 	@echo Running unit tests
@@ -24,18 +19,6 @@ unittest:
 	    -Xlinker stack-size=524288 \
 	    js test --prelude ./Tests/prelude.mjs
 
-.PHONY: benchmark_setup
-benchmark_setup:
-	SWIFT_BUILD_FLAGS="$(SWIFT_BUILD_FLAGS)" CONFIGURATION=release $(MAKE) -C IntegrationTests benchmark_setup
-
-.PHONY: run_benchmark
-run_benchmark:
-	SWIFT_BUILD_FLAGS="$(SWIFT_BUILD_FLAGS)" CONFIGURATION=release $(MAKE) -s -C IntegrationTests run_benchmark
-
-.PHONY: perf-tester
-perf-tester:
-	cd ci/perf-tester && npm ci
-
 .PHONY: regenerate_swiftpm_resources
 regenerate_swiftpm_resources:
 	npm run build
diff --git a/ci/perf-tester/package-lock.json b/ci/perf-tester/package-lock.json
deleted file mode 100644
index 82918bd59..000000000
--- a/ci/perf-tester/package-lock.json
+++ /dev/null
@@ -1,924 +0,0 @@
-{
-  "name": "perf-tester",
-  "lockfileVersion": 2,
-  "requires": true,
-  "packages": {
-    "": {
-      "devDependencies": {
-        "@actions/core": "^1.9.1",
-        "@actions/exec": "^1.0.3",
-        "@actions/github": "^2.0.1"
-      }
-    },
-    "node_modules/@actions/core": {
-      "version": "1.9.1",
-      "resolved": "https://registry.npmjs.org/@actions/core/-/core-1.9.1.tgz",
-      "integrity": "sha512-5ad+U2YGrmmiw6du20AQW5XuWo7UKN2052FjSV7MX+Wfjf8sCqcsZe62NfgHys4QI4/Y+vQvLKYL8jWtA1ZBTA==",
-      "dev": true,
-      "dependencies": {
-        "@actions/http-client": "^2.0.1",
-        "uuid": "^8.3.2"
-      }
-    },
-    "node_modules/@actions/exec": {
-      "version": "1.0.3",
-      "resolved": "https://registry.npmjs.org/@actions/exec/-/exec-1.0.3.tgz",
-      "integrity": "sha512-TogJGnueOmM7ntCi0ASTUj4LapRRtDfj57Ja4IhPmg2fls28uVOPbAn8N+JifaOumN2UG3oEO/Ixek2A4NcYSA==",
-      "dev": true,
-      "dependencies": {
-        "@actions/io": "^1.0.1"
-      }
-    },
-    "node_modules/@actions/github": {
-      "version": "2.0.1",
-      "resolved": "https://registry.npmjs.org/@actions/github/-/github-2.0.1.tgz",
-      "integrity": "sha512-C7dAsCkpPi1HxTzLldz+oY+9c5G+nnaK7xgk8KA83VVGlrGK7d603E3snUAFocWrqEu/uvdYD82ytggjcpYSQA==",
-      "dev": true,
-      "dependencies": {
-        "@octokit/graphql": "^4.3.1",
-        "@octokit/rest": "^16.15.0"
-      }
-    },
-    "node_modules/@actions/http-client": {
-      "version": "2.0.1",
-      "resolved": "https://registry.npmjs.org/@actions/http-client/-/http-client-2.0.1.tgz",
-      "integrity": "sha512-PIXiMVtz6VvyaRsGY268qvj57hXQEpsYogYOu2nrQhlf+XCGmZstmuZBbAybUl1nQGnvS1k1eEsQ69ZoD7xlSw==",
-      "dev": true,
-      "dependencies": {
-        "tunnel": "^0.0.6"
-      }
-    },
-    "node_modules/@actions/io": {
-      "version": "1.0.2",
-      "resolved": "https://registry.npmjs.org/@actions/io/-/io-1.0.2.tgz",
-      "integrity": "sha512-J8KuFqVPr3p6U8W93DOXlXW6zFvrQAJANdS+vw0YhusLIq+bszW8zmK2Fh1C2kDPX8FMvwIl1OUcFgvJoXLbAg==",
-      "dev": true
-    },
-    "node_modules/@octokit/endpoint": {
-      "version": "5.5.1",
-      "resolved": "https://registry.npmjs.org/@octokit/endpoint/-/endpoint-5.5.1.tgz",
-      "integrity": "sha512-nBFhRUb5YzVTCX/iAK1MgQ4uWo89Gu0TH00qQHoYRCsE12dWcG1OiLd7v2EIo2+tpUKPMOQ62QFy9hy9Vg2ULg==",
-      "dev": true,
-      "dependencies": {
-        "@octokit/types": "^2.0.0",
-        "is-plain-object": "^3.0.0",
-        "universal-user-agent": "^4.0.0"
-      }
-    },
-    "node_modules/@octokit/graphql": {
-      "version": "4.3.1",
-      "resolved": "https://registry.npmjs.org/@octokit/graphql/-/graphql-4.3.1.tgz",
-      "integrity": "sha512-hCdTjfvrK+ilU2keAdqNBWOk+gm1kai1ZcdjRfB30oA3/T6n53UVJb7w0L5cR3/rhU91xT3HSqCd+qbvH06yxA==",
-      "dev": true,
-      "dependencies": {
-        "@octokit/request": "^5.3.0",
-        "@octokit/types": "^2.0.0",
-        "universal-user-agent": "^4.0.0"
-      }
-    },
-    "node_modules/@octokit/request": {
-      "version": "5.3.1",
-      "resolved": "https://registry.npmjs.org/@octokit/request/-/request-5.3.1.tgz",
-      "integrity": "sha512-5/X0AL1ZgoU32fAepTfEoggFinO3rxsMLtzhlUX+RctLrusn/CApJuGFCd0v7GMFhF+8UiCsTTfsu7Fh1HnEJg==",
-      "dev": true,
-      "dependencies": {
-        "@octokit/endpoint": "^5.5.0",
-        "@octokit/request-error": "^1.0.1",
-        "@octokit/types": "^2.0.0",
-        "deprecation": "^2.0.0",
-        "is-plain-object": "^3.0.0",
-        "node-fetch": "^2.3.0",
-        "once": "^1.4.0",
-        "universal-user-agent": "^4.0.0"
-      }
-    },
-    "node_modules/@octokit/request-error": {
-      "version": "1.2.0",
-      "resolved": "https://registry.npmjs.org/@octokit/request-error/-/request-error-1.2.0.tgz",
-      "integrity": "sha512-DNBhROBYjjV/I9n7A8kVkmQNkqFAMem90dSxqvPq57e2hBr7mNTX98y3R2zDpqMQHVRpBDjsvsfIGgBzy+4PAg==",
-      "dev": true,
-      "dependencies": {
-        "@octokit/types": "^2.0.0",
-        "deprecation": "^2.0.0",
-        "once": "^1.4.0"
-      }
-    },
-    "node_modules/@octokit/rest": {
-      "version": "16.37.0",
-      "resolved": "https://registry.npmjs.org/@octokit/rest/-/rest-16.37.0.tgz",
-      "integrity": "sha512-qLPK9FOCK4iVpn6ghknNuv/gDDxXQG6+JBQvoCwWjQESyis9uemakjzN36nvvp8SCny7JuzHI2RV8ChbV5mYdQ==",
-      "dev": true,
-      "dependencies": {
-        "@octokit/request": "^5.2.0",
-        "@octokit/request-error": "^1.0.2",
-        "atob-lite": "^2.0.0",
-        "before-after-hook": "^2.0.0",
-        "btoa-lite": "^1.0.0",
-        "deprecation": "^2.0.0",
-        "lodash.get": "^4.4.2",
-        "lodash.set": "^4.3.2",
-        "lodash.uniq": "^4.5.0",
-        "octokit-pagination-methods": "^1.1.0",
-        "once": "^1.4.0",
-        "universal-user-agent": "^4.0.0"
-      }
-    },
-    "node_modules/@octokit/types": {
-      "version": "2.1.0",
-      "resolved": "https://registry.npmjs.org/@octokit/types/-/types-2.1.0.tgz",
-      "integrity": "sha512-n1GUYFgKm5glcy0E+U5jnqAFY2p04rnK4A0YhuM70C7Vm9Vyx+xYwd/WOTEr8nUJcbPSR/XL+/26+rirY6jJQA==",
-      "dev": true,
-      "dependencies": {
-        "@types/node": ">= 8"
-      }
-    },
-    "node_modules/@types/node": {
-      "version": "13.1.8",
-      "resolved": "https://registry.npmjs.org/@types/node/-/node-13.1.8.tgz",
-      "integrity": "sha512-6XzyyNM9EKQW4HKuzbo/CkOIjn/evtCmsU+MUM1xDfJ+3/rNjBttM1NgN7AOQvN6tP1Sl1D1PIKMreTArnxM9A==",
-      "dev": true
-    },
-    "node_modules/atob-lite": {
-      "version": "2.0.0",
-      "resolved": "https://registry.npmjs.org/atob-lite/-/atob-lite-2.0.0.tgz",
-      "integrity": "sha1-D+9a1G8b16hQLGVyfwNn1e5D1pY=",
-      "dev": true
-    },
-    "node_modules/before-after-hook": {
-      "version": "2.1.0",
-      "resolved": "https://registry.npmjs.org/before-after-hook/-/before-after-hook-2.1.0.tgz",
-      "integrity": "sha512-IWIbu7pMqyw3EAJHzzHbWa85b6oud/yfKYg5rqB5hNE8CeMi3nX+2C2sj0HswfblST86hpVEOAb9x34NZd6P7A==",
-      "dev": true
-    },
-    "node_modules/btoa-lite": {
-      "version": "1.0.0",
-      "resolved": "https://registry.npmjs.org/btoa-lite/-/btoa-lite-1.0.0.tgz",
-      "integrity": "sha1-M3dm2hWAEhD92VbCLpxokaudAzc=",
-      "dev": true
-    },
-    "node_modules/cross-spawn": {
-      "version": "6.0.5",
-      "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.5.tgz",
-      "integrity": "sha512-eTVLrBSt7fjbDygz805pMnstIs2VTBNkRm0qxZd+M7A5XDdxVRWO5MxGBXZhjY4cqLYLdtrGqRf8mBPmzwSpWQ==",
-      "dev": true,
-      "dependencies": {
-        "nice-try": "^1.0.4",
-        "path-key": "^2.0.1",
-        "semver": "^5.5.0",
-        "shebang-command": "^1.2.0",
-        "which": "^1.2.9"
-      },
-      "engines": {
-        "node": ">=4.8"
-      }
-    },
-    "node_modules/deprecation": {
-      "version": "2.3.1",
-      "resolved": "https://registry.npmjs.org/deprecation/-/deprecation-2.3.1.tgz",
-      "integrity": "sha512-xmHIy4F3scKVwMsQ4WnVaS8bHOx0DmVwRywosKhaILI0ywMDWPtBSku2HNxRvF7jtwDRsoEwYQSfbxj8b7RlJQ==",
-      "dev": true
-    },
-    "node_modules/end-of-stream": {
-      "version": "1.4.4",
-      "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz",
-      "integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==",
-      "dev": true,
-      "dependencies": {
-        "once": "^1.4.0"
-      }
-    },
-    "node_modules/execa": {
-      "version": "1.0.0",
-      "resolved": "https://registry.npmjs.org/execa/-/execa-1.0.0.tgz",
-      "integrity": "sha512-adbxcyWV46qiHyvSp50TKt05tB4tK3HcmF7/nxfAdhnox83seTDbwnaqKO4sXRy7roHAIFqJP/Rw/AuEbX61LA==",
-      "dev": true,
-      "dependencies": {
-        "cross-spawn": "^6.0.0",
-        "get-stream": "^4.0.0",
-        "is-stream": "^1.1.0",
-        "npm-run-path": "^2.0.0",
-        "p-finally": "^1.0.0",
-        "signal-exit": "^3.0.0",
-        "strip-eof": "^1.0.0"
-      },
-      "engines": {
-        "node": ">=6"
-      }
-    },
-    "node_modules/get-stream": {
-      "version": "4.1.0",
-      "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-4.1.0.tgz",
-      "integrity": "sha512-GMat4EJ5161kIy2HevLlr4luNjBgvmj413KaQA7jt4V8B4RDsfpHk7WQ9GVqfYyyx8OS/L66Kox+rJRNklLK7w==",
-      "dev": true,
-      "dependencies": {
-        "pump": "^3.0.0"
-      },
-      "engines": {
-        "node": ">=6"
-      }
-    },
-    "node_modules/is-plain-object": {
-      "version": "3.0.0",
-      "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-3.0.0.tgz",
-      "integrity": "sha512-tZIpofR+P05k8Aocp7UI/2UTa9lTJSebCXpFFoR9aibpokDj/uXBsJ8luUu0tTVYKkMU6URDUuOfJZ7koewXvg==",
-      "dev": true,
-      "dependencies": {
-        "isobject": "^4.0.0"
-      },
-      "engines": {
-        "node": ">=0.10.0"
-      }
-    },
-    "node_modules/is-stream": {
-      "version": "1.1.0",
-      "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-1.1.0.tgz",
-      "integrity": "sha1-EtSj3U5o4Lec6428hBc66A2RykQ=",
-      "dev": true,
-      "engines": {
-        "node": ">=0.10.0"
-      }
-    },
-    "node_modules/isexe": {
-      "version": "2.0.0",
-      "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
-      "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=",
-      "dev": true
-    },
-    "node_modules/isobject": {
-      "version": "4.0.0",
-      "resolved": "https://registry.npmjs.org/isobject/-/isobject-4.0.0.tgz",
-      "integrity": "sha512-S/2fF5wH8SJA/kmwr6HYhK/RI/OkhD84k8ntalo0iJjZikgq1XFvR5M8NPT1x5F7fBwCG3qHfnzeP/Vh/ZxCUA==",
-      "dev": true,
-      "engines": {
-        "node": ">=0.10.0"
-      }
-    },
-    "node_modules/lodash.get": {
-      "version": "4.4.2",
-      "resolved": "https://registry.npmjs.org/lodash.get/-/lodash.get-4.4.2.tgz",
-      "integrity": "sha1-LRd/ZS+jHpObRDjVNBSZ36OCXpk=",
-      "dev": true
-    },
-    "node_modules/lodash.set": {
-      "version": "4.3.2",
-      "resolved": "https://registry.npmjs.org/lodash.set/-/lodash.set-4.3.2.tgz",
-      "integrity": "sha1-2HV7HagH3eJIFrDWqEvqGnYjCyM=",
-      "dev": true
-    },
-    "node_modules/lodash.uniq": {
-      "version": "4.5.0",
-      "resolved": "https://registry.npmjs.org/lodash.uniq/-/lodash.uniq-4.5.0.tgz",
-      "integrity": "sha1-0CJTc662Uq3BvILklFM5qEJ1R3M=",
-      "dev": true
-    },
-    "node_modules/macos-release": {
-      "version": "2.3.0",
-      "resolved": "https://registry.npmjs.org/macos-release/-/macos-release-2.3.0.tgz",
-      "integrity": "sha512-OHhSbtcviqMPt7yfw5ef5aghS2jzFVKEFyCJndQt2YpSQ9qRVSEv2axSJI1paVThEu+FFGs584h/1YhxjVqajA==",
-      "dev": true,
-      "engines": {
-        "node": ">=6"
-      }
-    },
-    "node_modules/nice-try": {
-      "version": "1.0.5",
-      "resolved": "https://registry.npmjs.org/nice-try/-/nice-try-1.0.5.tgz",
-      "integrity": "sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==",
-      "dev": true
-    },
-    "node_modules/node-fetch": {
-      "version": "2.6.7",
-      "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.7.tgz",
-      "integrity": "sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ==",
-      "dev": true,
-      "dependencies": {
-        "whatwg-url": "^5.0.0"
-      },
-      "engines": {
-        "node": "4.x || >=6.0.0"
-      },
-      "peerDependencies": {
-        "encoding": "^0.1.0"
-      },
-      "peerDependenciesMeta": {
-        "encoding": {
-          "optional": true
-        }
-      }
-    },
-    "node_modules/npm-run-path": {
-      "version": "2.0.2",
-      "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-2.0.2.tgz",
-      "integrity": "sha1-NakjLfo11wZ7TLLd8jV7GHFTbF8=",
-      "dev": true,
-      "dependencies": {
-        "path-key": "^2.0.0"
-      },
-      "engines": {
-        "node": ">=4"
-      }
-    },
-    "node_modules/octokit-pagination-methods": {
-      "version": "1.1.0",
-      "resolved": "https://registry.npmjs.org/octokit-pagination-methods/-/octokit-pagination-methods-1.1.0.tgz",
-      "integrity": "sha512-fZ4qZdQ2nxJvtcasX7Ghl+WlWS/d9IgnBIwFZXVNNZUmzpno91SX5bc5vuxiuKoCtK78XxGGNuSCrDC7xYB3OQ==",
-      "dev": true
-    },
-    "node_modules/once": {
-      "version": "1.4.0",
-      "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
-      "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=",
-      "dev": true,
-      "dependencies": {
-        "wrappy": "1"
-      }
-    },
-    "node_modules/os-name": {
-      "version": "3.1.0",
-      "resolved": "https://registry.npmjs.org/os-name/-/os-name-3.1.0.tgz",
-      "integrity": "sha512-h8L+8aNjNcMpo/mAIBPn5PXCM16iyPGjHNWo6U1YO8sJTMHtEtyczI6QJnLoplswm6goopQkqc7OAnjhWcugVg==",
-      "dev": true,
-      "dependencies": {
-        "macos-release": "^2.2.0",
-        "windows-release": "^3.1.0"
-      },
-      "engines": {
-        "node": ">=6"
-      }
-    },
-    "node_modules/p-finally": {
-      "version": "1.0.0",
-      "resolved": "https://registry.npmjs.org/p-finally/-/p-finally-1.0.0.tgz",
-      "integrity": "sha1-P7z7FbiZpEEjs0ttzBi3JDNqLK4=",
-      "dev": true,
-      "engines": {
-        "node": ">=4"
-      }
-    },
-    "node_modules/path-key": {
-      "version": "2.0.1",
-      "resolved": "https://registry.npmjs.org/path-key/-/path-key-2.0.1.tgz",
-      "integrity": "sha1-QRyttXTFoUDTpLGRDUDYDMn0C0A=",
-      "dev": true,
-      "engines": {
-        "node": ">=4"
-      }
-    },
-    "node_modules/pump": {
-      "version": "3.0.0",
-      "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz",
-      "integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==",
-      "dev": true,
-      "dependencies": {
-        "end-of-stream": "^1.1.0",
-        "once": "^1.3.1"
-      }
-    },
-    "node_modules/semver": {
-      "version": "5.7.1",
-      "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz",
-      "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==",
-      "dev": true,
-      "bin": {
-        "semver": "bin/semver"
-      }
-    },
-    "node_modules/shebang-command": {
-      "version": "1.2.0",
-      "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-1.2.0.tgz",
-      "integrity": "sha1-RKrGW2lbAzmJaMOfNj/uXer98eo=",
-      "dev": true,
-      "dependencies": {
-        "shebang-regex": "^1.0.0"
-      },
-      "engines": {
-        "node": ">=0.10.0"
-      }
-    },
-    "node_modules/shebang-regex": {
-      "version": "1.0.0",
-      "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-1.0.0.tgz",
-      "integrity": "sha1-2kL0l0DAtC2yypcoVxyxkMmO/qM=",
-      "dev": true,
-      "engines": {
-        "node": ">=0.10.0"
-      }
-    },
-    "node_modules/signal-exit": {
-      "version": "3.0.2",
-      "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.2.tgz",
-      "integrity": "sha1-tf3AjxKH6hF4Yo5BXiUTK3NkbG0=",
-      "dev": true
-    },
-    "node_modules/strip-eof": {
-      "version": "1.0.0",
-      "resolved": "https://registry.npmjs.org/strip-eof/-/strip-eof-1.0.0.tgz",
-      "integrity": "sha1-u0P/VZim6wXYm1n80SnJgzE2Br8=",
-      "dev": true,
-      "engines": {
-        "node": ">=0.10.0"
-      }
-    },
-    "node_modules/tr46": {
-      "version": "0.0.3",
-      "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz",
-      "integrity": "sha1-gYT9NH2snNwYWZLzpmIuFLnZq2o=",
-      "dev": true
-    },
-    "node_modules/tunnel": {
-      "version": "0.0.6",
-      "resolved": "https://registry.npmjs.org/tunnel/-/tunnel-0.0.6.tgz",
-      "integrity": "sha512-1h/Lnq9yajKY2PEbBadPXj3VxsDDu844OnaAo52UVmIzIvwwtBPIuNvkjuzBlTWpfJyUbG3ez0KSBibQkj4ojg==",
-      "dev": true,
-      "engines": {
-        "node": ">=0.6.11 <=0.7.0 || >=0.7.3"
-      }
-    },
-    "node_modules/universal-user-agent": {
-      "version": "4.0.0",
-      "resolved": "https://registry.npmjs.org/universal-user-agent/-/universal-user-agent-4.0.0.tgz",
-      "integrity": "sha512-eM8knLpev67iBDizr/YtqkJsF3GK8gzDc6st/WKzrTuPtcsOKW/0IdL4cnMBsU69pOx0otavLWBDGTwg+dB0aA==",
-      "dev": true,
-      "dependencies": {
-        "os-name": "^3.1.0"
-      }
-    },
-    "node_modules/uuid": {
-      "version": "8.3.2",
-      "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz",
-      "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==",
-      "dev": true,
-      "bin": {
-        "uuid": "dist/bin/uuid"
-      }
-    },
-    "node_modules/webidl-conversions": {
-      "version": "3.0.1",
-      "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz",
-      "integrity": "sha1-JFNCdeKnvGvnvIZhHMFq4KVlSHE=",
-      "dev": true
-    },
-    "node_modules/whatwg-url": {
-      "version": "5.0.0",
-      "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz",
-      "integrity": "sha1-lmRU6HZUYuN2RNNib2dCzotwll0=",
-      "dev": true,
-      "dependencies": {
-        "tr46": "~0.0.3",
-        "webidl-conversions": "^3.0.0"
-      }
-    },
-    "node_modules/which": {
-      "version": "1.3.1",
-      "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz",
-      "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==",
-      "dev": true,
-      "dependencies": {
-        "isexe": "^2.0.0"
-      },
-      "bin": {
-        "which": "bin/which"
-      }
-    },
-    "node_modules/windows-release": {
-      "version": "3.2.0",
-      "resolved": "https://registry.npmjs.org/windows-release/-/windows-release-3.2.0.tgz",
-      "integrity": "sha512-QTlz2hKLrdqukrsapKsINzqMgOUpQW268eJ0OaOpJN32h272waxR9fkB9VoWRtK7uKHG5EHJcTXQBD8XZVJkFA==",
-      "dev": true,
-      "dependencies": {
-        "execa": "^1.0.0"
-      },
-      "engines": {
-        "node": ">=6"
-      }
-    },
-    "node_modules/wrappy": {
-      "version": "1.0.2",
-      "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
-      "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=",
-      "dev": true
-    }
-  },
-  "dependencies": {
-    "@actions/core": {
-      "version": "1.9.1",
-      "resolved": "https://registry.npmjs.org/@actions/core/-/core-1.9.1.tgz",
-      "integrity": "sha512-5ad+U2YGrmmiw6du20AQW5XuWo7UKN2052FjSV7MX+Wfjf8sCqcsZe62NfgHys4QI4/Y+vQvLKYL8jWtA1ZBTA==",
-      "dev": true,
-      "requires": {
-        "@actions/http-client": "^2.0.1",
-        "uuid": "^8.3.2"
-      }
-    },
-    "@actions/exec": {
-      "version": "1.0.3",
-      "resolved": "https://registry.npmjs.org/@actions/exec/-/exec-1.0.3.tgz",
-      "integrity": "sha512-TogJGnueOmM7ntCi0ASTUj4LapRRtDfj57Ja4IhPmg2fls28uVOPbAn8N+JifaOumN2UG3oEO/Ixek2A4NcYSA==",
-      "dev": true,
-      "requires": {
-        "@actions/io": "^1.0.1"
-      }
-    },
-    "@actions/github": {
-      "version": "2.0.1",
-      "resolved": "https://registry.npmjs.org/@actions/github/-/github-2.0.1.tgz",
-      "integrity": "sha512-C7dAsCkpPi1HxTzLldz+oY+9c5G+nnaK7xgk8KA83VVGlrGK7d603E3snUAFocWrqEu/uvdYD82ytggjcpYSQA==",
-      "dev": true,
-      "requires": {
-        "@octokit/graphql": "^4.3.1",
-        "@octokit/rest": "^16.15.0"
-      }
-    },
-    "@actions/http-client": {
-      "version": "2.0.1",
-      "resolved": "https://registry.npmjs.org/@actions/http-client/-/http-client-2.0.1.tgz",
-      "integrity": "sha512-PIXiMVtz6VvyaRsGY268qvj57hXQEpsYogYOu2nrQhlf+XCGmZstmuZBbAybUl1nQGnvS1k1eEsQ69ZoD7xlSw==",
-      "dev": true,
-      "requires": {
-        "tunnel": "^0.0.6"
-      }
-    },
-    "@actions/io": {
-      "version": "1.0.2",
-      "resolved": "https://registry.npmjs.org/@actions/io/-/io-1.0.2.tgz",
-      "integrity": "sha512-J8KuFqVPr3p6U8W93DOXlXW6zFvrQAJANdS+vw0YhusLIq+bszW8zmK2Fh1C2kDPX8FMvwIl1OUcFgvJoXLbAg==",
-      "dev": true
-    },
-    "@octokit/endpoint": {
-      "version": "5.5.1",
-      "resolved": "https://registry.npmjs.org/@octokit/endpoint/-/endpoint-5.5.1.tgz",
-      "integrity": "sha512-nBFhRUb5YzVTCX/iAK1MgQ4uWo89Gu0TH00qQHoYRCsE12dWcG1OiLd7v2EIo2+tpUKPMOQ62QFy9hy9Vg2ULg==",
-      "dev": true,
-      "requires": {
-        "@octokit/types": "^2.0.0",
-        "is-plain-object": "^3.0.0",
-        "universal-user-agent": "^4.0.0"
-      }
-    },
-    "@octokit/graphql": {
-      "version": "4.3.1",
-      "resolved": "https://registry.npmjs.org/@octokit/graphql/-/graphql-4.3.1.tgz",
-      "integrity": "sha512-hCdTjfvrK+ilU2keAdqNBWOk+gm1kai1ZcdjRfB30oA3/T6n53UVJb7w0L5cR3/rhU91xT3HSqCd+qbvH06yxA==",
-      "dev": true,
-      "requires": {
-        "@octokit/request": "^5.3.0",
-        "@octokit/types": "^2.0.0",
-        "universal-user-agent": "^4.0.0"
-      }
-    },
-    "@octokit/request": {
-      "version": "5.3.1",
-      "resolved": "https://registry.npmjs.org/@octokit/request/-/request-5.3.1.tgz",
-      "integrity": "sha512-5/X0AL1ZgoU32fAepTfEoggFinO3rxsMLtzhlUX+RctLrusn/CApJuGFCd0v7GMFhF+8UiCsTTfsu7Fh1HnEJg==",
-      "dev": true,
-      "requires": {
-        "@octokit/endpoint": "^5.5.0",
-        "@octokit/request-error": "^1.0.1",
-        "@octokit/types": "^2.0.0",
-        "deprecation": "^2.0.0",
-        "is-plain-object": "^3.0.0",
-        "node-fetch": "^2.3.0",
-        "once": "^1.4.0",
-        "universal-user-agent": "^4.0.0"
-      }
-    },
-    "@octokit/request-error": {
-      "version": "1.2.0",
-      "resolved": "https://registry.npmjs.org/@octokit/request-error/-/request-error-1.2.0.tgz",
-      "integrity": "sha512-DNBhROBYjjV/I9n7A8kVkmQNkqFAMem90dSxqvPq57e2hBr7mNTX98y3R2zDpqMQHVRpBDjsvsfIGgBzy+4PAg==",
-      "dev": true,
-      "requires": {
-        "@octokit/types": "^2.0.0",
-        "deprecation": "^2.0.0",
-        "once": "^1.4.0"
-      }
-    },
-    "@octokit/rest": {
-      "version": "16.37.0",
-      "resolved": "https://registry.npmjs.org/@octokit/rest/-/rest-16.37.0.tgz",
-      "integrity": "sha512-qLPK9FOCK4iVpn6ghknNuv/gDDxXQG6+JBQvoCwWjQESyis9uemakjzN36nvvp8SCny7JuzHI2RV8ChbV5mYdQ==",
-      "dev": true,
-      "requires": {
-        "@octokit/request": "^5.2.0",
-        "@octokit/request-error": "^1.0.2",
-        "atob-lite": "^2.0.0",
-        "before-after-hook": "^2.0.0",
-        "btoa-lite": "^1.0.0",
-        "deprecation": "^2.0.0",
-        "lodash.get": "^4.4.2",
-        "lodash.set": "^4.3.2",
-        "lodash.uniq": "^4.5.0",
-        "octokit-pagination-methods": "^1.1.0",
-        "once": "^1.4.0",
-        "universal-user-agent": "^4.0.0"
-      }
-    },
-    "@octokit/types": {
-      "version": "2.1.0",
-      "resolved": "https://registry.npmjs.org/@octokit/types/-/types-2.1.0.tgz",
-      "integrity": "sha512-n1GUYFgKm5glcy0E+U5jnqAFY2p04rnK4A0YhuM70C7Vm9Vyx+xYwd/WOTEr8nUJcbPSR/XL+/26+rirY6jJQA==",
-      "dev": true,
-      "requires": {
-        "@types/node": ">= 8"
-      }
-    },
-    "@types/node": {
-      "version": "13.1.8",
-      "resolved": "https://registry.npmjs.org/@types/node/-/node-13.1.8.tgz",
-      "integrity": "sha512-6XzyyNM9EKQW4HKuzbo/CkOIjn/evtCmsU+MUM1xDfJ+3/rNjBttM1NgN7AOQvN6tP1Sl1D1PIKMreTArnxM9A==",
-      "dev": true
-    },
-    "atob-lite": {
-      "version": "2.0.0",
-      "resolved": "https://registry.npmjs.org/atob-lite/-/atob-lite-2.0.0.tgz",
-      "integrity": "sha1-D+9a1G8b16hQLGVyfwNn1e5D1pY=",
-      "dev": true
-    },
-    "before-after-hook": {
-      "version": "2.1.0",
-      "resolved": "https://registry.npmjs.org/before-after-hook/-/before-after-hook-2.1.0.tgz",
-      "integrity": "sha512-IWIbu7pMqyw3EAJHzzHbWa85b6oud/yfKYg5rqB5hNE8CeMi3nX+2C2sj0HswfblST86hpVEOAb9x34NZd6P7A==",
-      "dev": true
-    },
-    "btoa-lite": {
-      "version": "1.0.0",
-      "resolved": "https://registry.npmjs.org/btoa-lite/-/btoa-lite-1.0.0.tgz",
-      "integrity": "sha1-M3dm2hWAEhD92VbCLpxokaudAzc=",
-      "dev": true
-    },
-    "cross-spawn": {
-      "version": "6.0.5",
-      "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.5.tgz",
-      "integrity": "sha512-eTVLrBSt7fjbDygz805pMnstIs2VTBNkRm0qxZd+M7A5XDdxVRWO5MxGBXZhjY4cqLYLdtrGqRf8mBPmzwSpWQ==",
-      "dev": true,
-      "requires": {
-        "nice-try": "^1.0.4",
-        "path-key": "^2.0.1",
-        "semver": "^5.5.0",
-        "shebang-command": "^1.2.0",
-        "which": "^1.2.9"
-      }
-    },
-    "deprecation": {
-      "version": "2.3.1",
-      "resolved": "https://registry.npmjs.org/deprecation/-/deprecation-2.3.1.tgz",
-      "integrity": "sha512-xmHIy4F3scKVwMsQ4WnVaS8bHOx0DmVwRywosKhaILI0ywMDWPtBSku2HNxRvF7jtwDRsoEwYQSfbxj8b7RlJQ==",
-      "dev": true
-    },
-    "end-of-stream": {
-      "version": "1.4.4",
-      "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz",
-      "integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==",
-      "dev": true,
-      "requires": {
-        "once": "^1.4.0"
-      }
-    },
-    "execa": {
-      "version": "1.0.0",
-      "resolved": "https://registry.npmjs.org/execa/-/execa-1.0.0.tgz",
-      "integrity": "sha512-adbxcyWV46qiHyvSp50TKt05tB4tK3HcmF7/nxfAdhnox83seTDbwnaqKO4sXRy7roHAIFqJP/Rw/AuEbX61LA==",
-      "dev": true,
-      "requires": {
-        "cross-spawn": "^6.0.0",
-        "get-stream": "^4.0.0",
-        "is-stream": "^1.1.0",
-        "npm-run-path": "^2.0.0",
-        "p-finally": "^1.0.0",
-        "signal-exit": "^3.0.0",
-        "strip-eof": "^1.0.0"
-      }
-    },
-    "get-stream": {
-      "version": "4.1.0",
-      "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-4.1.0.tgz",
-      "integrity": "sha512-GMat4EJ5161kIy2HevLlr4luNjBgvmj413KaQA7jt4V8B4RDsfpHk7WQ9GVqfYyyx8OS/L66Kox+rJRNklLK7w==",
-      "dev": true,
-      "requires": {
-        "pump": "^3.0.0"
-      }
-    },
-    "is-plain-object": {
-      "version": "3.0.0",
-      "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-3.0.0.tgz",
-      "integrity": "sha512-tZIpofR+P05k8Aocp7UI/2UTa9lTJSebCXpFFoR9aibpokDj/uXBsJ8luUu0tTVYKkMU6URDUuOfJZ7koewXvg==",
-      "dev": true,
-      "requires": {
-        "isobject": "^4.0.0"
-      }
-    },
-    "is-stream": {
-      "version": "1.1.0",
-      "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-1.1.0.tgz",
-      "integrity": "sha1-EtSj3U5o4Lec6428hBc66A2RykQ=",
-      "dev": true
-    },
-    "isexe": {
-      "version": "2.0.0",
-      "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
-      "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=",
-      "dev": true
-    },
-    "isobject": {
-      "version": "4.0.0",
-      "resolved": "https://registry.npmjs.org/isobject/-/isobject-4.0.0.tgz",
-      "integrity": "sha512-S/2fF5wH8SJA/kmwr6HYhK/RI/OkhD84k8ntalo0iJjZikgq1XFvR5M8NPT1x5F7fBwCG3qHfnzeP/Vh/ZxCUA==",
-      "dev": true
-    },
-    "lodash.get": {
-      "version": "4.4.2",
-      "resolved": "https://registry.npmjs.org/lodash.get/-/lodash.get-4.4.2.tgz",
-      "integrity": "sha1-LRd/ZS+jHpObRDjVNBSZ36OCXpk=",
-      "dev": true
-    },
-    "lodash.set": {
-      "version": "4.3.2",
-      "resolved": "https://registry.npmjs.org/lodash.set/-/lodash.set-4.3.2.tgz",
-      "integrity": "sha1-2HV7HagH3eJIFrDWqEvqGnYjCyM=",
-      "dev": true
-    },
-    "lodash.uniq": {
-      "version": "4.5.0",
-      "resolved": "https://registry.npmjs.org/lodash.uniq/-/lodash.uniq-4.5.0.tgz",
-      "integrity": "sha1-0CJTc662Uq3BvILklFM5qEJ1R3M=",
-      "dev": true
-    },
-    "macos-release": {
-      "version": "2.3.0",
-      "resolved": "https://registry.npmjs.org/macos-release/-/macos-release-2.3.0.tgz",
-      "integrity": "sha512-OHhSbtcviqMPt7yfw5ef5aghS2jzFVKEFyCJndQt2YpSQ9qRVSEv2axSJI1paVThEu+FFGs584h/1YhxjVqajA==",
-      "dev": true
-    },
-    "nice-try": {
-      "version": "1.0.5",
-      "resolved": "https://registry.npmjs.org/nice-try/-/nice-try-1.0.5.tgz",
-      "integrity": "sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==",
-      "dev": true
-    },
-    "node-fetch": {
-      "version": "2.6.7",
-      "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.7.tgz",
-      "integrity": "sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ==",
-      "dev": true,
-      "requires": {
-        "whatwg-url": "^5.0.0"
-      }
-    },
-    "npm-run-path": {
-      "version": "2.0.2",
-      "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-2.0.2.tgz",
-      "integrity": "sha1-NakjLfo11wZ7TLLd8jV7GHFTbF8=",
-      "dev": true,
-      "requires": {
-        "path-key": "^2.0.0"
-      }
-    },
-    "octokit-pagination-methods": {
-      "version": "1.1.0",
-      "resolved": "https://registry.npmjs.org/octokit-pagination-methods/-/octokit-pagination-methods-1.1.0.tgz",
-      "integrity": "sha512-fZ4qZdQ2nxJvtcasX7Ghl+WlWS/d9IgnBIwFZXVNNZUmzpno91SX5bc5vuxiuKoCtK78XxGGNuSCrDC7xYB3OQ==",
-      "dev": true
-    },
-    "once": {
-      "version": "1.4.0",
-      "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
-      "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=",
-      "dev": true,
-      "requires": {
-        "wrappy": "1"
-      }
-    },
-    "os-name": {
-      "version": "3.1.0",
-      "resolved": "https://registry.npmjs.org/os-name/-/os-name-3.1.0.tgz",
-      "integrity": "sha512-h8L+8aNjNcMpo/mAIBPn5PXCM16iyPGjHNWo6U1YO8sJTMHtEtyczI6QJnLoplswm6goopQkqc7OAnjhWcugVg==",
-      "dev": true,
-      "requires": {
-        "macos-release": "^2.2.0",
-        "windows-release": "^3.1.0"
-      }
-    },
-    "p-finally": {
-      "version": "1.0.0",
-      "resolved": "https://registry.npmjs.org/p-finally/-/p-finally-1.0.0.tgz",
-      "integrity": "sha1-P7z7FbiZpEEjs0ttzBi3JDNqLK4=",
-      "dev": true
-    },
-    "path-key": {
-      "version": "2.0.1",
-      "resolved": "https://registry.npmjs.org/path-key/-/path-key-2.0.1.tgz",
-      "integrity": "sha1-QRyttXTFoUDTpLGRDUDYDMn0C0A=",
-      "dev": true
-    },
-    "pump": {
-      "version": "3.0.0",
-      "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz",
-      "integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==",
-      "dev": true,
-      "requires": {
-        "end-of-stream": "^1.1.0",
-        "once": "^1.3.1"
-      }
-    },
-    "semver": {
-      "version": "5.7.1",
-      "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz",
-      "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==",
-      "dev": true
-    },
-    "shebang-command": {
-      "version": "1.2.0",
-      "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-1.2.0.tgz",
-      "integrity": "sha1-RKrGW2lbAzmJaMOfNj/uXer98eo=",
-      "dev": true,
-      "requires": {
-        "shebang-regex": "^1.0.0"
-      }
-    },
-    "shebang-regex": {
-      "version": "1.0.0",
-      "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-1.0.0.tgz",
-      "integrity": "sha1-2kL0l0DAtC2yypcoVxyxkMmO/qM=",
-      "dev": true
-    },
-    "signal-exit": {
-      "version": "3.0.2",
-      "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.2.tgz",
-      "integrity": "sha1-tf3AjxKH6hF4Yo5BXiUTK3NkbG0=",
-      "dev": true
-    },
-    "strip-eof": {
-      "version": "1.0.0",
-      "resolved": "https://registry.npmjs.org/strip-eof/-/strip-eof-1.0.0.tgz",
-      "integrity": "sha1-u0P/VZim6wXYm1n80SnJgzE2Br8=",
-      "dev": true
-    },
-    "tr46": {
-      "version": "0.0.3",
-      "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz",
-      "integrity": "sha1-gYT9NH2snNwYWZLzpmIuFLnZq2o=",
-      "dev": true
-    },
-    "tunnel": {
-      "version": "0.0.6",
-      "resolved": "https://registry.npmjs.org/tunnel/-/tunnel-0.0.6.tgz",
-      "integrity": "sha512-1h/Lnq9yajKY2PEbBadPXj3VxsDDu844OnaAo52UVmIzIvwwtBPIuNvkjuzBlTWpfJyUbG3ez0KSBibQkj4ojg==",
-      "dev": true
-    },
-    "universal-user-agent": {
-      "version": "4.0.0",
-      "resolved": "https://registry.npmjs.org/universal-user-agent/-/universal-user-agent-4.0.0.tgz",
-      "integrity": "sha512-eM8knLpev67iBDizr/YtqkJsF3GK8gzDc6st/WKzrTuPtcsOKW/0IdL4cnMBsU69pOx0otavLWBDGTwg+dB0aA==",
-      "dev": true,
-      "requires": {
-        "os-name": "^3.1.0"
-      }
-    },
-    "uuid": {
-      "version": "8.3.2",
-      "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz",
-      "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==",
-      "dev": true
-    },
-    "webidl-conversions": {
-      "version": "3.0.1",
-      "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz",
-      "integrity": "sha1-JFNCdeKnvGvnvIZhHMFq4KVlSHE=",
-      "dev": true
-    },
-    "whatwg-url": {
-      "version": "5.0.0",
-      "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz",
-      "integrity": "sha1-lmRU6HZUYuN2RNNib2dCzotwll0=",
-      "dev": true,
-      "requires": {
-        "tr46": "~0.0.3",
-        "webidl-conversions": "^3.0.0"
-      }
-    },
-    "which": {
-      "version": "1.3.1",
-      "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz",
-      "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==",
-      "dev": true,
-      "requires": {
-        "isexe": "^2.0.0"
-      }
-    },
-    "windows-release": {
-      "version": "3.2.0",
-      "resolved": "https://registry.npmjs.org/windows-release/-/windows-release-3.2.0.tgz",
-      "integrity": "sha512-QTlz2hKLrdqukrsapKsINzqMgOUpQW268eJ0OaOpJN32h272waxR9fkB9VoWRtK7uKHG5EHJcTXQBD8XZVJkFA==",
-      "dev": true,
-      "requires": {
-        "execa": "^1.0.0"
-      }
-    },
-    "wrappy": {
-      "version": "1.0.2",
-      "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
-      "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=",
-      "dev": true
-    }
-  }
-}
diff --git a/ci/perf-tester/package.json b/ci/perf-tester/package.json
deleted file mode 100644
index 7a00de44d..000000000
--- a/ci/perf-tester/package.json
+++ /dev/null
@@ -1,9 +0,0 @@
-{
-  "private": true,
-  "main": "src/index.js",
-  "devDependencies": {
-    "@actions/core": "^1.9.1",
-    "@actions/exec": "^1.0.3",
-    "@actions/github": "^2.0.1"
-  }
-}
diff --git a/ci/perf-tester/src/index.js b/ci/perf-tester/src/index.js
deleted file mode 100644
index 6dd4a5e61..000000000
--- a/ci/perf-tester/src/index.js
+++ /dev/null
@@ -1,212 +0,0 @@
-/*
-Adapted from preactjs/compressed-size-action, which is available under this license:
-
-MIT License
-Copyright (c) 2020 Preact
-Permission is hereby granted, free of charge, to any person obtaining a copy
-of this software and associated documentation files (the "Software"), to deal
-in the Software without restriction, including without limitation the rights
-to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
-copies of the Software, and to permit persons to whom the Software is
-furnished to do so, subject to the following conditions:
-The above copyright notice and this permission notice shall be included in all
-copies or substantial portions of the Software.
-THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
-IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
-FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
-AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
-LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
-OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
-SOFTWARE.
-*/
-
-const { setFailed, startGroup, endGroup, debug } = require("@actions/core");
-const { GitHub, context } = require("@actions/github");
-const { exec } = require("@actions/exec");
-const {
-    config,
-    runBenchmark,
-    averageBenchmarks,
-    toDiff,
-    diffTable,
-} = require("./utils.js");
-
-const benchmarkParallel = 4;
-const benchmarkSerial = 4;
-const runBenchmarks = async () => {
-    let results = [];
-    for (let i = 0; i < benchmarkSerial; i++) {
-        results = results.concat(
-            await Promise.all(Array(benchmarkParallel).fill().map(runBenchmark))
-        );
-    }
-    return averageBenchmarks(results);
-};
-
-const perfActionComment =
-    "";
-
-async function run(octokit, context) {
-    const { number: pull_number } = context.issue;
-
-    const pr = context.payload.pull_request;
-    try {
-        debug("pr" + JSON.stringify(pr, null, 2));
-    } catch (e) {}
-    if (!pr) {
-        throw Error(
-            'Could not retrieve PR information. Only "pull_request" triggered workflows are currently supported.'
-        );
-    }
-
-    console.log(
-        `PR #${pull_number} is targeted at ${pr.base.ref} (${pr.base.sha})`
-    );
-
-    startGroup(`[current] Build using '${config.buildScript}'`);
-    await exec(config.buildScript);
-    endGroup();
-
-    startGroup(`[current] Running benchmark`);
-    const newBenchmarks = await runBenchmarks();
-    endGroup();
-
-    startGroup(`[base] Checkout target branch`);
-    let baseRef;
-    try {
-        baseRef = context.payload.base.ref;
-        if (!baseRef)
-            throw Error("missing context.payload.pull_request.base.ref");
-        await exec(
-            `git fetch -n origin ${context.payload.pull_request.base.ref}`
-        );
-        console.log("successfully fetched base.ref");
-    } catch (e) {
-        console.log("fetching base.ref failed", e.message);
-        try {
-            await exec(`git fetch -n origin ${pr.base.sha}`);
-            console.log("successfully fetched base.sha");
-        } catch (e) {
-            console.log("fetching base.sha failed", e.message);
-            try {
-                await exec(`git fetch -n`);
-            } catch (e) {
-                console.log("fetch failed", e.message);
-            }
-        }
-    }
-
-    console.log("checking out and building base commit");
-    try {
-        if (!baseRef) throw Error("missing context.payload.base.ref");
-        await exec(`git reset --hard ${baseRef}`);
-    } catch (e) {
-        await exec(`git reset --hard ${pr.base.sha}`);
-    }
-    endGroup();
-
-    startGroup(`[base] Build using '${config.buildScript}'`);
-    await exec(config.buildScript);
-    endGroup();
-
-    startGroup(`[base] Running benchmark`);
-    const oldBenchmarks = await runBenchmarks();
-    endGroup();
-
-    const diff = toDiff(oldBenchmarks, newBenchmarks);
-
-    const markdownDiff = diffTable(diff, {
-        collapseUnchanged: true,
-        omitUnchanged: false,
-        showTotal: true,
-        minimumChangeThreshold: config.minimumChangeThreshold,
-    });
-
-    let outputRawMarkdown = false;
-
-    const commentInfo = {
-        ...context.repo,
-        issue_number: pull_number,
-    };
-
-    const comment = {
-        ...commentInfo,
-        body: markdownDiff + "\n\n" + perfActionComment,
-    };
-
-    startGroup(`Updating stats PR comment`);
-    let commentId;
-    try {
-        const comments = (await octokit.issues.listComments(commentInfo)).data;
-        for (let i = comments.length; i--; ) {
-            const c = comments[i];
-            if (c.user.type === "Bot" && c.body.includes(perfActionComment)) {
-                commentId = c.id;
-                break;
-            }
-        }
-    } catch (e) {
-        console.log("Error checking for previous comments: " + e.message);
-    }
-
-    if (commentId) {
-        console.log(`Updating previous comment #${commentId}`);
-        try {
-            await octokit.issues.updateComment({
-                ...context.repo,
-                comment_id: commentId,
-                body: comment.body,
-            });
-        } catch (e) {
-            console.log("Error editing previous comment: " + e.message);
-            commentId = null;
-        }
-    }
-
-    // no previous or edit failed
-    if (!commentId) {
-        console.log("Creating new comment");
-        try {
-            await octokit.issues.createComment(comment);
-        } catch (e) {
-            console.log(`Error creating comment: ${e.message}`);
-            console.log(`Submitting a PR review comment instead...`);
-            try {
-                const issue = context.issue || pr;
-                await octokit.pulls.createReview({
-                    owner: issue.owner,
-                    repo: issue.repo,
-                    pull_number: issue.number,
-                    event: "COMMENT",
-                    body: comment.body,
-                });
-            } catch (e) {
-                console.log("Error creating PR review.");
-                outputRawMarkdown = true;
-            }
-        }
-        endGroup();
-    }
-
-    if (outputRawMarkdown) {
-        console.log(
-            `
-			Error: performance-action was unable to comment on your PR.
-			This can happen for PR's originating from a fork without write permissions.
-			You can copy the size table directly into a comment using the markdown below:
-			\n\n${comment.body}\n\n
-		`.replace(/^(\t|  )+/gm, "")
-        );
-    }
-
-    console.log("All done!");
-}
-
-(async () => {
-    try {
-        const octokit = new GitHub(process.env.GITHUB_TOKEN);
-        await run(octokit, context);
-    } catch (e) {
-        setFailed(e.message);
-    }
-})();
diff --git a/ci/perf-tester/src/utils.js b/ci/perf-tester/src/utils.js
deleted file mode 100644
index c7ecd662b..000000000
--- a/ci/perf-tester/src/utils.js
+++ /dev/null
@@ -1,221 +0,0 @@
-/*
-Adapted from preactjs/compressed-size-action, which is available under this license:
-
-MIT License
-Copyright (c) 2020 Preact
-Permission is hereby granted, free of charge, to any person obtaining a copy
-of this software and associated documentation files (the "Software"), to deal
-in the Software without restriction, including without limitation the rights
-to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
-copies of the Software, and to permit persons to whom the Software is
-furnished to do so, subject to the following conditions:
-The above copyright notice and this permission notice shall be included in all
-copies or substantial portions of the Software.
-THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
-IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
-FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
-AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
-LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
-OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
-SOFTWARE.
-*/
-
-const { exec } = require("@actions/exec");
-
-const formatMS = (ms) =>
-    `${ms.toLocaleString("en-US", {
-        maximumFractionDigits: 0,
-    })}ms`;
-
-const config = {
-    buildScript: "make bootstrap benchmark_setup",
-    benchmark: "make -s run_benchmark",
-    minimumChangeThreshold: 5,
-};
-exports.config = config;
-
-exports.runBenchmark = async () => {
-    let benchmarkBuffers = [];
-    await exec(config.benchmark, [], {
-        listeners: {
-            stdout: (data) => benchmarkBuffers.push(data),
-        },
-    });
-    const output = Buffer.concat(benchmarkBuffers).toString("utf8");
-    return parse(output);
-};
-
-const firstLineRe = /^Running '(.+)' \.\.\.$/;
-const secondLineRe = /^done ([\d.]+) ms$/;
-
-function parse(benchmarkData) {
-    const lines = benchmarkData.trim().split("\n");
-    const benchmarks = Object.create(null);
-    for (let i = 0; i < lines.length - 1; i += 2) {
-        const [, name] = firstLineRe.exec(lines[i]);
-        const [, time] = secondLineRe.exec(lines[i + 1]);
-        benchmarks[name] = Math.round(parseFloat(time));
-    }
-    return benchmarks;
-}
-
-exports.averageBenchmarks = (benchmarks) => {
-    const result = Object.create(null);
-    for (const key of Object.keys(benchmarks[0])) {
-        result[key] =
-            benchmarks.reduce((acc, bench) => acc + bench[key], 0) /
-            benchmarks.length;
-    }
-    return result;
-};
-
-/**
- * @param {{[key: string]: number}} before
- * @param {{[key: string]: number}} after
- * @return {Diff[]}
- */
-exports.toDiff = (before, after) => {
-    const names = [...new Set([...Object.keys(before), ...Object.keys(after)])];
-    return names.map((name) => {
-        const timeBefore = before[name] || 0;
-        const timeAfter = after[name] || 0;
-        const delta = timeAfter - timeBefore;
-        return { name, time: timeAfter, delta };
-    });
-};
-
-/**
- * @param {number} delta
- * @param {number} difference
- */
-function getDeltaText(delta, difference) {
-    let deltaText = (delta > 0 ? "+" : "") + formatMS(delta);
-    if (delta && Math.abs(delta) > 1) {
-        deltaText += ` (${Math.abs(difference)}%)`;
-    }
-    return deltaText;
-}
-
-/**
- * @param {number} difference
- */
-function iconForDifference(difference) {
-    let icon = "";
-    if (difference >= 50) icon = "🆘";
-    else if (difference >= 20) icon = "🚨";
-    else if (difference >= 10) icon = "⚠️";
-    else if (difference >= 5) icon = "🔍";
-    else if (difference <= -50) icon = "🏆";
-    else if (difference <= -20) icon = "🎉";
-    else if (difference <= -10) icon = "👏";
-    else if (difference <= -5) icon = "✅";
-    return icon;
-}
-
-/**
- * Create a Markdown table from text rows
- * @param {string[]} rows
- */
-function markdownTable(rows) {
-    if (rows.length == 0) {
-        return "";
-    }
-
-    // Skip all empty columns
-    while (rows.every((columns) => !columns[columns.length - 1])) {
-        for (const columns of rows) {
-            columns.pop();
-        }
-    }
-
-    const [firstRow] = rows;
-    const columnLength = firstRow.length;
-    if (columnLength === 0) {
-        return "";
-    }
-
-    return [
-        // Header
-        ["Test name", "Duration", "Change", ""].slice(0, columnLength),
-        // Align
-        [":---", ":---:", ":---:", ":---:"].slice(0, columnLength),
-        // Body
-        ...rows,
-    ]
-        .map((columns) => `| ${columns.join(" | ")} |`)
-        .join("\n");
-}
-
-/**
- * @typedef {Object} Diff
- * @property {string} name
- * @property {number} time
- * @property {number} delta
- */
-
-/**
- * Create a Markdown table showing diff data
- * @param {Diff[]} tests
- * @param {object} options
- * @param {boolean} [options.showTotal]
- * @param {boolean} [options.collapseUnchanged]
- * @param {boolean} [options.omitUnchanged]
- * @param {number} [options.minimumChangeThreshold]
- */
-exports.diffTable = (
-    tests,
-    { showTotal, collapseUnchanged, omitUnchanged, minimumChangeThreshold }
-) => {
-    let changedRows = [];
-    let unChangedRows = [];
-    let baselineRows = [];
-
-    let totalTime = 0;
-    let totalDelta = 0;
-    for (const file of tests) {
-        const { name, time, delta } = file;
-        totalTime += time;
-        totalDelta += delta;
-
-        const difference = ((delta / time) * 100) | 0;
-        const isUnchanged = Math.abs(difference) < minimumChangeThreshold;
-
-        if (isUnchanged && omitUnchanged) continue;
-
-        const columns = [
-            name,
-            formatMS(time),
-            getDeltaText(delta, difference),
-            iconForDifference(difference),
-        ];
-        if (name.includes('directly')) {
-            baselineRows.push(columns);
-        } else if (isUnchanged && collapseUnchanged) {
-            unChangedRows.push(columns);
-        } else {
-            changedRows.push(columns);
-        }
-    }
-
-    let out = markdownTable(changedRows);
-
-    if (unChangedRows.length !== 0) {
-        const outUnchanged = markdownTable(unChangedRows);
-        out += `\n\n
View Unchanged\n\n${outUnchanged}\n\n
\n\n`; - } - - if (baselineRows.length !== 0) { - const outBaseline = markdownTable(baselineRows.map(line => line.slice(0, 2))); - out += `\n\n
View Baselines\n\n${outBaseline}\n\n
\n\n`; - } - - if (showTotal) { - const totalDifference = ((totalDelta / totalTime) * 100) | 0; - let totalDeltaText = getDeltaText(totalDelta, totalDifference); - let totalIcon = iconForDifference(totalDifference); - out = `**Total Time:** ${formatMS(totalTime)}\n\n${out}`; - out = `**Time Change:** ${totalDeltaText} ${totalIcon}\n\n${out}`; - } - - return out; -}; From 7e7aa80ed986b2305bef45b4c23994ef3d9a4838 Mon Sep 17 00:00:00 2001 From: Yuta Saito Date: Sat, 12 Apr 2025 00:31:09 +0000 Subject: [PATCH 370/373] Remove UMD build of JS runtime library We always use ESM, so we don't need to generate UMD runtime.js anymore. --- Makefile | 1 - Plugins/PackageToJS/Templates/runtime.js | 837 ----------------------- Runtime/rollup.config.mjs | 5 - Sources/JavaScriptKit/Runtime/index.js | 1 - 4 files changed, 844 deletions(-) delete mode 100644 Plugins/PackageToJS/Templates/runtime.js delete mode 120000 Sources/JavaScriptKit/Runtime/index.js diff --git a/Makefile b/Makefile index 1524ba1ba..d0d25f423 100644 --- a/Makefile +++ b/Makefile @@ -22,6 +22,5 @@ unittest: .PHONY: regenerate_swiftpm_resources regenerate_swiftpm_resources: npm run build - cp Runtime/lib/index.js Plugins/PackageToJS/Templates/runtime.js 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/Plugins/PackageToJS/Templates/runtime.js b/Plugins/PackageToJS/Templates/runtime.js deleted file mode 100644 index da27a1524..000000000 --- a/Plugins/PackageToJS/Templates/runtime.js +++ /dev/null @@ -1,837 +0,0 @@ -(function (global, factory) { - typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports) : - typeof define === 'function' && define.amd ? define(['exports'], factory) : - (global = typeof globalThis !== 'undefined' ? globalThis : global || self, factory(global.JavaScriptKit = {})); -})(this, (function (exports) { 'use strict'; - - /// Memory lifetime of closures in Swift are managed by Swift side - class SwiftClosureDeallocator { - constructor(exports) { - if (typeof FinalizationRegistry === "undefined") { - throw new Error("The Swift part of JavaScriptKit was configured to require " + - "the availability of JavaScript WeakRefs. Please build " + - "with `-Xswiftc -DJAVASCRIPTKIT_WITHOUT_WEAKREFS` to " + - "disable features that use WeakRefs."); - } - this.functionRegistry = new FinalizationRegistry((id) => { - exports.swjs_free_host_function(id); - }); - } - track(func, func_ref) { - this.functionRegistry.register(func, func_ref); - } - } - - function assertNever(x, message) { - throw new Error(message); - } - const MAIN_THREAD_TID = -1; - - const decode = (kind, payload1, payload2, memory) => { - switch (kind) { - case 0 /* 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 { - } - - exports.SwiftRuntime = SwiftRuntime; - -})); diff --git a/Runtime/rollup.config.mjs b/Runtime/rollup.config.mjs index 15efea491..b29609fe1 100644 --- a/Runtime/rollup.config.mjs +++ b/Runtime/rollup.config.mjs @@ -10,11 +10,6 @@ const config = [ file: "lib/index.mjs", format: "esm", }, - { - file: "lib/index.js", - format: "umd", - name: "JavaScriptKit", - }, ], plugins: [typescript()], }, diff --git a/Sources/JavaScriptKit/Runtime/index.js b/Sources/JavaScriptKit/Runtime/index.js deleted file mode 120000 index c60afde55..000000000 --- a/Sources/JavaScriptKit/Runtime/index.js +++ /dev/null @@ -1 +0,0 @@ -../../../Plugins/PackageToJS/Templates/runtime.js \ No newline at end of file From 62427ba2309d710a2ff13c9044b6507064cf925d Mon Sep 17 00:00:00 2001 From: Yuta Saito Date: Sat, 12 Apr 2025 03:30:30 +0000 Subject: [PATCH 371/373] Cleanup unused Makefile variables --- Makefile | 3 --- 1 file changed, 3 deletions(-) diff --git a/Makefile b/Makefile index d0d25f423..e2aef5f8d 100644 --- a/Makefile +++ b/Makefile @@ -1,7 +1,4 @@ -MAKEFILE_DIR := $(dir $(lastword $(MAKEFILE_LIST))) - SWIFT_SDK_ID ?= wasm32-unknown-wasi -SWIFT_BUILD_FLAGS := --swift-sdk $(SWIFT_SDK_ID) .PHONY: bootstrap bootstrap: From 2b5f6749fbb1b00b7e03d834c2665e5ff23d2075 Mon Sep 17 00:00:00 2001 From: Max Desiatov Date: Wed, 16 Apr 2025 10:45:17 +0100 Subject: [PATCH 372/373] Fix some Embedded Swift issues in JavaScriptEventLoop This change fixes some of the issues that appear when building this library with Embedded Swift. It adds missing concurrency imports and avoids use of throwing tasks that are not supported in Embedded Swift. Standard Swift's `Result` type is used instead to express error throwing. --- Sources/JavaScriptEventLoop/JSSending.swift | 1 + .../JavaScriptEventLoop.swift | 21 ++++++++++--------- Sources/JavaScriptEventLoop/JobQueue.swift | 1 + .../WebWorkerDedicatedExecutor.swift | 3 +++ .../WebWorkerTaskExecutor.swift | 2 +- 5 files changed, 17 insertions(+), 11 deletions(-) diff --git a/Sources/JavaScriptEventLoop/JSSending.swift b/Sources/JavaScriptEventLoop/JSSending.swift index e0e28a2f0..3408b232f 100644 --- a/Sources/JavaScriptEventLoop/JSSending.swift +++ b/Sources/JavaScriptEventLoop/JSSending.swift @@ -1,3 +1,4 @@ +import _Concurrency @_spi(JSObject_id) import JavaScriptKit import _CJavaScriptKit diff --git a/Sources/JavaScriptEventLoop/JavaScriptEventLoop.swift b/Sources/JavaScriptEventLoop/JavaScriptEventLoop.swift index 6cd8de171..8948723d4 100644 --- a/Sources/JavaScriptEventLoop/JavaScriptEventLoop.swift +++ b/Sources/JavaScriptEventLoop/JavaScriptEventLoop.swift @@ -1,4 +1,5 @@ import JavaScriptKit +import _Concurrency import _CJavaScriptEventLoop import _CJavaScriptKit @@ -259,38 +260,38 @@ extension JavaScriptEventLoop { extension JSPromise { /// Wait for the promise to complete, returning (or throwing) its result. public var value: JSValue { - get async throws { - try await withUnsafeThrowingContinuation { [self] continuation in + get async throws(JSException) { + try await withUnsafeContinuation { [self] continuation in self.then( success: { - continuation.resume(returning: $0) + continuation.resume(returning: Swift.Result.success($0)) return JSValue.undefined }, failure: { - continuation.resume(throwing: JSException($0)) + continuation.resume(returning: Swift.Result.failure(.init($0))) return JSValue.undefined } ) - } + }.get() } } /// Wait for the promise to complete, returning its result or exception as a Result. /// /// - Note: Calling this function does not switch from the caller's isolation domain. - public func value(isolation: isolated (any Actor)? = #isolation) async throws -> JSValue { - try await withUnsafeThrowingContinuation(isolation: isolation) { [self] continuation in + public func value(isolation: isolated (any Actor)? = #isolation) async throws(JSException) -> JSValue { + try await withUnsafeContinuation(isolation: isolation) { [self] continuation in self.then( success: { - continuation.resume(returning: $0) + continuation.resume(returning: Swift.Result.success($0)) return JSValue.undefined }, failure: { - continuation.resume(throwing: JSException($0)) + continuation.resume(returning: Swift.Result.failure(.init($0))) return JSValue.undefined } ) - } + }.get() } /// Wait for the promise to complete, returning its result or exception as a Result. diff --git a/Sources/JavaScriptEventLoop/JobQueue.swift b/Sources/JavaScriptEventLoop/JobQueue.swift index cb583dae3..a0f2c4bbb 100644 --- a/Sources/JavaScriptEventLoop/JobQueue.swift +++ b/Sources/JavaScriptEventLoop/JobQueue.swift @@ -2,6 +2,7 @@ // The current implementation is much simple to be easily debugged, but should be re-implemented // using priority queue ideally. +import _Concurrency import _CJavaScriptEventLoop #if compiler(>=5.5) diff --git a/Sources/JavaScriptEventLoop/WebWorkerDedicatedExecutor.swift b/Sources/JavaScriptEventLoop/WebWorkerDedicatedExecutor.swift index eecaf93c5..d42c5adda 100644 --- a/Sources/JavaScriptEventLoop/WebWorkerDedicatedExecutor.swift +++ b/Sources/JavaScriptEventLoop/WebWorkerDedicatedExecutor.swift @@ -1,5 +1,7 @@ +#if !hasFeature(Embedded) import JavaScriptKit import _CJavaScriptEventLoop +import _Concurrency #if canImport(Synchronization) import Synchronization @@ -60,3 +62,4 @@ public final class WebWorkerDedicatedExecutor: SerialExecutor { self.underlying.enqueue(job) } } +#endif diff --git a/Sources/JavaScriptEventLoop/WebWorkerTaskExecutor.swift b/Sources/JavaScriptEventLoop/WebWorkerTaskExecutor.swift index a1962eb77..9fa7b8810 100644 --- a/Sources/JavaScriptEventLoop/WebWorkerTaskExecutor.swift +++ b/Sources/JavaScriptEventLoop/WebWorkerTaskExecutor.swift @@ -1,4 +1,4 @@ -#if compiler(>=6.0) // `TaskExecutor` is available since Swift 6.0 +#if compiler(>=6.0) && !hasFeature(Embedded) // `TaskExecutor` is available since Swift 6.0, no multi-threading for embedded Wasm yet. import JavaScriptKit import _CJavaScriptKit From 86e2095e3024c57a927b1975ac39cc254517602a Mon Sep 17 00:00:00 2001 From: Max Desiatov Date: Wed, 16 Apr 2025 10:48:03 +0100 Subject: [PATCH 373/373] Fix formatting --- Sources/JavaScriptEventLoop/WebWorkerTaskExecutor.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/JavaScriptEventLoop/WebWorkerTaskExecutor.swift b/Sources/JavaScriptEventLoop/WebWorkerTaskExecutor.swift index 9fa7b8810..651e7be2a 100644 --- a/Sources/JavaScriptEventLoop/WebWorkerTaskExecutor.swift +++ b/Sources/JavaScriptEventLoop/WebWorkerTaskExecutor.swift @@ -1,4 +1,4 @@ -#if compiler(>=6.0) && !hasFeature(Embedded) // `TaskExecutor` is available since Swift 6.0, no multi-threading for embedded Wasm yet. +#if compiler(>=6.0) && !hasFeature(Embedded) // `TaskExecutor` is available since Swift 6.0, no multi-threading for embedded Wasm yet. import JavaScriptKit import _CJavaScriptKit