Skip to content

Upgrade to SvelteKit 3 - #2661

Merged
ejsmith merged 8 commits into
mainfrom
feature/sveltekit-3-upgrade
Oct 15, 2026
Merged

ejsmith merged 8 commits into
mainfrom
feature/sveltekit-3-upgrade

Conversation

@ejsmith

@ejsmith ejsmith commented Oct 14, 2026 •

Copy link
Copy Markdown
Member

Upgrades @sveltejs/kit 2.70.3 → 3.0.1 and @sveltejs/adapter-static 3 → 4, following the official migration guide and the sv migrate sveltekit-3 conventions, and adds list query parameters. The first three commits each build and pass the unit tests on their own: query parameters (still on kit 2), the kit 3 upgrade, and a mechanical rewrite of $ aliases to # subpath imports. Merges from main and review fixes follow. Unlike sv migrate, which writes .js, each specifier names the file on disk (.ts, .svelte.ts, /index.ts, .svelte), which allowImportingTsExtensions from $app/tsconfig permits.

Behavior changes:

  • A missing or deleted saved view now renders the full-page 404, as the template's error(404) intended. Under kit 2 that render-time error was uncaught and the page rendered nothing. Replacing the app shell cancels its in-flight requests, so the saved-view deletion test asserts the 404 page instead of no cancelled requests.
  • An open detail sheet stays open after a reload; its history state now persists.
  • Settings section links keep focus as well as scroll position. Kit 3 ignores data-sveltekit-noscroll; data-sveltekit-reset={false} is its replacement.
  • Kit 3 checks for new deployments hourly and when the tab regains focus (new default).

Validation: npm run validate, 1,048 unit tests, production build, Storybook build, and the hosted browser shards; locally, all 119 browser tests against the Aspire app before the merge from main. Compared against a kit 2 build of main on the same API, both with scripted journeys and by running on kit 2 the four browser tests that first failed on kit 3 (all four now pass on kit 3). build/update-config.sh from main versions the env.js import and refreshes the bootstrap's CSP hash on the kit 3 build, and the preview loads under the strict CSP with no violations.

SvelteKit 3 migration notes
  • Configuration moved into sveltekit() in vite.config.ts; tsconfig.json extends $app/tsconfig; param matchers are in src/params.ts with defineParams; $app/environment → $app/env.
  • Environment: src/env.ts declares the public variables as dynamic, so _app/env.js is still loaded at runtime and the container entrypoint can rewrite it. Values stay plain strings because the browser does not re-validate them. The localStorage overrides moved to #shared/environment.ts because $app/env/public exports immutable bindings.
  • Shallow routing uses goto(url, { shallow: true }) through updateShallowHistory. In kit 3 a shallow goto is asynchronous and aborts any navigation in progress, so writes are queued until navigations finish, derived when they run, and persist page state across reloads. Query parameters, detail sheets, and saved views read their markers from page.state; kit 2's sveltekit:states and sveltekit:history history keys no longer exist.
  • Kit applies a traversed entry's page state in its own popstate listener, and browsers flush effects between listeners, so Back and Forward are counted from the client init hook, before the router starts.
  • Kit 3 runs the navigation hooks for a shallow goto and for Back or Forward to a shallow entry (type: 'popstate', shallow: true); kit 2 ran neither. The canonical-URL guard, detail sheet, saved views, and query parameters skip shallow gotos and handle traversals themselves. The premium reset and telemetry page views skip any shallow navigation that stays on the same route, so a Back into another route's entry still counts as a page change.
  • goto now rejects URLs that are not app routes, so post-login and status-page redirects use gotoRedirect, which checks $app/paths match and otherwise loads the page in full.
  • handleError takes { kind, error, event }. Kit 3 keeps the old status/message only in development, so production would have reported every error without a status. The guide requires Svelte's async mode for an async hook, and the app keeps it off (as on main), so the hook returns at once, submits in the background, and returns an errorId that is the submitted event's reference id.
  • The strict CSP from Add strict nonce CSP for the modern Svelte frontend #2363 is in the sveltekit() options. connect-src lists http:, https:, and ws: because kit's CSP types reject '*'; on an HTTP(S) page they allow the same origins.
  • RouteId no longer includes layout-only routes: home links use resolve('/(app)') and base-path checks use resolve('').
  • Storybook 10 does not support kit 3. .storybook/main.ts backports Storybook 11's payload define and mocks $app/env/public; remove both when upgrading to Storybook 11.
  • runed declares kit ^2 as a peer, so it gets an override (the app does not import runed/kit). The cookie override is gone because kit 3 ships cookie@2. devalue is a dev dependency for an e2e helper that reads kit 3's serialized page state.
Query parameters
  • List types ('string[]', 'number[]', '<a,b>[]') use repeated keys, drop invalid and duplicate items, and are frozen. An emptied list with a non-empty default writes key= so a reload keeps it empty.
  • Writes apply only the instance's own keys onto the live URL, and a new instance reads the visible URL.
  • The suspected stale page.url premium gate did not reproduce: the API's 426 response shows the notice either way. Saved views keep page.url, which means "the URL that was navigated to" there.
Follow-ups
  • A nested src/routes/(app)/+error.svelte would keep the app shell (sidebar and queries) for errors inside the app. It needs an in-shell version of the error page design.
  • system/actions hand-rolls its q parameter instead of using createQueryParameters.
  • The shadcn-svelte CLI resolves # aliases but writes .js specifiers; AGENTS.md says to rename them to the .ts files after adding a component.

🤖 Generated with Claude Code

@ejsmith

ejsmith commented Oct 14, 2026

Copy link
Copy Markdown
Member Author

/preview

@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Oct 14, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-10-15T19:33:33.640625Z 56d8d0b New commits
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@github-actions github-actions Bot added the dev-preview Deploy this pull request to the shared dev environment. label Oct 14, 2026
@github-actions

github-actions Bot commented Oct 14, 2026 •

Copy link
Copy Markdown

Preview deployed

ejsmith and others added 2 commits October 14, 2026 09:40
Query parameter schemas accept list types (`'string[]'`, `'number[]'`,
`'<a,b>[]'`) encoded as repeated keys. Lists drop invalid and duplicate
items, compare item by item, and are frozen so an in-place mutation cannot
change state without updating the URL. An emptied list with a non-empty
default writes an empty marker so a reload keeps it empty.

Each instance now writes only its own keys onto the live URL, so several
instances on one page no longer overwrite each other, and a new instance
reads the visible URL rather than `page.url`, which shallow history writes
do not update.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Move the SvelteKit configuration into the Vite plugin, extend
`$app/tsconfig`, declare param matchers with `defineParams`, and declare
public environment variables in `src/env.ts`. They stay dynamic, so the
container entrypoint can still rewrite `_app/env.js` at startup.
localStorage overrides now live in an app-owned module because
`$app/env/public` exports immutable bindings.

Shallow routing uses `goto` with `shallow: true`. A shallow `goto` starts a
navigation that aborts one already in progress, and SvelteKit 3 writes it
asynchronously, so all shallow writes go through one queue that waits for
navigations, derives each write when it runs, and persists page state across
reloads. Query parameters, detail sheets, and saved views read their entry
markers from `page.state` instead of SvelteKit's internal history format.
SvelteKit applies a traversed entry's page state before component popstate
listeners run, so Back and Forward are counted from the client `init` hook.
Navigation hooks ignore shallow navigations.

Also: `goto` options and `data-sveltekit-reset` replace the removed
`noScroll`/`keepFocus` options and attributes; same-origin redirects that are
not app routes load as full pages because `goto` now rejects them; route IDs
follow SvelteKit 3's `RouteId`; the client `handleError` hook uses the new
`kind` input; and the organization query finishes after its observer
unmounts, like other cacheable reads, now that render errors show the error
page. Storybook 10 gets the payload define and public environment mock that
Storybook 11 adds for SvelteKit 3.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@ejsmith
ejsmith force-pushed the feature/sveltekit-3-upgrade branch from d5e0670 to 1dd4630 Compare October 14, 2026 14:41
@ejsmith

ejsmith commented Oct 14, 2026

Copy link
Copy Markdown
Member Author

/preview

@ejsmith
ejsmith force-pushed the feature/sveltekit-3-upgrade branch from 1dd4630 to adeea59 Compare October 14, 2026 16:02
ejsmith and others added 2 commits October 14, 2026 11:04
SvelteKit 3 no longer generates `$lib` and deprecates `config.alias`.
Declare `#comp`, `#features`, `#generated`, `#lib`, and `#shared` as
`package.json` subpath imports, which Vite and TypeScript resolve natively,
and drop the `alias` option.

Each specifier names the file on disk: `.ts` and `.svelte.ts` for modules,
`/index.ts` for barrels, and `.svelte` for components, which
`allowImportingTsExtensions` from `$app/tsconfig` permits. The rewrite
resolved all 2,285 specifiers to files on disk, and the few relative `.js`
specifiers now name their `.ts` files too; ESLint regrouped the imports.
`components.json` and the frontend guidance use the new aliases.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
SvelteKit 3 renders the full-page error page for the saved view's
render-time 404, which SvelteKit 2 left uncaught. Replacing the app shell
cancels whatever requests it has in flight, so the deletion step asserts the
not-found page, and the no-cancelled-requests assertion covers the home view
navigation it was written for. This replaces the narrower organization query
change, which only covered one of those requests.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@ejsmith
ejsmith force-pushed the feature/sveltekit-3-upgrade branch from adeea59 to 8507049 Compare October 14, 2026 16:04

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 85070494ef

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/Exceptionless.Web/ClientApp/vite.config.ts
Comment thread src/Exceptionless.Web/ClientApp/e2e/support/page-helpers.ts
Brings in the strict CSP for the Svelte frontend (#2363) and the .NET
10.0.13 and Foundatio 13.0.5 updates (#2663).

`svelte.config.js` is gone on this branch, so its new `csp` block moves into
the `sveltekit()` options in `vite.config.ts`. That file is type-checked,
and SvelteKit's CSP types reject `'*'`, so `connect-src` lists `http:`,
`https:`, and `ws:`, which allow the same origins on an HTTP(S) page. The
Zod `jitless` setup runs after the `#` imports in `hooks.client.ts`.

Kit 3's fallback page keeps the contract `update-config.sh` and the
backend rely on: one inline bootstrap that imports `/_app/env.js`, hashed
in the CSP meta tag. Running the script on a kit 3 build versions the
import and refreshes the hash to match.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@niemyjski

Copy link
Copy Markdown
Member

Most of this follows the SvelteKit 3 migration guide. The configuration, environment, matcher, and navigation changes have concrete migration purposes. I found one confirmed regression to fix before merging:

[P2] Preserve premium-page state during shallow Back/Forward. In the layout callback, shallow && type === 'goto' skips direct shallow writes, but query-history traversal can arrive as shallow: true, type: 'popstate'. That clears premiumPage.current while the Sessions component stays mounted. Its one-time initialization does not run again, so the Sessions premium upgrade notice can disappear. This reset should skip all shallow navigations. The telemetry callback has the same guard and should also be checked for extra page-usage events.

Scope recommendation: list query-parameter support has no application callers at this revision and is independent of the upgrade. Consider splitting it into a separate PR.

Validation: 1,039 unit tests, type checking, production build, Storybook build, and 48 old/new matcher comparisons passed locally. Full-app browser checks passed in CI. One unchanged runtime-config test failed locally because the script requires GNU sed; generated output inspection confirmed the existing runtime environment bootstrap contract.

Reproduction and migration audit details

Reviewed commit 85070494ef90c0fab4f8dfb0494eb9cf556a8d2e.

  • Reproduced the premium-state regression in Chromium using the actual locked SvelteKit 3.0.1 router, the exact layout callback and premium-state module, and the Sessions page's one-time initialization pattern. Initial page → filter entry A → filter entry B → Back to A produced Sessions → Sessions → Sessions → none, with no page errors. This was a minimal router fixture; the full authenticated app was not rerun locally. Add a representative browser regression for this sequence.
  • The new shallow-routing fake's traverse() updates state and emits popstate without invoking navigation hooks. It misses the query-history path above. Query-state and saved-view hooks intentionally handle traversal, so their guards should be assessed separately.
  • All 705 source files changed by the alias commit were equivalent after resolving old/new imports to their actual files and normalizing import order/whitespace. No hidden functional edits were found in that rewrite.
  • The deleted objectid, savedview, and stackaction matchers are consolidated in src/params.ts. Their accepted values are preserved; functions now return the matched value or undefined.
  • Deliberate behavior changes include the full-page 404 after deleting the active saved view, open sheets surviving reload, and settings links preserving focus. These should be accepted explicitly as part of the migration.
  • The PR description's .NET restore/deployment blocker is stale relative to the successful validation run.

ejsmith and others added 3 commits October 15, 2026 11:31
Brings in the KaTeX font allowance for the frontend CSP and the webhook
delivery logging changes (#2628). `svelte.config.js` is gone on this
branch, so the `data:` font source moves into the `csp` options in
`vite.config.ts`.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
SvelteKit 3 runs the navigation hooks with `shallow: true` and
`type: 'popstate'` when Back or Forward reaches a shallow entry, such as a
query parameter update, and SvelteKit 2 ran none. The layout reset the
premium page state on those traversals while the page stayed mounted, so
the Sessions upgrade notice disappeared, and telemetry recorded another
page view.

Both now skip shallow navigations that stay on the same route. A Back that
crosses into another route's shallow entry still resets the state and
records the view. The test fake runs the hooks for these traversals as
SvelteKit does, and a browser test covers the Sessions notice.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The SvelteKit 3 migration guide requires Svelte's async mode for an async
client `handleError`, and this app leaves it off. The hook now returns at
once and submits in the background, so telemetry never delays the error
page. The returned `errorId` is set as the submitted event's reference id;
before, it was the last session's reference id or an unrelated guid.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@ejsmith

ejsmith commented Oct 15, 2026

Copy link
Copy Markdown
Member Author

Thanks, Blake. Addressed in aed83b7 and 56d8d0b.

  • Premium state on shallow Back/Forward: confirmed. Kit 3 runs the hooks with shallow: true, type: 'popstate' for these traversals, and kit 2 ran none. Skipping every shallow navigation would leave a stale notice when Back crosses into another route's shallow entry, because to is that entry's route. The layout and telemetry now share isShallowNavigationWithinPage, which skips shallow navigations that stay on the same route. premium-notice-navigation.e2e.ts runs your sequence on Sessions (filter A, filter B, Back to A); it fails without the fix and passes with it.
  • Telemetry: same guard, so Back and Forward through query history no longer record extra page views.
  • Test fake: traverse() now runs the hooks as a shallow popstate navigation when the entry's URL differs from the rendered page, as kit does. The query-parameter and saved-view tests pass through that path unchanged. Their hooks are meant to handle traversals (they re-sync from the URL and rehydrate overrides), so their shallow && type === 'goto' guards stay.
  • Scope: keeping list parameters here. The same commit has query-parameter changes the kit 3 migration needed: writing onto the live URL, reading the visible URL at creation, and not trusting a stale page.url. That commit builds and passes the unit tests on kit 2 by itself, so it can be reviewed on its own.
  • Behavior changes: the full-page 404 after deleting the active saved view, sheets surviving reload, and settings links keeping focus are intended, and they're listed under "Behavior changes" in the description.
  • Description: removed the stale .NET restore note and updated validation and the hook guard notes.
  • GNU sed: build/update-config.test.mjs was removed on main in Add strict nonce CSP for the modern Svelte frontend #2363.

Codex's note on the async handleError is also addressed: the hook is synchronous now (details in that thread).

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 56d8d0b0b1

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +237 to +239
void updateShallowHistory({
state: withHistoryEntry(historyValue)
});

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Defer ownership until the shallow entry exists

When the user closes the sheet before this asynchronous write completes—especially while updateShallowHistory is queued behind another write or navigation—the component has already set ownsHistoryEntry, so consumeOwnedHistoryEntry() calls history.back() even though no sheet entry has been pushed yet. This can unexpectedly leave the current page, after which the queued write is discarded because its originating page changed. Mark the entry as owned only after the write completes, or handle a close while the push is pending without traversing history.

Useful? React with 👍 / 👎.

@github-actions

Copy link
Copy Markdown

Code Coverage

Package Line Rate Branch Rate Complexity Health
Exceptionless.AppHost 23% 23% 128 ❌
Exceptionless.Core 78% 69% 10924 ✔
Exceptionless.Insulation 51% 43% 370 ➖
Exceptionless.Web 86% 71% 9210 ✔
Summary 80% (28289 / 35212) 69% (13984 / 20200) 20632 ✔

@ejsmith
ejsmith merged commit d9d9bf9 into main Oct 15, 2026
22 checks passed
@ejsmith
ejsmith deleted the feature/sveltekit-3-upgrade branch October 15, 2026 20:56
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

dev-preview Deploy this pull request to the shared dev environment.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants