Merge branch 'main' into entangle-batches-2

entangle-batches-2
Simon H 2 weeks ago committed by GitHub
commit 0fa394fec5
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -0,0 +1,5 @@
---
'svelte': patch
---
fix: block template store subscriptions on the promise that assigns the store

@ -0,0 +1,5 @@
---
'svelte': patch
---
fix: route $derived teardown errors through invoke_error_boundary

@ -0,0 +1,5 @@
---
'svelte': patch
---
fix: track SvelteDate snapshots in reactions

@ -0,0 +1,5 @@
---
'svelte': patch
---
fix: remove `<svelte:head>` anchors on unmount

@ -1,6 +1,6 @@
{ {
"$schema": "https://unpkg.com/@changesets/config@2.3.0/schema.json", "$schema": "https://unpkg.com/@changesets/config@2.3.0/schema.json",
"changelog": ["@svitejs/changesets-changelog-github-compact", { "repo": "sveltejs/svelte" }], "changelog": ["@changesets/changelog-github", { "repo": "sveltejs/svelte", "template": "\n- {summary} {ref}" }],
"commit": false, "commit": false,
"fixed": [], "fixed": [],
"linked": [], "linked": [],

@ -0,0 +1,5 @@
---
'svelte': patch
---
fix: warn on undeclared shorthand event handlers on `<svelte:window>`, `<svelte:document>` and `<svelte:body>`

@ -0,0 +1,5 @@
---
'svelte': patch
---
perf: reuse the cached value in the `<option>`/`<select>` value guard

@ -0,0 +1,5 @@
---
'svelte': patch
---
fix: prevent malformed AST output for `<select>` with static `value` attribute

@ -0,0 +1,5 @@
---
'svelte': patch
---
fix: apply ownership mutation ignores to binding assignments

@ -0,0 +1,5 @@
---
'svelte': patch
---
fix: prevent onoutroend from firing twice when compilerOptions.hmr is true

@ -0,0 +1,5 @@
---
'svelte': patch
---
fix: preserve whitespace after inline elements when printing

@ -0,0 +1,5 @@
---
'svelte': patch
---
perf: fold SSR block-open markers into the branch's first push

@ -0,0 +1,5 @@
---
'svelte': patch
---
fix: run `onDestroy` callbacks when a server render throws

@ -0,0 +1,5 @@
---
'svelte': patch
---
fix: report `derived_invalid_export` for `export let x = $derived(...)` in runes mode

@ -0,0 +1,5 @@
---
'svelte': patch
---
fix: never apply class hash to elements inside `<svelte:head>`

@ -0,0 +1,5 @@
---
'svelte': patch
---
fix: keep `defaultChecked` on hydrated radio inputs with spread attributes

@ -0,0 +1,5 @@
---
'svelte': patch
---
fix: accept `onfocusin`/`onfocusout` in `a11y_mouse_events_have_key_events`

@ -0,0 +1,5 @@
---
'svelte': patch
---
perf: O(n²)→O(n) Map lookups for legacy `$:` reactive statement ordering

@ -0,0 +1,5 @@
---
'svelte': patch
---
fix: distinct memoizer on style/class directives

@ -0,0 +1,5 @@
---
'svelte': patch
---
fix: measure nested transitions before applying their starting styles

@ -0,0 +1,5 @@
---
'svelte': patch
---
fix: don't turn component instances stored in `$state` into state proxies

@ -0,0 +1,5 @@
---
'svelte': minor
---
feat: export `RenderOutput`, `SyncRenderOutput`, `Csp` and `Sha256Source` from `svelte/server`

@ -0,0 +1,5 @@
---
'svelte': patch
---
perf: emit `$.only_child` for elements with a single child

@ -0,0 +1,5 @@
---
'svelte': patch
---
fix: more robust rendering of Svelte custom element slots

@ -0,0 +1,5 @@
---
"svelte": patch
---
perf: optimize simple object destructuring in `@const` tags

@ -0,0 +1,5 @@
---
'svelte': patch
---
fix: end a restored reaction context at the end of its synchronous segment

@ -0,0 +1,5 @@
---
'svelte': patch
---
fix: keep the dependencies of a reaction that throws, so deriveds it read are neither leaked nor stuck in their error

@ -0,0 +1,5 @@
---
'svelte': patch
---
fix: don't resurrect outroing elements when an ancestor block is paused and resumed

@ -0,0 +1,5 @@
---
'svelte': patch
---
perf: use `$.comment()` for single-comment templates

@ -0,0 +1,5 @@
---
'svelte': patch
---
perf: store setters cache as `Set` instead of `Array`

@ -0,0 +1,5 @@
---
'svelte': patch
---
fix: transform derived assignments and select function bindings correctly during server-side rendering

@ -0,0 +1,5 @@
---
'svelte': patch
---
fix: keep boolean attributes with an empty string value when rendering attribute objects on the server

@ -0,0 +1,5 @@
---
'svelte': patch
---
fix: sync `SvelteURL` port signal when the protocol setter clears the port

@ -0,0 +1,5 @@
---
'svelte': minor
---
feat: support `defaultValue` on `<select>`

@ -0,0 +1,5 @@
---
'svelte': patch
---
fix: block declaration tags and `{@const}` on async values read inside closures

@ -0,0 +1,5 @@
---
'svelte': patch
---
fix: avoid css tree-shaking for exported Snippet

@ -0,0 +1,5 @@
---
'svelte': patch
---
perf: treat `<img loading>` as a static element again

@ -0,0 +1,5 @@
---
'svelte': patch
---
fix: prevent `selectedcontent` mutation from changing the selected option

@ -0,0 +1,5 @@
---
'svelte': patch
---
fix: avoid `NaN` keyframe values in `slide` transition for elements without a layout box

@ -0,0 +1,5 @@
---
'svelte': patch
---
fix: preserve line feed character references in attribute values

@ -0,0 +1,5 @@
---
'svelte': patch
---
fix: decode uppercase-`X` hex numeric character references (`&#X...;`)

@ -0,0 +1,5 @@
---
'svelte': patch
---
chore: clarify when `$effect.pre` runs relative to DOM updates

@ -0,0 +1,5 @@
---
'svelte': patch
---
fix: scope SSR boundary failed snippets to their boundary

@ -28,7 +28,7 @@ jobs:
- name: Get PR ref - name: Get PR ref
if: github.event_name != 'workflow_dispatch' if: github.event_name != 'workflow_dispatch'
id: pr id: pr
uses: actions/github-script@v8 uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
with: with:
script: | script: |
const { data: pull } = await github.rest.pulls.get({ const { data: pull } = await github.rest.pulls.get({
@ -46,12 +46,12 @@ jobs:
core.setFailed('PR is from a fork'); core.setFailed('PR is from a fork');
} }
core.setOutput('ref', pull.head.ref); core.setOutput('ref', pull.head.ref);
- uses: actions/checkout@v6 - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
if: github.event_name == 'workflow_dispatch' || steps.pr.outcome == 'success' if: github.event_name == 'workflow_dispatch' || steps.pr.outcome == 'success'
with: with:
ref: ${{ github.event_name == 'workflow_dispatch' && github.ref || steps.pr.outputs.ref }} ref: ${{ github.event_name == 'workflow_dispatch' && github.ref || steps.pr.outputs.ref }}
- uses: pnpm/action-setup@b906affcce14559ad1aafd4ab0e942779e9f58b1 # v4.3.0 - uses: pnpm/action-setup@0e279bb959325dab635dd2c09392533439d90093 # v6.0.8
- uses: actions/setup-node@v6 - uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6
with: with:
node-version: 24 node-version: 24
cache: pnpm cache: pnpm

@ -32,9 +32,9 @@ jobs:
os: ubuntu-latest os: ubuntu-latest
steps: steps:
- uses: actions/checkout@v6 - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
- uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v4 - uses: pnpm/action-setup@0e279bb959325dab635dd2c09392533439d90093 # v6.0.8
- uses: actions/setup-node@v6 - uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6
with: with:
node-version: ${{ matrix.node-version }} node-version: ${{ matrix.node-version }}
cache: pnpm cache: pnpm
@ -48,9 +48,9 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
timeout-minutes: 10 timeout-minutes: 10
steps: steps:
- uses: actions/checkout@v6 - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
- uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v4 - uses: pnpm/action-setup@0e279bb959325dab635dd2c09392533439d90093 # v6.0.8
- uses: actions/setup-node@v6 - uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6
with: with:
node-version: 22 node-version: 22
cache: pnpm cache: pnpm
@ -65,9 +65,9 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
timeout-minutes: 5 timeout-minutes: 5
steps: steps:
- uses: actions/checkout@v6 - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
- uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v4 - uses: pnpm/action-setup@0e279bb959325dab635dd2c09392533439d90093 # v6.0.8
- uses: actions/setup-node@v6 - uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6
with: with:
node-version: 24 node-version: 24
cache: pnpm cache: pnpm
@ -82,9 +82,9 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
timeout-minutes: 10 timeout-minutes: 10
steps: steps:
- uses: actions/checkout@v6 - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
- uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v4 - uses: pnpm/action-setup@0e279bb959325dab635dd2c09392533439d90093 # v6.0.8
- uses: actions/setup-node@v6 - uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6
with: with:
node-version: 24 node-version: 24
cache: pnpm cache: pnpm
@ -105,9 +105,9 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
timeout-minutes: 15 timeout-minutes: 15
steps: steps:
- uses: actions/checkout@v6 - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
- uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v4 - uses: pnpm/action-setup@0e279bb959325dab635dd2c09392533439d90093 # v6.0.8
- uses: actions/setup-node@v6 - uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6
with: with:
node-version: 24 node-version: 24
cache: pnpm cache: pnpm

@ -17,7 +17,7 @@ jobs:
contents: read # to clone the repo contents: read # to clone the repo
steps: steps:
- name: Check User Permissions - name: Check User Permissions
uses: actions/github-script@v8 uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
id: check-permissions id: check-permissions
with: with:
script: | script: |
@ -56,7 +56,7 @@ jobs:
} }
- name: Get PR Data - name: Get PR Data
uses: actions/github-script@v8 uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
id: get-pr-data id: get-pr-data
with: with:
script: | script: |
@ -106,7 +106,7 @@ jobs:
- name: Generate Token - name: Generate Token
id: generate-token id: generate-token
uses: actions/create-github-app-token@v2 uses: actions/create-github-app-token@fee1f7d63c2ff003460e3d139729b119787bc349 # v2
with: with:
app-id: ${{ secrets.ECOSYSTEM_CI_GITHUB_APP_ID }} app-id: ${{ secrets.ECOSYSTEM_CI_GITHUB_APP_ID }}
private-key: ${{ secrets.ECOSYSTEM_CI_GITHUB_APP_PRIVATE_KEY }} private-key: ${{ secrets.ECOSYSTEM_CI_GITHUB_APP_PRIVATE_KEY }}
@ -115,7 +115,7 @@ jobs:
svelte-ecosystem-ci svelte-ecosystem-ci
- name: Trigger Downstream Workflow - name: Trigger Downstream Workflow
uses: actions/github-script@v8 uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
id: trigger id: trigger
env: env:
COMMENT: ${{ github.event.comment.body }} COMMENT: ${{ github.event.comment.body }}

@ -1,229 +0,0 @@
name: pkg.pr.new
on:
pull_request_target:
types: [opened, synchronize]
push:
branches: [main]
workflow_dispatch:
inputs:
sha:
description: 'Commit SHA to build'
required: true
type: string
pr:
description: 'PR number to comment on'
required: true
type: number
permissions: {}
jobs:
build:
# Skip pull_request_target events from forks — maintainers can use workflow_dispatch instead
if: >
github.event_name != 'pull_request_target' ||
github.event.pull_request.head.repo.full_name == github.repository
runs-on: ubuntu-latest
# No permissions — this job runs user-controlled code
permissions: {}
steps:
- uses: actions/checkout@v6
with:
# For pull_request_target, check out the PR head.
# For workflow_dispatch, check out the manually specified SHA.
# For push, fall back to the push SHA.
ref: ${{ github.event.pull_request.head.sha || inputs.sha || github.sha }}
- uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v4
- uses: actions/setup-node@v6
with:
node-version: 22.x
cache: pnpm
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Build
run: pnpm build
- run: pnpx pkg-pr-new publish --comment=off --json output.json --compact --no-template './packages/svelte'
- name: Upload output
uses: actions/upload-artifact@v4
with:
name: output
path: ./output.json
# Sanitizes the untrusted output from the build job before it's consumed by
# jobs with elevated permissions. This ensures that only known package names
# and valid SHA prefixes make it through.
sanitize:
needs: build
runs-on: ubuntu-latest
permissions: {}
steps:
- name: Download artifact
uses: actions/download-artifact@v7
with:
name: output
- name: Sanitize output
uses: actions/github-script@v8
with:
script: |
const fs = require('fs');
const raw = JSON.parse(fs.readFileSync('output.json', 'utf8'));
const ALLOWED_PACKAGES = new Set(['svelte']);
const SHA_PATTERN = /^[0-9a-f]{7}$/;
const packages = (raw.packages || [])
.filter(p => {
if (!ALLOWED_PACKAGES.has(p.name)) {
console.log(`Skipping unexpected package: ${JSON.stringify(p.name)}`);
return false;
}
const sha = p.url?.replace(/^.+@([^@]+)$/, '$1');
if (!sha || !SHA_PATTERN.test(sha)) {
console.log(`Skipping package with invalid SHA: ${JSON.stringify(p.url)}`);
return false;
}
return true;
})
.map(p => ({
name: p.name,
sha: p.url.replace(/^.+@([^@]+)$/, '$1'),
}));
fs.writeFileSync('sanitized-output.json', JSON.stringify({ packages }), 'utf8');
- name: Upload sanitized output
uses: actions/upload-artifact@v4
with:
name: sanitized-output
path: ./sanitized-output.json
comment:
needs: sanitize
if: github.event_name == 'pull_request_target' || github.event_name == 'workflow_dispatch'
runs-on: ubuntu-latest
permissions:
contents: read
pull-requests: write
steps:
- name: Download sanitized artifact
uses: actions/download-artifact@v7
with:
name: sanitized-output
- name: Resolve PR number
id: pr
uses: actions/github-script@v8
with:
script: |
if (context.eventName === 'pull_request_target') {
core.setOutput('number', context.issue.number);
return;
}
// For workflow_dispatch, use the explicitly provided PR number.
// We can't use listPullRequestsAssociatedWithCommit because fork
// commits don't exist in the base repo, so the API returns nothing.
const pr = Number('${{ inputs.pr }}');
if (!pr || isNaN(pr)) {
core.setFailed('workflow_dispatch requires a valid pr input');
return;
}
core.setOutput('number', pr);
- name: Post or update comment
uses: actions/github-script@v8
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
script: |
const fs = require('fs');
const { packages } = JSON.parse(fs.readFileSync('sanitized-output.json', 'utf8'));
if (packages.length === 0) {
console.log('No valid packages found. Skipping comment.');
return;
}
const issue_number = parseInt('${{ steps.pr.outputs.number }}', 10);
const bot_comment_identifier = `<!-- pkg.pr.new comment -->`;
const body = `${bot_comment_identifier}
[Playground](https://svelte.dev/playground?version=pr-${issue_number})
\`\`\`
${packages.map(p => `pnpm add https://pkg.pr.new/${p.name}@${issue_number}`).join('\n')}
\`\`\`
`;
const comments = await github.rest.issues.listComments({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number,
});
const existing = comments.data.find(c => c.body.includes(bot_comment_identifier));
if (existing) {
await github.rest.issues.updateComment({
owner: context.repo.owner,
repo: context.repo.repo,
comment_id: existing.id,
body,
});
} else {
await github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number,
body,
});
}
log:
needs: sanitize
if: github.event_name == 'push'
runs-on: ubuntu-latest
permissions: {}
steps:
- name: Download sanitized artifact
uses: actions/download-artifact@v7
with:
name: sanitized-output
- name: Log publish info
uses: actions/github-script@v8
with:
script: |
const fs = require('fs');
const { packages } = JSON.parse(fs.readFileSync('sanitized-output.json', 'utf8'));
if (packages.length === 0) {
console.log('No valid packages found.');
return;
}
console.log('\n' + '='.repeat(50));
console.log('Publish Information');
console.log('='.repeat(50));
for (const p of packages) {
console.log(`${p.name} - pnpm add https://pkg.pr.new/${p.name}@${p.sha}`);
}
const svelte = packages.find(p => p.name === 'svelte');
if (svelte) {
console.log(`\nPlayground: https://svelte.dev/playground?version=commit-${svelte.sha}`);
}
console.log('='.repeat(50));

@ -23,13 +23,13 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Checkout Repo - name: Checkout Repo
uses: actions/checkout@v6 uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
with: with:
# This makes Actions fetch all Git history so that Changesets can generate changelogs with the correct commits # This makes Actions fetch all Git history so that Changesets can generate changelogs with the correct commits
fetch-depth: 0 fetch-depth: 0
- uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v4 - uses: pnpm/action-setup@0e279bb959325dab635dd2c09392533439d90093 # v6.0.8
- name: Setup Node.js - name: Setup Node.js
uses: actions/setup-node@v6 uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6
with: with:
node-version: 24.x node-version: 24.x
cache: pnpm cache: pnpm

@ -205,6 +205,8 @@ In rare cases, you may need to run code _before_ the DOM updates. For this we ca
</div> </div>
``` ```
`$effect.pre` runs before DOM updates that are scheduled after it, not before every DOM mutation in the flush - DOM of parent components may already be updated. When using [await expressions](await-expressions), block updates like `{#if ...}` and `{#each ...}` in the same component also run before `$effect.pre`.
Apart from the timing, `$effect.pre` works exactly like `$effect`. Apart from the timing, `$effect.pre` works exactly like `$effect`.
## `$effect.tracking` ## `$effect.tracking`

@ -52,3 +52,5 @@ In this case, you can specify a fallback value for when no prop is passed at all
/// file: FancyInput.svelte /// file: FancyInput.svelte
let { value = $bindable('fallback'), ...props } = $props(); let { value = $bindable('fallback'), ...props } = $props();
``` ```
When a bindable prop has a fallback value, the parent must pass a value other than `undefined` if it uses `bind:`. This avoids ambiguity about which value should apply, since the parent and child should share the same value for a binding.

@ -202,7 +202,7 @@ You can add a special comment starting with `@component` that will show up when
- You can also use code blocks here. - You can also use code blocks here.
- Usage: - Usage:
```html ```html
<Main name="Arethra"> <Main name="Aretha">
``` ```
--> -->
<script> <script>
@ -215,3 +215,14 @@ You can add a special comment starting with `@component` that will show up when
</h1> </h1>
</main> </main>
```` ````
You can also put JavaScript-style comments within tags between attributes:
```svelte
<div
// this is a comment!
data-foo="bar"
>
foo bar
</div>
```

@ -13,7 +13,7 @@ Declaration tags define local variables inside markup with `const` or `let`:
{#each boxes as box} {#each boxes as box}
{const area = box.width * box.height} {const area = box.width * box.height}
{const label = `${box.width} ${box.height} = ${area}`} {const label = `${box.width} × ${box.height} = ${area}`}
<p>{label}</p> <p>{label}</p>
{/each} {/each}

@ -251,6 +251,19 @@ You can give the `<select>` a default value by adding a `selected` attribute to
</select> </select>
``` ```
Since 5.57.0, if a `<select>` has a `defaultValue` and is part of a form, it will revert to that value instead of the empty string when the form is reset. Note that for the initial render the value of the binding takes precedence unless it is `null` or `undefined`.
```svelte
<form>
<select bind:value defaultValue="b">
<option>a</option>
<option>b</option>
<option>c</option>
</select>
<input type="reset" value="Reset">
</form>
```
## `<audio>` ## `<audio>`
`<audio>` elements have their own set of bindings — five two-way ones... `<audio>` elements have their own set of bindings — five two-way ones...

@ -53,6 +53,12 @@ Transitions can have parameters.
{/if} {/if}
``` ```
## Accessibility
Transitions are driven by the [Web Animations API](https://developer.mozilla.org/en-US/docs/Web/API/Web_Animations_API) rather than by CSS. A global `@media (prefers-reduced-motion: reduce)` rule that zeroes `transition-duration` and `animation-duration` therefore has no effect on them.
Use [`prefersReducedMotion`](svelte-motion#prefersReducedMotion) to adjust (or completely disable) the transition accordingly for devices who request reduced motion.
## Custom transition functions ## Custom transition functions
```js ```js

@ -109,7 +109,7 @@ By default, error boundaries have no effect on the server — if an error occurs
Since 5.51 you can control this behaviour for boundaries with a `failed` snippet, by calling [`render(...)`](imperative-component-api#render) with a `transformError` function. Since 5.51 you can control this behaviour for boundaries with a `failed` snippet, by calling [`render(...)`](imperative-component-api#render) with a `transformError` function.
> [!NOTE] If you're using Svelte via a framework such as SvelteKit, you most likely don't have direct access to the `render(...)` call — the framework must configure `transformError` on your behalf. SvelteKit will add support for this in the near future, via the [`handleError`](../kit/hooks#Shared-hooks-handleError) hook. > [!NOTE] If you're using Svelte via a framework such as SvelteKit, you most likely don't have direct access to the `render(...)` call — the framework must configure `transformError` on your behalf. SvelteKit will add support for this in the near future, via the [`handleError`](../kit/hooks#handleError) hook.
The `transformError` function must return a JSON-stringifiable object which will be used to render the `failed` snippet. This object will be serialized and used to hydrate the snippet in the browser: The `transformError` function must return a JSON-stringifiable object which will be used to render the `failed` snippet. This object will be serialized and used to hydrate the snippet in the browser:

@ -165,9 +165,9 @@ Svelte will warn you if you get it wrong.
Similarly, to pass primitive values through context, use functions as described in [Passing state into functions]($state#Passing-state-into-functions). Similarly, to pass primitive values through context, use functions as described in [Passing state into functions]($state#Passing-state-into-functions).
## Component testing ## Mounting components with context
When writing [component tests](testing#Unit-and-component-tests-with-Vitest-Component-testing), it can be useful to create a wrapper component that sets the context in order to check the behaviour of a component that uses it. As of version 5.49, you can do this sort of thing: To mount a component with specific context, create a wrapper component that sets the context before rendering the component. This is useful for [component tests](testing#Unit-and-component-tests-with-Vitest-Component-testing), or any other scenario that needs to provide context through `mount`. As of version 5.49, you can do this sort of thing:
```js ```js
import { mount, unmount } from 'svelte'; import { mount, unmount } from 'svelte';
@ -193,6 +193,8 @@ test('MyComponent', () => {
This approach also works with [`hydrate`](imperative-component-api#hydrate) and [`render`](imperative-component-api#render). This approach also works with [`hydrate`](imperative-component-api#hydrate) and [`render`](imperative-component-api#render).
The context set by the wrapper only applies to that mounted component tree. Each call to `mount`, `hydrate` or `render` creates a separate wrapper instance, so the context does not leak into other mounted components.
## Replacing global state ## Replacing global state
When you have state shared by many different components, you might be tempted to put it in its own module and just import it wherever it's needed: When you have state shared by many different components, you might be tempted to put it in its own module and just import it wherever it's needed:

@ -102,7 +102,7 @@ To implement a chat window that autoscrolls to the bottom when new messages appe
In Svelte 4, we do this with `beforeUpdate`, but this is a flawed approach — it fires before _every_ update, whether it's relevant or not. In the example below, we need to introduce checks like `updatingMessages` to make sure we don't mess with the scroll position when someone toggles dark mode. In Svelte 4, we do this with `beforeUpdate`, but this is a flawed approach — it fires before _every_ update, whether it's relevant or not. In the example below, we need to introduce checks like `updatingMessages` to make sure we don't mess with the scroll position when someone toggles dark mode.
With runes, we can use `$effect.pre`, which behaves the same as `$effect` but runs before the DOM is updated. As long as we explicitly reference `messages` inside the effect body, it will run whenever `messages` changes, but _not_ when `theme` changes. With runes, we can use `$effect.pre`, which behaves the same as `$effect` but runs before DOM updates scheduled after it (see [$effect.pre]($effect#$effect.pre) for the exact ordering). As long as we explicitly reference `messages` inside the effect body, it will run whenever `messages` changes, but _not_ when `theme` changes.
`beforeUpdate`, and its equally troublesome counterpart `afterUpdate`, are therefore deprecated in Svelte 5. `beforeUpdate`, and its equally troublesome counterpart `afterUpdate`, are therefore deprecated in Svelte 5.

@ -170,6 +170,12 @@ If you don't give `$state` an initial value, part of its types will be `undefine
let count: number = $state(); let count: number = $state();
``` ```
You can pass the type directly as a generic parameter to safely handle this. TypeScript will infer the variable as `number | undefined`.
```ts
let count = $state<number>();
```
If you know that the variable _will_ be defined before you first use it, use an `as` casting. This is especially useful in the context of classes: If you know that the variable _will_ be defined before you first use it, use an `as` casting. This is especially useful in the context of classes:
```ts ```ts

@ -99,7 +99,9 @@ However, you can use any router library. A sampling of available routers are hig
While most mobile apps are written without using JavaScript, if you'd like to leverage your existing Svelte components and knowledge of Svelte when building mobile apps, you can turn a [SvelteKit SPA](https://kit.svelte.dev/docs/single-page-apps) into a mobile app with [Tauri](https://v2.tauri.app/start/frontend/sveltekit/) or [Capacitor](https://capacitorjs.com/solution/svelte). Mobile features like the camera, geolocation, and push notifications are available via plugins for both platforms. While most mobile apps are written without using JavaScript, if you'd like to leverage your existing Svelte components and knowledge of Svelte when building mobile apps, you can turn a [SvelteKit SPA](https://kit.svelte.dev/docs/single-page-apps) into a mobile app with [Tauri](https://v2.tauri.app/start/frontend/sveltekit/) or [Capacitor](https://capacitorjs.com/solution/svelte). Mobile features like the camera, geolocation, and push notifications are available via plugins for both platforms.
Some work has been completed towards [custom renderer support in Svelte 5](https://github.com/sveltejs/svelte/issues/15470), but this feature is not yet available. The custom rendering API would support additional mobile frameworks like Lynx JS and Svelte Native. Svelte Native was an option available for Svelte 4, but Svelte 5 does not currently support it. Svelte Native lets you write NativeScript apps using Svelte components that contain [NativeScript UI components](https://docs.nativescript.org/ui/) rather than DOM elements, which may be familiar for users coming from React Native. You can also write apps in Svelte that compiles to native components by using [Symbiote Native](https://docs.symbiote-native.dev/), which leverages the infrastructure provided by React Native.
Work has been completed towards [custom renderer support in Svelte 5](https://github.com/sveltejs/svelte/issues/15470), but this feature is not yet merged. The custom rendering API will allow support in additional mobile frameworks like Lynx JS and Svelte Native. Symbiote Native will also adopt this API. Svelte Native was an option available for Svelte 4, but Svelte 5 does not currently support it. Svelte Native lets you write NativeScript apps using Svelte components that contain [NativeScript UI components](https://docs.nativescript.org/ui/) rather than DOM elements.
## Can I tell Svelte not to remove my unused styles? ## Can I tell Svelte not to remove my unused styles?

@ -345,27 +345,6 @@ Reactive `$state(...)` proxies and the values they proxy have different identiti
To resolve this, ensure you're comparing values where both values were created with `$state(...)`, or neither were. Note that `$state.raw(...)` will _not_ create a state proxy. To resolve this, ensure you're comparing values where both values were created with `$state(...)`, or neither were. Note that `$state.raw(...)` will _not_ create a state proxy.
### state_proxy_unmount
```
Tried to unmount a state proxy, rather than a component
```
`unmount` was called with a state proxy:
```js
import { mount, unmount } from 'svelte';
import Component from './Component.svelte';
let target = document.body;
// ---cut---
let component = $state(mount(Component, { target }));
// later...
unmount(component);
```
Avoid using `$state` here. If `component` _does_ need to be reactive for some reason, use `$state.raw` instead.
### svelte_boundary_reset_noop ### svelte_boundary_reset_noop
``` ```

@ -821,6 +821,18 @@ Cannot use `%rune%` rune in non-runes mode
Cannot use rune without parentheses Cannot use rune without parentheses
``` ```
Runes are keywords rather than values — they can't be assigned to a variable or passed to a function, only called. Referencing one without parentheses is therefore an error...
```js
let count = $state;
```
...whether it's a rune like `$state` or one reached through a property, like `$derived.by`. Add the parentheses, along with any arguments the rune expects:
```js
let count = $state(0);
```
### rune_removed ### rune_removed
``` ```

@ -360,7 +360,7 @@ Enforce that heading elements (`h1`, `h2`, etc.) and anchors have content and th
'%event%' event must be accompanied by '%accompanied_by%' event '%event%' event must be accompanied by '%accompanied_by%' event
``` ```
Enforce that `onmouseover` and `onmouseout` are accompanied by `onfocus` and `onblur`, respectively. This helps to ensure that any functionality triggered by these mouse events is also accessible to keyboard users. Enforce that `onmouseover` and `onmouseout` are accompanied by `onfocus` (or `onfocusin`) and `onblur` (or `onfocusout`), respectively. This helps to ensure that any functionality triggered by these mouse events is also accessible to keyboard users.
```svelte ```svelte
<!-- A11y: onmouseover must be accompanied by onfocus --> <!-- A11y: onmouseover must be accompanied by onfocus -->

@ -75,10 +75,10 @@ Certain lifecycle methods can only be used during component initialisation. To f
### missing_context ### missing_context
``` ```
Context was not set in a parent component Context was not set in the current component or any of its ancestors
``` ```
The [`createContext()`](svelte#createContext) utility returns a `[get, set]` pair of functions. `get` will throw an error if `set` was not used to set the context in a parent component. The [`createContext()`](svelte#createContext) utility returns a `[get, set]` pair of functions. `get` will throw an error if `set` was not used to set the context in the current component or any of its ancestors.
### snippet_without_render_tag ### snippet_without_render_tag

@ -2,4 +2,6 @@
title: svelte/easing title: svelte/easing
--- ---
This module provides a set of functions that allow you to manipulate time values in different ways. Its particularly useful for animations when combined with the `motion` module.
> MODULE: svelte/easing > MODULE: svelte/easing

@ -5,7 +5,7 @@
"private": true, "private": true,
"type": "module", "type": "module",
"license": "MIT", "license": "MIT",
"packageManager": "pnpm@10.4.0", "packageManager": "pnpm@10.33.4+sha512.1c67b3b359b2d408119ba1ed289f34b8fc3c6873412bec6fd264fbdc82489e510fcbecb9ce9d22dae7f3b76269d8441046014bdca53b9979cd7a561ad631b800",
"engines": { "engines": {
"pnpm": ">=9.0.0" "pnpm": ">=9.0.0"
}, },
@ -26,10 +26,10 @@
"bench:debug": "NODE_ENV=production node --allow-natives-syntax --inspect-brk ./benchmarking/run.js" "bench:debug": "NODE_ENV=production node --allow-natives-syntax --inspect-brk ./benchmarking/run.js"
}, },
"devDependencies": { "devDependencies": {
"@changesets/changelog-github": "1.0.0-next.6",
"@changesets/cli": "^2.29.8", "@changesets/cli": "^2.29.8",
"@eslint/js": "^10.0.0", "@eslint/js": "^10.0.0",
"@sveltejs/eslint-config": "^9.0.0", "@sveltejs/eslint-config": "^9.0.0",
"@svitejs/changesets-changelog-github-compact": "^1.1.0",
"@types/node": "^20.11.5", "@types/node": "^20.11.5",
"@types/picomatch": "^4.0.2", "@types/picomatch": "^4.0.2",
"@vitest/coverage-v8": "^4.1.7", "@vitest/coverage-v8": "^4.1.7",

@ -1,5 +1,79 @@
# svelte # svelte
## 5.56.10
### Patch Changes
- fix: preserve CSS escape sequences when printing selectors ([#18667](https://github.com/sveltejs/svelte/pull/18667))
- fix: parse `:nth-child(2n of.foo)` where `of` is not followed by whitespace ([#18611](https://github.com/sveltejs/svelte/pull/18611))
- fix: transform expressions inside labeled statements during server compilation ([#18617](https://github.com/sveltejs/svelte/pull/18617))
- docs: clarify that context lookup includes the current component and all ancestors ([#18581](https://github.com/sveltejs/svelte/pull/18581))
- fix: apply CSS custom properties with falsy values on components ([#18634](https://github.com/sveltejs/svelte/pull/18634))
- fix: correctly print `{#await ... catch x}` et al ([#18645](https://github.com/sveltejs/svelte/pull/18645))
- fix: ignore comments of Program node during migration script ([#18656](https://github.com/sveltejs/svelte/pull/18656))
- fix: reliably resolve append_style to its correct root ([#18614](https://github.com/sveltejs/svelte/pull/18614))
- fix: clean up removed capture event handlers from spread attributes ([#18618](https://github.com/sveltejs/svelte/pull/18618))
- fix: don't corrupt renderer type during SSR's legacy `bind:` retry loop ([#18616](https://github.com/sveltejs/svelte/pull/18616))
- fix: treat concise arrow function bodies as implicit returns when calculating blockers ([#18613](https://github.com/sveltejs/svelte/pull/18613))
- fix: give effect teardowns the value from before the first write in a flush ([#18620](https://github.com/sveltejs/svelte/pull/18620))
- fix: avoid double-calling a derived reference when destructuring `$derived` of another `$derived` during server-side rendering ([#18668](https://github.com/sveltejs/svelte/pull/18668))
- fix: preserve namespaces in CSS type selectors ([#18678](https://github.com/sveltejs/svelte/pull/18678))
- fix: increment private state fields through a non-`this` receiver ([#18622](https://github.com/sveltejs/svelte/pull/18622))
- chore: deduplicate client and server context helpers ([#18580](https://github.com/sveltejs/svelte/pull/18580))
- fix: release `last_propagated_event` after event propagation settles so it no longer retains the last event's target subtree ([#18569](https://github.com/sveltejs/svelte/pull/18569))
- fix: allow custom elements to receive async values as props ([#18661](https://github.com/sveltejs/svelte/pull/18661))
- fix: strip comments from inline `style` values in linear time ([#18553](https://github.com/sveltejs/svelte/pull/18553))
- fix: prevent declaration comments from breaking server derived references ([#18641](https://github.com/sveltejs/svelte/pull/18641))
- perf: make async blocker analysis scale linearly with the number of top-level references ([#18549](https://github.com/sveltejs/svelte/pull/18549))
- fix: preserve short-circuiting for logical assignments to private state fields ([#18594](https://github.com/sveltejs/svelte/pull/18594))
## 5.56.9
### Patch Changes
- fix: skip controlled each fast path while another batch is pending ([#18625](https://github.com/sveltejs/svelte/pull/18625))
- fix: better whitespace handling inside printer ([#18638](https://github.com/sveltejs/svelte/pull/18638))
- fix: don't duplicate comments in attributes ([#18636](https://github.com/sveltejs/svelte/pull/18636))
- fix: preserve CSS comments in the AST printer ([#18637](https://github.com/sveltejs/svelte/pull/18637))
## 5.56.8
### Patch Changes
- fix: call `onerror` and provide a working `reset` when hydrating a failed boundary ([#18556](https://github.com/sveltejs/svelte/pull/18556))
- fix: preserve select selection when spread attributes omit value ([#18561](https://github.com/sveltejs/svelte/pull/18561))
## 5.56.7
### Patch Changes
- chore: provide `indent` option for `print` ([#18474](https://github.com/sveltejs/svelte/pull/18474))
## 5.56.6 ## 5.56.6
### Patch Changes ### Patch Changes

@ -1352,6 +1352,9 @@ export interface HTMLSelectAttributes extends HTMLAttributes<HTMLSelectElement>
required?: boolean | undefined | null; required?: boolean | undefined | null;
size?: number | undefined | null; size?: number | undefined | null;
value?: any; value?: any;
// needs both casing variants because language tools does lowercase names of non-shorthand attributes
defaultValue?: any;
defaultvalue?: any;
'on:change'?: ChangeEventHandler<HTMLSelectElement> | undefined | null; 'on:change'?: ChangeEventHandler<HTMLSelectElement> | undefined | null;
onchange?: ChangeEventHandler<HTMLSelectElement> | undefined | null; onchange?: ChangeEventHandler<HTMLSelectElement> | undefined | null;

@ -299,25 +299,6 @@ To silence the warning, ensure that `value`:
To resolve this, ensure you're comparing values where both values were created with `$state(...)`, or neither were. Note that `$state.raw(...)` will _not_ create a state proxy. To resolve this, ensure you're comparing values where both values were created with `$state(...)`, or neither were. Note that `$state.raw(...)` will _not_ create a state proxy.
## state_proxy_unmount
> Tried to unmount a state proxy, rather than a component
`unmount` was called with a state proxy:
```js
import { mount, unmount } from 'svelte';
import Component from './Component.svelte';
let target = document.body;
// ---cut---
let component = $state(mount(Component, { target }));
// later...
unmount(component);
```
Avoid using `$state` here. If `component` _does_ need to be reactive for some reason, use `$state.raw` instead.
## svelte_boundary_reset_noop ## svelte_boundary_reset_noop
> A `<svelte:boundary>` `reset` function only resets the boundary the first time it is called > A `<svelte:boundary>` `reset` function only resets the boundary the first time it is called

@ -186,6 +186,18 @@ This turned out to be buggy and unpredictable, particularly when working with de
> Cannot use rune without parentheses > Cannot use rune without parentheses
Runes are keywords rather than values — they can't be assigned to a variable or passed to a function, only called. Referencing one without parentheses is therefore an error...
```js
let count = $state;
```
...whether it's a rune like `$state` or one reached through a property, like `$derived.by`. Add the parentheses, along with any arguments the rune expects:
```js
let count = $state(0);
```
## rune_removed ## rune_removed
> The `%name%` rune has been removed > The `%name%` rune has been removed

@ -300,7 +300,7 @@ Enforce that heading elements (`h1`, `h2`, etc.) and anchors have content and th
> '%event%' event must be accompanied by '%accompanied_by%' event > '%event%' event must be accompanied by '%accompanied_by%' event
Enforce that `onmouseover` and `onmouseout` are accompanied by `onfocus` and `onblur`, respectively. This helps to ensure that any functionality triggered by these mouse events is also accessible to keyboard users. Enforce that `onmouseover` and `onmouseout` are accompanied by `onfocus` (or `onfocusin`) and `onblur` (or `onfocusout`), respectively. This helps to ensure that any functionality triggered by these mouse events is also accessible to keyboard users.
```svelte ```svelte
<!-- A11y: onmouseover must be accompanied by onfocus --> <!-- A11y: onmouseover must be accompanied by onfocus -->

@ -62,9 +62,9 @@ Certain lifecycle methods can only be used during component initialisation. To f
## missing_context ## missing_context
> Context was not set in a parent component > Context was not set in the current component or any of its ancestors
The [`createContext()`](svelte#createContext) utility returns a `[get, set]` pair of functions. `get` will throw an error if `set` was not used to set the context in a parent component. The [`createContext()`](svelte#createContext) utility returns a `[get, set]` pair of functions. `get` will throw an error if `set` was not used to set the context in the current component or any of its ancestors.
## snippet_without_render_tag ## snippet_without_render_tag

@ -2,7 +2,7 @@
"name": "svelte", "name": "svelte",
"description": "Cybernetically enhanced web apps", "description": "Cybernetically enhanced web apps",
"license": "MIT", "license": "MIT",
"version": "5.56.6", "version": "5.56.10",
"type": "module", "type": "module",
"types": "./types/index.d.ts", "types": "./types/index.d.ts",
"engines": { "engines": {

@ -261,7 +261,7 @@ declare function $effect(fn: () => void | (() => void)): void;
declare namespace $effect { declare namespace $effect {
/** /**
* Runs code right before a component is mounted to the DOM, and then whenever its dependencies change, i.e. `$state` or `$derived` values. * Runs code right before a component is mounted to the DOM, and then whenever its dependencies change, i.e. `$state` or `$derived` values.
* The timing of the execution is right before the DOM is updated. * The timing of the execution is right before the DOM that comes after it is updated; parent DOM may already have been updated by the time it runs.
* *
* Example: * Example:
* ```ts * ```ts

@ -141,7 +141,8 @@ export function parseCss(source) {
type: 'StyleSheetFile', type: 'StyleSheetFile',
start: 0, start: 0,
end: source.length, end: source.length,
children children,
comments: parser.css_comments
}; };
} }

@ -1590,7 +1590,12 @@ function migrate_slot_usage(node, path, state) {
*/ */
function extract_type_and_comment(declarator, state, path) { function extract_type_and_comment(declarator, state, path) {
const str = state.str; const str = state.str;
const parent = path.at(-1); let parent = path.at(-1);
if (parent?.type === 'Program') {
// We don't want comments from the program node
parent = undefined;
}
// Try to find jsdoc above the declaration // Try to find jsdoc above the declaration
let comment_node = /** @type {Node} */ (parent)?.leadingComments?.at(-1); let comment_node = /** @type {Node} */ (parent)?.leadingComments?.at(-1);

@ -59,7 +59,7 @@ export function parse(source, comments, typescript, is_script) {
return /** @type {Program} */ (ast); return /** @type {Program} */ (ast);
} catch (err) { } catch (err) {
// TODO the `return` in necessary for TS<7 due to a bug; otherwise // TODO the `return` is necessary for TS<7 due to a bug; otherwise
// the `finally` block is regarded as unreachable // the `finally` block is regarded as unreachable
return handle_parse_error(err); return handle_parse_error(err);
} finally { } finally {

@ -50,6 +50,9 @@ export class Parser {
/** */ /** */
index = 0; index = 0;
/** @type {AST.CSS.CSSComment[]} */
css_comments = [];
/** /**
* Creates a minimal parser instance for CSS-only parsing. * Creates a minimal parser instance for CSS-only parsing.
* Skips Svelte component parsing setup. * Skips Svelte component parsing setup.
@ -61,6 +64,7 @@ export class Parser {
parser.template = source; parser.template = source;
parser.index = 0; parser.index = 0;
parser.loose = false; parser.loose = false;
parser.css_comments = [];
return parser; return parser;
} }

@ -7,8 +7,11 @@ const REGEX_CLOSING_BRACKET = /[\s\]]/;
const REGEX_ATTRIBUTE_FLAGS = /[a-zA-Z]+/y; // only `i` and `s` are valid today, but make it future-proof const REGEX_ATTRIBUTE_FLAGS = /[a-zA-Z]+/y; // only `i` and `s` are valid today, but make it future-proof
const REGEX_COMBINATOR = /(\+|~|>|\|\|)/y; const REGEX_COMBINATOR = /(\+|~|>|\|\|)/y;
const REGEX_PERCENTAGE = /\d+(\.\d+)?%/y; const REGEX_PERCENTAGE = /\d+(\.\d+)?%/y;
// `of` must be preceded by whitespace, otherwise it would be part of the `<an+b>` token
// (`2nof` is a single dimension token). It does not need to be followed by whitespace,
// because a `.`, `#`, `[`, `*`, `:` or `&` already ends the `of` identifier — minifiers rely on that
const REGEX_NTH_OF = const REGEX_NTH_OF =
/(even|odd|\+?(\d+|\d*n(\s*[+-]\s*\d+)?)|-\d*n(\s*\+\s*\d+))((?=\s*[,)])|\s+of\s+)/y; /(even|odd|\+?(\d+|\d*n(\s*[+-]\s*\d+)?)|-\d*n(\s*\+\s*\d+))((?=\s*[,)])|\s+of(\s+|(?=[.#[*:&])))/y;
const REGEX_WHITESPACE_OR_COLON = /[\s:]/; const REGEX_WHITESPACE_OR_COLON = /[\s:]/;
const REGEX_LEADING_HYPHEN_OR_DIGIT = /-?\d/y; const REGEX_LEADING_HYPHEN_OR_DIGIT = /-?\d/y;
const REGEX_VALID_IDENTIFIER_CHAR = /[a-zA-Z0-9_-]/; const REGEX_VALID_IDENTIFIER_CHAR = /[a-zA-Z0-9_-]/;
@ -24,6 +27,7 @@ const REGEX_HTML_COMMENT_CLOSE = /-->/;
*/ */
export default function read_style(parser, start, attributes) { export default function read_style(parser, start, attributes) {
const content_start = parser.index; const content_start = parser.index;
parser.css_comments = [];
const children = read_body(parser, (p) => p.match('</style') || p.index >= p.template.length); const children = read_body(parser, (p) => p.match('</style') || p.index >= p.template.length);
const content_end = parser.index; const content_end = parser.index;
@ -36,6 +40,7 @@ export default function read_style(parser, start, attributes) {
end: parser.index, end: parser.index,
attributes, attributes,
children, children,
comments: parser.css_comments,
content: { content: {
start: content_start, start: content_start,
end: content_end, end: content_end,
@ -202,15 +207,18 @@ function read_selector(parser, inside_pseudo_class = false) {
}); });
} else if (parser.eat('*')) { } else if (parser.eat('*')) {
let name = '*'; let name = '*';
/** @type {string | undefined} */
let namespace;
if (parser.eat('|')) { if (parser.eat('|')) {
// * is the namespace (which we ignore) namespace = name;
name = read_identifier(parser); name = parser.eat('*') ? '*' : read_identifier(parser);
} }
relative_selector.selectors.push({ relative_selector.selectors.push({
type: 'TypeSelector', type: 'TypeSelector',
name, name,
...(namespace !== undefined && { namespace }),
start, start,
end: parser.index end: parser.index
}); });
@ -229,18 +237,22 @@ function read_selector(parser, inside_pseudo_class = false) {
end: parser.index end: parser.index
}); });
} else if (parser.eat('::')) { } else if (parser.eat('::')) {
const name = read_identifier(parser);
/** @type {AST.CSS.SelectorList | null} */
let args = null;
if (parser.eat('(')) {
args = read_selector_list(parser, true);
parser.eat(')', true);
}
relative_selector.selectors.push({ relative_selector.selectors.push({
type: 'PseudoElementSelector', type: 'PseudoElementSelector',
name: read_identifier(parser), name,
start, start,
end: parser.index end: parser.index,
...(args && { args })
}); });
// We read the inner selectors of a pseudo element to ensure it parses correctly,
// but we don't do anything with the result.
if (parser.eat('(')) {
read_selector_list(parser, true);
parser.eat(')', true);
}
} else if (parser.eat(':')) { } else if (parser.eat(':')) {
const name = read_identifier(parser); const name = read_identifier(parser);
@ -308,22 +320,25 @@ function read_selector(parser, inside_pseudo_class = false) {
}); });
} else if (!parser.match_regex(REGEX_COMBINATOR)) { } else if (!parser.match_regex(REGEX_COMBINATOR)) {
let name = read_identifier(parser); let name = read_identifier(parser);
/** @type {string | undefined} */
let namespace;
if (parser.eat('|')) { if (parser.eat('|')) {
// we ignore the namespace when trying to find matching element classes namespace = name;
name = read_identifier(parser); name = parser.eat('*') ? '*' : read_identifier(parser);
} }
relative_selector.selectors.push({ relative_selector.selectors.push({
type: 'TypeSelector', type: 'TypeSelector',
name, name,
...(namespace !== undefined && { namespace }),
start, start,
end: parser.index end: parser.index
}); });
} }
const index = parser.index; const index = parser.index;
allow_comment_or_whitespace(parser); allow_comment_or_whitespace(parser, false);
if (parser.match(',') || (inside_pseudo_class ? parser.match(')') : parser.match('{'))) { if (parser.match(',') || (inside_pseudo_class ? parser.match(')') : parser.match('{'))) {
// rewind, so we know whether to continue building the selector list // rewind, so we know whether to continue building the selector list
@ -449,7 +464,7 @@ function read_block_item(parser) {
// read ahead to understand whether we're dealing with a declaration or a nested rule. // read ahead to understand whether we're dealing with a declaration or a nested rule.
// this involves some duplicated work, but avoids a try-catch that would disguise errors // this involves some duplicated work, but avoids a try-catch that would disguise errors
const start = parser.index; const start = parser.index;
read_value(parser); read_value(parser, false);
const char = parser.template[parser.index]; const char = parser.template[parser.index];
parser.index = start; parser.index = start;
@ -492,10 +507,13 @@ function read_declaration(parser) {
/** /**
* @param {Parser} parser * @param {Parser} parser
* @param {boolean} [capture_comments]
* @returns {string} * @returns {string}
*/ */
function read_value(parser) { function read_value(parser, capture_comments = true) {
let value = ''; let value = '';
/** @type {AST.CSS.CSSComment[]} */
const value_comments = [];
let escaped = false; let escaped = false;
let in_url = false; let in_url = false;
@ -523,6 +541,13 @@ function read_value(parser) {
} else if (char === '(' && value.slice(-3) === 'url') { } else if (char === '(' && value.slice(-3) === 'url') {
in_url = true; in_url = true;
} else if ((char === ';' || char === '{' || char === '}') && !in_url && !quote_mark) { } else if ((char === ';' || char === '{' || char === '}') && !in_url && !quote_mark) {
const leading_whitespace = value.length - value.trimStart().length;
for (const comment of value_comments) {
comment.position = Math.max(
0,
/** @type {number} */ (comment.position) - leading_whitespace
);
}
return value.trim(); return value.trim();
} else if ( } else if (
char === '/' && char === '/' &&
@ -530,13 +555,11 @@ function read_value(parser) {
!quote_mark && !quote_mark &&
parser.template[parser.index + 1] === '*' parser.template[parser.index + 1] === '*'
) { ) {
parser.index += 2; const comment = read_comment(parser);
while (parser.index < parser.template.length) { if (capture_comments) {
if (parser.template[parser.index] === '*' && parser.template[parser.index + 1] === '/') { comment.position = value.length;
parser.index += 2; parser.css_comments.push(comment);
break; value_comments.push(comment);
}
parser.index++;
} }
continue; continue;
} }
@ -600,7 +623,8 @@ function read_identifier(parser) {
if (char === '\\') { if (char === '\\') {
const sequence = parser.match_regex(REGEX_UNICODE_SEQUENCE); const sequence = parser.match_regex(REGEX_UNICODE_SEQUENCE);
if (sequence) { if (sequence) {
identifier += String.fromCodePoint(parseInt(sequence.slice(1), 16)); const character = String.fromCodePoint(parseInt(sequence.slice(1), 16));
identifier += character === '\\' ? '\\\\' : character;
parser.index += sequence.length; parser.index += sequence.length;
} else { } else {
identifier += '\\' + parser.template[parser.index + 1]; identifier += '\\' + parser.template[parser.index + 1];
@ -624,13 +648,16 @@ function read_identifier(parser) {
return identifier; return identifier;
} }
/** @param {Parser} parser */ /**
function allow_comment_or_whitespace(parser) { * @param {Parser} parser
* @param {boolean} [capture_comments]
*/
function allow_comment_or_whitespace(parser, capture_comments = true) {
parser.allow_whitespace(); parser.allow_whitespace();
while (parser.match('/*') || parser.match('<!--')) { while (parser.match('/*') || parser.match('<!--')) {
if (parser.eat('/*')) { if (parser.match('/*')) {
parser.read_until(REGEX_COMMENT_CLOSE); const comment = read_comment(parser);
parser.eat('*/', true); if (capture_comments) parser.css_comments.push(comment);
} }
if (parser.eat('<!--')) { if (parser.eat('<!--')) {
@ -642,6 +669,25 @@ function allow_comment_or_whitespace(parser) {
} }
} }
/**
* @param {Parser} parser
* @returns {AST.CSS.CSSComment}
*/
function read_comment(parser) {
const start = parser.index;
parser.eat('/*', true);
const value = parser.read_until(REGEX_COMMENT_CLOSE);
parser.eat('*/', true);
const end = parser.index;
return {
type: 'CSSComment',
value,
start,
end
};
}
/** /**
* Parse standalone CSS content (not wrapped in `<style>`). * Parse standalone CSS content (not wrapped in `<style>`).
* @param {Parser} parser * @param {Parser} parser

@ -20,7 +20,7 @@ function reg_exp_entity(entity_name, is_attribute_value) {
/** @param {boolean} is_attribute_value */ /** @param {boolean} is_attribute_value */
function get_entity_pattern(is_attribute_value) { function get_entity_pattern(is_attribute_value) {
const reg_exp_num = '#(?:x[a-fA-F\\d]+|\\d+)(?:;)?'; const reg_exp_num = '#(?:[xX][a-fA-F\\d]+|\\d+)(?:;)?';
const reg_exp_entities = Object.keys(entities).map( const reg_exp_entities = Object.keys(entities).map(
/** @param {any} entity_name */ (entity_name) => reg_exp_entity(entity_name, is_attribute_value) /** @param {any} entity_name */ (entity_name) => reg_exp_entity(entity_name, is_attribute_value)
); );
@ -50,7 +50,7 @@ export function decode_character_references(html, is_attribute_value) {
// Handle named entities // Handle named entities
if (entity[0] !== '#') { if (entity[0] !== '#') {
code = entities[entity]; code = entities[entity];
} else if (entity[1] === 'x') { } else if (entity[1] === 'x' || entity[1] === 'X') {
code = parseInt(entity.substring(2), 16); code = parseInt(entity.substring(2), 16);
} else { } else {
code = parseInt(entity.substring(1), 10); code = parseInt(entity.substring(1), 10);
@ -60,7 +60,7 @@ export function decode_character_references(html, is_attribute_value) {
return match; return match;
} }
return String.fromCodePoint(validate_code(code)); return String.fromCodePoint(validate_code(code, is_attribute_value));
} }
); );
} }
@ -75,10 +75,15 @@ const NUL = 0;
// Also see: https://en.wikipedia.org/wiki/Plane_(Unicode) // Also see: https://en.wikipedia.org/wiki/Plane_(Unicode)
// Also see: https://html.spec.whatwg.org/multipage/parsing.html#preprocessing-the-input-stream // Also see: https://html.spec.whatwg.org/multipage/parsing.html#preprocessing-the-input-stream
/** @param {number} code */ /**
function validate_code(code) { * @param {number} code
// line feed becomes generic whitespace * @param {boolean} is_attribute_value
if (code === 10) { */
function validate_code(code, is_attribute_value) {
// line feed becomes generic whitespace, since it is collapsed along with the
// surrounding whitespace anyway. In an attribute value it is significant, so it
// is left alone there
if (code === 10 && !is_attribute_value) {
return 32; return 32;
} }

@ -312,6 +312,9 @@ const css_visitors = {
} }
context.next(); context.next();
},
PseudoElementSelector() {
// Don't analyse these
} }
}; };

@ -122,6 +122,13 @@ const any_selector = {
*/ */
const seen = new Set(); const seen = new Set();
/**
* @param {Compiler.AST.RegularElement | Compiler.AST.SvelteElement | Compiler.AST.RenderTag | Compiler.AST.Component | Compiler.AST.SvelteComponent | Compiler.AST.SvelteSelf} node
*/
function is_inside_svelte_head(node) {
return node.metadata.path.some((ancestor) => ancestor.type === 'SvelteHead');
}
/** /**
* *
* @param {Compiler.AST.CSS.StyleSheet} stylesheet * @param {Compiler.AST.CSS.StyleSheet} stylesheet
@ -143,6 +150,9 @@ export function prune(stylesheet, elements) {
seen.clear(); seen.clear();
if ( if (
// Elements rendered through <svelte:head> are not style-scopable.
// Prevent css hash injection (class="s-...") on tags like <meta>, <link>, <script>.
!is_inside_svelte_head(element) &&
apply_selector( apply_selector(
selectors, selectors,
/** @type {Compiler.AST.CSS.Rule} */ (node.metadata.rule), /** @type {Compiler.AST.CSS.Rule} */ (node.metadata.rule),

@ -23,6 +23,9 @@ const visitors = {
context.next(); context.next();
} }
}, },
PseudoElementSelector() {
// Don't analyse these
},
ComplexSelector(node, context) { ComplexSelector(node, context) {
if ( if (
!node.metadata.used && !node.metadata.used &&

@ -285,7 +285,7 @@ export function analyze_module(source, options) {
runes: true, runes: true,
immutable: true, immutable: true,
tracing: false, tracing: false,
async_deriveds: new Set(), async_deriveds: new Map(),
comments, comments,
classes: new Map(), classes: new Map(),
pickled_awaits: new Set() pickled_awaits: new Set()
@ -557,7 +557,7 @@ export function analyze_component(root, source, options) {
source, source,
snippet_renderers: new Map(), snippet_renderers: new Map(),
snippets: new Set(), snippets: new Set(),
async_deriveds: new Set(), async_deriveds: new Map(),
pickled_awaits: new Set(), pickled_awaits: new Set(),
instance_body: { instance_body: {
sync: [], sync: [],
@ -832,6 +832,11 @@ export function analyze_component(root, source, options) {
} else { } else {
e.export_undefined(specifier, name); e.export_undefined(specifier, name);
} }
} else if (binding.initial?.type === 'SnippetBlock') {
// If a snippet is exported, a consumer could only import this named export and not the default export (the component).
// In this case we need to set hasGlobal of our output to true so that e.g. vite-plugin-svelte does not tell Vite to
// tree-shake the CSS if the default export is not used.
analysis.css.has_global = true;
} }
} }
} }
@ -958,7 +963,7 @@ function calculate_blockers(instance, analysis) {
* @param {Set<Binding>} touched * @param {Set<Binding>} touched
* @param {Set<ESTree.Node>} seen * @param {Set<ESTree.Node>} seen
*/ */
const touch = (expression, scope, touched, seen = new Set()) => { const touch = (expression, scope, touched, seen) => {
if (seen.has(expression)) return; if (seen.has(expression)) return;
seen.add(expression); seen.add(expression);
@ -1015,6 +1020,13 @@ function calculate_blockers(instance, analysis) {
} }
} }
// Share seen nodes across calls so transitive assignments are only visited once.
// Keep separate read/write state because the target sets can differ.
/** @type {Set<ESTree.Node>} */
const writes_seen = new Set();
/** @type {Set<ESTree.Node>} */
const reads_seen = new Set();
walk( walk(
node, node,
{ scope }, { scope },
@ -1044,13 +1056,7 @@ function calculate_blockers(instance, analysis) {
const rune = get_rune(node, context.state.scope); const rune = get_rune(node, context.state.scope);
if (rune === '$effect') return; if (rune === '$effect') return;
/** @type {Set<Binding>} */ touch(node, context.state.scope, writes, writes_seen);
const touched = new Set();
touch(node, context.state.scope, touched);
for (const b of touched) {
writes.add(b);
}
}, },
Identifier(node, context) { Identifier(node, context) {
const parent = /** @type {ESTree.Node} */ (context.path.at(-1)); const parent = /** @type {ESTree.Node} */ (context.path.at(-1));
@ -1066,7 +1072,7 @@ function calculate_blockers(instance, analysis) {
// might be called immediately, so we have to touch all references within it. Example: // might be called immediately, so we have to touch all references within it. Example:
// function foo() { return () => blocker; } foo(); // blocker is touched // function foo() { return () => blocker; } foo(); // blocker is touched
if (node.argument) { if (node.argument) {
touch(node.argument, context.state.scope, reads); touch(node.argument, context.state.scope, reads, reads_seen);
} }
}, },
// don't look inside functions until they are called // don't look inside functions until they are called
@ -1227,6 +1233,23 @@ function calculate_blockers(instance, analysis) {
flush_sync_group(); flush_sync_group();
// a store subscription must wait on whatever blocks the store itself; this must happen
// before function tracing so that functions reading `$store` inherit the blocker
for (const [name, binding] of instance.scope.declarations) {
if (binding.kind !== 'store_sub') continue;
const store_blocker = instance.scope.get(name.slice(1))?.blocker;
if (!store_blocker) continue;
if (
!binding.blocker ||
/** @type {ESTree.SimpleLiteral & { value: number }} */ (binding.blocker.property).value <
/** @type {ESTree.SimpleLiteral & { value: number }} */ (store_blocker.property).value
) {
binding.blocker = store_blocker;
}
}
for (const fn of functions) { for (const fn of functions) {
/** @type {Set<Binding>} */ /** @type {Set<Binding>} */
const reads_writes = new Set(); const reads_writes = new Set();
@ -1235,12 +1258,15 @@ function calculate_blockers(instance, analysis) {
? /** @type {ESTree.FunctionExpression | ESTree.ArrowFunctionExpression} */ (fn.init) ? /** @type {ESTree.FunctionExpression | ESTree.ArrowFunctionExpression} */ (fn.init)
: fn; : fn;
trace_references( const fn_scope = /** @type {Scope} */ (instance.scopes.get(init));
init.body,
reads_writes, if (init.body.type === 'BlockStatement') {
reads_writes, trace_references(init.body, reads_writes, reads_writes, fn_scope);
/** @type {Scope} */ (instance.scopes.get(init)) } else {
); // A concise arrow body is an implicit return, so treat it like the
// `ReturnStatement` visitor in `trace_references` would.
touch(init.body, fn_scope, reads_writes, new Set());
}
const max = [...reads_writes].reduce((max, binding) => { const max = [...reads_writes].reduce((max, binding) => {
if (binding.blocker) { if (binding.blocker) {
@ -1314,7 +1340,8 @@ function order_reactive_statements(unsorted_reactive_declarations) {
* @returns * @returns
*/ */
const add_declaration = (node, declaration) => { const add_declaration = (node, declaration) => {
if ([...reactive_declarations.values()].includes(declaration)) return; // Visited set: each ReactiveStatement is stored under exactly one LabeledStatement node
if (reactive_declarations.has(node)) return;
for (const binding of declaration.dependencies) { for (const binding of declaration.dependencies) {
if (declaration.assignments.has(binding)) continue; if (declaration.assignments.has(binding)) continue;

@ -1,6 +1,7 @@
/** @import { AwaitExpression, Expression, SpreadElement, Property } from 'estree' */ /** @import { AwaitExpression, Expression, SpreadElement, Property } from 'estree' */
/** @import { Context } from '../types' */ /** @import { Context } from '../types' */
/** @import { AST } from '#compiler' */ /** @import { AST } from '#compiler' */
/** @import { ExpressionMetadata } from '../../nodes.js' */
import * as e from '../../../errors.js'; import * as e from '../../../errors.js';
/** /**
@ -10,15 +11,20 @@ import * as e from '../../../errors.js';
export function AwaitExpression(node, context) { export function AwaitExpression(node, context) {
const tla = context.state.ast_type === 'instance' && context.state.function_depth === 1; const tla = context.state.ast_type === 'instance' && context.state.function_depth === 1;
// preserve context for awaits that precede other expressions in template or `$derived(...)`
if ( if (
is_reactive_expression( is_reactive_expression(
context.path, context.path,
context.state.derived_function_depth === context.state.function_depth context.state.derived_function_depth === context.state.function_depth
) && )
!is_last_evaluated_expression(context.path, node)
) { ) {
const expression = /** @type {ExpressionMetadata} */ (context.state.expression);
// preserve context for awaits that precede other expressions in template or `$derived(...)`,
// and for any await that follows one, so the restored context ends at the next suspension
if (expression.has_pickled_await || !is_last_evaluated_expression(context.path, node)) {
context.state.analysis.pickled_awaits.add(node); context.state.analysis.pickled_awaits.add(node);
expression.has_pickled_await = true;
}
} }
let suspend = tla; let suspend = tla;

@ -253,7 +253,7 @@ export function CallExpression(node, context) {
}); });
if (expression.has_await) { if (expression.has_await) {
context.state.analysis.async_deriveds.add(node); context.state.analysis.async_deriveds.set(node, expression);
} }
// Tell surrounding declaration tag about metadata for correct calculation of blockers etc // Tell surrounding declaration tag about metadata for correct calculation of blockers etc

@ -43,7 +43,8 @@ export function DeclarationTag(node, context) {
*/ */
export function mark_async_declaration(context, metadata, declarations) { export function mark_async_declaration(context, metadata, declarations) {
const has_await = metadata.expression.has_await; const has_await = metadata.expression.has_await;
const blockers = [...metadata.expression.dependencies] // reads inside closures must block too, like they do in template expressions
const blockers = [...metadata.expression.references]
.map((dep) => dep.blocker) .map((dep) => dep.blocker)
.filter((b) => b !== null && b.object !== context.state.async_consts?.id); .filter((b) => b !== null && b.object !== context.state.async_consts?.id);

@ -1,4 +1,4 @@
/** @import { ExportNamedDeclaration, Identifier } from 'estree' */ /** @import { ExportNamedDeclaration, Identifier, VariableDeclaration } from 'estree' */
/** @import { Context } from '../types' */ /** @import { Context } from '../types' */
import * as e from '../../../errors.js'; import * as e from '../../../errors.js';
import { extract_identifiers } from '../../../utils/ast.js'; import { extract_identifiers } from '../../../utils/ast.js';
@ -23,15 +23,6 @@ export function ExportNamedDeclaration(node, context) {
} }
if (node.declaration?.type === 'VariableDeclaration') { if (node.declaration?.type === 'VariableDeclaration') {
// in runes mode, forbid `export let`
if (
context.state.analysis.runes &&
context.state.ast_type === 'instance' &&
node.declaration.kind === 'let'
) {
e.legacy_export_invalid(node);
}
for (const declarator of node.declaration.declarations) { for (const declarator of node.declaration.declarations) {
for (const id of extract_identifiers(declarator.id)) { for (const id of extract_identifiers(declarator.id)) {
const binding = context.state.scope.get(id.name); const binding = context.state.scope.get(id.name);
@ -46,6 +37,15 @@ export function ExportNamedDeclaration(node, context) {
} }
} }
} }
// in runes mode, forbid `export let`
if (
context.state.analysis.runes &&
context.state.ast_type === 'instance' &&
node.declaration.kind === 'let'
) {
e.legacy_export_invalid(node);
}
} }
if (context.state.analysis.runes) { if (context.state.analysis.runes) {

@ -3,6 +3,7 @@
import * as e from '../../../errors.js'; import * as e from '../../../errors.js';
import { is_event_attribute } from '../../../utils/ast.js'; import { is_event_attribute } from '../../../utils/ast.js';
import { disallow_children } from './shared/special-element.js'; import { disallow_children } from './shared/special-element.js';
import { check_global_event_reference } from './shared/utils.js';
/** /**
* @param {AST.SvelteBody} node * @param {AST.SvelteBody} node
@ -11,10 +12,9 @@ import { disallow_children } from './shared/special-element.js';
export function SvelteBody(node, context) { export function SvelteBody(node, context) {
disallow_children(node); disallow_children(node);
for (const attribute of node.attributes) { for (const attribute of node.attributes) {
if ( if (attribute.type === 'Attribute' && is_event_attribute(attribute)) {
attribute.type === 'SpreadAttribute' || check_global_event_reference(attribute, context);
(attribute.type === 'Attribute' && !is_event_attribute(attribute)) } else if (attribute.type === 'SpreadAttribute' || attribute.type === 'Attribute') {
) {
e.svelte_body_illegal_attribute(attribute); e.svelte_body_illegal_attribute(attribute);
} }
} }

@ -3,6 +3,7 @@
import { disallow_children } from './shared/special-element.js'; import { disallow_children } from './shared/special-element.js';
import * as e from '../../../errors.js'; import * as e from '../../../errors.js';
import { is_event_attribute } from '../../../utils/ast.js'; import { is_event_attribute } from '../../../utils/ast.js';
import { check_global_event_reference } from './shared/utils.js';
/** /**
* @param {AST.SvelteDocument} node * @param {AST.SvelteDocument} node
@ -12,10 +13,9 @@ export function SvelteDocument(node, context) {
disallow_children(node); disallow_children(node);
for (const attribute of node.attributes) { for (const attribute of node.attributes) {
if ( if (attribute.type === 'Attribute' && is_event_attribute(attribute)) {
attribute.type === 'SpreadAttribute' || check_global_event_reference(attribute, context);
(attribute.type === 'Attribute' && !is_event_attribute(attribute)) } else if (attribute.type === 'SpreadAttribute' || attribute.type === 'Attribute') {
) {
e.illegal_element_attribute(attribute, 'svelte:document'); e.illegal_element_attribute(attribute, 'svelte:document');
} }
} }

@ -3,6 +3,7 @@
import { disallow_children } from './shared/special-element.js'; import { disallow_children } from './shared/special-element.js';
import * as e from '../../../errors.js'; import * as e from '../../../errors.js';
import { is_event_attribute } from '../../../utils/ast.js'; import { is_event_attribute } from '../../../utils/ast.js';
import { check_global_event_reference } from './shared/utils.js';
/** /**
* @param {AST.SvelteWindow} node * @param {AST.SvelteWindow} node
@ -12,10 +13,9 @@ export function SvelteWindow(node, context) {
disallow_children(node); disallow_children(node);
for (const attribute of node.attributes) { for (const attribute of node.attributes) {
if ( if (attribute.type === 'Attribute' && is_event_attribute(attribute)) {
attribute.type === 'SpreadAttribute' || check_global_event_reference(attribute, context);
(attribute.type === 'Attribute' && !is_event_attribute(attribute)) } else if (attribute.type === 'SpreadAttribute' || attribute.type === 'Attribute') {
) {
e.illegal_element_attribute(attribute, 'svelte:window'); e.illegal_element_attribute(attribute, 'svelte:window');
} }
} }

@ -373,11 +373,23 @@ export function check_element(node, context) {
} }
} }
if (!has_spread && handlers.has('mouseover') && !handlers.has('focus')) { // `focus`/`blur` don't bubble, so an element that isn't focusable itself can only be notified
// about focus changes inside it through `focusin`/`focusout` — accept those as well
if (
!has_spread &&
handlers.has('mouseover') &&
!handlers.has('focus') &&
!handlers.has('focusin')
) {
w.a11y_mouse_events_have_key_events(node, 'mouseover', 'focus'); w.a11y_mouse_events_have_key_events(node, 'mouseover', 'focus');
} }
if (!has_spread && handlers.has('mouseout') && !handlers.has('blur')) { if (
!has_spread &&
handlers.has('mouseout') &&
!handlers.has('blur') &&
!handlers.has('focusout')
) {
w.a11y_mouse_events_have_key_events(node, 'mouseout', 'blur'); w.a11y_mouse_events_have_key_events(node, 'mouseout', 'blur');
} }

@ -9,6 +9,7 @@ import {
validate_attribute_name, validate_attribute_name,
validate_slot_attribute validate_slot_attribute
} from './attribute.js'; } from './attribute.js';
import { check_global_event_reference } from './utils.js';
const EVENT_MODIFIERS = [ const EVENT_MODIFIERS = [
'preventDefault', 'preventDefault',
@ -64,14 +65,7 @@ export function validate_element(node, context) {
e.attribute_invalid_event_handler(attribute); e.attribute_invalid_event_handler(attribute);
} }
const value = get_attribute_expression(attribute); check_global_event_reference(attribute, context);
if (
value.type === 'Identifier' &&
value.name === attribute.name &&
!context.state.scope.get(value.name)
) {
w.attribute_global_event_reference(attribute, attribute.name);
}
} }
if (attribute.name === 'slot') { if (attribute.name === 'slot') {

@ -4,7 +4,11 @@
/** @import { Scope } from '../../../scope' */ /** @import { Scope } from '../../../scope' */
/** @import { NodeLike } from '../../../../errors.js' */ /** @import { NodeLike } from '../../../../errors.js' */
import * as e from '../../../../errors.js'; import * as e from '../../../../errors.js';
import { extract_identifiers, get_parent } from '../../../../utils/ast.js'; import {
extract_identifiers,
get_attribute_expression,
get_parent
} from '../../../../utils/ast.js';
import * as w from '../../../../warnings.js'; import * as w from '../../../../warnings.js';
import * as b from '#compiler/builders'; import * as b from '#compiler/builders';
import { get_rune } from '../../../scope.js'; import { get_rune } from '../../../scope.js';
@ -298,3 +302,21 @@ export function validate_export(node, scope, name) {
e.state_invalid_export(node); e.state_invalid_export(node);
} }
} }
/**
* Warns when an event attribute uses the shorthand form (`{onclick}`) but the
* referenced name isn't declared, so it silently resolves to the global handler.
* @param {AST.Attribute & { value: [AST.ExpressionTag] | AST.ExpressionTag }} attribute
* @param {Context} context
*/
export function check_global_event_reference(attribute, context) {
const value = get_attribute_expression(attribute);
if (
value.type === 'Identifier' &&
value.name === attribute.name &&
!context.state.scope.get(value.name)
) {
w.attribute_global_event_reference(attribute, attribute.name);
}
}

@ -247,11 +247,11 @@ export function client_component(analysis, options) {
} }
for (const [node] of analysis.reactive_statements) { for (const [node] of analysis.reactive_statements) {
const statement = [...state.legacy_reactive_statements].find(([n]) => n === node); const statement = state.legacy_reactive_statements.get(node);
if (statement === undefined) { if (statement === undefined) {
throw new Error('Could not find reactive statement'); throw new Error('Could not find reactive statement');
} }
instance.body.push(statement[1]); instance.body.push(statement);
} }
if (analysis.reactive_statements.size > 0) { if (analysis.reactive_statements.size > 0) {

@ -39,6 +39,12 @@ export function transform_template(state, name, flags = 0) {
const namespace = state.metadata.namespace; const namespace = state.metadata.namespace;
const tree = state.options.fragments === 'tree'; const tree = state.options.fragments === 'tree';
const { nodes } = state.template;
const is_lone_anchor = nodes.length === 1 && nodes[0].type === 'comment';
// special case - `$.comment` creates the anchor more cheaply than cloning a template
if (is_lone_anchor) return b.id('$.comment');
const expression = tree ? state.template.as_tree() : state.template.as_html(); const expression = tree ? state.template.as_tree() : state.template.as_html();
const key = const key =

@ -2,6 +2,7 @@
/** @import { Binding } from '#compiler' */ /** @import { Binding } from '#compiler' */
/** @import { ClientTransformState, ComponentClientTransformState } from './types.js' */ /** @import { ClientTransformState, ComponentClientTransformState } from './types.js' */
/** @import { Analysis } from '../../types.js' */ /** @import { Analysis } from '../../types.js' */
/** @import { ExpressionMetadata } from '../../nodes.js' */
/** @import { Scope } from '../../scope.js' */ /** @import { Scope } from '../../scope.js' */
import * as b from '#compiler/builders'; import * as b from '#compiler/builders';
import { is_simple_expression, save } from '../../../utils/ast.js'; import { is_simple_expression, save } from '../../../utils/ast.js';
@ -164,20 +165,46 @@ export function should_proxy(node, scope) {
return true; return true;
} }
/**
* An async thunk. If an `await` inside restores the reaction context via `$.save`,
* the body exits through `$.unsave` so the context cannot leak into foreign microtasks
* that run before the returned promise settles
* @param {Expression | BlockStatement} body
* @param {ExpressionMetadata} metadata
*/
export function async_thunk(body, metadata) {
if (!metadata.has_pickled_await) {
return b.arrow([], body, true);
}
const block = body.type === 'BlockStatement' ? body : b.block([b.return(body)]);
return b.arrow(
[],
b.block([
{
type: 'TryStatement',
block,
handler: null,
finalizer: b.block([b.stmt(b.call('$.unsave'))])
}
]),
true
);
}
/** /**
* Svelte legacy mode should use safe equals in most places, runes mode shouldn't * Svelte legacy mode should use safe equals in most places, runes mode shouldn't
* @param {ComponentClientTransformState} state * @param {ComponentClientTransformState} state
* @param {Expression | BlockStatement} expression * @param {Expression | BlockStatement} expression
* @param {boolean} [async] * @param {ExpressionMetadata} [metadata]
*/ */
export function create_derived(state, expression, async = false) { export function create_derived(state, expression, metadata) {
const thunk = b.thunk(expression, async); if (metadata?.has_await) {
return save(b.call('$.async_derived', async_thunk(expression, metadata)));
if (async) {
return save(b.call('$.async_derived', thunk));
} else {
return b.call(state.analysis.runes ? '$.derived' : '$.derived_safe_equal', thunk);
} }
return b.call(state.analysis.runes ? '$.derived' : '$.derived_safe_equal', b.thunk(expression));
} }
/** /**

@ -1,4 +1,4 @@
/** @import { AssignmentExpression, AssignmentOperator, Expression, Identifier, Pattern } from 'estree' */ /** @import { AssignmentExpression, AssignmentOperator, Expression, Identifier, LogicalOperator, Pattern } from 'estree' */
/** @import { AST } from '#compiler' */ /** @import { AST } from '#compiler' */
/** @import { Context } from '../types.js' */ /** @import { Context } from '../types.js' */
import * as b from '#compiler/builders'; import * as b from '#compiler/builders';
@ -79,8 +79,13 @@ function build_assignment(operator, left, right, context) {
// special case — assignment to private state field // special case — assignment to private state field
if (left.property.type === 'PrivateIdentifier') { if (left.property.type === 'PrivateIdentifier') {
const logical_operator = ['||=', '&&=', '??='].includes(operator)
? /** @type {LogicalOperator} */ (operator.slice(0, -1))
: null;
let value = /** @type {Expression} */ ( let value = /** @type {Expression} */ (
context.visit(build_assignment_value(operator, left, right)) context.visit(
logical_operator === null ? build_assignment_value(operator, left, right) : right
)
); );
const needs_proxy = const needs_proxy =
@ -88,7 +93,15 @@ function build_assignment(operator, left, right, context) {
is_non_coercive_operator(operator) && is_non_coercive_operator(operator) &&
should_proxy(value, context.state.scope); should_proxy(value, context.state.scope);
return b.call('$.set', left, value, needs_proxy && b.true); const assignment = b.call('$.set', left, value, needs_proxy && b.true);
return logical_operator === null
? assignment
: b.logical(
logical_operator,
/** @type {Expression} */ (context.visit(left)),
assignment
);
} }
} }
} }

@ -1,9 +1,9 @@
/** @import { BlockStatement, Pattern, Statement } from 'estree' */ /** @import { BlockStatement, Pattern, Statement } from 'estree' */
/** @import { AST } from '#compiler' */ /** @import { AST } from '#compiler' */
/** @import { ComponentClientTransformState, ComponentContext } from '../types' */ /** @import { ComponentClientTransformState, ComponentContext } from '../types' */
import { extract_identifiers, is_expression_async } from '../../../../utils/ast.js'; import { extract_identifiers } from '../../../../utils/ast.js';
import * as b from '#compiler/builders'; import * as b from '#compiler/builders';
import { create_derived } from '../utils.js'; import { async_thunk, create_derived } from '../utils.js';
import { get_value } from './shared/declarations.js'; import { get_value } from './shared/declarations.js';
import { build_expression, add_svelte_meta } from './shared/utils.js'; import { build_expression, add_svelte_meta } from './shared/utils.js';
@ -15,10 +15,10 @@ export function AwaitBlock(node, context) {
context.state.template.push_comment(); context.state.template.push_comment();
// Visit {#await <expression>} first to ensure that scopes are in the correct order // Visit {#await <expression>} first to ensure that scopes are in the correct order
const expression = b.thunk( const input = build_expression(context, node.expression, node.metadata.expression);
build_expression(context, node.expression, node.metadata.expression), const expression = node.metadata.expression.has_await
node.metadata.expression.has_await ? async_thunk(input, node.metadata.expression)
); : b.thunk(input);
let then_block; let then_block;
let catch_block; let catch_block;

@ -1,7 +1,7 @@
/** @import { CallExpression, Expression, Pattern } from 'estree' */ /** @import { CallExpression, Expression, Pattern } from 'estree' */
/** @import { AST } from '#compiler' */ /** @import { AST } from '#compiler' */
/** @import { ComponentContext } from '../types' */ /** @import { ComponentContext } from '../types' */
import { dev, is_ignored } from '../../../../state.js'; import { dev, ignore_map, is_ignored } from '../../../../state.js';
import { is_text_attribute } from '../../../../utils/ast.js'; import { is_text_attribute } from '../../../../utils/ast.js';
import * as b from '#compiler/builders'; import * as b from '#compiler/builders';
import { binding_properties } from '../../../bindings.js'; import { binding_properties } from '../../../bindings.js';
@ -40,9 +40,15 @@ export function BindDirective(node, context) {
validate_binding(context.state, node, expression); validate_binding(context.state, node, expression);
} }
const assignment = /** @type {Expression} */ ( const raw_assignment = b.assignment(
context.visit(b.assignment('=', /** @type {Pattern} */ (node.expression), b.id('$$value'))) '=',
/** @type {Pattern} */ (node.expression),
b.id('$$value')
); );
// The assignment is generated, so inherit any ignores attached to the binding
ignore_map.set(raw_assignment, ignore_map.get(node) ?? []);
const assignment = /** @type {Expression} */ (context.visit(raw_assignment));
if (dev) { if (dev) {
// in dev, create named functions, so that `$inspect(...)` delivers // in dev, create named functions, so that `$inspect(...)` delivers
@ -58,16 +64,7 @@ export function BindDirective(node, context) {
get = b.thunk(expression); get = b.thunk(expression);
/** @type {Expression | undefined} */ /** @type {Expression | undefined} */
set = b.unthunk( set = b.unthunk(b.arrow([b.id('$$value')], assignment));
b.arrow(
[b.id('$$value')],
/** @type {Expression} */ (
context.visit(
b.assignment('=', /** @type {Pattern} */ (node.expression), b.id('$$value'))
)
)
)
);
if (get === set) { if (get === set) {
set = undefined; set = undefined;

@ -19,7 +19,7 @@ export function ConstTag(node, context) {
if (declaration.id.type === 'Identifier') { if (declaration.id.type === 'Identifier') {
const init = build_expression(context, declaration.init, node.metadata.expression); const init = build_expression(context, declaration.init, node.metadata.expression);
let expression = create_derived(context.state, init, node.metadata.expression.has_await); let expression = create_derived(context.state, init, node.metadata.expression);
if (dev) { if (dev) {
expression = b.call('$.tag', expression, b.literal(declaration.id.name)); expression = b.call('$.tag', expression, b.literal(declaration.id.name));
@ -45,20 +45,31 @@ export function ConstTag(node, context) {
transform transform
}); });
// TODO optimise the simple `{ x } = y` case — we can just return `y` const is_simple_object_pattern =
// instead of destructuring it only to return a new object declaration.id.type === 'ObjectPattern' &&
declaration.id.properties.every(
(p) =>
p.type === 'Property' &&
!p.computed &&
p.key.type === 'Identifier' &&
p.value.type === 'Identifier' &&
p.key.name === p.value.name
);
const init = build_expression( const init = build_expression(
{ ...context, state: child_state }, { ...context, state: child_state },
declaration.init, declaration.init,
node.metadata.expression node.metadata.expression
); );
const block = b.block([ const block = is_simple_object_pattern
? b.block([b.return(init)])
: b.block([
b.const(/** @type {Pattern} */ (context.visit(declaration.id, child_state)), init), b.const(/** @type {Pattern} */ (context.visit(declaration.id, child_state)), init),
b.return(b.object(identifiers.map((node) => b.prop('init', node, node)))) b.return(b.object(identifiers.map((node) => b.prop('init', node, node))))
]); ]);
let expression = create_derived(context.state, block, node.metadata.expression.has_await); let expression = create_derived(context.state, block, node.metadata.expression);
if (dev) { if (dev) {
expression = b.call('$.tag', expression, b.literal('[@const]')); expression = b.call('$.tag', expression, b.literal('[@const]'));

Some files were not shown because too many files have changed in this diff Show More

Loading…
Cancel
Save