@@ -4,8 +4,6 @@ import PackagePlugin
44@main
55struct PackageToJS : CommandPlugin {
66 struct Options {
7- /// Product to build (default: executable target if there's only one)
8- var product : String ?
97 /// Path to the output directory
108 var outputPath : String ?
119 /// Name of the package (default: lowercased Package.swift name)
@@ -14,34 +12,82 @@ struct PackageToJS: CommandPlugin {
1412 var explain : Bool = false
1513
1614 static func parse( from extractor: inout ArgumentExtractor ) -> Options {
17- let product = extractor. extractOption ( named: " product " ) . last
1815 let outputPath = extractor. extractOption ( named: " output " ) . last
1916 let packageName = extractor. extractOption ( named: " package-name " ) . last
2017 let explain = extractor. extractFlag ( named: " explain " )
2118 return Options (
22- product: product, outputPath: outputPath, packageName: packageName,
23- explain: explain != 0
19+ outputPath: outputPath, packageName: packageName, explain: explain != 0
2420 )
2521 }
22+ }
23+
24+ struct BuildOptions {
25+ /// Product to build (default: executable target if there's only one)
26+ var product : String ?
27+ var options : Options
28+
29+ static func parse( from extractor: inout ArgumentExtractor ) -> BuildOptions {
30+ let product = extractor. extractOption ( named: " product " ) . last
31+ let options = Options . parse ( from: & extractor)
32+ return BuildOptions ( product: product, options: options)
33+ }
2634
2735 static func help( ) -> String {
2836 return """
29- Usage: swift package --swift-sdk <swift-sdk> [swift-package options] plugin run PackageToJS [options]
37+ OVERVIEW: Builds a JavaScript module from a Swift package.
3038
31- Options:
39+ USAGE: swift package --swift-sdk <swift-sdk> [SwiftPM options] PackageToJS [options] [subcommand]
40+
41+ OPTIONS:
3242 --product <product> Product to build (default: executable target if there's only one)
3343 --output <path> Path to the output directory (default: .build/plugins/PackageToJS/outputs/Package)
3444 --package-name <name> Name of the package (default: lowercased Package.swift name)
3545 --explain Whether to explain the build plan
3646
37- Examples:
47+ SUBCOMMANDS:
48+ test Builds and runs tests
49+
50+ EXAMPLES:
3851 $ swift package --swift-sdk wasm32-unknown-wasi plugin js
52+ # Build a specific product
3953 $ swift package --swift-sdk wasm32-unknown-wasi plugin js --product Example
54+ # Build in release configuration
4055 $ swift package --swift-sdk wasm32-unknown-wasi -c release plugin js
56+
57+ # Run tests
58+ $ swift package --swift-sdk wasm32-unknown-wasi plugin js test
4159 """
4260 }
4361 }
4462
63+ struct TestOptions {
64+ /// Whether to only build tests, don't run them
65+ var buildOnly : Bool = false
66+ var options : Options
67+
68+ static func parse( from extractor: inout ArgumentExtractor ) -> TestOptions {
69+ let buildOnly = extractor. extractFlag ( named: " build-only " )
70+ let options = Options . parse ( from: & extractor)
71+ return TestOptions ( buildOnly: buildOnly != 0 , options: options)
72+ }
73+
74+ static func help( ) -> String {
75+ return """
76+ OVERVIEW: Builds and runs tests
77+
78+ USAGE: swift package --swift-sdk <swift-sdk> [SwiftPM options] PackageToJS test [options]
79+
80+ OPTIONS:
81+ --build-only Whether to build only (default: false)
82+
83+ EXAMPLES:
84+ $ swift package --swift-sdk wasm32-unknown-wasi plugin js test
85+ # Just build tests, don't run them
86+ $ swift package --swift-sdk wasm32-unknown-wasi plugin js test --build-only
87+ """
88+ }
89+ }
90+
4591 static let friendlyBuildDiagnostics :
4692 [ @Sendable ( _ build: PackageManager . BuildResult , _ arguments: [ String ] ) -> String ? ] = [
4793 (
@@ -83,58 +129,129 @@ struct PackageToJS: CommandPlugin {
83129 """
84130 } ) ,
85131 ]
132+ static private func reportBuildFailure( _ build: PackageManager . BuildResult , _ arguments: [ String ] ) {
133+ for diagnostic in Self . friendlyBuildDiagnostics {
134+ if let message = diagnostic ( build, arguments) {
135+ printStderr ( " \n " + message)
136+ }
137+ }
138+ }
86139
87140 func performCommand( context: PluginContext , arguments: [ String ] ) throws {
88141 if arguments. contains ( where: { [ " -h " , " --help " ] . contains ( $0) } ) {
89- printStderr ( Options . help ( ) )
142+ printStderr ( BuildOptions . help ( ) )
90143 return
91144 }
92145
146+ if arguments. first == " test " {
147+ return try performTestCommand ( context: context, arguments: Array ( arguments. dropFirst ( ) ) )
148+ }
149+
150+ return try performBuildCommand ( context: context, arguments: arguments)
151+ }
152+
153+ static let JAVASCRIPTKIT_PACKAGE_ID : Package . ID = " javascriptkit "
154+
155+ func performBuildCommand( context: PluginContext , arguments: [ String ] ) throws {
93156 var extractor = ArgumentExtractor ( arguments)
94- let options = Options . parse ( from: & extractor)
157+ let buildOptions = BuildOptions . parse ( from: & extractor)
95158
96159 if extractor. remainingArguments. count > 0 {
97160 printStderr (
98161 " Unexpected arguments: \( extractor. remainingArguments. joined ( separator: " " ) ) " )
99- printStderr ( Options . help ( ) )
162+ printStderr ( BuildOptions . help ( ) )
100163 exit ( 1 )
101164 }
102165
103166 // Build products
104- let ( productArtifact, build) = try buildWasm ( options: options, context: context)
105- guard let productArtifact = productArtifact else {
106- for diagnostic in Self . friendlyBuildDiagnostics {
107- if let message = diagnostic ( build, arguments) {
108- printStderr ( " \n " + message)
109- }
110- }
167+ let productName = try buildOptions. product ?? deriveDefaultProduct ( package : context. package )
168+ let build = try buildWasm ( productName: productName, context: context)
169+ guard build. succeeded else {
170+ Self . reportBuildFailure ( build, arguments)
111171 exit ( 1 )
112172 }
173+ let productArtifact = try build. findWasmArtifact ( for: productName)
113174 let outputDir =
114- if let outputPath = options. outputPath {
175+ if let outputPath = buildOptions . options. outputPath {
115176 URL ( fileURLWithPath: outputPath)
116177 } else {
117178 context. pluginWorkDirectoryURL. appending ( path: " Package " )
118179 }
119180 guard
120181 let selfPackage = findPackageInDependencies (
121- package : context. package , id: " javascriptkit " )
182+ package : context. package , id: Self . JAVASCRIPTKIT_PACKAGE_ID )
122183 else {
123184 throw PackageToJSError ( " Failed to find JavaScriptKit in dependencies!? " )
124185 }
125- var make = MiniMake ( explain: options. explain)
126- let allTask = constructPackagingPlan (
127- make: & make, options: options, context: context, wasmProductArtifact: productArtifact,
128- selfPackage: selfPackage, outputDir: outputDir)
129- cleanIfBuildGraphChanged ( root: allTask, make: make, context: context)
186+ var make = MiniMake ( explain: buildOptions. options. explain)
187+ let planner = PackagingPlanner (
188+ options: buildOptions. options, context: context, selfPackage: selfPackage, outputDir: outputDir)
189+ let rootTask = planner. planBuild (
190+ make: & make, wasmProductArtifact: productArtifact)
191+ cleanIfBuildGraphChanged ( root: rootTask, make: make, context: context)
130192 print ( " Packaging... " )
131- try make. build ( output: allTask )
193+ try make. build ( output: rootTask )
132194 print ( " Packaging finished " )
133195 }
134196
135- private func buildWasm( options: Options , context: PluginContext ) throws -> (
136- productArtifact: URL ? , build: PackageManager . BuildResult
137- ) {
197+ func performTestCommand( context: PluginContext , arguments: [ String ] ) throws {
198+ var extractor = ArgumentExtractor ( arguments)
199+ let testOptions = TestOptions . parse ( from: & extractor)
200+
201+ if extractor. remainingArguments. count > 0 {
202+ printStderr ( " Unexpected arguments: \( extractor. remainingArguments. joined ( separator: " " ) ) " )
203+ printStderr ( TestOptions . help ( ) )
204+ exit ( 1 )
205+ }
206+
207+ let productName = " \( context. package . displayName) PackageTests "
208+ let build = try buildWasm ( productName: productName, context: context)
209+ guard build. succeeded else {
210+ Self . reportBuildFailure ( build, arguments)
211+ exit ( 1 )
212+ }
213+
214+ // NOTE: Find the product artifact from the default build directory
215+ // because PackageManager.BuildResult doesn't include the
216+ // product artifact for tests.
217+ // This doesn't work when `--scratch-path` is used but
218+ // we don't have a way to guess the correct path. (we can find
219+ // the path by building a dummy executable product but it's
220+ // not worth the overhead)
221+ var productArtifact : URL ?
222+ for fileExtension in [ " wasm " , " xctest " ] {
223+ let path = " .build/debug/ \( productName) . \( fileExtension) "
224+ if FileManager . default. fileExists ( atPath: path) {
225+ productArtifact = URL ( fileURLWithPath: path)
226+ break
227+ }
228+ }
229+ 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 " )
236+ }
237+ guard
238+ let selfPackage = findPackageInDependencies (
239+ package : context. package , id: Self . JAVASCRIPTKIT_PACKAGE_ID)
240+ else {
241+ throw PackageToJSError ( " Failed to find JavaScriptKit in dependencies!? " )
242+ }
243+ var make = MiniMake ( explain: testOptions. options. explain)
244+ let planner = PackagingPlanner (
245+ options: testOptions. options, context: context, selfPackage: selfPackage, outputDir: outputDir)
246+ let rootTask = planner. planTestBuild (
247+ make: & make, wasmProductArtifact: productArtifact)
248+ cleanIfBuildGraphChanged ( root: rootTask, make: make, context: context)
249+ print ( " Packaging tests... " )
250+ try make. build ( output: rootTask)
251+ print ( " Packaging tests finished " )
252+ }
253+
254+ private func buildWasm( productName: String , context: PluginContext ) throws -> PackageManager . BuildResult {
138255 var parameters = PackageManager . BuildParameters (
139256 configuration: . inherit,
140257 logging: . concise
@@ -154,118 +271,7 @@ struct PackageToJS: CommandPlugin {
154271 " --export-if-defined=__main_argc_argv "
155272 ]
156273 }
157- let productName = try options. product ?? deriveDefaultProduct ( package : context. package )
158- let build = try self . packageManager. build ( . product( productName) , parameters: parameters)
159-
160- var productArtifact : URL ?
161- if build. succeeded {
162- let testProductName = " \( context. package . displayName) PackageTests "
163- if productName == testProductName {
164- for fileExtension in [ " wasm " , " xctest " ] {
165- let path = " .build/debug/ \( testProductName) . \( fileExtension) "
166- if FileManager . default. fileExists ( atPath: path) {
167- productArtifact = URL ( fileURLWithPath: path)
168- break
169- }
170- }
171- } else {
172- productArtifact = try build. findWasmArtifact ( for: productName)
173- }
174- }
175-
176- return ( productArtifact, build)
177- }
178-
179- /// Construct the build plan and return the root task key
180- private func constructPackagingPlan(
181- make: inout MiniMake ,
182- options: Options ,
183- context: PluginContext ,
184- wasmProductArtifact: URL ,
185- selfPackage: Package ,
186- outputDir: URL
187- ) -> MiniMake . TaskKey {
188- let selfPackageURL = selfPackage. directoryURL
189- let selfPath = String ( #filePath)
190-
191- // Prepare output directory
192- let outputDirTask = make. addTask (
193- inputFiles: [ selfPath] , output: outputDir. path, attributes: [ . silent]
194- ) {
195- guard !FileManager. default. fileExists ( atPath: $0. output) else { return }
196- try FileManager . default. createDirectory (
197- atPath: $0. output, withIntermediateDirectories: true , attributes: nil )
198- }
199-
200- var packageInputs : [ MiniMake . TaskKey ] = [ ]
201-
202- func syncFile( from: String , to: String ) throws {
203- if FileManager . default. fileExists ( atPath: to) {
204- try FileManager . default. removeItem ( atPath: to)
205- }
206- try FileManager . default. copyItem ( atPath: from, toPath: to)
207- }
208-
209- // Copy the wasm product artifact
210- let wasmFilename = " main.wasm "
211- let wasm = make. addTask (
212- inputFiles: [ selfPath, wasmProductArtifact. path] , inputTasks: [ outputDirTask] ,
213- output: outputDir. appending ( path: wasmFilename) . path
214- ) {
215- try syncFile ( from: wasmProductArtifact. path, to: $0. output)
216- }
217- packageInputs. append ( wasm)
218-
219- // Write package.json
220- let packageJSON = make. addTask (
221- inputFiles: [ selfPath] , inputTasks: [ outputDirTask] ,
222- output: outputDir. appending ( path: " package.json " ) . path
223- ) {
224- let packageJSON = """
225- {
226- " name " : " \( options. packageName ?? context. package . id. lowercased ( ) ) " ,
227- " version " : " 0.0.0 " ,
228- " type " : " module " ,
229- " exports " : {
230- " . " : " ./index.js " ,
231- " ./wasm " : " ./ \( wasmFilename) "
232- },
233- " dependencies " : {
234- " @bjorn3/browser_wasi_shim " : " ^0.4.1 "
235- }
236- }
237- """
238- try packageJSON. write ( toFile: $0. output, atomically: true , encoding: . utf8)
239- }
240- packageInputs. append ( packageJSON)
241-
242- // Copy the template files
243- let substitutions = [
244- " @PACKAGE_TO_JS_MODULE_PATH@ " : wasmFilename
245- ]
246- for (file, output) in [
247- ( " Plugins/PackageToJS/Templates/index.js " , " index.js " ) ,
248- ( " Plugins/PackageToJS/Templates/index.d.ts " , " index.d.ts " ) ,
249- ( " Plugins/PackageToJS/Templates/instantiate.js " , " instantiate.js " ) ,
250- ( " Plugins/PackageToJS/Templates/instantiate.d.ts " , " instantiate.d.ts " ) ,
251- ( " Sources/JavaScriptKit/Runtime/index.mjs " , " runtime.js " ) ,
252- ] {
253- let inputPath = selfPackageURL. appending ( path: file)
254- let copied = make. addTask (
255- inputFiles: [ selfPath, inputPath. path] , inputTasks: [ outputDirTask] ,
256- output: outputDir. appending ( path: output) . path
257- ) {
258- var content = try String ( contentsOf: inputPath, encoding: . utf8)
259- for (key, value) in substitutions {
260- content = content. replacingOccurrences ( of: key, with: value)
261- }
262- try content. write ( toFile: $0. output, atomically: true , encoding: . utf8)
263- }
264- packageInputs. append ( copied)
265- }
266- return make. addTask (
267- inputTasks: packageInputs, output: " all " , attributes: [ . phony, . silent]
268- ) { _ in }
274+ return try self . packageManager. build ( . product( productName) , parameters: parameters)
269275 }
270276
271277 /// Clean if the build graph of the packaging process has changed
0 commit comments