Repository navigation
[ai][js][cfg][ci][make][sh] web: migrate to SvelteKit 3 and adapter-static 4 - #705
Conversation
SvelteKit 3 removes the $lib alias in favour of a Node subpath import declared in package.json "imports". Vite, TypeScript and Jest already resolve that on SvelteKit 2, so the rename lands first, on its own, and the kit 3 migration that follows carries only the changes that need judgement. The 34 files follow the rule `sv migrate sveltekit-3` applies: `$lib/foo` becomes `#lib/foo.js` when foo is a .ts module (TypeScript's convention for naming a .ts source), and a .svelte import keeps its extension. The extension is not optional: TypeScript rejects an extensionless `#lib/foo` with TS2307. Jest maps `#lib/foo.js` back to src/lib/foo and lets moduleFileExtensions find the .ts. On SvelteKit 2.70.3: svelte-check 0 errors, 906 tests pass, lint clean, build passes. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Dependabot's kit 3.0.0 bump (#681) failed the web build and #693 held kit at 2.x. This is the migration that bump needed. Config. Kit 3 refuses svelte.config.js, so its options move into sveltekit({...}) in vite.config.ts, flat rather than under `kit:`; the note on why $scene is a Vite alias and not a kit alias moves with them. tsconfig.json extends $app/tsconfig, which svelte-kit sync writes into node_modules and which already sets allowJs, checkJs, bundler resolution and the rest, so only strict and sourceMap remain. That parent has no include list, and kit's suggested "*" would put jest.config.js and the .cjs transformer under checkJs, so the include is src plus vite.config.ts, which kit 2's generated config also checked. Bundling. Kit 3 sets codeSplitting itself and ignores the manualChunks: () => 'everything.js' that #25 added ("bundle chunks, improve go server log-lines"; #25 served web/build from disk, and the embed came in #30). output.bundleStrategy: 'single' is kit's option for the same result: one bundle.<hash>.js and one bundle.<hash>.css, the CSS byte-identical to kit 2's everything.<hash>.css. It keeps the shipped shape of the kit 2 build. Kit's split default would also serve correctly now: since #704 the server embeds all of web/build, so a chunk whose hash begins with "_" is no longer dropped from the binary. Switching to split is a separate decision about load behaviour on the Pi. emptyOutDir goes: kit overrides it, and adapter-static clears build/ itself. Build provenance. Kit 3 fills %sveltekit.env.NAME% in app.html only for variables declared in src/env.ts. Without one, the app-git-sha, app-build-time and app-build-version meta tags render empty, and `make dev-go` would rebuild web on every run, since its staleness check greps app-build-version. src/env.ts declares the three as public and static, and required: set-build-env.js sets all three for every build and dev run, and a bare `vite build` now fails naming the missing ones. Runtime APIs. $app/environment is now $app/env. $app/stores is gone, and the two pages that used it read $app/state through svelte/store's toStore: they use legacy `$:` statements, which never see $app/state change. Read directly, a saved new scene would leave the reused /scene/[id] component on 'new', so a second Save would create a second scene, and the tracks page would ignore a changed sensor_id. page.url is now a ReadonlyURL, so the layout gives svelte-ux's NavItem (typed for URL, reads only the pathname) a copy. resolve() treats a leading "/" as a route ID, so dynamic links pass params: resolve('/scene/[id]', { id }). engines.node is >=22.17, kit 3's own floor. CI's setup-node "22" and the ubuntu-24.04 runner image (Node 22.23.3) both satisfy it. Verified: svelte-check 0 errors, the web tests, lint, make build-web, and a smoke test of the embedded build under /app on a scratch port covering every route, client-side navigation, scene create then update (one POST, then a PUT), and a sensor_id change on the tracks page. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…again #693 ignored semver-major updates of @sveltejs/kit and @sveltejs/adapter-static until the web app was migrated to kit 3. It now is, so the hold goes and Dependabot will raise kit 3.x and adapter-static 4.x bumps as usual. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…sk for Node 22.17 ensure-dev-web-build did not treat a newer vite.config.ts as making the web build stale. The SvelteKit config now lives there (svelte.config.js, which it did not watch either, is gone), so it does. dev-setup.sh asked for Node.js 18 and offered the NodeSource 20.x installer; the web app needs 22.17, which kit 3 requires. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
#542 exempted @sveltejs/kit@2.70.2 from pnpm's minimum release age so that bump could install the day it shipped. The lock has since moved to 2.70.3 and now to 3.0.0, so the exemption names a version nothing resolves to. The lockfile does not record the setting, and `pnpm install --frozen-lockfile` is unchanged. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Codecov Report✅ All modified and coverable lines are covered by tests. 📢 Thoughts on this report? Let us know! |
There was a problem hiding this comment.
Copilot review overview
🔵 Needs a closer look
It is a major framework (SvelteKit 2→3) migration spanning build config, provenance, and runtime APIs across 40+ files whose correctness depends on external behaviour that cannot be built or run in this review environment, so it warrants final human verification.
Review effort: Balanced
Findings: None
What changed in this PR
This PR migrates web/ from SvelteKit 2.70.3 / adapter-static 3.0.10 to SvelteKit 3.0.0 / adapter-static 4.0.0 — the framework migration that Dependabot's grouped bump (#681) needed and that #693 deliberately held back. It is a cross-cutting change to the web build system: subpath imports, Vite-based kit config, build provenance, and several renamed runtime APIs, plus the CI/tooling and dependency-manifest updates that follow from the new Node floor (>=22.17).
Changes:
- Imports & config: Replaces the
$libalias with the#libpackage.json subpath import across 34 files (TS modules as#lib/foo.js, components keep.svelte), moves kit options from the now-deletedsvelte.config.jsintosveltekit({...})invite.config.ts(withbundleStrategy: 'single'replacingmanualChunks), and re-pointstsconfig.jsonat$app/tsconfig. - Runtime APIs & provenance:
$app/environment→$app/env,$app/stores→$app/state(withtoStorebridges so legacy$:statements keep tracking navigation), typed route-IDresolve('/scene/[id]', …)calls, aReadonlyURL-safenavUrlcopy forNavItem, and a newsrc/env.tsdeclaring the threePUBLIC_*build-provenance vars. - Tooling/deps: Node floor
>=22.17, Jest#libmapper,ensure-dev-web-buildtreatsvite.config.tsas a staleness input,dev-setup.shasks for Node 22.17, and the lockfile / dependabot ignores / pnpmminimumReleaseAgeExcludeare updated to match.
| File | Description |
|---|---|
| web/vite.config.ts | Moves kit config into sveltekit({...}); bundleStrategy: 'single'; drops manualChunks/emptyOutDir |
| web/svelte.config.js | Deleted (kit 3 refuses it) |
| web/tsconfig.json | Extends $app/tsconfig; trims to strict/sourceMap; adds include/exclude |
| web/src/env.ts | New build-provenance env-var declarations via defineEnvVars |
| web/src/mocks/$app/env.ts | New Jest mock for the renamed $app/env module |
| web/src/routes/+layout.svelte | navUrl derived copy for NavItem; #lib imports |
| web/src/routes/scene/[id]/+page.svelte, lidar/tracks/+page.svelte | $app/state + toStore bridges; typed resolve() |
| web/src/routes/site/+page.svelte, scene/+page.svelte | Typed route-ID resolve('/.../[id]', { id }) |
| web/src/routes/**/+page.svelte, web/src/lib/**, components/** | $lib→#lib/*.js; $app/environment→$app/env |
| web/package.json | imports map, engines.node >=22.17, kit/adapter-static majors |
| web/jest.config.js | #lib moduleNameMapper rules |
| web/pnpm-workspace.yaml, web/pnpm-lock.yaml | Drop stale minimumReleaseAgeExclude; kit 3 / adapter-static 4 + transitive deps |
| Makefile | ensure-dev-web-build treats vite.config.ts as a staleness input |
| scripts/dev-setup.sh | Node 22.17 guidance / setup_22.x |
| .github/dependabot.yml | Removes the kit / adapter-static semver-major holds |
I reviewed the migration for completeness and internal consistency: no leftover $lib/$app/environment/$app/stores references, all three $app/state consumers correctly handled, all dynamic resolve() calls converted to the route-ID form, the #lib Jest mapper ordered specific→general, and src/env.ts aligned with app.html and set-build-env.js. I found no concrete defects to comment on.
Files not reviewed (1)
- web/pnpm-lock.yaml: Generated file
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
…ering is clean Every build printed prerender_unseen_routes for /scene/[id] and /site/[id]. Kit 3 leads with the error code, so it now reads like a failure, but kit 2 printed the same thing; handleUnseenRoutes: 'warn' (from #49) kept it from failing the build. The root +layout.js marks every route prerenderable, and kit cannot prerender these two: their ids are database rows, and with ssr off the prerendered HTML is a bare shell with no links for the crawler to follow. They work at run time because the Go server answers any unknown /app path with index.html, the app shell, and the client router renders the page. Each route now says so with `export const prerender = false`. With two routes not prerendered and no adapter fallback page, adapter-static's strict check would refuse the build, so it gets `strict: false` and a comment naming the Go server's fallback as the reason that is safe. handleUnseenRoutes goes back to kit's default, 'fail': with the marker on site/[id] moved aside, the build now fails naming /site/[id] instead of passing with a warning. The build output is unchanged (the same 17 files, the version stamp in index.html intact). /app/site/1 and /app/scene/<id>, loaded directly from the embedded build, render their pages from the fallback shell with no console errors. svelte-check 0 errors, 909 tests, lint clean. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Summary
Migrates
web/from@sveltejs/kit2.70.3 /@sveltejs/adapter-static3.0.10 to kit 3.0.0 / adapter-static 4.0.0. This is the migration Dependabot's #681 needed. It also removes the semver-major hold that #693 put on both packages in.github/dependabot.yml.Six commits, each passing lint, svelte-check, tests and the build on its own:
#libsubpath imports (on kit 2). This is the mechanical part: 34 files go from$lib/footo#lib/foo.js(.sveltekeeps its extension), plus a package.jsonimportsmap and the Jest mapper. It follows the rulesv migrate sveltekit-3applies. TypeScript rejects an extensionless#lib/foo, so the extension is required.ensure-dev-web-buildtreats a newervite.config.tsas stale, since the kit config lives there now.scripts/dev-setup.shasks for Node 22.17.minimumReleaseAgeExcludeentry for@sveltejs/kit@2.70.2, added in Bump the "application" group with 2 updates across multiple ecosystems #542. The lock no longer resolves to that version.prerender_unseen_routesfor/scene/[id]and/site/[id]. Kit 2 printed it too, but kit 3 leads with the error code, so it read like a failure;handleUnseenRoutes: 'warn'from [go][js][py] update file prefix and timezones #49 had been keeping the build passing. Those two routes can't be prerendered because their ids are database rows, so each now setsprerender = false. They still work because the Go server answers any unknown/apppath with the app shell. adapter-static getsstrict: false, with a comment saying why that is safe here, andhandleUnseenRoutesgoes back to kit's default,'fail'. With one marker removed, the build now fails naming the route. The output is the same 17 files, and both routes still load when opened directly.What changed and why
Config. Kit 3 refuses
svelte.config.js. Its options move intosveltekit({...})invite.config.ts, flat rather than under akit:key, and the$scenealias rationale moves with them.tsconfig.jsonnow extends$app/tsconfig, whichsvelte-kit syncgenerates intonode_modulesand which already setsallowJs,checkJs, bundler resolution and so on. OnlystrictandsourceMapremain in ours. That parent config has noinclude, and kit's suggested"*"would type-checkjest.config.jsand the.cjstransformer, so the include list is["src", "vite.config.ts"]. Kit 2's generated config also checkedvite.config.ts.Bundling:
manualChunks→output.bundleStrategy: 'single'. Kit 3 setscodeSplittingitself and ignoresrollupOptions.output.manualChunks. It also overridesbuild.emptyOutDir.manualChunks: () => 'everything.js'arrived in [js][go] serve frontend from backend server #25 under the bullet "bundle chunks, improve go server log-lines". At that point the Go server servedweb/buildfrom disk; thego:embedcame a day later in [go] fix, bundle prod web assets #30. Nothing records a correctness reason, and the most likely motive was fewer requests in the server's per-request log.//go:embed web/build/*silently skipped any file belowweb/buildwhose name starts with_, and kit names split chunks<hash>.jsfrom an alphabet that includes_. Since [ai][go][js][make][cfg][docs] embed all of web/build, serve /favicon.ico from it, stop embedding static/ #704 the server embedsall:web/build, so split builds would be served correctly too. The build-copy step thatmake clean-webused to tidy up (copy-build.jsintostatic/) is gone as well.bundleStrategy: 'single'is kit's own option for one JS and one CSS file, which keeps the shipped shape of the kit 2 build. Outputs are named after the entry,bundle.<hash>.js/bundle.<hash>.css. The CSS is byte-identical to kit 2'severything.Di0ZKB7s.css. Output drops from 35 files to 17; the JS bundle is 1.72 MB, under the existing 2000 kB warning limit.emptyOutDiris removed because adapter-static clearsbuild/itself.Build provenance: new
src/env.ts. The trial upgrade did not show this. Kit 3 substitutes%sveltekit.env.NAME%inapp.htmlonly for variables declared insrc/env.ts. Without that file, theapp-git-sha,app-build-timeandapp-build-versionmeta tags render empty, andasset-naming.mdcalls the last of these the canonical version tag.make dev-gowould then rebuild web on every run, becauseensure-dev-web-buildgrepsapp-build-version.src/env.tsdeclares all three as public, static and required.set-build-env.jssets them for everypnpm run build/dev. A barevite buildnow fails withenv_invalidnaming the missing variables, andsvelte-kit sync(used bycheckandprepare) does not need them.Runtime APIs.
$app/environment→$app/env(5 files).$app/stores→$app/stateon two pages. Both use legacy$:statements, and kit documents that$app/statechanges reach only runes; read directly,$:"will never update after initial load". Each page therefore readspagethroughsvelte/store'stoStore. Without that bridge, saving a new scene navigates to/scene/<id>and reuses the component withsceneIdstill'new', so a second Save would create a duplicate scene. The tracks page would also ignore a changedsensor_id.page.urlis now aReadonlyURL. svelte-ux'sNavItemis typed forURLand only readspathname, so the layout passes it one derived copy instead of casting.resolve()now treats any/-leading argument as a route ID (pathnames are written without the leading slash). Dynamic links therefore use the typed route-ID form, e.g.resolve('/scene/[id]', { id })andresolve('/site/[id]', { id: String(siteId) }). The old calls happened to work at runtime but no longer type-check.Node. Kit 3 declares
engines.node >=22.17, andweb/package.jsonnow matches. Web CI'ssetup-node-pnpmusesnode-version: "22"(latest 22.x).go-ciandnightly-full-cibuild web on the runner's preinstalled Node, which is 22.23.3 on the current ubuntu-24.04 image (20260927.320.1). Both satisfy the floor, andweb/.npmrchasengine-strict=true, so an older Node fails at install instead of mid-build.Verification
pnpm run check(svelte-check): 0 errors, 0 warnings, as on kit 2 (the trial upgrade had 25).pnpm run test:ci: 909 / 909 onmainat [ai][go][js][make][cfg][docs] embed all of web/build, serve /favicon.ico from it, stop embedding static/ #704, coverage thresholds met. The lockfile passespnpm install --frozen-lockfileand the supply-chain policy check.pnpm run lintclean;make format-web,make lint-web,make test-web,make build-webpass.make build-webstampsapp-build-version0.5.1-pre44and the full SHA, and leaves nostatic/behind.make build-radar-localrun without--debug, asmake dev-goruns it. It used127.0.0.1:18080and a scratch DB because 8080 was taken. The HTTP checks and the scene flow were re-run onmainat [ai][go][js][make][cfg][docs] embed all of web/build, serve /favicon.ico from it, stop embedding static/ #704./appreturns 200, the bundle and CSS are served with the right types, an unknown_appasset returns 404, and/redirects to/app/./favicon.icoreturns the build's icon (fixed by [ai][go][js][make][cfg][docs] embed all of web/build, serve /favicon.ico from it, stop embedding static/ #704)./app/site/1and/app/site/new.POST201, client navigation to/app/scene/smoke1, edit mode, then a second Save sendsPUT. Exactly one scene exists afterwards.?sensor_id=client-side updates the header and the API requests.🤖 Generated with Claude Code