Windows single binary application#151
Merged
Merged
Conversation
The shutdown command used Linux-only flags (-h now) which silently did nothing on Windows. Replaced with the Windows equivalent (/s /t 0). The ping command also used Linux-only flags (-c/-W) and passed the IP directly into a shell string, opening a command injection vector. Fixed by using platform-specific flags and validating the IP against a strict IPv4 regex before building the command. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The previous uwsCompanionPlugin copied the uWebSockets.js .node binary
alongside the pkg output. This does not work with SEA (no node_modules).
The new uwsSeaPlugin does a string replacement at bundle time on the
compiled CJS output, replacing require("uWebSockets.js") with an IIFE
that in SEA mode extracts the embedded .node asset from the SEA blob to
a temp directory and loads it via process.dlopen. In dev/pkg mode it
falls through to the regular require() call unchanged.
process.dlopen is used instead of require() for the extracted path
because embedderRequire (the SEA require) cannot load .node files from
arbitrary absolute paths.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Added _isSea() helper that checks node:sea.isSea() to determine whether the process is running inside a Node.js SEA binary. This is included in the IS_PLATFORM_PACKAGED condition so that the static server and other packaged-mode behaviour activate correctly when running as a SEA binary. createRequire(process.execPath) is used instead of import.meta.url because import.meta.url is undefined in Vite's CJS bundle output. process.execPath is always a valid absolute path on all platforms. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
In SEA mode there is no dist/ directory on disk — frontend files are embedded as assets in the SEA blob. StaticServer now detects SEA mode via getSea() (using createRequire(process.execPath) for the same reason as index.ts) and serves all responses directly from sea.getAsset(). Asset keys follow the convention "dist/<relative-path>" matching the keys written into sea-config.json by the build script. A SPA catch-all fallback serves dist/index.html for unknown routes. The original filesystem-based serving path is kept as a fallback for dev mode and pkg (Linux/macOS) builds. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Adds scripts/build-sea-win.mjs which produces a single Windows binary
using Node.js SEA (Single Executable Application) instead of pkg.
The pipeline:
1. Builds frontend (Vite) and backend (Vite CJS)
2. Embeds all dist/ files and uws_win32_x64_<ver>.node as SEA assets
3. Generates the SEA blob via node --experimental-sea-config
4. Copies the node binary from pkg cache or process.execPath
5. Strips the Authenticode signature and clears CFG via patch-pe-no-cfg.mjs
so postject does not leave the binary in a corrupted-signature state
6. Injects the blob with postject
7. Compiles and bundles the Go launcher (see next commit)
The output binary MUST be named node.exe — Windows applies an AppCompat
shim exclusively to executables with that name, which is required for
process.dlopen to load the uWebSockets.js NAPI module without crashing
with 0xC0000005. The launcher (simple-win.exe) provides the user-facing
entry point.
Removes node24-windows-x64 from pkg targets; Windows now uses SEA.
Adds postject devDependency and build:sea:win npm script.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The SEA worker binary must be named node.exe, but users need a
recognisable entry point. launcher-win.go is a small Go program that:
- Reads the footer appended to itself at build time
([8-byte embedded size LE][4-byte magic "SWPN"]) to locate the
embedded node.exe bytes
- On first launch, extracts node.exe to a temp directory keyed by the
embedded size (%TEMP%\swp-node-<hex-size>\node.exe) and reuses the
cache on subsequent launches (validated by file size)
- Spawns the extracted node.exe with inherited stdin/stdout/stderr,
ignores SIGINT so node.exe handles its own graceful shutdown, and
forwards the exit code
The build script appends the SEA node.exe bytes and footer to the
compiled launcher, producing a single distributable file (simple-win.exe)
that contains everything. Go was chosen because it produces a statically
linked native Windows PE with no runtime dependencies (~2 MB overhead).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
build-ci.yaml:
- Renamed the ubuntu job to compilation-unix (builds Linux + macOS
via pkg with cross-compilation tools: qemu, ldid)
- Removed the dead Windows artifact upload (pkg no longer targets
Windows)
- Added a parallel compilation-windows job on windows-latest that
sets up Node 24 and Go, runs build:sea:win, and uploads
bin/win/simple-win.exe as the windows-compiled-archive artifact
release.yaml:
- Split the single job into three:
build-unix (ubuntu): builds Linux + macOS, uploads each as a
separate named artifact
build-windows (windows-latest): builds the SEA binary, uploads
bin/win/simple-win.exe
release: depends on both build jobs, downloads all three
artifacts into release-files/ and publishes them to GitHub
in a single softprops/action-gh-release step
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
sea-config.json and sea-prep.blob are auto-generated by build:sea:win on every build and should not be committed. Added both to .gitignore. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Adds scripts/build-sea-unix.mjs, a single script that builds a SEA
binary for the current Unix platform (Linux or macOS). It follows the
same pipeline as build-sea-win.mjs: builds frontend and backend, embeds
all dist/ files and the platform-specific uWebSockets.js .node file as
SEA assets, generates the blob, and injects it via postject.
Platform differences handled by the script:
- macOS (Mach-O): postject requires --macho-segment-name NODE_SEA,
and the binary must be re-signed with codesign --sign - after
injection (Apple Silicon blocks modified unsigned binaries)
- Linux: no signing or extra flags needed
Unlike Windows, neither Linux nor macOS require the binary to be named
node.exe, so no launcher or self-extraction is needed — the output is a
directly executable single file (bin/simple-linux or bin/simple-macos).
Removes @yao-pkg/pkg devDependency and its config block. The
build:executable and build:executable-compressed scripts are removed;
all three platforms now use the build:sea:* family of scripts.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
All three platforms now build on their native runner using Node.js SEA, removing the need for qemu, ldid, and cross-compilation from ubuntu. build-ci.yaml: - compilation-linux (ubuntu-latest): build:sea:linux - compilation-macos (macos-latest): build:sea:macos - compilation-windows (windows-latest): build:sea:win + Go release.yaml: - build-linux (ubuntu-latest) - build-macos (macos-latest) - build-windows (windows-latest) - release: waits for all three, collects artifacts, publishes
- My laptop uses latest NodeJS 26 over which the compilation is failing - Add a catch failing the compilation with explicit message
- Strong assumption that a Mac Mini == M2L2
ad226cb to
4d8f109
Compare
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.




Pull Request
Checklist
Description
Fix single binary application issue on windows
Related Issue