fix(web): bundle deps into the server (noExternals) — kill the 47k-file install
apple / swift (push) Successful in 1m0s
ci / rust (push) Successful in 1m18s
ci / web (push) Successful in 43s
ci / docs-site (push) Successful in 1m4s
android / android (push) Successful in 3m26s
deb / build-publish (push) Successful in 2m37s
apple / screenshots (push) Successful in 5m9s
decky / build-publish (push) Successful in 14s
docker / build-push (--build-arg FEDORA_VERSION=44, ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm) (push) Successful in 5s
docker / build-push (., web/Dockerfile, punktfunk-web) (push) Successful in 25s
docker / build-push (ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Successful in 4s
windows-host / package (push) Successful in 6m51s
docker / build-push (ci, ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Successful in 4s
ci / bench (push) Successful in 4m35s
docker / build-push (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 47s
rpm / build-publish (fedora-44, punktfunk-fedora44-rpm) (push) Successful in 9m3s
rpm / build-publish (bazzite, punktfunk-fedora-rpm) (push) Successful in 9m8s
docker / deploy-docs (push) Successful in 19s

The Windows installer ballooned to 154 MB and installed forever because the node-server
bundle externalized the WHOLE @unom/ui dependency tree (payload, lexical, date-fns,
prismjs…) to .output/server/node_modules — 47,567 files / 730 MB copied into Program
Files. Set Nitro `noExternals: true` so every dependency is bundled + tree-shaken into the
server output: .output drops to ~75 files / 10 MB, and the bare external imports
(srvx, seroval…) bun couldn't resolve at runtime are gone — so the console runs on bun
(no node, no node_modules), which is the issue we previously worked around with node.

Windows installer now ships bun.exe + the ~75-file .output (was node.exe + a node_modules
forest) and runs `bun .output\server\index.mjs`:
- windows-host.yml: fetch a pinned portable bun (build tool AND shipped runtime); drop the
  node fetch + the .output/server install; smoke-boot under the bundled bun.
- pack-host-installer.ps1 / punktfunk-host.iss: -NodeExe -> -BunExe; stage {app}\bun\bun.exe.
- web-run.cmd / build-web.ps1: run/restart on bun; docs updated.

Net win everywhere: the Linux .deb shrinks (node still runs the self-contained output), and
the docker web image — which already ran `bun run .output/server/index.mjs` with only
.output copied — is fixed (the externals had no node_modules to resolve at runtime).

Validated locally: noExternals build = 75 files / 10 MB; node AND bun both serve /login
(200) + static assets (200) + gate /api (401).

(A true single binary via `bun build --compile` is blocked for now: Nitro serves public
assets from an import.meta-relative path `--compile` doesn't embed (/$bunfs/public); the
75-file payload is the clean result.)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-22 21:18:31 +02:00
parent e4e34fdb48
commit de232ec2f7
10 changed files with 77 additions and 87 deletions
+24 -37
View File
@@ -1,7 +1,7 @@
# Build the punktfunk Windows HOST as a signed Inno Setup installer and publish it to Gitea's generic # Build the punktfunk Windows HOST as a signed Inno Setup installer and publish it to Gitea's generic
# package registry, so a Windows GPU box can install the streaming host (SYSTEM service + bundled # package registry, so a Windows GPU box can install the streaming host (SYSTEM service + bundled
# SudoVDA virtual-display driver + the web management console, run by a scheduled task on a bundled # SudoVDA virtual-display driver + the web management console, run by a scheduled task on a bundled
# Node) from one signed setup.exe. Runs on the self-hosted Windows runner # bun) from one signed setup.exe. Runs on the self-hosted Windows runner
# (host mode; scripts/ci/setup-windows-runner.ps1) — same MSVC/Windows-SDK/LLVM env as windows.yml. # (host mode; scripts/ci/setup-windows-runner.ps1) — same MSVC/Windows-SDK/LLVM env as windows.yml.
# #
# Why an installer and not MSIX (like the client): the host installs a LocalSystem SCM service that # Why an installer and not MSIX (like the client): the host installs a LocalSystem SCM service that
@@ -104,46 +104,36 @@ jobs:
choco install innosetup -y --no-progress choco install innosetup -y --no-progress
} }
- name: Fetch portable Node runtime (bundled to run the console) - name: Fetch portable bun runtime (build tool + bundled to run the console)
shell: pwsh shell: pwsh
run: | run: |
# The installer ships a self-contained node.exe so the web console runs with no system-Node # ONE pinned bun, used both to BUILD the console and shipped in the installer to RUN it. The
# prerequisite. Pinned LTS (>= 20, matching the punktfunk-web .deb's `nodejs (>= 20)`); the # .output is self-contained (Nitro noExternals — deps bundled + tree-shaken, no node_modules),
# smoke test below validates the .output bundle runs under exactly this node before shipping. # so the installer ships just bun + a ~75-file .output instead of node + a node_modules forest.
$ver = 'v22.11.0' $ver = 'bun-v1.3.14'
$url = "https://nodejs.org/dist/$ver/node-$ver-win-x64.zip" $url = "https://github.com/oven-sh/bun/releases/download/$ver/bun-windows-x64.zip"
New-Item -ItemType Directory -Force -Path C:\t | Out-Null New-Item -ItemType Directory -Force -Path C:\t | Out-Null
$zip = 'C:\t\node.zip'; $dst = 'C:\t\nodedist' $zip = 'C:\t\bun.zip'; $dst = 'C:\t\bundist'
Invoke-WebRequest -Uri $url -OutFile $zip Invoke-WebRequest -Uri $url -OutFile $zip
if (Test-Path $dst) { Remove-Item $dst -Recurse -Force } if (Test-Path $dst) { Remove-Item $dst -Recurse -Force }
Expand-Archive -Path $zip -DestinationPath $dst -Force Expand-Archive -Path $zip -DestinationPath $dst -Force
$node = (Get-ChildItem -Path $dst -Recurse -Filter node.exe | Select-Object -First 1).FullName $bun = (Get-ChildItem -Path $dst -Recurse -Filter bun.exe | Select-Object -First 1).FullName
if (-not $node) { throw "node.exe not found in $url" } if (-not $bun) { throw "bun.exe not found in $url" }
"NODE_EXE=$node" | Out-File -FilePath $env:GITHUB_ENV -Append -Encoding utf8 "BUN_EXE=$bun" | Out-File -FilePath $env:GITHUB_ENV -Append -Encoding utf8
& $node --version & $bun --version
- name: Build + smoke-boot web console (node-server preset) - name: Build + smoke-boot web console (bun)
shell: pwsh shell: pwsh
env: env:
# PAT with read access to the unom org packages — the @unom npm registry needs auth. # PAT with read access to the unom org packages — the @unom npm registry needs auth to BUILD.
REGISTRY_TOKEN: ${{ secrets.REGISTRY_TOKEN }} REGISTRY_TOKEN: ${{ secrets.REGISTRY_TOKEN }}
# Same shape as deb.yml: bun builds the Nitro node-server bundle, node runs it; the installer # The bun fetched above builds the Nitro server AND runs it. noExternals (vite.config) makes the
# bundles web\.output (handed over via WEB_OUTPUT_DIR) + the portable node above. The runner # output self-contained, so there's no .output/server install — the installer ships bun + the
# runs as SYSTEM (no dev-user PATH/npmrc), so bootstrap bun if absent and supply the private # ~75-file .output. The runner is SYSTEM with no ~/.npmrc, so supply the private @unom token in
# @unom registry token via the SYSTEM home .npmrc kept OUT of the shipped bundle. # the SYSTEM home .npmrc to BUILD (kept OUT of the shipped bundle — web\.npmrc has only the
# registry mapping, and nothing copies it into .output).
run: | run: |
$bun = (Get-Command bun -ErrorAction SilentlyContinue).Source $bun = $env:BUN_EXE
if (-not $bun) { foreach ($p in @("$env:USERPROFILE\.bun\bin\bun.exe", 'C:\Users\Public\bun\bin\bun.exe')) { if (Test-Path $p) { $bun = $p; break } } }
if (-not $bun) {
Write-Output "bun not found - installing via bun.sh"
Invoke-RestMethod https://bun.sh/install.ps1 | Invoke-Expression
$bun = Join-Path $env:USERPROFILE '.bun\bin\bun.exe'
}
if (-not (Test-Path $bun)) { throw "bun unavailable (install failed?): $bun" }
& $bun --version
# @unom is a private Gitea npm registry. The committed web\.npmrc has only the registry
# mapping; put the mapping + auth token in the SYSTEM home .npmrc so the token never lands in
# the shipped bundle (.output\server\.npmrc stays the clean mapping-only copy).
if ($env:REGISTRY_TOKEN) { if ($env:REGISTRY_TOKEN) {
$rc = Join-Path $env:USERPROFILE '.npmrc' $rc = Join-Path $env:USERPROFILE '.npmrc'
Add-Content -Path $rc -Value '@unom:registry=https://git.unom.io/api/packages/unom/npm/' Add-Content -Path $rc -Value '@unom:registry=https://git.unom.io/api/packages/unom/npm/'
@@ -155,19 +145,16 @@ jobs:
if (Select-String -Path .output\server\index.mjs -Pattern 'Bun\.serve' -Quiet) { if (Select-String -Path .output\server\index.mjs -Pattern 'Bun\.serve' -Quiet) {
throw "web build is a bun bundle (Bun.serve) - need the node-server preset" throw "web build is a bun bundle (Bun.serve) - need the node-server preset"
} }
# Externalized @unom SSR deps must be installed inside .output\server (registry mapping via .npmrc).
Copy-Item .npmrc .output\server\.npmrc -Force
Push-Location .output\server; & $bun install; if ($LASTEXITCODE) { throw ".output/server dep install failed ($LASTEXITCODE)" }; Pop-Location
Pop-Location Pop-Location
# Gate the installer on a real node boot serving /login (the runtime the installer ships). # Gate the installer on a real boot under the BUNDLED bun (the runtime it ships), serving /login.
$env:PORT = '3009'; $env:HOST = '127.0.0.1'; $env:PUNKTFUNK_UI_PASSWORD = 'ci' $env:PORT = '3009'; $env:HOST = '127.0.0.1'; $env:PUNKTFUNK_UI_PASSWORD = 'ci'
$server = (Resolve-Path 'web\.output\server\index.mjs').Path $server = (Resolve-Path 'web\.output\server\index.mjs').Path
$p = Start-Process -FilePath $env:NODE_EXE -ArgumentList $server -PassThru -WindowStyle Hidden $p = Start-Process -FilePath $bun -ArgumentList $server -PassThru -WindowStyle Hidden
Start-Sleep -Seconds 4 Start-Sleep -Seconds 4
try { $code = (Invoke-WebRequest -Uri 'http://127.0.0.1:3009/login' -UseBasicParsing -TimeoutSec 10).StatusCode } catch { $code = 0 } try { $code = (Invoke-WebRequest -Uri 'http://127.0.0.1:3009/login' -UseBasicParsing -TimeoutSec 10).StatusCode } catch { $code = 0 }
Stop-Process -Id $p.Id -Force -ErrorAction SilentlyContinue Stop-Process -Id $p.Id -Force -ErrorAction SilentlyContinue
Write-Output "web console smoke: /login -> $code" Write-Output "web console smoke (bun): /login -> $code"
if ($code -ne 200) { throw "web console failed to boot under node" } if ($code -ne 200) { throw "web console failed to boot under bun" }
"WEB_OUTPUT_DIR=$((Resolve-Path 'web\.output').Path)" | Out-File -FilePath $env:GITHUB_ENV -Append -Encoding utf8 "WEB_OUTPUT_DIR=$((Resolve-Path 'web\.output').Path)" | Out-File -FilePath $env:GITHUB_ENV -Append -Encoding utf8
- name: Pack + sign installer - name: Pack + sign installer
+1 -1
View File
@@ -33,7 +33,7 @@ and the CLI `punktfunk-host service install` path) are in
[`packaging/windows`](https://git.unom.io/unom/punktfunk/src/branch/main/packaging/windows/README.md). [`packaging/windows`](https://git.unom.io/unom/punktfunk/src/branch/main/packaging/windows/README.md).
The installer also sets up the **web management console** (status, paired devices, the PIN pairing The installer also sets up the **web management console** (status, paired devices, the PIN pairing
flow): it bundles the console plus its own Node runtime and runs it as the **`PunktfunkWeb`** service flow): it bundles the console plus its own bun runtime and runs it as the **`PunktfunkWeb`** service
on **`http://<this-PC>:3000`**, starting at boot. During setup you choose the console **login on **`http://<this-PC>:3000`**, starting at boot. During setup you choose the console **login
password** (pre-filled with a secure random default and shown again on the final page); change it password** (pre-filled with a secure random default and shown again on the final page); change it
later in `%ProgramData%\punktfunk\web-password`. Open the console from any browser on the LAN and log later in `%ProgramData%\punktfunk\web-password`. Open the console from any browser on the LAN and log
+8 -7
View File
@@ -33,11 +33,12 @@ exe into `C:\Program Files\punktfunk\` and calls that subcommand, elevated.
display without it). display without it).
- Runs `punktfunk-host service install` (idempotent; writes a default `host.env` only if absent, so - Runs `punktfunk-host service install` (idempotent; writes a default `host.env` only if absent, so
user config survives upgrades) and, by the *Start service now* task, `service start`. user config survives upgrades) and, by the *Start service now* task, `service start`.
- **Web management console** (bundled when packed with `-WebDir`/`-NodeExe`, which the CI always is): - **Web management console** (bundled when packed with `-WebDir`/`-BunExe`, which the CI always is):
lays down the built `.output` server + a portable Node, prompts for a console login password lays down the built **self-contained** `.output` server (Nitro `noExternals` — deps bundled +
(pre-filled with a secure random default, shown again on the final page; kept on upgrade), then tree-shaken, ~75 files, no `node_modules`) + a portable **bun**, prompts for a console login
`web-setup.ps1` writes the ACL'd `%ProgramData%\punktfunk\web-password`, registers the password (pre-filled with a secure random default, shown again on the final page; kept on upgrade),
**`PunktfunkWeb`** scheduled task (boot, SYSTEM, restart-on-failure → `web-run.cmd``node` on then `web-setup.ps1` writes the ACL'd `%ProgramData%\punktfunk\web-password`, registers the
**`PunktfunkWeb`** scheduled task (boot, SYSTEM, restart-on-failure → `web-run.cmd``bun` on
`:3000`), opens TCP 3000, and starts it. It proxies the host's loopback mgmt API with the host's `:3000`), opens TCP 3000, and starts it. It proxies the host's loopback mgmt API with the host's
own `%ProgramData%\punktfunk\mgmt-token`. own `%ProgramData%\punktfunk\mgmt-token`.
- **Upgrade:** stops a running `PunktfunkHost` service and waits for `STOPPED` before replacing files - **Upgrade:** stops a running `PunktfunkHost` service and waits for `STOPPED` before replacing files
@@ -63,10 +64,10 @@ read it from `%ProgramData%\punktfunk\web-password`.
| File | Role | | File | Role |
|------|------| |------|------|
| `punktfunk-host.iss` | Inno Setup script (the installer definition). | | `punktfunk-host.iss` | Inno Setup script (the installer definition). |
| `pack-host-installer.ps1` | Orchestrator: cert + sign, stage the driver + FFmpeg + **web console** (`.output` + node) bundles, run ISCC, sign setup.exe, emit registry paths. | | `pack-host-installer.ps1` | Orchestrator: cert + sign, stage the driver + FFmpeg + **web console** (`.output` + bun) bundles, run ISCC, sign setup.exe, emit registry paths. |
| `stage-sudovda.ps1` | Stage the **vendored** SudoVDA driver + fetch/verify the **pinned** nefcon release into the bundle. | | `stage-sudovda.ps1` | Stage the **vendored** SudoVDA driver + fetch/verify the **pinned** nefcon release into the bundle. |
| `install-sudovda.ps1` | Runs at install time (elevated): trust cert → gated device-node create → `pnputil` install. | | `install-sudovda.ps1` | Runs at install time (elevated): trust cert → gated device-node create → `pnputil` install. |
| `../../scripts/windows/web-run.cmd` | The `PunktfunkWeb` task action: loads the mgmt token + login password env, runs the bundled `node` on the Nitro server (`:3000`). | | `../../scripts/windows/web-run.cmd` | The `PunktfunkWeb` task action: loads the mgmt token + login password env, runs the bundled `bun` on the Nitro server (`:3000`). |
| `../../scripts/windows/web-setup.ps1` | Install-time (elevated): write the ACL'd console password, register the `PunktfunkWeb` task + firewall rule, start it. | | `../../scripts/windows/web-setup.ps1` | Install-time (elevated): write the ACL'd console password, register the `PunktfunkWeb` task + firewall rule, start it. |
| `sudovda/` | **Vendored** prebuilt SudoVDA driver: `SudoVDA.inf` / `sudovda.cat` / `SudoVDA.dll` / `sudovda.cer`. | | `sudovda/` | **Vendored** prebuilt SudoVDA driver: `SudoVDA.inf` / `sudovda.cat` / `SudoVDA.dll` / `sudovda.cer`. |
| `nvenc/nvenc.def`, `nvenc/gen-nvenc-importlib.ps1` | Synthesise `nvencodeapi.lib` for the `--features nvenc` link (llvm-dlltool / lib.exe). | | `nvenc/nvenc.def`, `nvenc/gen-nvenc-importlib.ps1` | Synthesise `nvencodeapi.lib` for the `--features nvenc` link (llvm-dlltool / lib.exe). |
+12 -12
View File
@@ -27,7 +27,7 @@ param(
[string]$PfxPassword = $env:MSIX_CERT_PASSWORD, [string]$PfxPassword = $env:MSIX_CERT_PASSWORD,
[string]$FfmpegDir = $env:FFMPEG_DIR, # bundle its bin\*.dll (amf-qsv build) [string]$FfmpegDir = $env:FFMPEG_DIR, # bundle its bin\*.dll (amf-qsv build)
[string]$WebDir = $env:WEB_OUTPUT_DIR, # built web .output tree -> bundle the mgmt console [string]$WebDir = $env:WEB_OUTPUT_DIR, # built web .output tree -> bundle the mgmt console
[string]$NodeExe = $env:NODE_EXE, # portable node.exe (>= 20) runtime for the console [string]$BunExe = $env:BUN_EXE, # portable bun.exe runtime for the console
[switch]$NoDriver, # build without the bundled SudoVDA driver [switch]$NoDriver, # build without the bundled SudoVDA driver
[switch]$NoSign # skip signing (local debug) [switch]$NoSign # skip signing (local debug)
) )
@@ -184,30 +184,30 @@ if ($ffmpegBinSrc -and (Test-Path $ffmpegBinSrc)) {
} }
else { Write-Host "no FFMPEG_DIR\bin -> installer built WITHOUT FFmpeg DLLs (nvenc/software-only host)" } else { Write-Host "no FFMPEG_DIR\bin -> installer built WITHOUT FFmpeg DLLs (nvenc/software-only host)" }
# --- stage the web management console (the built .output tree + a portable node + the launcher) --- # --- stage the web management console (the self-contained .output tree + a portable bun + launcher) -
# The console runs as the PunktfunkWeb scheduled task (`node {app}\web\.output\server\index.mjs`), # The console runs as the PunktfunkWeb scheduled task (`bun {app}\web\.output\server\index.mjs`),
# auto-wired to the host's loopback mgmt API. Stage everything ISCC reads into $OutDir (the # auto-wired to the host's loopback mgmt API. Stage everything ISCC reads into $OutDir (the
# non-WOW64-redirected C:\t area, same reason as the .iss/host.env staging above). Built upstream # non-WOW64-redirected C:\t area, same reason as the .iss/host.env staging above). The .output is
# (windows-host.yml mirrors deb.yml: bun build -> node-server preset + the .output/server deps); # self-contained (Nitro noExternals — deps bundled + tree-shaken, no node_modules), so bun runs it
# omitted when -WebDir/-NodeExe are unset (host-only installer, e.g. a local debug pack). # directly; omitted when -WebDir/-BunExe are unset (host-only installer, e.g. a local debug pack).
if ($WebDir -and (Test-Path $WebDir) -and $NodeExe -and (Test-Path $NodeExe)) { if ($WebDir -and (Test-Path $WebDir) -and $BunExe -and (Test-Path $BunExe)) {
$webStage = Join-Path $OutDir 'web' $webStage = Join-Path $OutDir 'web'
if (Test-Path $webStage) { Remove-Item $webStage -Recurse -Force } if (Test-Path $webStage) { Remove-Item $webStage -Recurse -Force }
New-Item -ItemType Directory -Force -Path $webStage | Out-Null New-Item -ItemType Directory -Force -Path $webStage | Out-Null
Copy-Item (Join-Path $WebDir '*') -Destination $webStage -Recurse -Force Copy-Item (Join-Path $WebDir '*') -Destination $webStage -Recurse -Force
$nodeStage = Join-Path $OutDir 'node.exe' $bunStage = Join-Path $OutDir 'bun.exe'
Copy-Item -LiteralPath $NodeExe -Destination $nodeStage -Force Copy-Item -LiteralPath $BunExe -Destination $bunStage -Force
$webRun = Join-Path $OutDir 'web-run.cmd' $webRun = Join-Path $OutDir 'web-run.cmd'
$webSetup = Join-Path $OutDir 'web-setup.ps1' $webSetup = Join-Path $OutDir 'web-setup.ps1'
Copy-Item (Join-Path $repoRoot 'scripts\windows\web-run.cmd') -Destination $webRun -Force Copy-Item (Join-Path $repoRoot 'scripts\windows\web-run.cmd') -Destination $webRun -Force
Copy-Item (Join-Path $repoRoot 'scripts\windows\web-setup.ps1') -Destination $webSetup -Force Copy-Item (Join-Path $repoRoot 'scripts\windows\web-setup.ps1') -Destination $webSetup -Force
$defines += "/DWebDir=$webStage" $defines += "/DWebDir=$webStage"
$defines += "/DNodeExe=$nodeStage" $defines += "/DBunExe=$bunStage"
$defines += "/DWebRunCmd=$webRun" $defines += "/DWebRunCmd=$webRun"
$defines += "/DWebSetup=$webSetup" $defines += "/DWebSetup=$webSetup"
Write-Host "bundling the web console from $WebDir (+ node $NodeExe)" Write-Host "bundling the web console from $WebDir (+ bun $BunExe)"
} }
else { Write-Host "no -WebDir/-NodeExe -> installer built WITHOUT the web console" } else { Write-Host "no -WebDir/-BunExe -> installer built WITHOUT the web console" }
# --- build the installer (from the non-redirected copy under C:\t) ----------------------------- # --- build the installer (from the non-redirected copy under C:\t) -----------------------------
Write-Host "==> ISCC $($defines -join ' ') $issLocal" Write-Host "==> ISCC $($defines -join ' ') $issLocal"
+7 -7
View File
@@ -49,10 +49,10 @@
#ifdef FfmpegBin #ifdef FfmpegBin
#define WithFfmpeg #define WithFfmpeg
#endif #endif
; WebDir (the built web .output tree) + NodeExe (a portable node.exe) are passed together by ; WebDir (the built web .output tree) + BunExe (a portable bun.exe) are passed together by
; pack-host-installer.ps1 to bundle the management console. Both required → WithWeb. ; pack-host-installer.ps1 to bundle the management console. Both required → WithWeb.
#ifdef WebDir #ifdef WebDir
#ifdef NodeExe #ifdef BunExe
#define WithWeb #define WithWeb
#endif #endif
#endif #endif
@@ -102,12 +102,12 @@ Source: "{#Readme}"; DestDir: "{app}"; DestName: "README.txt"; Flags: ignorevers
Source: "{#FfmpegBin}\*.dll"; DestDir: "{app}"; Flags: ignoreversion Source: "{#FfmpegBin}\*.dll"; DestDir: "{app}"; Flags: ignoreversion
#endif #endif
#ifdef WithWeb #ifdef WithWeb
; The web management console: the built Nitro/Node SSR bundle (.output = server + public assets) → ; The web management console: the self-contained Nitro SSR bundle (.output = server + public; deps
; {app}\web\.output, a portable Node runtime → {app}\node\node.exe, and the launcher the ; bundled in, no node_modules) → {app}\web\.output, a portable bun runtime → {app}\bun\bun.exe, and
; PunktfunkWeb task runs → {app}\web\web-run.cmd. web-setup.ps1 (the provisioner) goes to {tmp} and ; the launcher the PunktfunkWeb task runs → {app}\web\web-run.cmd. web-setup.ps1 (the provisioner)
; is removed after install. ; goes to {tmp} and is removed after install.
Source: "{#WebDir}\*"; DestDir: "{app}\web\.output"; Flags: ignoreversion recursesubdirs createallsubdirs Source: "{#WebDir}\*"; DestDir: "{app}\web\.output"; Flags: ignoreversion recursesubdirs createallsubdirs
Source: "{#NodeExe}"; DestDir: "{app}\node"; DestName: "node.exe"; Flags: ignoreversion Source: "{#BunExe}"; DestDir: "{app}\bun"; DestName: "bun.exe"; Flags: ignoreversion
Source: "{#WebRunCmd}"; DestDir: "{app}\web"; DestName: "web-run.cmd"; Flags: ignoreversion Source: "{#WebRunCmd}"; DestDir: "{app}\web"; DestName: "web-run.cmd"; Flags: ignoreversion
Source: "{#WebSetup}"; DestDir: "{tmp}"; DestName: "web-setup.ps1"; Flags: deleteafterinstall Source: "{#WebSetup}"; DestDir: "{tmp}"; DestName: "web-setup.ps1"; Flags: deleteafterinstall
#endif #endif
+4 -4
View File
@@ -35,10 +35,10 @@ won't start. The service is down only for the build duration.
## Web management console ## Web management console
On an **installed** host (the `setup.exe`) the console is set up automatically — no manual steps. On an **installed** host (the `setup.exe`) the console is set up automatically — no manual steps.
The installer bundles the built `.output` server + a portable Node and runs The installer bundles the built (self-contained, no-`node_modules`) `.output` server + a portable
`scripts\windows\web-setup.ps1`, which registers the **`PunktfunkWeb`** scheduled task (at boot, as bun and runs `scripts\windows\web-setup.ps1`, which registers the **`PunktfunkWeb`** scheduled task
SYSTEM, restart-on-failure) running `{app}\web\web-run.cmd` `node …\.output\server\index.mjs` on (at boot, as SYSTEM, restart-on-failure) running `{app}\web\web-run.cmd`
`:3000`, opens inbound TCP 3000, and writes the login password to `bun …\.output\server\index.mjs` on `:3000`, opens inbound TCP 3000, and writes the login password to
`%ProgramData%\punktfunk\web-password` (ACL'd to Administrators + SYSTEM). The mgmt bearer token it `%ProgramData%\punktfunk\web-password` (ACL'd to Administrators + SYSTEM). The mgmt bearer token it
proxies with is the host's own `%ProgramData%\punktfunk\mgmt-token`. Browse `http://<host-ip>:3000` proxies with is the host's own `%ProgramData%\punktfunk\mgmt-token`. Browse `http://<host-ip>:3000`
and log in with the password the installer shows on its final page. To change it, edit and log in with the password the installer shows on its final page. To change it, edit
+5 -11
View File
@@ -3,9 +3,9 @@
powershell -ExecutionPolicy Bypass -File scripts\windows\build-web.ps1 powershell -ExecutionPolicy Bypass -File scripts\windows\build-web.ps1
bun = build tool, node = runtime (the Nitro bundle externalizes srvx/@unom for SSR, which bun is both the build tool AND the runtime: vite.config's Nitro noExternals bundles every dep
bun fails to resolve at runtime). The PunktfunkWeb scheduled task runs web\web-run.cmd -> into the self-contained .output (no node_modules, nothing for bun to fail to resolve), so the
node .output\server\index.mjs on :3000. PunktfunkWeb task runs web\web-run.cmd -> bun .output\server\index.mjs on :3000.
#> #>
$ErrorActionPreference = 'Stop' $ErrorActionPreference = 'Stop'
$repo = Split-Path (Split-Path $PSScriptRoot) $repo = Split-Path (Split-Path $PSScriptRoot)
@@ -19,17 +19,11 @@ Write-Host "bun install + build ..."
& $bun install & $bun install
& $bun run build & $bun run build
if ($LASTEXITCODE -ne 0) { throw "web build failed (exit $LASTEXITCODE)" } if ($LASTEXITCODE -ne 0) { throw "web build failed (exit $LASTEXITCODE)" }
# No .output/server install: noExternals means the output has no externalized deps to resolve.
# The Nitro server bundle externalizes its runtime deps - install them in .output/server,
# with the @unom registry .npmrc present (else @unom/* 404s on npmjs).
Write-Host "installing externalized server deps ..."
Copy-Item "$web\.npmrc" "$web\.output\server\.npmrc" -Force
Set-Location "$web\.output\server"
& $bun install
Write-Host "restarting $task ..." Write-Host "restarting $task ..."
& schtasks /end /tn $task 2>$null | Out-Null & schtasks /end /tn $task 2>$null | Out-Null
Get-CimInstance Win32_Process -Filter "Name='node.exe'" -ErrorAction SilentlyContinue | Get-CimInstance Win32_Process -Filter "Name='bun.exe'" -ErrorAction SilentlyContinue |
Where-Object { $_.CommandLine -match 'index\.mjs' } | Where-Object { $_.CommandLine -match 'index\.mjs' } |
ForEach-Object { Stop-Process -Id $_.ProcessId -Force -ErrorAction SilentlyContinue } ForEach-Object { Stop-Process -Id $_.ProcessId -Force -ErrorAction SilentlyContinue }
Start-Sleep 2 Start-Sleep 2
+6 -6
View File
@@ -2,10 +2,10 @@
rem punktfunk web console launcher — the action the PunktfunkWeb scheduled task runs at boot. rem punktfunk web console launcher — the action the PunktfunkWeb scheduled task runs at boot.
rem rem
rem Lays out next to the installed payload: {app}\web\web-run.cmd, {app}\web\.output\... and rem Lays out next to the installed payload: {app}\web\web-run.cmd, {app}\web\.output\... and
rem {app}\node\node.exe (so %~dp0 = {app}\web\). Auto-wires the console the same way the Linux rem {app}\bun\bun.exe (so %~dp0 = {app}\web\). Auto-wires the console the same way the Linux
rem systemd unit does: it sources the host's mgmt bearer token + the console login password from rem systemd unit does: it sources the host's mgmt bearer token + the console login password from
rem %ProgramData%\punktfunk\, points the /api proxy at the host's loopback HTTPS mgmt API, and runs rem %ProgramData%\punktfunk\, points the /api proxy at the host's loopback HTTPS mgmt API, and runs
rem the Nitro/Node server on :3000. No env editing on a packaged install. rem the (self-contained, no-node_modules) Nitro server on :3000 with the bundled bun. No env editing.
setlocal EnableExtensions setlocal EnableExtensions
set "PFDATA=%ProgramData%\punktfunk" set "PFDATA=%ProgramData%\punktfunk"
@@ -31,10 +31,10 @@ set "HOST=0.0.0.0"
set "PUNKTFUNK_MGMT_URL=https://127.0.0.1:47990" set "PUNKTFUNK_MGMT_URL=https://127.0.0.1:47990"
set "NODE_TLS_REJECT_UNAUTHORIZED=0" set "NODE_TLS_REJECT_UNAUTHORIZED=0"
set "NODE=%~dp0..\node\node.exe" set "BUN=%~dp0..\bun\bun.exe"
set "SERVER=%~dp0.output\server\index.mjs" set "SERVER=%~dp0.output\server\index.mjs"
if not exist "%NODE%" ( if not exist "%BUN%" (
echo [punktfunk-web] bundled node runtime missing at "%NODE%". echo [punktfunk-web] bundled bun runtime missing at "%BUN%".
exit /b 1 exit /b 1
) )
"%NODE%" "%SERVER%" "%BUN%" "%SERVER%"
+2 -2
View File
@@ -1,6 +1,6 @@
<# <#
Provision the punktfunk web console after the host installer has laid down its payload Provision the punktfunk web console after the host installer has laid down its payload
({app}\web\.output, {app}\node\node.exe, {app}\web\web-run.cmd). Invoked elevated from the ({app}\web\.output, {app}\bun\bun.exe, {app}\web\web-run.cmd). Invoked elevated from the
installer's [Run] section; idempotent (safe to re-run on upgrade). installer's [Run] section; idempotent (safe to re-run on upgrade).
1. Sets the console login password file %ProgramData%\punktfunk\web-password 1. Sets the console login password file %ProgramData%\punktfunk\web-password
@@ -71,7 +71,7 @@ $settings = New-ScheduledTaskSettingsSet -AllowStartIfOnBatteries -DontStopIfGoi
-StartWhenAvailable -RestartInterval (New-TimeSpan -Minutes 1) -RestartCount 10 ` -StartWhenAvailable -RestartInterval (New-TimeSpan -Minutes 1) -RestartCount 10 `
-ExecutionTimeLimit (New-TimeSpan -Seconds 0) -ExecutionTimeLimit (New-TimeSpan -Seconds 0)
Register-ScheduledTask -TaskName $TaskName -Action $action -Trigger $trigger -Principal $principal ` Register-ScheduledTask -TaskName $TaskName -Action $action -Trigger $trigger -Principal $principal `
-Settings $settings -Description 'punktfunk web management console (Nitro/Node SSR on :3000)' ` -Settings $settings -Description 'punktfunk web management console (Nitro SSR on bun, :3000)' `
-Force | Out-Null -Force | Out-Null
Write-Host "registered scheduled task $TaskName -> $cmd" Write-Host "registered scheduled task $TaskName -> $cmd"
+8
View File
@@ -46,6 +46,14 @@ export default defineConfig({
// .deb depend on apt-native `nodejs (>= 20)` instead of vendoring bun. CI still BUILDS with // .deb depend on apt-native `nodejs (>= 20)` instead of vendoring bun. CI still BUILDS with
// bun; only the runtime target changes. (dev `vite dev` is unaffected.) // bun; only the runtime target changes. (dev `vite dev` is unaffected.)
preset: 'node-server', preset: 'node-server',
// BUNDLE every dependency into the server output (no externalized node_modules). Three wins:
// (1) the .output tree drops from ~47k files / 730 MB (the whole untree-shaken @unom/ui dep
// tree — payload, lexical, date-fns…) to a handful of tree-shaken chunks; (2) it makes the
// output a self-contained graph `bun build --compile` can fold into ONE native binary (the
// Windows installer ships that instead of node + a node_modules forest); (3) it removes the
// bare external imports (`srvx`, `seroval`…) bun couldn't resolve at runtime — the reason we
// used to need node. node still runs the same self-contained output for the Linux .deb.
noExternals: true,
compatibilityDate: '2026-06-10', compatibilityDate: '2026-06-10',
// Scan server/{middleware,routes} for the auth gate + the /api proxy. // Scan server/{middleware,routes} for the auth gate + the /api proxy.
scanDirs: [serverDir], scanDirs: [serverDir],