@@ -54,3 +54,246 @@ struct PackageToJSError: Swift.Error, CustomStringConvertible {
5454 self . description = " Error: " + message
5555 }
5656}
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