scmJS docs

Desktop build

desktop/ is an Electron shell around the same web bundle. main.ts serves dist/ under app://scmjs/, searches the disk for the game archives and extracts them into the app's data folder, opens maps handed over by the file manager, guards the window's close while a map has unsaved changes, and holds the updater. preload.ts exposes that to the page as window.scmjsDesktop, typed in src/gamedata/desktop.ts.

npm run desktop                 # bundle the main process and run Electron against dist/
SCMJS_DEV_URL=http://localhost:5173 npm run desktop    # against the dev server instead
npm run build:desktop           # web build + main bundle + electron-builder for this OS

npm run build:desktop takes the packaging step's platform, architectures and targets on the command line; with none it builds this OS with the targets electron-builder.yml lists, which is what CI does:

npm run build:desktop -- win                 # Windows: nsis + zip
npm run build:desktop -- win nsis            # just the installer
npm run build:desktop -- linux AppImage x64 arm64
npm run build:desktop -- mac dmg arm64
npm run build:desktop -- --dir               # unpacked app, no installer: the fast check
npm run build:desktop -- win --skip-web      # repackage the dist/ already on disk
npm run build:desktop -- --skip-plugins      # leave plugins/ alone (no vendoring fetch)

Platforms are win, mac and linux; architectures x64, ia32, arm64, armv7l and universal; any electron-builder target name (nsis, dmg, zip, AppImage, deb, …) applies to every platform named. --skip-web and --skip-main reuse the bundles on disk, --publish <mode> and anything after a bare -- go to electron-builder as they are. Cross-building is electron-builder's business: the script warns about combinations that need tooling the machine may lack (a macOS installer off a Mac, an NSIS installer without Wine) and runs them anyway; --dir and zip cross-build with nothing installed. Output lands in release/.

The build also writes THIRD-PARTY-NOTICES.txt into dist/: the license text of every runtime dependency and of the compiled-in plugins, generated by scripts/lib/notices.mjs from package.json rather than from a list, so it rides in the web zip, the installers and the container image without anyone maintaining it.

The desktop build and the container image also carry the runtimes the default plugins would otherwise download on first use — today eudplib's, Pyodide and the eudplib wheel, about 15 MB — so building an EUD map needs no network there. scripts/bundle-plugin-runtimes.mjs reads a runtime.json from each pinned default's repository, fetches the files it lists (with their licences) and puts them in dist/plugin-runtime/<plugin>/<version>/; downloads are kept in the gitignored plugin-runtimes/, so only the first build after a pin moves fetches anything. A plugin finds its copy by its own version, so one updated past the editor's goes back to downloading. The hosted editor and the web zip carry none: they are online anyway, and jsDelivr serves those downloads rather than the site's own bandwidth.

What ships is decided by electron-builder.yml: dist/ and desktop/dist/ only, never node_modules (Vite bundles everything, mopaq and the extraction included) and never the game data a developer's public/ may hold. Windows gets an NSIS installer and a zip (not electron-builder's portable target, which re-extracts the whole app on every launch), macOS a dmg per architecture, Linux an AppImage and a .deb. The installers register .scm, .scx and .chk. Asset names carry no version, for the reason under Download links. Only en-US of Chromium's locales is included, since the editor has no translations. Builds are unsigned: Windows shows SmartScreen's "unrecognized app" screen, and macOS refuses an unsigned app until the user clears the quarantine attribute.

The app's data folder is the platform's usual one for an app named scm-js (%APPDATA%\scm-js on Windows, ~/.config/scm-js on Linux, ~/Library/Application Support/scm-js on macOS). It holds the extracted game data, the remembered window geometry in window.json, and startup.log, the milestones of the last launch (process start, main script, window created, dom-ready, which signal showed the window, and the window geometry at each step). SCMJS_TRACE=1 echoes the same to the terminal. When a launch "hangs and then opens", that log is what tells a slow disk, a virus scanner reading the binaries, and a slow first paint apart.

The launch itself is arranged so that something is on screen early: the window is created hidden and shown on the renderer's first paint, index.html carries a boot splash in plain markup so that paint needs no stylesheet or bundle, and the app mounts its chrome two frames after the splash. The saved maximized state is applied after the window is shown. The reasons, and the measurements behind them, are in the comments in desktop/main.ts and src/App.tsx.

Running the Windows build from WSL#

The desktop build cross-builds from WSL, but where the packaged app is run from changes the launch more than anything in the code: \\wsl.localhost\… is a network share, and Chromium loading its binaries and then every asset through it takes minutes. The same build, launched from the WSL filesystem and from C::

milestone /mnt/c/… \\wsl.localhost\…
main script evaluated 47 ms 628 ms
window created 142 ms 963 ms
dom-ready 215 ms not within 25 s

So copy it over first:

npm run build:desktop -- win --dir          # release/win-unpacked
cp -r release/win-unpacked /mnt/c/Users/<you>/scmjs-test
SCMJS_TRACE=1 WSLENV=SCMJS_TRACE /mnt/c/Users/<you>/scmjs-test/scmJS.exe

WSLENV forwards the variable into the Windows process. From WSL, the app's data folder is /mnt/c/Users/<you>/AppData/Roaming/scm-js.

In-app updates#

The desktop app checks GitHub releases at startup (a preference, on by default) and through Help ▸ Check for Updates…, using electron-updater over the latest*.yml feed files the release workflow uploads beside the installers. Finding one raises a toast with a Download button rather than a dialog; nothing downloads or installs unasked, and installing goes through the same unsaved-changes gate as closing the window. A nightly build follows the nightlies and a stable build follows stable releases; a preference lets a stable install opt into nightlies.

Windows, the AppImage and the .deb install updates themselves. macOS can see one but not install it until the app is code-signed, so the dialog offers the release page there. A development run (npm run desktop) and an unpacked Linux folder report that they cannot check, since the updater has nothing it could replace. desktop/updater.ts is the main-process side, src/editor/updates.ts the state machine and every string, tests/updates.test.ts the cases.

scmJS 0.7.3 + main@e2d8ba5 · Generated from the repository. The source is under the MIT license, which does not cover what ATTRIBUTION.md lists. StarCraft and Brood War are trademarks of Blizzard Entertainment; this project ships none of their data.