@@ -10,63 +10,57 @@ import mill.scalajslib.worker.ScalaJSWorker
1010
1111/** A Mill module trait that adds content hashing to Scala.js linked output.
1212 *
13- * Mix this trait into a `ScalaJSModule` to produce JS files whose names
14- * include a SHA-256 content hash, e.g. `main.a1b2c3d4.js`. Internal
15- * references between modules (import/require statements and
16- * `sourceMappingURL` comments) are automatically rewritten to point at the
17- * new hashed filenames. This allows long-lived HTTP caching while
13+ * Mix this trait into a `ScalaJSModule` to produce JS files whose names include a SHA-256 content hash, e.g.
14+ * `main.a1b2c3d4.js`. Internal references between modules (import/require statements and `sourceMappingURL` comments)
15+ * are automatically rewritten to point at the new hashed filenames. This allows long-lived HTTP caching while
1816 * guaranteeing cache busting whenever file content changes.
1917 *
2018 * @note
21- * This trait is placed in the `mill.scalajslib` package (vendored alongside
22- * Mill) so that it can access the `private[scalajslib]` members
23- * [[ScalaJSWorker ]] and [[ScalaJSModule.linkJs ]], which are intentionally
24- * hidden from external packages. No Mill sources are modified; we only add
25- * a new trait that overrides one `private[scalajslib]` method.
19+ * This trait is placed in the `mill.scalajslib` package (vendored alongside Mill) so that it can access the
20+ * `private[scalajslib]` members [[ScalaJSWorker ]] and [[ScalaJSModule.linkJs ]], which are intentionally hidden from
21+ * external packages. No Mill sources are modified; we only add a new trait that overrides one `private[scalajslib]`
22+ * method.
2623 *
2724 * @example
28- * {{{
25+ * {{{
2926 * object app extends ScalaJSModule with ContentHashScalaJSModule {
3027 * def scalaVersion = "3.3.6"
3128 * def scalaJSVersion = "1.19.0"
3229 * }
33- * }}}
30+ * }}}
3431 *
35- * Running `mill app.fastLinkJS` (or `fullLinkJS`) will produce hashed files
36- * in the task output directory, e.g. `out/app/fastLinkJS.dest/main.a1b2c3d4.js`.
32+ * Running `mill app.fastLinkJS` (or `fullLinkJS`) will produce hashed files in the task output directory, e.g.
33+ * `out/app/fastLinkJS.dest/main.a1b2c3d4.js`.
3734 */
3835trait ContentHashScalaJSModule extends ScalaJSModule :
3936
40- /** Override [[linkJs ]] to capture linker output in a temporary directory,
41- * compute SHA-256 content hashes, rewrite intra-bundle references, and
42- * write only the hashed files to the task output directory (`ctx.dest`).
37+ /** Override [[linkJs ]] to capture linker output in a temporary directory, compute SHA-256 content hashes, rewrite
38+ * intra-bundle references, and write only the hashed files to the task output directory (`ctx.dest`).
4339 *
44- * This is the linker-wrapper approach: instead of post-processing files
45- * that a prior task has already written to disk, we intercept at the
46- * linking step itself — redirecting [[ScalaJSWorker.link ]] output to a
47- * temporary directory — so only the final hashed artefacts ever land in
48- * Mill's task output directory.
40+ * This is the linker-wrapper approach: instead of post-processing files that a prior task has already written to
41+ * disk, we intercept at the linking step itself — redirecting [[ScalaJSWorker.link ]] output to a temporary directory
42+ * — so only the final hashed artefacts ever land in Mill's task output directory.
4943 *
50- * The trait must reside in `mill.scalajslib` because both [[ScalaJSWorker ]]
51- * and [[ linkJs ]] are declared `private[scalajslib]`.
44+ * The trait must reside in `mill.scalajslib` because both [[ScalaJSWorker ]] and [[ linkJs ]] are declared
45+ * `private[scalajslib]`.
5246 */
5347 private [scalajslib] override def linkJs (
54- worker : ScalaJSWorker ,
55- toolsClasspath : Seq [PathRef ],
56- runClasspath : Seq [PathRef ],
57- mainClass : Result [String ],
58- forceOutJs : Boolean ,
59- testBridgeInit : Boolean ,
60- isFullLinkJS : Boolean ,
61- optimizer : Boolean ,
62- sourceMap : Boolean ,
63- moduleKind : ModuleKind ,
64- esFeatures : ESFeatures ,
65- moduleSplitStyle : ModuleSplitStyle ,
66- outputPatterns : OutputPatterns ,
67- minify : Boolean ,
68- importMap : Seq [ESModuleImportMapping ],
69- experimentalUseWebAssembly : Boolean
48+ worker : ScalaJSWorker ,
49+ toolsClasspath : Seq [PathRef ],
50+ runClasspath : Seq [PathRef ],
51+ mainClass : Result [String ],
52+ forceOutJs : Boolean ,
53+ testBridgeInit : Boolean ,
54+ isFullLinkJS : Boolean ,
55+ optimizer : Boolean ,
56+ sourceMap : Boolean ,
57+ moduleKind : ModuleKind ,
58+ esFeatures : ESFeatures ,
59+ moduleSplitStyle : ModuleSplitStyle ,
60+ outputPatterns : OutputPatterns ,
61+ minify : Boolean ,
62+ importMap : Seq [ESModuleImportMapping ],
63+ experimentalUseWebAssembly : Boolean
7064 )(implicit ctx : TaskCtx ): Result [Report ] =
7165 val tempDir = os.temp.dir()
7266 try
@@ -89,21 +83,23 @@ trait ContentHashScalaJSModule extends ScalaJSModule:
8983 importMap = importMap,
9084 experimentalUseWebAssembly = experimentalUseWebAssembly
9185 )
92- .map { report =>
93- ContentHashScalaJSModule .applyContentHash(report, ctx.dest)
86+ .map {
87+ report =>
88+ ContentHashScalaJSModule .applyContentHash(report, ctx.dest)
9489 }
9590 finally os.remove.all(tempDir)
91+ end try
92+ end linkJs
9693
9794end ContentHashScalaJSModule
9895
9996object ContentHashScalaJSModule :
10097
101- /** Post-process a `Report` by computing SHA-256 content hashes for every
102- * emitted `.js` file, renaming each file to include the hash, rewriting
103- * all intra-bundle references, and returning an updated `Report`.
98+ /** Post-process a `Report` by computing SHA-256 content hashes for every emitted `.js` file, renaming each file to
99+ * include the hash, rewriting all intra-bundle references, and returning an updated `Report`.
104100 *
105- * `report.dest.path` is treated as the source directory (where the Scala.js
106- * linker wrote unhashed output). Hashed files are written to `destDir`.
101+ * `report.dest.path` is treated as the source directory (where the Scala.js linker wrote unhashed output). Hashed
102+ * files are written to `destDir`.
107103 */
108104 def applyContentHash (report : Report , destDir : os.Path ): Report =
109105 val srcDir = report.dest.path
@@ -113,47 +109,56 @@ object ContentHashScalaJSModule:
113109 val jsFiles = os.list(srcDir).filter(p => os.isFile(p) && p.ext == " js" )
114110
115111 // Compute hash → new name mapping for every JS file
116- val jsHashMapping : Map [String , String ] = jsFiles.map { f =>
117- val bytes = os.read.bytes(f)
118- val hashed = computeContentHash(bytes)
119- val newName = s " ${f.baseName}. $hashed. ${f.ext}"
120- (f.last, newName)
121- }.toMap
112+ val jsHashMapping : Map [String , String ] = jsFiles
113+ .map {
114+ f =>
115+ val bytes = os.read.bytes(f)
116+ val hashed = computeContentHash(bytes)
117+ val newName = s " ${f.baseName}. $hashed. ${f.ext}"
118+ (f.last, newName)
119+ }
120+ .toMap
122121
123122 // A combined mapping that also covers `.js.map` names so that
124123 // sourceMappingURL lines and source-map `"file"` fields are updated.
125- val fullMapping : Map [String , String ] = jsHashMapping.flatMap { case (orig, hashed) =>
126- Seq (
127- orig -> hashed,
128- s " $orig.map " -> s " $hashed.map "
129- )
124+ val fullMapping : Map [String , String ] = jsHashMapping.flatMap {
125+ case (orig, hashed) =>
126+ Seq (
127+ orig -> hashed,
128+ s " $orig.map " -> s " $hashed.map "
129+ )
130130 }
131131
132132 // Write hashed JS files with rewritten intra-bundle references
133- jsFiles.foreach { f =>
134- val originalContent = os.read(f)
135- val rewritten = rewriteJsReferences(originalContent, fullMapping)
136- os.write(destDir / jsHashMapping(f.last), rewritten.getBytes(" UTF-8" ))
133+ jsFiles.foreach {
134+ f =>
135+ val originalContent = os.read(f)
136+ val rewritten = rewriteJsReferences(originalContent, fullMapping)
137+ os.write(destDir / jsHashMapping(f.last), rewritten.getBytes(" UTF-8" ))
137138 }
138139
139140 // Copy / rename remaining files (source maps, wasm, etc.)
140141 val copiedSoFar = jsHashMapping.keySet
141142 os.list(srcDir)
142143 .filter(p => os.isFile(p) && ! copiedSoFar.contains(p.last))
143- .foreach { f =>
144- val newName = fullMapping.getOrElse(f.last, f.last)
145- os.copy(f, destDir / newName)
144+ .foreach {
145+ f =>
146+ val newName = fullMapping.getOrElse(f.last, f.last)
147+ os.copy(f, destDir / newName)
146148 }
147149
148150 // Build an updated Report that names the hashed files
149- val updatedModules = report.publicModules.map { m =>
150- Report .Module (
151- moduleID = m.moduleID,
152- jsFileName = jsHashMapping.getOrElse(m.jsFileName, m.jsFileName),
153- sourceMapName = m.sourceMapName.map(sm => fullMapping.getOrElse(sm, sm)),
154- moduleKind = m.moduleKind
155- )
156- }
151+ val updatedModules = report
152+ .publicModules
153+ .map {
154+ m =>
155+ Report .Module (
156+ moduleID = m.moduleID,
157+ jsFileName = jsHashMapping.getOrElse(m.jsFileName, m.jsFileName),
158+ sourceMapName = m.sourceMapName.map(sm => fullMapping.getOrElse(sm, sm)),
159+ moduleKind = m.moduleKind
160+ )
161+ }
157162
158163 Report (updatedModules, PathRef (destDir))
159164 end applyContentHash
@@ -162,21 +167,23 @@ object ContentHashScalaJSModule:
162167 def computeContentHash (bytes : Array [Byte ]): String =
163168 val digest = MessageDigest .getInstance(" SHA-256" )
164169 digest.digest(bytes).take(8 ).map(" %02x" .format(_)).mkString
170+ end computeContentHash
165171
166- /** Replace every occurrence of an original filename (quoted or in a
167- * `sourceMappingURL` comment) with the corresponding hashed filename.
172+ /** Replace every occurrence of an original filename (quoted or in a `sourceMappingURL` comment) with the
173+ * corresponding hashed filename.
168174 *
169175 * Handles:
170176 * - Double-quoted import/require arguments: `"original.js"`
171177 * - Single-quoted import/require arguments: `'original.js'`
172178 * - Inline source-map comments: `//# sourceMappingURL=original.js.map`
173179 */
174180 def rewriteJsReferences (content : String , mapping : Map [String , String ]): String =
175- mapping.foldLeft(content) { case (acc, (orig, hashed)) =>
176- acc
177- .replace(s """ " $orig" """ , s """ " $hashed" """ )
178- .replace(s " ' $orig' " , s " ' $hashed' " )
179- .replace(s " sourceMappingURL= $orig" , s " sourceMappingURL= $hashed" )
181+ mapping.foldLeft(content) {
182+ case (acc, (orig, hashed)) =>
183+ acc
184+ .replace(s """ " $orig" """ , s """ " $hashed" """ )
185+ .replace(s " ' $orig' " , s " ' $hashed' " )
186+ .replace(s " sourceMappingURL= $orig" , s " sourceMappingURL= $hashed" )
180187 }
181188
182189end ContentHashScalaJSModule
0 commit comments