Electron 40 is affected by GHSA-9f4c-93c8-jc8g (CVSS 7.2): a sandboxed iframe can bypass the allow-popups restriction through the OpenURL navigation path. Motionity renders no iframes, so the practical exposure was near zero, but Electron only patches its latest three majors — on 40 there would be no fix for the next advisory either. npm audit now reports zero vulnerabilities. electron/main.js needed no changes: it uses only APIs stable across 40 to 43. While verifying the packaged output, the asar turned out to carry @ffmpeg's transitive dependencies (node-fetch, whatwg-url, regenerator-runtime and friends). The app loads the vendored UMD bundle from src/vendor/ffmpeg/ and requires nothing outside electron and node builtins, so build.files now excludes node_modules wholesale instead of naming @ffmpeg — the same result without having to track a dependency tree that is not ours. Verified: electron-builder 26.15.3 packages electron 43.4.0, and the asar contains the five vendored ffmpeg files and zero node_modules entries. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
8.5 KiB
Packaging Motionity
Three distribution targets share one source tree (src/, a plain static app):
| Target | Output | Build host |
|---|---|---|
| Desktop | Motionity Setup 1.0.0.exe, Motionity-1.0.0-x64.AppImage, .flatpak |
Windows for .exe, Linux (or WSL2) for AppImage/Flatpak |
| Container | motionity:latest, ~86 MB on disk / ~57 MB pulled |
any Docker host |
| Bare metal | src/ behind Node, nginx or Caddy |
any |
0. One prerequisite for every target: vendor the assets
npm install # now required for every target, not just the desktop ones
npm run vendor # ~24 MB, writes src/vendor/ (gitignored)
index.html used to pull jQuery, fabric.js, lottie, pickr, selection-js, the
WebFont loader and Inter from five different CDNs. scripts/vendor.mjs
downloads all of them into src/vendor/ and the app now references only those
local copies. Without this step the page loads but every script tag 404s.
ffmpeg.wasm is handled differently: vendor.mjs copies it out of
node_modules instead of downloading it, which is why npm install is now a
prerequisite everywhere. package-lock.json pins those packages by integrity
hash, so the bytes that reach the image are the bytes npm verified. The asm.js
build this replaced was fetched from a public archive.org mirror with no
integrity check of any kind — a changed object there would have executed in the
page unnoticed.
Two consequences worth knowing:
@ffmpeg/core-stis the single-threaded core, chosen deliberately. The default@ffmpeg/coreis built with pthreads and needsSharedArrayBuffer, which requires COOP/COEP cross-origin isolation, which would break the Pixabay, Unsplash and Google Fonts requests the editor makes.- The two
@ffmpeg/*packages aredependencies, notdevDependencies, so the Docker vendor stage cannpm ci --omit=devwithout pulling in electron. That makes electron-builder want to bundle them into the asar too, sobuild.filesexcludesnode_modules/**outright — the packaged app requires nothing butelectronand node builtins, and the copies it loads live insrc/vendor/ffmpeg/.
The Docker build runs npm ci and the vendor step inside the image, so it is
the one target where you can skip both locally.
1. Desktop — Electron
electron/main.js starts the same static server the bare-metal target uses, on
127.0.0.1 with a random port, and points the window at it.
This is deliberate, do not "simplify" it to loadFile(). Chromium exposes
WebCodecs (VideoEncoder, the fast exporter in src/js/render.js) and
IndexedDB (project storage via localbase) only in a secure context. file://
is not one; a loopback HTTP origin is. Load the app from file:// and export
silently falls back to real-time MediaRecorder capture and projects stop
saving.
npm run dev # run the desktop app from source
npm run dist:win # NSIS installer + portable .exe -> dist/
npm run dist:appimage # .AppImage -> dist/
npm run dist:flatpak # .flatpak -> dist/
npm run dist:linux # both Linux targets
Each dist:* script re-runs vendor and regenerates build/icon.png
(scripts/make-icon.cjs rasterises the logo geometry with zlib only — no
ImageMagick, no sharp).
If the NSIS step fails with "Access denied" on Motionity.exe
On a locked-down Windows machine the security agent can take an exclusive lock
on the freshly written 214 MB unsigned dist/win-unpacked/Motionity.exe, and
the 7-Zip step that builds the installer payload then cannot read it. The
symptom is a build that produces dist/win-unpacked/ correctly and dies right
after "Archive size":
.\Motionity.exe : Accès refusé.
WARNING: Cannot open 2 files
The ACL is intact (the owner still has FullControl), so this is a filter driver, not a permissions problem. Fixes, in order of preference:
- Exclude the repository's
dist/directory in the endpoint protection agent. - Build on a machine or CI runner without that agent.
- Ship
dist/win-unpacked/—electron-builder --win diris unaffected.
Running npm run dev from a VS Code terminal
VS Code exports ELECTRON_RUN_AS_NODE=1, which turns the electron binary into
plain Node and leaves the electron module empty. main.js detects this and
prints a message. Fix it in the shell:
env -u ELECTRON_RUN_AS_NODE npm run dev # bash
$env:ELECTRON_RUN_AS_NODE=$null; npm run dev # PowerShell
Is WSL enough for AppImage and Flatpak?
AppImage: yes. electron-builder produces the squashfs itself, so no FUSE is
needed at build time. To run the result inside WSL you need libfuse2
(or ./Motionity-1.0.0-x64.AppImage --appimage-extract-and-run), and WSLg on
Windows 11 gives you the GUI.
Flatpak: technically yes, practically annoying. flatpak-builder runs under
WSL2 (the kernel has the user namespaces and /dev/fuse that bubblewrap needs),
but you must install the runtimes by hand first and there is no
xdg-desktop-portal to fall back on. If it fights you, build it in a Linux
container instead — it is the same command with fewer moving parts.
.exe: no. Build it on the Windows side. Cross-building NSIS from Linux
needs Wine and rules out signing.
This machine currently has no WSL distro other than docker-desktop, so the
Linux targets have not been run here. Setup, from PowerShell:
wsl --install -d Ubuntu
Then inside Ubuntu:
sudo apt update
sudo apt install -y nodejs npm libfuse2 # AppImage
sudo apt install -y flatpak flatpak-builder elfutils # Flatpak
flatpak remote-add --if-not-exists --user flathub \
https://dl.flathub.org/repo/flathub.flatpakrepo
flatpak install --user -y flathub \
org.freedesktop.Platform//23.08 \
org.freedesktop.Sdk//23.08 \
org.electronjs.Electron2.BaseApp//23.08
cd /mnt/c/Users/<you>/git\ azuze/motionity-2
npm install
npm run dist:linux
Note that building on /mnt/c is slow. Copying the tree into the WSL
filesystem (~/motionity) is several times faster.
2. Docker
docker build -t motionity:latest .
docker run --rm -p 8080:8080 motionity:latest
# or: docker compose up --build
The runtime image is static-web-server (Rust) on Alpine — 9 MB of base image,
no Node, no shell tooling, running as UID 65534. Everything above that is the
app itself: 46 MB of bundled stock media plus 20 MB of vendored libraries.
Two knobs if the size matters more than offline completeness:
# -23 MB: MP4/GIF export reports itself unavailable (WEBM export is unaffected).
# There is no runtime download to fall back on any more, by design.
docker build --build-arg WITH_FFMPEG=0 -t motionity:slim .
# -33 MB: drop the bundled royalty-free music library (removes the Audio panel
# presets; uploads still work). Add to .dockerignore:
# src/assets/audio
3. Bare metal
npm run vendor
npm start # http://127.0.0.1:8080
HOST=0.0.0.0 PORT=3000 npm start
scripts/server.cjs is dependency-free: correct MIME types, byte ranges (the
audio and video panels seek), no directory listings, path-traversal guard. Node
18+.
For a real install, deploy/ has a systemd unit plus nginx and Caddy configs
that serve src/ directly, no Node process involved:
sudo cp -r . /opt/motionity
sudo cp deploy/motionity.service /etc/systemd/system/
sudo systemctl enable --now motionity
The secure-context rule, once more
Both the container and the bare-metal targets serve plain HTTP. That is fine on
http://localhost, which browsers treat as secure. It is not fine on a LAN
address or a domain: VideoEncoder disappears (export falls back to slow
real-time capture) and IndexedDB is blocked (projects stop saving), with no
error message beyond a console warning.
Anything beyond localhost needs TLS. deploy/Caddyfile is the shortest path —
it obtains the certificate itself.
What still needs the internet
Vendoring makes the editor start and export offline. Three features remain online by design, and degrade quietly rather than breaking:
- Google Fonts — the font picker loads families through
WebFont.load, andsrc/js/init.jslists them from the Google Fonts API. Offline, text falls back to a system font. Only Inter (the UI font) is bundled. - Pixabay — the Images, Videos and Audio browsers search Pixabay live.
- Unsplash — sample images on the empty-state screen.
MP4 and GIF export used to be a fourth: the encoder came from archive.org at conversion time. It is now vendored, so export works fully offline.