sveltejs/svelte
github.com/sveltejs/svelte ↗ · 4000 files · 2.1 MB · scan capped
What is Svelte? Svelte is a new way to build web applications.
Overview
This is the `sveltejs/svelte` monorepo: a single publishable package (`packages/svelte`, v5.57.1) containing both a compiler that turns `.svelte` components into plain JavaScript modules and a signals-based client/server runtime those modules import at runtime. The compiler runs a strict three-phase pipeline (parse → analyze → transform) with separate client and server code generators, while the runtime implements Svelte 5 runes ($state/$derived/$effect) on top of a push-pull reactive graph with bitflag-tagged signals, batched flushing, and an experimental async/fork mode. It is aimed at framework consumers (SvelteKit, vite-plugin-svelte, svelte-check, the REPL) rather than app authors directly, and is distinctive for shipping unbundled ESM source as the published artifact, generating its error/warning catalogue and its `.d.ts` types from source, and carrying ~3,400 fixture directories as its primary test surface.
System map
The repo is a pnpm workspace (`pnpm-workspace.yaml` globs `packages/*` and `playgrounds/*`) with exactly one real package, `packages/svelte`, plus a Vite sandbox playground and a Node benchmarking harness at the root. The package has two runtime boundaries encoded in its `exports` map — `browser`/`worker` conditions split every entry into client and server implementations — and a third, the compiler, which is deliberately firewalled from runtime code by a custom ESLint rule. Storage is entirely file-based: there is no database; the only persistent artefacts are compiled test fixtures under `tests/*/samples/*/_output/`, generated docs under `documentation/docs/98-reference/.generated/`, and the checked-in `types/index.d.ts`. External integrations are all build-time libraries (acorn, zimmerframe, esrap, magic-string, devalue) rather than services. Deployment is npm publish via Changesets on push to `main`.
- The published artifact is unbundled ESM source: `packages/svelte/package.json` sets `"main"`/`"module"` to `./src/index-client.js` and its `files` array ships `src` directly, so consumers import the raw runtime sources rather than a bundle.
- Client/server split is done entirely through export conditions — `.`, `./legacy`, `./reactivity`, and `./store` each map `worker` and `default` to `*-server.js` and `browser` to `*-client.js` (e.g. `./src/index-server.js` vs `./src/index-client.js`).
- The compiler's public surface is five functions exported from `src/compiler/index.js`: `compile` (L23), `compileModule` (L69), `parse` (L117), `parseCss` (L131), plus re-exported `preprocess`, `print`, and `migrate` (L202).
- Compilation is a fixed three-phase pipeline — `_parse` from `phases/1-parse/index.js`, then `analyze_component` (2-analyze/index.js:333), then `transform_component` (3-transform/index.js) — with `state.reset()` called first to clear module-global compiler state.
- Codegen forks per target inside `transform_component`: `options.generate === 'server'` dispatches to `server_component` in `3-transform/server/transform-server.js`, otherwise `client_component` in `3-transform/client/transform-client.js`; both emit an ESTree program that `esrap`'s `print` serialises with a source map.
- The runtime reactivity core lives in four files — `internal/client/runtime.js` (866 lines: `get`, `update_reaction`, `update_effect`, `is_dirty`), `reactivity/sources.js`, `reactivity/deriveds.js`, and `reactivity/batch.js` (1475 lines, the `Batch` class and `flushSync`).
- Signal identity and lifecycle are encoded as bitflags in `internal/client/constants.js` — `DERIVED`, `EFFECT`, `RENDER_EFFECT`, `BLOCK_EFFECT`, `BRANCH_EFFECT`, `BOUNDARY_EFFECT`, `CLEAN`/`DIRTY`/`MAYBE_DIRTY`, `ASYNC`, `ERROR_VALUE` — rather than as distinct classes.
- Server rendering is a string-accumulating `Renderer` class (`internal/server/renderer.js`) with a private `#out` array and a separate `head` sub-renderer; `render()` in `internal/server/index.js:71` is a thin wrapper over `Renderer.render`.
- Build commands: `pnpm build` at root fans out to `packages/*`; inside `packages/svelte`, `build` = `rollup -c && pnpm generate && node scripts/check-treeshakeability.js`, where rollup produces only the UMD `compiler/index.js` for CommonJS/browser consumers.
- `pnpm generate` is a three-step codegen chain: `scripts/process-messages` (markdown → `errors.js`/`warnings.js` + docs), `scripts/generate-types.js` (dts-buddy), and `scripts/generate-browser-support.ts` (web-features → docs page).
- Tests run from the repo root via a single Vitest config that aliases every `svelte/*` import back to `packages/svelte` source using the package's own `exports` map, and picks the `server` export when the importer path contains `_output/server`.
- Deployment shape is npm publish driven by Changesets: `.github/workflows/release.yml` runs on push to `main`, calls `changesets/action` with `pnpm changeset:version` / `pnpm changeset:publish`, and sets `NPM_CONFIG_PROVENANCE: true` with `id-token: write`.
Feature catalog
There are no routes or screens — the user-facing surface is a set of npm subpath exports and a compiler API. Broadly: the compiler API (`compile`, `compileModule`, `parse`, `parseCss`, `preprocess`, `print`, `migrate`), the component runtime lifecycle API (`mount`, `hydrate`, `unmount`, `flushSync`, `fork`, `tick`, `untrack`, `settled`), context and lifecycle helpers, and a set of reactive standard-library classes. Around those sit optional subpackages for transitions, animations, easing, motion springs, stores, attachments, and events. Compiler behaviour is configurable through a validated options table, and two experimental features — async mode and `hydratable` — gate themselves on a runtime flag.
- `compile(source, options)` returns `{ js, css, warnings, metadata: { runes }, ast }`, where `ast` is post-processed by `to_public_ast` into either a modern AST (metadata stripped via a zimmerframe walk) or a Svelte 4-shaped legacy AST via `convert` from `compiler/legacy.js`.
- `parseCss(source)` is a standalone CSS-only entry point that uses `Parser.forCss` — a static factory that `Object.create`s a Parser prototype and skips all Svelte component parsing setup — returning a `StyleSheetFile` node with collected `css_comments`.
- `migrate(source, { filename, use_ts })` (2,003 lines in `compiler/migrate/index.js`) is a Svelte 4 → 5 source-to-source codemod exposed publicly through `svelte/compiler`.
- `preprocess` accepts a `PreprocessorGroup` or array and applies hooks in a fixed order per preprocessor — `markup`, then `script`, then `style` — matching `<script>`/`<style>` blocks with dedicated regexes and stitching sourcemaps back together.
- Compiler options are a declarative validator table in `validate-options.js`: `css` ('injected'|'external'), `cssHash`, `dev`, `discloseVersion`, `hmr`, `modernAst`, `namespace` ('html'|'mathml'|'svg'), `preserveComments`, `preserveWhitespace`, `fragments` ('html'|'tree'), `warningFilter`, and `experimental.async`.
- `mount(component, options)` and `hydrate(component, options)` are the two client entry points; both funnel into `_mount`, which accepts `target`, `anchor`, `props`, `events`, `context`, `intro`, and `transformError`.
- The top-level `svelte` export surfaces `onMount`, `onDestroy`, `beforeUpdate`, `afterUpdate`, `createEventDispatcher`, `getAbortSignal`, plus re-exports of `flushSync`/`fork`, the context family, `hydratable`, `mount`/`hydrate`/`unmount`, `tick`/`untrack`/`settled`, and `createRawSnippet`.
- `getAbortSignal()` lazily creates an `AbortController` on the active reaction (`active_reaction.ac ??= new AbortController()`) and throws `get_abort_signal_outside_reaction` if called outside a derived/effect; the abort reason is the shared `STALE_REACTION` singleton error.
- `svelte/reactivity` ships seven reactive primitives: `SvelteDate`, `SvelteSet`, `SvelteMap`, `SvelteURL`, `SvelteURLSearchParams`, `MediaQuery`, and `createSubscriber`.
- `svelte/attachments` provides `createAttachmentKey()` (a `Symbol(ATTACHMENT_KEY)` recognised when spread onto an element) and `fromAction()`, which adapts a legacy `use:` action into the newer `{@attach}` form.
- `render(component, options)` on the server accepts `props`, `context`, `idPrefix`, `csp`, and `transformError`, and hard-errors with `invalid_csp` if both `csp.hash` and `csp.nonce` are supplied.
- `hydratable(key, fn)` is an experimental server→client data-transfer feature: the server side serialises values with `devalue` into a lookup keyed by string, and the client side reads them back out of `window.__svelte.h`; both sides throw `experimental_async_required` when `async_mode_flag` is false.
Data flow
There are two distinct data pipelines. The compile-time pipeline moves a `.svelte` string through a hand-written state-machine parser into a Svelte AST, through a zimmerframe scope/analysis walk that attaches `metadata` and binding information, and out through target-specific ESTree builders into code + sourcemap + CSS. The runtime pipeline moves user mutations into `Source` signals, propagates dirtiness lazily through deriveds, collects affected effects into a `Batch`, and flushes them to write DOM. The two never share code — an ESLint rule enforces that runtime files cannot import from `../compiler/` or `#compiler`. State ownership is module-global on both sides: the compiler owns `compiler/state.js`, the runtime owns `active_reaction`/`active_effect`/`current_batch`.
- Input boundary for compilation is `remove_bom(source)` followed by `state.reset({ warning, filename })`, which clears `warnings`, `source`, `dev`, `runes`, and installs the user's `warningFilter` — every compile starts from a wiped module-global.
- Options arrive from two merged sources: the caller's `CompileOptions` (validated) and `<svelte:options>` attributes parsed out of the source into `parsed.options`, with the in-file values overriding the caller's for `css` and `runes`.
- TypeScript is detected before parsing via a `lang=` regex over `<script>` tags in the `Parser` constructor (setting `this.ts`), and TS nodes are then stripped from `fragment`/`instance`/`module` by `remove_typescript_nodes` before analysis ever sees them.
- The parser is a mutable state machine: `Parser` holds `index`, `stack`, `fragments`, and loops `state = state(this) || fragment` over `template.length`, with states in `1-parse/state/{fragment,element,tag,text}.js`.
- Analysis is a zimmerframe walk over a per-node-type visitor table; the fallthrough `_` visitor reconstructs `svelte-ignore` scope by scanning preceding `Comment` siblings and `leadingComments`, pushing onto `ignore_stack` in `compiler/state.js`.
- Scope and binding resolution is separated into `phases/scope.js` (`create_scopes`, `Scope`, `ScopeRoot`, `get_rune`, `set_scope`), and cyclic reactive statements are caught by `utils/check_graph_for_cycles.js`.
- CSS flows through its own sub-pipeline inside analysis — `analyze_css`, then `prune` (matching selectors against the template), then `warn_unused` — before `render_stylesheet` emits the final stylesheet in the transform phase.
- The output boundary merges sourcemaps twice: `esrap`'s `print` emits a map with `sourceMapContent: source`, then `merge_with_preprocessor_map(js, options, js_source_name)` folds in any preprocessor map supplied via `options.sourcemap`.
- On the runtime side, `$state` values pass through `proxy()` in `internal/client/proxy.js`, which refuses non-objects, anything already carrying `STATE_SYMBOL`, anything with `COMPONENT_SYMBOL`, and any object whose prototype is neither `Object.prototype` nor `Array.prototype`.
- Dependency tracking is a read-side side effect of `get(signal)`: when `active_reaction` is set and not `untracking`, the signal is appended to `new_deps` (or counted as `skipped_deps` when the read order matches the previous run, avoiding array allocation).
- Writes go through `internal_set` in `reactivity/sources.js`, which bumps the global `write_version` (via `increment_write_version` in runtime.js) and schedules dependents; `old_values` is a module-level `Map` used to restore pre-batch values.
- Effects are not run on write — they are collected into the singleton `current_batch` (`Batch.ensure()`), which owns `schedule`, `capture`, `apply`, `flush`, `discard`, and `settled`, and clears `collected_effects`/`legacy_updates`/`old_values` in its `finally` block.
Behavior traces
Three flows dominate. (1) Compile: `compile()` resets global state, validates options, parses, strips TS, analyzes, transforms, and re-attaches a public AST. (2) Client startup: `mount()` initialises DOM operation caches, opens a root effect and an error boundary, pushes a component context, invokes the compiled component function, and returns its exports. (3) Update: a `$state` write sets a source, which schedules its reactions into a `Batch`, which is flushed on the microtask or synchronously via `flushSync`. A fourth, the SSR→hydrate round trip, is what the test harness itself exercises on every fixture.
- Compile trace: `remove_bom` → `state.reset` → `validate_component_options` → `_parse(source)` → destructure `parsed.options` into `combined_options` → conditional `remove_typescript_nodes` → `analyze_component(parsed, source, combined_options)` → `transform_component(analysis, source, combined_options)` → `to_public_ast`.
- Transform trace (client): `client_component` seeds state with `hoisted: [b.import_all('$', 'svelte/internal/client')]` and an empty `templates` Map, walks the template AST with the visitor table, appends `template.body` into `component_block.body`, pushes a `$$css` const when styles are injected, then re-orders `module.body` so imports stay on top followed by hoisted statements.
- Mount trace: `mount()` → `_mount()` → `init_operations()` → `component_root(...)` → create an anchor text node via `target.appendChild(create_text())` → `boundary(anchor, { pending }, cb)` → `push({})` to open a component context → assign `props.$$events` if `events` given → set `should_intro = intro` → call `Component(anchor_node, props)`.
- Hydrate trace: `hydrate()` forces `intro ?? false`, scans forward from `get_first_child(target)` for a comment node whose `data === HYDRATION_START`, throws `HYDRATION_ERROR` if absent, calls `set_hydrating(true)` + `set_hydrate_node(anchor)`, then delegates to the same `_mount` with the discovered anchor.
- Hydration failure recovery: non-Svelte errors are `console.warn`ed, `options.recover === false` escalates to `e.hydration_failed()`, otherwise `init_operations()` + `clear_text_content(target)` runs and the component falls back to a fresh `mount(component, options)` — with `hydrating`/`hydrate_node` restored in `finally`.
- Read trace: `get(signal)` adds the signal to `captured_signals`, registers the dependency on `active_reaction`, and takes one of two registration paths depending on whether `REACTION_IS_UPDATING` is set — the post-`await` path deduplicates with `includes.call` because `remove_reactions` would otherwise disconnect still-live deps.
- Synchronous flush trace: `flushSync(fn)` sets `is_flushing_sync`, flushes any non-fork `current_batch` before running `fn`, then loops `flush_tasks()` + `current_batch.flush()` until `current_batch === null`, restoring the previous flag in `finally`.
- Batch flush trace: `Batch.flush()` clears DEV `source_stacks`, sets `is_processing = true` and `current_batch = this`, runs the private `#process()`, then in `finally` resets `flush_count`, `last_scheduled_effect`, `collected_effects`, `legacy_updates`, `current_batch`, `batch_values`, and `old_values`.
- `{@html}` trace: the `html()` block runs inside a `template_effect`, short-circuits on unchanged value, uses `parent_node.innerHTML = value` when it is the only child, otherwise builds a `<template>`/`<svg>`/`<math>` wrapper, sets `wrapper.innerHTML`, and inserts the resulting nodes via `anchor.before(...)`.
- HMR trace: `hmr(fn)` wraps the component in a `source(fn)` signal; a `block()` effect reads it, and on change deletes every key from the old `instance`, calls `destroy_effect(effect)`, and re-creates a `branch()` with `set_should_intro(false)` so the swap plays no transitions.
- Test trace: `runtime_suite` → `common_setup` builds a `CompileOptions` (forcing `accessors: true` by default and `experimental.async` from `runes && async_mode`) and calls `compile_directory(cwd, 'client')` and `compile_directory(cwd, 'server')` → `run_test_variant` imports from `_output/` and calls `render`, `mount`, or `hydrate` per variant.
- `compile_directory` trace: wipe `_output/<generate>`, glob every non-`_`-prefixed file, route `.svelte.js` through `compileModule`, plain `.js` through a re-export shim so `_config.js` can share module state, and `.svelte` through optional `preprocess` then `compile`, honouring `.client.svelte`/`.server.svelte` suffixes.
Testing signals
Testing is Vitest-driven with a fixture-directory convention: 22 suites under `packages/svelte/tests/`, each a `test.ts` plus a `samples/` directory where every subdirectory is one test, optionally configured by a `_config.js`. Coverage is enormous by fixture count — 1,209 legacy runtime samples, 1,054 runes runtime samples, 335 validator samples, 184 CSS samples — and the runtime suites multiply each by four variants (dom, hydrate, ssr, async-ssr). Real-browser coverage is narrow: only `runtime-browser` (35 samples) and the custom-elements samples run under Playwright Chromium. CI runs the full suite on Node 20/22/24 across Windows, macOS, and Ubuntu, plus a no-async pass, a tsgo typecheck, a lint/types-drift gate, and a benchmark job.
- Vitest 4 is the single test runner, configured once at the repo root with `include` patterns `packages/svelte/**/*.test.ts`, `packages/svelte/tests/*/test.ts`, and `packages/svelte/tests/runtime-browser/test-ssr.ts`, and `**/samples/**` excluded so fixtures are never collected as tests.
- The global `testTimeout` is raised to 30s with an in-file rationale: dev-mode tests that trip `effect_update_depth_exceeded` allocate ~1000 Error objects per flush for stack tracking, which overruns the 5s default under Vitest 4.
- `tests/suite.ts` provides the two harness primitives: `suite()` (one `it` per sample dir) and `suite_with_variants()` (one `it` per dir × variant, with a `should_skip_variant` hook returning `true` to skip or `'no-test'` to omit entirely).
- Test selection supports both `pnpm test <suite> -t <name>` and a `FILTER` env var that compiles to an anchored or `/regex/` RegExp in `suite.ts`, plus per-config `skip`/`solo`/`timeout` flags on `BaseTest`.
- Runtime suites run four variants per sample — `dom`, `hydrate`, `ssr`, `async-ssr` — with `async-ssr` gated on both runes mode and `async_mode` (i.e. `process.env.SVELTE_NO_ASYNC !== 'true'`).
- Fixture counts skew heavily toward runtime behaviour: `runtime-legacy/samples` (1209), `runtime-runes/samples` (1054), `validator/samples` (335), `css/samples` (184), `compiler-errors/samples` (146), `server-side-rendering/samples` (132).
- Real-browser testing is limited to `tests/runtime-browser/test.ts`, which launches a single Chromium via `@playwright/test`, bundles each sample with `esbuild`, and runs `dom`/`hydrate` variants plus a separate `custom-elements-samples` suite — with `retry: 1` on CI because 'browser tests are brittle and slow on CI'.
- XHTML/XML-document behaviour gets its own environment: `vitest-xhtml-environment.ts` wraps the jsdom builtin with `contentType: 'application/xhtml+xml'`, consumed by `tests/runtime-xhtml/test.ts` via a `@vitest-environment` pragma (13 samples).
- Unit tests colocated with source are rare — only `src/utils.test.ts`, `src/internal/client/proxy.test.ts`, `src/internal/client/error-handling.test.ts`, the four `src/reactivity/*.test.ts` files, and `src/internal/server/{crypto,hydratable,renderer}.test.ts`.
- Snapshot-style suites (`snapshot`, `parser-modern`, `parser-legacy`, `print`, `sourcemaps`) assert generated output against checked-in expectations and are regenerated with `UPDATE_SNAPSHOTS=true pnpm test`.
- CI's `Tests` job matrixes Node 20 across windows/macOS/ubuntu plus Node 22 and 24 on ubuntu, installs only Chromium (`PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD: '1'` then `pnpm playwright install chromium`), with a 15-minute timeout.
- A dedicated `TestNoAsync` job re-runs only `runtime-runes` with `SVELTE_NO_ASYNC: true`, guarding the non-async code path that the default run never exercises.
Risk map
The highest-severity surfaces are the two `innerHTML` sinks (`{@html}` and the template reconciler) and the server-side CSP/hash path, all of which are correct-by-design but unforgiving of misuse. Structurally, the biggest fragility is module-global mutable state on both sides of the codebase — `compiler/state.js` makes `compile()` non-reentrant, and `active_reaction`/`active_effect`/`current_batch`/`old_values` make the runtime a single-threaded global machine where any missed `finally` leaks state across components. Blast radius is concentrated in `runtime.js`, `reactivity/batch.js`, and `phases/2-analyze/index.js`, each of which sits under thousands of fixtures. Legacy code is not dead but is a permanent second code path: legacy mode, legacy AST conversion, and `beforeUpdate`/`afterUpdate` all have their own branches.
- `{@html}` is the primary XSS surface and is documented as such: `html()` assigns user values straight to `parent_node.innerHTML` or `wrapper.innerHTML` with no sanitisation, relying only on the fact that `innerHTML` does not execute `<script>`.
- A second `innerHTML` sink exists in the template reconciler, where compiled template strings are assigned via `create_trusted_html(html.replaceAll('<!>', '<!---->'))` — safe only because the input is compiler-generated rather than user-supplied. (inferred)
- `escape_html` escapes only `&`, `<`, and (in attribute mode) `"` — single quotes and `>` are deliberately not escaped, so any compiler codegen path that emits a single-quoted attribute without also escaping would be an injection hole. (inferred)
- The server-side CSP path computes `sha256` hashes over inline script bodies and pushes them onto `global.csp.script_hashes`; `invalid_csp` is thrown only for the hash+nonce combination, and the nonce is interpolated into the attribute unescaped (` nonce="${this.global.csp.nonce}"`).
- `internal/server/crypto.js` performs a deliberately obfuscated dynamic import — `import(/* @vite-ignore */ module_name)` on the string `'node:crypto'` — specifically so static analysers cannot see the Node dependency; this defeats bundler and supply-chain tooling that relies on static import graphs.
- The compiler is not reentrant: `compiler/state.js` holds `warnings`, `source`, `filename`, `dev`, `runes`, `ignore_stack`, and `ignore_map` as module-level `let`s that `reset()` clobbers, so two concurrent `compile()` calls in one process would interleave diagnostics. (inferred)
- The runtime has the same shape of risk at larger scale: `active_reaction`, `active_effect`, `untracking`, `new_deps`, `skipped_deps`, `untracked_writes`, `write_version`, and `update_version` are all module-level mutable globals in `runtime.js`.
- `Batch.flush()` relies entirely on its `finally` block to reset seven pieces of cross-cutting state (`flush_count`, `last_scheduled_effect`, `collected_effects`, `legacy_updates`, `current_batch`, `batch_values`, `old_values`); an early return added above that block would leave the whole runtime wedged. (inferred)
- Signal semantics are encoded in 26 overlapping bitflags, and two of them collide by design — `MANAGED_EFFECT` and `DESTROYING` both use `1 << 24`, and `EFFECT_OFFSCREEN` and `DESTROYING` both use `1 << 25` — meaning correctness depends on flags never co-occurring on the same signal.
- Hydration failure is silently recoverable by default: any non-Svelte error triggers `console.warn` + full client re-render, so a genuine SSR/CSR divergence only surfaces as a warning unless the consumer passes `recover: false`.
- Legacy Svelte 4 support is a permanently parallel code path, not dead code: `legacy_mode_flag`, `compiler/legacy.js`'s AST `convert`, `legacy_pre_effect`/`legacy_pre_effect_reset`, `internal/client/dev/legacy.js`, and the `beforeUpdate`/`afterUpdate` callback queue all exist solely for it.
- Async mode is experimental and dual-pathed — `enable_async_mode_flag` is set only by importing `svelte/internal/flags/async`, and there is a `disable_async_mode_flag` explicitly annotated 'ONLY USE THIS DURING TESTING' — so the non-async path is only covered by the single `TestNoAsync` CI job.
Extension points
Almost every extension point is a plain object literal keyed by node type or property name, which makes adding language features mechanical: drop a file into the right `visitors/` directory and register it in the table. The three big ones are the analysis visitor table, the client and server transform visitor tables, and the `binding_properties` registry that defines what `bind:` can target. Below those sit the compiler options validator table, the markdown-driven message catalogue, the preprocessor hook interface, and the export map itself. On the runtime side, `createSubscriber` and `createAttachmentKey` are the sanctioned APIs for library authors who need to plug into reactivity and element lifecycle without touching internals.
- Adding a new analysable node type means creating `phases/2-analyze/visitors/<NodeType>.js` and adding it to the `visitors` object in `2-analyze/index.js` — the file currently imports ~60 such visitors explicitly by name.
- Client codegen has a parallel registry: `const visitors = {...}` at `3-transform/client/transform-client.js:65`, backed by ~45 files in `3-transform/client/visitors/` plus a `shared/` directory for logic common to both targets.
- The server codegen mirrors it in `3-transform/server/transform-server.js` exporting `server_component` and `server_module`, so a new template feature generally requires a matching visitor in both client and server visitor directories.
- `phases/bindings.js` exports `binding_properties`, a ~47-entry `Record<string, BindingProperty>` where each entry declares `event`, `bidirectional`, `omit_in_ssr`, `valid_elements`, and `invalid_elements` — this is the single table that defines which `bind:` directives exist.
- New compiler options are registered declaratively in `validate-options.js` using the `string()`, `boolean()`, `list()`, `fun()`, and `warning()` combinators, split between `common_options` and `component_options`.
- New errors/warnings are added as markdown `## CODE` sections under `packages/svelte/messages/<category>/*.md` across eight categories (client/server/shared/compile × errors/warnings); `scripts/process-messages` generates both the runtime throwers and the reference docs from them.
- The generated message modules are explicitly excluded from linting (`src/compiler/errors.js`, `src/internal/client/errors.js`, `src/internal/client/warnings.js`, `src/internal/shared/warnings.js`, `src/internal/server/warnings.js`), which is the signal that they must never be hand-edited.
- The public plugin interface for build tooling is `PreprocessorGroup` — an object with optional `markup`, `script`, and `style` async hooks, each receiving `{ content, attributes, markup, filename }` and returning `{ code, map }`.
- New public subpaths are added by editing the `exports` map in `packages/svelte/package.json`; `scripts/check-treeshakeability.js` then reads that same map to resolve each entry and asserts via a rollup bundle that importing it produces no side effects.
- `createSubscriber` (in `src/reactivity/create-subscriber.js`, exported from `svelte/reactivity`) is the sanctioned hook for library authors wiring external event sources into Svelte's reactivity — it is how `MediaQuery` is built.
- `createAttachmentKey()` is the programmatic extension point for element lifecycle: it returns a `Symbol(ATTACHMENT_KEY)` that the runtime recognises when spread onto an element, letting library authors attach behaviour without a `use:` directive.
- New block types plug into `internal/client/dom/blocks/` — the directory currently holds `async`, `await`, `boundary`, `branches`, `css-props`, `each`, `html`, `if`, `key`, `slot`, `snippet`, `svelte-component`, `svelte-element`, and `svelte-head`, each a function invoked by generated code.
Contributor handoff
Read `AGENTS.md`, `CONTRIBUTING.md`, and `.github/PULL_REQUEST_TEMPLATE.md` first — the repo's own agent guide makes the PR template mandatory and forbids submitting without a full test run. The dominant convention is `snake_case` internally, `camelCase` publicly; the dominant workflow is add a fixture directory, not a new test file. The two hard edit boundaries are generated files (types, error/warning modules, generated docs) and the compiler/runtime import firewall. Expect long feedback loops: the full suite covers ~3,400 fixtures across 4 variants, so filter aggressively while iterating and only run the full suite before opening a PR.
- `AGENTS.md` is explicit that `CONTRIBUTING.md` must also be read, that `PULL_REQUEST_TEMPLATE.md` must be read and filled out correctly for any PR, and that PRs must not be submitted without running the full test suite.
- Naming convention is enforced, not advisory: `snake_case` for internal variables and methods, `camelCase` for public ones, backed by the `lube/svelte-naming-convention` ESLint rule set to `error` with `fixSameNames: true`.
- Never hand-edit `packages/svelte/types/index.d.ts`, `src/compiler/errors.js`, or the four `warnings.js` modules — they are generated by `pnpm generate` and CI fails the `Lint` job if `git status` is dirty after `pnpm build`.
- Never import from `../compiler/` or `#compiler` inside `packages/svelte/src/**` outside `src/compiler/**` — the custom `no_compiler_imports` rule string-searches the file text and rejects even JSDoc type references.
- Adding a test means creating a directory under `packages/svelte/tests/<suite>/samples/`, optionally with a `_config.js` default-exporting the config object — `for_each_dir` in `suite.ts` discovers it automatically, so no test file is edited.
- Files prefixed with `_` inside a sample directory are excluded from compilation by `compile_directory`, which is how `_config.js` coexists with the component files it tests.
- A sample directory containing only `_output` or `_actual.json` is treated as leftover from another branch and silently skipped — so a suite that appears to pass may simply not have run.
- Tests require Chromium to be installed first (`pnpm playwright install chromium`) or the `runtime-browser` and custom-elements suites will fail to launch.
- Iterate with `pnpm test <suite>`, `pnpm test <suite> -t <name>`, or `FILTER=<name> pnpm test <suite>`; snapshot suites are regenerated with `UPDATE_SNAPSHOTS=true pnpm test`.
- Typechecking is a three-project run inside `packages/svelte`: `tsc --project tsconfig.runtime.json && tsc && cd ./tests/types && tsc` — the `tests/types` project exists purely to typecheck public API ergonomics (`actions.ts`, `snippet.ts`, `store.ts`, `bindings.svelte`, …).
- Any change under `packages/svelte/src` requires a changeset (`npx changeset`) per the PR template; the root `changeset:version` script also runs `pnpm -r generate:version` so `src/version.js` stays in sync.
- For performance work, use the `performance-investigation` skill: run `pnpm bench:compare main <branch>`, read `benchmarking/compare/.results/report.txt`, then diff the per-benchmark `.md` CPU-profile summaries, and re-run `pnpm test runtime-runes` afterward.
Languages
| Language | Files | Share of source |
|---|---|---|
| Svelte | 4547 | 56% |
| JavaScript | 3438 | 43% |
| JSON | 461 | docs/config |
| HTML | 254 | docs/config |
| CSS | 189 | docs/config |
| Markdown | 136 | docs/config |
| TypeScript | 86 | 1% |
| YAML | 10 | docs/config |
Start reading here
Probable source entrypoint: packages/ — inferred from the directory tree and manifest, not the upstream docs.
Source roots
packages/ (3813 files)
Source layout — packages/
| Path | Files |
|---|---|
packages/svelte/ | 134 |
Repository structure
| Top-level path | Files |
|---|---|
packages/ | 3813 |
documentation/ | 106 |
benchmarking/ | 33 |
playgrounds/ | 16 |
.github/ | 10 |
.changeset/ | 2 |
.well-known/ | 1 |
.agents/ | 1 |
Docs found: AGENTS.mdCONTRIBUTING.mdLICENSE.mdREADME.mddocumentation/docs/01-introduction/01-overview.mddocumentation/docs/01-introduction/02-getting-started.mddocumentation/docs/01-introduction/03-svelte-files.mddocumentation/docs/01-introduction/04-svelte-js-files.mddocumentation/docs/01-introduction/index.mddocumentation/docs/02-runes/01-what-are-runes.md
Package
package.json — svelte-monorepo v0.0.1, 19 declared dependencies.
@changesets/changelog-github@changesets/cli@eslint/js@sveltejs/eslint-config@types/node@types/picomatch@vitest/coverage-v8eslinteslint-plugin-lubeeslint-plugin-sveltejsdomplaywright
CI & config entrypoints
README.md— Repository readmeAGENTS.md— Agent instructions.github/workflows/autofix.yml— GitHub Actions workflow.github/workflows/ci.yml— GitHub Actions workflow.github/workflows/ecosystem-ci-trigger.yml— GitHub Actions workflow.github/workflows/release.yml— GitHub Actions workflowplaygrounds/sandbox/vite.config.js— Vite configpackage.json— npm scripts: bench, bench:compare, bench:debug, build, changeset:publish, changeset:version, check, formatpackages/svelte/package.json— npm scripts: build, check, check:tsgo, check:watch, dev, generate, generate:browser-support, generate:typesplaygrounds/sandbox/package.json— npm scripts: build, create-test, dev, download, hash, prepare, preview, prod
Structural health signals
Average score 9/10 across 30 analyzed source files; 1 hotspot surfaced.
playgrounds/sandbox/scripts/download.js— 815 lines, score 6.4 (Large file, High churn)benchmarking/utils.js— 333 lines, score 7.4 (Long function/block, High churn)benchmarking/analyze-compiler-profile.js— 346 lines, score 8.1 (High churn, No adjacent test signal)
These are deterministic review leads — size, churn, and structural signals — not proof of a bug in this project.
Recent history
636eaaaa6f062026-09-18 — Version Packages (#18750)8030ff2a2c8b2026-09-18 — fix: remove incorrect special handling of :export pseudo-selectors (#18839)a72dc8eadbf62026-09-18 — fix: make Object.hasOwn reactive for state proxies (#18838)6eb720a1b7ca2026-09-11 — chore: scope.js stops importing from the analysis visitors (#18787)e6ae460caa722026-09-11 — chore: analysis and transform stop importing each other (#18786)