The 10 Svelte 5 & SvelteKit footguns your AI review bot waves through — and how to catch them in PR review

Collapse
X
 
  • Time
  • Show
Clear All
new posts
  • MyrinNew
    Senior Member
    • Feb 2024
    • 5168

    #1

    The 10 Svelte 5 & SvelteKit footguns your AI review bot waves through — and how to catch them in PR review


    TL;DR — Svelte 5's runes are powerful and quiet about their failure modes. Generic AI review bots (CodeRabbit, Greptile, the default Copilot reviewer) don't model them, so they approve secret leaks, SSR crashes, and broken reactivity without a word. This post catalogues 10 Svelte 5 / SvelteKit footguns, with safe rewrites, and ships a free GitHub Action (Marketplace) that flags them in the PR.





    One import that shipped an API secret to every browser

    You needed a feature flag in a component, so you reached for the env:







    import { STRIPE_SECRET_KEY } from '$env/static/private'; // ❌
    const flagsOn = STRIPE_SECRET_KEY.length > 0;








    It type-checked. It worked locally. And in a universal component, $env/static/private is reachable on the client — so SvelteKit either refuses to build, or (in the subtler dynamic/server-data variants below) your secret rides along to the browser bundle.


    The reviewer approved it. The generic AI bot approved it. Nothing in the standard flow says "this value is server-only and you just pulled it into client-reachable code."


    This is one of ten footguns I want to put on the table.


    Why generic review bots miss all of these

    A general-purpose bot reviews your diff as TypeScript. It doesn't know:

    1. Rune semantics. That a $effect writing state it derives should be a $derived. That destructuring $state snapshots the value and drops reactivity. These are valid TS — the bug is in the framework contract.
    2. SvelteKit's server/client boundary. Which files run where. A +page.ts runs on the server and the client; a +page.server.ts never reaches the browser. The bot can't see that distinction in a diff.
    3. SSR. That touching window at module top level runs during server render and crashes the page, not just the browser.


    So the review passes. The bug ships.


    The catalogue — 10 patterns to never merge

    Runes & reactivity

    1. $effect used to compute derived state






    let total = $state(0);
    $effect(() => { total = price * qty; }); // ❌







    This creates an extra render pass and can loop. If a value is a pure function of other state, it's derived.


    Safe rewrite:






    const total = $derived(price * qty); // ✅







    2. An effect that loops by writing state it reads






    $effect(() => { count = count + items.length; }); // ❌ reads + writes count







    Schedules itself forever.


    Safe rewrite: derive it, or read with untrack() if a side effect is truly intended.


    3. Destructured $state loses reactivity






    let user = $state({ name: 'Ada' });
    const { name } = user; // ❌ name is now a plain string snapshot







    Safe rewrite: keep the access live — user.name in the template, or wrap with $derived.


    4. Non-reactive let in a .svelte.ts module






    // store.svelte.ts
    let count = 0; // ❌ exported and mutated, but nothing re-renders







    Safe rewrite: let count = $state(0); — runes work in .svelte.ts, plain let doesn't react.


    5. Mutating a non-$bindable prop






    let { value } = $props();
    function reset() { value = ''; } // ❌ won't propagate to parent







    Safe rewrite: declare let { value = $bindable() } = $props(); if two-way is intended, or emit an event/callback.


    SvelteKit server/client boundary

    6. Secrets returned from a universal load






    // +page.ts — runs on client too
    export const load = async () => ({ apiKey: process.env.SECRET }); // ❌







    Safe rewrite: move it to +page.server.ts. Universal loads serialize their return value into the HTML.


    7. $env/static/private (or $env/dynamic/private) in client-reachable code

    Covered above. Private env belongs only in *.server.ts, server hooks, or +server.ts.


    Safe rewrite: read it server-side, pass only the derived, non-secret result to the client.


    8. window / document / localStorage at module top level






    const theme = localStorage.getItem('theme'); // ❌ runs during SSR → crash







    Safe rewrite: guard with import { browser } from '$app/environment', or read inside onMount (client-only).


    Security & accessibility

    9. {@html} on unsanitized input






    {@html comment.body}







    Safe rewrite: sanitize server-side before render, or don't use {@html} for user content.


    10. Missing a11y basics

    Images without alt, inputs without an associated , click handlers on non-interactive elements with no keyboard equivalent. The compiler warns on some; a reviewer should catch the rest.



    What about ESLint / the Svelte compiler warnings?

    Both are great and you should run both. Neither is enough:
    • eslint-plugin-svelte — excellent for style and some rune misuse, but doesn't model the SvelteKit server/client boundary across files, and won't reason about whether a returned value is secret.
    • Svelte compiler a11y warnings — cover a subset of accessibility, nothing on reactivity or leaks.
    • Generic AI review bots (CodeRabbit, Greptile, default Copilot) — review the diff as TypeScript; they'll happily approve a $effect-as-$derived or a private-env leak.


    There's a gap: a reviewer that actually knows Svelte 5 + SvelteKit semantics. So I built it.

    Catch them in the PR — svelte-autopilot

    Svelte Autopilot is a free, MIT-licensed GitHub Action that reads each PR's diff and leaves a single, focused review of the Svelte 5 / SvelteKit issues that matter — and keeps it updated as you push.


    Two minutes to set up:






    # .github/workflows/svelte-review.yml
    name: Svelte Autopilot
    on: pull_request
    permissions:
    contents: read
    pull-requests: write
    jobs:
    review:
    runs-on: ubuntu-latest
    steps:
    - uses: isabellehuecloser-ctrl/svelte-autopilot@v0
    with:
    api-key: ${{ secrets.OPENAI_API_KEY }}







    No checkout needed — the diff is read through the GitHub API. Bring your own OpenAI key (gpt-4o for accuracy, gpt-4o-mini to cut cost). A hosted Pro version runs with no API key and no CI setup.


    What I'd love your feedback on

    • Which Svelte 5 footgun is missing? Especially around $derived.by, snippets, and await in load.
    • False positives on your real SvelteKit app? Install it on a branch and tell me what fires wrongly.
    • Is the single-comment format right, or do you want inline?


    Source, issues, contributions:

    https://github.com/isabellehuecloser...elte-autopilot


    If it catches a real footgun on a PR of yours — a ⭐ on the repo tells me it's landing.





    References & further reading:



    More...
Working...