---
description: Scala.js browser packaging — compilation pipeline, module kinds, HTML wrappers, asset bundling, and dev/production workflows
---

# Scala.js Browser Packaging

## Scope

This is **not** a multiarch-scala feature. Everything described here is pure
`sbt-scalajs`. The skill exists because projects using `sbt-multiarch-scala`
often also target the browser via Scala.js, and browser packaging has enough
moving parts to warrant a dedicated reference.

## Compilation Pipeline

```
Scala source (.scala)
  │
  ▼  scalac with Scala.js compiler plugin
Scala.js IR (.sjsir files in target/)
  │
  ▼  Scala.js linker (fastLinkJS or fullLinkJS)
JavaScript output (.js files)
  │
  ▼  wrapped in index.html + assets
Browser-ready application
```

The linker is the Scala.js-specific step. It reads `.sjsir` intermediate
representation, resolves dependencies, eliminates dead code, and emits
JavaScript (or optionally WASM). Two linker modes exist:

| Task | Speed | Output size | Use case |
|------|-------|-------------|----------|
| `fastLinkJS` | Fast, incremental | Large, unoptimized | Development |
| `fullLinkJS` | Slow (Closure Compiler) | Small, optimized | Production |

## Enabling ScalaJSPlugin

### Standalone project

```scala
// project/plugins.sbt
addSbtPlugin("org.scala-js" % "sbt-scalajs" % "1.21.0")

// build.sbt
lazy val browser = project.in(file("browser"))
  .enablePlugins(ScalaJSPlugin)
  .settings(
    scalaVersion := "3.8.3",
    scalaJSUseMainModuleInitializer := true,
    Compile / mainClass := Some("myapp.Main")
  )
```

### projectMatrix JS platform

```scala
// project/plugins.sbt
addSbtPlugin("com.eed3si9n"  % "sbt-projectmatrix" % "0.11.0")
addSbtPlugin("org.scala-js"  % "sbt-scalajs"       % "1.21.0")
addSbtPlugin("org.scala-native" % "sbt-scala-native" % "0.5.10")

// build.sbt
val scala3 = "3.8.3"

lazy val core = (projectMatrix in file("core"))
  .settings(name := "my-app-core", scalaVersion := scala3)
  .jvmPlatform(scalaVersions = Seq(scala3))
  .jsPlatform(scalaVersions = Seq(scala3), settings = Seq(
    scalaJSUseMainModuleInitializer := true,
    scalaJSLinkerConfig ~= { _.withModuleKind(ModuleKind.ESModule) }
  ))
  .nativePlatform(scalaVersions = Seq(scala3))
```

When using projectMatrix, the JS subproject ID is `coreJS3` (the `JS` suffix
plus Scala binary version). JVM is `core3` (no suffix), Native is `coreNative3`.

## scalaJSUseMainModuleInitializer

Set to `true` for **applications** that need a `main` method entry point.
Without this, the linker produces a library — it exports symbols but nothing
executes on load.

```scala
scalaJSUseMainModuleInitializer := true
```

Your `Main` object must extend `scala.scalajs.js.annotation.JSExportTopLevel`
or simply have a standard `def main(args: Array[String]): Unit`.

## ModuleKind

Controls how the JavaScript output is structured.

```scala
import org.scalajs.linker.interface.ModuleKind

scalaJSLinkerConfig ~= { _.withModuleKind(ModuleKind.ESModule) }
```

| ModuleKind | Output | Loading | Notes |
|------------|--------|---------|-------|
| `NoModule` (default) | Single `.js` file, no import/export | `<script src="main.js">` | Simplest; no module system |
| `CommonJSModule` | `require()`-style modules | Node.js / bundler | Not directly loadable in browsers |
| `ESModule` | ES module with `import`/`export` | `<script type="module">` | Browser-native; **required** for WASM backend |

**Recommendation**: Use `ESModule` for browser targets. It enables native
browser module loading, better tree-shaking by bundlers, and is the only
option compatible with the experimental WASM backend.

## Linking: fastLinkJS vs fullLinkJS

### Development: fastLinkJS

```bash
sbt --client 'browser/fastLinkJS'
# or for projectMatrix:
sbt --client 'coreJS3/fastLinkJS'
```

- Incremental (only re-links changed code)
- No Closure Compiler optimization
- Output in `target/scala-3.x.y/<project>-fastopt/`
- Use for rapid iteration with a local dev server

### Production: fullLinkJS

```bash
sbt --client 'browser/fullLinkJS'
# or for projectMatrix:
sbt --client 'coreJS3/fullLinkJS'
```

- Full dead-code elimination + Closure Compiler advanced optimizations
- Significantly smaller output
- Output in `target/scala-3.x.y/<project>-opt/`
- Use for deployment and release packaging

### Output files

The linker produces its output in a directory, not a single file. For
`ESModule`, the main entry point is typically `main.js`. For `NoModule`,
it may be named after the project.

```
target/scala-3.8.3/my-app-fastopt/
├── main.js          # Entry point
├── main.js.map      # Source map (development only)
└── internal-*.js    # Additional modules (ESModule split)
```

## HTML Wrapper Pattern

The browser needs an `index.html` to load the JavaScript output. This is
not generated by sbt-scalajs — you create it yourself.

### Minimal index.html (ESModule)

```html
<!DOCTYPE html>
<html>
<head>
  <meta charset="UTF-8">
  <title>My App</title>
  <style>
    body { margin: 0; overflow: hidden; }
    canvas { display: block; }
  </style>
</head>
<body>
  <canvas id="canvas"></canvas>
  <script type="module" src="main.js"></script>
</body>
</html>
```

### Minimal index.html (NoModule)

```html
<!DOCTYPE html>
<html>
<head>
  <meta charset="UTF-8">
  <title>My App</title>
</head>
<body>
  <canvas id="canvas"></canvas>
  <script src="main.js"></script>
</body>
</html>
```

### Directory layout for browser deployment

```
dist/
├── index.html
├── main.js            # Copied from fullLinkJS output
├── main.js.map        # Optional, for debugging
└── assets/
    ├── textures/
    │   └── player.png
    ├── audio/
    │   └── bgm.ogg
    └── data/
        └── levels.json
```

## Asset Bundling

### The problem

Scala.js does not have access to `java.io.File`. Resources from
`src/main/resources/` are not automatically available in the browser.
You must explicitly copy resource files alongside the JavaScript output
and load them via HTTP (e.g., `fetch()`, `XMLHttpRequest`, or `<img>` tags).

### Manual approach

1. Place assets in `src/main/resources/` (or a dedicated `assets/` directory)
2. After linking, copy them to the output directory
3. Load them at runtime via relative URL paths

### sbt task for copying resources

```scala
lazy val packageBrowser = taskKey[File]("Package browser build")

packageBrowser := {
  val linkResult = (Compile / fullLinkJS).value
  val outputDir  = target.value / "browser-dist"

  // Copy JS output
  val jsDir = linkResult.data.publicModules.head.jsFileName
  val jsSource = (Compile / fullLinkJS / scalaJSLinkerOutputDirectory).value
  IO.copyDirectory(jsSource, outputDir)

  // Copy resources
  val resourceDir = (Compile / resourceDirectory).value
  if (resourceDir.exists()) {
    IO.copyDirectory(resourceDir, outputDir / "assets")
  }

  // Generate index.html
  val html = s"""<!DOCTYPE html>
    |<html>
    |<head><meta charset="UTF-8"><title>My App</title></head>
    |<body>
    |  <canvas id="canvas"></canvas>
    |  <script type="module" src="main.js"></script>
    |</body>
    |</html>""".stripMargin
  IO.write(outputDir / "index.html", html)

  outputDir
}
```

### SGE reference: sgePackageBrowser

SGE's `sgePackageBrowser` task is a production-grade example of browser
packaging. It performs these steps:

1. Runs `fullLinkJS` to produce optimized JavaScript
2. Copies `main.js` to the output directory
3. Copies all resource files to an `assets/` subdirectory
4. Generates an `assets.txt` manifest listing every file in `assets/`
   (so the app can discover available resources without directory listing)
5. Generates an `index.html` that loads `main.js` as an ES module

The `assets.txt` manifest pattern solves a key browser limitation: there
is no way to list files in a directory via HTTP. The manifest file lets the
application enumerate available assets at runtime.

## Dev Server

### Using fastLinkJS output

For development, serve the `fastLinkJS` output directory with any HTTP server.
The server must serve files with correct MIME types (especially
`application/javascript` for `.js` files and `application/wasm` for `.wasm`).

```bash
# After running fastLinkJS, serve the output directory
# Using Node.js http-server (install: npm install -g http-server)
cd target/scala-3.8.3/my-app-fastopt
http-server -c-1 -p 8080

# Or using Python (if allowed by project rules)
# python3 -m http.server 8080
```

For ESModule output, the server must support the `application/javascript`
MIME type for `.js` files served with `import` statements.

### Continuous development workflow

In one terminal, run sbt in watch mode:

```bash
sbt --client '~coreJS3/fastLinkJS'
```

This recompiles and re-links on every source change. In another terminal,
run the HTTP server pointing at the output directory. Refresh the browser
to pick up changes.

## sbt run vs Browser

```bash
sbt --client 'browser/run'
```

This executes the Scala.js output via **Node.js**, not a browser. Node.js
does not have browser APIs (`document`, `window`, `canvas`, `fetch` for
relative URLs). Use `sbt run` only for code that is pure computation or
uses Node.js-compatible APIs.

To test in an actual browser, use the dev server approach described above
or a test framework with browser integration (e.g., Playwright).

## Platform Limitations

Scala.js does **not** support:

| Missing | Alternative |
|---------|-------------|
| `java.io.File` | `fetch()` API, `scala.scalajs.js.typedarray` |
| `java.net.Socket` | `fetch()`, `WebSocket`, `XMLHttpRequest` |
| Threads (`java.lang.Thread`) | `scala.scalajs.js.Promise`, Web Workers |
| Reflection (`Class.forName`) | Explicit registration, `@JSExportTopLevel` |
| `System.exit()` | Not applicable in browser context |
| Blocking I/O | Everything is async in the browser |

Use platform-specific source directories (`src/main/scala-js/`) to provide
browser-specific implementations of APIs that differ across platforms.

## Testing

### Test configuration

```scala
// Test linker config can differ from main
Test / scalaJSLinkerConfig ~= {
  _.withModuleKind(ModuleKind.CommonJSModule)
}
```

Tests run on Node.js by default. For browser-based testing, configure
a browser JS environment:

```scala
// project/plugins.sbt
libraryDependencies += "org.scala-js" %% "scalajs-env-selenium" % "1.1.1"

// build.sbt
Test / jsEnv := new org.scalajs.jsenv.selenium.SeleniumJSEnv(
  new org.openqa.selenium.chrome.ChromeOptions()
)
```

### Playwright integration (SGE pattern)

SGE uses Playwright for browser smoke tests:

1. `sgePackageBrowser` produces the packaged output
2. A Node.js script starts a local server serving the output
3. Playwright launches a headless browser, navigates to the page
4. Assertions check that the canvas renders and no errors occur

This validates the full pipeline: Scala.js compilation, linking, HTML
wrapper, asset loading, and browser execution.

## Complete Example: projectMatrix with Browser Packaging

```scala
// project/plugins.sbt
addSbtPlugin("com.eed3si9n"       % "sbt-projectmatrix"   % "0.11.0")
addSbtPlugin("org.scala-js"       % "sbt-scalajs"         % "1.21.0")
addSbtPlugin("org.scala-native"   % "sbt-scala-native"    % "0.5.10")
addSbtPlugin("com.kubuszok"       % "sbt-multiarch-scala" % "0.1.2")

// build.sbt
val scala3 = "3.8.3"

val commonSettings = Seq(
  scalaVersion := scala3,
  scalacOptions ++= Seq("-deprecation", "-feature", "-no-indent")
)

lazy val core = (projectMatrix in file("core"))
  .settings(commonSettings)
  .settings(name := "my-game-core")
  .jvmPlatform(scalaVersions = Seq(scala3))
  .jsPlatform(scalaVersions = Seq(scala3), settings = Seq(
    scalaJSUseMainModuleInitializer := true,
    scalaJSLinkerConfig ~= { _.withModuleKind(ModuleKind.ESModule) },
    Compile / mainClass := Some("mygame.Main")
  ))
  .nativePlatform(scalaVersions = Seq(scala3))

// Browser packaging task on the JS platform
lazy val coreJS3 = core.js(scala3)
```

### Build commands

```bash
# Development build
sbt --client 'coreJS3/fastLinkJS'

# Production build
sbt --client 'coreJS3/fullLinkJS'

# Run via Node.js (not a browser)
sbt --client 'coreJS3/run'

# Run tests
sbt --client 'coreJS3/test'
```

## Troubleshooting

### 1. "TypeError: package scala.scalajs.js does not have a member method async"

**Cause**: sbt-scalajs version too old for Scala 3.8.x
**Fix**: Use sbt-scalajs >= 1.20.0 with Scala 3.8.x

### 2. JavaScript output not loading in browser

**Symptom**: Blank page, console shows MIME type or CORS errors
**Cause**: Serving files without an HTTP server (file:// protocol) or wrong MIME types
**Fix**: Use an HTTP server. ES modules require `text/javascript` MIME type and
do not work over `file://` due to CORS restrictions.

### 3. "ReferenceError: document is not defined"

**Symptom**: Error when running via `sbt run`
**Cause**: `sbt run` uses Node.js, which has no DOM
**Fix**: Browser APIs are only available in an actual browser. Use `sbt run`
only for non-DOM code. Test DOM code via a dev server or Playwright.

### 4. Resources not found at runtime

**Symptom**: `fetch("assets/texture.png")` returns 404
**Cause**: Resource files not copied alongside JS output
**Fix**: Copy `src/main/resources/` to the output directory. Use a packaging
task (see "sbt task for copying resources" above) or serve resources from a
separate directory with the correct URL mapping.

### 5. Large output size with fastLinkJS

**Symptom**: `main.js` is several MB in development
**Cause**: `fastLinkJS` does not optimize — this is expected
**Fix**: Use `fullLinkJS` for production. Development size is not indicative
of final bundle size. Closure Compiler typically reduces output by 5-10x.

### 6. ES modules fail with "import.meta" errors in Node.js tests

**Symptom**: Test failures when using `ModuleKind.ESModule`
**Cause**: Node.js test runner may not support ES modules by default
**Fix**: Use `CommonJSModule` for the test configuration:

```scala
Test / scalaJSLinkerConfig ~= {
  _.withModuleKind(ModuleKind.CommonJSModule)
}
```

### 7. Source maps not working

**Symptom**: Browser debugger shows compiled JavaScript, not Scala source
**Cause**: Source map file not served or not found
**Fix**: Ensure `.js.map` files are in the same directory as the `.js` files
and the HTTP server serves them. Source maps are generated by default for
`fastLinkJS` but not for `fullLinkJS` (enable with
`scalaJSLinkerConfig ~= { _.withSourceMap(true) }`).
