From 880c382dfa884581f5555b69fff0ff53569428c2 Mon Sep 17 00:00:00 2001 From: Chris Thompson Date: Sun, 20 Sep 2026 14:45:39 -0600 Subject: [PATCH] webui: make the committed bundle reproducible dist/index.html is committed and embedded into the server binary at configure time (CMakeLists.txt), which is what lets the project build a working server without a JavaScript toolchain. But the bundle is not reproducible: SvelteKit defaults kit.version.name to Date.now() and derives the __sveltekit_ global it embeds from it, so two builds of identical source differ. before: build 1 __sveltekit_8hf8y8 sha256 416fcbab... build 2 __sveltekit_12z8hlh sha256 c1990991... after: build 1 __sveltekit_1wn864 sha256 25cc1247... build 2 __sveltekit_1wn864 sha256 25cc1247... Any two branches that rebuild the web UI therefore conflict in that file whether or not their source changes overlap -- which is what happened to #539, where dist/index.html was the only conflict while src/lib/text.ts merged cleanly and no upstream commit had touched either file. Naming the version after the UI package makes the bundle a pure function of the source tree. The version is bumped from the placeholder 0.1.0 -- untouched since the web UI landed in #185 -- to 0.8.1, the current release, so the identifier means something. Nothing derives it automatically because audio.cpp's own version is tag-driven: AUDIOCPP_VERSION defaults to "dev" and is passed at configure time, and no version string is committed anywhere in the tree. So it needs bumping alongside a release, and the comment says so. The comment also says plainly that this is an identifier and not a live update channel. An earlier draft justified the package version over a constant by saying it kept SvelteKit's "app has been updated" check working; that was wrong three ways. The app never imports the `updated` store, version.pollInterval is left at its default of 0, and the single-file bundle ships no _app/version.json for a client to poll. A stale version here costs a stale label and nothing else. Reproducibility verified beyond repeat runs: a clean `npm ci` build on node 20, 22 and 24 produces a byte-identical dist/index.html, matching the file committed here. Note that main's committed bundle does not currently match a clean build of its own source -- about 4.5 KB apart, and 1668 differing lines once the build id is normalised. The differences are minifier identifier renaming rather than missing UI: the feature strings sampled from the recent web UI work are present in both. So the bundle committed here is what this tree actually produces, which is not quite what was committed before. That drift is the argument for a guard: a CI step asserting `npm run build` leaves git status clean would catch it at PR time instead of as a mystery conflict later. Left out as separate work. Reported as #545. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01TMmMgd5xNGnjsQgybuUiK3 --- webui/native/dist/index.html | 60 +++++++++++++++++----------------- webui/native/package-lock.json | 4 +-- webui/native/package.json | 2 +- webui/native/svelte.config.js | 27 +++++++++++++++ 4 files changed, 60 insertions(+), 33 deletions(-) diff --git a/webui/native/dist/index.html b/webui/native/dist/index.html index 2f173ad9a..96c2ddfad 100644 --- a/webui/native/dist/index.html +++ b/webui/native/dist/index.html @@ -31,20 +31,20 @@
diff --git a/webui/native/package-lock.json b/webui/native/package-lock.json index 7287976c2..eed075683 100644 --- a/webui/native/package-lock.json +++ b/webui/native/package-lock.json @@ -1,12 +1,12 @@ { "name": "audiocpp-native-webui", - "version": "0.1.0", + "version": "0.8.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "audiocpp-native-webui", - "version": "0.1.0", + "version": "0.8.1", "dependencies": { "abcjs": "^6.7.0" }, diff --git a/webui/native/package.json b/webui/native/package.json index 967e48f47..3ccfe5e2a 100644 --- a/webui/native/package.json +++ b/webui/native/package.json @@ -1,7 +1,7 @@ { "name": "audiocpp-native-webui", "private": true, - "version": "0.1.0", + "version": "0.8.1", "type": "module", "scripts": { "dev": "vite dev", diff --git a/webui/native/svelte.config.js b/webui/native/svelte.config.js index a5ce70d05..d56fb91bd 100644 --- a/webui/native/svelte.config.js +++ b/webui/native/svelte.config.js @@ -1,5 +1,29 @@ +import { readFileSync } from 'node:fs'; + import adapter from '@sveltejs/adapter-static'; +// SvelteKit defaults kit.version.name to Date.now() and derives the +// __sveltekit_ global it embeds in dist/index.html from it, so two builds +// of identical source differ. dist/index.html is committed -- it is embedded +// into the server binary at configure time, which is what lets the project +// build a working server without a JavaScript toolchain -- so any two branches +// that rebuild the web UI conflict in it whether or not their source changes +// overlap. +// +// Naming the version after the UI package makes the bundle a pure function of +// the source tree. Keep webui/native/package.json's version in step with the +// release it ships in; nothing derives it automatically, because audio.cpp's +// own version is tag-driven (AUDIOCPP_VERSION is passed at configure time and +// is not committed anywhere in the tree). +// +// Note that this is an identifier, not a live update channel: the app never +// imports SvelteKit's `updated` store, version.pollInterval is left at its +// default of 0, and the single-file bundle ships no _app/version.json for the +// client to poll. A stale version here costs nothing but a stale label. +const { version } = JSON.parse( + readFileSync(new URL('./package.json', import.meta.url), 'utf8') +); + /** @type {import('@sveltejs/kit').Config} */ const config = { kit: { @@ -17,6 +41,9 @@ const config = { }, paths: { relative: true + }, + version: { + name: version } } };