Project & Tooling
Scaffolding with the sv CLI
Section titled “Scaffolding with the sv CLI”The current way to start a Svelte or SvelteKit project is the sv CLI. One command scaffolds the project and can add integrations (TypeScript, Prettier, ESLint, Tailwind, testing) interactively:
npx sv create my-appcd my-appnpm installnpm run devsv create replaced the older npm create svelte@latest flow. It sets up a SvelteKit project by default (the recommended way to build Svelte apps), wires up Vite, and lets you opt into add-ons up front. There’s also npx sv add <name> to add an integration to an existing project later.
Vite drives everything
Section titled “Vite drives everything”A Svelte project runs on Vite. In development, Vite serves your modules over native ESM with hot module replacement — edit a component and the change appears without a full reload, preserving state where possible. For production, vite build bundles and optimizes the output.
{ "scripts": { "dev": "vite dev", "build": "vite build", "preview": "vite preview", "check": "svelte-check --tsconfig ./tsconfig.json" }}The Svelte integration for Vite (@sveltejs/vite-plugin-svelte, wired up for you by sv create) is what runs the compiler on your .svelte files during dev and build. You rarely touch it directly.
flowchart LR code["your .svelte / .ts files"] --> vite["Vite + svelte plugin"] vite --> dev["dev: HMR server"] vite --> build["build: compiled, bundled output"]
Editor support and svelte-check
Section titled “Editor support and svelte-check”Two tools keep the authoring experience honest:
- The Svelte editor extension (official, for VS Code and others) gives syntax highlighting, autocompletion, and inline diagnostics for
.sveltefiles — including type errors inside the markup, not just the script. svelte-checkis the command-line equivalent: it type-checks and lints your whole project, including expressions in templates, so you catch errors in CI that the editor would show you locally.
npm run check # runs svelte-check across the projectBecause svelte-check understands .svelte files end to end, it catches mistakes a plain tsc would miss — a wrong prop type passed in markup, a typo in a template expression, an unused CSS selector.
svelte-check, proven with a real error
Section titled “svelte-check, proven with a real error”“Catches a wrong prop type passed in markup” isn’t a hypothetical — here’s a component with a typed prop, and a caller that passes the wrong type:
<script lang="ts"> interface Props { name: string; } let { name }: Props = $props();</script><p>Hello {name.toUpperCase()}</p><!-- @expect-error Type 'number' is not assignable to type 'string'. --><script lang="ts"> import Greeter from './Greeter.svelte';</script><Greeter name={42} />Running svelte-check against the project (real output, only the path trimmed):
$ svelte-check --tsconfig ./tsconfig.jsonsrc/lib/BadUsage.svelte:4:10Type 'number' is not assignable to type 'string'.A plain tsc run over this project wouldn’t catch this at all — the type error is inside a .svelte file’s markup (<Greeter name={42} />), which only a tool that understands the .svelte format, not just .ts/.tsx, can type-check.
Linting .svelte files: eslint-plugin-svelte
Section titled “Linting .svelte files: eslint-plugin-svelte”Type-checking is one axis; linting is another. sv create can wire up ESLint with eslint-plugin-svelte, which understands Svelte-specific patterns a generic JS/TS linter has no concept of — an unescaped {@html}, a missing {#each} key, a deprecated on: directive. It’s ESLint’s modern flat config (eslint.config.js) only, matching current ESLint itself:
import svelteConfig from './svelte.config.js';import { defineConfig } from 'eslint/config';import globals from 'globals';import js from '@eslint/js';import svelte from 'eslint-plugin-svelte';
export default defineConfig([ js.configs.recommended, svelte.configs.recommended, { languageOptions: { globals: { ...globals.browser, ...globals.node }, }, }, { files: ['**/*.svelte', '**/*.svelte.js', '**/*.svelte.ts'], languageOptions: { parserOptions: { svelteConfig } }, },]);Run against a component that injects unsanitized HTML:
<script> let unsafeHtml = $state('<b>hi</b>'); let count = $state(0);</script>
{@html unsafeHtml}<button onclick={() => count++}>{count}</button>Real output (eslint-plugin-svelte 3.23.0, ESLint 10):
$ npx eslint src/lib/LintViolation.svelte/.../src/lib/LintViolation.svelte 6:1 error `{@html}` can lead to XSS attack svelte/no-at-html-tags
✖ 1 problem (1 error, 0 warnings)svelte.configs.recommended bundles rules like this one — plus deprecation-aware rules that flag on:click and other Svelte-4-era patterns — so a CI lint step catches them before a PR merges, not just when svelte-check happens to notice.
vite-plugin-svelte: the compiler options underneath everything
Section titled “vite-plugin-svelte: the compiler options underneath everything”@sveltejs/vite-plugin-svelte (wired up by sv create, mentioned earlier in this lesson) is where every compiler option ultimately gets set for the whole project, in svelte.config.js:
export default { compilerOptions: { // e.g. { runes: true } to force runes mode project-wide (see the // "Why Svelte, the Compiler" lesson for what this overrides) }, vitePlugin: { // dynamicCompileOptions lets you vary compiler options PER FILE — // e.g. forcing runes mode only for files under one directory during // an incremental migration. experimental: { dynamicCompileOptions({ filename, compileOptions }) { if (filename.includes('/src/routes/legacy/') || compileOptions.runes) return; return { runes: true }; }, }, },};The experimental.async flag this course’s Async Svelte lesson proves — required before await works inside $derived or a template expression — is set exactly this way, as a compilerOptions.experimental entry; that lesson’s real compile error (Cannot use \await` in deriveds and template expressions… unless the `experimental.async` compiler option is `true“) is what happens when this project-wide switch is left off.
Migrating Svelte 4 code: sv migrate svelte-5
Section titled “Migrating Svelte 4 code: sv migrate svelte-5”For an existing Svelte 4 project, the sv CLI also drives the migration itself:
npx sv migrate svelte-5Per the migration guide: it “automates repetitive tasks such as updating dependencies, converting let declarations to runes, migrating event attributes like on:click to onclick, and transforming slot-based patterns into the new snippet and render tag syntax.” It handles the mechanical rewriting; the legacy-recognition table in the events & attributes lesson is the reference for what each of those transformations targets, and for the real compile errors you’ll hit on anything the script’s automated pass doesn’t fully cover.
Mental Model
Section titled “Mental Model”Everything in this lesson is tooling built around one fact: a .svelte file is not plain JavaScript, so any tool that only understands JS/TS (tsc, a generic ESLint config) is structurally blind to markup-level mistakes. svelte-check and eslint-plugin-svelte both exist because someone has to parse the .svelte format itself before type-checking or linting can see inside the markup — that’s the whole reason they’re separate tools instead of tsc/eslint just working out of the box.
Trade-offs
Section titled “Trade-offs”| What you get | Benefit | Cost |
|---|---|---|
svelte-check over plain tsc | Catches errors inside markup — proven above with a prop-type mismatch tsc alone cannot see | A second, slower command in CI; needs its own --tsconfig invocation |
eslint-plugin-svelte flat config | Svelte-aware rules ({@html} risk, deprecated on:, missing each-keys) — proven above | Flat config only; a project still on .eslintrc.js must migrate that first |
dynamicCompileOptions for per-file compiler settings | Lets a big codebase force runes mode file-by-file during migration, instead of all-or-nothing | One more layer of project config to reason about beyond the simple compilerOptions object |
sv migrate svelte-5 | Automates the mechanical parts of a Svelte 4 → 5 upgrade | Does not catch every case — anything it misses surfaces as the real compile errors proven in the legacy-recognition table |
Common Misconceptions
Section titled “Common Misconceptions”- “
tscalone is enough to type-check a Svelte project.” Proven above: a wrong prop type passed in a.sveltefile’s markup is invisible to plaintsc— onlysvelte-check, which actually parses the Svelte file format, catches it. - “Any ESLint config works with
.sveltefiles.”eslint-plugin-sveltev3 requires flat config (eslint.config.js) specifically, and needssvelte-eslint-parserto even read the file — a legacy.eslintrc.jssetup doesn’t lint.sveltefiles’ markup at all. - “
experimental.asyncis a<svelte:options>or per-component setting.” It’s a project-wide (or, viadynamicCompileOptions, per-file) compiler option set throughvite-plugin-svelte/svelte.config.js— not something you toggle per component.
Reading AI-generated code
Section titled “Reading AI-generated code”When reviewing project configuration and tooling setup, check for:
- Is CI running only
tsc --noEmit(or nothing at all) for a SvelteKit project, with nosvelte-checkstep — meaning markup-level type errors like the one proven above ship silently? - Is there an
.eslintrc.js/.eslintrc.jsonalongsideeslint-plugin-svelte— a combination that doesn’t work, since the plugin’s current major version requires flat config only? - Does a lesson, README, or generated snippet still say
npm create svelte@latestor referencesvelte-migrateas a standalone package, instead of the currentnpx sv create/npx sv migrate svelte-5? - Is a compiler option like
experimental.asyncset somewhere per-component (a prop, a comment) rather than insvelte.config.js’scompilerOptions/vitePlugin, where it actually has to live?
🐛 Spot the bug
import js from '@eslint/js';
export default [js.configs.recommended,// ... rest of the config, no Svelte-specific plugin at all];Show the answer
This lints plain .js/.ts files fine, but it never mentions eslint-plugin-svelte or its parser — so every .svelte file in the project is either skipped entirely or, if included via a glob, parsed as if it were plain JavaScript (which it isn’t: the markup, {expressions}, and directives aren’t valid JS syntax). Either way, none of the Svelte-specific rules proven above (like the real svelte/no-at-html-tags error) ever run, and a review that only looks at whether “ESLint passes” gets false confidence.
The fix is the flat config from this lesson: import eslint-plugin-svelte, extend svelte.configs.recommended, and point .svelte/.svelte.js/.svelte.ts files at the Svelte parser via languageOptions.parserOptions.svelteConfig.