Use JSKit runtime from SwiftPM resources (#335)
* Use JSKit runtime from SwiftPM resources * Fix Node.js test runner * Remove unused webpack npm packages * Update Swift version in `.swiftformat` * Fix browser and Node.js CJS/ESM handling * Fix one of the tests, add CI time limit * Use Tokamak `update-jskit` branch to fix tests * Use latest Vapor with `.mjs` content-type fix * Use dynamic import to detect JSKit presence * Fix missing `runtimeConstructor` reference * Update `StaticArchive.swift` * Reduce the diff * Address PR feedback * Fix Node.js <-> JSKit integration test * Update SwiftPM dependencies * Fix comment typo in `testNode.js` * Reuse `__stack_sanitizer` across entrypoints
This commit is contained in:
parent
b9dab08e86
commit
1ba7dd5d82
|
@ -13,13 +13,14 @@ jobs:
|
|||
include:
|
||||
- os: macos-12
|
||||
swift_version: 5.6
|
||||
xcode: /Applications/Xcode_13.3.app/Contents/Developer
|
||||
xcode: /Applications/Xcode_13.4.app/Contents/Developer
|
||||
- os: macos-11
|
||||
swift_version: 5.5
|
||||
xcode: /Applications/Xcode_13.2.1.app/Contents/Developer
|
||||
- os: ubuntu-20.04
|
||||
swift_version: 5.5
|
||||
name: Build on ${{ matrix.os }} with Swift ${{ matrix.swift_version }}
|
||||
timeout-minutes: 40
|
||||
runs-on: ${{ matrix.os }}
|
||||
|
||||
steps:
|
||||
|
|
|
@ -6,5 +6,5 @@
|
|||
--ifdef noindent
|
||||
--stripunusedargs closure-only
|
||||
--disable andOperator
|
||||
--swiftversion 5.2
|
||||
--maxwidth 100
|
||||
--swiftversion 5.5
|
||||
--maxwidth 120
|
||||
|
|
|
@ -15,8 +15,8 @@
|
|||
"repositoryURL": "https://github.com/vapor/async-kit.git",
|
||||
"state": {
|
||||
"branch": null,
|
||||
"revision": "e2f741640364c1d271405da637029ea6a33f754e",
|
||||
"version": "1.11.1"
|
||||
"revision": "017dc7da68c1ec9f0f46fcd1a8002d14a5662732",
|
||||
"version": "1.12.0"
|
||||
}
|
||||
},
|
||||
{
|
||||
|
@ -24,8 +24,8 @@
|
|||
"repositoryURL": "https://github.com/vapor/console-kit.git",
|
||||
"state": {
|
||||
"branch": null,
|
||||
"revision": "75ea3b627d88221440b878e5dfccc73fd06842ed",
|
||||
"version": "4.2.7"
|
||||
"revision": "98d6e72ea7e756da9f87184c7239650223f95800",
|
||||
"version": "4.3.0"
|
||||
}
|
||||
},
|
||||
{
|
||||
|
@ -33,8 +33,8 @@
|
|||
"repositoryURL": "https://github.com/vapor/multipart-kit.git",
|
||||
"state": {
|
||||
"branch": null,
|
||||
"revision": "2dd9368a3c9580792b77c7ef364f3735909d9996",
|
||||
"version": "4.5.1"
|
||||
"revision": "0d55c35e788451ee27222783c7d363cb88092fab",
|
||||
"version": "4.5.2"
|
||||
}
|
||||
},
|
||||
{
|
||||
|
@ -42,8 +42,8 @@
|
|||
"repositoryURL": "https://github.com/vapor/routing-kit.git",
|
||||
"state": {
|
||||
"branch": null,
|
||||
"revision": "5603b81ceb744b8318feab1e60943704977a866b",
|
||||
"version": "4.3.1"
|
||||
"revision": "9e181d685a3dec1eef1fc6dacf606af364f86d68",
|
||||
"version": "4.5.0"
|
||||
}
|
||||
},
|
||||
{
|
||||
|
@ -73,6 +73,15 @@
|
|||
"version": "1.3.1"
|
||||
}
|
||||
},
|
||||
{
|
||||
"package": "swift-collections",
|
||||
"repositoryURL": "https://github.com/apple/swift-collections.git",
|
||||
"state": {
|
||||
"branch": null,
|
||||
"revision": "48254824bb4248676bf7ce56014ff57b142b77eb",
|
||||
"version": "1.0.2"
|
||||
}
|
||||
},
|
||||
{
|
||||
"package": "swift-crypto",
|
||||
"repositoryURL": "https://github.com/apple/swift-crypto.git",
|
||||
|
@ -186,8 +195,8 @@
|
|||
"repositoryURL": "https://github.com/vapor/vapor.git",
|
||||
"state": {
|
||||
"branch": null,
|
||||
"revision": "b05266f863ee93eeff4b6d075364ccc8f81bbc1e",
|
||||
"version": "4.57.0"
|
||||
"revision": "ca823e66f8efd4dec7f5b996776d0ccc4cb34585",
|
||||
"version": "4.60.0"
|
||||
}
|
||||
},
|
||||
{
|
||||
|
|
|
@ -33,7 +33,7 @@ let package = Package(
|
|||
url: "https://github.com/apple/swift-tools-support-core.git",
|
||||
.branch("release/5.6")
|
||||
),
|
||||
.package(url: "https://github.com/vapor/vapor.git", from: "4.53.0"),
|
||||
.package(url: "https://github.com/vapor/vapor.git", from: "4.57.1"),
|
||||
.package(url: "https://github.com/apple/swift-crypto.git", from: "1.1.0"),
|
||||
.package(url: "https://github.com/JohnSundell/Splash.git", from: "0.16.0"),
|
||||
.package(
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
// Copyright 2020 Carton contributors
|
||||
// Copyright 2022 Carton contributors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
|
@ -21,6 +21,7 @@ private enum Constants {
|
|||
static let entrypoint = Entrypoint(fileName: "testNode.js", sha256: testNodeEntrypointSHA256)
|
||||
}
|
||||
|
||||
/// Test runner for Node.js.
|
||||
struct NodeTestRunner: TestRunner {
|
||||
let testFilePath: AbsolutePath
|
||||
let listTestCases: Bool
|
||||
|
@ -28,9 +29,31 @@ struct NodeTestRunner: TestRunner {
|
|||
let terminal: InteractiveWriter
|
||||
|
||||
func run() async throws {
|
||||
terminal.write("\nRunning the test bundle with NodeJS:\n", inColor: .yellow)
|
||||
let (_, _, filePath) = Constants.entrypoint.paths(on: localFileSystem)
|
||||
var nodeArguments = ["node", filePath.pathString, testFilePath.pathString]
|
||||
terminal.write("\nRunning the test bundle with Node.js:\n", inColor: .yellow)
|
||||
|
||||
let (_, _, entrypointPath) = Constants.entrypoint.paths(on: localFileSystem)
|
||||
|
||||
// Allow Node.js to resolve modules from resource directories by making them relative to the entrypoint path.
|
||||
let buildDirectory = testFilePath.parentDirectory
|
||||
let staticDirectory = entrypointPath.parentDirectory
|
||||
|
||||
// Clean up existing symlinks before creating new ones.
|
||||
for existingSymlink in try localFileSystem.resourcesDirectoryNames(relativeTo: staticDirectory) {
|
||||
try localFileSystem.removeFileTree(staticDirectory.appending(component: existingSymlink))
|
||||
}
|
||||
|
||||
let resourceDirectories = try localFileSystem.resourcesDirectoryNames(relativeTo: buildDirectory)
|
||||
|
||||
// Create new symlink for each resource directory.
|
||||
for resourcesDirectoryName in resourceDirectories {
|
||||
try localFileSystem.createSymbolicLink(
|
||||
staticDirectory.appending(component: resourcesDirectoryName),
|
||||
pointingAt: buildDirectory.appending(component: resourcesDirectoryName),
|
||||
relative: false
|
||||
)
|
||||
}
|
||||
|
||||
var nodeArguments = ["node", entrypointPath.pathString, testFilePath.pathString]
|
||||
if listTestCases {
|
||||
nodeArguments.append(contentsOf: ["--", "-l"])
|
||||
} else if !testCases.isEmpty {
|
||||
|
@ -39,5 +62,4 @@ struct NodeTestRunner: TestRunner {
|
|||
}
|
||||
try await Process.run(nodeArguments, terminal)
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -65,6 +65,7 @@ public extension TSCBasic.Process {
|
|||
// swiftlint:disable:next function_body_length
|
||||
static func run(
|
||||
_ arguments: [String],
|
||||
environment: [String: String] = [:],
|
||||
loadingMessage: String = "Running...",
|
||||
parser: ProcessOutputParser? = nil,
|
||||
_ terminal: InteractiveWriter
|
||||
|
@ -72,6 +73,10 @@ public extension TSCBasic.Process {
|
|||
terminal.clearLine()
|
||||
terminal.write("\(loadingMessage)\n", inColor: .yellow)
|
||||
|
||||
if !environment.isEmpty {
|
||||
terminal.write(environment.map { "\($0)=\($1)" }.joined(separator: " ") + " ")
|
||||
}
|
||||
|
||||
let processName = arguments[0].first == "/" ?
|
||||
AbsolutePath(arguments[0]).basename : arguments[0]
|
||||
|
||||
|
@ -98,6 +103,7 @@ public extension TSCBasic.Process {
|
|||
|
||||
let process = Process(
|
||||
arguments: arguments,
|
||||
environment: ProcessEnv.vars.merging(environment) { _, new in new },
|
||||
outputRedirection: .stream(stdout: stdout, stderr: stderr),
|
||||
verbose: true,
|
||||
startNewProcessGroup: true
|
||||
|
|
|
@ -60,22 +60,27 @@ extension Application {
|
|||
|
||||
get("main.wasm") {
|
||||
// stream the file
|
||||
$0.eventLoop
|
||||
.makeSucceededFuture($0.fileio.streamFile(at: configuration.mainWasmPath.pathString))
|
||||
$0.fileio.streamFile(at: configuration.mainWasmPath.pathString)
|
||||
}
|
||||
|
||||
// Serve resources for all targets at their respective paths.
|
||||
let buildDirectory = configuration.mainWasmPath.parentDirectory
|
||||
|
||||
for directoryName in try localFileSystem.resourcesDirectoryNames(relativeTo: buildDirectory) {
|
||||
get(.constant(directoryName), "**") {
|
||||
$0.eventLoop.makeSucceededFuture($0.fileio.streamFile(at: AbsolutePath(
|
||||
buildDirectory.appending(component: directoryName),
|
||||
$0.parameters.getCatchall().joined(separator: "/")
|
||||
).pathString))
|
||||
func requestHandler(_ directoryName: String) -> ((Request) -> Response) {
|
||||
{ (request: Request) -> Response in
|
||||
request.fileio.streamFile(
|
||||
at: AbsolutePath(
|
||||
buildDirectory.appending(component: directoryName),
|
||||
request.parameters.getCatchall().joined(separator: "/")
|
||||
).pathString
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
for directoryName in try localFileSystem.resourcesDirectoryNames(relativeTo: buildDirectory) {
|
||||
get(.constant(directoryName), "**", use: requestHandler(directoryName))
|
||||
}
|
||||
|
||||
let inferredMainTarget = configuration.manifest.targets.first {
|
||||
configuration.product?.targets.contains($0.name) == true
|
||||
}
|
||||
|
@ -84,11 +89,6 @@ extension Application {
|
|||
guard let mainTarget = inferredMainTarget else { return }
|
||||
|
||||
let resourcesPath = configuration.manifest.resourcesPath(for: mainTarget)
|
||||
get("**") {
|
||||
$0.eventLoop.makeSucceededFuture($0.fileio.streamFile(at: AbsolutePath(
|
||||
buildDirectory.appending(component: resourcesPath),
|
||||
$0.parameters.getCatchall().joined(separator: "/")
|
||||
).pathString))
|
||||
}
|
||||
get("**", use: requestHandler(resourcesPath))
|
||||
}
|
||||
}
|
||||
|
|
|
@ -53,7 +53,7 @@ extension HTML: ResponseEncodable {
|
|||
}
|
||||
|
||||
public static func indexPage(customContent: String?, entrypointName: String) -> String {
|
||||
let scriptTag = #"<script type="text/javascript" src="\#(entrypointName)"></script>"#
|
||||
let scriptTag = #"<script type="module" src="\#(entrypointName)"></script>"#
|
||||
if let customContent = customContent {
|
||||
return customContent.replacingOccurrences(
|
||||
of: "</head>",
|
||||
|
|
|
@ -229,7 +229,7 @@ public actor Server {
|
|||
/// Blocking function that starts the HTTP server.
|
||||
public nonisolated func run() async throws {
|
||||
// Explicitly hop to another thread to avoid blocking the thread that is running the actor executor
|
||||
try await withCheckedThrowingContinuation { (continuation: CheckedContinuation<Void, Error>) in
|
||||
try await withCheckedThrowingContinuation { (continuation: CheckedContinuation<(), Error>) in
|
||||
Thread {
|
||||
Task {
|
||||
do {
|
||||
|
|
File diff suppressed because one or more lines are too long
|
@ -19,7 +19,7 @@ import TSCBasic
|
|||
import TSCUtility
|
||||
import WasmTransformer
|
||||
|
||||
public let compatibleJSKitVersion = Version(0, 14, 0)
|
||||
public let compatibleJSKitVersion = Version(0, 15, 0)
|
||||
|
||||
enum ToolchainError: Error, CustomStringConvertible {
|
||||
case directoryDoesNotExist(AbsolutePath)
|
||||
|
@ -86,10 +86,10 @@ extension PackageDependency {
|
|||
default: break
|
||||
}
|
||||
if let exactVersion = exactVersion {
|
||||
return exactVersion == compatibleJSKitVersion
|
||||
return exactVersion >= compatibleJSKitVersion
|
||||
}
|
||||
if let versionRange = versionRange {
|
||||
return versionRange.upperBound >= compatibleJSKitVersion
|
||||
return versionRange.lowerBound >= compatibleJSKitVersion
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
@ -127,7 +127,8 @@ public final class Toolchain {
|
|||
self.terminal = terminal
|
||||
if let workingDirectory = fileSystem.currentWorkingDirectory {
|
||||
let swiftc = swiftPath.parentDirectory.appending(component: "swiftc")
|
||||
manifest = await Result { try await Manifest.from(path: workingDirectory, swiftc: swiftc, fileSystem: fileSystem, terminal: terminal)
|
||||
manifest = await Result {
|
||||
try await Manifest.from(path: workingDirectory, swiftc: swiftc, fileSystem: fileSystem, terminal: terminal)
|
||||
}
|
||||
} else {
|
||||
manifest = .failure(ToolchainError.noWorkingDirectory)
|
||||
|
@ -147,7 +148,7 @@ public final class Toolchain {
|
|||
}
|
||||
|
||||
private func inferDevProduct(hint: String?) throws -> ProductDescription? {
|
||||
let manifest = try self.manifest.get()
|
||||
let manifest = try manifest.get()
|
||||
|
||||
var candidateProducts = manifest.products
|
||||
.filter { $0.type == .executable }
|
||||
|
@ -201,7 +202,7 @@ public final class Toolchain {
|
|||
}
|
||||
|
||||
public func inferSourcesPaths() throws -> [AbsolutePath] {
|
||||
let manifest = try self.manifest.get()
|
||||
let manifest = try manifest.get()
|
||||
|
||||
let targetPaths = manifest.targets.compactMap { target -> String? in
|
||||
|
||||
|
@ -224,7 +225,7 @@ public final class Toolchain {
|
|||
}
|
||||
|
||||
private func emitJSKitWarningIfNeeded() throws {
|
||||
let manifest = try self.manifest.get()
|
||||
let manifest = try manifest.get()
|
||||
guard let jsKit = manifest.dependencies.first(where: {
|
||||
$0.nameForTargetDependencyResolutionOnly == "JavaScriptKit"
|
||||
}) else {
|
||||
|
@ -305,7 +306,7 @@ public final class Toolchain {
|
|||
public func buildTestBundle(
|
||||
flavor: BuildFlavor
|
||||
) async throws -> AbsolutePath {
|
||||
let manifest = try self.manifest.get()
|
||||
let manifest = try manifest.get()
|
||||
let binPath = try inferBinPath(isRelease: flavor.isRelease)
|
||||
let testProductName = "\(manifest.displayName)PackageTests"
|
||||
let testBundlePath = binPath.appending(component: "\(testProductName).wasm")
|
||||
|
|
|
@ -45,16 +45,34 @@ struct HashArchive: AsyncParsableCommand {
|
|||
)
|
||||
|
||||
try localFileSystem.createDirectory(dotFilesStaticPath, recursive: true)
|
||||
let hashes = try await ["dev", "bundle", "test", "testNode"].asyncMap { entrypoint -> (String, String) in
|
||||
try await Process.run(["npm", "run", entrypoint], terminal)
|
||||
let entrypointPath = AbsolutePath(staticPath, "\(entrypoint).js")
|
||||
let dotFilesEntrypointPath = dotFilesStaticPath.appending(component: "\(entrypoint).js")
|
||||
try localFileSystem.removeFileTree(dotFilesEntrypointPath)
|
||||
try localFileSystem.copy(from: entrypointPath, to: dotFilesEntrypointPath)
|
||||
let hashes = try await (["dev", "bundle", "test", "testNode"])
|
||||
.asyncMap { entrypoint -> (String, String) in
|
||||
let filename = "\(entrypoint).js"
|
||||
var arguments = [
|
||||
"npx", "esbuild", "--bundle", "entrypoint/\(filename)", "--outfile=static/\(filename)",
|
||||
]
|
||||
|
||||
return (entrypoint, try SHA256().hash(localFileSystem.readFileContents(entrypointPath))
|
||||
.hexadecimalRepresentation.uppercased())
|
||||
}
|
||||
if entrypoint == "testNode" {
|
||||
arguments.append(contentsOf: [
|
||||
"--format=cjs", "--platform=node",
|
||||
"--external:./JavaScriptKit_JavaScriptKit.resources/Runtime/index.js",
|
||||
])
|
||||
} else {
|
||||
arguments.append(contentsOf: [
|
||||
"--format=esm",
|
||||
"--external:./JavaScriptKit_JavaScriptKit.resources/Runtime/index.mjs",
|
||||
])
|
||||
}
|
||||
|
||||
try await Process.run(arguments, terminal)
|
||||
let entrypointPath = AbsolutePath(staticPath, filename)
|
||||
let dotFilesEntrypointPath = dotFilesStaticPath.appending(component: filename)
|
||||
try localFileSystem.removeFileTree(dotFilesEntrypointPath)
|
||||
try localFileSystem.copy(from: entrypointPath, to: dotFilesEntrypointPath)
|
||||
|
||||
return (entrypoint, try SHA256().hash(localFileSystem.readFileContents(entrypointPath))
|
||||
.hexadecimalRepresentation.uppercased())
|
||||
}
|
||||
|
||||
try localFileSystem.writeFileContents(
|
||||
staticPath.appending(component: "so_sanitizer.wasm"),
|
||||
|
@ -83,10 +101,8 @@ struct HashArchive: AsyncParsableCommand {
|
|||
public let \($0)EntrypointSHA256 = ByteString([
|
||||
\(arrayString(from: $1))
|
||||
])
|
||||
|
||||
|
||||
"""
|
||||
}.joined())
|
||||
}.joined(separator: "\n\n"))
|
||||
|
||||
public let staticArchiveContents = "\(staticArchiveContents.withData { $0.base64EncodedString() })"
|
||||
"""
|
||||
|
|
|
@ -30,7 +30,7 @@ final class DevCommandTests: XCTestCase {
|
|||
client = nil
|
||||
}
|
||||
|
||||
#if os(macOS)
|
||||
#if os(macOS)
|
||||
func testWithNoArguments() throws {
|
||||
let url = "http://127.0.0.1:8080"
|
||||
|
||||
|
@ -88,7 +88,7 @@ final class DevCommandTests: XCTestCase {
|
|||
// clean up
|
||||
do { try packageDirectory.appending(component: ".build").delete() } catch {}
|
||||
}
|
||||
#endif
|
||||
#endif
|
||||
|
||||
func checkForExpectedContent(at url: String) {
|
||||
// client time out for connecting and responding
|
||||
|
@ -106,7 +106,7 @@ final class DevCommandTests: XCTestCase {
|
|||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<script type="text/javascript" src="dev.js"></script>
|
||||
<script type="module" src="dev.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
</body>
|
||||
|
|
|
@ -24,7 +24,8 @@ import XCTest
|
|||
extension TestCommandTests: Testable {}
|
||||
|
||||
private enum Constants {
|
||||
static let anyPackageName = "TestApp"
|
||||
static let testAppPackageName = "TestApp"
|
||||
static let nodeJSKitPackageName = "NodeJSKitTest"
|
||||
}
|
||||
|
||||
final class TestCommandTests: XCTestCase {
|
||||
|
@ -36,7 +37,7 @@ final class TestCommandTests: XCTestCase {
|
|||
}
|
||||
|
||||
func testWithNoArguments() throws {
|
||||
let packageDirectory = givenAPackageTestDirectory(Constants.anyPackageName)
|
||||
let packageDirectory = givenAPackageTestDirectory(Constants.testAppPackageName)
|
||||
|
||||
AssertExecuteCommand(
|
||||
command: "carton test",
|
||||
|
@ -45,8 +46,18 @@ final class TestCommandTests: XCTestCase {
|
|||
)
|
||||
}
|
||||
|
||||
func testEnvironmentNode() throws {
|
||||
let packageDirectory = givenAPackageTestDirectory(Constants.anyPackageName)
|
||||
func testEnvironmentNodeNoJSKit() throws {
|
||||
let packageDirectory = givenAPackageTestDirectory(Constants.testAppPackageName)
|
||||
|
||||
AssertExecuteCommand(
|
||||
command: "carton test --environment node",
|
||||
cwd: packageDirectory.url,
|
||||
debug: true
|
||||
)
|
||||
}
|
||||
|
||||
func testEnvironmentNodeJSKit() throws {
|
||||
let packageDirectory = givenAPackageTestDirectory(Constants.nodeJSKitPackageName)
|
||||
|
||||
AssertExecuteCommand(
|
||||
command: "carton test --environment node",
|
||||
|
@ -57,30 +68,30 @@ final class TestCommandTests: XCTestCase {
|
|||
|
||||
// This test is prone to hanging on Linux.
|
||||
#if os(macOS)
|
||||
func testEnvironmentDefaultBrowser() throws {
|
||||
let packageDirectory = givenAPackageTestDirectory()
|
||||
func testEnvironmentDefaultBrowser() throws {
|
||||
let packageDirectory = givenAPackageTestDirectory()
|
||||
|
||||
let expectedTestSuiteCount = 1
|
||||
let expectedTestsCount = 1
|
||||
let expectedTestSuiteCount = 1
|
||||
let expectedTestsCount = 1
|
||||
|
||||
let expectedContent =
|
||||
"""
|
||||
Test Suites: \(ControlCode.CSI)32m\(expectedTestSuiteCount) passed\(ControlCode
|
||||
let expectedContent =
|
||||
"""
|
||||
Test Suites: \(ControlCode.CSI)32m\(expectedTestSuiteCount) passed\(ControlCode
|
||||
.CSI)0m, \(expectedTestSuiteCount) total
|
||||
Tests: \(ControlCode.CSI)32m\(expectedTestsCount) passed\(ControlCode
|
||||
Tests: \(ControlCode.CSI)32m\(expectedTestsCount) passed\(ControlCode
|
||||
.CSI)0m, \(expectedTestsCount) total
|
||||
"""
|
||||
"""
|
||||
|
||||
AssertExecuteCommand(
|
||||
command: "carton test --environment defaultBrowser",
|
||||
cwd: packageDirectory.url,
|
||||
expected: expectedContent,
|
||||
expectedContains: true
|
||||
)
|
||||
}
|
||||
AssertExecuteCommand(
|
||||
command: "carton test --environment defaultBrowser",
|
||||
cwd: packageDirectory.url,
|
||||
expected: expectedContent,
|
||||
expectedContains: true
|
||||
)
|
||||
}
|
||||
#endif
|
||||
|
||||
private func givenAPackageTestDirectory(_ name: String = Constants.anyPackageName)
|
||||
private func givenAPackageTestDirectory(_ name: String = Constants.testAppPackageName)
|
||||
-> TestDirectory
|
||||
{
|
||||
let packageDirectory = TestDirectory(testFixturesDirectory, name)
|
||||
|
@ -89,7 +100,6 @@ final class TestCommandTests: XCTestCase {
|
|||
|
||||
return packageDirectory
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private class TestDirectory {
|
||||
|
@ -99,7 +109,7 @@ private class TestDirectory {
|
|||
var exists: Bool { directory.exists }
|
||||
|
||||
init(_ testDirectory: AbsolutePath, _ dirName: String) {
|
||||
self.directory = testDirectory.appending(components: dirName)
|
||||
directory = testDirectory.appending(components: dirName)
|
||||
cleanBuildDir()
|
||||
}
|
||||
|
||||
|
|
|
@ -1,61 +1,59 @@
|
|||
{
|
||||
"object": {
|
||||
"pins": [
|
||||
{
|
||||
"package": "JavaScriptKit",
|
||||
"repositoryURL": "https://github.com/swiftwasm/JavaScriptKit.git",
|
||||
"state": {
|
||||
"branch": null,
|
||||
"revision": "9a3b7d5f316e6d1709bd25e3e0392efca357f525",
|
||||
"version": "0.14.0"
|
||||
}
|
||||
},
|
||||
{
|
||||
"package": "OpenCombine",
|
||||
"repositoryURL": "https://github.com/OpenCombine/OpenCombine.git",
|
||||
"state": {
|
||||
"branch": null,
|
||||
"revision": "28993ae57de5a4ea7e164787636cafad442d568c",
|
||||
"version": "0.12.0"
|
||||
}
|
||||
},
|
||||
{
|
||||
"package": "OpenCombineJS",
|
||||
"repositoryURL": "https://github.com/swiftwasm/OpenCombineJS.git",
|
||||
"state": {
|
||||
"branch": null,
|
||||
"revision": "f1f1799ddbb9876a0ef8c5700a3b78d352d0b969",
|
||||
"version": "0.1.2"
|
||||
}
|
||||
},
|
||||
{
|
||||
"package": "swift-argument-parser",
|
||||
"repositoryURL": "https://github.com/apple/swift-argument-parser",
|
||||
"state": {
|
||||
"branch": null,
|
||||
"revision": "83b23d940471b313427da226196661856f6ba3e0",
|
||||
"version": "0.4.4"
|
||||
}
|
||||
},
|
||||
{
|
||||
"package": "Benchmark",
|
||||
"repositoryURL": "https://github.com/google/swift-benchmark",
|
||||
"state": {
|
||||
"branch": null,
|
||||
"revision": "8e0ef8bb7482ab97dcd2cd1d6855bd38921c345d",
|
||||
"version": "0.1.0"
|
||||
}
|
||||
},
|
||||
{
|
||||
"package": "Tokamak",
|
||||
"repositoryURL": "https://github.com/TokamakUI/Tokamak",
|
||||
"state": {
|
||||
"branch": null,
|
||||
"revision": "6905fdff1910df163f9f58c7a51e97b66fe59f40",
|
||||
"version": "0.10.0"
|
||||
}
|
||||
"pins" : [
|
||||
{
|
||||
"identity" : "javascriptkit",
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/swiftwasm/JavaScriptKit.git",
|
||||
"state" : {
|
||||
"revision" : "2d7bc960eed438dce7355710ece43fa004bbb3ac",
|
||||
"version" : "0.15.0"
|
||||
}
|
||||
]
|
||||
},
|
||||
"version": 1
|
||||
},
|
||||
{
|
||||
"identity" : "opencombine",
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/OpenCombine/OpenCombine.git",
|
||||
"state" : {
|
||||
"revision" : "9cf67e363738dbab61b47fb5eaed78d3db31e5ee",
|
||||
"version" : "0.13.0"
|
||||
}
|
||||
},
|
||||
{
|
||||
"identity" : "opencombinejs",
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/swiftwasm/OpenCombineJS.git",
|
||||
"state" : {
|
||||
"revision" : "e574e418ba468ff5c2d4c499eb56f108aeb4d2ba",
|
||||
"version" : "0.2.0"
|
||||
}
|
||||
},
|
||||
{
|
||||
"identity" : "swift-argument-parser",
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/apple/swift-argument-parser",
|
||||
"state" : {
|
||||
"revision" : "f3c9084a71ef4376f2fabbdf1d3d90a49f1fabdb",
|
||||
"version" : "1.1.2"
|
||||
}
|
||||
},
|
||||
{
|
||||
"identity" : "swift-benchmark",
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/google/swift-benchmark",
|
||||
"state" : {
|
||||
"revision" : "8163295f6fe82356b0bcf8e1ab991645de17d096",
|
||||
"version" : "0.1.2"
|
||||
}
|
||||
},
|
||||
{
|
||||
"identity" : "tokamak",
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/TokamakUI/Tokamak",
|
||||
"state" : {
|
||||
"branch" : "main",
|
||||
"revision" : "337b80a4c408db814e5e9e61bd232b4f96251308"
|
||||
}
|
||||
}
|
||||
],
|
||||
"version" : 2
|
||||
}
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
// swift-tools-version:5.3
|
||||
// swift-tools-version:5.6
|
||||
import PackageDescription
|
||||
let package = Package(
|
||||
name: "Milk",
|
||||
|
@ -7,10 +7,10 @@ let package = Package(
|
|||
.executable(name: "Milk", targets: ["Milk"]),
|
||||
],
|
||||
dependencies: [
|
||||
.package(name: "Tokamak", url: "https://github.com/TokamakUI/Tokamak", from: "0.10.0"),
|
||||
.package(url: "https://github.com/TokamakUI/Tokamak", branch: "main"),
|
||||
],
|
||||
targets: [
|
||||
.target(
|
||||
.executableTarget(
|
||||
name: "Milk",
|
||||
dependencies: [
|
||||
.product(name: "TokamakShim", package: "Tokamak"),
|
||||
|
|
|
@ -0,0 +1,9 @@
|
|||
.DS_Store
|
||||
/.build
|
||||
/Packages
|
||||
/*.xcodeproj
|
||||
xcuserdata/
|
||||
DerivedData/
|
||||
.swiftpm/config/registries.json
|
||||
.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata
|
||||
.netrc
|
|
@ -0,0 +1,14 @@
|
|||
{
|
||||
"pins" : [
|
||||
{
|
||||
"identity" : "javascriptkit",
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/swiftwasm/JavaScriptKit",
|
||||
"state" : {
|
||||
"revision" : "2d7bc960eed438dce7355710ece43fa004bbb3ac",
|
||||
"version" : "0.15.0"
|
||||
}
|
||||
}
|
||||
],
|
||||
"version" : 2
|
||||
}
|
|
@ -0,0 +1,23 @@
|
|||
// swift-tools-version: 5.6
|
||||
// The swift-tools-version declares the minimum version of Swift required to build this package.
|
||||
|
||||
import PackageDescription
|
||||
|
||||
let package = Package(
|
||||
name: "NodeJSKitTest",
|
||||
dependencies: [
|
||||
.package(url: "https://github.com/swiftwasm/JavaScriptKit", from: "0.15.0"),
|
||||
],
|
||||
targets: [
|
||||
// Targets are the basic building blocks of a package. A target can define a module or a test suite.
|
||||
// Targets can depend on other targets in this package, and on products in packages this package depends on.
|
||||
.executableTarget(
|
||||
name: "NodeJSKitTest",
|
||||
dependencies: []
|
||||
),
|
||||
.testTarget(
|
||||
name: "NodeJSKitTestTests",
|
||||
dependencies: ["JavaScriptKit"]
|
||||
),
|
||||
]
|
||||
)
|
|
@ -0,0 +1 @@
|
|||
print("Hello, world!")
|
|
@ -0,0 +1,15 @@
|
|||
import JavaScriptKit
|
||||
import XCTest
|
||||
|
||||
final class NodeJSKitTestTests: XCTestCase {
|
||||
func testExample() throws {
|
||||
let require = JSObject.global.require.function!
|
||||
let Buffer = require("buffer").object!.Buffer.function!
|
||||
|
||||
let testString = "Hello"
|
||||
let base64String = Buffer.from!(testString).toString("base64")
|
||||
let decodedString = Buffer.from!(base64String, "base64").toString("ascii").string!
|
||||
|
||||
XCTAssertEqual(decodedString, testString)
|
||||
}
|
||||
}
|
|
@ -6,8 +6,8 @@
|
|||
"repositoryURL": "https://github.com/swiftwasm/JavaScriptKit",
|
||||
"state": {
|
||||
"branch": null,
|
||||
"revision": "9a3b7d5f316e6d1709bd25e3e0392efca357f525",
|
||||
"version": "0.14.0"
|
||||
"revision": "2d7bc960eed438dce7355710ece43fa004bbb3ac",
|
||||
"version": "0.15.0"
|
||||
}
|
||||
}
|
||||
]
|
||||
|
|
|
@ -9,7 +9,7 @@ let package = Package(
|
|||
.executable(name: "TestApp", targets: ["TestApp"]),
|
||||
],
|
||||
dependencies: [
|
||||
.package(url: "https://github.com/swiftwasm/JavaScriptKit", from: "0.14.0"),
|
||||
.package(url: "https://github.com/swiftwasm/JavaScriptKit", from: "0.15.0"),
|
||||
],
|
||||
targets: [
|
||||
// Targets are the basic building blocks of a package. A target can define a module or a test
|
||||
|
|
|
@ -1,11 +0,0 @@
|
|||
const path = require("path");
|
||||
const outputPath = path.resolve(__dirname, "../static");
|
||||
|
||||
module.exports = {
|
||||
entry: "./entrypoint/bundle.js",
|
||||
mode: "production",
|
||||
output: {
|
||||
filename: "bundle.js",
|
||||
path: outputPath,
|
||||
},
|
||||
};
|
|
@ -12,18 +12,28 @@
|
|||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
import { WasmRunner } from './common'
|
||||
|
||||
const wasmRunner = WasmRunner()
|
||||
import { WasmRunner } from "./common.js";
|
||||
|
||||
const startWasiTask = async () => {
|
||||
// Fetch our Wasm File
|
||||
const response = await fetch("REPLACE_THIS_WITH_THE_MAIN_WEBASSEMBLY_MODULE");
|
||||
const responseArrayBuffer = await response.arrayBuffer();
|
||||
|
||||
let runtimeConstructor;
|
||||
try {
|
||||
const { SwiftRuntime } = await import(
|
||||
"./JavaScriptKit_JavaScriptKit.resources/Runtime/index.mjs"
|
||||
);
|
||||
runtimeConstructor = SwiftRuntime;
|
||||
} catch {
|
||||
// JavaScriptKit module not available, running without JavaScriptKit runtime.
|
||||
}
|
||||
|
||||
const wasmRunner = WasmRunner(false, runtimeConstructor);
|
||||
|
||||
// Instantiate the WebAssembly file
|
||||
const wasmBytes = new Uint8Array(responseArrayBuffer).buffer;
|
||||
await wasmRunner.run(wasmBytes)
|
||||
await wasmRunner.run(wasmBytes);
|
||||
};
|
||||
|
||||
function handleError(e) {
|
||||
|
|
|
@ -12,14 +12,16 @@
|
|||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
import { SwiftRuntime } from "javascript-kit-swift";
|
||||
import { WASI } from "@wasmer/wasi";
|
||||
import { WasmFs } from "@wasmer/wasmfs";
|
||||
|
||||
export const WasmRunner = (rawOptions) => {
|
||||
export const WasmRunner = (rawOptions, SwiftRuntime) => {
|
||||
const options = defaultRunnerOptions(rawOptions);
|
||||
|
||||
const swift = new SwiftRuntime();
|
||||
let swift;
|
||||
if (SwiftRuntime) {
|
||||
swift = new SwiftRuntime();
|
||||
}
|
||||
|
||||
const wasmFs = createWasmFS(
|
||||
(stdout) => {
|
||||
|
@ -44,9 +46,12 @@ export const WasmRunner = (rawOptions) => {
|
|||
const createWasmImportObject = (extraWasmImports) => {
|
||||
const importObject = {
|
||||
wasi_snapshot_preview1: wrapWASI(wasi),
|
||||
javascript_kit: swift.wasmImports,
|
||||
};
|
||||
|
||||
if (swift) {
|
||||
importObject.javascript_kit = swift.wasmImports;
|
||||
}
|
||||
|
||||
if (extraWasmImports) {
|
||||
// Shallow clone
|
||||
for (const key in extraWasmImports) {
|
||||
|
@ -59,13 +64,21 @@ export const WasmRunner = (rawOptions) => {
|
|||
|
||||
return {
|
||||
async run(wasmBytes, extraWasmImports) {
|
||||
if (!extraWasmImports) {
|
||||
extraWasmImports = {};
|
||||
}
|
||||
extraWasmImports.__stack_sanitizer = {
|
||||
report_stack_overflow: () => {
|
||||
throw new Error("Detected stack buffer overflow.");
|
||||
},
|
||||
};
|
||||
const importObject = createWasmImportObject(extraWasmImports);
|
||||
const module = await WebAssembly.instantiate(wasmBytes, importObject);
|
||||
|
||||
// Node support
|
||||
const instance = "instance" in module ? module.instance : module;
|
||||
|
||||
if (instance.exports.swjs_library_version) {
|
||||
if (swift && instance.exports.swjs_library_version) {
|
||||
swift.setInstance(instance);
|
||||
}
|
||||
|
||||
|
|
|
@ -1,11 +0,0 @@
|
|||
const path = require("path");
|
||||
const outputPath = path.resolve(__dirname, "../static");
|
||||
|
||||
module.exports = {
|
||||
entry: "./entrypoint/dev.js",
|
||||
mode: "development",
|
||||
output: {
|
||||
filename: "dev.js",
|
||||
path: outputPath,
|
||||
},
|
||||
};
|
|
@ -13,7 +13,7 @@
|
|||
// limitations under the License.
|
||||
|
||||
import ReconnectingWebSocket from "reconnecting-websocket";
|
||||
import { WasmRunner } from "./common"
|
||||
import { WasmRunner } from "./common.js";
|
||||
|
||||
const socket = new ReconnectingWebSocket(`ws://${location.host}/watcher`);
|
||||
|
||||
|
@ -23,34 +23,43 @@ socket.addEventListener("message", (message) => {
|
|||
}
|
||||
});
|
||||
|
||||
const wasmRunner = WasmRunner({
|
||||
onStderr() {
|
||||
const prevLimit = Error.stackTraceLimit;
|
||||
Error.stackTraceLimit = 1000
|
||||
socket.send(
|
||||
JSON.stringify({
|
||||
kind: "stackTrace",
|
||||
stackTrace: new Error().stack,
|
||||
})
|
||||
);
|
||||
Error.stackTraceLimit = prevLimit;
|
||||
}
|
||||
})
|
||||
|
||||
const startWasiTask = async () => {
|
||||
// Fetch our Wasm File
|
||||
const response = await fetch("/main.wasm");
|
||||
const responseArrayBuffer = await response.arrayBuffer();
|
||||
|
||||
let runtimeConstructor;
|
||||
try {
|
||||
const { SwiftRuntime } = await import(
|
||||
"./JavaScriptKit_JavaScriptKit.resources/Runtime/index.mjs"
|
||||
);
|
||||
runtimeConstructor = SwiftRuntime;
|
||||
} catch {
|
||||
console.log(
|
||||
"JavaScriptKit module not available, running without JavaScriptKit runtime."
|
||||
);
|
||||
}
|
||||
|
||||
const wasmRunner = WasmRunner(
|
||||
{
|
||||
onStderr() {
|
||||
const prevLimit = Error.stackTraceLimit;
|
||||
Error.stackTraceLimit = 1000;
|
||||
socket.send(
|
||||
JSON.stringify({
|
||||
kind: "stackTrace",
|
||||
stackTrace: new Error().stack,
|
||||
})
|
||||
);
|
||||
Error.stackTraceLimit = prevLimit;
|
||||
},
|
||||
},
|
||||
runtimeConstructor
|
||||
);
|
||||
|
||||
// Instantiate the WebAssembly file
|
||||
const wasmBytes = new Uint8Array(responseArrayBuffer).buffer;
|
||||
await wasmRunner.run(wasmBytes, {
|
||||
__stack_sanitizer: {
|
||||
report_stack_overflow: () => {
|
||||
throw new Error("Detected stack-buffer-overflow.")
|
||||
}
|
||||
}
|
||||
})
|
||||
await wasmRunner.run(wasmBytes);
|
||||
};
|
||||
|
||||
function handleError(e) {
|
||||
|
|
|
@ -1,11 +0,0 @@
|
|||
const path = require("path");
|
||||
const outputPath = path.resolve(__dirname, "../static");
|
||||
|
||||
module.exports = {
|
||||
entry: "./entrypoint/test.js",
|
||||
mode: "development",
|
||||
output: {
|
||||
filename: "test.js",
|
||||
path: outputPath,
|
||||
},
|
||||
};
|
|
@ -14,7 +14,7 @@
|
|||
|
||||
import ReconnectingWebSocket from "reconnecting-websocket";
|
||||
import { WASIExitError } from "@wasmer/wasi";
|
||||
import { WasmRunner } from "./common";
|
||||
import { WasmRunner } from "./common.js";
|
||||
|
||||
const socket = new ReconnectingWebSocket(`ws://${location.host}/watcher`);
|
||||
socket.addEventListener("message", (message) => {
|
||||
|
@ -23,38 +23,46 @@ socket.addEventListener("message", (message) => {
|
|||
}
|
||||
});
|
||||
|
||||
let testRunOutput = "";
|
||||
const wasmRunner = WasmRunner({
|
||||
onStdout: (text) => {
|
||||
testRunOutput += text + "\n";
|
||||
},
|
||||
onStderr: () => {
|
||||
socket.send(
|
||||
JSON.stringify({
|
||||
kind: "stackTrace",
|
||||
stackTrace: new Error().stack,
|
||||
})
|
||||
);
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
const startWasiTask = async () => {
|
||||
// Fetch our Wasm File
|
||||
const response = await fetch("/main.wasm");
|
||||
const responseArrayBuffer = await response.arrayBuffer();
|
||||
|
||||
let runtimeConstructor;
|
||||
try {
|
||||
const { SwiftRuntime } = await import(
|
||||
"./JavaScriptKit_JavaScriptKit.resources/Runtime/index.mjs"
|
||||
);
|
||||
runtimeConstructor = SwiftRuntime;
|
||||
} catch {
|
||||
console.log(
|
||||
"JavaScriptKit module not available, running without JavaScriptKit runtime."
|
||||
);
|
||||
}
|
||||
|
||||
let testRunOutput = "";
|
||||
const wasmRunner = WasmRunner(
|
||||
{
|
||||
onStdout: (text) => {
|
||||
testRunOutput += text + "\n";
|
||||
},
|
||||
onStderr: () => {
|
||||
socket.send(
|
||||
JSON.stringify({
|
||||
kind: "stackTrace",
|
||||
stackTrace: new Error().stack,
|
||||
})
|
||||
);
|
||||
},
|
||||
},
|
||||
runtimeConstructor
|
||||
);
|
||||
|
||||
// Instantiate the WebAssembly file
|
||||
const wasmBytes = new Uint8Array(responseArrayBuffer).buffer;
|
||||
// Start the WebAssembly WASI instance
|
||||
try {
|
||||
await wasmRunner.run(wasmBytes, {
|
||||
__stack_sanitizer: {
|
||||
report_stack_overflow: () => {
|
||||
throw new Error("Detected stack-buffer-overflow.");
|
||||
},
|
||||
},
|
||||
});
|
||||
await wasmRunner.run(wasmBytes);
|
||||
} catch (error) {
|
||||
if (!(error instanceof WASIExitError) || error.code != 0) {
|
||||
throw error; // not a successful test run, rethrow
|
||||
|
|
|
@ -1,20 +0,0 @@
|
|||
const path = require("path");
|
||||
const outputPath = path.resolve(__dirname, "../static");
|
||||
|
||||
module.exports = {
|
||||
entry: "./entrypoint/testNode.js",
|
||||
mode: "development",
|
||||
target: "node",
|
||||
output: {
|
||||
filename: "testNode.js",
|
||||
path: outputPath,
|
||||
libraryTarget: "commonjs"
|
||||
},
|
||||
node: {
|
||||
__dirname: false,
|
||||
__filename: false,
|
||||
},
|
||||
resolve: {
|
||||
mainFields: ["main", "module"],
|
||||
},
|
||||
};
|
|
@ -1,4 +1,4 @@
|
|||
// Copyright 2020 Carton contributors
|
||||
// Copyright 2022 Carton contributors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
|
@ -13,7 +13,7 @@
|
|||
// limitations under the License.
|
||||
|
||||
import fs from "fs/promises";
|
||||
import { WasmRunner } from "./common";
|
||||
import { WasmRunner } from "./common.js";
|
||||
|
||||
const args = [...process.argv];
|
||||
args.shift();
|
||||
|
@ -24,18 +24,27 @@ if (!wasmFile) {
|
|||
throw Error("No WASM test file specified, can not run tests");
|
||||
}
|
||||
|
||||
const wasmRunner = WasmRunner({ args: testArgs });
|
||||
|
||||
const startWasiTask = async () => {
|
||||
const wasmBytes = await fs.readFile(wasmFile);
|
||||
|
||||
await wasmRunner.run(wasmBytes, {
|
||||
__stack_sanitizer: {
|
||||
report_stack_overflow: () => {
|
||||
throw new Error("Detected stack-buffer-overflow.");
|
||||
},
|
||||
},
|
||||
});
|
||||
let runtimeConstructor;
|
||||
try {
|
||||
const { SwiftRuntime } = await import(
|
||||
"./JavaScriptKit_JavaScriptKit.resources/Runtime/index.mjs"
|
||||
);
|
||||
|
||||
runtimeConstructor = SwiftRuntime;
|
||||
|
||||
// Make `require` function available in the Swift environment. By default it's only available in the local scope,
|
||||
// but not on the `global` object.
|
||||
global.require = require;
|
||||
} catch {
|
||||
// No JavaScriptKit module found, run the Wasm module without JSKit
|
||||
}
|
||||
|
||||
const wasmRunner = WasmRunner({ args: testArgs }, runtimeConstructor);
|
||||
|
||||
await wasmRunner.run(wasmBytes);
|
||||
};
|
||||
|
||||
startWasiTask().catch((e) => {
|
||||
|
|
File diff suppressed because it is too large
Load Diff
13
package.json
13
package.json
|
@ -3,13 +3,6 @@
|
|||
"version": "0.14.2",
|
||||
"description": "📦 Watcher, bundler, and test runner for your SwiftWasm apps ",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
"dev": "webpack --config entrypoint/dev.config.js",
|
||||
"bundle": "webpack --config entrypoint/bundle.config.js",
|
||||
"test": "webpack --config entrypoint/test.config.js",
|
||||
"testNode": "webpack --config entrypoint/testNode.config.js",
|
||||
"build": "run-p dev bundle test"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/swiftwasm/carton.git"
|
||||
|
@ -29,10 +22,8 @@
|
|||
"devDependencies": {
|
||||
"@wasmer/wasi": "^0.12.0",
|
||||
"@wasmer/wasmfs": "^0.12.0",
|
||||
"javascript-kit-swift": "^0.14.0",
|
||||
"esbuild": "^0.14.38",
|
||||
"npm-run-all": "^4.1.5",
|
||||
"reconnecting-websocket": "^4.4.0",
|
||||
"webpack": "^5.64.2",
|
||||
"webpack-cli": "^4.9.1"
|
||||
"reconnecting-websocket": "^4.4.0"
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue