Skip to content
Open
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions Package.resolved

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

8 changes: 7 additions & 1 deletion Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,13 @@ let package = Package(
name: "CollectionConcurrencyKit",
url: "https://github.com/johnsundell/collectionConcurrencyKit.git",
from: "0.1.0"
),
.package(
name: "FileWatcher",
url: "https://github.com/eonist/FileWatcher.git",
from: "0.2.3"
)

],
targets: [
.target(
Expand All @@ -66,7 +72,7 @@ let package = Package(
),
.target(
name: "PublishCLICore",
dependencies: ["Publish"]
dependencies: ["Publish", "FileWatcher"]
),
.testTarget(
name: "PublishTests",
Expand Down
32 changes: 20 additions & 12 deletions Sources/PublishCLICore/CLI.swift
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import Foundation
import ShellOut

public struct CLI {
private static let defaultPortNumber = 8000
private let arguments: [String]
private let publishRepositoryURL: URL
private let publishVersion: String
Expand Down Expand Up @@ -44,7 +45,8 @@ public struct CLI {
try deployer.deploy()
case "run":
let portNumber = extractPortNumber(from: arguments)
let runner = WebsiteRunner(folder: folder, portNumber: portNumber)
let shouldWatch = extractShouldWatch(from: arguments)
let runner = WebsiteRunner(folder: folder, portNumber: portNumber, shouldWatch: shouldWatch)
try runner.run()
default:
outputHelpText()
Expand All @@ -68,24 +70,30 @@ private extension CLI {
- run: Generate and run a localhost server on default port 8000
for the website in the current folder. Use the "-p"
or "--port" option for customizing the default port.
Use the "-w" or "--watch" option to watch for file changes.
- deploy: Generate and deploy the website in the current
folder, according to its deployment method.
""")
}

private func extractPortNumber(from arguments: [String]) -> Int {
if arguments.count > 3 {
switch arguments[2] {
case "-p", "--port":
guard let portNumber = Int(arguments[3]) else {
break
}
return portNumber
default:
return 8000 // default portNumber
}
guard let index = arguments.firstIndex(of: "-p") ?? arguments.firstIndex(of: "--port") else {
return Self.defaultPortNumber
}
return 8000 // default portNumber

guard arguments.count > index + 1 else {
return Self.defaultPortNumber
}

guard let portNumber = Int(arguments[index + 1]) else {
return Self.defaultPortNumber
}

return portNumber
}

private func extractShouldWatch(from arguments: [String]) -> Bool {
arguments.contains("-w") || arguments.contains("--watch")
}

private func resolveProjectKind(from arguments: [String]) -> ProjectKind {
Expand Down
130 changes: 119 additions & 11 deletions Sources/PublishCLICore/WebsiteRunner.swift
Original file line number Diff line number Diff line change
@@ -1,20 +1,117 @@
/**
* Publish
* Copyright (c) John Sundell 2019
* MIT license, see LICENSE file for details
*/
* Publish
* Copyright (c) John Sundell 2019
* MIT license, see LICENSE file for details
*/

import Foundation
import Files
import ShellOut
import FileWatcher

internal struct WebsiteRunner {
static let normalTerminationStatus = 15
static let debounceDuration = 3 * NSEC_PER_SEC
static let runLoopInterval: TimeInterval = 0.1
static let exitMessage = "Press CTRL+C to stop the server and exit"
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was unable to get the folder watching to work while maintaining the Press ENTER to stop the server and exit behaviour - I replaced readLine() with a non-blocking implementation (checking standard input on each iteration of the while loop), but that interfered with the watching functionality for an unknown reason.

let folder: Folder
var portNumber: Int
let portNumber: Int
let shouldWatch: Bool

var foldersToWatch: [Folder] {
get throws {
try ["Sources", "Resources", "Content"].map(folder.subfolder(named:))
}
}

func run() throws {
let serverProcess: Process = try generateAndRun()

var watchTask: Task<Void, Error>?

if shouldWatch {
watchTask = Task.detached {
for try await _ in changes(on: try foldersToWatch, debouncedBy: Self.debounceDuration) {
print("Changes detected, regenerating...")
let generator = WebsiteGenerator(folder: folder)
do {
try generator.generate()
print(Self.exitMessage)
} catch {
outputErrorMessage("Regeneration failed")
}
}
}
}

let interruptHandler = registerInterruptHandler {
watchTask?.cancel()
serverProcess.terminate()
exit(0)
}

interruptHandler.resume()

while true {
RunLoop.main.run(until: Date(timeIntervalSinceNow: Self.runLoopInterval))
}
}
}

private extension WebsiteRunner {
func changes(on folders: [Folder], debouncedBy nanoseconds: UInt64?) -> AsyncThrowingStream<String, Error> {
.init { continuation in
let watcher = FileWatcher(folders.map(\.path))

var deferredTask: Task<Void, Error>?

watcher.callback = { event in
guard event.isFileChanged || event.isDirectoryChanged else {
return
}

guard let nanoseconds = nanoseconds else {
continuation.yield(event.path)
return
}

deferredTask?.cancel()

deferredTask = Task {
do {
try await Task.sleep(nanoseconds: nanoseconds)
continuation.yield(event.path)
} catch where !(error is CancellationError) {
continuation.finish()
}
}
}

watcher.start()

continuation.onTermination = { _ in
watcher.stop()
}
}
}


func registerInterruptHandler(_ handler: @escaping () -> Void) -> DispatchSourceSignal {
let interruptHandler = DispatchSource.makeSignalSource(signal: SIGINT, queue: .main)

signal(SIGINT, SIG_IGN)

interruptHandler.setEventHandler(handler: handler)
return interruptHandler
}

func generate() throws {
let generator = WebsiteGenerator(folder: folder)
try generator.generate()
}

func generateAndRun() throws -> Process {
try generate()

let outputFolder = try resolveOutputFolder()

Expand All @@ -24,7 +121,7 @@ internal struct WebsiteRunner {
print("""
🌍 Starting web server at http://localhost:\(portNumber)

Press ENTER to stop the server and exit
\(Self.exitMessage)
""")

serverQueue.async {
Expand All @@ -44,12 +141,9 @@ internal struct WebsiteRunner {
exit(1)
}

_ = readLine()
serverProcess.terminate()
return serverProcess
}
}

private extension WebsiteRunner {
func resolveOutputFolder() throws -> Folder {
do { return try folder.subfolder(named: "Output") }
catch { throw CLIError.outputFolderNotFound }
Expand All @@ -70,6 +164,20 @@ private extension WebsiteRunner {
"""
}

fputs("\n❌ Failed to start local web server:\n\(message)\n", stderr)
outputErrorMessage("Failed to start local web server:\n\(message)")
}

func outputErrorMessage(_ message: String) {
fputs("\n❌ \(message)\n", stderr)
}
}

private extension FileWatcherEvent {
var isFileChanged: Bool {
fileRenamed || fileRemoved || fileCreated || fileModified
}

var isDirectoryChanged: Bool {
dirRenamed || dirRemoved || dirCreated || dirModified
}
}