Serve main bundle resources from root directory (#176)

This is required to make the `Image` view work as described in https://github.com/TokamakUI/Tokamak/pull/155#issuecomment-723677472.

* Demangle and print Firefox stacktraces in terminal

* Update the entrypoints archive URL, rename file

* Add missing newline to `ProcessRunner.swift`

* Remove redundant `console.log` call from `dev.js`

* Detect destination env from `User-Agent` header

* Silence linter in tests where it can't be avoided

* Add support for basic testing in browsers

* Add a browser message on finish, shut down server

* Serve main bundle resources from root directory

* Fix archive hashes, bump JSKit in TestApp to 0.9.0

* Add comments to clarify behavior

* Fix typo in doc comment in `Package.swift`
This commit is contained in:
Max Desiatov 2020-12-01 16:20:46 +00:00 committed by GitHub
parent ef402e71e8
commit 09a4e67e9e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 73 additions and 26 deletions

View File

@ -66,6 +66,11 @@ struct ProductType: Codable {
public struct Product: Codable {
let name: String
let type: ProductType
/** List of names of targets that this product is composed of. Can be used to infer actual
target descriptions used in this product.
*/
public let targets: [String]
}
public enum TargetType: String, Codable {

View File

@ -114,26 +114,25 @@ public final class Toolchain {
return AbsolutePath(binPath)
}
private func inferDevProduct(hint: String?) throws -> String? {
private func inferDevProduct(hint: String?) throws -> Product? {
let package = try self.package.get()
var candidateProducts = package.products
.filter { $0.type.library == nil }
.map(\.name)
if let product = hint {
candidateProducts = candidateProducts.filter { $0 == product }
if let productName = hint {
candidateProducts = candidateProducts.filter { $0.name == productName }
guard candidateProducts.count == 1 else {
terminal.write("""
Failed to disambiguate the executable product, \
make sure `\(product)` product is present in Package.swift
make sure `\(productName)` product is present in Package.swift
""", inColor: .red)
return nil
}
terminal.logLookup("- development product: ", product)
return product
terminal.logLookup("- development product: ", productName)
return candidateProducts[0]
} else if candidateProducts.count == 1 {
return candidateProducts[0]
} else {
@ -194,7 +193,7 @@ public final class Toolchain {
public func buildCurrentProject(
product: String?,
isRelease: Bool
) throws -> (builderArguments: [String], mainWasmPath: AbsolutePath) {
) throws -> (builderArguments: [String], mainWasmPath: AbsolutePath, Product) {
guard let product = try inferDevProduct(hint: product)
else { throw ToolchainError.noExecutableProduct }
@ -217,14 +216,14 @@ public final class Toolchain {
}
let binPath = try inferBinPath(isRelease: isRelease)
let mainWasmPath = binPath.appending(component: "\(product).wasm")
let mainWasmPath = binPath.appending(component: "\(product.name).wasm")
terminal.logLookup("- development binary to serve: ", mainWasmPath.pathString)
terminal.write("\nBuilding the project before spinning up a server...\n", inColor: .yellow)
let builderArguments = [
swiftPath.pathString, "build", "-c", isRelease ? "release" : "debug", "--product", product,
"--enable-test-discovery", "--triple", "wasm32-unknown-wasi",
swiftPath.pathString, "build", "-c", isRelease ? "release" : "debug",
"--product", product.name, "--enable-test-discovery", "--triple", "wasm32-unknown-wasi",
]
try Builder(arguments: builderArguments, mainWasmPath: mainWasmPath, fileSystem, terminal)
@ -235,10 +234,10 @@ public final class Toolchain {
"Failed to build the main executable binary, fix the build errors and restart\n",
inColor: .red
)
throw ToolchainError.failedToBuild(product: product)
throw ToolchainError.failedToBuild(product: product.name)
}
return (builderArguments, mainWasmPath)
return (builderArguments, mainWasmPath, product)
}
/// Returns an absolute path to the resulting test bundle

View File

@ -48,7 +48,7 @@ struct Bundle: ParsableCommand {
let toolchain = try Toolchain(localFileSystem, terminal)
let (_, mainWasmPath) = try toolchain.buildCurrentProject(
let (_, mainWasmPath, inferredProduct) = try toolchain.buildCurrentProject(
product: product,
isRelease: !debug
)
@ -85,7 +85,8 @@ struct Bundle: ParsableCommand {
optimizedPath: optimizedPath,
buildDirectory: mainWasmPath.parentDirectory,
bundleDirectory: bundleDirectory,
toolchain: toolchain
toolchain: toolchain,
product: inferredProduct
)
terminal.write("Bundle generation finished successfully\n", inColor: .green, bold: true)
@ -102,7 +103,8 @@ struct Bundle: ParsableCommand {
optimizedPath: AbsolutePath,
buildDirectory: AbsolutePath,
bundleDirectory: AbsolutePath,
toolchain: Toolchain
toolchain: Toolchain,
product: Product
) throws {
// Rename the final binary to use a part of its hash to bust browsers and CDN caches.
let optimizedHash = try localFileSystem.readFileContents(optimizedPath).hexSHA256.prefix(16)
@ -144,5 +146,29 @@ struct Bundle: ParsableCommand {
terminal.logLookup("Copying resources to ", targetDirectory)
try localFileSystem.copy(from: resourcesPath, to: targetDirectory)
}
/* While a product may be composed of multiple targets, not sure this is widely used in
practice. Just assuming here that the first target of this product is an executable target,
at least until SwiftPM allows specifying executable targets explicitly, as proposed in
swiftlint:disable:next line_length
https://forums.swift.org/t/pitch-ability-to-declare-executable-targets-in-swiftpm-manifests-to-support-main/41968
*/
let inferredMainTarget = package.targets.first {
product.targets.contains($0.name)
}
guard let mainTarget = inferredMainTarget else { return }
let targetPath = package.resourcesPath(for: mainTarget)
let resourcesPath = buildDirectory.appending(component: targetPath)
for file in try localFileSystem.traverseRecursively(resourcesPath) {
let targetPath = bundleDirectory.appending(component: file.basename)
guard localFileSystem.exists(resourcesPath) && !localFileSystem.exists(targetPath)
else { continue }
terminal.logLookup("Copying this resource to the root bundle directory ", file)
try localFileSystem.copy(from: file, to: targetPath)
}
}
}

View File

@ -78,7 +78,7 @@ struct Dev: ParsableCommand {
)
}
let (arguments, mainWasmPath) = try toolchain.buildCurrentProject(
let (arguments, mainWasmPath, inferredProduct) = try toolchain.buildCurrentProject(
product: product,
isRelease: release
)
@ -110,6 +110,7 @@ struct Dev: ParsableCommand {
customIndexContent: HTML.readCustomIndexPage(at: customIndexPage, on: localFileSystem),
// swiftlint:disable:next force_try
package: try! toolchain.package.get(),
product: inferredProduct,
entrypoint: Self.entrypoint
),
terminal

View File

@ -50,12 +50,7 @@ struct Test: ParsableCommand {
@Argument(help: "The list of test cases to run in the test suite.")
var testCases = [String]()
@Option(
help: """
Environment used to run the tests, either a browser, or command-line Wasm host.
Possible values: `defaultBrowser` or `wasmer`.
"""
)
@Option(help: "Environment used to run the tests, either a browser, or command-line Wasm host.")
private var environment = Environment.wasmer
@Option(
@ -94,6 +89,7 @@ struct Test: ParsableCommand {
customIndexContent: nil,
// swiftlint:disable:next force_try
package: try! toolchain.package.get(),
product: nil,
entrypoint: Self.entrypoint
),
terminal

View File

@ -23,6 +23,7 @@ extension Application {
let mainWasmPath: AbsolutePath
let customIndexContent: String?
let package: SwiftToolchain.Package
let product: Product?
let entrypoint: Entrypoint
let onWebSocketOpen: (WebSocket, DestinationEnvironment) -> ()
let onWebSocketClose: (WebSocket) -> ()
@ -71,5 +72,19 @@ extension Application {
).pathString))
}
}
let inferredMainTarget = configuration.package.targets.first {
configuration.product?.targets.contains($0.name) == true
}
guard let mainTarget = inferredMainTarget else { return }
let resourcesPath = configuration.package.resourcesPath(for: mainTarget)
get("**") {
$0.eventLoop.makeSucceededFuture($0.fileio.streamFile(at: AbsolutePath(
buildDirectory.appending(component: resourcesPath),
$0.parameters.getCatchall().joined(separator: "/")
).pathString))
}
}
}

View File

@ -83,6 +83,7 @@ final class Server {
let port: Int
let customIndexContent: String?
let package: SwiftToolchain.Package
let product: Product?
let entrypoint: Entrypoint
}
@ -111,6 +112,7 @@ final class Server {
mainWasmPath: configuration.mainWasmPath,
customIndexContent: configuration.customIndexContent,
package: configuration.package,
product: configuration.product,
entrypoint: configuration.entrypoint,
onWebSocketOpen: { [weak self] ws, environment in
ws.onText { _, text in

View File

@ -6,8 +6,8 @@
"repositoryURL": "https://github.com/swiftwasm/JavaScriptKit",
"state": {
"branch": null,
"revision": "8ba4135d5fd6a734c3771ef3fac66896bbcb0214",
"version": "0.8.0"
"revision": "b7a02434c6e973c08c3fd5069105ef44dd82b891",
"version": "0.9.0"
}
}
]

View File

@ -9,7 +9,7 @@ let package = Package(
.executable(name: "TestApp", targets: ["TestApp"]),
],
dependencies: [
.package(url: "https://github.com/swiftwasm/JavaScriptKit", from: "0.8.0"),
.package(url: "https://github.com/swiftwasm/JavaScriptKit", from: "0.9.0"),
],
targets: [
// Targets are the basic building blocks of a package. A target can define a module or a test

View File

@ -45,6 +45,9 @@ buttonNode.onclick = .function(handler)
var div = document.createElement("div")
div.innerHTML = .string(#"""
<a href=\#(Bundle.module.path(forResource: "data", ofType: "json")!)>Link to a static resource</a>
<br/>
<a href=\#(Bundle.main
.path(forResource: "data", ofType: "json")!)>Link to a <code>Bundle.main</code> resource</a>
"""#)
_ = document.body.appendChild(div)