@@ -54,3 +54,246 @@ struct PackageToJSError: Swift.Error, CustomStringConvertible {
54
54
self . description = " Error: " + message
55
55
}
56
56
}
57
+
58
+ /// Plans the build for packaging.
59
+ struct PackagingPlanner {
60
+ /// The options of the plugin
61
+ let options : PackageToJS . Options
62
+ /// The package ID of the package that this plugin is running on
63
+ let packageId : String
64
+ /// The directory of the package that contains this plugin
65
+ let selfPackageDir : URL
66
+ /// The path of this file itself, used to capture changes of planner code
67
+ let selfPath : String
68
+ /// The directory for the final output
69
+ let outputDir : URL
70
+ /// The directory for intermediate files
71
+ let intermediatesDir : URL
72
+ /// The filename of the .wasm file
73
+ let wasmFilename = " main.wasm "
74
+
75
+ init (
76
+ options: PackageToJS . Options ,
77
+ packageId: String ,
78
+ pluginWorkDirectoryURL: URL ,
79
+ selfPackageDir: URL ,
80
+ outputDir: URL
81
+ ) {
82
+ self . options = options
83
+ self . packageId = packageId
84
+ self . selfPackageDir = selfPackageDir
85
+ self . outputDir = outputDir
86
+ self . intermediatesDir = pluginWorkDirectoryURL. appending ( path: outputDir. lastPathComponent + " .tmp " )
87
+ self . selfPath = String ( #filePath)
88
+ }
89
+
90
+ // MARK: - Primitive build operations
91
+
92
+ private static func syncFile( from: String , to: String ) throws {
93
+ if FileManager . default. fileExists ( atPath: to) {
94
+ try FileManager . default. removeItem ( atPath: to)
95
+ }
96
+ try FileManager . default. copyItem ( atPath: from, toPath: to)
97
+ }
98
+
99
+ private static func createDirectory( atPath: String ) throws {
100
+ guard !FileManager. default. fileExists ( atPath: atPath) else { return }
101
+ try FileManager . default. createDirectory (
102
+ atPath: atPath, withIntermediateDirectories: true , attributes: nil
103
+ )
104
+ }
105
+
106
+ private static func runCommand( _ command: URL , _ arguments: [ String ] ) throws {
107
+ let task = Process ( )
108
+ task. executableURL = command
109
+ task. arguments = arguments
110
+ task. currentDirectoryURL = URL ( fileURLWithPath: FileManager . default. currentDirectoryPath)
111
+ try task. run ( )
112
+ task. waitUntilExit ( )
113
+ guard task. terminationStatus == 0 else {
114
+ throw PackageToJSError ( " Command failed with status \( task. terminationStatus) " )
115
+ }
116
+ }
117
+
118
+ // MARK: - Build plans
119
+
120
+ /// Construct the build plan and return the root task key
121
+ func planBuild(
122
+ make: inout MiniMake ,
123
+ splitDebug: Bool ,
124
+ wasmProductArtifact: URL
125
+ ) throws -> MiniMake . TaskKey {
126
+ let ( allTasks, _) = try planBuildInternal (
127
+ make: & make, splitDebug: splitDebug, wasmProductArtifact: wasmProductArtifact
128
+ )
129
+ return make. addTask (
130
+ inputTasks: allTasks, output: " all " , attributes: [ . phony, . silent]
131
+ ) { _ in }
132
+ }
133
+
134
+ private func planBuildInternal(
135
+ make: inout MiniMake ,
136
+ splitDebug: Bool ,
137
+ wasmProductArtifact: URL
138
+ ) throws -> ( allTasks: [ MiniMake . TaskKey ] , outputDirTask: MiniMake . TaskKey ) {
139
+ // Prepare output directory
140
+ let outputDirTask = make. addTask (
141
+ inputFiles: [ selfPath] , output: outputDir. path, attributes: [ . silent]
142
+ ) {
143
+ try Self . createDirectory ( atPath: $0. output)
144
+ }
145
+
146
+ var packageInputs : [ MiniMake . TaskKey ] = [ ]
147
+
148
+ // Guess the build configuration from the parent directory name of .wasm file
149
+ let buildConfiguration = wasmProductArtifact. deletingLastPathComponent ( ) . lastPathComponent
150
+ let wasm : MiniMake . TaskKey
151
+
152
+ let shouldOptimize : Bool
153
+ let wasmOptPath = try ? which ( " wasm-opt " )
154
+ if buildConfiguration == " debug " {
155
+ shouldOptimize = false
156
+ } else {
157
+ if wasmOptPath != nil {
158
+ shouldOptimize = true
159
+ } else {
160
+ print ( " Warning: wasm-opt not found in PATH, skipping optimizations " )
161
+ shouldOptimize = false
162
+ }
163
+ }
164
+
165
+ if let wasmOptPath = wasmOptPath, shouldOptimize {
166
+ // Optimize the wasm in release mode
167
+ let intermediatesDirTask = make. addTask (
168
+ inputFiles: [ selfPath] , output: intermediatesDir. path, attributes: [ . silent]
169
+ ) {
170
+ try Self . createDirectory ( atPath: $0. output)
171
+ }
172
+ // If splitDebug is true, we need to place the DWARF-stripped wasm file (but "name" section remains)
173
+ // in the output directory.
174
+ let stripWasmPath = ( splitDebug ? outputDir : intermediatesDir) . appending ( path: wasmFilename + " .debug " ) . path
175
+
176
+ // First, strip DWARF sections as their existence enables DWARF preserving mode in wasm-opt
177
+ let stripWasm = make. addTask (
178
+ inputFiles: [ selfPath, wasmProductArtifact. path] , inputTasks: [ outputDirTask, intermediatesDirTask] ,
179
+ output: stripWasmPath
180
+ ) {
181
+ print ( " Stripping DWARF debug info... " )
182
+ try Self . runCommand ( wasmOptPath, [ wasmProductArtifact. path, " --strip-dwarf " , " --debuginfo " , " -o " , $0. output] )
183
+ }
184
+ // Then, run wasm-opt with all optimizations
185
+ wasm = make. addTask (
186
+ inputFiles: [ selfPath] , inputTasks: [ outputDirTask, stripWasm] ,
187
+ output: outputDir. appending ( path: wasmFilename) . path
188
+ ) {
189
+ print ( " Optimizing the wasm file... " )
190
+ try Self . runCommand ( wasmOptPath, [ stripWasmPath, " -Os " , " -o " , $0. output] )
191
+ }
192
+ } else {
193
+ // Copy the wasm product artifact
194
+ wasm = make. addTask (
195
+ inputFiles: [ selfPath, wasmProductArtifact. path] , inputTasks: [ outputDirTask] ,
196
+ output: outputDir. appending ( path: wasmFilename) . path
197
+ ) {
198
+ try Self . syncFile ( from: wasmProductArtifact. path, to: $0. output)
199
+ }
200
+ }
201
+ packageInputs. append ( wasm)
202
+
203
+ // Write package.json
204
+ let packageJSON = make. addTask (
205
+ inputFiles: [ selfPath] , inputTasks: [ outputDirTask] ,
206
+ output: outputDir. appending ( path: " package.json " ) . path
207
+ ) {
208
+ let packageJSON = """
209
+ {
210
+ " name " : " \( options. packageName ?? packageId. lowercased ( ) ) " ,
211
+ " version " : " 0.0.0 " ,
212
+ " type " : " module " ,
213
+ " exports " : {
214
+ " . " : " ./index.js " ,
215
+ " ./wasm " : " ./ \( wasmFilename) "
216
+ },
217
+ " dependencies " : {
218
+ " @bjorn3/browser_wasi_shim " : " ^0.4.1 "
219
+ }
220
+ }
221
+ """
222
+ try packageJSON. write ( toFile: $0. output, atomically: true , encoding: . utf8)
223
+ }
224
+ packageInputs. append ( packageJSON)
225
+
226
+ // Copy the template files
227
+ for (file, output) in [
228
+ ( " Plugins/PackageToJS/Templates/index.js " , " index.js " ) ,
229
+ ( " Plugins/PackageToJS/Templates/index.d.ts " , " index.d.ts " ) ,
230
+ ( " Plugins/PackageToJS/Templates/instantiate.js " , " instantiate.js " ) ,
231
+ ( " Plugins/PackageToJS/Templates/instantiate.d.ts " , " instantiate.d.ts " ) ,
232
+ ( " Sources/JavaScriptKit/Runtime/index.mjs " , " runtime.js " ) ,
233
+ ] {
234
+ packageInputs. append ( planCopyTemplateFile (
235
+ make: & make, file: file, output: output, outputDirTask: outputDirTask,
236
+ inputs: [ ]
237
+ ) )
238
+ }
239
+ return ( packageInputs, outputDirTask)
240
+ }
241
+
242
+ /// Construct the test build plan and return the root task key
243
+ func planTestBuild(
244
+ make: inout MiniMake ,
245
+ wasmProductArtifact: URL
246
+ ) throws -> ( rootTask: MiniMake . TaskKey , binDir: URL ) {
247
+ var ( allTasks, outputDirTask) = try planBuildInternal (
248
+ make: & make, splitDebug: false , wasmProductArtifact: wasmProductArtifact
249
+ )
250
+
251
+ let binDir = outputDir. appending ( path: " bin " )
252
+ let binDirTask = make. addTask (
253
+ inputFiles: [ selfPath] , inputTasks: [ outputDirTask] ,
254
+ output: binDir. path
255
+ ) {
256
+ try Self . createDirectory ( atPath: $0. output)
257
+ }
258
+ allTasks. append ( binDirTask)
259
+
260
+ // Copy the template files
261
+ for (file, output) in [
262
+ ( " Plugins/PackageToJS/Templates/test.js " , " test.js " ) ,
263
+ ( " Plugins/PackageToJS/Templates/test.d.ts " , " test.d.ts " ) ,
264
+ ( " Plugins/PackageToJS/Templates/bin/test.js " , " bin/test.js " ) ,
265
+ ] {
266
+ allTasks. append ( planCopyTemplateFile (
267
+ make: & make, file: file, output: output, outputDirTask: outputDirTask,
268
+ inputs: [ binDirTask]
269
+ ) )
270
+ }
271
+ let rootTask = make. addTask (
272
+ inputTasks: allTasks, output: " all " , attributes: [ . phony, . silent]
273
+ ) { _ in }
274
+ return ( rootTask, binDir)
275
+ }
276
+
277
+ private func planCopyTemplateFile(
278
+ make: inout MiniMake ,
279
+ file: String ,
280
+ output: String ,
281
+ outputDirTask: MiniMake . TaskKey ,
282
+ inputs: [ MiniMake . TaskKey ]
283
+ ) -> MiniMake . TaskKey {
284
+ let inputPath = selfPackageDir. appending ( path: file)
285
+ let substitutions = [
286
+ " @PACKAGE_TO_JS_MODULE_PATH@ " : wasmFilename
287
+ ]
288
+ return make. addTask (
289
+ inputFiles: [ selfPath, inputPath. path] , inputTasks: [ outputDirTask] + inputs,
290
+ output: outputDir. appending ( path: output) . path
291
+ ) {
292
+ var content = try String ( contentsOf: inputPath, encoding: . utf8)
293
+ for (key, value) in substitutions {
294
+ content = content. replacingOccurrences ( of: key, with: value)
295
+ }
296
+ try content. write ( toFile: $0. output, atomically: true , encoding: . utf8)
297
+ }
298
+ }
299
+ }
0 commit comments