Implement rebuilds and reloads for `carton dev`
This commit is contained in:
parent
28a0658269
commit
e6aec262b3
|
@ -21,30 +21,63 @@ struct BuilderError: Error, CustomStringConvertible {
|
|||
let description: String
|
||||
}
|
||||
|
||||
struct Builder {
|
||||
final class Builder {
|
||||
let publisher: AnyPublisher<String, Error>
|
||||
|
||||
private let process: TSCBasic.Process
|
||||
|
||||
init(_ arguments: [String]) {
|
||||
init(_ arguments: [String], _ terminal: TerminalController) {
|
||||
let subject = PassthroughSubject<String, Error>()
|
||||
publisher = subject.eraseToAnyPublisher()
|
||||
publisher = subject
|
||||
.handleEvents(receiveOutput: {
|
||||
terminal.write($0)
|
||||
}, receiveCompletion: {
|
||||
switch $0 {
|
||||
case .finished:
|
||||
terminal.write("Build completed successfully\n\n", inColor: .green, bold: false)
|
||||
case let .failure(error):
|
||||
let errorString = String(describing: error)
|
||||
if errorString.isEmpty {
|
||||
terminal.write("Build failed, check the build process output above.\n", inColor: .red)
|
||||
} else {
|
||||
terminal.write("Build failed and produced following output: \n", inColor: .red)
|
||||
print(error)
|
||||
}
|
||||
}
|
||||
})
|
||||
.eraseToAnyPublisher()
|
||||
|
||||
let stdout: TSCBasic.Process.OutputClosure = {
|
||||
guard let string = String(data: Data($0), encoding: .utf8) else { return }
|
||||
subject.send(string)
|
||||
DispatchQueue.global().async {
|
||||
let stdout: TSCBasic.Process.OutputClosure = {
|
||||
guard let string = String(data: Data($0), encoding: .utf8) else { return }
|
||||
subject.send(string)
|
||||
}
|
||||
|
||||
var stderrBuffer = [UInt8]()
|
||||
|
||||
let stderr: TSCBasic.Process.OutputClosure = {
|
||||
stderrBuffer.append(contentsOf: $0)
|
||||
}
|
||||
|
||||
let process = Process(
|
||||
arguments: arguments,
|
||||
outputRedirection: .stream(stdout: stdout, stderr: stderr),
|
||||
verbose: true,
|
||||
startNewProcessGroup: true
|
||||
)
|
||||
|
||||
let result = Result<ProcessResult, Error> {
|
||||
try process.launch()
|
||||
return try process.waitUntilExit()
|
||||
}
|
||||
|
||||
switch result.map(\.exitStatus) {
|
||||
case .success(.terminated(code: 0)):
|
||||
subject.send(completion: .finished)
|
||||
case let .failure(error):
|
||||
subject.send(completion: .failure(error))
|
||||
default:
|
||||
let errorDescription = String(data: Data(stderrBuffer), encoding: .utf8) ?? ""
|
||||
return subject.send(completion: .failure(BuilderError(description: errorDescription)))
|
||||
}
|
||||
}
|
||||
|
||||
let stderr: TSCBasic.Process.OutputClosure = {
|
||||
guard let string = String(data: Data($0), encoding: .utf8) else { return }
|
||||
subject.send(completion: .failure(BuilderError(description: string)))
|
||||
}
|
||||
|
||||
process = Process(
|
||||
arguments: arguments,
|
||||
outputRedirection: .stream(stdout: stdout, stderr: stderr),
|
||||
verbose: true,
|
||||
startNewProcessGroup: true
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,332 @@
|
|||
// MIT License
|
||||
|
||||
// Copyright (c) 2019 Sergej Jaskiewicz
|
||||
|
||||
// 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.
|
||||
|
||||
// ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
|
||||
// ┃ ┃
|
||||
// ┃ Auto-generated from GYB template. DO NOT EDIT! ┃
|
||||
// ┃ ┃
|
||||
// ┃ ┃
|
||||
// ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
|
||||
//
|
||||
// Publishers.Catch.swift
|
||||
//
|
||||
//
|
||||
// Created by Sergej Jaskiewicz on 25.12.2019.
|
||||
//
|
||||
|
||||
import COpenCombineHelpers
|
||||
import OpenCombine
|
||||
|
||||
extension Publisher {
|
||||
/// Handles errors from an upstream publisher by replacing it with another publisher.
|
||||
///
|
||||
/// The following example replaces any error from the upstream publisher and replaces
|
||||
/// the upstream with a `Just` publisher. This continues the stream by publishing
|
||||
/// a single value and completing normally.
|
||||
/// ```
|
||||
/// enum SimpleError: Error { case error }
|
||||
/// let errorPublisher = (0..<10).publisher.tryMap { v -> Int in
|
||||
/// if v < 5 {
|
||||
/// return v
|
||||
/// } else {
|
||||
/// throw SimpleError.error
|
||||
/// }
|
||||
/// }
|
||||
///
|
||||
/// let noErrorPublisher = errorPublisher.catch { _ in
|
||||
/// return Just(100)
|
||||
/// }
|
||||
/// ```
|
||||
/// Backpressure note: This publisher passes through `request` and `cancel` to
|
||||
/// the upstream. After receiving an error, the publisher sends sends any unfulfilled
|
||||
/// demand to the new `Publisher`.
|
||||
///
|
||||
/// - Parameter handler: A closure that accepts the upstream failure as input and
|
||||
/// returns a publisher to replace the upstream publisher.
|
||||
/// - Returns: A publisher that handles errors from an upstream publisher by replacing
|
||||
/// the failed publisher with another publisher.
|
||||
public func `catch`<NewPublisher: Publisher>(
|
||||
_ handler: @escaping (Failure) -> NewPublisher
|
||||
) -> Publishers.Catch<Self, NewPublisher>
|
||||
where NewPublisher.Output == Output {
|
||||
return .init(upstream: self, handler: handler)
|
||||
}
|
||||
}
|
||||
|
||||
extension Publishers {
|
||||
/// A publisher that handles errors from an upstream publisher by replacing the failed
|
||||
/// publisher with another publisher.
|
||||
public struct Catch<Upstream: Publisher, NewPublisher: Publisher>: Publisher
|
||||
where Upstream.Output == NewPublisher.Output {
|
||||
public typealias Output = Upstream.Output
|
||||
|
||||
public typealias Failure = NewPublisher.Failure
|
||||
|
||||
/// The publisher that this publisher receives elements from.
|
||||
public let upstream: Upstream
|
||||
|
||||
/// A closure that accepts the upstream failure as input and returns a publisher
|
||||
/// to replace the upstream publisher.
|
||||
public let handler: (Upstream.Failure) -> NewPublisher
|
||||
|
||||
/// Creates a publisher that handles errors from an upstream publisher by
|
||||
/// replacing the failed publisher with another publisher.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - upstream: The publisher that this publisher receives elements from.
|
||||
/// - handler: A closure that accepts the upstream failure as input and returns
|
||||
/// a publisher to replace the upstream publisher.
|
||||
public init(upstream: Upstream,
|
||||
handler: @escaping (Upstream.Failure) -> NewPublisher) {
|
||||
self.upstream = upstream
|
||||
self.handler = handler
|
||||
}
|
||||
|
||||
public func receive<Downstream: Subscriber>(subscriber: Downstream)
|
||||
where Downstream.Input == Output, Downstream.Failure == Failure {
|
||||
let inner = Inner(downstream: subscriber, handler: handler)
|
||||
let uncaughtS = Inner.UncaughtS(inner: inner)
|
||||
upstream.subscribe(uncaughtS)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension Publishers.Catch {
|
||||
private final class Inner<Downstream: Subscriber>:
|
||||
Subscription,
|
||||
CustomStringConvertible,
|
||||
CustomReflectable,
|
||||
CustomPlaygroundDisplayConvertible
|
||||
where Downstream.Input == Upstream.Output,
|
||||
Downstream.Failure == NewPublisher.Failure {
|
||||
struct UncaughtS: Subscriber,
|
||||
CustomStringConvertible,
|
||||
CustomReflectable,
|
||||
CustomPlaygroundDisplayConvertible {
|
||||
typealias Input = Upstream.Output
|
||||
|
||||
typealias Failure = Upstream.Failure
|
||||
|
||||
let inner: Inner
|
||||
|
||||
var combineIdentifier: CombineIdentifier { inner.combineIdentifier }
|
||||
|
||||
func receive(subscription: Subscription) {
|
||||
inner.receivePre(subscription: subscription)
|
||||
}
|
||||
|
||||
func receive(_ input: Input) -> Subscribers.Demand {
|
||||
inner.receivePre(input)
|
||||
}
|
||||
|
||||
func receive(completion: Subscribers.Completion<Failure>) {
|
||||
inner.receivePre(completion: completion)
|
||||
}
|
||||
|
||||
var description: String { inner.description }
|
||||
|
||||
var customMirror: Mirror { inner.customMirror }
|
||||
|
||||
var playgroundDescription: Any { description }
|
||||
}
|
||||
|
||||
struct CaughtS: Subscriber,
|
||||
CustomStringConvertible,
|
||||
CustomReflectable,
|
||||
CustomPlaygroundDisplayConvertible {
|
||||
typealias Input = NewPublisher.Output
|
||||
|
||||
typealias Failure = NewPublisher.Failure
|
||||
|
||||
let inner: Inner
|
||||
|
||||
var combineIdentifier: CombineIdentifier { inner.combineIdentifier }
|
||||
|
||||
func receive(subscription: Subscription) {
|
||||
inner.receivePost(subscription: subscription)
|
||||
}
|
||||
|
||||
func receive(_ input: Input) -> Subscribers.Demand {
|
||||
inner.receivePost(input)
|
||||
}
|
||||
|
||||
func receive(completion: Subscribers.Completion<Failure>) {
|
||||
inner.receivePost(completion: completion)
|
||||
}
|
||||
|
||||
var description: String { inner.description }
|
||||
|
||||
var customMirror: Mirror { inner.customMirror }
|
||||
|
||||
var playgroundDescription: Any { description }
|
||||
}
|
||||
|
||||
private enum State {
|
||||
case pendingPre
|
||||
case pre(Subscription)
|
||||
case pendingPost
|
||||
case post(Subscription)
|
||||
case cancelled
|
||||
}
|
||||
|
||||
private let lock = __UnfairLock.allocate() // 0x10
|
||||
private var demand = Subscribers.Demand.none // 0x18
|
||||
private var state = State.pendingPre // 0x20
|
||||
private let downstream: Downstream
|
||||
|
||||
private let handler: (Upstream.Failure) -> NewPublisher
|
||||
|
||||
init(downstream: Downstream,
|
||||
handler: @escaping (Upstream.Failure) -> NewPublisher) {
|
||||
self.downstream = downstream
|
||||
self.handler = handler
|
||||
}
|
||||
|
||||
deinit {
|
||||
lock.deallocate()
|
||||
}
|
||||
|
||||
func receivePre(subscription: Subscription) {
|
||||
lock.lock()
|
||||
guard case .pendingPre = state else {
|
||||
lock.unlock()
|
||||
subscription.cancel()
|
||||
return
|
||||
}
|
||||
state = .pre(subscription)
|
||||
lock.unlock()
|
||||
downstream.receive(subscription: self)
|
||||
}
|
||||
|
||||
func receivePre(_ input: Upstream.Output) -> Subscribers.Demand {
|
||||
lock.lock()
|
||||
demand -= 1
|
||||
lock.unlock()
|
||||
let newDemand = downstream.receive(input)
|
||||
lock.lock()
|
||||
demand += newDemand
|
||||
lock.unlock()
|
||||
return newDemand
|
||||
}
|
||||
|
||||
func receivePre(completion: Subscribers.Completion<Upstream.Failure>) {
|
||||
switch completion {
|
||||
case .finished:
|
||||
lock.lock()
|
||||
if case .pre = state {
|
||||
state = .cancelled
|
||||
lock.unlock()
|
||||
downstream.receive(completion: .finished)
|
||||
} else {
|
||||
lock.unlock()
|
||||
}
|
||||
case let .failure(error):
|
||||
lock.lock()
|
||||
if case .pre = state {
|
||||
state = .pendingPost
|
||||
lock.unlock()
|
||||
handler(error).subscribe(CaughtS(inner: self))
|
||||
} else {
|
||||
lock.unlock()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func receivePost(subscription: Subscription) {
|
||||
lock.lock()
|
||||
guard case .pendingPost = state else {
|
||||
lock.unlock()
|
||||
subscription.cancel()
|
||||
return
|
||||
}
|
||||
state = .post(subscription)
|
||||
let demand = self.demand
|
||||
lock.unlock()
|
||||
if demand > 0 {
|
||||
subscription.request(demand)
|
||||
}
|
||||
}
|
||||
|
||||
func receivePost(_ input: NewPublisher.Output) -> Subscribers.Demand {
|
||||
downstream.receive(input)
|
||||
}
|
||||
|
||||
func receivePost(completion: Subscribers.Completion<NewPublisher.Failure>) {
|
||||
lock.lock()
|
||||
guard case .post = state else {
|
||||
lock.unlock()
|
||||
return
|
||||
}
|
||||
state = .cancelled
|
||||
lock.unlock()
|
||||
downstream.receive(completion: completion)
|
||||
}
|
||||
|
||||
func request(_ demand: Subscribers.Demand) {
|
||||
if demand == .none {
|
||||
fatalError("API Violation: demand must not be zero")
|
||||
}
|
||||
lock.lock()
|
||||
switch state {
|
||||
case .pendingPre:
|
||||
lock.unlock()
|
||||
case let .pre(subscription):
|
||||
self.demand += demand
|
||||
lock.unlock()
|
||||
subscription.request(demand)
|
||||
case .pendingPost:
|
||||
self.demand += demand
|
||||
lock.unlock()
|
||||
case let .post(subscription):
|
||||
lock.unlock()
|
||||
subscription.request(demand)
|
||||
case .cancelled:
|
||||
lock.unlock()
|
||||
}
|
||||
}
|
||||
|
||||
func cancel() {
|
||||
lock.lock()
|
||||
switch state {
|
||||
case let .pre(subscription), let .post(subscription):
|
||||
state = .cancelled
|
||||
lock.unlock()
|
||||
subscription.cancel()
|
||||
default:
|
||||
state = .cancelled
|
||||
lock.unlock()
|
||||
}
|
||||
}
|
||||
|
||||
var description: String { "Catch" }
|
||||
|
||||
var customMirror: Mirror {
|
||||
let children: [Mirror.Child] = [
|
||||
("downstream", downstream),
|
||||
("demand", demand),
|
||||
]
|
||||
return Mirror(self, children: children)
|
||||
}
|
||||
|
||||
var playgroundDescription: Any { description }
|
||||
}
|
||||
}
|
|
@ -31,7 +31,7 @@ extension Publisher where Output == Progress, Failure: CustomStringConvertible {
|
|||
switch $0 {
|
||||
case .finished:
|
||||
progressAnimation.complete(success: true)
|
||||
terminal.write("Build finished succesfully\n", inColor: .green, bold: false)
|
||||
terminal.write("Build finished succesfully\n", inColor: .green)
|
||||
case let .failure(error):
|
||||
progressAnimation.complete(success: false)
|
||||
terminal.write(error.description, inColor: .red, bold: false)
|
||||
|
|
|
@ -14,6 +14,7 @@
|
|||
|
||||
import ArgumentParser
|
||||
import Foundation
|
||||
import OpenCombine
|
||||
import TSCBasic
|
||||
|
||||
func processDataOutput(_ arguments: [String]) throws -> Data {
|
||||
|
@ -41,15 +42,17 @@ struct Dev: ParsableCommand {
|
|||
else { fatalError("failed to create an instance of `TerminalController`") }
|
||||
// try checkDevDependencies(on: localFileSystem, terminal)
|
||||
|
||||
let fm = FileManager.default
|
||||
|
||||
terminal.write("Inferring basic settings...\n", inColor: .yellow)
|
||||
let swiftPath: String
|
||||
if
|
||||
let data = FileManager.default.contents(atPath: ".swift-version"),
|
||||
let data = fm.contents(atPath: ".swift-version"),
|
||||
// get the first line of the file
|
||||
let swiftVersion = String(data: data, encoding: .utf8)?.components(
|
||||
separatedBy: CharacterSet.newlines
|
||||
).first {
|
||||
swiftPath = FileManager.default.homeDirectoryForCurrentUser
|
||||
swiftPath = fm.homeDirectoryForCurrentUser
|
||||
.appending(".swiftenv", "versions", swiftVersion, "usr", "bin", "swift")
|
||||
.path
|
||||
} else {
|
||||
|
@ -89,13 +92,33 @@ struct Dev: ParsableCommand {
|
|||
guard let sources = localFileSystem.currentWorkingDirectory?.appending(component: "Sources")
|
||||
else { fatalError("failed to infer the sources directory") }
|
||||
|
||||
terminal.write("\nWatching this directory for changes: ", inColor: .green, bold: false)
|
||||
terminal.write("\nBuilding the project before spinning up a server...\n", inColor: .yellow)
|
||||
|
||||
let builderArguments = [swiftPath, "build", "--triple", "wasm32-unknown-wasi"]
|
||||
var subscription: AnyCancellable?
|
||||
try await { completion in
|
||||
subscription = Builder(builderArguments, terminal).publisher
|
||||
.sink(
|
||||
receiveCompletion: { _ in completion(Result<(), Never>.success(())) },
|
||||
receiveValue: { _ in }
|
||||
)
|
||||
}
|
||||
|
||||
guard fm.fileExists(atPath: mainWasmURL.path) else {
|
||||
return terminal.write(
|
||||
"Failed to build the main executable binary, fix the build errors and restart\n"
|
||||
)
|
||||
}
|
||||
|
||||
terminal.write("\nWatching this directory for changes: ", inColor: .green)
|
||||
terminal.logLookup("", sources)
|
||||
terminal.write("\n")
|
||||
|
||||
try Server(
|
||||
builderArguments: builderArguments,
|
||||
pathsToWatch: localFileSystem.traverseRecursively(sources),
|
||||
mainWasmPath: mainWasmURL.path
|
||||
mainWasmPath: mainWasmURL.path,
|
||||
terminal
|
||||
).run()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -22,9 +22,15 @@ final class Server {
|
|||
private var wsConnection: WebSocket?
|
||||
private var subscriptions = [AnyCancellable]()
|
||||
private let watcher: Watcher
|
||||
private var builder: Builder?
|
||||
private let app: Application
|
||||
|
||||
init(pathsToWatch: [AbsolutePath], mainWasmPath: String) throws {
|
||||
init(
|
||||
builderArguments: [String],
|
||||
pathsToWatch: [AbsolutePath],
|
||||
mainWasmPath: String,
|
||||
_ terminal: TerminalController
|
||||
) throws {
|
||||
watcher = try Watcher(pathsToWatch)
|
||||
|
||||
var env = Environment.development
|
||||
|
@ -35,9 +41,24 @@ final class Server {
|
|||
}
|
||||
|
||||
watcher.publisher
|
||||
.sink { [weak self] _ in
|
||||
self?.wsConnection?.send("reload")
|
||||
.flatMap(maxPublishers: .max(1)) { changes -> AnyPublisher<String, Never> in
|
||||
terminal.write("\nThese paths have changed, rebuilding...\n", inColor: .yellow)
|
||||
for change in changes.map(\.pathString) {
|
||||
terminal.write("- \(change)\n", inColor: .cyan)
|
||||
}
|
||||
terminal.write("\n")
|
||||
return Builder(builderArguments, terminal)
|
||||
.publisher
|
||||
.handleEvents(receiveCompletion: { [weak self] in
|
||||
guard case .finished = $0 else { return }
|
||||
self?.wsConnection?.send("reload")
|
||||
})
|
||||
.catch { _ in Empty().eraseToAnyPublisher() }
|
||||
.eraseToAnyPublisher()
|
||||
}
|
||||
.sink(
|
||||
receiveValue: { _ in }
|
||||
)
|
||||
.store(in: &subscriptions)
|
||||
}
|
||||
|
||||
|
|
|
@ -22,4 +22,4 @@ divElement.innerText = "Hello, world"
|
|||
let body = document.body.object!
|
||||
_ = body.appendChild!(divElement)
|
||||
|
||||
print("hello, world")
|
||||
print("Hello, world!")
|
||||
|
|
|
@ -16,7 +16,11 @@ const wasi = new WASI({
|
|||
|
||||
const socket = new WebSocket("ws://127.0.0.1:8080/watcher");
|
||||
|
||||
socket.onmessage = (message) => console.log(message.data);
|
||||
socket.onmessage = (message) => {
|
||||
if (message.data === "reload") {
|
||||
location.reload();
|
||||
}
|
||||
};
|
||||
|
||||
const startWasiTask = async () => {
|
||||
// Fetch our Wasm File
|
||||
|
|
Loading…
Reference in New Issue