You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Mill's `ScalaJSModule` exposes `scalaJSMinify: T[Boolean]` (defaults `true`). This flag is passed directly to the Scala.js linker's `minify` parameter, enabling linker-level size reduction during `fullLinkJS`. It is unrelated to terser.
4
+
5
+
The plugin provides two module traits with different output strategies:
6
+
7
+
-**`FileBasedContentHashScalaJSModule`** — always writes to disk. Has a `minified` task that post-processes `fullLinkJS` output through `terser`. When `scalaJSExperimentalUseWebAssembly = true`, the linker emits JS loader files alongside the `.wasm` binary; terser can break these loaders. Currently has no special WASM handling in `minified`.
8
+
-**`InMemoryHashScalaJSModule`** — keeps linker output in an in-memory directory. `fastLinkJS` hashes in-memory and writes to `Task.dest`. `fullLinkJS` does the same (plus `wasm-opt` in WASM mode). Has **no `minified` task** — so JS files are never terser-minified in production builds.
9
+
10
+
The desired end-state:
11
+
- Both module traits should terser-minify JS output for non-WASM `fullLinkJS` builds (controlled by `scalaJSMinify`).
12
+
- Neither should run terser in WASM mode (where `wasm-opt` is the appropriate tool).
13
+
-`fastLinkJS` output stays in-memory for `InMemoryHashScalaJSModule`; disk output is only for `fullLinkJS`.
14
+
15
+
## Goals / Non-Goals
16
+
17
+
**Goals:**
18
+
-`FileBasedContentHashScalaJSModule.minified`: when `scalaJSExperimentalUseWebAssembly()` is `true`, copy all files unchanged (skip terser). Non-WASM path unchanged.
19
+
-`InMemoryHashScalaJSModule.fullLinkJS`: when `scalaJSExperimentalUseWebAssembly()` is `false` and `scalaJSMinify()` is `true`, run terser on each JS file in-memory before hashing and writing to `Task.dest`.
20
+
-`InMemoryHashScalaJSModule.fastLinkJS`: no change — stays in-memory, no terser.
21
+
-`InMemoryHashScalaJSModule.fullLinkJS` WASM path: no change — `wasm-opt` already runs; do not add terser.
22
+
23
+
**Non-Goals:**
24
+
- Changing how `scalaJSMinify` interacts with the Scala.js linker itself.
25
+
- Running terser during `fastLinkJS` on either module.
26
+
- Running terser on WASM loader JS in either module.
27
+
28
+
## Decisions
29
+
30
+
### Gate terser on `scalaJSMinify` in `InMemoryHashScalaJSModule`
31
+
32
+
`scalaJSMinify` is the user-facing flag that already controls linker-level minification. Reusing it for the terser gate is the least-surprise approach: one flag that means "apply all production minification". This avoids introducing a second flag.
33
+
34
+
### Terser invocation for `InMemoryHashScalaJSModule`
35
+
36
+
For each JS file in the in-memory directory, write it to a temp file, invoke `terser`, read the result back into memory, then proceed with the existing hash-and-write-to-disk flow. Source maps are updated as in `FileBasedContentHashScalaJSModule.minified`. This reuses the existing `terserConfig` task.
37
+
38
+
### Detect WASM mode via `scalaJSExperimentalUseWebAssembly`
39
+
40
+
Both module traits check `scalaJSExperimentalUseWebAssembly()` directly — the canonical flag already used by `fullLinkJS` — rather than inspecting output file extensions.
41
+
42
+
## Risks / Trade-offs
43
+
44
+
-[Risk]`InMemoryHashScalaJSModule.fullLinkJS` now writes to disk (it already did) and invokes terser — build time increases for production JS-only builds. This is expected and acceptable; users can set `scalaJSMinify = false` to skip it.
45
+
-[Risk]`scalaJSMinify` could be confused with the `minified` task on `FileBasedContentHashScalaJSModule` — they are related but distinct: `scalaJSMinify` gates both the Scala.js linker pass and the terser pass; `minified` is the `FileBasedContentHashScalaJSModule`-specific task that wraps terser. Doc-comments on both should clarify this.
46
+
47
+
## Migration Plan
48
+
49
+
No migration needed. Non-WASM production builds gain terser minification in `InMemoryHashScalaJSModule` (previously absent). WASM builds that previously failed under terser in `FileBasedContentHashScalaJSModule.minified` now succeed. Users can opt out by setting `override def scalaJSMinify = Task(false)`.
When Scala.js emits WASM output, the linker also emits JS loader files (e.g. `__loader.js`) that bootstrap the WASM module. The current `minified` task passes every `.js` file in the `fullLinkJS` output through `terser`, including these loader files. Terser does not understand the specialised WASM-bootstrap patterns and can produce broken output; even when it does not break, minifying a tiny loader file is unnecessary overhead. Only the WASM binary itself benefits meaningfully from optimisation (which is already handled by `wasm-opt`).
4
+
5
+
## What Changes
6
+
7
+
- The `minified` task in `FileBasedContentHashScalaJSModule` (and the equivalent path in `InMemoryHashScalaJSModule`) will skip JS files when the module is in WASM mode.
8
+
- When `scalaJSExperimentalUseWebAssembly` is `true`, `minified` will copy JS loader files unchanged and only pass `.wasm` files through the existing `wasm-opt` pipeline (which already runs in `fullLinkJS`).
9
+
- No change to the non-WASM code path.
10
+
11
+
## Capabilities
12
+
13
+
### New Capabilities
14
+
15
+
-`wasm-js-passthrough`: When emitting WASM, JS loader files emitted alongside the `.wasm` binary are copied as-is by `minified`, bypassing terser.
16
+
17
+
### Modified Capabilities
18
+
19
+
-`wasm-minification`: The existing requirement that `wasm-opt` is the only minification tool applied to WASM builds is made explicit and enforced in `minified` as well as `fullLinkJS`.
20
+
21
+
## Impact
22
+
23
+
-`plugin/src/FileBasedContentHashScalaJSModule.scala` — `minified` task gains a WASM branch
24
+
-`plugin/integration/src/wasm.test.scala` — new integration test scenario covering `minified` in WASM mode
25
+
- No API or published interface changes; `wasmOptFlags` and `terserConfig` remain unchanged
### Requirement: FileBasedContentHashScalaJSModule does not run terser on WASM builds
4
+
When `scalaJSExperimentalUseWebAssembly` is `true`, the `minified` task in `FileBasedContentHashScalaJSModule` SHALL copy all files unchanged to the output directory instead of passing JS files through `terser`.
5
+
6
+
#### Scenario: JS loader files are copied unchanged in WASM mode
7
+
-**WHEN** a `FileBasedContentHashScalaJSModule` with `scalaJSExperimentalUseWebAssembly = true` runs `minified`
8
+
-**THEN** all `.js` files in the output SHALL be the same size as the corresponding files in the `fullLinkJS` output
9
+
10
+
#### Scenario: minified succeeds in WASM mode without terser
11
+
-**WHEN** a `FileBasedContentHashScalaJSModule` with `scalaJSExperimentalUseWebAssembly = true` runs `minified`
12
+
-**THEN** the task SHALL complete successfully without invoking `terser`
13
+
14
+
#### Scenario: WASM file is included in minified output
15
+
-**WHEN** a `FileBasedContentHashScalaJSModule` with `scalaJSExperimentalUseWebAssembly = true` runs `minified`
16
+
-**THEN** the output directory SHALL contain a content-hashed `.wasm` file copied from the `fullLinkJS` output
17
+
18
+
### Requirement: InMemoryHashScalaJSModule runs terser during fullLinkJS for non-WASM builds
19
+
When `scalaJSExperimentalUseWebAssembly` is `false` and `scalaJSMinify` is `true`, `InMemoryHashScalaJSModule.fullLinkJS` SHALL invoke `terser` on each JS file before hashing and writing to `Task.dest`.
20
+
21
+
#### Scenario: fullLinkJS JS output is smaller than fastLinkJS when scalaJSMinify is true
22
+
-**WHEN** an `InMemoryHashScalaJSModule` with `scalaJSMinify = true` runs `fullLinkJS`
23
+
-**THEN** the total size of `.js` files in `Task.dest` SHALL be smaller than the total size of `.js` files produced by `fastLinkJS`
24
+
25
+
#### Scenario: fullLinkJS skips terser when scalaJSMinify is false
26
+
-**WHEN** an `InMemoryHashScalaJSModule` with `scalaJSMinify = false` runs `fullLinkJS`
27
+
-**THEN**`terser` SHALL NOT be invoked
28
+
29
+
#### Scenario: fastLinkJS does not run terser
30
+
-**WHEN** an `InMemoryHashScalaJSModule` runs `fastLinkJS`
31
+
-**THEN**`terser` SHALL NOT be invoked and in-memory output SHALL be present
32
+
33
+
#### Scenario: fullLinkJS WASM path does not run terser
34
+
-**WHEN** an `InMemoryHashScalaJSModule` with `scalaJSExperimentalUseWebAssembly = true` runs `fullLinkJS`
### Requirement: wasm-opt is invoked on WASM output during fullLinkJS
4
+
When `scalaJSExperimentalUseWebAssembly` is enabled, both `InMemoryHashScalaJSModule` and `FileBasedContentHashScalaJSModule` SHALL invoke `wasm-opt` on the emitted `.wasm` file as part of `fullLinkJS`, using the flags supplied by `wasmOptFlags`. The `minified` task SHALL NOT invoke `wasm-opt` a second time — the WASM binary produced by `fullLinkJS` is already optimised and is used as-is.
5
+
6
+
#### Scenario: WASM file is optimised during full link
7
+
-**WHEN** a module with `scalaJSExperimentalUseWebAssembly = true` runs `fullLinkJS`
8
+
-**THEN** the emitted `.wasm` file SHALL be smaller than or equal to the unoptimised `.wasm` produced by `fastLinkJS`
9
+
10
+
#### Scenario: fullLinkJS succeeds with default flags
11
+
-**WHEN**`wasmOptFlags` is not overridden (defaults to `Seq("-O2", "-all")`)
12
+
-**THEN**`wasm-opt` SHALL complete without error
13
+
14
+
#### Scenario: minified does not re-run wasm-opt
15
+
-**WHEN** a `FileBasedContentHashScalaJSModule` with `scalaJSExperimentalUseWebAssembly = true` runs `minified`
16
+
-**THEN**`wasm-opt` SHALL NOT be invoked
17
+
-**AND** the `.wasm` file in the `minified` output SHALL be the same binary as produced by `fullLinkJS`
-[x] 1.1 In `FileBasedContentHashScalaJSModule.minified`, read `scalaJSExperimentalUseWebAssembly()` at the top of the task body
4
+
-[x] 1.2 When `true`, copy all files from the `fullLinkJS` output directory directly to `Task.dest` (skipping the terser loop); leave the existing terser path untouched for the `false` branch
5
+
-[x] 1.3 Update the scaladoc on `minified` to clarify the WASM skip behaviour and distinguish it from the `scalaJSMinify` linker flag
6
+
7
+
## 2. InMemoryHashScalaJSModule — terser on fullLinkJS
8
+
9
+
-[x] 2.1 Add `terserConfig` task to `InMemoryHashScalaJSModule` (same default JSON as `FileBasedContentHashScalaJSModule`)
10
+
-[x] 2.2 In `InMemoryHashScalaJSModule.fullLinkJS`, in the non-WASM branch: when `scalaJSMinify()` is `true`, for each JS file in `inMemoryOutputDirectory` run terser (write to temp file, invoke CLI, read result back) and replace the in-memory entry with the minified bytes before the existing hash-and-write-to-disk flow
11
+
-[x] 2.3 Ensure source map `sourceMappingURL` comments are updated to use the final hashed filename after terser rewrites them
12
+
-[x] 2.4 Leave the WASM branch of `fullLinkJS` and the entirety of `fastLinkJS` unchanged
13
+
14
+
## 3. Tests
15
+
16
+
-[x] 3.1 `wasm.test.scala`: call `minified` on `FileBasedContentHashScalaJSModule` with `scalaJSExperimentalUseWebAssembly = true`; assert task succeeds, output contains `.wasm` and JS loader files, and each JS file is byte-for-byte the same size as in `fullLinkJS` output
17
+
-[x] 3.2 `linkInMem.test.scala`: call `fullLinkJS` on `InMemoryHashScalaJSModule` with `scalaJSMinify = true` (default); assert the output JS files are smaller than their `fastLinkJS` counterparts (confirming terser ran)
18
+
-[x] 3.3 Call `fastLinkJS` on `InMemoryHashScalaJSModule`; assert in-memory output is present and that fastLinkJS does not write into the directory - i.e. no file IO.
19
+
-[x] 3.4 Call `fullLinkJS` on `InMemoryHashScalaJSModule`; assert that files are written to disk and that terser is invoked when `scalaJSMinify = true` and not invoked when `scalaJSMinify = false`
When Scala.js emits WASM output, the JS loader files it produces use WebAssembly-specific bootstrap patterns that terser can break. This spec defines the passthrough behaviour for JS files in WASM builds, and the terser gating behaviour for non-WASM production builds.
6
+
7
+
### Requirement: FileBasedContentHashScalaJSModule does not run terser on WASM builds
8
+
When `scalaJSExperimentalUseWebAssembly` is `true`, the `minified` task in `FileBasedContentHashScalaJSModule` SHALL copy all files unchanged to the output directory instead of passing JS files through `terser`.
9
+
10
+
#### Scenario: JS loader files are copied unchanged in WASM mode
11
+
-**WHEN** a `FileBasedContentHashScalaJSModule` with `scalaJSExperimentalUseWebAssembly = true` runs `minified`
12
+
-**THEN** all `.js` files in the output SHALL be the same size as the corresponding files in the `fullLinkJS` output
13
+
14
+
#### Scenario: minified succeeds in WASM mode without terser
15
+
-**WHEN** a `FileBasedContentHashScalaJSModule` with `scalaJSExperimentalUseWebAssembly = true` runs `minified`
16
+
-**THEN** the task SHALL complete successfully without invoking `terser`
17
+
18
+
#### Scenario: WASM file is included in minified output
19
+
-**WHEN** a `FileBasedContentHashScalaJSModule` with `scalaJSExperimentalUseWebAssembly = true` runs `minified`
20
+
-**THEN** the output directory SHALL contain a content-hashed `.wasm` file copied from the `fullLinkJS` output
21
+
22
+
### Requirement: InMemoryHashScalaJSModule runs terser during fullLinkJS for non-WASM builds
23
+
When `scalaJSExperimentalUseWebAssembly` is `false` and `scalaJSMinify` is `true`, `InMemoryHashScalaJSModule.fullLinkJS` SHALL invoke `terser` on each JS file before hashing and writing to `Task.dest`.
24
+
25
+
#### Scenario: fullLinkJS JS output is smaller than fastLinkJS when scalaJSMinify is true
26
+
-**WHEN** an `InMemoryHashScalaJSModule` with `scalaJSMinify = true` runs `fullLinkJS`
27
+
-**THEN** the total size of `.js` files in `Task.dest` SHALL be smaller than the total size of `.js` files produced by `fastLinkJS`
28
+
29
+
#### Scenario: fullLinkJS skips terser when scalaJSMinify is false
30
+
-**WHEN** an `InMemoryHashScalaJSModule` with `scalaJSMinify = false` runs `fullLinkJS`
31
+
-**THEN**`terser` SHALL NOT be invoked
32
+
33
+
#### Scenario: fastLinkJS does not run terser
34
+
-**WHEN** an `InMemoryHashScalaJSModule` runs `fastLinkJS`
35
+
-**THEN**`terser` SHALL NOT be invoked and in-memory output SHALL be present
36
+
37
+
#### Scenario: fullLinkJS WASM path does not run terser
38
+
-**WHEN** an `InMemoryHashScalaJSModule` with `scalaJSExperimentalUseWebAssembly = true` runs `fullLinkJS`
0 commit comments