Skip to content

[ai][js][cfg][ci][make][sh] web: migrate to SvelteKit 3 and adapter-static 4 - #705

Merged
ddol merged 6 commits into
mainfrom
claude/sveltekit-3
Oct 7, 2026
Merged

ddol merged 6 commits into
mainfrom
claude/sveltekit-3

Conversation

@ddol

@ddol ddol commented Oct 7, 2026 •

Copy link
Copy Markdown
Member

Summary

Migrates web/ from @sveltejs/kit 2.70.3 / @sveltejs/adapter-static 3.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:

  1. #lib subpath imports (on kit 2). This is the mechanical part: 34 files go from $lib/foo to #lib/foo.js (.svelte keeps its extension), plus a package.json imports map and the Jest mapper. It follows the rule sv migrate sveltekit-3 applies. TypeScript rejects an extensionless #lib/foo, so the extension is required.
  2. SvelteKit 3 migration. Covers config, tsconfig, bundling, build provenance and runtime APIs (details below).
  3. Dependabot. Drops the kit / adapter-static major-version ignore entries.
  4. Tooling. ensure-dev-web-build treats a newer vite.config.ts as stale, since the kit config lives there now. scripts/dev-setup.sh asks for Node 22.17.
  5. pnpm workspace. Drops the minimumReleaseAgeExclude entry 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.
  6. Clean prerendering. Every build printed prerender_unseen_routes for /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 sets prerender = false. They still work because the Go server answers any unknown /app path with the app shell. adapter-static gets strict: false, with a comment saying why that is safe here, and handleUnseenRoutes goes 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 into sveltekit({...}) in vite.config.ts, flat rather than under a kit: key, and the $scene alias rationale moves with them. tsconfig.json now extends $app/tsconfig, which svelte-kit sync generates into node_modules and which already sets allowJs, checkJs, bundler resolution and so on. Only strict and sourceMap remain in ours. That parent config has no include, and kit's suggested "*" would type-check jest.config.js and the .cjs transformer, so the include list is ["src", "vite.config.ts"]. Kit 2's generated config also checked vite.config.ts.

Bundling: manualChunks → output.bundleStrategy: 'single'. Kit 3 sets codeSplitting itself and ignores rollupOptions.output.manualChunks. It also overrides build.emptyOutDir.

  • Why it existed: 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 served web/build from disk; the go:embed came 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.
  • Does anything depend on it? Not any more. Looking into this turned up [ai][go][js][make][cfg][docs] embed all of web/build, serve /favicon.ico from it, stop embedding static/ #704: the old //go:embed web/build/* silently skipped any file below web/build whose name starts with _, and kit names split chunks <hash>.js from 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 embeds all:web/build, so split builds would be served correctly too. The build-copy step that make clean-web used to tidy up (copy-build.js into static/) is gone as well.
  • What this PR does: 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's everything.Di0ZKB7s.css. Output drops from 35 files to 17; the JS bundle is 1.72 MB, under the existing 2000 kB warning limit. emptyOutDir is removed because adapter-static clears build/ itself.
  • Moving to kit's split default is a separate decision, about initial load on the Pi versus request count, and is not part of this migration.

Build provenance: new src/env.ts. The trial upgrade did not show this. Kit 3 substitutes %sveltekit.env.NAME% in app.html only for variables declared in src/env.ts. Without that file, the app-git-sha, app-build-time and app-build-version meta tags render empty, and asset-naming.md calls the last of these the canonical version tag. make dev-go would then rebuild web on every run, because ensure-dev-web-build greps app-build-version. src/env.ts declares all three as public, static and required. set-build-env.js sets them for every pnpm run build/dev. A bare vite build now fails with env_invalid naming the missing variables, and svelte-kit sync (used by check and prepare) does not need them.

Runtime APIs.

  • $app/environment → $app/env (5 files).
  • $app/stores → $app/state on two pages. Both use legacy $: statements, and kit documents that $app/state changes reach only runes; read directly, $: "will never update after initial load". Each page therefore reads page through svelte/store's toStore. Without that bridge, saving a new scene navigates to /scene/<id> and reuses the component with sceneId still 'new', so a second Save would create a duplicate scene. The tracks page would also ignore a changed sensor_id.
  • page.url is now a ReadonlyURL. svelte-ux's NavItem is typed for URL and only reads pathname, 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 }) and resolve('/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, and web/package.json now matches. Web CI's setup-node-pnpm uses node-version: "22" (latest 22.x). go-ci and nightly-full-ci build 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, and web/.npmrc has engine-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 on main at [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 passes pnpm install --frozen-lockfile and the supply-chain policy check.
  • pnpm run lint clean; make format-web, make lint-web, make test-web, make build-web pass. make build-web stamps app-build-version 0.5.1-pre44 and the full SHA, and leaves no static/ behind.
  • Smoke test of the embedded build, using the binary from make build-radar-local run without --debug, as make dev-go runs it. It used 127.0.0.1:18080 and a scratch DB because 8080 was taken. The HTTP checks and the scene flow were re-run on main at [ai][go][js][make][cfg][docs] embed all of web/build, serve /favicon.ico from it, stop embedding static/ #704.
    • Every route under /app returns 200, the bundle and CSS are served with the right types, an unknown _app asset returns 404, and / redirects to /app/. /favicon.ico returns 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).
    • In the browser, client-side navigation works and the active nav item follows the route.
    • Site Edit / New go to /app/site/1 and /app/site/new.
    • Scene create-then-save gives POST 201, client navigation to /app/scene/smoke1, edit mode, then a second Save sends PUT. Exactly one scene exists afterwards.
    • On the tracks page, changing ?sensor_id= client-side updates the header and the API requests.
    • The only console errors were the expected LiDAR 404s (LiDAR disabled).

🤖 Generated with Claude Code

ddol and others added 5 commits October 6, 2026 17:18
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>
Copilot AI balanced review requested due to automatic review settings October 7, 2026 00:27
@codecov

codecov Bot commented Oct 7, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 $lib alias with the #lib package.json subpath import across 34 files (TS modules as #lib/foo.js, components keep .svelte), moves kit options from the now-deleted svelte.config.js into sveltekit({...}) in vite.config.ts (with bundleStrategy: 'single' replacing manualChunks), and re-points tsconfig.json at $app/tsconfig.
  • Runtime APIs & provenance: $app/environment→$app/env, $app/stores→$app/state (with toStore bridges so legacy $: statements keep tracking navigation), typed route-ID resolve('/scene/[id]', …) calls, a ReadonlyURL-safe navUrl copy for NavItem, and a new src/env.ts declaring the three PUBLIC_* build-provenance vars.
  • Tooling/deps: Node floor >=22.17, Jest #lib mapper, ensure-dev-web-build treats vite.config.ts as a staleness input, dev-setup.sh asks for Node 22.17, and the lockfile / dependabot ignores / pnpm minimumReleaseAgeExclude are 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>
@ddol
ddol merged commit 1be2643 into main Oct 7, 2026
17 checks passed
@ddol
ddol deleted the claude/sveltekit-3 branch October 7, 2026 02:12
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants