Skip to content

Commit 0128cc1

Browse files
Add Node.js harness
1 parent 33bf228 commit 0128cc1

File tree

4 files changed

+123
-26
lines changed

4 files changed

+123
-26
lines changed

Diff for: Plugins/PackageToJS/PackageToJS.swift

+84-19
Original file line numberDiff line numberDiff line change
@@ -62,13 +62,20 @@ struct PackageToJS: CommandPlugin {
6262

6363
struct TestOptions {
6464
/// Whether to only build tests, don't run them
65-
var buildOnly: Bool = false
65+
var buildOnly: Bool
66+
var listTests: Bool
67+
var testLibrary: String?
68+
var filter: [String]
69+
6670
var options: Options
6771

6872
static func parse(from extractor: inout ArgumentExtractor) -> TestOptions {
6973
let buildOnly = extractor.extractFlag(named: "build-only")
74+
let listTests = extractor.extractFlag(named: "list-tests")
75+
let testLibrary = extractor.extractOption(named: "test-library").last
76+
let filter = extractor.extractOption(named: "filter")
7077
let options = Options.parse(from: &extractor)
71-
return TestOptions(buildOnly: buildOnly != 0, options: options)
78+
return TestOptions(buildOnly: buildOnly != 0, listTests: listTests != 0, testLibrary: testLibrary, filter: filter, options: options)
7279
}
7380

7481
static func help() -> String {
@@ -84,7 +91,7 @@ struct PackageToJS: CommandPlugin {
8491
$ swift package --swift-sdk wasm32-unknown-wasi plugin js test
8592
# Just build tests, don't run them
8693
$ swift package --swift-sdk wasm32-unknown-wasi plugin js test --build-only
87-
"""
94+
"""
8895
}
8996
}
9097

@@ -129,7 +136,9 @@ struct PackageToJS: CommandPlugin {
129136
"""
130137
}),
131138
]
132-
static private func reportBuildFailure(_ build: PackageManager.BuildResult, _ arguments: [String]) {
139+
static private func reportBuildFailure(
140+
_ build: PackageManager.BuildResult, _ arguments: [String]
141+
) {
133142
for diagnostic in Self.friendlyBuildDiagnostics {
134143
if let message = diagnostic(build, arguments) {
135144
printStderr("\n" + message)
@@ -138,11 +147,6 @@ struct PackageToJS: CommandPlugin {
138147
}
139148

140149
func performCommand(context: PluginContext, arguments: [String]) throws {
141-
if arguments.contains(where: { ["-h", "--help"].contains($0) }) {
142-
printStderr(BuildOptions.help())
143-
return
144-
}
145-
146150
if arguments.first == "test" {
147151
return try performTestCommand(context: context, arguments: Array(arguments.dropFirst()))
148152
}
@@ -153,6 +157,11 @@ struct PackageToJS: CommandPlugin {
153157
static let JAVASCRIPTKIT_PACKAGE_ID: Package.ID = "javascriptkit"
154158

155159
func performBuildCommand(context: PluginContext, arguments: [String]) throws {
160+
if arguments.contains(where: { ["-h", "--help"].contains($0) }) {
161+
printStderr(BuildOptions.help())
162+
return
163+
}
164+
156165
var extractor = ArgumentExtractor(arguments)
157166
let buildOptions = BuildOptions.parse(from: &extractor)
158167

@@ -185,7 +194,8 @@ struct PackageToJS: CommandPlugin {
185194
}
186195
var make = MiniMake(explain: buildOptions.options.explain)
187196
let planner = PackagingPlanner(
188-
options: buildOptions.options, context: context, selfPackage: selfPackage, outputDir: outputDir)
197+
options: buildOptions.options, context: context, selfPackage: selfPackage,
198+
outputDir: outputDir)
189199
let rootTask = planner.planBuild(
190200
make: &make, wasmProductArtifact: productArtifact)
191201
cleanIfBuildGraphChanged(root: rootTask, make: make, context: context)
@@ -195,6 +205,11 @@ struct PackageToJS: CommandPlugin {
195205
}
196206

197207
func performTestCommand(context: PluginContext, arguments: [String]) throws {
208+
if arguments.contains(where: { ["-h", "--help"].contains($0) }) {
209+
printStderr(TestOptions.help())
210+
return
211+
}
212+
198213
var extractor = ArgumentExtractor(arguments)
199214
let testOptions = TestOptions.parse(from: &extractor)
200215

@@ -227,13 +242,15 @@ struct PackageToJS: CommandPlugin {
227242
}
228243
}
229244
guard let productArtifact = productArtifact else {
230-
throw PackageToJSError("Failed to find '\(productName).wasm' or '\(productName).xctest'")
231-
}
232-
let outputDir = if let outputPath = testOptions.options.outputPath {
233-
URL(fileURLWithPath: outputPath)
234-
} else {
235-
context.pluginWorkDirectoryURL.appending(path: "PackageTests")
245+
throw PackageToJSError(
246+
"Failed to find '\(productName).wasm' or '\(productName).xctest'")
236247
}
248+
let outputDir =
249+
if let outputPath = testOptions.options.outputPath {
250+
URL(fileURLWithPath: outputPath)
251+
} else {
252+
context.pluginWorkDirectoryURL.appending(path: "PackageTests")
253+
}
237254
guard
238255
let selfPackage = findPackageInDependencies(
239256
package: context.package, id: Self.JAVASCRIPTKIT_PACKAGE_ID)
@@ -242,16 +259,47 @@ struct PackageToJS: CommandPlugin {
242259
}
243260
var make = MiniMake(explain: testOptions.options.explain)
244261
let planner = PackagingPlanner(
245-
options: testOptions.options, context: context, selfPackage: selfPackage, outputDir: outputDir)
246-
let rootTask = planner.planTestBuild(
262+
options: testOptions.options, context: context, selfPackage: selfPackage,
263+
outputDir: outputDir)
264+
let (rootTask, binDir) = planner.planTestBuild(
247265
make: &make, wasmProductArtifact: productArtifact)
248266
cleanIfBuildGraphChanged(root: rootTask, make: make, context: context)
249267
print("Packaging tests...")
250268
try make.build(output: rootTask)
251269
print("Packaging tests finished")
270+
271+
let testRunner = binDir.appending(path: "test.js")
272+
if !testOptions.buildOnly {
273+
var extraArguments: [String] = []
274+
if testOptions.listTests {
275+
extraArguments += ["--list-tests"]
276+
}
277+
try runTest(testRunner: testRunner, context: context, extraArguments: extraArguments + testOptions.filter)
278+
try runTest(testRunner: testRunner, context: context,
279+
extraArguments: ["--testing-library", "swift-testing"] + extraArguments + testOptions.filter.flatMap { ["--filter", $0] })
280+
}
281+
}
282+
283+
private func runTest(testRunner: URL, context: PluginContext, extraArguments: [String]) throws {
284+
let node = try which("node")
285+
let arguments = ["--experimental-wasi-unstable-preview1", testRunner.path] + extraArguments
286+
print("Running test...")
287+
print("$ \(([node.path] + arguments).map { "\"\($0)\"" }.joined(separator: " "))")
288+
289+
let task = Process()
290+
task.executableURL = node
291+
task.arguments = arguments
292+
task.currentDirectoryURL = context.pluginWorkDirectoryURL
293+
try task.run()
294+
task.waitUntilExit()
295+
guard task.terminationStatus == 0 else {
296+
throw PackageToJSError("Test failed with status \(task.terminationStatus)")
297+
}
252298
}
253299

254-
private func buildWasm(productName: String, context: PluginContext) throws -> PackageManager.BuildResult {
300+
private func buildWasm(productName: String, context: PluginContext) throws
301+
-> PackageManager.BuildResult
302+
{
255303
var parameters = PackageManager.BuildParameters(
256304
configuration: .inherit,
257305
logging: .concise
@@ -357,6 +405,23 @@ private func printStderr(_ message: String) {
357405
fputs(message + "\n", stderr)
358406
}
359407

408+
private func which(_ executable: String) throws -> URL {
409+
let pathSeparator: Character
410+
#if os(Windows)
411+
pathSeparator = ";"
412+
#else
413+
pathSeparator = ":"
414+
#endif
415+
let paths = ProcessInfo.processInfo.environment["PATH"]!.split(separator: pathSeparator)
416+
for path in paths {
417+
let url = URL(fileURLWithPath: String(path)).appendingPathComponent(executable)
418+
if FileManager.default.isExecutableFile(atPath: url.path) {
419+
return url
420+
}
421+
}
422+
throw PackageToJSError("Executable \(executable) not found in PATH")
423+
}
424+
360425
private struct PackageToJSError: Swift.Error, CustomStringConvertible {
361426
let description: String
362427

Diff for: Plugins/PackageToJS/PackagingPlanner.swift

+3-2
Original file line numberDiff line numberDiff line change
@@ -110,7 +110,7 @@ struct PackagingPlanner {
110110
func planTestBuild(
111111
make: inout MiniMake,
112112
wasmProductArtifact: URL,
113-
) -> MiniMake.TaskKey {
113+
) -> (rootTask: MiniMake.TaskKey, binDir: URL) {
114114
var (allTasks, outputDirTask) = planBuildInternal(make: &make, wasmProductArtifact: wasmProductArtifact)
115115

116116
let binDir = outputDir.appending(path: "bin")
@@ -133,9 +133,10 @@ struct PackagingPlanner {
133133
inputs: [binDirTask]
134134
))
135135
}
136-
return make.addTask(
136+
let rootTask = make.addTask(
137137
inputTasks: allTasks, output: "all", attributes: [.phony, .silent]
138138
) { _ in }
139+
return (rootTask, binDir)
139140
}
140141

141142
private func planCopyTemplateFile(

Diff for: Plugins/PackageToJS/Templates/test.d.ts

+9
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
export declare class NodeRunner {
2+
constructor()
3+
run(): Promise<void>
4+
}
5+
6+
export declare class BrowserRunner {
7+
constructor()
8+
run(): Promise<void>
9+
}

Diff for: Plugins/PackageToJS/Templates/test.js

+27-5
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,42 @@
11
// @ts-check
2-
import { WASI } from "wasi"
32
import { instantiate } from "./instantiate.js"
43
import { MODULE_PATH } from "./index.js"
5-
import { readFile } from "fs/promises"
64

75
export class NodeRunner {
86
constructor() { }
97

108
async run() {
9+
try {
10+
await this._run()
11+
} catch (error) {
12+
// Print hint for the user
13+
if (error instanceof WebAssembly.CompileError) {
14+
// Old Node.js doesn't support some wasm features.
15+
// Our minimum supported version is v18.x
16+
throw new Error(`${error.message}
17+
18+
Hint: Some WebAssembly features might not be supported in your Node.js version.
19+
Please ensure you are using Node.js v18.x or newer.
20+
`)
21+
}
22+
throw error
23+
}
24+
}
25+
26+
async _run() {
27+
const { WASI } = await import("wasi")
28+
const path = await import("node:path");
29+
const { fileURLToPath } = await import("node:url");
30+
const { readFile } = await import("node:fs/promises")
31+
1132
const wasi = new WASI({
1233
version: "preview1",
13-
args: ["--testing-library", "swift-testing"],
34+
args: [MODULE_PATH, ...process.argv.slice(2)],
35+
preopens: {
36+
"./": process.cwd(),
37+
},
1438
returnOnExit: false,
1539
})
16-
const path = await import("node:path");
17-
const { fileURLToPath } = await import("node:url");
1840
const dirname = path.dirname(fileURLToPath(import.meta.url))
1941
const { swift } = await instantiate(
2042
await readFile(path.join(dirname, MODULE_PATH)),

0 commit comments

Comments
 (0)