Production minify
The point of spliceFull is a modern production JS file without a JS toolchain. You do not install Node, Vite,
or esbuild. JDK and sbt were already on the machine. The first build that needs it fetches a pinned esbuild for this
OS/arch through Coursier (sha256 of the binary, then chmod +x). After that it is a cache hit, like a jar.
spliceFast and spliceFull leave the linker's imports in place. esbuild bundles that file into one classic script
(--format=iife, no global name) and resolves each mapped specifier to its pinned file. spliceFast is development
(process.env.NODE_ENV is "development"). spliceFull is the same invocation plus minify (NODE_ENV
"production", --target=es2015). Locals and whitespace shrink. JS property names stay, so a Preact
class extends Component still has .render / .setState. class extends Error keeps super(). There is no
keep-list. Empty spliceLibs does not take this path: the file stays a classic script, and full still minifies it.
spliceClosure still rewrites mapped @JSImports to __splice_* globals before link, still puts an
export * as library bundle ahead of Scala.js, and still does not parse library source. Unused library exports stay
there, so spliceFull is the one that drops them. JDK 21+ for that task only. Property renaming stays off. process
is a stub so ZIO-shaped code runs in a browser.
Three tools, three jobs
Scala.js minify (1.16+, on in fullLink) shortens Scala class fields and methods. The linker has types, so
those names cannot be a JS protocol (render, setState, connectedCallback). That is the type-aware property pass.
esbuild (pinned native binary, fetched the first time a build needs it) bundles the linker's imports. For
spliceFull that same call minifies: locals, syntax, whitespace. JS property names stay. class extends Error
keeps super(). This is the pass Scala.js 1.21 named Vite for, without Node.
Closure runs ADVANCED on Scala.js's output alone, property renaming off, with the library bundle ahead of it and
its __splice_* bindings as externs. npm code was never written for Closure, so Closure never sees library source.
Unused library exports stay in that prefix. That is extra inlining for the Scala side, not the production default.
JDK 21+.
The plugin never runs npm, never reads package.json, and never leaves import "preact" for a bundler to fix.
Sealed pins and one <script> tag are the product.
What Vite actually minifies
Vite production minify shortens local variables, strips whitespace, and drops unused ESM exports. Property names
stay. .setState, .render, and connectedCallback are still those strings. spliceFull does the same with
esbuild. A namespace import keeps only the properties the program reads. Passing the module as a value keeps the
rest. Top-level side effects stay. export * as inside a pinned file is not shaken. CommonJS is not promised to
shrink. A library the linker never imports is left out.
Terser documents mangle.properties as unsafe and off by default. A Preact class component that worked under
Vite is the baseline, not a special case splice has to list names for.
What Closure advanced does that they refuse
Closure ADVANCED can rename properties across the whole program. Google’s interoperability story is externs for
every public name: a whitelist. Mainstream bundlers never took that pass, so they never had to maintain one.
Scala.js emits a subclass of a spliced class as class extends $superClass. Closure cannot see that the super is the
library. Property renaming and per-type disambiguation then split the protocol:
The library’s
.renderis renamed, the override stays literal, and class components render nothing.Or
Component.prototype.setStateis kept, the subclass call becomesa.uJ, and you getis not a function.
Those are the same compiler mistake. spliceFull never takes that pass. spliceClosure turns property renaming,
disambiguation, and closed-world property analysis off for the whole compile. Every .foo stays .foo.
h.render() and if (h.componentWillMount) h.componentWillMount() stay dynamic, so a lazy Scala.js subclass
matches the spliced base. Consumers do not list render / setState / connectedCallback. There is no keep-list API.
Subclassing a spliced class needs no extra setting.
What we still want from Closure
spliceFull (esbuild) is smaller than concatenating unminified fullLink with the same vendor files. Linker DCE
already dropped unused Scala. esbuild then drops unused ESM exports and shortens what is left. On that library axis
spliceClosure is the fatter file: its prefix keeps every export of a mapped module.
spliceClosure adds Closure's dead-code removal and inlining on the Scala side, with property renaming off. It is
not as small as Gmail-era Closure with property renaming. Protocol strings stay. Unused library exports stay too.
Scala.js minify already emits class $c_jl_Throwable extends Error with super(), then getter-only
@JSExport("message") / @JSExport("name"). That is SuperCall. esbuild --target=es2015 keeps it. Closure
languageOut is ES2015 so spliceClosure does not rewrite it to Error.call(this); this.message = ….
Fast vs full
List(
"spliceFast" -> "bundle the linker's imports (development)",
"spliceFull" -> "bundle and minify (production)",
"spliceClosure" -> "Closure on Scala.js; libraries are not shaken",
)List((spliceFast,bundle the linker's imports (development)), (spliceFull,bundle and minify (production)), (spliceClosure,Closure on Scala.js; libraries are not shaken))