diff --git a/.changeset/chatty-mammals-find.md b/.changeset/chatty-mammals-find.md
new file mode 100644
index 0000000000..373dc0059a
--- /dev/null
+++ b/.changeset/chatty-mammals-find.md
@@ -0,0 +1,5 @@
+---
+'svelte': patch
+---
+
+fix: ensure infinite effect loops are cleared after flushing
diff --git a/.changeset/clear-olives-share.md b/.changeset/clear-olives-share.md
new file mode 100644
index 0000000000..975b2b21b6
--- /dev/null
+++ b/.changeset/clear-olives-share.md
@@ -0,0 +1,5 @@
+---
+'svelte': patch
+---
+
+fix: allow `{#key NaN}`
diff --git a/.changeset/fix-each-bind-store-logical.md b/.changeset/fix-each-bind-store-logical.md
new file mode 100644
index 0000000000..1327015124
--- /dev/null
+++ b/.changeset/fix-each-bind-store-logical.md
@@ -0,0 +1,5 @@
+---
+'svelte': patch
+---
+
+fix: detect store in each block expression regardless of AST shape
diff --git a/.changeset/fix-menu-element-a11y-roles.md b/.changeset/fix-menu-element-a11y-roles.md
new file mode 100644
index 0000000000..75cfe8d310
--- /dev/null
+++ b/.changeset/fix-menu-element-a11y-roles.md
@@ -0,0 +1,5 @@
+---
+'svelte': patch
+---
+
+fix: treat `
` like ` `/`` for a11y role checks
diff --git a/.changeset/forty-worlds-attack.md b/.changeset/forty-worlds-attack.md
new file mode 100644
index 0000000000..52bb3644c0
--- /dev/null
+++ b/.changeset/forty-worlds-attack.md
@@ -0,0 +1,5 @@
+---
+'svelte': patch
+---
+
+fix: add vite-ignore comment inside dynamic crypto import
diff --git a/.changeset/loud-bottles-own.md b/.changeset/loud-bottles-own.md
new file mode 100644
index 0000000000..493dc6c47f
--- /dev/null
+++ b/.changeset/loud-bottles-own.md
@@ -0,0 +1,5 @@
+---
+'svelte': patch
+---
+
+chore: wrap JSDoc URLs in `@see` and `@link` tags
diff --git a/.changeset/orange-ants-greet.md b/.changeset/orange-ants-greet.md
new file mode 100644
index 0000000000..6f7c684fee
--- /dev/null
+++ b/.changeset/orange-ants-greet.md
@@ -0,0 +1,5 @@
+---
+'svelte': minor
+---
+
+feat: allow use of createContext when instantiating components programmatically
diff --git a/.changeset/poor-students-nail.md b/.changeset/poor-students-nail.md
new file mode 100644
index 0000000000..cee650c002
--- /dev/null
+++ b/.changeset/poor-students-nail.md
@@ -0,0 +1,5 @@
+---
+"svelte": patch
+---
+
+fix: properly hydrate already-resolved async blocks
diff --git a/.changeset/sharp-snakes-poke.md b/.changeset/sharp-snakes-poke.md
new file mode 100644
index 0000000000..7f7f8aa7b2
--- /dev/null
+++ b/.changeset/sharp-snakes-poke.md
@@ -0,0 +1,5 @@
+---
+'svelte': patch
+---
+
+fix: emit `each_key_duplicate` error in production
diff --git a/.changeset/tiny-owls-pay.md b/.changeset/tiny-owls-pay.md
new file mode 100644
index 0000000000..ac25500258
--- /dev/null
+++ b/.changeset/tiny-owls-pay.md
@@ -0,0 +1,5 @@
+---
+'svelte': patch
+---
+
+fix: exit resolved async blocks on correct node when hydrating
diff --git a/.github/workflows/pkg.pr.new-comment.yml b/.github/workflows/pkg.pr.new-comment.yml
deleted file mode 100644
index 64495cc5c8..0000000000
--- a/.github/workflows/pkg.pr.new-comment.yml
+++ /dev/null
@@ -1,115 +0,0 @@
-name: Update pkg.pr.new comment
-
-on:
- workflow_run:
- workflows: ['Publish Any Commit']
- types:
- - completed
-
-permissions:
- pull-requests: write
-
-jobs:
- build:
- name: 'Update comment'
- runs-on: ubuntu-latest
- steps:
- - name: Download artifact
- uses: actions/download-artifact@v7
- with:
- name: output
- github-token: ${{ secrets.GITHUB_TOKEN }}
- run-id: ${{ github.event.workflow_run.id }}
-
- - run: ls -R .
- - name: 'Post or update comment'
- uses: actions/github-script@v8
- with:
- github-token: ${{ secrets.GITHUB_TOKEN }}
- script: |
- const fs = require('fs');
- const output = JSON.parse(fs.readFileSync('output.json', 'utf8'));
-
- const bot_comment_identifier = ``;
-
- const body = (number) => `${bot_comment_identifier}
-
- [Playground](https://svelte.dev/playground?version=pr-${number})
-
- \`\`\`
- ${output.packages.map((p) => `pnpm add https://pkg.pr.new/${p.name}@${number}`).join('\n')}
- \`\`\`
- `;
-
- async function find_bot_comment(issue_number) {
- if (!issue_number) return null;
- const comments = await github.rest.issues.listComments({
- owner: context.repo.owner,
- repo: context.repo.repo,
- issue_number: issue_number,
- });
- return comments.data.find((comment) =>
- comment.body.includes(bot_comment_identifier)
- );
- }
-
- async function create_or_update_comment(issue_number) {
- if (!issue_number) {
- console.log('No issue number provided. Cannot post or update comment.');
- return;
- }
-
- const existing_comment = await find_bot_comment(issue_number);
- if (existing_comment) {
- await github.rest.issues.updateComment({
- owner: context.repo.owner,
- repo: context.repo.repo,
- comment_id: existing_comment.id,
- body: body(issue_number),
- });
- } else {
- await github.rest.issues.createComment({
- issue_number: issue_number,
- owner: context.repo.owner,
- repo: context.repo.repo,
- body: body(issue_number),
- });
- }
- }
-
- async function log_publish_info() {
- const svelte_package = output.packages.find(p => p.name === 'svelte');
- const svelte_sha = svelte_package.url.replace(/^.+@([^@]+)$/, '$1');
- console.log('\n' + '='.repeat(50));
- console.log('Publish Information');
- console.log('='.repeat(50));
- console.log('\nPublished Packages:');
- console.log(output.packages.map((p) => `${p.name} - pnpm add https://pkg.pr.new/${p.name}@${p.url.replace(/^.+@([^@]+)$/, '$1')}`).join('\n'));
- if(svelte_sha){
- console.log('\nPlayground URL:');
- console.log(`\nhttps://svelte.dev/playground?version=commit-${svelte_sha}`)
- }
- console.log('\n' + '='.repeat(50));
- }
-
- if (output.event_name === 'pull_request') {
- if (output.number) {
- await create_or_update_comment(output.number);
- }
- } else if (output.event_name === 'push') {
- const pull_requests = await github.rest.pulls.list({
- owner: context.repo.owner,
- repo: context.repo.repo,
- state: 'open',
- head: `${context.repo.owner}:${output.ref.replace('refs/heads/', '')}`,
- });
-
- if (pull_requests.data.length > 0) {
- await create_or_update_comment(pull_requests.data[0].number);
- } else {
- console.log(
- 'No open pull request found for this push. Logging publish information to console:'
- );
- await log_publish_info();
- }
- }
diff --git a/.github/workflows/pkg.pr.new.yml b/.github/workflows/pkg.pr.new.yml
index 252cbed769..0f0c433361 100644
--- a/.github/workflows/pkg.pr.new.yml
+++ b/.github/workflows/pkg.pr.new.yml
@@ -1,16 +1,51 @@
-name: Publish Any Commit
-on: [push, pull_request]
+name: pkg.pr.new
+on:
+ pull_request_target:
+ types: [opened, synchronize]
+ push:
+ branches: [main]
permissions: {}
jobs:
- build:
- permissions: {}
+ # This job determines the environment to use for the build job. It ensures that:
+ # - For pushes to main, we use the "Publish pkg.pr.new (maintainers)" environment.
+ # - For PRs from the same repository, we also use the "Publish pkg.pr.new (maintainers)" environment, since these are trusted.
+ # - For PRs from forks, we use the "Publish pkg.pr.new (external contributors)" environment, which requires manual approval by a maintainer before the build job can run.
+ # This protects us from running untrusted code while still allowing external contributors to use pkg.pr.new.
+ resolve-env:
+ runs-on: ubuntu-latest
+ outputs:
+ environment: ${{ steps.resolve.outputs.environment }}
+ steps:
+ - name: Determine environment
+ id: resolve
+ run: |
+ if [[ "${{ github.event_name }}" == "push" ]]; then
+ echo "environment=Publish pkg.pr.new (maintainers)" >> "$GITHUB_OUTPUT"
+ elif [[ "${{ github.event.pull_request.head.repo.full_name }}" == "${{ github.repository }}" ]]; then
+ echo "environment=Publish pkg.pr.new (maintainers)" >> "$GITHUB_OUTPUT"
+ else
+ echo "environment=Publish pkg.pr.new (external contributors)" >> "$GITHUB_OUTPUT"
+ fi
+ build:
+ needs: resolve-env
runs-on: ubuntu-latest
+ # This is the line that ensures forks require manual approval before running the build job
+ environment: ${{ needs.resolve-env.outputs.environment }}
+
+ # No permissions — this job runs user-controlled code
+ permissions: {}
steps:
- uses: actions/checkout@v6
+ with:
+ # For pull_request_target, we must explicitly check out the PR head.
+ # This is safe because the environment gate above has already fired —
+ # an org member has approved this specific commit for external PRs.
+ ref: ${{ github.event.pull_request.head.sha || github.sha }}
+
- uses: pnpm/action-setup@v4
- uses: actions/setup-node@v6
with:
@@ -24,21 +59,161 @@ jobs:
run: pnpm build
- run: pnpx pkg-pr-new publish --comment=off --json output.json --compact --no-template './packages/svelte'
- - name: Add metadata to output
+
+ - 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'
+ runs-on: ubuntu-latest
+
+ permissions:
+ pull-requests: write
+
+ steps:
+ - name: Download sanitized artifact
+ uses: actions/download-artifact@v7
+ with:
+ name: sanitized-output
+
+ - name: Post or update comment
uses: actions/github-script@v8
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
script: |
const fs = require('fs');
- const output = JSON.parse(fs.readFileSync('output.json', 'utf8'));
- output.number = context.issue.number;
- output.event_name = context.eventName;
- output.ref = context.ref;
- fs.writeFileSync('output.json', JSON.stringify(output), 'utf8');
- - name: Upload output
- uses: actions/upload-artifact@v6
+ const { packages } = JSON.parse(fs.readFileSync('sanitized-output.json', 'utf8'));
+
+ if (packages.length === 0) {
+ console.log('No valid packages found. Skipping comment.');
+ return;
+ }
+
+ // Issue number from trusted event context, never from the artifact
+ const issue_number = context.issue.number;
+
+ const bot_comment_identifier = ``;
+
+ 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: output
- path: ./output.json
+ 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;
+ }
- - run: ls -R .
+ 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));
diff --git a/documentation/docs/06-runtime/02-context.md b/documentation/docs/06-runtime/02-context.md
index 0dfb996164..61b203f93e 100644
--- a/documentation/docs/06-runtime/02-context.md
+++ b/documentation/docs/06-runtime/02-context.md
@@ -97,6 +97,32 @@ import { createContext } from 'svelte';
export const [getUserContext, setUserContext] = createContext();
```
+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:
+
+```js
+import { mount, unmount } from 'svelte';
+import { expect, test } from 'vitest';
+import { setUserContext } from './context';
+import MyComponent from './MyComponent.svelte';
+
+test('MyComponent', () => {
+ function Wrapper(...args) {
+ setUserContext({ name: 'Bob' });
+ return MyComponent(...args);
+ }
+
+ const component = mount(Wrapper, {
+ target: document.body
+ });
+
+ expect(document.body.innerHTML).toBe('Hello Bob! ');
+
+ unmount(component);
+});
+```
+
+This approach also works with [`hydrate`](imperative-component-api#hydrate) and [`render`](imperative-component-api#render).
+
## 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:
diff --git a/documentation/docs/07-misc/02-testing.md b/documentation/docs/07-misc/02-testing.md
index 85db7fc01f..c1bc69c1c2 100644
--- a/documentation/docs/07-misc/02-testing.md
+++ b/documentation/docs/07-misc/02-testing.md
@@ -181,7 +181,7 @@ export default defineConfig({
/* ... */
],
test: {
- // If you are testing components client-side, you need to setup a DOM environment.
+ // If you are testing components client-side, you need to set up a DOM environment.
// If not all your files should have this environment, you can use a
// `// @vitest-environment jsdom` comment at the top of the test files instead.
environment: 'jsdom'
diff --git a/documentation/docs/07-misc/07-v5-migration-guide.md b/documentation/docs/07-misc/07-v5-migration-guide.md
index 40cbc3bd9e..5a80734b7a 100644
--- a/documentation/docs/07-misc/07-v5-migration-guide.md
+++ b/documentation/docs/07-misc/07-v5-migration-guide.md
@@ -778,9 +778,9 @@ In Svelte 4, doing the following triggered reactivity:
This is because the Svelte compiler treated the assignment to `foo.value` as an instruction to update anything that referenced `foo`. In Svelte 5, reactivity is determined at runtime rather than compile time, so you should define `value` as a reactive `$state` field on the `Foo` class. Wrapping `new Foo()` with `$state(...)` will have no effect — only vanilla objects and arrays are made deeply reactive.
-### Touch and wheel events are passive
+### Touch events are passive
-When using `onwheel`, `onmousewheel`, `ontouchstart` and `ontouchmove` event attributes, the handlers are [passive](https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/addEventListener#using_passive_listeners) to align with browser defaults. This greatly improves responsiveness by allowing the browser to scroll the document immediately, rather than waiting to see if the event handler calls `event.preventDefault()`.
+When using `ontouchstart` and `ontouchmove` event attributes, the handlers are [passive](https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/addEventListener#using_passive_listeners) to align with browser defaults. This greatly improves responsiveness by allowing the browser to scroll the document immediately, rather than waiting to see if the event handler calls `event.preventDefault()`.
In the very rare cases that you need to prevent these event defaults, you should use [`on`](/docs/svelte/svelte-events#on) instead (for example inside an action).
diff --git a/packages/svelte/CHANGELOG.md b/packages/svelte/CHANGELOG.md
index c7c38302e8..a71effd5dd 100644
--- a/packages/svelte/CHANGELOG.md
+++ b/packages/svelte/CHANGELOG.md
@@ -1,5 +1,17 @@
# svelte
+## 5.49.2
+
+### Patch Changes
+
+- chore: remove SvelteKit data attributes from elements.d.ts ([#17613](https://github.com/sveltejs/svelte/pull/17613))
+
+- fix: avoid erroneous async derived expressions for blocks ([#17604](https://github.com/sveltejs/svelte/pull/17604))
+
+- fix: avoid Cloudflare warnings about not having the "node:crypto" module ([#17612](https://github.com/sveltejs/svelte/pull/17612))
+
+- fix: reschedule effects inside unskipped branches ([#17604](https://github.com/sveltejs/svelte/pull/17604))
+
## 5.49.1
### Patch Changes
diff --git a/packages/svelte/elements.d.ts b/packages/svelte/elements.d.ts
index fa74124472..885004dd2a 100644
--- a/packages/svelte/elements.d.ts
+++ b/packages/svelte/elements.d.ts
@@ -851,23 +851,6 @@ export interface HTMLAttributes extends AriaAttributes, D
readonly 'bind:offsetWidth'?: number | undefined | null;
readonly 'bind:offsetHeight'?: number | undefined | null;
- // SvelteKit
- 'data-sveltekit-keepfocus'?: true | '' | 'off' | undefined | null;
- 'data-sveltekit-noscroll'?: true | '' | 'off' | undefined | null;
- 'data-sveltekit-preload-code'?:
- | true
- | ''
- | 'eager'
- | 'viewport'
- | 'hover'
- | 'tap'
- | 'off'
- | undefined
- | null;
- 'data-sveltekit-preload-data'?: true | '' | 'hover' | 'tap' | 'off' | undefined | null;
- 'data-sveltekit-reload'?: true | '' | 'off' | undefined | null;
- 'data-sveltekit-replacestate'?: true | '' | 'off' | undefined | null;
-
// allow any data- attribute
[key: `data-${string}`]: any;
diff --git a/packages/svelte/package.json b/packages/svelte/package.json
index fd4b98679e..48f492783f 100644
--- a/packages/svelte/package.json
+++ b/packages/svelte/package.json
@@ -2,7 +2,7 @@
"name": "svelte",
"description": "Cybernetically enhanced web apps",
"license": "MIT",
- "version": "5.49.1",
+ "version": "5.49.2",
"type": "module",
"types": "./types/index.d.ts",
"engines": {
diff --git a/packages/svelte/src/ambient.d.ts b/packages/svelte/src/ambient.d.ts
index ec5b799470..159a568477 100644
--- a/packages/svelte/src/ambient.d.ts
+++ b/packages/svelte/src/ambient.d.ts
@@ -16,7 +16,7 @@ declare module '*.svelte' {
* let count = $state(0);
* ```
*
- * https://svelte.dev/docs/svelte/$state
+ * @see {@link https://svelte.dev/docs/svelte/$state Documentation}
*
* @param initial The initial value
*/
@@ -126,7 +126,7 @@ declare namespace $state {
*
* ```
*
- * https://svelte.dev/docs/svelte/$state#$state.raw
+ * @see {@link https://svelte.dev/docs/svelte/$state#$state.raw Documentation}
*
* @param initial The initial value
*/
@@ -147,7 +147,7 @@ declare namespace $state {
*
* ```
*
- * https://svelte.dev/docs/svelte/$state#$state.snapshot
+ * @see {@link https://svelte.dev/docs/svelte/$state#$state.snapshot Documentation}
*
* @param state The value to snapshot
*/
@@ -187,7 +187,7 @@ declare namespace $state {
* let double = $derived(count * 2);
* ```
*
- * https://svelte.dev/docs/svelte/$derived
+ * @see {@link https://svelte.dev/docs/svelte/$derived Documentation}
*
* @param expression The derived state expression
*/
@@ -209,7 +209,7 @@ declare namespace $derived {
* });
* ```
*
- * https://svelte.dev/docs/svelte/$derived#$derived.by
+ * @see {@link https://svelte.dev/docs/svelte/$derived#$derived.by Documentation}
*/
export function by(fn: () => T): T;
@@ -251,7 +251,7 @@ declare namespace $derived {
*
* Does not run during server-side rendering.
*
- * https://svelte.dev/docs/svelte/$effect
+ * @see {@link https://svelte.dev/docs/svelte/$effect Documentation}
* @param fn The function to execute
*/
declare function $effect(fn: () => void | (() => void)): void;
@@ -270,7 +270,7 @@ declare namespace $effect {
*
* Does not run during server-side rendering.
*
- * https://svelte.dev/docs/svelte/$effect#$effect.pre
+ * @see {@link https://svelte.dev/docs/svelte/$effect#$effect.pre Documentation}
* @param fn The function to execute
*/
export function pre(fn: () => void | (() => void)): void;
@@ -278,7 +278,7 @@ declare namespace $effect {
/**
* Returns the number of promises that are pending in the current boundary, not including child boundaries.
*
- * https://svelte.dev/docs/svelte/$effect#$effect.pending
+ * @see {@link https://svelte.dev/docs/svelte/$effect#$effect.pending Documentation}
*/
export function pending(): number;
@@ -300,7 +300,7 @@ declare namespace $effect {
*
* This allows you to (for example) add things like subscriptions without causing memory leaks, by putting them in child effects.
*
- * https://svelte.dev/docs/svelte/$effect#$effect.tracking
+ * @see {@link https://svelte.dev/docs/svelte/$effect#$effect.tracking Documentation}
*/
export function tracking(): boolean;
@@ -328,7 +328,7 @@ declare namespace $effect {
* cleanup()}>cleanup
* ```
*
- * https://svelte.dev/docs/svelte/$effect#$effect.root
+ * @see {@link https://svelte.dev/docs/svelte/$effect#$effect.root Documentation}
*/
export function root(fn: () => void | (() => void)): () => void;
@@ -364,7 +364,7 @@ declare namespace $effect {
* let { optionalProp = 42, requiredProp, bindableProp = $bindable() }: { optionalProp?: number; requiredProps: string; bindableProp: boolean } = $props();
* ```
*
- * https://svelte.dev/docs/svelte/$props
+ * @see {@link https://svelte.dev/docs/svelte/$props Documentation}
*/
declare function $props(): any;
@@ -410,7 +410,7 @@ declare namespace $props {
* let { propName = $bindable() }: { propName: boolean } = $props();
* ```
*
- * https://svelte.dev/docs/svelte/$bindable
+ * @see {@link https://svelte.dev/docs/svelte/$bindable Documentation}
*/
declare function $bindable(fallback?: T): T;
@@ -456,7 +456,7 @@ declare namespace $bindable {
* $inspect(x, y).with(() => { debugger; });
* ```
*
- * https://svelte.dev/docs/svelte/$inspect
+ * @see {@link https://svelte.dev/docs/svelte/$inspect Documentation}
*/
declare function $inspect(
...values: T
@@ -522,7 +522,7 @@ declare namespace $inspect {
*
* Only available inside custom element components, and only on the client-side.
*
- * https://svelte.dev/docs/svelte/$host
+ * @see {@link https://svelte.dev/docs/svelte/$host Documentation}
*/
declare function $host(): El;
diff --git a/packages/svelte/src/compiler/phases/1-parse/read/style.js b/packages/svelte/src/compiler/phases/1-parse/read/style.js
index 55647b2c94..4f2db349cd 100644
--- a/packages/svelte/src/compiler/phases/1-parse/read/style.js
+++ b/packages/svelte/src/compiler/phases/1-parse/read/style.js
@@ -568,7 +568,7 @@ function read_attribute_value(parser) {
}
/**
- * https://www.w3.org/TR/css-syntax-3/#ident-token-diagram
+ * @see {@link https://www.w3.org/TR/css-syntax-3/#ident-token-diagram CSS Syntax Module Level 3}
* @param {Parser} parser
*/
function read_identifier(parser) {
diff --git a/packages/svelte/src/compiler/phases/2-analyze/visitors/shared/a11y/constants.js b/packages/svelte/src/compiler/phases/2-analyze/visitors/shared/a11y/constants.js
index 684ea92094..49099c2cb1 100644
--- a/packages/svelte/src/compiler/phases/2-analyze/visitors/shared/a11y/constants.js
+++ b/packages/svelte/src/compiler/phases/2-analyze/visitors/shared/a11y/constants.js
@@ -174,6 +174,7 @@ export const input_type_to_implicit_role = new Map([
export const a11y_non_interactive_element_to_interactive_role_exceptions = {
ul: ['listbox', 'menu', 'menubar', 'radiogroup', 'tablist', 'tree', 'treegrid'],
ol: ['listbox', 'menu', 'menubar', 'radiogroup', 'tablist', 'tree', 'treegrid'],
+ menu: ['listbox', 'menu', 'menubar', 'radiogroup', 'tablist', 'tree', 'treegrid'],
li: ['menuitem', 'option', 'row', 'tab', 'treeitem'],
table: ['grid'],
td: ['gridcell'],
diff --git a/packages/svelte/src/compiler/phases/2-analyze/visitors/shared/a11y/index.js b/packages/svelte/src/compiler/phases/2-analyze/visitors/shared/a11y/index.js
index f5f4982ed2..45de8b10a1 100644
--- a/packages/svelte/src/compiler/phases/2-analyze/visitors/shared/a11y/index.js
+++ b/packages/svelte/src/compiler/phases/2-analyze/visitors/shared/a11y/index.js
@@ -167,7 +167,7 @@ export function check_element(node, context) {
if (
current_role === get_implicit_role(node.name, attribute_map) &&
// is ok because CSS list-style:none removes the semantics and this is a way to bring them back
- !['ul', 'ol', 'li'].includes(node.name) &&
+ !['ul', 'ol', 'li', 'menu'].includes(node.name) &&
// is ok because without href the a tag doesn't have a role of link
!(node.name === 'a' && !attribute_map.has('href'))
) {
diff --git a/packages/svelte/src/compiler/phases/3-transform/client/transform-client.js b/packages/svelte/src/compiler/phases/3-transform/client/transform-client.js
index d5ce3caaa9..b50a73b8b6 100644
--- a/packages/svelte/src/compiler/phases/3-transform/client/transform-client.js
+++ b/packages/svelte/src/compiler/phases/3-transform/client/transform-client.js
@@ -166,6 +166,7 @@ export function client_component(analysis, options) {
in_constructor: false,
instance_level_snippets: [],
module_level_snippets: [],
+ is_standalone: false,
// these are set inside the `Fragment` visitor, and cannot be used until then
init: /** @type {any} */ (null),
diff --git a/packages/svelte/src/compiler/phases/3-transform/client/types.d.ts b/packages/svelte/src/compiler/phases/3-transform/client/types.d.ts
index 4438ec015b..287bf24ac6 100644
--- a/packages/svelte/src/compiler/phases/3-transform/client/types.d.ts
+++ b/packages/svelte/src/compiler/phases/3-transform/client/types.d.ts
@@ -83,6 +83,9 @@ export interface ComponentClientTransformState extends ClientTransformState {
readonly instance_level_snippets: VariableDeclaration[];
/** Snippets hoisted to the module */
readonly module_level_snippets: VariableDeclaration[];
+
+ /** True if the current node is a) a component or render tag and b) the sole child of a block */
+ readonly is_standalone: boolean;
}
export type Context = import('zimmerframe').Context;
diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/EachBlock.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/EachBlock.js
index c0bfe272e5..a1371b516a 100644
--- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/EachBlock.js
+++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/EachBlock.js
@@ -101,15 +101,11 @@ export function EachBlock(node, context) {
}
// If the array is a store expression, we need to invalidate it when the array is changed.
- // This doesn't catch all cases, but all the ones that Svelte 4 catches, too.
let store_to_invalidate = '';
- if (node.expression.type === 'Identifier' || node.expression.type === 'MemberExpression') {
- const id = object(node.expression);
- if (id) {
- const binding = context.state.scope.get(id.name);
- if (binding?.kind === 'store_sub') {
- store_to_invalidate = id.name;
- }
+ for (const binding of node.metadata.expression.dependencies) {
+ if (binding.kind === 'store_sub') {
+ store_to_invalidate = binding.node.name;
+ break;
}
}
@@ -312,10 +308,10 @@ export function EachBlock(node, context) {
declarations.push(b.let(node.index, index));
}
- const is_async = node.metadata.expression.is_async();
+ const has_await = node.metadata.expression.has_await;
- const get_collection = b.thunk(collection, node.metadata.expression.has_await);
- const thunk = is_async ? b.thunk(b.call('$.get', b.id('$$collection'))) : get_collection;
+ const get_collection = b.thunk(collection, has_await);
+ const thunk = has_await ? b.thunk(b.call('$.get', b.id('$$collection'))) : get_collection;
const render_args = [b.id('$$anchor'), item];
if (uses_index || collection_id) render_args.push(index);
@@ -338,19 +334,18 @@ export function EachBlock(node, context) {
const statements = [add_svelte_meta(b.call('$.each', ...args), node, 'each')];
- if (dev && node.metadata.keyed) {
- statements.unshift(b.stmt(b.call('$.validate_each_keys', thunk, key_function)));
- }
-
- if (is_async) {
+ if (node.metadata.expression.is_async()) {
context.state.init.push(
b.stmt(
b.call(
'$.async',
context.state.node,
node.metadata.expression.blockers(),
- b.array([get_collection]),
- b.arrow([context.state.node, b.id('$$collection')], b.block(statements))
+ has_await ? b.array([get_collection]) : b.void0,
+ b.arrow(
+ has_await ? [context.state.node, b.id('$$collection')] : [context.state.node],
+ b.block(statements)
+ )
)
)
);
diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/Fragment.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/Fragment.js
index 79a443967c..00b0cfaa2e 100644
--- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/Fragment.js
+++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/Fragment.js
@@ -120,34 +120,35 @@ export function Fragment(node, context) {
state.init.unshift(b.var(id, b.call('$.text')));
close = b.stmt(b.call('$.append', b.id('$$anchor'), id));
+ } else if (is_standalone) {
+ // no need to create a template, we can just use the existing block's anchor
+ process_children(trimmed, () => b.id('$$anchor'), false, {
+ ...context,
+ state: { ...state, is_standalone }
+ });
} else {
- if (is_standalone) {
- // no need to create a template, we can just use the existing block's anchor
- process_children(trimmed, () => b.id('$$anchor'), false, { ...context, state });
- } else {
- /** @type {(is_text: boolean) => Expression} */
- const expression = (is_text) => b.call('$.first_child', id, is_text && b.true);
-
- process_children(trimmed, expression, false, { ...context, state });
+ /** @type {(is_text: boolean) => Expression} */
+ const expression = (is_text) => b.call('$.first_child', id, is_text && b.true);
- let flags = TEMPLATE_FRAGMENT;
+ process_children(trimmed, expression, false, { ...context, state });
- if (state.template.needs_import_node) {
- flags |= TEMPLATE_USE_IMPORT_NODE;
- }
+ let flags = TEMPLATE_FRAGMENT;
- if (state.template.nodes.length === 1 && state.template.nodes[0].type === 'comment') {
- // special case — we can use `$.comment` instead of creating a unique template
- state.init.unshift(b.var(id, b.call('$.comment')));
- } else {
- const template = transform_template(state, namespace, flags);
- state.hoisted.push(b.var(template_name, template));
+ if (state.template.needs_import_node) {
+ flags |= TEMPLATE_USE_IMPORT_NODE;
+ }
- state.init.unshift(b.var(id, b.call(template_name)));
- }
+ if (state.template.nodes.length === 1 && state.template.nodes[0].type === 'comment') {
+ // special case — we can use `$.comment` instead of creating a unique template
+ state.init.unshift(b.var(id, b.call('$.comment')));
+ } else {
+ const template = transform_template(state, namespace, flags);
+ state.hoisted.push(b.var(template_name, template));
- close = b.stmt(b.call('$.append', b.id('$$anchor'), id));
+ state.init.unshift(b.var(id, b.call(template_name)));
}
+
+ close = b.stmt(b.call('$.append', b.id('$$anchor'), id));
}
}
diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/HtmlTag.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/HtmlTag.js
index 0567edc610..2706cf7f0a 100644
--- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/HtmlTag.js
+++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/HtmlTag.js
@@ -11,10 +11,11 @@ import { build_expression } from './shared/utils.js';
export function HtmlTag(node, context) {
context.state.template.push_comment();
- const is_async = node.metadata.expression.is_async();
+ const has_await = node.metadata.expression.has_await;
+ const has_blockers = node.metadata.expression.has_blockers();
const expression = build_expression(context, node.expression, node.metadata.expression);
- const html = is_async ? b.call('$.get', b.id('$$html')) : expression;
+ const html = has_await ? b.call('$.get', b.id('$$html')) : expression;
const is_svg = context.state.metadata.namespace === 'svg';
const is_mathml = context.state.metadata.namespace === 'mathml';
@@ -31,15 +32,18 @@ export function HtmlTag(node, context) {
);
// push into init, so that bindings run afterwards, which might trigger another run and override hydration
- if (is_async) {
+ if (has_await || has_blockers) {
context.state.init.push(
b.stmt(
b.call(
'$.async',
context.state.node,
node.metadata.expression.blockers(),
- b.array([b.thunk(expression, node.metadata.expression.has_await)]),
- b.arrow([context.state.node, b.id('$$html')], b.block([statement]))
+ has_await ? b.array([b.thunk(expression, true)]) : b.void0,
+ b.arrow(
+ has_await ? [context.state.node, b.id('$$html')] : [context.state.node],
+ b.block([statement])
+ )
)
)
);
diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/IfBlock.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/IfBlock.js
index fcbb59ba74..c0e66635df 100644
--- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/IfBlock.js
+++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/IfBlock.js
@@ -25,10 +25,11 @@ export function IfBlock(node, context) {
statements.push(b.var(alternate_id, b.arrow([b.id('$$anchor')], alternate)));
}
- const is_async = node.metadata.expression.is_async();
+ const has_await = node.metadata.expression.has_await;
+ const has_blockers = node.metadata.expression.has_blockers();
const expression = build_expression(context, node.test, node.metadata.expression);
- const test = is_async ? b.call('$.get', b.id('$$condition')) : expression;
+ const test = has_await ? b.call('$.get', b.id('$$condition')) : expression;
/** @type {Expression[]} */
const args = [
@@ -72,15 +73,18 @@ export function IfBlock(node, context) {
statements.push(add_svelte_meta(b.call('$.if', ...args), node, 'if'));
- if (is_async) {
+ if (has_await || has_blockers) {
context.state.init.push(
b.stmt(
b.call(
'$.async',
context.state.node,
node.metadata.expression.blockers(),
- b.array([b.thunk(expression, node.metadata.expression.has_await)]),
- b.arrow([context.state.node, b.id('$$condition')], b.block(statements))
+ has_await ? b.array([b.thunk(expression, true)]) : b.void0,
+ b.arrow(
+ has_await ? [context.state.node, b.id('$$condition')] : [context.state.node],
+ b.block(statements)
+ )
)
)
);
diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/KeyBlock.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/KeyBlock.js
index d050155e8b..143a4e8edd 100644
--- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/KeyBlock.js
+++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/KeyBlock.js
@@ -11,29 +11,35 @@ import { build_expression, add_svelte_meta } from './shared/utils.js';
export function KeyBlock(node, context) {
context.state.template.push_comment();
- const is_async = node.metadata.expression.is_async();
+ const has_await = node.metadata.expression.has_await;
+ const has_blockers = node.metadata.expression.has_blockers();
const expression = build_expression(context, node.expression, node.metadata.expression);
- const key = b.thunk(is_async ? b.call('$.get', b.id('$$key')) : expression);
+ const key = b.thunk(has_await ? b.call('$.get', b.id('$$key')) : expression);
const body = /** @type {Expression} */ (context.visit(node.fragment));
- let statement = add_svelte_meta(
+ const statement = add_svelte_meta(
b.call('$.key', context.state.node, key, b.arrow([b.id('$$anchor')], body)),
node,
'key'
);
- if (is_async) {
- statement = b.stmt(
- b.call(
- '$.async',
- context.state.node,
- node.metadata.expression.blockers(),
- b.array([b.thunk(expression, node.metadata.expression.has_await)]),
- b.arrow([context.state.node, b.id('$$key')], b.block([statement]))
+ if (has_await || has_blockers) {
+ context.state.init.push(
+ b.stmt(
+ b.call(
+ '$.async',
+ context.state.node,
+ node.metadata.expression.blockers(),
+ has_await ? b.array([b.thunk(expression, true)]) : b.void0,
+ b.arrow(
+ has_await ? [context.state.node, b.id('$$key')] : [context.state.node],
+ b.block([statement])
+ )
+ )
)
);
+ } else {
+ context.state.init.push(statement);
}
-
- context.state.init.push(statement);
}
diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/RenderTag.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/RenderTag.js
index d14336bb7e..5d39cf2216 100644
--- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/RenderTag.js
+++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/RenderTag.js
@@ -85,6 +85,10 @@ export function RenderTag(node, context) {
)
)
);
+
+ if (context.state.is_standalone) {
+ context.state.init.push(b.stmt(b.call('$.next')));
+ }
} else {
context.state.init.push(statements.length === 1 ? statements[0] : b.block(statements));
}
diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/SvelteElement.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/SvelteElement.js
index c8192cf00a..10024298fa 100644
--- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/SvelteElement.js
+++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/SvelteElement.js
@@ -93,10 +93,11 @@ export function SvelteElement(node, context) {
);
}
- const is_async = node.metadata.expression.is_async();
+ const has_await = node.metadata.expression.has_await;
+ const has_blockers = node.metadata.expression.has_blockers();
const expression = /** @type {Expression} */ (context.visit(node.tag));
- const get_tag = b.thunk(is_async ? b.call('$.get', b.id('$$tag')) : expression);
+ const get_tag = b.thunk(has_await ? b.call('$.get', b.id('$$tag')) : expression);
/** @type {Statement[]} */
const inner = inner_context.state.init;
@@ -139,15 +140,18 @@ export function SvelteElement(node, context) {
)
);
- if (is_async) {
+ if (has_await || has_blockers) {
context.state.init.push(
b.stmt(
b.call(
'$.async',
context.state.node,
node.metadata.expression.blockers(),
- b.array([b.thunk(expression, node.metadata.expression.has_await)]),
- b.arrow([context.state.node, b.id('$$tag')], b.block(statements))
+ has_await ? b.array([b.thunk(expression, true)]) : b.void0,
+ b.arrow(
+ has_await ? [context.state.node, b.id('$$tag')] : [context.state.node],
+ b.block(statements)
+ )
)
)
);
diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/component.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/component.js
index bb72794af8..1d6d3413bf 100644
--- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/component.js
+++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/component.js
@@ -461,7 +461,7 @@ export function build_component(node, component_name, loc, context) {
memoizer.check_blockers(node.metadata.expression);
}
- const statements = [...snippet_declarations, ...memoizer.deriveds(context.state.analysis.runes)];
+ let statements = [...snippet_declarations, ...memoizer.deriveds(context.state.analysis.runes)];
if (is_component_dynamic) {
const prev = fn;
@@ -515,15 +515,21 @@ export function build_component(node, component_name, loc, context) {
const blockers = memoizer.blockers();
if (async_values || blockers) {
- return b.stmt(
- b.call(
- '$.async',
- anchor,
- blockers,
- async_values,
- b.arrow([b.id('$$anchor'), ...memoizer.async_ids()], b.block(statements))
+ statements = [
+ b.stmt(
+ b.call(
+ '$.async',
+ anchor,
+ blockers,
+ async_values,
+ b.arrow([b.id('$$anchor'), ...memoizer.async_ids()], b.block(statements))
+ )
)
- );
+ ];
+
+ if (context.state.is_standalone) {
+ statements.push(b.stmt(b.call('$.next')));
+ }
}
return statements.length > 1 ? b.block(statements) : statements[0];
diff --git a/packages/svelte/src/compiler/phases/3-transform/server/transform-server.js b/packages/svelte/src/compiler/phases/3-transform/server/transform-server.js
index 0e2f68d0f5..b9f1441bff 100644
--- a/packages/svelte/src/compiler/phases/3-transform/server/transform-server.js
+++ b/packages/svelte/src/compiler/phases/3-transform/server/transform-server.js
@@ -41,7 +41,6 @@ import { TitleElement } from './visitors/TitleElement.js';
import { UpdateExpression } from './visitors/UpdateExpression.js';
import { VariableDeclaration } from './visitors/VariableDeclaration.js';
import { SvelteBoundary } from './visitors/SvelteBoundary.js';
-import { call_component_renderer } from './visitors/shared/utils.js';
/** @type {Visitors} */
const global_visitors = {
@@ -105,7 +104,7 @@ export function server_component(analysis, options) {
namespace: options.namespace,
preserve_whitespace: options.preserveWhitespace,
state_fields: new Map(),
- skip_hydration_boundaries: false,
+ is_standalone: false,
is_instance: false
};
@@ -260,7 +259,13 @@ export function server_component(analysis, options) {
if (should_inject_context) {
component_block = b.block([
- call_component_renderer(component_block, dev && b.id(component_name))
+ b.stmt(
+ b.call(
+ '$$renderer.component',
+ b.arrow([b.id('$$renderer')], component_block, false),
+ dev && b.id(component_name)
+ )
+ )
]);
}
diff --git a/packages/svelte/src/compiler/phases/3-transform/server/types.d.ts b/packages/svelte/src/compiler/phases/3-transform/server/types.d.ts
index e7a72fb8ad..4912728a1e 100644
--- a/packages/svelte/src/compiler/phases/3-transform/server/types.d.ts
+++ b/packages/svelte/src/compiler/phases/3-transform/server/types.d.ts
@@ -26,7 +26,8 @@ export interface ComponentServerTransformState extends ServerTransformState {
readonly template: Array;
readonly namespace: Namespace;
readonly preserve_whitespace: boolean;
- readonly skip_hydration_boundaries: boolean;
+ /** True if the current node is a) a component or render tag and b) the sole child of a block */
+ readonly is_standalone: boolean;
/** Transformed async `{@const }` declarations (if any) and those coming after them */
async_consts?: {
id: Identifier;
diff --git a/packages/svelte/src/compiler/phases/3-transform/server/visitors/AwaitBlock.js b/packages/svelte/src/compiler/phases/3-transform/server/visitors/AwaitBlock.js
index b8d2e42144..84c2a81612 100644
--- a/packages/svelte/src/compiler/phases/3-transform/server/visitors/AwaitBlock.js
+++ b/packages/svelte/src/compiler/phases/3-transform/server/visitors/AwaitBlock.js
@@ -2,7 +2,7 @@
/** @import { AST } from '#compiler' */
/** @import { ComponentContext } from '../types.js' */
import * as b from '#compiler/builders';
-import { block_close, create_async_block } from './shared/utils.js';
+import { block_close, create_child_block } from './shared/utils.js';
/**
* @param {AST.AwaitBlock} node
@@ -25,13 +25,12 @@ export function AwaitBlock(node, context) {
)
);
- if (node.metadata.expression.is_async()) {
- statement = create_async_block(
- b.block([statement]),
+ context.state.template.push(
+ ...create_child_block(
+ [statement],
node.metadata.expression.blockers(),
node.metadata.expression.has_await
- );
- }
-
- context.state.template.push(statement, block_close);
+ ),
+ block_close
+ );
}
diff --git a/packages/svelte/src/compiler/phases/3-transform/server/visitors/EachBlock.js b/packages/svelte/src/compiler/phases/3-transform/server/visitors/EachBlock.js
index 3c0a8c1676..cb5a61d52f 100644
--- a/packages/svelte/src/compiler/phases/3-transform/server/visitors/EachBlock.js
+++ b/packages/svelte/src/compiler/phases/3-transform/server/visitors/EachBlock.js
@@ -2,7 +2,7 @@
/** @import { AST } from '#compiler' */
/** @import { ComponentContext } from '../types.js' */
import * as b from '#compiler/builders';
-import { block_close, block_open, block_open_else, create_async_block } from './shared/utils.js';
+import { block_close, block_open, block_open_else, create_child_block } from './shared/utils.js';
/**
* @param {AST.EachBlock} node
@@ -18,8 +18,8 @@ export function EachBlock(node, context) {
const array_id = state.scope.root.unique('each_array');
- /** @type {Statement} */
- let block = b.block([b.const(array_id, b.call('$.ensure_array_like', collection))]);
+ /** @type {Statement[]} */
+ let statements = [b.const(array_id, b.call('$.ensure_array_like', collection))];
/** @type {Statement[]} */
const each = [];
@@ -53,7 +53,7 @@ export function EachBlock(node, context) {
fallback.body.unshift(b.stmt(b.call(b.id('$$renderer.push'), block_open_else)));
- block.body.push(
+ statements.push(
b.if(
b.binary('!==', b.member(array_id, 'length'), b.literal(0)),
b.block([open, for_loop]),
@@ -62,19 +62,15 @@ export function EachBlock(node, context) {
);
} else {
state.template.push(block_open);
- block.body.push(for_loop);
+ statements.push(for_loop);
}
- if (node.metadata.expression.is_async()) {
- state.template.push(
- create_async_block(
- block,
- node.metadata.expression.blockers(),
- node.metadata.expression.has_await
- ),
- block_close
- );
- } else {
- state.template.push(...block.body, block_close);
- }
+ state.template.push(
+ ...create_child_block(
+ statements,
+ node.metadata.expression.blockers(),
+ node.metadata.expression.has_await
+ ),
+ block_close
+ );
}
diff --git a/packages/svelte/src/compiler/phases/3-transform/server/visitors/Fragment.js b/packages/svelte/src/compiler/phases/3-transform/server/visitors/Fragment.js
index ef5bd985ae..a6ff33c7ab 100644
--- a/packages/svelte/src/compiler/phases/3-transform/server/visitors/Fragment.js
+++ b/packages/svelte/src/compiler/phases/3-transform/server/visitors/Fragment.js
@@ -28,7 +28,7 @@ export function Fragment(node, context) {
init: [],
template: [],
namespace,
- skip_hydration_boundaries: is_standalone,
+ is_standalone,
async_consts: undefined
};
diff --git a/packages/svelte/src/compiler/phases/3-transform/server/visitors/HtmlTag.js b/packages/svelte/src/compiler/phases/3-transform/server/visitors/HtmlTag.js
index 3f423fa60d..ee790c3e7c 100644
--- a/packages/svelte/src/compiler/phases/3-transform/server/visitors/HtmlTag.js
+++ b/packages/svelte/src/compiler/phases/3-transform/server/visitors/HtmlTag.js
@@ -2,25 +2,24 @@
/** @import { AST } from '#compiler' */
/** @import { ComponentContext } from '../types.js' */
import * as b from '#compiler/builders';
-import { block_close, block_open, create_push } from './shared/utils.js';
+import { create_child_block } from './shared/utils.js';
/**
* @param {AST.HtmlTag} node
* @param {ComponentContext} context
*/
export function HtmlTag(node, context) {
- const expression = /** @type {Expression} */ (context.visit(node.expression));
- const call = b.call('$.html', expression);
+ const expression = b.call('$.html', /** @type {Expression} */ (context.visit(node.expression)));
- const has_await = node.metadata.expression.has_await;
-
- if (has_await) {
- context.state.template.push(block_open);
- }
-
- context.state.template.push(create_push(call, node.metadata.expression, true));
-
- if (has_await) {
- context.state.template.push(block_close);
+ if (node.metadata.expression.is_async()) {
+ context.state.template.push(
+ ...create_child_block(
+ [b.stmt(b.call('$$renderer.push', expression))],
+ node.metadata.expression.blockers(),
+ node.metadata.expression.has_await
+ )
+ );
+ } else {
+ context.state.template.push(expression);
}
}
diff --git a/packages/svelte/src/compiler/phases/3-transform/server/visitors/IfBlock.js b/packages/svelte/src/compiler/phases/3-transform/server/visitors/IfBlock.js
index e8418343be..06b1b1e966 100644
--- a/packages/svelte/src/compiler/phases/3-transform/server/visitors/IfBlock.js
+++ b/packages/svelte/src/compiler/phases/3-transform/server/visitors/IfBlock.js
@@ -2,7 +2,7 @@
/** @import { AST } from '#compiler' */
/** @import { ComponentContext } from '../types.js' */
import * as b from '#compiler/builders';
-import { block_close, block_open, block_open_else, create_async_block } from './shared/utils.js';
+import { block_close, block_open, block_open_else, create_child_block } from './shared/utils.js';
/**
* @param {AST.IfBlock} node
@@ -23,17 +23,12 @@ export function IfBlock(node, context) {
/** @type {Statement} */
let statement = b.if(test, consequent, alternate);
- const is_async = node.metadata.expression.is_async();
-
- const has_await = node.metadata.expression.has_await;
-
- if (is_async || has_await) {
- statement = create_async_block(
- b.block([statement]),
+ context.state.template.push(
+ ...create_child_block(
+ [statement],
node.metadata.expression.blockers(),
- !!has_await
- );
- }
-
- context.state.template.push(statement, block_close);
+ node.metadata.expression.has_await
+ ),
+ block_close
+ );
}
diff --git a/packages/svelte/src/compiler/phases/3-transform/server/visitors/RegularElement.js b/packages/svelte/src/compiler/phases/3-transform/server/visitors/RegularElement.js
index 5b905752f4..1c6bb0a198 100644
--- a/packages/svelte/src/compiler/phases/3-transform/server/visitors/RegularElement.js
+++ b/packages/svelte/src/compiler/phases/3-transform/server/visitors/RegularElement.js
@@ -1,5 +1,4 @@
/** @import { Expression } from 'estree' */
-/** @import { Location } from 'locate-character' */
/** @import { AST } from '#compiler' */
/** @import { ComponentContext, ComponentServerTransformState } from '../types.js' */
/** @import { Scope } from '../../../scope.js' */
@@ -8,13 +7,7 @@ import { dev, locator } from '../../../../state.js';
import * as b from '#compiler/builders';
import { clean_nodes, determine_namespace_for_children } from '../../utils.js';
import { build_element_attributes, prepare_element_spread_object } from './shared/element.js';
-import {
- process_children,
- build_template,
- create_child_block,
- PromiseOptimiser,
- create_async_block
-} from './shared/utils.js';
+import { process_children, build_template, PromiseOptimiser } from './shared/utils.js';
import { is_customizable_select_element } from '../../../nodes.js';
/**
@@ -66,17 +59,9 @@ export function RegularElement(node, context) {
b.literal(`${node.name}>`)
);
- // TODO this is a real edge case, would be good to DRY this out
- if (optimiser.expressions.length > 0) {
- context.state.template.push(
- create_child_block(
- b.block([optimiser.apply(), ...state.init, ...build_template(state.template)])
- )
- );
- } else {
- context.state.init.push(...state.init);
- context.state.template.push(...state.template);
- }
+ context.state.template.push(
+ ...optimiser.render([...state.init, ...build_template(state.template)])
+ );
return;
}
@@ -130,13 +115,7 @@ export function RegularElement(node, context) {
const statement = b.stmt(b.call('$$renderer.select', attributes, fn, ...rest));
- if (optimiser.expressions.length > 0) {
- context.state.template.push(
- create_child_block(b.block([optimiser.apply(), ...state.init, statement]))
- );
- } else {
- context.state.template.push(...state.init, statement);
- }
+ context.state.template.push(...optimiser.render([...state.init, statement]));
return;
}
@@ -183,13 +162,7 @@ export function RegularElement(node, context) {
const statement = b.stmt(b.call('$$renderer.option', attributes, body, ...rest));
- if (optimiser.expressions.length > 0) {
- context.state.template.push(
- create_child_block(b.block([optimiser.apply(), ...state.init, statement]))
- );
- } else {
- context.state.template.push(...state.init, statement);
- }
+ context.state.template.push(...optimiser.render([...state.init, statement]));
return;
}
@@ -235,19 +208,9 @@ export function RegularElement(node, context) {
}
if (optimiser.is_async()) {
- let statements = [...state.init, ...build_template(state.template)];
-
- if (optimiser.has_await) {
- statements = [create_child_block(b.block([optimiser.apply(), ...statements]))];
- }
-
- const blockers = optimiser.blockers();
-
- if (blockers.elements.length > 0) {
- statements = [create_async_block(b.block(statements), blockers, false, false)];
- }
-
- context.state.template.push(...statements);
+ context.state.template.push(
+ ...optimiser.render([...state.init, ...build_template(state.template)])
+ );
} else {
context.state.init.push(...state.init);
context.state.template.push(...state.template);
diff --git a/packages/svelte/src/compiler/phases/3-transform/server/visitors/RenderTag.js b/packages/svelte/src/compiler/phases/3-transform/server/visitors/RenderTag.js
index 6d7cef0d95..fe20e85e1b 100644
--- a/packages/svelte/src/compiler/phases/3-transform/server/visitors/RenderTag.js
+++ b/packages/svelte/src/compiler/phases/3-transform/server/visitors/RenderTag.js
@@ -3,7 +3,7 @@
/** @import { ComponentContext } from '../types.js' */
import { unwrap_optional } from '../../../../utils/ast.js';
import * as b from '#compiler/builders';
-import { create_async_block, empty_comment, PromiseOptimiser } from './shared/utils.js';
+import { empty_comment, PromiseOptimiser } from './shared/utils.js';
/**
* @param {AST.RenderTag} node
@@ -35,17 +35,9 @@ export function RenderTag(node, context) {
)
);
- if (optimiser.is_async()) {
- statement = create_async_block(
- b.block([optimiser.apply(), statement]),
- optimiser.blockers(),
- optimiser.has_await
- );
- }
-
- context.state.template.push(statement);
+ context.state.template.push(...optimiser.render_block([statement]));
- if (!context.state.skip_hydration_boundaries) {
+ if (!context.state.is_standalone) {
context.state.template.push(empty_comment);
}
}
diff --git a/packages/svelte/src/compiler/phases/3-transform/server/visitors/SlotElement.js b/packages/svelte/src/compiler/phases/3-transform/server/visitors/SlotElement.js
index d0f8e25d02..3cebdf4541 100644
--- a/packages/svelte/src/compiler/phases/3-transform/server/visitors/SlotElement.js
+++ b/packages/svelte/src/compiler/phases/3-transform/server/visitors/SlotElement.js
@@ -5,7 +5,6 @@ import * as b from '#compiler/builders';
import {
build_attribute_value,
PromiseOptimiser,
- create_async_block,
block_open,
block_close
} from './shared/utils.js';
@@ -65,13 +64,5 @@ export function SlotElement(node, context) {
fallback
);
- const statement = optimiser.is_async()
- ? create_async_block(
- b.block([optimiser.apply(), b.stmt(slot)]),
- optimiser.blockers(),
- optimiser.has_await
- )
- : b.stmt(slot);
-
- context.state.template.push(block_open, statement, block_close);
+ context.state.template.push(block_open, ...optimiser.render_block([b.stmt(slot)]), block_close);
}
diff --git a/packages/svelte/src/compiler/phases/3-transform/server/visitors/SvelteElement.js b/packages/svelte/src/compiler/phases/3-transform/server/visitors/SvelteElement.js
index 75ba323e20..398ecc68a2 100644
--- a/packages/svelte/src/compiler/phases/3-transform/server/visitors/SvelteElement.js
+++ b/packages/svelte/src/compiler/phases/3-transform/server/visitors/SvelteElement.js
@@ -1,4 +1,3 @@
-/** @import { Location } from 'locate-character' */
/** @import { BlockStatement, Expression, Statement } from 'estree' */
/** @import { AST } from '#compiler' */
/** @import { ComponentContext } from '../types.js' */
@@ -6,12 +5,7 @@ import { dev, locator } from '../../../../state.js';
import * as b from '#compiler/builders';
import { determine_namespace_for_children } from '../../utils.js';
import { build_element_attributes } from './shared/element.js';
-import {
- build_template,
- create_async_block,
- create_child_block,
- PromiseOptimiser
-} from './shared/utils.js';
+import { build_template, create_child_block, PromiseOptimiser } from './shared/utils.js';
/**
* @param {AST.SvelteElement} node
@@ -67,36 +61,29 @@ export function SvelteElement(node, context) {
const attributes = b.block([...state.init, ...build_template(state.template)]);
const children = /** @type {BlockStatement} */ (context.visit(node.fragment, state));
- /** @type {Statement} */
- let statement = b.stmt(
- b.call(
- '$.element',
- b.id('$$renderer'),
- tag,
- attributes.body.length > 0 && b.thunk(attributes),
- children.body.length > 0 && b.thunk(children)
- )
+ statements.push(
+ ...optimiser.render([
+ b.stmt(
+ b.call(
+ '$.element',
+ b.id('$$renderer'),
+ tag,
+ attributes.body.length > 0 && b.thunk(attributes),
+ children.body.length > 0 && b.thunk(children)
+ )
+ )
+ ])
);
- if (optimiser.expressions.length > 0) {
- statement = create_child_block(b.block([optimiser.apply(), statement]));
- }
-
- statements.push(statement);
-
if (dev) {
statements.push(b.stmt(b.call('$.pop_element')));
}
- if (node.metadata.expression.is_async()) {
- statements = [
- create_async_block(
- b.block(statements),
- node.metadata.expression.blockers(),
- node.metadata.expression.has_await
- )
- ];
- }
-
- context.state.template.push(...statements);
+ context.state.template.push(
+ ...create_child_block(
+ statements,
+ node.metadata.expression.blockers(),
+ node.metadata.expression.has_await
+ )
+ );
}
diff --git a/packages/svelte/src/compiler/phases/3-transform/server/visitors/shared/component.js b/packages/svelte/src/compiler/phases/3-transform/server/visitors/shared/component.js
index cfd9a27d7b..6a2c6eb0be 100644
--- a/packages/svelte/src/compiler/phases/3-transform/server/visitors/shared/component.js
+++ b/packages/svelte/src/compiler/phases/3-transform/server/visitors/shared/component.js
@@ -1,12 +1,7 @@
/** @import { BlockStatement, Expression, Pattern, Property, SequenceExpression, Statement } from 'estree' */
/** @import { AST } from '#compiler' */
/** @import { ComponentContext } from '../../types.js' */
-import {
- empty_comment,
- build_attribute_value,
- create_async_block,
- PromiseOptimiser
-} from './utils.js';
+import { empty_comment, build_attribute_value, PromiseOptimiser } from './utils.js';
import * as b from '#compiler/builders';
import { is_element_node } from '../../../../nodes.js';
import { dev } from '../../../../../state.js';
@@ -106,10 +101,16 @@ export function build_inline_component(node, expression, context) {
}
push_prop(b.prop('init', b.key(attribute.name), value));
- } else if (attribute.type === 'BindDirective' && attribute.name !== 'this') {
+ } else if (attribute.type === 'BindDirective') {
// Bindings are a bit special: we don't want to add them to (async) deriveds but we need to check if they have blockers
optimiser.check_blockers(attribute.metadata.expression);
+ if (attribute.name === 'this') {
+ // bind:this is client-only, but we still need to check for blockers to ensure
+ // the server generates matching hydration markers if the client wraps in $.async
+ continue;
+ }
+
if (attribute.expression.type === 'SequenceExpression') {
const [get, set] = /** @type {SequenceExpression} */ (context.visit(attribute.expression))
.expressions;
@@ -325,32 +326,16 @@ export function build_inline_component(node, expression, context) {
optimiser.check_blockers(node.metadata.expression);
}
- const is_async = optimiser.is_async();
-
- if (is_async) {
- statement = create_async_block(
- b.block([
- optimiser.apply(),
- dynamic && custom_css_props.length === 0
- ? b.stmt(b.call('$$renderer.push', empty_comment))
- : b.empty,
- statement
- ]),
- optimiser.blockers(),
- optimiser.has_await
- );
- } else if (dynamic && custom_css_props.length === 0) {
- context.state.template.push(empty_comment);
- }
-
- context.state.template.push(statement);
+ context.state.template.push(
+ ...optimiser.render_block([
+ dynamic && custom_css_props.length === 0
+ ? b.stmt(b.call('$$renderer.push', empty_comment))
+ : b.empty,
+ statement
+ ])
+ );
- if (
- !is_async &&
- !context.state.skip_hydration_boundaries &&
- custom_css_props.length === 0 &&
- optimiser.expressions.length === 0
- ) {
+ if (!optimiser.is_async() && !context.state.is_standalone && custom_css_props.length === 0) {
context.state.template.push(empty_comment);
}
}
diff --git a/packages/svelte/src/compiler/phases/3-transform/server/visitors/shared/utils.js b/packages/svelte/src/compiler/phases/3-transform/server/visitors/shared/utils.js
index b02a935ed2..ee14a4d135 100644
--- a/packages/svelte/src/compiler/phases/3-transform/server/visitors/shared/utils.js
+++ b/packages/svelte/src/compiler/phases/3-transform/server/visitors/shared/utils.js
@@ -81,7 +81,19 @@ export function process_children(nodes, { visit, state }) {
flush();
const expression = /** @type {Expression} */ (visit(node.expression));
- state.template.push(create_push(b.call('$.escape', expression), node.metadata.expression));
+
+ let call = b.call(
+ '$$renderer.push',
+ b.thunk(b.call('$.escape', expression), node.metadata.expression.has_await)
+ );
+
+ const blockers = node.metadata.expression.blockers();
+
+ if (blockers.elements.length > 0) {
+ call = b.call('$$renderer.async', blockers, b.arrow([b.id('$$renderer')], call));
+ }
+
+ state.template.push(b.stmt(call));
} else if (node.type === 'Text' || node.type === 'Comment' || node.type === 'ExpressionTag') {
sequence.push(node);
} else {
@@ -262,72 +274,20 @@ export function build_getter(node, state) {
}
/**
- * Creates a `$$renderer.child(...)` expression statement
- * @param {BlockStatement | Expression} body
- * @returns {Statement}
- */
-export function create_child_block(body) {
- return b.stmt(b.call('$$renderer.child', b.arrow([b.id('$$renderer')], body, true)));
-}
-
-/**
- * Creates a `$$renderer.async(...)` expression statement
- * @param {BlockStatement | Expression} body
+ * @param {Statement[]} statements
* @param {ArrayExpression} blockers
* @param {boolean} has_await
- * @param {boolean} needs_hydration_markers
*/
-export function create_async_block(
- body,
- blockers = b.array([]),
- has_await = true,
- needs_hydration_markers = true
-) {
- return b.stmt(
- b.call(
- needs_hydration_markers ? '$$renderer.async_block' : '$$renderer.async',
- blockers,
- b.arrow([b.id('$$renderer')], body, has_await)
- )
- );
-}
-
-/**
- * @param {Expression} expression
- * @param {ExpressionMetadata} metadata
- * @param {boolean} needs_hydration_markers
- * @returns {Expression | Statement}
- */
-export function create_push(expression, metadata, needs_hydration_markers = false) {
- if (metadata.is_async()) {
- let statement = b.stmt(b.call('$$renderer.push', b.thunk(expression, metadata.has_await)));
-
- const blockers = metadata.blockers();
-
- if (blockers.elements.length > 0) {
- statement = create_async_block(
- b.block([statement]),
- blockers,
- false,
- needs_hydration_markers
- );
- }
-
- return statement;
+export function create_child_block(statements, blockers, has_await) {
+ if (blockers.elements.length === 0 && !has_await) {
+ return statements;
}
- return expression;
-}
+ const fn = b.arrow([b.id('$$renderer')], b.block(statements), has_await);
-/**
- * @param {BlockStatement | Expression} body
- * @param {Identifier | false} component_fn_id
- * @returns {Statement}
- */
-export function call_component_renderer(body, component_fn_id) {
- return b.stmt(
- b.call('$$renderer.component', b.arrow([b.id('$$renderer')], body, false), component_fn_id)
- );
+ return blockers.elements.length > 0
+ ? [b.stmt(b.call('$$renderer.async_block', blockers, fn))]
+ : [b.stmt(b.call('$$renderer.child_block', fn))];
}
/**
@@ -373,7 +333,7 @@ export class PromiseOptimiser {
}
}
- apply() {
+ #apply() {
if (this.expressions.length === 0) {
return b.empty;
}
@@ -403,4 +363,38 @@ export class PromiseOptimiser {
is_async() {
return this.expressions.length > 0 || this.#blockers.size > 0;
}
+
+ /**
+ * @param {Statement[]} statements
+ * @returns {Statement[]}
+ */
+ render(statements) {
+ if (!this.is_async()) {
+ return statements;
+ }
+
+ const fn = b.arrow(
+ [b.id('$$renderer')],
+ b.block([this.#apply(), ...statements]),
+ this.has_await
+ );
+
+ const blockers = this.blockers();
+
+ return blockers.elements.length > 0
+ ? [b.stmt(b.call('$$renderer.async', blockers, fn))]
+ : [b.stmt(b.call('$$renderer.child', fn))];
+ }
+
+ /**
+ * @param {Statement[]} statements
+ * @returns {Statement[]}
+ */
+ render_block(statements) {
+ if (!this.is_async()) {
+ return statements;
+ }
+
+ return create_child_block([this.#apply(), ...statements], this.blockers(), this.has_await);
+ }
}
diff --git a/packages/svelte/src/internal/client/dom/blocks/async.js b/packages/svelte/src/internal/client/dom/blocks/async.js
index 0e3ab33dda..e8c9cf0643 100644
--- a/packages/svelte/src/internal/client/dom/blocks/async.js
+++ b/packages/svelte/src/internal/client/dom/blocks/async.js
@@ -20,13 +20,27 @@ import { get_boundary } from './boundary.js';
*/
export function async(node, blockers = [], expressions = [], fn) {
var was_hydrating = hydrating;
+ var end = null;
if (was_hydrating) {
hydrate_next();
+ end = skip_nodes(false);
}
if (expressions.length === 0 && blockers.every((b) => b.settled)) {
fn(node);
+
+ // This is necessary because it is not guaranteed that the render function will
+ // advance the hydration node to $.async's end marker: it may stop at an inner
+ // block's end marker (in case of an inner if block for example), but it also may
+ // stop at the correct $.async end marker (in case of component child) - hence
+ // we can't just use hydrate_next()
+ // TODO this feels indicative of a bug elsewhere; ideally we wouldn't need
+ // to double-traverse in the already-resolved case
+ if (was_hydrating) {
+ set_hydrate_node(end);
+ }
+
return;
}
@@ -39,7 +53,6 @@ export function async(node, blockers = [], expressions = [], fn) {
if (was_hydrating) {
var previous_hydrate_node = hydrate_node;
- var end = skip_nodes(false);
set_hydrate_node(end);
}
diff --git a/packages/svelte/src/internal/client/dom/blocks/branches.js b/packages/svelte/src/internal/client/dom/blocks/branches.js
index 527f0b0a8f..6b77903574 100644
--- a/packages/svelte/src/internal/client/dom/blocks/branches.js
+++ b/packages/svelte/src/internal/client/dom/blocks/branches.js
@@ -200,17 +200,17 @@ export class BranchManager {
if (defer) {
for (const [k, effect] of this.#onscreen) {
if (k === key) {
- batch.skipped_effects.delete(effect);
+ batch.unskip_effect(effect);
} else {
- batch.skipped_effects.add(effect);
+ batch.skip_effect(effect);
}
}
for (const [k, branch] of this.#offscreen) {
if (k === key) {
- batch.skipped_effects.delete(branch.effect);
+ batch.unskip_effect(branch.effect);
} else {
- batch.skipped_effects.add(branch.effect);
+ batch.skip_effect(branch.effect);
}
}
diff --git a/packages/svelte/src/internal/client/dom/blocks/each.js b/packages/svelte/src/internal/client/dom/blocks/each.js
index 232656ec11..25f7cf91eb 100644
--- a/packages/svelte/src/internal/client/dom/blocks/each.js
+++ b/packages/svelte/src/internal/client/dom/blocks/each.js
@@ -40,6 +40,7 @@ import { get } from '../../runtime.js';
import { DEV } from 'esm-env';
import { derived_safe_equal } from '../../reactivity/deriveds.js';
import { current_batch } from '../../reactivity/batch.js';
+import * as e from '../../errors.js';
// When making substantive changes to this file, validate them with the each block stress test:
// https://svelte.dev/playground/1972b2cf46564476ad8c8c6405b23b7b
@@ -257,7 +258,7 @@ export function each(node, flags, get_collection, get_key, render_fn, fallback_f
if (item.i) internal_set(item.i, index);
if (defer) {
- batch.skipped_effects.delete(item.e);
+ batch.unskip_effect(item.e);
}
} else {
item = create_item(
@@ -290,6 +291,15 @@ export function each(node, flags, get_collection, get_key, render_fn, fallback_f
}
}
+ if (length > keys.size) {
+ if (DEV) {
+ validate_each_keys(array, get_key);
+ } else {
+ // in prod, the additional information isn't printed, so don't bother computing it
+ e.each_key_duplicate('', '', '');
+ }
+ }
+
// remove excess nodes
if (hydrating && length > 0) {
set_hydrate_node(skip_nodes());
@@ -299,7 +309,7 @@ export function each(node, flags, get_collection, get_key, render_fn, fallback_f
if (defer) {
for (const [key, item] of items) {
if (!keys.has(key)) {
- batch.skipped_effects.add(item.e);
+ batch.skip_effect(item.e);
}
}
@@ -676,3 +686,30 @@ function link(state, prev, next) {
next.prev = prev;
}
}
+
+/**
+ * @param {Array} array
+ * @param {(item: any, index: number) => string} key_fn
+ * @returns {void}
+ */
+function validate_each_keys(array, key_fn) {
+ const keys = new Map();
+ const length = array.length;
+
+ for (let i = 0; i < length; i++) {
+ const key = key_fn(array[i], i);
+
+ if (keys.has(key)) {
+ const a = String(keys.get(key));
+ const b = String(i);
+
+ /** @type {string | null} */
+ let k = String(key);
+ if (k.startsWith('[object ')) k = null;
+
+ e.each_key_duplicate(a, b, k);
+ }
+
+ keys.set(key, i);
+ }
+}
diff --git a/packages/svelte/src/internal/client/dom/blocks/key.js b/packages/svelte/src/internal/client/dom/blocks/key.js
index 849b1c2447..dd40d8a1b8 100644
--- a/packages/svelte/src/internal/client/dom/blocks/key.js
+++ b/packages/svelte/src/internal/client/dom/blocks/key.js
@@ -4,6 +4,8 @@ import { block } from '../../reactivity/effects.js';
import { hydrate_next, hydrating } from '../hydration.js';
import { BranchManager } from './branches.js';
+const NAN = Symbol('NaN');
+
/**
* @template V
* @param {TemplateNode} node
@@ -23,6 +25,11 @@ export function key(node, get_key, render_fn) {
block(() => {
var key = get_key();
+ // NaN !== NaN, hence we do this workaround to not trigger remounts unnecessarily
+ if (key !== key) {
+ key = /** @type {any} */ (NAN);
+ }
+
// key blocks in Svelte <5 had stupid semantics
if (legacy && key !== null && typeof key === 'object') {
key = /** @type {V} */ ({});
diff --git a/packages/svelte/src/internal/client/dom/elements/bindings/size.js b/packages/svelte/src/internal/client/dom/elements/bindings/size.js
index a76c70aab1..016ee5a547 100644
--- a/packages/svelte/src/internal/client/dom/elements/bindings/size.js
+++ b/packages/svelte/src/internal/client/dom/elements/bindings/size.js
@@ -2,9 +2,8 @@ import { effect, teardown } from '../../../reactivity/effects.js';
import { untrack } from '../../../runtime.js';
/**
- * Resize observer singleton.
- * One listener per element only!
- * https://groups.google.com/a/chromium.org/g/blink-dev/c/z6ienONUb5A/m/F5-VcUZtBAAJ
+ * We create one listener for all elements
+ * @see {@link https://groups.google.com/a/chromium.org/g/blink-dev/c/z6ienONUb5A/m/F5-VcUZtBAAJ Explanation}
*/
class ResizeObserverSingleton {
/** */
diff --git a/packages/svelte/src/internal/client/index.js b/packages/svelte/src/internal/client/index.js
index 1d9f7dfff7..7fcaf77dc5 100644
--- a/packages/svelte/src/internal/client/index.js
+++ b/packages/svelte/src/internal/client/index.js
@@ -158,7 +158,7 @@ export {
deep_read_state,
active_effect
} from './runtime.js';
-export { validate_binding, validate_each_keys } from './validate.js';
+export { validate_binding } from './validate.js';
export { raf } from './timing.js';
export { proxy } from './proxy.js';
export { create_custom_element } from './dom/elements/custom-element.js';
diff --git a/packages/svelte/src/internal/client/reactivity/batch.js b/packages/svelte/src/internal/client/reactivity/batch.js
index 2b6e84889b..9bf93c873f 100644
--- a/packages/svelte/src/internal/client/reactivity/batch.js
+++ b/packages/svelte/src/internal/client/reactivity/batch.js
@@ -130,11 +130,13 @@ export class Batch {
#maybe_dirty_effects = new Set();
/**
- * A set of branches that still exist, but will be destroyed when this batch
- * is committed — we skip over these during `process`
- * @type {Set}
+ * A map of branches that still exist, but will be destroyed when this batch
+ * is committed — we skip over these during `process`.
+ * The value contains child effects that were dirty/maybe_dirty before being reset,
+ * so they can be rescheduled if the branch survives.
+ * @type {Map}
*/
- skipped_effects = new Set();
+ #skipped_branches = new Map();
is_fork = false;
@@ -144,6 +146,38 @@ export class Batch {
return this.is_fork || this.#blocking_pending > 0;
}
+ /**
+ * Add an effect to the #skipped_branches map and reset its children
+ * @param {Effect} effect
+ */
+ skip_effect(effect) {
+ if (!this.#skipped_branches.has(effect)) {
+ this.#skipped_branches.set(effect, { d: [], m: [] });
+ }
+ }
+
+ /**
+ * Remove an effect from the #skipped_branches map and reschedule
+ * any tracked dirty/maybe_dirty child effects
+ * @param {Effect} effect
+ */
+ unskip_effect(effect) {
+ var tracked = this.#skipped_branches.get(effect);
+ if (tracked) {
+ this.#skipped_branches.delete(effect);
+
+ for (var e of tracked.d) {
+ set_signal_status(e, DIRTY);
+ schedule_effect(e);
+ }
+
+ for (e of tracked.m) {
+ set_signal_status(e, MAYBE_DIRTY);
+ schedule_effect(e);
+ }
+ }
+ }
+
/**
*
* @param {Effect[]} root_effects
@@ -172,8 +206,8 @@ export class Batch {
this.#defer_effects(render_effects);
this.#defer_effects(effects);
- for (const e of this.skipped_effects) {
- reset_branch(e);
+ for (const [e, t] of this.#skipped_branches) {
+ reset_branch(e, t);
}
} else {
// append/remove branches
@@ -220,7 +254,7 @@ export class Batch {
var is_branch = (flags & (BRANCH_EFFECT | ROOT_EFFECT)) !== 0;
var is_skippable_branch = is_branch && (flags & CLEAN) !== 0;
- var skip = is_skippable_branch || (flags & INERT) !== 0 || this.skipped_effects.has(effect);
+ var skip = is_skippable_branch || (flags & INERT) !== 0 || this.#skipped_branches.has(effect);
// Inside a `` with a pending snippet,
// all effects are deferred until the boundary resolves
@@ -613,8 +647,9 @@ function flush_effects() {
}
}
} finally {
- is_flushing = false;
+ queued_root_effects = [];
+ is_flushing = false;
last_scheduled_effect = null;
if (DEV) {
@@ -807,7 +842,8 @@ export function schedule_effect(signal) {
var flags = effect.f;
// if the effect is being scheduled because a parent (each/await/etc) block
- // updated an internal source, bail out or we'll cause a second flush
+ // updated an internal source, or because a branch is being unskipped,
+ // bail out or we'll cause a second flush
if (
is_flushing &&
effect === active_effect &&
@@ -887,20 +923,28 @@ export function eager(fn) {
/**
* Mark all the effects inside a skipped branch CLEAN, so that
- * they can be correctly rescheduled later
+ * they can be correctly rescheduled later. Tracks dirty and maybe_dirty
+ * effects so they can be rescheduled if the branch survives.
* @param {Effect} effect
+ * @param {{ d: Effect[], m: Effect[] }} tracked
*/
-function reset_branch(effect) {
+function reset_branch(effect, tracked) {
// clean branch = nothing dirty inside, no need to traverse further
if ((effect.f & BRANCH_EFFECT) !== 0 && (effect.f & CLEAN) !== 0) {
return;
}
+ if ((effect.f & DIRTY) !== 0) {
+ tracked.d.push(effect);
+ } else if ((effect.f & MAYBE_DIRTY) !== 0) {
+ tracked.m.push(effect);
+ }
+
set_signal_status(effect, CLEAN);
var e = effect.first;
while (e !== null) {
- reset_branch(e);
+ reset_branch(e, tracked);
e = e.next;
}
}
diff --git a/packages/svelte/src/internal/client/render.js b/packages/svelte/src/internal/client/render.js
index c76f0b1ce7..c09f5fdd05 100644
--- a/packages/svelte/src/internal/client/render.js
+++ b/packages/svelte/src/internal/client/render.js
@@ -208,11 +208,9 @@ function _mount(Component, { target, anchor, props = {}, events, context, intro
pending: () => {}
},
(anchor_node) => {
- if (context) {
- push({});
- var ctx = /** @type {ComponentContext} */ (component_context);
- ctx.c = context;
- }
+ push({});
+ var ctx = /** @type {ComponentContext} */ (component_context);
+ if (context) ctx.c = context;
if (events) {
// We can't spread the object or else we'd lose the state proxy stuff, if it is one
@@ -241,9 +239,7 @@ function _mount(Component, { target, anchor, props = {}, events, context, intro
}
}
- if (context) {
- pop();
- }
+ pop();
}
);
diff --git a/packages/svelte/src/internal/client/validate.js b/packages/svelte/src/internal/client/validate.js
index 48a44db304..a169225f1e 100644
--- a/packages/svelte/src/internal/client/validate.js
+++ b/packages/svelte/src/internal/client/validate.js
@@ -1,45 +1,11 @@
/** @import { Blocker } from '#client' */
import { dev_current_component_function } from './context.js';
-import { is_array } from '../shared/utils.js';
-import * as e from './errors.js';
import { FILENAME } from '../../constants.js';
import { render_effect } from './reactivity/effects.js';
import * as w from './warnings.js';
import { capture_store_binding } from './reactivity/store.js';
import { run_after_blockers } from './reactivity/async.js';
-/**
- * @param {() => any} collection
- * @param {(item: any, index: number) => string} key_fn
- * @returns {void}
- */
-export function validate_each_keys(collection, key_fn) {
- render_effect(() => {
- const keys = new Map();
- const maybe_array = collection();
- const array = is_array(maybe_array)
- ? maybe_array
- : maybe_array == null
- ? []
- : Array.from(maybe_array);
- const length = array.length;
- for (let i = 0; i < length; i++) {
- const key = key_fn(array[i], i);
- if (keys.has(key)) {
- const a = String(keys.get(key));
- const b = String(i);
-
- /** @type {string | null} */
- let k = String(key);
- if (k.startsWith('[object ')) k = null;
-
- e.each_key_duplicate(a, b, k);
- }
- keys.set(key, i);
- }
- });
-}
-
/**
* @param {string} binding
* @param {Blocker[]} blockers
diff --git a/packages/svelte/src/internal/server/crypto.js b/packages/svelte/src/internal/server/crypto.js
index 8727635481..76d66b6d1a 100644
--- a/packages/svelte/src/internal/server/crypto.js
+++ b/packages/svelte/src/internal/server/crypto.js
@@ -12,7 +12,8 @@ export async function sha256(data) {
crypto ??= globalThis.crypto?.subtle?.digest
? globalThis.crypto
: // @ts-ignore - we don't install node types in the prod build
- (await import('node:crypto')).webcrypto;
+ // don't use 'node:crypto' because static analysers will think we rely on node when we don't
+ (await import(/* @vite-ignore */ 'node:' + 'crypto')).webcrypto;
const hash_buffer = await crypto.subtle.digest('SHA-256', text_encoder.encode(data));
diff --git a/packages/svelte/src/internal/server/renderer.js b/packages/svelte/src/internal/server/renderer.js
index 65f9bcae6a..62196350bf 100644
--- a/packages/svelte/src/internal/server/renderer.js
+++ b/packages/svelte/src/internal/server/renderer.js
@@ -171,6 +171,15 @@ export class Renderer {
return promises;
}
+ /**
+ * @param {(renderer: Renderer) => MaybePromise} fn
+ */
+ child_block(fn) {
+ this.#out.push(BLOCK_OPEN);
+ this.child(fn);
+ this.#out.push(BLOCK_CLOSE);
+ }
+
/**
* Create a child renderer. The child renderer inherits the state from the parent,
* but has its own content.
@@ -617,18 +626,14 @@ export class Renderer {
renderer.push(BLOCK_OPEN);
- if (options.context) {
- push();
- /** @type {SSRContext} */ (ssr_context).c = options.context;
- /** @type {SSRContext} */ (ssr_context).r = renderer;
- }
+ push();
+ if (options.context) /** @type {SSRContext} */ (ssr_context).c = options.context;
+ /** @type {SSRContext} */ (ssr_context).r = renderer;
// @ts-expect-error
component(renderer, options.props ?? {});
- if (options.context) {
- pop();
- }
+ pop();
renderer.push(BLOCK_CLOSE);
diff --git a/packages/svelte/src/reactivity/media-query.js b/packages/svelte/src/reactivity/media-query.js
index d286709719..6d1d084298 100644
--- a/packages/svelte/src/reactivity/media-query.js
+++ b/packages/svelte/src/reactivity/media-query.js
@@ -7,7 +7,7 @@ const parenthesis_regex = /\(.+\)/;
//
// eg: new MediaQuery('screen')
//
-// however because of the auto-parenthesis logic in the constructor since there's no parentehesis
+// however because of the auto-parenthesis logic in the constructor since there's no parenthesis
// in the media query they'll be surrounded by parenthesis
//
// however we can check if the media query is only composed of these keywords
diff --git a/packages/svelte/src/version.js b/packages/svelte/src/version.js
index 557b1ff156..31538c3fe7 100644
--- a/packages/svelte/src/version.js
+++ b/packages/svelte/src/version.js
@@ -4,5 +4,5 @@
* The current version, as set in package.json.
* @type {string}
*/
-export const VERSION = '5.49.1';
+export const VERSION = '5.49.2';
export const PUBLIC_VERSION = '5';
diff --git a/packages/svelte/tests/css/samples/global-block/_config.js b/packages/svelte/tests/css/samples/global-block/_config.js
index 18a56e9a97..84b4989f76 100644
--- a/packages/svelte/tests/css/samples/global-block/_config.js
+++ b/packages/svelte/tests/css/samples/global-block/_config.js
@@ -9,12 +9,12 @@ export default test({
start: {
line: 73,
column: 1,
- character: 964
+ character: 966
},
end: {
line: 73,
column: 16,
- character: 979
+ character: 981
}
},
{
@@ -23,12 +23,12 @@ export default test({
start: {
line: 104,
column: 29,
- character: 1270
+ character: 1272
},
end: {
line: 104,
column: 43,
- character: 1284
+ character: 1286
}
}
]
diff --git a/packages/svelte/tests/css/samples/global-block/expected.css b/packages/svelte/tests/css/samples/global-block/expected.css
index be1838fd98..ef68280093 100644
--- a/packages/svelte/tests/css/samples/global-block/expected.css
+++ b/packages/svelte/tests/css/samples/global-block/expected.css
@@ -39,7 +39,7 @@
/*}*/
}
- /* ...wich is equivalent to `div :global { &.x { ...} }` ... */
+ /* ...which is equivalent to `div :global { &.x { ...} }` ... */
div.svelte-xyz {
&.x {
color: green;
@@ -51,7 +51,7 @@
color: green;
}
- /* ...and therefore `div { :global.x { ... }` aswell */
+ /* ...and therefore `div { :global.x { ... }` as well */
div.svelte-xyz {
&.x {
color: green;
diff --git a/packages/svelte/tests/css/samples/global-block/input.svelte b/packages/svelte/tests/css/samples/global-block/input.svelte
index 86d438031a..1a9993bfe0 100644
--- a/packages/svelte/tests/css/samples/global-block/input.svelte
+++ b/packages/svelte/tests/css/samples/global-block/input.svelte
@@ -41,7 +41,7 @@
}
}
- /* ...wich is equivalent to `div :global { &.x { ...} }` ... */
+ /* ...which is equivalent to `div :global { &.x { ...} }` ... */
div :global {
&.x {
color: green;
@@ -53,7 +53,7 @@
color: green;
}
- /* ...and therefore `div { :global.x { ... }` aswell */
+ /* ...and therefore `div { :global.x { ... }` as well */
div {
:global.x {
color: green;
diff --git a/packages/svelte/tests/runtime-legacy/samples/store-each-binding-logical/_config.js b/packages/svelte/tests/runtime-legacy/samples/store-each-binding-logical/_config.js
new file mode 100644
index 0000000000..1134b20d0c
--- /dev/null
+++ b/packages/svelte/tests/runtime-legacy/samples/store-each-binding-logical/_config.js
@@ -0,0 +1,22 @@
+import { flushSync } from 'svelte';
+import { ok, test } from '../../test';
+
+export default test({
+ test({ assert, target, window }) {
+ const input = target.querySelector('input');
+ ok(input);
+
+ const event = new window.Event('input');
+ input.value = 'changed';
+ input.dispatchEvent(event);
+ flushSync();
+
+ assert.htmlEqual(
+ target.innerHTML,
+ `
+
+ changed
+ `
+ );
+ }
+});
diff --git a/packages/svelte/tests/runtime-legacy/samples/store-each-binding-logical/main.svelte b/packages/svelte/tests/runtime-legacy/samples/store-each-binding-logical/main.svelte
new file mode 100644
index 0000000000..77731ea5a4
--- /dev/null
+++ b/packages/svelte/tests/runtime-legacy/samples/store-each-binding-logical/main.svelte
@@ -0,0 +1,13 @@
+
+
+{#each $items ?? [] as item}
+
+{/each}
+
+{$items[0].text}
diff --git a/packages/svelte/tests/runtime-legacy/shared.ts b/packages/svelte/tests/runtime-legacy/shared.ts
index 13975c68ee..c5317f822e 100644
--- a/packages/svelte/tests/runtime-legacy/shared.ts
+++ b/packages/svelte/tests/runtime-legacy/shared.ts
@@ -47,9 +47,9 @@ export interface RuntimeTest = Record;
/** Temporarily skip specific modes, without skipping the entire test */
skip_mode?: Array<'server' | 'async-server' | 'client' | 'hydrate'>;
- /** Skip if running with process.env.NO_ASYNC */
+ /** Skip if running with process.env.SVELTE_NO_ASYNC */
skip_no_async?: boolean;
- /** Skip if running without process.env.NO_ASYNC */
+ /** Skip if running without process.env.SVELTE_NO_ASYNC */
skip_async?: boolean;
html?: string;
ssrHtml?: string;
@@ -521,6 +521,8 @@ async function run_test_variant(
errors,
hydrate: hydrate_fn
});
+
+ flushSync();
}
if (config.runtime_error && !unhandled_rejection) {
diff --git a/packages/svelte/tests/runtime-production/samples/keyed-each-unique-2/_config.js b/packages/svelte/tests/runtime-production/samples/keyed-each-unique-2/_config.js
new file mode 100644
index 0000000000..bc945876ea
--- /dev/null
+++ b/packages/svelte/tests/runtime-production/samples/keyed-each-unique-2/_config.js
@@ -0,0 +1,12 @@
+import { flushSync } from 'svelte';
+import { test } from '../../test';
+
+export default test({
+ test({ assert, target }) {
+ let button = target.querySelector('button');
+
+ button?.click();
+
+ assert.throws(flushSync, 'https://svelte.dev/e/each_key_duplicate');
+ }
+});
diff --git a/packages/svelte/tests/runtime-production/samples/keyed-each-unique-2/main.svelte b/packages/svelte/tests/runtime-production/samples/keyed-each-unique-2/main.svelte
new file mode 100644
index 0000000000..f8ba50d866
--- /dev/null
+++ b/packages/svelte/tests/runtime-production/samples/keyed-each-unique-2/main.svelte
@@ -0,0 +1,8 @@
+
+
+ data = [1, 1, 1]}>add
+{#each data as d (d)}
+ {d}
+{/each}
diff --git a/packages/svelte/tests/runtime-production/samples/keyed-each-unique-3/_config.js b/packages/svelte/tests/runtime-production/samples/keyed-each-unique-3/_config.js
new file mode 100644
index 0000000000..7e1840200a
--- /dev/null
+++ b/packages/svelte/tests/runtime-production/samples/keyed-each-unique-3/_config.js
@@ -0,0 +1,5 @@
+import { test } from '../../test';
+
+export default test({
+ error: 'each_key_duplicate'
+});
diff --git a/packages/svelte/tests/runtime-production/samples/keyed-each-unique-3/main.svelte b/packages/svelte/tests/runtime-production/samples/keyed-each-unique-3/main.svelte
new file mode 100644
index 0000000000..a05781bcb9
--- /dev/null
+++ b/packages/svelte/tests/runtime-production/samples/keyed-each-unique-3/main.svelte
@@ -0,0 +1,7 @@
+
+
+{#each data as d (d)}
+ {d}
+{/each}
diff --git a/packages/svelte/tests/runtime-production/samples/keyed-each-unique/_config.js b/packages/svelte/tests/runtime-production/samples/keyed-each-unique/_config.js
new file mode 100644
index 0000000000..bc945876ea
--- /dev/null
+++ b/packages/svelte/tests/runtime-production/samples/keyed-each-unique/_config.js
@@ -0,0 +1,12 @@
+import { flushSync } from 'svelte';
+import { test } from '../../test';
+
+export default test({
+ test({ assert, target }) {
+ let button = target.querySelector('button');
+
+ button?.click();
+
+ assert.throws(flushSync, 'https://svelte.dev/e/each_key_duplicate');
+ }
+});
diff --git a/packages/svelte/tests/runtime-production/samples/keyed-each-unique/main.svelte b/packages/svelte/tests/runtime-production/samples/keyed-each-unique/main.svelte
new file mode 100644
index 0000000000..3d52179372
--- /dev/null
+++ b/packages/svelte/tests/runtime-production/samples/keyed-each-unique/main.svelte
@@ -0,0 +1,8 @@
+
+
+ data = [1, 1, 3, 1]}>add
+{#each data as d (d)}
+ {d}
+{/each}
diff --git a/packages/svelte/tests/runtime-runes/samples/async-attach-hydration-mismatch/Inner.svelte b/packages/svelte/tests/runtime-runes/samples/async-attach-hydration-mismatch/Inner.svelte
new file mode 100644
index 0000000000..99f885189b
--- /dev/null
+++ b/packages/svelte/tests/runtime-runes/samples/async-attach-hydration-mismatch/Inner.svelte
@@ -0,0 +1,4 @@
+
+{@render children?.()}
diff --git a/packages/svelte/tests/runtime-runes/samples/async-attach-hydration-mismatch/Outer.svelte b/packages/svelte/tests/runtime-runes/samples/async-attach-hydration-mismatch/Outer.svelte
new file mode 100644
index 0000000000..99f885189b
--- /dev/null
+++ b/packages/svelte/tests/runtime-runes/samples/async-attach-hydration-mismatch/Outer.svelte
@@ -0,0 +1,4 @@
+
+{@render children?.()}
diff --git a/packages/svelte/tests/runtime-runes/samples/async-attach-hydration-mismatch/Trigger.svelte b/packages/svelte/tests/runtime-runes/samples/async-attach-hydration-mismatch/Trigger.svelte
new file mode 100644
index 0000000000..fc434d748e
--- /dev/null
+++ b/packages/svelte/tests/runtime-runes/samples/async-attach-hydration-mismatch/Trigger.svelte
@@ -0,0 +1,7 @@
+
diff --git a/packages/svelte/tests/runtime-runes/samples/async-attach-hydration-mismatch/_config.js b/packages/svelte/tests/runtime-runes/samples/async-attach-hydration-mismatch/_config.js
new file mode 100644
index 0000000000..d77ba45ae4
--- /dev/null
+++ b/packages/svelte/tests/runtime-runes/samples/async-attach-hydration-mismatch/_config.js
@@ -0,0 +1,10 @@
+import { tick } from 'svelte';
+import { test } from '../../test';
+
+export default test({
+ mode: ['hydrate'],
+ async test({ assert, target }) {
+ await tick();
+ assert.htmlEqual(target.innerHTML, '');
+ }
+});
diff --git a/packages/svelte/tests/runtime-runes/samples/async-attach-hydration-mismatch/main.svelte b/packages/svelte/tests/runtime-runes/samples/async-attach-hydration-mismatch/main.svelte
new file mode 100644
index 0000000000..6cfc73ca25
--- /dev/null
+++ b/packages/svelte/tests/runtime-runes/samples/async-attach-hydration-mismatch/main.svelte
@@ -0,0 +1,16 @@
+
+
+
+
+ foo
+
+
+
+
diff --git a/packages/svelte/tests/runtime-runes/samples/async-derived-unmount-undefined-props/Child.svelte b/packages/svelte/tests/runtime-runes/samples/async-derived-unmount-undefined-props/Child.svelte
new file mode 100644
index 0000000000..fecbe222e3
--- /dev/null
+++ b/packages/svelte/tests/runtime-runes/samples/async-derived-unmount-undefined-props/Child.svelte
@@ -0,0 +1,9 @@
+
diff --git a/packages/svelte/tests/runtime-runes/samples/async-derived-unmount-undefined-props/_config.js b/packages/svelte/tests/runtime-runes/samples/async-derived-unmount-undefined-props/_config.js
new file mode 100644
index 0000000000..2f371bc6b7
--- /dev/null
+++ b/packages/svelte/tests/runtime-runes/samples/async-derived-unmount-undefined-props/_config.js
@@ -0,0 +1,16 @@
+import { tick } from 'svelte';
+import { test } from '../../test';
+
+export default test({
+ async test({ assert, target, logs }) {
+ await tick();
+
+ assert.deepEqual(logs, ['promise resolved with:', 'some-id']);
+
+ const button = target.querySelector('button');
+ button?.click();
+ await tick();
+
+ assert.deepEqual(logs, ['promise resolved with:', 'some-id']);
+ }
+});
diff --git a/packages/svelte/tests/runtime-runes/samples/async-derived-unmount-undefined-props/main.svelte b/packages/svelte/tests/runtime-runes/samples/async-derived-unmount-undefined-props/main.svelte
new file mode 100644
index 0000000000..d5cdd5967e
--- /dev/null
+++ b/packages/svelte/tests/runtime-runes/samples/async-derived-unmount-undefined-props/main.svelte
@@ -0,0 +1,14 @@
+
+
+{#if active}
+
+{/if}
+
+ active = undefined}>close
diff --git a/packages/svelte/tests/runtime-runes/samples/async-each-item-duplication/Component.svelte b/packages/svelte/tests/runtime-runes/samples/async-each-item-duplication/Component.svelte
new file mode 100644
index 0000000000..9f4e638629
--- /dev/null
+++ b/packages/svelte/tests/runtime-runes/samples/async-each-item-duplication/Component.svelte
@@ -0,0 +1,5 @@
+
+
+{message}
diff --git a/packages/svelte/tests/runtime-runes/samples/async-each-item-duplication/_config.js b/packages/svelte/tests/runtime-runes/samples/async-each-item-duplication/_config.js
new file mode 100644
index 0000000000..2e20f83f7f
--- /dev/null
+++ b/packages/svelte/tests/runtime-runes/samples/async-each-item-duplication/_config.js
@@ -0,0 +1,14 @@
+import { tick } from 'svelte';
+import { test } from '../../test';
+
+export default test({
+ mode: ['hydrate'],
+
+ ssrHtml: `item 1
item 2
item 3
`,
+ html: `item 1
item 2
item 3
`,
+
+ async test({ assert, target }) {
+ await tick();
+ assert.htmlEqual(target.innerHTML, 'item 1
item 2
item 3
');
+ }
+});
diff --git a/packages/svelte/tests/runtime-runes/samples/async-each-item-duplication/main.svelte b/packages/svelte/tests/runtime-runes/samples/async-each-item-duplication/main.svelte
new file mode 100644
index 0000000000..ae54b63414
--- /dev/null
+++ b/packages/svelte/tests/runtime-runes/samples/async-each-item-duplication/main.svelte
@@ -0,0 +1,10 @@
+
+
+{#each messages as message}
+
+{/each}
diff --git a/packages/svelte/tests/runtime-runes/samples/async-if-hydration/Child.svelte b/packages/svelte/tests/runtime-runes/samples/async-if-hydration/Child.svelte
new file mode 100644
index 0000000000..02ef294d99
--- /dev/null
+++ b/packages/svelte/tests/runtime-runes/samples/async-if-hydration/Child.svelte
@@ -0,0 +1,5 @@
+
+
+{b}
diff --git a/packages/svelte/tests/runtime-runes/samples/async-if-hydration/_config.js b/packages/svelte/tests/runtime-runes/samples/async-if-hydration/_config.js
new file mode 100644
index 0000000000..8132e9c522
--- /dev/null
+++ b/packages/svelte/tests/runtime-runes/samples/async-if-hydration/_config.js
@@ -0,0 +1,11 @@
+import { tick } from 'svelte';
+import { test } from '../../test';
+
+export default test({
+ mode: ['hydrate'],
+
+ async test({ assert, target }) {
+ await tick();
+ assert.htmlEqual(target.innerHTML, ` true
`);
+ }
+});
diff --git a/packages/svelte/tests/runtime-runes/samples/async-if-hydration/main.svelte b/packages/svelte/tests/runtime-runes/samples/async-if-hydration/main.svelte
new file mode 100644
index 0000000000..3b08d41640
--- /dev/null
+++ b/packages/svelte/tests/runtime-runes/samples/async-if-hydration/main.svelte
@@ -0,0 +1,18 @@
+
+
+{#if a}
+
+ {#if b}
+
hello
+ {/if}
+
+
+
+
+{/if}
diff --git a/packages/svelte/tests/runtime-runes/samples/create-context-programmatic/Child.svelte b/packages/svelte/tests/runtime-runes/samples/create-context-programmatic/Child.svelte
new file mode 100644
index 0000000000..3e39d5043e
--- /dev/null
+++ b/packages/svelte/tests/runtime-runes/samples/create-context-programmatic/Child.svelte
@@ -0,0 +1,7 @@
+
+
+{message}
diff --git a/packages/svelte/tests/runtime-runes/samples/create-context-programmatic/_config.js b/packages/svelte/tests/runtime-runes/samples/create-context-programmatic/_config.js
new file mode 100644
index 0000000000..f4374c8759
--- /dev/null
+++ b/packages/svelte/tests/runtime-runes/samples/create-context-programmatic/_config.js
@@ -0,0 +1,8 @@
+import { test } from '../../test';
+
+export default test({
+ ssrHtml: `
`,
+ html: `
hello `,
+
+ test() {}
+});
diff --git a/packages/svelte/tests/runtime-runes/samples/create-context-programmatic/main.svelte b/packages/svelte/tests/runtime-runes/samples/create-context-programmatic/main.svelte
new file mode 100644
index 0000000000..3c42dbf180
--- /dev/null
+++ b/packages/svelte/tests/runtime-runes/samples/create-context-programmatic/main.svelte
@@ -0,0 +1,20 @@
+
+
+ {
+ mount(Wrapper(Child), { target });
+}}>
diff --git a/packages/svelte/tests/runtime-runes/samples/effect-loop-infinite/_config.js b/packages/svelte/tests/runtime-runes/samples/effect-loop-infinite/_config.js
index 57f60c2b44..44cf5b09e2 100644
--- a/packages/svelte/tests/runtime-runes/samples/effect-loop-infinite/_config.js
+++ b/packages/svelte/tests/runtime-runes/samples/effect-loop-infinite/_config.js
@@ -11,11 +11,12 @@ export default test({
test({ assert, errors }) {
const [button] = document.querySelectorAll('button');
- try {
+ assert.throws(() => {
flushSync(() => button.click());
- } catch (e) {
- assert.equal(errors.length, 1); // for whatever reason we can't get the name which should be 'updated at'
- assert.ok(/** @type {Error} */ (e).message.startsWith('effect_update_depth_exceeded'));
- }
+ }, /effect_update_depth_exceeded/);
+
+ assert.equal(errors.length, 1);
+
+ assert.doesNotThrow(flushSync);
}
});
diff --git a/packages/svelte/tests/runtime-runes/samples/error-boundary-12/main.svelte b/packages/svelte/tests/runtime-runes/samples/error-boundary-12/main.svelte
index d9dee1e2b0..748acab91e 100644
--- a/packages/svelte/tests/runtime-runes/samples/error-boundary-12/main.svelte
+++ b/packages/svelte/tests/runtime-runes/samples/error-boundary-12/main.svelte
@@ -15,6 +15,6 @@
{d}
{#snippet failed()}
- Error occured
+ Error occurred
{/snippet}
diff --git a/packages/svelte/tests/runtime-runes/samples/key-nan/_config.js b/packages/svelte/tests/runtime-runes/samples/key-nan/_config.js
new file mode 100644
index 0000000000..bd0dd06761
--- /dev/null
+++ b/packages/svelte/tests/runtime-runes/samples/key-nan/_config.js
@@ -0,0 +1,17 @@
+import { flushSync } from 'svelte';
+import { test } from '../../test';
+
+export default test({
+ html: 'update it rendered
',
+
+ test({ assert, target, logs }) {
+ assert.deepEqual(logs, ['rendering']);
+
+ const btn = target.querySelector('button');
+ flushSync(() => btn?.click());
+
+ // should not re-render
+ assert.deepEqual(logs, ['rendering']);
+ assert.htmlEqual(target.innerHTML, 'update it rendered
');
+ }
+});
diff --git a/packages/svelte/tests/runtime-runes/samples/key-nan/main.svelte b/packages/svelte/tests/runtime-runes/samples/key-nan/main.svelte
new file mode 100644
index 0000000000..24f7f2d179
--- /dev/null
+++ b/packages/svelte/tests/runtime-runes/samples/key-nan/main.svelte
@@ -0,0 +1,10 @@
+
+
+ x = NaN}>update
+
+{#key x}
+ {console.log('rendering')}
+ it rendered
+{/key}
diff --git a/packages/svelte/tests/runtime-runes/samples/transition-component/_config.js b/packages/svelte/tests/runtime-runes/samples/transition-component/_config.js
index 414a8b0cc2..1bdf3a5a2e 100644
--- a/packages/svelte/tests/runtime-runes/samples/transition-component/_config.js
+++ b/packages/svelte/tests/runtime-runes/samples/transition-component/_config.js
@@ -3,7 +3,7 @@ import { test } from '../../test';
/**
* $.component() should not break transition
- * https://github.com/sveltejs/svelte/issues/13645
+ * @see {@link https://github.com/sveltejs/svelte/issues/13645}
*/
export default test({
test({ assert, raf, target }) {
diff --git a/packages/svelte/tests/signals/test.ts b/packages/svelte/tests/signals/test.ts
index 23c4bb42f9..5486ccdb45 100644
--- a/packages/svelte/tests/signals/test.ts
+++ b/packages/svelte/tests/signals/test.ts
@@ -21,7 +21,7 @@ import { disable_async_mode_flag, enable_async_mode_flag } from '../../src/inter
/**
* @param runes runes mode
- * @param fn A function that returns a function because we first need to setup all the signals
+ * @param fn A function that returns a function because we first need to set up all the signals
* and then execute the test in order to simulate a real component
*/
function run_test(runes: boolean, fn: (runes: boolean) => () => void) {
diff --git a/packages/svelte/tests/snapshot/samples/async-const/_expected/server/index.svelte.js b/packages/svelte/tests/snapshot/samples/async-const/_expected/server/index.svelte.js
index a726b903bc..03bbc5ba88 100644
--- a/packages/svelte/tests/snapshot/samples/async-const/_expected/server/index.svelte.js
+++ b/packages/svelte/tests/snapshot/samples/async-const/_expected/server/index.svelte.js
@@ -19,11 +19,7 @@ export default function Async_const($$renderer) {
]);
$$renderer.push(``);
-
- $$renderer.async([promises[1]], ($$renderer) => {
- $$renderer.push(() => $.escape(b));
- });
-
+ $$renderer.async([promises[1]], ($$renderer) => $$renderer.push(() => $.escape(b)));
$$renderer.push(`
`);
} else {
$$renderer.push('');
diff --git a/packages/svelte/tests/snapshot/samples/async-each-fallback-hoisting/_expected/server/index.svelte.js b/packages/svelte/tests/snapshot/samples/async-each-fallback-hoisting/_expected/server/index.svelte.js
index 7249fd6e4f..b331875d04 100644
--- a/packages/svelte/tests/snapshot/samples/async-each-fallback-hoisting/_expected/server/index.svelte.js
+++ b/packages/svelte/tests/snapshot/samples/async-each-fallback-hoisting/_expected/server/index.svelte.js
@@ -2,7 +2,7 @@ import 'svelte/internal/flags/async';
import * as $ from 'svelte/internal/server';
export default function Async_each_fallback_hoisting($$renderer) {
- $$renderer.async_block([], async ($$renderer) => {
+ $$renderer.child_block(async ($$renderer) => {
const each_array = $.ensure_array_like((await $.save(Promise.resolve([])))());
if (each_array.length !== 0) {
diff --git a/packages/svelte/tests/snapshot/samples/async-each-hoisting/_expected/server/index.svelte.js b/packages/svelte/tests/snapshot/samples/async-each-hoisting/_expected/server/index.svelte.js
index 43fe9414eb..86948b4a3a 100644
--- a/packages/svelte/tests/snapshot/samples/async-each-hoisting/_expected/server/index.svelte.js
+++ b/packages/svelte/tests/snapshot/samples/async-each-hoisting/_expected/server/index.svelte.js
@@ -8,7 +8,7 @@ export default function Async_each_hoisting($$renderer) {
$$renderer.push(``);
- $$renderer.async_block([], async ($$renderer) => {
+ $$renderer.child_block(async ($$renderer) => {
const each_array = $.ensure_array_like((await $.save(Promise.resolve([first, second, third])))());
for (let $$index = 0, $$length = each_array.length; $$index < $$length; $$index++) {
diff --git a/packages/svelte/tests/snapshot/samples/async-if-alternate-hoisting/_expected/server/index.svelte.js b/packages/svelte/tests/snapshot/samples/async-if-alternate-hoisting/_expected/server/index.svelte.js
index 1e7330429a..c69c038973 100644
--- a/packages/svelte/tests/snapshot/samples/async-if-alternate-hoisting/_expected/server/index.svelte.js
+++ b/packages/svelte/tests/snapshot/samples/async-if-alternate-hoisting/_expected/server/index.svelte.js
@@ -2,7 +2,7 @@ import 'svelte/internal/flags/async';
import * as $ from 'svelte/internal/server';
export default function Async_if_alternate_hoisting($$renderer) {
- $$renderer.async_block([], async ($$renderer) => {
+ $$renderer.child_block(async ($$renderer) => {
if ((await $.save(Promise.resolve(false)))()) {
$$renderer.push('');
$$renderer.push(async () => $.escape(await Promise.reject('no no no')));
diff --git a/packages/svelte/tests/snapshot/samples/async-if-hoisting/_expected/server/index.svelte.js b/packages/svelte/tests/snapshot/samples/async-if-hoisting/_expected/server/index.svelte.js
index 1ca24cf81a..1355ba34f0 100644
--- a/packages/svelte/tests/snapshot/samples/async-if-hoisting/_expected/server/index.svelte.js
+++ b/packages/svelte/tests/snapshot/samples/async-if-hoisting/_expected/server/index.svelte.js
@@ -2,7 +2,7 @@ import 'svelte/internal/flags/async';
import * as $ from 'svelte/internal/server';
export default function Async_if_hoisting($$renderer) {
- $$renderer.async_block([], async ($$renderer) => {
+ $$renderer.child_block(async ($$renderer) => {
if ((await $.save(Promise.resolve(true)))()) {
$$renderer.push('');
$$renderer.push(async () => $.escape(await Promise.resolve('yes yes yes')));
diff --git a/packages/svelte/tests/snapshot/samples/async-top-level-inspect-server/_expected/server/index.svelte.js b/packages/svelte/tests/snapshot/samples/async-top-level-inspect-server/_expected/server/index.svelte.js
index eb99a2fe1d..cff5f2d569 100644
--- a/packages/svelte/tests/snapshot/samples/async-top-level-inspect-server/_expected/server/index.svelte.js
+++ b/packages/svelte/tests/snapshot/samples/async-top-level-inspect-server/_expected/server/index.svelte.js
@@ -6,10 +6,6 @@ export default function Async_top_level_inspect_server($$renderer) {
var $$promises = $$renderer.run([async () => data = await Promise.resolve(42),,]);
$$renderer.push(``);
-
- $$renderer.async([$$promises[1]], ($$renderer) => {
- $$renderer.push(() => $.escape(data));
- });
-
+ $$renderer.async([$$promises[1]], ($$renderer) => $$renderer.push(() => $.escape(data)));
$$renderer.push(`
`);
}
\ No newline at end of file
diff --git a/packages/svelte/tests/sourcemaps/test.ts b/packages/svelte/tests/sourcemaps/test.ts
index 0ac0e6f905..6749483079 100644
--- a/packages/svelte/tests/sourcemaps/test.ts
+++ b/packages/svelte/tests/sourcemaps/test.ts
@@ -8,9 +8,9 @@ import { decode } from '@jridgewell/sourcemap-codec';
type SourceMapEntry =
| string
| {
- /** If not the first occurence, but the nth should be found */
+ /** If not the first occurrence, but the nth should be found */
idxOriginal?: number;
- /** If not the first occurence, but the nth should be found */
+ /** If not the first occurrence, but the nth should be found */
idxGenerated?: number;
/** The original string to find */
str: string;
diff --git a/packages/svelte/tests/validator/samples/a11y-no-noninteractive-element-to-interactive-role/input.svelte b/packages/svelte/tests/validator/samples/a11y-no-noninteractive-element-to-interactive-role/input.svelte
index e5db8719b0..edfa3eeada 100644
--- a/packages/svelte/tests/validator/samples/a11y-no-noninteractive-element-to-interactive-role/input.svelte
+++ b/packages/svelte/tests/validator/samples/a11y-no-noninteractive-element-to-interactive-role/input.svelte
@@ -69,6 +69,13 @@
+
+
+
+
+
+
+
diff --git a/packages/svelte/tests/validator/samples/a11y-no-redundant-roles/input.svelte b/packages/svelte/tests/validator/samples/a11y-no-redundant-roles/input.svelte
index 2ccd765aa2..9e06ebe420 100644
--- a/packages/svelte/tests/validator/samples/a11y-no-redundant-roles/input.svelte
+++ b/packages/svelte/tests/validator/samples/a11y-no-redundant-roles/input.svelte
@@ -22,7 +22,7 @@
-
+
@@ -44,5 +44,5 @@
-
+
diff --git a/packages/svelte/tests/validator/samples/a11y-no-redundant-roles/warnings.json b/packages/svelte/tests/validator/samples/a11y-no-redundant-roles/warnings.json
index 861f000aef..e041037d71 100644
--- a/packages/svelte/tests/validator/samples/a11y-no-redundant-roles/warnings.json
+++ b/packages/svelte/tests/validator/samples/a11y-no-redundant-roles/warnings.json
@@ -275,18 +275,6 @@
"line": 24
}
},
- {
- "code": "a11y_no_redundant_roles",
- "end": {
- "column": 17,
- "line": 25
- },
- "message": "Redundant role 'list'",
- "start": {
- "column": 6,
- "line": 25
- }
- },
{
"code": "a11y_no_redundant_roles",
"end": {
diff --git a/packages/svelte/types/index.d.ts b/packages/svelte/types/index.d.ts
index 730e0ff655..62c0e210be 100644
--- a/packages/svelte/types/index.d.ts
+++ b/packages/svelte/types/index.d.ts
@@ -3188,7 +3188,7 @@ declare module 'svelte/types/compiler/interfaces' {
* let count = $state(0);
* ```
*
- * https://svelte.dev/docs/svelte/$state
+ * @see {@link https://svelte.dev/docs/svelte/$state Documentation}
*
* @param initial The initial value
*/
@@ -3298,7 +3298,7 @@ declare namespace $state {
*
* ```
*
- * https://svelte.dev/docs/svelte/$state#$state.raw
+ * @see {@link https://svelte.dev/docs/svelte/$state#$state.raw Documentation}
*
* @param initial The initial value
*/
@@ -3319,7 +3319,7 @@ declare namespace $state {
*
* ```
*
- * https://svelte.dev/docs/svelte/$state#$state.snapshot
+ * @see {@link https://svelte.dev/docs/svelte/$state#$state.snapshot Documentation}
*
* @param state The value to snapshot
*/
@@ -3359,7 +3359,7 @@ declare namespace $state {
* let double = $derived(count * 2);
* ```
*
- * https://svelte.dev/docs/svelte/$derived
+ * @see {@link https://svelte.dev/docs/svelte/$derived Documentation}
*
* @param expression The derived state expression
*/
@@ -3381,7 +3381,7 @@ declare namespace $derived {
* });
* ```
*
- * https://svelte.dev/docs/svelte/$derived#$derived.by
+ * @see {@link https://svelte.dev/docs/svelte/$derived#$derived.by Documentation}
*/
export function by(fn: () => T): T;
@@ -3423,7 +3423,7 @@ declare namespace $derived {
*
* Does not run during server-side rendering.
*
- * https://svelte.dev/docs/svelte/$effect
+ * @see {@link https://svelte.dev/docs/svelte/$effect Documentation}
* @param fn The function to execute
*/
declare function $effect(fn: () => void | (() => void)): void;
@@ -3442,7 +3442,7 @@ declare namespace $effect {
*
* Does not run during server-side rendering.
*
- * https://svelte.dev/docs/svelte/$effect#$effect.pre
+ * @see {@link https://svelte.dev/docs/svelte/$effect#$effect.pre Documentation}
* @param fn The function to execute
*/
export function pre(fn: () => void | (() => void)): void;
@@ -3450,7 +3450,7 @@ declare namespace $effect {
/**
* Returns the number of promises that are pending in the current boundary, not including child boundaries.
*
- * https://svelte.dev/docs/svelte/$effect#$effect.pending
+ * @see {@link https://svelte.dev/docs/svelte/$effect#$effect.pending Documentation}
*/
export function pending(): number;
@@ -3472,7 +3472,7 @@ declare namespace $effect {
*
* This allows you to (for example) add things like subscriptions without causing memory leaks, by putting them in child effects.
*
- * https://svelte.dev/docs/svelte/$effect#$effect.tracking
+ * @see {@link https://svelte.dev/docs/svelte/$effect#$effect.tracking Documentation}
*/
export function tracking(): boolean;
@@ -3500,7 +3500,7 @@ declare namespace $effect {
* cleanup()}>cleanup
* ```
*
- * https://svelte.dev/docs/svelte/$effect#$effect.root
+ * @see {@link https://svelte.dev/docs/svelte/$effect#$effect.root Documentation}
*/
export function root(fn: () => void | (() => void)): () => void;
@@ -3536,7 +3536,7 @@ declare namespace $effect {
* let { optionalProp = 42, requiredProp, bindableProp = $bindable() }: { optionalProp?: number; requiredProps: string; bindableProp: boolean } = $props();
* ```
*
- * https://svelte.dev/docs/svelte/$props
+ * @see {@link https://svelte.dev/docs/svelte/$props Documentation}
*/
declare function $props(): any;
@@ -3582,7 +3582,7 @@ declare namespace $props {
* let { propName = $bindable() }: { propName: boolean } = $props();
* ```
*
- * https://svelte.dev/docs/svelte/$bindable
+ * @see {@link https://svelte.dev/docs/svelte/$bindable Documentation}
*/
declare function $bindable(fallback?: T): T;
@@ -3628,7 +3628,7 @@ declare namespace $bindable {
* $inspect(x, y).with(() => { debugger; });
* ```
*
- * https://svelte.dev/docs/svelte/$inspect
+ * @see {@link https://svelte.dev/docs/svelte/$inspect Documentation}
*/
declare function $inspect(
...values: T
@@ -3694,7 +3694,7 @@ declare namespace $inspect {
*
* Only available inside custom element components, and only on the client-side.
*
- * https://svelte.dev/docs/svelte/$host
+ * @see {@link https://svelte.dev/docs/svelte/$host Documentation}
*/
declare function $host(): El;
diff --git a/playgrounds/sandbox/scripts/download.js b/playgrounds/sandbox/scripts/download.js
index e3c862cb61..538b4956fe 100644
--- a/playgrounds/sandbox/scripts/download.js
+++ b/playgrounds/sandbox/scripts/download.js
@@ -635,7 +635,19 @@ if (is_local) {
} else if (url && url.origin === 'https://svelte.dev' && url.pathname.startsWith('/playground/')) {
// Svelte playground URL handling (existing logic)
if (url.hash.length > 1) {
- const decoded = atob(url.hash.slice(1).replaceAll('-', '+').replaceAll('_', '/'));
+ // Decode percent-encoded characters (e.g., %5F => _), and replace base64 chars.
+ let decoded;
+ try {
+ // First, decode URI components to handle %xx encodings (e.g. %5F -> _) (LLMs calling this script sometimes encode them for some reason)
+ decoded = url.hash.slice(1);
+ decoded = decodeURIComponent(decoded);
+
+ // Now, restore for base64 (replace -/+, _/ /)
+ decoded = atob(decoded.replaceAll('-', '+').replaceAll('_', '/'));
+ } catch (e) {
+ console.error('Failed to decode URL hash:', e);
+ process.exit(1);
+ }
// putting it directly into the blob gives a corrupted file
const u8 = new Uint8Array(decoded.length);
for (let i = 0; i < decoded.length; i++) {