Merge branch 'main' into each-block-pending

each-block-pending
Rich Harris 2 weeks ago
commit 3c7388218b

@ -1,5 +0,0 @@
---
'svelte': minor
---
feat: allow passing `ShadowRootInit` object to custom element `shadow` option

@ -0,0 +1,5 @@
---
'svelte': patch
---
fix: use symbols for encapsulated event delegation

@ -86,7 +86,7 @@ jobs:
- uses: pnpm/action-setup@v4
- uses: actions/setup-node@v6
with:
node-version: 18
node-version: 24
cache: pnpm
- name: install
run: pnpm install --frozen-lockfile

@ -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 = `<!-- pkg.pr.new comment -->`;
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();
}
}

@ -1,16 +1,36 @@
name: Publish Any Commit
on: [push, pull_request]
name: pkg.pr.new
on:
pull_request_target:
types: [opened, synchronize]
push:
branches: [main]
workflow_dispatch:
inputs:
sha:
description: 'Commit SHA to build'
required: true
type: string
permissions: {}
jobs:
build:
permissions: {}
# Skip pull_request_target events from forks — maintainers can use workflow_dispatch instead
if: >
github.event_name != 'pull_request_target' ||
github.event.pull_request.head.repo.full_name == github.repository
runs-on: ubuntu-latest
# No permissions — this job runs user-controlled code
permissions: {}
steps:
- uses: actions/checkout@v6
with:
# For pull_request_target, check out the PR head.
# For workflow_dispatch, check out the manually specified SHA.
# For push, fall back to the push SHA.
ref: ${{ github.event.pull_request.head.sha || inputs.sha || github.sha }}
- uses: pnpm/action-setup@v4
- uses: actions/setup-node@v6
with:
@ -24,21 +44,192 @@ 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' || github.event_name == 'workflow_dispatch'
runs-on: ubuntu-latest
permissions:
contents: read
pull-requests: write
steps:
- name: Download sanitized artifact
uses: actions/download-artifact@v7
with:
name: sanitized-output
- name: Resolve PR number
id: pr
uses: actions/github-script@v8
with:
script: |
if (context.eventName === 'pull_request_target') {
core.setOutput('number', context.issue.number);
return;
}
const { data: pulls } = await github.rest.repos.listPullRequestsAssociatedWithCommit({
owner: context.repo.owner,
repo: context.repo.repo,
commit_sha: '${{ inputs.sha }}',
});
const open = pulls.filter(p => p.state === 'open');
if (open.length === 0) {
core.setFailed(`No open PR found for commit ${{ inputs.sha }}`);
return;
}
if (open.length > 1) {
const nums = open.map(p => `#${p.number}`).join(', ');
core.setFailed(`Multiple open PRs found for commit ${{ inputs.sha }}: ${nums}`);
return;
}
core.setOutput('number', open[0].number);
- 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;
}
const issue_number = parseInt('${{ steps.pr.outputs.number }}', 10);
const bot_comment_identifier = `<!-- pkg.pr.new comment -->`;
const body = `${bot_comment_identifier}
[Playground](https://svelte.dev/playground?version=pr-${issue_number})
\`\`\`
${packages.map(p => `pnpm add https://pkg.pr.new/${p.name}@${issue_number}`).join('\n')}
\`\`\`
`;
const comments = await github.rest.issues.listComments({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number,
});
const existing = comments.data.find(c => c.body.includes(bot_comment_identifier));
if (existing) {
await github.rest.issues.updateComment({
owner: context.repo.owner,
repo: context.repo.repo,
comment_id: existing.id,
body,
});
} else {
await github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number,
body,
});
}
log:
needs: sanitize
if: github.event_name == 'push'
runs-on: ubuntu-latest
permissions: {}
steps:
- name: Download sanitized artifact
uses: actions/download-artifact@v7
with:
name: 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));

@ -1,5 +1,6 @@
---
title: $state
tags: rune-state
---
The `$state` rune allows you to create _reactive state_, which means that your UI _reacts_ when it changes.

@ -1,5 +1,6 @@
---
title: $derived
tags: rune-derived
---
Derived state is declared with the `$derived` rune:

@ -1,5 +1,6 @@
---
title: $effect
tags: rune-effect
---
Effects are functions that run when state updates, and can be used for things like calling third-party libraries, drawing on `<canvas>` elements, or making network requests. They only run in the browser, not during server-side rendering.

@ -1,5 +1,6 @@
---
title: $props
tags: rune-props
---
The inputs to a component are referred to as _props_, which is short for _properties_. You pass props to components just like you pass attributes to elements:

@ -1,5 +1,6 @@
---
title: $inspect
tags: rune-inspect
---
> [!NOTE] `$inspect` only works during development. In a production build it becomes a noop.

@ -1,5 +1,6 @@
---
title: {#if ...}
tags: template-if
---
```svelte

@ -1,5 +1,6 @@
---
title: {#each ...}
tags: template-each
---
```svelte
@ -12,7 +13,7 @@ title: {#each ...}
{#each expression as name, index}...{/each}
```
Iterating over values can be done with an each block. The values in question can be arrays, array-like objects (i.e. anything with a `length` property), or iterables like `Map` and `Set`— in other words, anything that can be used with `Array.from`.
Iterating over values can be done with an each block. The values in question can be arrays, array-like objects (i.e. anything with a `length` property), or iterables like `Map` and `Set`. (Internally, they are converted to arrays with [`Array.from`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/from).)
If the value is `null` or `undefined`, it is treated the same as an empty array (which will cause [else blocks](#Else-blocks) to be rendered, where applicable).

@ -1,5 +1,6 @@
---
title: {#key ...}
tags: template-key
---
```svelte

@ -1,5 +1,6 @@
---
title: {#await ...}
tags: template-await
---
```svelte

@ -1,5 +1,6 @@
---
title: {@html ...}
tags: template-html
---
To inject raw HTML into your component, use the `{@html ...}` tag:

@ -1,5 +1,6 @@
---
title: {@attach ...}
tags: attachments
---
Attachments are functions that run in an [effect]($effect) when an element is mounted to the DOM or when [state]($state) read inside the function updates.
@ -82,6 +83,14 @@ Attachments can also be created inline ([demo](/playground/untitled#H4sIAAAAAAAA
> [!NOTE]
> The nested effect runs whenever `color` changes, while the outer effect (where `canvas.getContext(...)` is called) only runs once, since it doesn't read any reactive state.
## Conditional attachments
Falsy values like `false` or `undefined` are treated as no attachment, enabling conditional usage:
```svelte
<div {@attach enabled && myAttachment}>...</div>
```
## Passing attachments to components
When used on a component, `{@attach ...}` will create a prop whose key is a [`Symbol`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Symbol). If the component then [spreads](/tutorial/svelte/spread-props) props onto an element, the element will receive those attachments.

@ -1,5 +1,6 @@
---
title: transition:
tags: transitions
---
A _transition_ is triggered by an element entering or leaving the DOM as a result of a state change.

@ -1,5 +1,6 @@
---
title: in: and out:
tags: transitions
---
The `in:` and `out:` directives are identical to [`transition:`](transition), except that the resulting transitions are not bidirectional — an `in` transition will continue to 'play' alongside the `out` transition, rather than reversing, if the block is outroed while the transition is in progress. If an out transition is aborted, transitions will restart from scratch.
@ -7,7 +8,7 @@ The `in:` and `out:` directives are identical to [`transition:`](transition), ex
```svelte
<script>
import { fade, fly } from 'svelte/transition';
let visible = $state(false);
</script>

@ -1,5 +1,6 @@
---
title: style:
tags: template-style
---
The `style:` directive provides a shorthand for setting multiple styles on an element.

@ -1,5 +1,6 @@
---
title: class
tags: template-style
---
There are two ways to set classes on elements: the `class` attribute, and the `class:` directive.

@ -1,5 +1,6 @@
---
title: Scoped styles
tags: styles-scoped
---
Svelte components can include a `<style>` element containing CSS that belongs to the component. This CSS is _scoped_ by default, meaning that styles will not apply to any elements on the page outside the component in question.

@ -1,5 +1,6 @@
---
title: Global styles
tags: styles-global
---
## :global(...)

@ -1,5 +1,6 @@
---
title: Custom properties
tags: styles-custom-properties
---
You can pass CSS custom properties — both static and dynamic — to components:

@ -97,6 +97,32 @@ import { createContext } from 'svelte';
export const [getUserContext, setUserContext] = createContext<User>();
```
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('<h1>Hello Bob!</h1>');
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:

@ -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'

@ -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).

@ -91,7 +91,7 @@ Some resources for getting started with testing:
## Is there a router?
The official routing library is [SvelteKit](/docs/kit). SvelteKit provides a filesystem router, server-side rendering (SSR), and hot module reloading (HMR) in one easy-to-use package. It shares similarities with Next.js for React and Nuxt.js for Vue.
The official routing library is [SvelteKit](/docs/kit). SvelteKit provides a filesystem router, server-side rendering (SSR), and hot module reloading (HMR) in one easy-to-use package. It shares similarities with Next.js for React and Nuxt.js for Vue. SvelteKit also supports hash-based routing for client-side applications.
However, you can use any router library. A sampling of available routers are highlighted [on the packages page](/packages#routing).

@ -1,5 +1,6 @@
---
title: svelte/attachments
tags: attachments
---
> MODULE: svelte/attachments

@ -1,5 +1,6 @@
---
title: svelte/transition
tags: transitions
---
> MODULE: svelte/transition

@ -33,7 +33,7 @@ const no_compiler_imports = {
}
};
/** @type {import('eslint').Linter.FlatConfig[]} */
/** @type {import('eslint').Linter.Config[]} */
export default [
...svelte_config,
{
@ -102,6 +102,7 @@ export default [
'playgrounds/sandbox/**',
// exclude top level config files
'*.config.js',
'vitest-xhtml-environment.ts',
// documentation can contain invalid examples
'documentation',
'tmp/**'

@ -27,22 +27,30 @@
},
"devDependencies": {
"@changesets/cli": "^2.29.8",
"@sveltejs/eslint-config": "^8.3.3",
"@sveltejs/eslint-config": "^8.3.5",
"@svitejs/changesets-changelog-github-compact": "^1.1.0",
"@types/node": "^20.11.5",
"@types/picomatch": "^4.0.2",
"@vitest/coverage-v8": "^2.1.9",
"eslint": "^9.9.1",
"eslint-plugin-lube": "^0.4.3",
"eslint-plugin-svelte": "^3.11.0",
"@eslint/js": "^10.0.0",
"eslint": "^10.0.0",
"eslint-plugin-lube": "^0.5.1",
"eslint-plugin-svelte": "^3.15.0",
"jsdom": "25.0.1",
"playwright": "^1.46.1",
"playwright": "^1.58.0",
"prettier": "^3.2.4",
"prettier-plugin-svelte": "^3.4.0",
"svelte": "workspace:^",
"typescript": "^5.5.4",
"typescript-eslint": "^8.48.1",
"typescript-eslint": "^8.55.0",
"v8-natives": "^1.2.5",
"vitest": "^2.1.9"
},
"pnpm": {
"peerDependencyRules": {
"allowedVersions": {
"eslint": "10"
}
}
}
}

@ -1,5 +1,127 @@
# svelte
## 5.51.0
### Minor Changes
- feat: Use `TrustedTypes` for HTML handling where supported ([#16271](https://github.com/sveltejs/svelte/pull/16271))
### Patch Changes
- fix: sanitize template-literal-special-characters in SSR attribute values ([#17692](https://github.com/sveltejs/svelte/pull/17692))
- fix: follow-up formatting in `print()` — flush block-level elements into separate sequences ([#17699](https://github.com/sveltejs/svelte/pull/17699))
- fix: preserve delegated event handlers as long as one or more root components are using them ([#17695](https://github.com/sveltejs/svelte/pull/17695))
## 5.50.3
### Patch Changes
- fix: take into account `nodeName` case sensitivity on XHTML pages ([#17689](https://github.com/sveltejs/svelte/pull/17689))
- fix: render `multiple` and `selected` attributes as empty strings for XHTML compliance ([#17689](https://github.com/sveltejs/svelte/pull/17689))
- fix: always lowercase HTML elements, for XHTML compliance ([#17664](https://github.com/sveltejs/svelte/pull/17664))
- fix: freeze effects-inside-deriveds when disconnecting, unfreeze on reconnect ([#17682](https://github.com/sveltejs/svelte/pull/17682))
- fix: propagate `$effect` errors to `<svelte:boundary>` ([#17684](https://github.com/sveltejs/svelte/pull/17684))
## 5.50.2
### Patch Changes
- fix: resolve `effect_update_depth_exceeded` when using `bind:value` on `<select>` with derived state in legacy mode ([#17645](https://github.com/sveltejs/svelte/pull/17645))
- fix: don't swallow `DOMException` when `media.play()` fails in `bind:paused` ([#17656](https://github.com/sveltejs/svelte/pull/17656))
- chore: provide proper public type for `parseCss` result ([#17654](https://github.com/sveltejs/svelte/pull/17654))
- fix: robustify blocker calculation ([#17676](https://github.com/sveltejs/svelte/pull/17676))
- fix: reduce if block nesting ([#17662](https://github.com/sveltejs/svelte/pull/17662))
## 5.50.1
### Patch Changes
- fix: render boolean attribute values as empty strings for XHTML compliance ([#17648](https://github.com/sveltejs/svelte/pull/17648))
- fix: prevent async render tag hydration mismatches ([#17652](https://github.com/sveltejs/svelte/pull/17652))
## 5.50.0
### Minor Changes
- feat: allow use of createContext when instantiating components programmatically ([#17575](https://github.com/sveltejs/svelte/pull/17575))
### Patch Changes
- fix: ensure infinite effect loops are cleared after flushing ([#17601](https://github.com/sveltejs/svelte/pull/17601))
- fix: allow `{#key NaN}` ([#17642](https://github.com/sveltejs/svelte/pull/17642))
- fix: detect store in each block expression regardless of AST shape ([#17636](https://github.com/sveltejs/svelte/pull/17636))
- fix: treat `<menu>` like `<ul>`/`<ol>` for a11y role checks ([#17638](https://github.com/sveltejs/svelte/pull/17638))
- fix: add vite-ignore comment inside dynamic crypto import ([#17623](https://github.com/sveltejs/svelte/pull/17623))
- chore: wrap JSDoc URLs in `@see` and `@link` tags ([#17617](https://github.com/sveltejs/svelte/pull/17617))
- fix: properly hydrate already-resolved async blocks ([#17641](https://github.com/sveltejs/svelte/pull/17641))
- fix: emit `each_key_duplicate` error in production ([#16724](https://github.com/sveltejs/svelte/pull/16724))
- fix: exit resolved async blocks on correct node when hydrating ([#17640](https://github.com/sveltejs/svelte/pull/17640))
## 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
- fix: merge consecutive large text nodes ([#17587](https://github.com/sveltejs/svelte/pull/17587))
- fix: only create async functions in SSR output when necessary ([#17593](https://github.com/sveltejs/svelte/pull/17593))
- fix: properly separate multiline html blocks from each other in `print()` ([#17319](https://github.com/sveltejs/svelte/pull/17319))
- fix: prevent unhandled exceptions arising from dangling promises in <script> ([#17591](https://github.com/sveltejs/svelte/pull/17591))
## 5.49.0
### Minor Changes
- feat: allow passing `ShadowRootInit` object to custom element `shadow` option ([#17088](https://github.com/sveltejs/svelte/pull/17088))
### Patch Changes
- fix: throw for unset `createContext` get on the server ([#17580](https://github.com/sveltejs/svelte/pull/17580))
- fix: reset effects inside skipped branches ([#17581](https://github.com/sveltejs/svelte/pull/17581))
- fix: preserve old dependencies when updating reaction inside fork ([#17579](https://github.com/sveltejs/svelte/pull/17579))
- fix: more conservative assignment_value_stale warnings ([#17574](https://github.com/sveltejs/svelte/pull/17574))
- fix: disregard `popover` elements when determining whether an element has content ([#17367](https://github.com/sveltejs/svelte/pull/17367))
- fix: fire introstart/outrostart events after delay, if specified ([#17567](https://github.com/sveltejs/svelte/pull/17567))
- fix: increment signal versions when discarding forks ([#17577](https://github.com/sveltejs/svelte/pull/17577))
## 5.48.5
### Patch Changes

@ -851,23 +851,6 @@ export interface HTMLAttributes<T extends EventTarget> 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;

@ -2,7 +2,7 @@
"name": "svelte",
"description": "Cybernetically enhanced web apps",
"license": "MIT",
"version": "5.48.5",
"version": "5.51.0",
"type": "module",
"types": "./types/index.d.ts",
"engines": {
@ -150,7 +150,7 @@
},
"devDependencies": {
"@jridgewell/trace-mapping": "^0.3.25",
"@playwright/test": "^1.46.1",
"@playwright/test": "^1.58.0",
"@rollup/plugin-commonjs": "^28.0.1",
"@rollup/plugin-node-resolve": "^15.3.0",
"@rollup/plugin-terser": "^0.4.4",
@ -170,13 +170,14 @@
"@jridgewell/sourcemap-codec": "^1.5.0",
"@sveltejs/acorn-typescript": "^1.0.5",
"@types/estree": "^1.0.5",
"@types/trusted-types": "^2.0.7",
"acorn": "^8.12.1",
"aria-query": "^5.3.1",
"axobject-query": "^4.1.0",
"clsx": "^2.1.1",
"devalue": "^5.6.2",
"esm-env": "^1.2.1",
"esrap": "^2.2.1",
"esrap": "^2.2.2",
"is-reference": "^3.0.3",
"locate-character": "^3.0.0",
"magic-string": "^0.30.11",

@ -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 {
* </button>
* ```
*
* 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 {
* </script>
* ```
*
* 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<T>(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 {
* <button onclick={() => cleanup()}>cleanup</button>
* ```
*
* 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<T>(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<T extends any[]>(
...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 extends HTMLElement = HTMLElement>(): El;

@ -123,7 +123,7 @@ export function parse(source, { modern, loose } = {}) {
* The parseCss function parses a CSS stylesheet, returning its abstract syntax tree.
*
* @param {string} source The CSS source code
* @returns {Omit<AST.CSS.StyleSheet, 'attributes' | 'content'>}
* @returns {AST.CSS.StyleSheetFile}
*/
export function parseCss(source) {
source = remove_bom(source);
@ -135,7 +135,7 @@ export function parseCss(source) {
const children = parse_stylesheet(parser);
return {
type: 'StyleSheet',
type: 'StyleSheetFile',
start: 0,
end: source.length,
children

@ -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) {

@ -46,10 +46,11 @@ function levenshtein(str1, str2) {
/** @type {number[]} */
const current = [];
let prev = 0;
let value = 0;
for (let i = 0; i <= str2.length; i++) {
for (let j = 0; j <= str1.length; j++) {
let value;
if (i && j) {
if (str1.charAt(j - 1) === str2.charAt(i - 1)) {
value = prev;

@ -690,7 +690,7 @@ export function analyze_component(root, source, options) {
}
}
calculate_blockers(instance, scopes, analysis);
calculate_blockers(instance, analysis);
if (analysis.runes) {
const props_refs = module.scope.references.get('$$props');
@ -940,11 +940,10 @@ export function analyze_component(root, source, options) {
* top level statements. This includes indirect blockers such as functions referencing async top level statements.
*
* @param {Js} instance
* @param {Map<AST.SvelteNode, Scope>} scopes
* @param {ComponentAnalysis} analysis
* @returns {void}
*/
function calculate_blockers(instance, scopes, analysis) {
function calculate_blockers(instance, analysis) {
/**
* @param {ESTree.Node} expression
* @param {Scope} scope
@ -959,6 +958,14 @@ function calculate_blockers(instance, scopes, analysis) {
expression,
{ scope },
{
_(node, context) {
const scope = instance.scopes.get(node);
if (scope) {
context.next({ scope });
} else {
context.next();
}
},
ImportDeclaration(node) {},
Identifier(node, context) {
const parent = /** @type {ESTree.Node} */ (context.path.at(-1));
@ -979,14 +986,11 @@ function calculate_blockers(instance, scopes, analysis) {
/**
* @param {ESTree.Node} node
* @param {Set<ESTree.Node>} seen
* @param {Set<Binding>} reads
* @param {Set<Binding>} writes
* @param {Scope} scope
*/
const trace_references = (node, reads, writes, seen = new Set()) => {
if (seen.has(node)) return;
seen.add(node);
const trace_references = (node, reads, writes, scope) => {
/**
* @param {ESTree.Pattern} node
* @param {Scope} scope
@ -1005,10 +1009,10 @@ function calculate_blockers(instance, scopes, analysis) {
walk(
node,
{ scope: instance.scope },
{ scope },
{
_(node, context) {
const scope = scopes.get(node);
const scope = instance.scopes.get(node);
if (scope) {
context.next({ scope });
} else {
@ -1040,10 +1044,6 @@ function calculate_blockers(instance, scopes, analysis) {
writes.add(b);
}
},
// don't look inside functions until they are called
ArrowFunctionExpression(_, context) {},
FunctionDeclaration(_, context) {},
FunctionExpression(_, context) {},
Identifier(node, context) {
const parent = /** @type {ESTree.Node} */ (context.path.at(-1));
if (is_reference(node, parent)) {
@ -1052,7 +1052,19 @@ function calculate_blockers(instance, scopes, analysis) {
reads.add(binding);
}
}
}
},
ReturnStatement(node, context) {
// We have to assume that anything returned from a function, even if it's a function itself,
// might be called immediately, so we have to touch all references within it. Example:
// function foo() { return () => blocker; } foo(); // blocker is touched
if (node.argument) {
touch(node.argument, context.state.scope, reads);
}
},
// don't look inside functions until they are called
ArrowFunctionExpression(_, context) {},
FunctionDeclaration(_, context) {},
FunctionExpression(_, context) {}
}
);
};
@ -1132,7 +1144,7 @@ function calculate_blockers(instance, scopes, analysis) {
/** @type {Set<Binding>} */
const writes = new Set();
trace_references(declarator, reads, writes);
trace_references(declarator, reads, writes, instance.scope);
const blocker = /** @type {NonNullable<Binding['blocker']>} */ (
b.member(promises, b.literal(analysis.instance_body.async.length), true)
@ -1160,7 +1172,7 @@ function calculate_blockers(instance, scopes, analysis) {
/** @type {Set<Binding>} */
const writes = new Set();
trace_references(node, reads, writes);
trace_references(node, reads, writes, instance.scope);
const blocker = /** @type {NonNullable<Binding['blocker']>} */ (
b.member(promises, b.literal(analysis.instance_body.async.length), true)
@ -1184,12 +1196,17 @@ function calculate_blockers(instance, scopes, analysis) {
for (const fn of functions) {
/** @type {Set<Binding>} */
const reads_writes = new Set();
const body =
const init =
fn.type === 'VariableDeclarator'
? /** @type {ESTree.FunctionExpression | ESTree.ArrowFunctionExpression} */ (fn.init).body
: fn.body;
trace_references(body, reads_writes, reads_writes);
? /** @type {ESTree.FunctionExpression | ESTree.ArrowFunctionExpression} */ (fn.init)
: fn;
trace_references(
init.body,
reads_writes,
reads_writes,
/** @type {Scope} */ (instance.scopes.get(init))
);
const max = [...reads_writes].reduce((max, binding) => {
if (binding.blocker) {

@ -24,4 +24,23 @@ export function IfBlock(node, context) {
context.visit(node.consequent);
if (node.alternate) context.visit(node.alternate);
// Check if we can flatten branches
const alt = node.alternate;
if (alt && alt.nodes.length === 1 && alt.nodes[0].type === 'IfBlock' && alt.nodes[0].elseif) {
const elseif = alt.nodes[0];
// Don't flatten if this else-if has an await expression or new blockers
// TODO would be nice to check the await expression itself to see if it's awaiting the same thing as a previous if expression
if (
!elseif.metadata.expression.has_await &&
!elseif.metadata.expression.has_more_blockers_than(node.metadata.expression)
) {
// Roll the existing flattened branches (if any) into this one, then delete those of the else-if block
// to avoid processing them multiple times as we walk down the chain during code transformation.
node.metadata.flattened = [elseif, ...(elseif.metadata.flattened ?? [])];
elseif.metadata.flattened = undefined;
}
}
}

@ -16,6 +16,8 @@ import { regex_starts_with_newline } from '../../patterns.js';
import { check_element } from './shared/a11y/index.js';
import { validate_element } from './shared/element.js';
import { mark_subtree_dynamic } from './shared/fragment.js';
import { object } from '../../../utils/ast.js';
import { runes } from '../../../state.js';
/**
* @param {AST.RegularElement} node
@ -64,6 +66,34 @@ export function RegularElement(node, context) {
}
}
// Special case: `<select bind:value={foo}><option>{bar}</option>`
// means we need to invalidate `bar` whenever `foo` is mutated
if (node.name === 'select' && !runes) {
for (const attribute of node.attributes) {
if (
attribute.type === 'BindDirective' &&
attribute.name === 'value' &&
attribute.expression.type !== 'SequenceExpression'
) {
const identifier = object(attribute.expression);
const binding = identifier && context.state.scope.get(identifier.name);
if (binding) {
for (const name of context.state.scope.references.keys()) {
if (name === binding.node.name) continue;
const indirect = context.state.scope.get(name);
if (indirect) {
binding.legacy_indirect_bindings.add(indirect);
}
}
}
break;
}
}
}
// Special case: single expression tag child of option element -> add "fake" attribute
// to ensure that value types are the same (else for example numbers would be strings)
if (

@ -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'],

@ -167,7 +167,7 @@ export function check_element(node, context) {
if (
current_role === get_implicit_role(node.name, attribute_map) &&
// <ul role="list"> 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) &&
// <a role="link" /> is ok because without href the a tag doesn't have a role of link
!(node.name === 'a' && !attribute_map.has('href'))
) {
@ -824,6 +824,10 @@ function has_content(element) {
}
if (node.type === 'RegularElement' || node.type === 'SvelteElement') {
if (node.attributes.some((a) => a.type === 'Attribute' && a.name === 'popover')) {
continue;
}
if (
node.name === 'img' &&
node.attributes.some((node) => node.type === 'Attribute' && node.name === 'alt')

@ -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),

@ -30,13 +30,15 @@ export class Template {
/**
* @param {string} name
* @param {number} start
* @param {boolean} is_html
*/
push_element(name, start) {
push_element(name, start, is_html) {
this.#element = {
type: 'element',
name,
attributes: {},
children: [],
is_html,
start
};
@ -100,7 +102,7 @@ function stringify(item) {
for (const key in item.attributes) {
const value = item.attributes[key];
str += ` ${key}`;
str += ` ${item.is_html ? key.toLowerCase() : key}`;
if (value !== undefined) str += `="${escape_html(value, true)}"`;
}

@ -5,6 +5,7 @@ export interface Element {
name: string;
attributes: Record<string, string | undefined>;
children: Node[];
is_html: boolean;
/** used for populating __svelte_meta */
start: number;
}

@ -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<AST.SvelteNode, ClientTransformState>;

@ -8,7 +8,7 @@ import {
is_event_attribute
} from '../../../../utils/ast.js';
import { dev, locate_node } from '../../../../state.js';
import { should_proxy } from '../utils.js';
import { build_getter, should_proxy } from '../utils.js';
import { visit_assignment_expression } from '../../shared/assignments.js';
import { validate_mutation } from './shared/utils.js';
import { get_rune } from '../../../scope.js';
@ -147,7 +147,7 @@ function build_assignment(operator, left, right, context) {
// mutation
if (transform?.mutate) {
return transform.mutate(
let mutation = transform.mutate(
object,
b.assignment(
operator,
@ -155,6 +155,25 @@ function build_assignment(operator, left, right, context) {
/** @type {Expression} */ (context.visit(right))
)
);
if (binding.legacy_indirect_bindings.size > 0) {
mutation = b.sequence([
mutation,
b.call(
'$.invalidate_inner_signals',
b.arrow(
[],
b.block(
Array.from(binding.legacy_indirect_bindings).map((binding) =>
b.stmt(build_getter({ ...binding.node }, context.state))
)
)
)
)
]);
}
return mutation;
}
// in cases like `(object.items ??= []).push(value)`, we may need to warn
@ -162,7 +181,10 @@ function build_assignment(operator, left, right, context) {
// will be pushed to. we do this by transforming it to something like
// `$.assign_nullish(object, 'items', [])`
let should_transform =
dev && path.at(-1) !== 'ExpressionStatement' && is_non_coercive_operator(operator);
dev &&
path.at(-1) !== 'ExpressionStatement' &&
is_non_coercive_operator(operator) &&
!context.state.scope.evaluate(right).is_primitive;
// special case — ignore `onclick={() => (...)}`
if (

@ -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)
)
)
)
);

@ -47,7 +47,11 @@ export function Fragment(node, context) {
const is_single_element = trimmed.length === 1 && trimmed[0].type === 'RegularElement';
const is_single_child_not_needing_template =
trimmed.length === 1 &&
(trimmed[0].type === 'SvelteFragment' || trimmed[0].type === 'TitleElement');
(trimmed[0].type === 'SvelteFragment' ||
trimmed[0].type === 'TitleElement' ||
(trimmed[0].type === 'IfBlock' &&
trimmed[0].elseif &&
/** @type {AST.IfBlock} */ (parent).metadata.flattened?.includes(trimmed[0])));
const template_name = context.state.scope.root.unique('root'); // TODO infer name from parent
/** @type {Statement[]} */
@ -120,34 +124,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));
}
}

@ -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])
)
)
)
);

@ -1,4 +1,4 @@
/** @import { BlockStatement, Expression } from 'estree' */
/** @import { BlockStatement, Expression, IfStatement, Statement } from 'estree' */
/** @import { AST } from '#compiler' */
/** @import { ComponentContext } from '../types' */
import * as b from '#compiler/builders';
@ -10,39 +10,75 @@ import { build_expression, add_svelte_meta } from './shared/utils.js';
*/
export function IfBlock(node, context) {
context.state.template.push_comment();
/** @type {Statement[]} */
const statements = [];
const consequent = /** @type {BlockStatement} */ (context.visit(node.consequent));
const consequent_id = b.id(context.state.scope.generate('consequent'));
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);
statements.push(b.var(consequent_id, b.arrow([b.id('$$anchor')], consequent)));
// Build the if/else-if/else chain
let index = 0;
/** @type {IfStatement | undefined} */
let first_if;
/** @type {IfStatement | undefined} */
let last_if;
/** @type {AST.IfBlock | undefined} */
let last_alt;
let alternate_id;
for (const branch of [node, ...(node.metadata.flattened ?? [])]) {
const consequent = /** @type {BlockStatement} */ (context.visit(branch.consequent));
const consequent_id = b.id(context.state.scope.generate('consequent'));
statements.push(b.var(consequent_id, b.arrow([b.id('$$anchor')], consequent)));
if (node.alternate) {
const alternate = /** @type {BlockStatement} */ (context.visit(node.alternate));
alternate_id = b.id(context.state.scope.generate('alternate'));
statements.push(b.var(alternate_id, b.arrow([b.id('$$anchor')], alternate)));
// Build the test expression for this branch
/** @type {Expression} */
let test;
if (branch.metadata.expression.has_await) {
// Top-level condition with await: already resolved by $.async wrapper
test = b.call('$.get', b.id('$$condition'));
} else {
const expression = build_expression(context, branch.test, branch.metadata.expression);
if (branch.metadata.expression.has_call) {
const derived_id = b.id(context.state.scope.generate('d'));
statements.push(b.var(derived_id, b.call('$.derived', b.arrow([], expression))));
test = b.call('$.get', derived_id);
} else {
test = expression;
}
}
const render_call = b.stmt(b.call('$$render', consequent_id, index > 0 && b.literal(index)));
const new_if = b.if(test, render_call);
if (last_if) {
last_if.alternate = new_if;
} else {
first_if = new_if;
}
last_alt = branch;
last_if = new_if;
index++;
}
const is_async = node.metadata.expression.is_async();
// Handle final alternate (else branch, remaining async chain, or nothing)
if (last_if && last_alt?.alternate) {
const alternate = /** @type {BlockStatement} */ (context.visit(last_alt.alternate));
const alternate_id = b.id(context.state.scope.generate('alternate'));
statements.push(b.var(alternate_id, b.arrow([b.id('$$anchor')], alternate)));
const expression = build_expression(context, node.test, node.metadata.expression);
const test = is_async ? b.call('$.get', b.id('$$condition')) : expression;
last_if.alternate = b.stmt(b.call('$$render', alternate_id, b.literal(false)));
}
// Build $.if() arguments
/** @type {Expression[]} */
const args = [
context.state.node,
b.arrow(
[b.id('$$render')],
b.block([
b.if(
test,
b.stmt(b.call('$$render', consequent_id)),
alternate_id && b.stmt(b.call('$$render', alternate_id, b.literal(false)))
)
])
)
b.arrow([b.id('$$render')], first_if ? b.block([first_if]) : b.block([]))
];
if (node.elseif) {
@ -66,21 +102,26 @@ export function IfBlock(node, context) {
//
// ...even though they're logically equivalent. In the first case, the
// transition will only play when `y` changes, but in the second it
// should play when `x` or `y` change — both are considered 'local'
// should play when `x` or `y` change — both are considered 'local'.
// This could also be a non-flattened elseif (because it has an async expression).
// In both cases mark as elseif so the runtime uses EFFECT_TRANSPARENT for transitions.
args.push(b.true);
}
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)
)
)
)
);

@ -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);
}

@ -34,5 +34,5 @@ export function OnDirective(node, context) {
node.modifiers.includes('passive') ||
(node.modifiers.includes('nonpassive') ? false : undefined);
return build_event(node.name, context.state.node, handler, capture, passive);
return build_event(context, node.name, handler, capture, passive, false);
}

@ -38,9 +38,11 @@ import { TEMPLATE_FRAGMENT } from '../../../../../constants.js';
* @param {ComponentContext} context
*/
export function RegularElement(node, context) {
context.state.template.push_element(node.name, node.start);
const is_html = context.state.metadata.namespace === 'html' && node.name !== 'svg';
const name = is_html ? node.name.toLowerCase() : node.name;
context.state.template.push_element(name, node.start, is_html);
if (node.name === 'noscript') {
if (name === 'noscript') {
context.state.template.pop_element();
return;
}
@ -53,9 +55,9 @@ export function RegularElement(node, context) {
// Therefore we need to use importNode instead, which doesn't have this caveat.
// Additionally, Webkit browsers need importNode for video elements for autoplay
// to work correctly.
context.state.template.needs_import_node ||= node.name === 'video' || is_custom_element;
context.state.template.needs_import_node ||= name === 'video' || is_custom_element;
context.state.template.contains_script_tag ||= node.name === 'script';
context.state.template.contains_script_tag ||= name === 'script';
/** @type {Array<AST.Attribute | AST.SpreadAttribute>} */
const attributes = [];
@ -161,7 +163,7 @@ export function RegularElement(node, context) {
}
}
if (node.name === 'input') {
if (name === 'input') {
const has_value_attribute = attributes.some(
(attribute) =>
attribute.type === 'Attribute' &&
@ -190,7 +192,7 @@ export function RegularElement(node, context) {
}
}
if (node.name === 'textarea') {
if (name === 'textarea') {
const attribute = lookup.get('value') ?? lookup.get('checked');
const needs_content_reset = attribute && !is_text_attribute(attribute);
@ -199,10 +201,6 @@ export function RegularElement(node, context) {
}
}
if (node.name === 'select' && bindings.has('value')) {
setup_select_synchronization(/** @type {AST.BindDirective} */ (bindings.get('value')), context);
}
// Let bindings first, they can be used on attributes
context.state.init.push(...lets);
@ -210,10 +208,7 @@ export function RegularElement(node, context) {
/** If true, needs `__value` for inputs */
const needs_special_value_handling =
node.name === 'option' ||
node.name === 'select' ||
bindings.has('group') ||
bindings.has('checked');
name === 'option' || name === 'select' || bindings.has('group') || bindings.has('checked');
if (has_spread) {
build_attribute_effect(
@ -256,16 +251,12 @@ export function RegularElement(node, context) {
}
if (name !== 'class' || value) {
context.state.template.set_prop(
attribute.name,
is_boolean_attribute(name) && value === true ? undefined : value === true ? '' : value
);
context.state.template.set_prop(attribute.name, value === true ? '' : value);
}
} else if (name === 'autofocus') {
let { value } = build_attribute_value(attribute.value, context);
context.state.init.push(b.stmt(b.call('$.autofocus', node_id, value)));
} else if (name === 'class') {
const is_html = context.state.metadata.namespace === 'html' && node.name !== 'svg';
build_set_class(node, node_id, attribute, class_directives, context, is_html);
} else if (name === 'style') {
build_set_style(node_id, attribute, style_directives, context);
@ -286,7 +277,7 @@ export function RegularElement(node, context) {
}
if (
is_load_error_element(node.name) &&
is_load_error_element(name) &&
(has_spread || has_use || lookup.has('onload') || lookup.has('onerror'))
) {
context.state.after_update.push(b.stmt(b.call('$.replay_events', node_id)));
@ -314,8 +305,7 @@ export function RegularElement(node, context) {
...context.state,
metadata,
scope: /** @type {Scope} */ (context.state.scopes.get(node.fragment)),
preserve_whitespace:
context.state.preserve_whitespace || node.name === 'pre' || node.name === 'textarea'
preserve_whitespace: context.state.preserve_whitespace || name === 'pre' || name === 'textarea'
};
const { hoisted, trimmed } = clean_nodes(
@ -324,7 +314,7 @@ export function RegularElement(node, context) {
context.path,
state.metadata.namespace,
state,
node.name === 'script' || state.preserve_whitespace,
name === 'script' || state.preserve_whitespace,
state.options.preserveComments
);
@ -370,7 +360,7 @@ export function RegularElement(node, context) {
context.state.template.push_comment();
// Create a separate template for the rich content
const template_name = context.state.scope.root.unique(`${node.name}_content`);
const template_name = context.state.scope.root.unique(`${name}_content`);
const fragment_id = b.id(context.state.scope.generate('fragment'));
const anchor_id = b.id(context.state.scope.generate('anchor'));
@ -421,7 +411,7 @@ export function RegularElement(node, context) {
// The same applies if it's a `<template>` element, since we need to
// set the value of `hydrate_node` to `node.content`
if (node.name === 'template') {
if (name === 'template') {
needs_reset = true;
child_state.init.push(b.stmt(b.call('$.hydrate_template', arg)));
arg = b.member(arg, 'content');
@ -458,7 +448,7 @@ export function RegularElement(node, context) {
context.state.after_update.push(...element_state.after_update);
}
if (node.name === 'selectedcontent') {
if (name === 'selectedcontent') {
context.state.init.push(
b.stmt(
b.call(
@ -490,11 +480,11 @@ export function RegularElement(node, context) {
// this node is an `option` that didn't have a `value` attribute, but had
// a single-expression child, so we treat the value of that expression as
// the value of the option
build_element_special_value_attribute(node.name, node_id, synthetic_attribute, context, true);
build_element_special_value_attribute(name, node_id, synthetic_attribute, context, true);
} else {
for (const attribute of /** @type {AST.Attribute[]} */ (attributes)) {
if (attribute.name === 'value') {
build_element_special_value_attribute(node.name, node_id, attribute, context);
build_element_special_value_attribute(name, node_id, attribute, context);
break;
}
}
@ -504,62 +494,6 @@ export function RegularElement(node, context) {
context.state.template.pop_element();
}
/**
* Special case: if we have a value binding on a select element, we need to set up synchronization
* between the value binding and inner signals, for indirect updates
* @param {AST.BindDirective} value_binding
* @param {ComponentContext} context
*/
function setup_select_synchronization(value_binding, context) {
if (context.state.analysis.runes) return;
let bound = value_binding.expression;
if (bound.type === 'SequenceExpression') {
return;
}
while (bound.type === 'MemberExpression') {
bound = /** @type {Identifier | MemberExpression} */ (bound.object);
}
/** @type {string[]} */
const names = [];
for (const [name, refs] of context.state.scope.references) {
if (
refs.length > 0 &&
// prevent infinite loop
name !== bound.name
) {
names.push(name);
}
}
const invalidator = b.call(
'$.invalidate_inner_signals',
b.thunk(
b.block(
names.map((name) => {
const serialized = build_getter(b.id(name), context.state);
return b.stmt(serialized);
})
)
)
);
context.state.init.push(
b.stmt(
b.call(
'$.template_effect',
b.thunk(
b.block([b.stmt(/** @type {Expression} */ (context.visit(bound))), b.stmt(invalidator)])
)
)
)
);
}
/**
* @param {AST.ClassDirective[]} class_directives
* @param {ComponentContext} context

@ -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));
}

@ -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)
)
)
)
);

@ -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;
@ -488,10 +488,10 @@ export function build_component(node, component_name, loc, context) {
if (Object.keys(custom_css_props).length > 0) {
if (context.state.metadata.namespace === 'svg') {
// this boils down to <g><!></g>
context.state.template.push_element('g', node.start);
context.state.template.push_element('g', node.start, false);
} else {
// this boils down to <svelte-css-wrapper style='display: contents'><!></svelte-css-wrapper>
context.state.template.push_element('svelte-css-wrapper', node.start);
context.state.template.push_element('svelte-css-wrapper', node.start, false);
context.state.template.set_prop('style', 'display: contents');
}
@ -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];

@ -27,55 +27,58 @@ export function visit_event_attribute(node, context) {
let handler = build_event_handler(tag.expression, tag.metadata.expression, context);
if (node.metadata.delegated) {
if (!context.state.events.has(event_name)) {
context.state.events.add(event_name);
}
context.state.events.add(event_name);
}
context.state.init.push(
b.stmt(
b.assignment(
'=',
b.member(context.state.node, b.id('__' + event_name, node.name_loc)),
handler
)
)
);
} else {
const statement = b.stmt(
build_event(
event_name,
context.state.node,
handler,
capture,
is_passive_event(event_name) ? true : undefined
)
);
const statement = b.stmt(
build_event(
context,
event_name,
handler,
capture,
is_passive_event(event_name) ? true : undefined,
node.metadata.delegated
)
);
const type = /** @type {AST.SvelteNode} */ (context.path.at(-1)).type;
const type = /** @type {AST.SvelteNode} */ (context.path.at(-1)).type;
if (type === 'SvelteDocument' || type === 'SvelteWindow' || type === 'SvelteBody') {
// These nodes are above the component tree, and its events should run parent first
context.state.init.push(statement);
} else {
context.state.after_update.push(statement);
}
if (type === 'SvelteDocument' || type === 'SvelteWindow' || type === 'SvelteBody') {
// These nodes are above the component tree, and its events should run parent first
context.state.init.push(statement);
} else {
context.state.after_update.push(statement);
}
}
/**
* Creates a `$.event(...)` call for non-delegated event handlers
* @param {ComponentContext} context
* @param {string} event_name
* @param {Expression} node
* @param {Expression} handler
* @param {boolean} capture
* @param {boolean | undefined} passive
* @param {boolean | undefined} delegated
*/
export function build_event(event_name, node, handler, capture, passive) {
export function build_event(context, event_name, handler, capture, passive, delegated) {
let fn = handler;
if (dev && handler.type === 'ArrowFunctionExpression') {
// create a named function for better debugging
const name = context.state.scope.generate(event_name);
fn = b.function(
b.id(name),
handler.params,
handler.body.type === 'BlockStatement' ? handler.body : b.block([b.return(handler.body)])
);
}
return b.call(
'$.event',
delegated ? '$.delegated' : '$.event',
b.literal(event_name),
node,
handler,
context.state.node,
fn,
capture && b.true,
passive === undefined ? undefined : b.literal(passive)
);

@ -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)
)
)
]);
}

@ -26,7 +26,8 @@ export interface ComponentServerTransformState extends ServerTransformState {
readonly template: Array<Statement | Expression>;
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;

@ -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
);
}

@ -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
);
}

@ -28,7 +28,7 @@ export function Fragment(node, context) {
init: [],
template: [],
namespace,
skip_hydration_boundaries: is_standalone,
is_standalone,
async_consts: undefined
};

@ -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);
}
}

@ -1,39 +1,48 @@
/** @import { BlockStatement, Expression, Statement } from 'estree' */
/** @import { BlockStatement, Expression, IfStatement, Statement } from 'estree' */
/** @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
* @param {ComponentContext} context
*/
export function IfBlock(node, context) {
const test = /** @type {Expression} */ (context.visit(node.test));
const consequent = /** @type {BlockStatement} */ (context.visit(node.consequent));
const alternate = node.alternate
? /** @type {BlockStatement} */ (context.visit(node.alternate))
: b.block([]);
consequent.body.unshift(b.stmt(b.call(b.id('$$renderer.push'), block_open)));
alternate.body.unshift(b.stmt(b.call(b.id('$$renderer.push'), block_open_else)));
/** @type {IfStatement} */
let if_statement = b.if(/** @type {Expression} */ (context.visit(node.test)), consequent);
/** @type {Statement} */
let statement = b.if(test, consequent, alternate);
let index = 1;
let current_if = if_statement;
let alt = node.alternate;
const is_async = node.metadata.expression.is_async();
// Walk the else-if chain, flattening branches
for (const elseif of node.metadata.flattened ?? []) {
const branch = /** @type {BlockStatement} */ (context.visit(elseif.consequent));
branch.body.unshift(b.stmt(b.call(b.id('$$renderer.push'), b.literal(`<!--[${index++}-->`))));
const has_await = node.metadata.expression.has_await;
if (is_async || has_await) {
statement = create_async_block(
b.block([statement]),
node.metadata.expression.blockers(),
!!has_await
current_if = current_if.alternate = b.if(
/** @type {Expression} */ (context.visit(elseif.test)),
branch
);
alt = elseif.alternate;
}
context.state.template.push(statement, block_close);
// Handle final else (or remaining async chain)
const final_alternate = alt ? /** @type {BlockStatement} */ (context.visit(alt)) : b.block([]);
final_alternate.body.unshift(b.stmt(b.call(b.id('$$renderer.push'), block_open_else)));
current_if.alternate = final_alternate;
context.state.template.push(
...create_child_block(
[if_statement],
node.metadata.expression.blockers(),
node.metadata.expression.has_await
),
block_close
);
}

@ -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';
/**
@ -22,6 +15,7 @@ import { is_customizable_select_element } from '../../../nodes.js';
* @param {ComponentContext} context
*/
export function RegularElement(node, context) {
const name = context.state.namespace === 'html' ? node.name.toLowerCase() : node.name;
const namespace = determine_namespace_for_children(node, context.state.namespace);
/** @type {ComponentServerTransformState} */
@ -34,7 +28,7 @@ export function RegularElement(node, context) {
template: []
};
const node_is_void = is_void(node.name);
const node_is_void = is_void(name);
const optimiser = new PromiseOptimiser();
@ -42,42 +36,33 @@ export function RegularElement(node, context) {
// avoid calling build_element_attributes here to prevent evaluating/awaiting
// attribute expressions twice. We'll handle attributes in the special branch.
const is_select_special =
node.name === 'select' &&
name === 'select' &&
node.attributes.some(
(attribute) =>
((attribute.type === 'Attribute' || attribute.type === 'BindDirective') &&
attribute.name === 'value') ||
attribute.type === 'SpreadAttribute'
);
const is_option_special = node.name === 'option';
const is_option_special = name === 'option';
const is_special = is_select_special || is_option_special;
let body = /** @type {Expression | null} */ (null);
if (!is_special) {
// only open the tag in the non-special path
state.template.push(b.literal(`<${node.name}`));
state.template.push(b.literal(`<${name}`));
body = build_element_attributes(node, { ...context, state }, optimiser.transform);
state.template.push(b.literal(node_is_void ? '/>' : '>')); // add `/>` for XHTML compliance
}
if ((node.name === 'script' || node.name === 'style') && node.fragment.nodes.length === 1) {
if ((name === 'script' || name === 'style') && node.fragment.nodes.length === 1) {
state.template.push(
b.literal(/** @type {AST.Text} */ (node.fragment.nodes[0]).data),
b.literal(`</${node.name}>`)
b.literal(`</${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)]),
true
)
);
} 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;
}
@ -106,7 +91,7 @@ export function RegularElement(node, context) {
b.call(
'$.push_element',
b.id('$$renderer'),
b.literal(node.name),
b.literal(name),
b.literal(location.line),
b.literal(location.column)
)
@ -131,13 +116,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]), true)
);
} else {
context.state.template.push(...state.init, statement);
}
context.state.template.push(...optimiser.render([...state.init, statement]));
return;
}
@ -164,7 +143,7 @@ export function RegularElement(node, context) {
b.call(
'$.push_element',
b.id('$$renderer'),
b.literal(node.name),
b.literal(name),
b.literal(location.line),
b.literal(location.column)
)
@ -184,13 +163,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]), true)
);
} else {
context.state.template.push(...state.init, statement);
}
context.state.template.push(...optimiser.render([...state.init, statement]));
return;
}
@ -219,16 +192,13 @@ export function RegularElement(node, context) {
} else {
// For optgroup or select with rich content, add hydration marker at the start
process_children(trimmed, { ...context, state });
if (
(node.name === 'optgroup' || node.name === 'select') &&
is_customizable_select_element(node)
) {
if ((name === 'optgroup' || name === 'select') && is_customizable_select_element(node)) {
state.template.push(b.literal('<!>'));
}
}
if (!node_is_void) {
state.template.push(b.literal(`</${node.name}>`));
state.template.push(b.literal(`</${name}>`));
}
if (dev) {
@ -236,18 +206,9 @@ export function RegularElement(node, context) {
}
if (optimiser.is_async()) {
let statement = create_child_block(
b.block([optimiser.apply(), ...state.init, ...build_template(state.template)]),
true
context.state.template.push(
...optimiser.render([...state.init, ...build_template(state.template)])
);
const blockers = optimiser.blockers();
if (blockers.elements.length > 0) {
statement = create_async_block(b.block([statement]), blockers, false, false);
}
context.state.template.push(statement);
} else {
context.state.init.push(...state.init);
context.state.template.push(...state.template);

@ -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,11 @@ 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 the render tag is wrapped in $.async, that $.async call already contains surrounding markers,
// so we don't need to (or rather must not, to avoid hydration mismatches) add our own.
if (!optimiser.is_async() && !context.state.is_standalone) {
context.state.template.push(empty_comment);
}
}

@ -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);
}

@ -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]), true);
}
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
)
);
}

@ -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);
}
}

@ -235,13 +235,7 @@ export function build_element_attributes(node, context, transform) {
if (name !== 'class' || literal_value) {
context.state.template.push(
b.literal(
` ${attribute.name}${
is_boolean_attribute(name) && literal_value === true
? ''
: `="${literal_value === true ? '' : String(literal_value)}"`
}`
)
b.literal(` ${name}="${literal_value === true ? '' : String(literal_value)}"`)
);
}
@ -544,7 +538,7 @@ function build_attr_style(style_directives, expression, context, transform) {
name = name.toLowerCase();
}
const property = b.init(directive.name, expression);
const property = b.init(name, expression);
if (directive.modifiers.includes('important')) {
important_properties.push(property);
} else {

@ -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 {
@ -213,7 +225,7 @@ export function build_attribute_value(
const node = value[i];
if (node.type === 'Text') {
quasi.value.raw += trim_whitespace
quasi.value.cooked += trim_whitespace
? node.data.replace(regex_whitespaces_strict, ' ')
: node.data;
} else {
@ -232,6 +244,10 @@ export function build_attribute_value(
}
}
for (const quasi of quasis) {
quasi.value.raw = sanitize_template_string(/** @type {string} */ (quasi.value.cooked));
}
return b.template(quasis, expressions);
}
@ -262,73 +278,20 @@ export function build_getter(node, state) {
}
/**
* Creates a `$$renderer.child(...)` expression statement
* @param {BlockStatement | Expression} body
* @param {boolean} async
* @returns {Statement}
*/
export function create_child_block(body, async) {
return b.stmt(b.call('$$renderer.child', b.arrow([b.id('$$renderer')], body, async)));
}
/**
* 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))];
}
/**
@ -374,7 +337,7 @@ export class PromiseOptimiser {
}
}
apply() {
#apply() {
if (this.expressions.length === 0) {
return b.empty;
}
@ -404,4 +367,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);
}
}

@ -118,6 +118,19 @@ export class ExpressionMetadata {
return this.#get_blockers().size > 0;
}
/**
* @param {ExpressionMetadata} other
*/
has_more_blockers_than(other) {
for (const blocker of this.#get_blockers()) {
if (!other.#get_blockers().has(blocker)) {
return true;
}
}
return false;
}
is_async() {
return this.has_await || this.#get_blockers().size > 0;
}

@ -120,6 +120,12 @@ export class Binding {
*/
legacy_dependencies = [];
/**
* Bindings that should be invalidated when this binding is invalidated
* @type {Set<Binding>}
*/
legacy_indirect_bindings = new Set();
/**
* Legacy props: the `class` in `{ export klass as class}`. $props(): The `class` in { class: klass } = $props()
* @type {string | null}
@ -228,6 +234,13 @@ class Evaluation {
*/
is_number = true;
/**
* True if the value is known to be a primitive
* @readonly
* @type {boolean}
*/
is_primitive = true;
/**
* True if the value is known to be a function
* @readonly
@ -577,6 +590,7 @@ class Evaluation {
if (value === UNKNOWN) {
this.has_unknown = true;
this.is_primitive = false;
}
}

@ -115,57 +115,17 @@ function base_element(node, context) {
const is_doctype_node = node.name.toLowerCase() === '!doctype';
const is_self_closing =
is_void(node.name) || (node.type === 'Component' && node.fragment.nodes.length === 0);
let multiline_content = false;
if (is_doctype_node) child_context.write(`>`);
else if (is_self_closing) {
child_context.write(`${multiline_attributes ? '' : ' '}/>`);
} else {
child_context.write('>');
// Process the element's content in a separate context for measurement
const content_context = child_context.new();
const allow_inline_content = child_context.measure() < LINE_BREAK_THRESHOLD;
block(content_context, node.fragment, allow_inline_content);
// Determine if content should be formatted on multiple lines
multiline_content = content_context.measure() > LINE_BREAK_THRESHOLD;
if (multiline_content) {
child_context.newline();
// Only indent if attributes are inline and content itself isn't already multiline
const should_indent = !multiline_attributes && !content_context.multiline;
if (should_indent) {
child_context.indent();
}
child_context.append(content_context);
if (should_indent) {
child_context.dedent();
}
child_context.newline();
} else {
child_context.append(content_context);
}
block(child_context, node.fragment, true);
child_context.write(`</${node.name}>`);
}
const break_line_after = child_context.measure() > LINE_BREAK_THRESHOLD;
if ((multiline_content || multiline_attributes) && !context.empty()) {
context.newline();
}
context.append(child_context);
if (is_self_closing) return;
if (multiline_content || multiline_attributes || break_line_after) {
context.newline();
}
}
/** @type {Visitors<AST.SvelteNode>} */
@ -411,7 +371,23 @@ const svelte_visitors = {
}
}
} else {
const is_block_element =
child_node.type === 'RegularElement' ||
child_node.type === 'Component' ||
child_node.type === 'SvelteHead' ||
child_node.type === 'SvelteFragment' ||
child_node.type === 'SvelteBoundary' ||
child_node.type === 'SvelteDocument' ||
child_node.type === 'SvelteSelf' ||
child_node.type === 'SvelteWindow' ||
child_node.type === 'SvelteComponent' ||
child_node.type === 'SvelteElement' ||
child_node.type === 'SlotElement' ||
child_node.type === 'TitleElement';
if (is_block_element && sequence.length > 0) flush();
sequence.push(child_node);
if (is_block_element) flush();
}
}
@ -420,18 +396,20 @@ const svelte_visitors = {
let multiline = false;
let width = 0;
const child_contexts = items.map((sequence) => {
const child_context = context.new();
const child_contexts = items
.filter((x) => x.length > 0)
.map((sequence) => {
const child_context = context.new();
for (const node of sequence) {
child_context.visit(node);
multiline ||= child_context.multiline;
}
for (const node of sequence) {
child_context.visit(node);
multiline ||= child_context.multiline;
}
width += child_context.measure();
width += child_context.measure();
return child_context;
});
return child_context;
});
multiline ||= width > LINE_BREAK_THRESHOLD;

@ -6,10 +6,17 @@ export namespace _CSS {
end: number;
}
export interface StyleSheet extends BaseNode {
export interface StyleSheetBase extends BaseNode {
children: Array<Atrule | Rule>;
}
export interface StyleSheetFile extends StyleSheetBase {
type: 'StyleSheetFile';
}
export interface StyleSheet extends StyleSheetBase {
type: 'StyleSheet';
attributes: any[]; // TODO
children: Array<Atrule | Rule>;
content: {
start: number;
end: number;

@ -483,6 +483,8 @@ export namespace AST {
alternate: Fragment | null;
/** @internal */
metadata: {
/** List of else-if blocks that can be flattened into this if block */
flattened?: IfBlock[];
expression: ExpressionMetadata;
};
}

@ -27,10 +27,10 @@ export const DIRTY = 1 << 11;
export const MAYBE_DIRTY = 1 << 12;
export const INERT = 1 << 13;
export const DESTROYED = 1 << 14;
/** Set once a reaction has run for the first time */
export const REACTION_RAN = 1 << 15;
// Flags exclusive to effects
/** Set once an effect that should run synchronously has run */
export const EFFECT_RAN = 1 << 15;
/**
* 'Transparent' effects do not create a transition boundary.
* This is on a block effect 99% of the time but may also be on a branch effect if its parent block effect was pruned
@ -48,7 +48,7 @@ export const EFFECT_OFFSCREEN = 1 << 25;
* Will be lifted during execution of the derived and during checking its dirty state (both are necessary
* because a derived might be checked but not executed).
*/
export const WAS_MARKED = 1 << 15;
export const WAS_MARKED = 1 << 16;
// Flags used for async
export const REACTION_IS_UPDATING = 1 << 21;
@ -67,6 +67,7 @@ export const STALE_REACTION = new (class StaleReactionError extends Error {
message = 'The reaction that called `getAbortSignal()` was re-run or destroyed';
})();
export const IS_XHTML = /* @__PURE__ */ globalThis.document?.contentType.includes('xml') ?? false;
export const ELEMENT_NODE = 1;
export const TEXT_NODE = 3;
export const COMMENT_NODE = 8;

@ -5,7 +5,7 @@ import { active_effect, active_reaction } from './runtime.js';
import { create_user_effect } from './reactivity/effects.js';
import { async_mode_flag, legacy_mode_flag } from '../flags/index.js';
import { FILENAME } from '../../constants.js';
import { BRANCH_EFFECT, EFFECT_RAN } from './constants.js';
import { BRANCH_EFFECT, REACTION_RAN } from './constants.js';
/** @type {ComponentContext | null} */
export let component_context = null;

@ -1,3 +1,4 @@
import { STATE_SYMBOL } from '#client/constants';
import { sanitize_location } from '../../../utils.js';
import { untrack } from '../runtime.js';
import * as w from '../warnings.js';
@ -10,7 +11,7 @@ import * as w from '../warnings.js';
* @param {string} location
*/
function compare(a, b, property, location) {
if (a !== b) {
if (a !== b && typeof b === 'object' && STATE_SYMBOL in b) {
w.assignment_value_stale(property, /** @type {string} */ (sanitize_location(location)));
}

@ -28,7 +28,9 @@ export function log_if_contains_state(method, ...objects) {
// eslint-disable-next-line no-console
console.log('%c[snapshot]', 'color: grey', ...transformed);
}
} catch {}
} catch {
// Errors can occur when trying to snapshot objects with getters that throw or non-enumerable properties.
}
});
return objects;

@ -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);
}

@ -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);
}
}

@ -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<any>} 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);
}
}

@ -9,14 +9,12 @@ import {
set_hydrating
} from '../hydration.js';
import { block } from '../../reactivity/effects.js';
import { HYDRATION_START_ELSE } from '../../../../constants.js';
import { BranchManager } from './branches.js';
// TODO reinstate https://github.com/sveltejs/svelte/pull/15250
import { HYDRATION_START, HYDRATION_START_ELSE } from '../../../../constants.js';
/**
* @param {TemplateNode} node
* @param {(branch: (fn: (anchor: Node) => void, flag?: boolean) => void) => void} fn
* @param {(branch: (fn: (anchor: Node) => void, key?: number | false) => void) => void} fn
* @param {boolean} [elseif] True if this is an `{:else if ...}` block rather than an `{#if ...}`, as that affects which transitions are considered 'local'
* @returns {void}
*/
@ -29,14 +27,28 @@ export function if_block(node, fn, elseif = false) {
var flags = elseif ? EFFECT_TRANSPARENT : 0;
/**
* @param {boolean} condition,
* @param {number | false} key
* @param {null | ((anchor: Node) => void)} fn
*/
function update_branch(condition, fn) {
function update_branch(key, fn) {
if (hydrating) {
const is_else = read_hydration_instruction(node) === HYDRATION_START_ELSE;
const data = read_hydration_instruction(node);
/**
* @type {number | false}
* "[" = branch 0, "[1" = branch 1, "[2" = branch 2, ..., "[!" = else (false)
*/
var hydrated_key;
if (data === HYDRATION_START) {
hydrated_key = 0;
} else if (data === HYDRATION_START_ELSE) {
hydrated_key = false;
} else {
hydrated_key = parseInt(data.substring(1)); // "[1", "[2", etc.
}
if (condition === is_else) {
if (key !== hydrated_key) {
// Hydration mismatch: remove everything inside the anchor and start fresh.
// This could happen with `{#if browser}...{/if}`, for example
var anchor = skip_nodes();
@ -45,22 +57,22 @@ export function if_block(node, fn, elseif = false) {
branches.anchor = anchor;
set_hydrating(false);
branches.ensure(condition, fn);
branches.ensure(key, fn);
set_hydrating(true);
return;
}
}
branches.ensure(condition, fn);
branches.ensure(key, fn);
}
block(() => {
var has_branch = false;
fn((fn, flag = true) => {
fn((fn, key = 0) => {
has_branch = true;
update_branch(flag, fn);
update_branch(key, fn);
});
if (!has_branch) {

@ -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} */ ({});

@ -83,7 +83,7 @@ export function createRawSnippet(fn) {
hydrate_next();
} else {
var html = snippet.render().trim();
var fragment = create_fragment_from_html(html);
var fragment = create_fragment_from_html(html, true);
element = /** @type {Element} */ (get_first_child(fragment));
if (DEV && (get_next_sibling(element) !== null || element.nodeType !== ELEMENT_NODE)) {

@ -7,7 +7,7 @@ import {
set_hydrate_node,
set_hydrating
} from '../hydration.js';
import { create_text, get_first_child } from '../operations.js';
import { create_element, create_text, get_first_child } from '../operations.js';
import { block, teardown } from '../../reactivity/effects.js';
import { set_should_intro } from '../../render.js';
import { active_effect } from '../../runtime.js';
@ -57,7 +57,11 @@ export function element(node, get_tag, is_svg, render_fn, get_namespace, locatio
block(() => {
const next_tag = get_tag() || null;
var ns = get_namespace ? get_namespace() : is_svg || next_tag === 'svg' ? NAMESPACE_SVG : null;
var ns = get_namespace
? get_namespace()
: is_svg || next_tag === 'svg'
? NAMESPACE_SVG
: undefined;
if (next_tag === null) {
branches.ensure(null, null);
@ -67,11 +71,7 @@ export function element(node, get_tag, is_svg, render_fn, get_namespace, locatio
branches.ensure(next_tag, (anchor) => {
if (next_tag) {
element = hydrating
? /** @type {Element} */ (element)
: ns
? document.createElementNS(ns, next_tag)
: document.createElement(next_tag);
element = hydrating ? /** @type {Element} */ (element) : create_element(next_tag, ns);
if (DEV && location) {
// @ts-expect-error

@ -1,6 +1,7 @@
import { DEV } from 'esm-env';
import { register_style } from '../dev/css.js';
import { effect } from '../reactivity/effects.js';
import { create_element } from './operations.js';
/**
* @param {Node} anchor
@ -18,7 +19,7 @@ export function append_styles(anchor, css) {
// Always querying the DOM is roughly the same perf as additionally checking for presence in a map first assuming
// that you'll get cache hits half of the time, so we just always query the dom for simplicity and code savings.
if (!target.querySelector('#' + css.hash)) {
const style = document.createElement('style');
const style = create_element('style');
style.id = css.hash;
style.textContent = css.code;

@ -2,10 +2,10 @@
import { DEV } from 'esm-env';
import { hydrating, set_hydrating } from '../hydration.js';
import { get_descriptors, get_prototype_of } from '../../../shared/utils.js';
import { create_event, delegate } from './events.js';
import { create_event, delegate, delegated, event, event_symbol } from './events.js';
import { add_form_reset_listener, autofocus } from './misc.js';
import * as w from '../../warnings.js';
import { LOADING_ATTR_SYMBOL } from '#client/constants';
import { IS_XHTML, LOADING_ATTR_SYMBOL } from '#client/constants';
import { queue_micro_task } from '../task.js';
import { is_capture_event, can_delegate_event, normalize_attribute } from '../../../../utils.js';
import {
@ -30,6 +30,12 @@ export const STYLE = Symbol('style');
const IS_CUSTOM_ELEMENT = Symbol('is custom element');
const IS_HTML = Symbol('is html');
const LINK_TAG = IS_XHTML ? 'link' : 'LINK';
const INPUT_TAG = IS_XHTML ? 'input' : 'INPUT';
const OPTION_TAG = IS_XHTML ? 'option' : 'OPTION';
const SELECT_TAG = IS_XHTML ? 'select' : 'SELECT';
const PROGRESS_TAG = IS_XHTML ? 'progress' : 'PROGRESS';
/**
* The value/checked attribute in the template actually corresponds to the defaultValue property, so we need
* to remove it upon hydration to avoid a bug when someone resets the form value.
@ -83,7 +89,7 @@ export function set_value(element, value) {
value ?? undefined) ||
// @ts-expect-error
// `progress` elements always need their value set when it's `0`
(element.value === value && (value !== 0 || element.nodeName !== 'PROGRESS'))
(element.value === value && (value !== 0 || element.nodeName !== PROGRESS_TAG))
) {
return;
}
@ -168,7 +174,7 @@ export function set_attribute(element, attribute, value, skip_warning) {
if (
attribute === 'src' ||
attribute === 'srcset' ||
(attribute === 'href' && element.nodeName === 'LINK')
(attribute === 'href' && element.nodeName === LINK_TAG)
) {
if (!skip_warning) {
check_src_in_dev_hydration(element, attribute, value ?? '');
@ -241,7 +247,7 @@ export function set_custom_element_data(node, prop, value) {
(setters_cache.has(node.getAttribute('is') || node.nodeName) ||
// customElements may not be available in browser extension contexts
!customElements ||
customElements.get(node.getAttribute('is') || node.tagName.toLowerCase())
customElements.get(node.getAttribute('is') || node.nodeName.toLowerCase())
? get_setters(node).includes(prop)
: value && typeof value === 'object')
) {
@ -280,7 +286,7 @@ function set_attributes(
should_remove_defaults = false,
skip_warning = false
) {
if (hydrating && should_remove_defaults && element.tagName === 'INPUT') {
if (hydrating && should_remove_defaults && element.nodeName === INPUT_TAG) {
var input = /** @type {HTMLInputElement} */ (element);
var attribute = input.type === 'checkbox' ? 'defaultChecked' : 'defaultValue';
@ -302,7 +308,7 @@ function set_attributes(
}
var current = prev || {};
var is_option_element = element.tagName === 'OPTION';
var is_option_element = element.nodeName === OPTION_TAG;
for (var key in prev) {
if (!(key in next)) {
@ -378,14 +384,14 @@ function set_attributes(
const opts = {};
const event_handle_key = '$$' + key;
let event_name = key.slice(2);
var delegated = can_delegate_event(event_name);
var is_delegated = can_delegate_event(event_name);
if (is_capture_event(event_name)) {
event_name = event_name.slice(0, -7);
opts.capture = true;
}
if (!delegated && prev_value) {
if (!is_delegated && prev_value) {
// Listening to same event but different handler -> our handle function below takes care of this
// If we were to remove and add listeners in this case, it could happen that the event is "swallowed"
// (the browser seems to not know yet that a new one exists now) and doesn't reach the handler
@ -396,25 +402,19 @@ function set_attributes(
current[event_handle_key] = null;
}
if (value != null) {
if (!delegated) {
/**
* @this {any}
* @param {Event} evt
*/
function handle(evt) {
current[key].call(this, evt);
}
current[event_handle_key] = create_event(event_name, element, handle, opts);
} else {
// @ts-ignore
element[`__${event_name}`] = value;
delegate([event_name]);
if (is_delegated) {
delegated(event_name, element, value);
delegate([event_name]);
} else if (value != null) {
/**
* @this {any}
* @param {Event} evt
*/
function handle(evt) {
current[key].call(this, evt);
}
} else if (delegated) {
// @ts-ignore
element[`__${event_name}`] = undefined;
current[event_handle_key] = create_event(event_name, element, handle, opts);
}
} else if (key === 'style') {
// avoid using the setter
@ -505,7 +505,7 @@ export function attribute_effect(
/** @type {Record<symbol, Effect>} */
var effects = {};
var is_select = element.nodeName === 'SELECT';
var is_select = element.nodeName === SELECT_TAG;
var inited = false;
managed(() => {

@ -175,8 +175,9 @@ export function bind_paused(media, get, set = get) {
if (paused) {
media.pause();
} else {
media.play().catch(() => {
media.play().catch((error) => {
set((paused = true));
throw error;
});
}
}

@ -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 {
/** */

@ -2,6 +2,7 @@ import { createClassComponent } from '../../../../legacy/legacy-client.js';
import { effect_root, render_effect } from '../../reactivity/effects.js';
import { append } from '../template.js';
import { define_property, get_descriptor, object_keys } from '../../../shared/utils.js';
import { create_element } from '../operations.js';
/**
* @typedef {Object} CustomElementPropDefinition
@ -103,7 +104,7 @@ if (typeof HTMLElement === 'function') {
* @param {Element} anchor
*/
return (anchor) => {
const slot = document.createElement('slot');
const slot = create_element('slot');
if (name !== 'default') slot.name = name;
append(anchor, slot);

@ -1,5 +1,5 @@
import { hydrating, reset, set_hydrate_node, set_hydrating } from '../hydration.js';
import { create_comment } from '../operations.js';
import { create_comment, create_element } from '../operations.js';
import { attach } from './attachments.js';
/** @type {boolean | null} */
@ -13,7 +13,7 @@ let supported = null;
*/
function is_supported() {
if (supported === null) {
var select = document.createElement('select');
var select = create_element('select');
select.innerHTML = '<option><span>t</span></option>';
supported = /** @type {Element} */ (select.firstChild)?.firstChild?.nodeType === 1;
}

@ -11,6 +11,9 @@ import {
set_active_reaction
} from '../../runtime.js';
import { without_reactive_context } from './bindings/shared.js';
import { can_delegate_event } from '../../../../utils.js';
export const event_symbol = Symbol('events');
/** @type {Set<string>} */
export const all_registered_events = new Set();
@ -127,6 +130,17 @@ export function event(event_name, dom, handler, capture, passive) {
}
}
/**
* @param {string} event_name
* @param {Element} element
* @param {EventListener} [handler]
* @returns {void}
*/
export function delegated(event_name, element, handler) {
// @ts-expect-error
(element[event_symbol] ??= {})[event_name] = handler;
}
/**
* @param {Array<string>} events
* @returns {void}
@ -249,7 +263,7 @@ export function handle_event_propagation(event) {
try {
// @ts-expect-error
var delegated = current_target['__' + event_name];
var delegated = current_target[event_symbol]?.[event_name];
if (
delegated != null &&

@ -5,7 +5,7 @@ import { active_effect, untrack } from '../../runtime.js';
import { loop } from '../../loop.js';
import { should_intro } from '../../render.js';
import { TRANSITION_GLOBAL, TRANSITION_IN, TRANSITION_OUT } from '../../../../constants.js';
import { BLOCK_EFFECT, EFFECT_RAN, EFFECT_TRANSPARENT } from '#client/constants';
import { BLOCK_EFFECT, REACTION_RAN, EFFECT_TRANSPARENT } from '#client/constants';
import { queue_micro_task } from '../task.js';
import { without_reactive_context } from './bindings/shared.js';
@ -239,8 +239,6 @@ export function transition(flags, element, get_fn, get_params) {
intro?.abort();
}
dispatch_event(element, 'introstart');
intro = animate(element, get_options(), outro, 1, () => {
dispatch_event(element, 'introend');
@ -260,8 +258,6 @@ export function transition(flags, element, get_fn, get_params) {
element.inert = true;
dispatch_event(element, 'outrostart');
outro = animate(element, get_options(), intro, 0, () => {
dispatch_event(element, 'outroend');
fn?.();
@ -293,7 +289,7 @@ export function transition(flags, element, get_fn, get_params) {
}
}
run = !block || (block.f & EFFECT_RAN) !== 0;
run = !block || (block.f & REACTION_RAN) !== 0;
}
if (run) {
@ -345,7 +341,8 @@ function animate(element, options, counterpart, t2, on_finish) {
counterpart?.deactivate();
if (!options?.duration) {
if (!options?.duration && !options?.delay) {
dispatch_event(element, is_intro ? 'introstart' : 'outrostart');
on_finish();
return {
@ -385,6 +382,8 @@ function animate(element, options, counterpart, t2, on_finish) {
// remove dummy animation from the stack to prevent conflict with main animation
animation.cancel();
dispatch_event(element, is_intro ? 'introstart' : 'outrostart');
// for bidirectional transitions, we start from the current position,
// rather than doing a full intro/outro
var t1 = counterpart?.t() ?? 1 - t2;

@ -95,7 +95,12 @@ export function skip_nodes(remove = true) {
if (data === HYDRATION_END) {
if (depth === 0) return node;
depth -= 1;
} else if (data === HYDRATION_START || data === HYDRATION_START_ELSE) {
} else if (
data === HYDRATION_START ||
data === HYDRATION_START_ELSE ||
// "[1", "[2", etc. for if blocks
(data[0] === '[' && !isNaN(Number(data.slice(1))))
) {
depth += 1;
}
}

@ -5,8 +5,9 @@ import { init_array_prototype_warnings } from '../dev/equality.js';
import { get_descriptor, is_extensible } from '../../shared/utils.js';
import { active_effect } from '../runtime.js';
import { async_mode_flag } from '../../flags/index.js';
import { TEXT_NODE, EFFECT_RAN } from '#client/constants';
import { TEXT_NODE, REACTION_RAN } from '#client/constants';
import { eager_block_effects } from '../reactivity/batch.js';
import { NAMESPACE_HTML } from '../../../constants.js';
// export these for reference in the compiled code, making global name deduplication unnecessary
/** @type {Window} */
@ -122,6 +123,10 @@ export function child(node, is_text) {
return text;
}
if (is_text) {
merge_text_nodes(/** @type {Text} */ (child));
}
set_hydrate_node(child);
return child;
}
@ -142,14 +147,18 @@ export function first_child(node, is_text = false) {
return first;
}
// if an {expression} is empty during SSR, there might be no
// text node to hydrate — we must therefore create one
if (is_text && hydrate_node?.nodeType !== TEXT_NODE) {
var text = create_text();
if (is_text) {
// if an {expression} is empty during SSR, there might be no
// text node to hydrate — we must therefore create one
if (hydrate_node?.nodeType !== TEXT_NODE) {
var text = create_text();
hydrate_node?.before(text);
set_hydrate_node(text);
return text;
hydrate_node?.before(text);
set_hydrate_node(text);
return text;
}
merge_text_nodes(/** @type {Text} */ (hydrate_node));
}
return hydrate_node;
@ -175,20 +184,24 @@ export function sibling(node, count = 1, is_text = false) {
return next_sibling;
}
// if a sibling {expression} is empty during SSR, there might be no
// text node to hydrate — we must therefore create one
if (is_text && next_sibling?.nodeType !== TEXT_NODE) {
var text = create_text();
// If the next sibling is `null` and we're handling text then it's because
// the SSR content was empty for the text, so we need to generate a new text
// node and insert it after the last sibling
if (next_sibling === null) {
last_sibling?.after(text);
} else {
next_sibling.before(text);
if (is_text) {
// if a sibling {expression} is empty during SSR, there might be no
// text node to hydrate — we must therefore create one
if (next_sibling?.nodeType !== TEXT_NODE) {
var text = create_text();
// If the next sibling is `null` and we're handling text then it's because
// the SSR content was empty for the text, so we need to generate a new text
// node and insert it after the last sibling
if (next_sibling === null) {
last_sibling?.after(text);
} else {
next_sibling.before(text);
}
set_hydrate_node(text);
return text;
}
set_hydrate_node(text);
return text;
merge_text_nodes(/** @type {Text} */ (next_sibling));
}
set_hydrate_node(next_sibling);
@ -215,22 +228,21 @@ export function should_defer_append() {
if (eager_block_effects !== null) return false;
var flags = /** @type {Effect} */ (active_effect).f;
return (flags & EFFECT_RAN) !== 0;
return (flags & REACTION_RAN) !== 0;
}
/**
*
* @param {string} tag
* @template {keyof HTMLElementTagNameMap | string} T
* @param {T} tag
* @param {string} [namespace]
* @param {string} [is]
* @returns
* @returns {T extends keyof HTMLElementTagNameMap ? HTMLElementTagNameMap[T] : Element}
*/
export function create_element(tag, namespace, is) {
let options = is ? { is } : undefined;
if (namespace) {
return document.createElementNS(namespace, tag, options);
}
return document.createElement(tag, options);
return /** @type {T extends keyof HTMLElementTagNameMap ? HTMLElementTagNameMap[T] : Element} */ (
document.createElementNS(namespace ?? NAMESPACE_HTML, tag, options)
);
}
export function create_fragment() {
@ -258,3 +270,24 @@ export function set_attribute(element, key, value = '') {
}
return element.setAttribute(key, value);
}
/**
* Browsers split text nodes larger than 65536 bytes when parsing.
* For hydration to succeed, we need to stitch them back together
* @param {Text} text
*/
export function merge_text_nodes(text) {
if (/** @type {string} */ (text.nodeValue).length < 65536) {
return;
}
let next = text.nextSibling;
while (next !== null && next.nodeType === TEXT_NODE) {
next.remove();
/** @type {string} */ (text.nodeValue) += /** @type {string} */ (next.nodeValue);
next = text.nextSibling;
}
}

@ -1,6 +1,29 @@
/** @import {} from 'trusted-types' */
import { create_element } from './operations.js';
const policy = /* @__PURE__ */ globalThis?.window?.trustedTypes?.createPolicy(
'svelte-trusted-html',
{
/** @param {string} html */
createHTML: (html) => {
return html;
}
}
);
/** @param {string} html */
export function create_fragment_from_html(html) {
var elem = document.createElement('template');
elem.innerHTML = html.replaceAll('<!>', '<!---->'); // XHTML compliance
function create_trusted_html(html) {
return /** @type {string} */ (policy?.createHTML(html) ?? html);
}
/**
* @param {string} html
* @param {boolean} trusted
*/
export function create_fragment_from_html(html, trusted = false) {
var elem = create_element('template');
html = html.replaceAll('<!>', '<!---->'); // XHTML compliance
elem.innerHTML = trusted ? create_trusted_html(html) : html;
return elem.content;
}

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

Loading…
Cancel
Save