Merge branch 'main' into effect-pending-value

state-eager-derived-block
Simon H 6 days ago committed by GitHub
commit f38319aec7
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -1,5 +0,0 @@
---
'svelte': patch
---
chore: simplify batch logic

@ -0,0 +1,5 @@
---
'svelte': patch
---
fix: track the user's getter of `bind:this`

@ -0,0 +1,5 @@
---
'svelte': patch
---
fix: generate correct SSR code for the case where `pending` is an attribute

@ -0,0 +1,5 @@
---
'svelte': patch
---
fix: generate correct code for `each` blocks with async body

@ -4,19 +4,20 @@ on:
issue_comment:
types: [created]
permissions: {}
jobs:
trigger:
runs-on: ubuntu-latest
if: github.repository == 'sveltejs/svelte' && github.event.issue.pull_request && startsWith(github.event.comment.body, '/ecosystem-ci run')
permissions:
issues: write # to add / delete reactions
issues: write # to add / delete reactions, post comments
pull-requests: write # to read PR data, and to add labels
actions: read # to check workflow status
contents: read # to clone the repo
steps:
- name: monitor action permissions
- name: check user authorization # user needs triage permission
uses: actions/github-script@v7
- name: Check User Permissions
uses: actions/github-script@v8
id: check-permissions
with:
script: |
@ -55,7 +56,7 @@ jobs:
}
- name: Get PR Data
uses: actions/github-script@v7
uses: actions/github-script@v8
id: get-pr-data
with:
script: |
@ -65,6 +66,37 @@ jobs:
repo: context.repo.repo,
pull_number: context.issue.number
})
const commentCreatedAt = new Date(context.payload.comment.created_at)
const commitPushedAt = new Date(pr.head.repo.pushed_at)
console.log(`Comment created at: ${commentCreatedAt.toISOString()}`)
console.log(`PR last pushed at: ${commitPushedAt.toISOString()}`)
// Check if any commits were pushed after the comment was created
if (commitPushedAt > commentCreatedAt) {
const errorMsg = [
'⚠️ Security warning: PR was updated after the trigger command was posted.',
'',
`Comment posted at: ${commentCreatedAt.toISOString()}`,
`PR last pushed at: ${commitPushedAt.toISOString()}`,
'',
'This could indicate an attempt to inject code after approval.',
'Please review the latest changes and re-run /ecosystem-ci run if they are acceptable.'
].join('\n')
core.setFailed(errorMsg)
await github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.issue.number,
body: errorMsg
})
throw new Error('PR was pushed to after comment was created')
}
return {
num: context.issue.number,
branchName: pr.head.ref,
@ -83,15 +115,16 @@ jobs:
svelte-ecosystem-ci
- name: Trigger Downstream Workflow
uses: actions/github-script@v7
uses: actions/github-script@v8
id: trigger
env:
COMMENT: ${{ github.event.comment.body }}
PR_DATA: ${{ steps.get-pr-data.outputs.result }}
with:
github-token: ${{ steps.generate-token.outputs.token }}
script: |
const comment = process.env.COMMENT.trim()
const prData = ${{ steps.get-pr-data.outputs.result }}
const prData = JSON.parse(process.env.PR_DATA)
const suite = comment.split('\n')[0].replace(/^\/ecosystem-ci run/, '').trim()

@ -1,6 +1,8 @@
name: Publish Any Commit
on: [push, pull_request]
permissions: {}
jobs:
build:
permissions: {}

@ -15,11 +15,11 @@ Don't worry if you don't know Svelte yet! You can ignore all the nice features S
## Alternatives to SvelteKit
You can also use Svelte directly with Vite by running `npm create vite@latest` and selecting the `svelte` option. With this, `npm run build` will generate HTML, JS, and CSS files inside the `dist` directory using [vite-plugin-svelte](https://github.com/sveltejs/vite-plugin-svelte). In most cases, you will probably need to [choose a routing library](faq#Is-there-a-router) as well.
You can also use Svelte directly with Vite by running `npm create vite@latest` and selecting the `svelte` option. With this, `npm run build` will generate HTML, JS, and CSS files inside the `dist` directory using [vite-plugin-svelte](https://github.com/sveltejs/vite-plugin-svelte). In most cases, you will probably need to [choose a routing library](/packages#routing) as well.
>[!NOTE] Vite is often used in standalone mode to build [single page apps (SPAs)](../kit/glossary#SPA), which you can also [build with SvelteKit](../kit/single-page-apps).
There are also plugins for [Rollup](https://github.com/sveltejs/rollup-plugin-svelte), [Webpack](https://github.com/sveltejs/svelte-loader) [and a few others](https://sveltesociety.dev/packages?category=build-plugins), but we recommend Vite.
There are also [plugins for other bundlers](/packages#bundler-plugins), but we recommend Vite.
## Editor tooling

@ -95,7 +95,7 @@ Since 5.6.0, if an `<input>` has a `defaultValue` and is part of a form, it will
## `<input bind:checked>`
Checkbox and radio inputs can be bound with `bind:checked`:
Checkbox inputs can be bound with `bind:checked`:
```svelte
<label>
@ -117,6 +117,8 @@ Since 5.6.0, if an `<input>` has a `defaultChecked` attribute and is part of a f
</form>
```
> [!NOTE] Use `bind:group` for radio inputs instead of `bind:checked`.
## `<input bind:indeterminate>`
Checkboxes can be in an [indeterminate](https://developer.mozilla.org/en-US/docs/Web/API/HTMLInputElement/indeterminate) state, independently of whether they are checked or unchecked:
@ -362,6 +364,8 @@ Components also support `bind:this`, allowing you to interact with component ins
</script>
```
> [!NOTE] In case of using [the function bindings](#Function-bindings), the getter is required to ensure that the correct value is nullified on component or element destruction.
## bind:_property_ for components
```svelte

@ -139,8 +139,6 @@ If a `<svelte:boundary>` with a `pending` snippet is encountered during SSR, tha
As an experimental feature, the details of how `await` is handled (and related APIs like `$effect.pending()`) are subject to breaking changes outside of a semver major release, though we intend to keep such changes to a bare minimum.
Currently, server-side rendering is synchronous. If a `<svelte:boundary>` with a `pending` snippet is encountered during SSR, only the `pending` snippet will be rendered.
## Breaking changes
Effects run in a slightly different order when the `experimental.async` option is `true`. Specifically, _block_ effects like `{#if ...}` and `{#each ...}` now run before an `$effect.pre` or `beforeUpdate` in the same component, which means that in [very rare situations](/playground/untitled?#H4sIAAAAAAAAE22R3VLDIBCFX2WLvUhnTHsf0zre-Q7WmfwtFV2BgU1rJ5N3F0jaOuoVcPbw7VkYhK4_URTiGYkMnIyjDjLsFGO3EvdCKkIvipdB8NlGXxSCPt96snbtj0gctab2-J_eGs2oOWBE6VunLO_2es-EDKZ5x5ZhC0vPNWM2gHXGouNzAex6hHH1cPHil_Lsb95YT9VQX6KUAbS2DrNsBdsdDFHe8_XSYjH1SrhELTe3MLpsemajweiWVPuxHSbKNd-8eQTdE0EBf4OOaSg2hwNhhE_ABB_ulJzjj9FULvIcqgm5vnAqUB7wWFMfhuugQWkcAr8hVD-mq8D12kOep24J_IszToOXdveGDsuNnZwbJUNlXsKnhJdhUcTo42s41YpOSneikDV5HL8BktM6yRcCAAA=) it is possible to update a block that should no longer exist, but only if you update state inside an effect, [which you should avoid]($effect#When-not-to-use-$effect).

@ -4,7 +4,7 @@ title: Testing
Testing helps you write and maintain your code and guard against regressions. Testing frameworks help you with that, allowing you to describe assertions or expectations about how your code should behave. Svelte is unopinionated about which testing framework you use — you can write unit tests, integration tests, and end-to-end tests using solutions like [Vitest](https://vitest.dev/), [Jasmine](https://jasmine.github.io/), [Cypress](https://www.cypress.io/) and [Playwright](https://playwright.dev/).
## Unit and integration testing using Vitest
## Unit and component tests with Vitest
Unit tests allow you to test small isolated parts of your code. Integration tests allow you to test parts of your application to see if they work together. If you're using Vite (including via SvelteKit), we recommend using [Vitest](https://vitest.dev/). You can use the Svelte CLI to [setup Vitest](/docs/cli/vitest) either during project creation or later on.
@ -246,7 +246,7 @@ test('Component', async () => {
When writing component tests that involve two-way bindings, context or snippet props, it's best to create a wrapper component for your specific test and interact with that. `@testing-library/svelte` contains some [examples](https://testing-library.com/docs/svelte-testing-library/example).
### Component testing with Storybook
## Component tests with Storybook
[Storybook](https://storybook.js.org) is a tool for developing and documenting UI components, and it can also be used to test your components. They're run with Vitest's browser mode, which renders your components in a real browser for the most realistic testing environment.
@ -288,7 +288,7 @@ You can create stories for component variations and test interactions with the [
/>
```
## E2E tests using Playwright
## End-to-end tests with Playwright
E2E (short for 'end to end') tests allow you to test your full application through the eyes of the user. This section uses [Playwright](https://playwright.dev/) as an example, but you can also use other solutions like [Cypress](https://www.cypress.io/) or [NightwatchJS](https://nightwatchjs.org/).

@ -65,7 +65,7 @@ There will be a blog post about this eventually, but in the meantime, check out
## Is there a UI component library?
There are several UI component libraries as well as standalone components. Find them under the [design systems section of the components page](https://sveltesociety.dev/packages?category=design-system) on the Svelte Society website.
There are several [UI component libraries](/packages#component-libraries) as well as standalone components listed on [the packages page](/packages).
## How do I test Svelte apps?
@ -91,17 +91,9 @@ 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.
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.
However, you can use any router library. A lot of people use [page.js](https://github.com/visionmedia/page.js). There's also [navaid](https://github.com/lukeed/navaid), which is very similar. And [universal-router](https://github.com/kriasoft/universal-router), which is isomorphic with child routes, but without built-in history support.
If you prefer a declarative HTML approach, there's the isomorphic [svelte-routing](https://github.com/EmilTholin/svelte-routing) library and a fork of it called [svelte-navigator](https://github.com/mefechoel/svelte-navigator) containing some additional functionality.
If you need hash-based routing on the client side, check out the [hash option](https://svelte.dev/docs/kit/configuration#router) in SvelteKit, [svelte-spa-router](https://github.com/ItalyPaleAle/svelte-spa-router), or [abstract-state-router](https://github.com/TehShrike/abstract-state-router/).
[Routify](https://routify.dev) is another filesystem-based router, similar to SvelteKit's router. Version 3 supports Svelte's native SSR.
You can see a [community-maintained list of routers on sveltesociety.dev](https://sveltesociety.dev/packages?category=routers).
However, you can use any router library. A sampling of available routers are highlighted [on the packages page](/packages#routing).
## How do I write a mobile app with Svelte?

@ -81,7 +81,7 @@ Coding for the keyboard is important for users with physical disabilities who ca
### a11y_consider_explicit_label
```
Buttons and links should either contain text or have an `aria-label` or `aria-labelledby` attribute
Buttons and links should either contain text or have an `aria-label`, `aria-labelledby` or `title` attribute
```
### a11y_distracting_elements

@ -26,7 +26,7 @@
"bench:debug": "node --allow-natives-syntax --inspect-brk ./benchmarking/run.js"
},
"devDependencies": {
"@changesets/cli": "^2.27.8",
"@changesets/cli": "^2.29.7",
"@sveltejs/eslint-config": "^8.3.3",
"@svitejs/changesets-changelog-github-compact": "^1.1.0",
"@types/node": "^20.11.5",

@ -1,5 +1,57 @@
# svelte
## 5.39.11
### Patch Changes
- fix: flush batches whenever an async value resolves ([#16912](https://github.com/sveltejs/svelte/pull/16912))
## 5.39.10
### Patch Changes
- fix: hydrate each blocks inside element correctly ([#16908](https://github.com/sveltejs/svelte/pull/16908))
- fix: allow await in if block consequent and alternate ([#16890](https://github.com/sveltejs/svelte/pull/16890))
- fix: don't replace rest props with `$$props` for excluded props ([#16898](https://github.com/sveltejs/svelte/pull/16898))
- fix: correctly transform `$derived` private fields on server ([#16894](https://github.com/sveltejs/svelte/pull/16894))
- fix: add `UNKNOWN` evaluation value before breaking for `binding.initial===SnippetBlock` ([#16910](https://github.com/sveltejs/svelte/pull/16910))
## 5.39.9
### Patch Changes
- fix: flush when pending boundaries resolve ([#16897](https://github.com/sveltejs/svelte/pull/16897))
## 5.39.8
### Patch Changes
- fix: check boundary `pending` attribute at runtime on server ([#16855](https://github.com/sveltejs/svelte/pull/16855))
- fix: preserve tuple type in `$state.snapshot` ([#16864](https://github.com/sveltejs/svelte/pull/16864))
- fix: allow await in svelte:boundary without pending ([#16857](https://github.com/sveltejs/svelte/pull/16857))
- fix: update `bind:checked` error message to clarify usage with radio inputs ([#16874](https://github.com/sveltejs/svelte/pull/16874))
## 5.39.7
### Patch Changes
- chore: simplify batch logic ([#16847](https://github.com/sveltejs/svelte/pull/16847))
- fix: rebase pending batches when other batches are committed ([#16866](https://github.com/sveltejs/svelte/pull/16866))
- fix: wrap async `children` in `$$renderer.async` ([#16862](https://github.com/sveltejs/svelte/pull/16862))
- fix: silence label warning for buttons and anchor tags with title attributes ([#16872](https://github.com/sveltejs/svelte/pull/16872))
- fix: coerce nullish `<title>` to empty string ([#16863](https://github.com/sveltejs/svelte/pull/16863))
## 5.39.6
### Patch Changes

@ -66,7 +66,7 @@ Coding for the keyboard is important for users with physical disabilities who ca
## a11y_consider_explicit_label
> Buttons and links should either contain text or have an `aria-label` or `aria-labelledby` attribute
> Buttons and links should either contain text or have an `aria-label`, `aria-labelledby` or `title` attribute
## a11y_distracting_elements

@ -2,7 +2,7 @@
"name": "svelte",
"description": "Cybernetically enhanced web apps",
"license": "MIT",
"version": "5.39.6",
"version": "5.39.11",
"type": "module",
"types": "./types/index.d.ts",
"engines": {
@ -158,7 +158,7 @@
"@types/aria-query": "^5.0.4",
"@types/node": "^20.11.5",
"dts-buddy": "^0.5.5",
"esbuild": "^0.21.5",
"esbuild": "^0.25.10",
"rollup": "^4.22.4",
"source-map": "^0.7.4",
"tinyglobby": "^0.2.12",

@ -85,13 +85,15 @@ declare namespace $state {
? NonReactive<T>
: T extends { toJSON(): infer R }
? R
: T extends Array<infer U>
? Array<Snapshot<U>>
: T extends object
? T extends { [key: string]: any }
? { [K in keyof T]: Snapshot<T[K]> }
: never
: never;
: T extends readonly unknown[]
? { [K in keyof T]: Snapshot<T[K]> }
: T extends Array<infer U>
? Array<Snapshot<U>>
: T extends object
? T extends { [key: string]: any }
? { [K in keyof T]: Snapshot<T[K]> }
: never
: never;
/**
* Declares state that is _not_ made deeply reactive instead of mutating it,

@ -33,7 +33,7 @@ export function BindDirective(node, context) {
e.bind_invalid_target(
node,
node.name,
property.valid_elements.map((valid_element) => `<${valid_element}>`).join(', ')
property.valid_elements.map((valid_element) => `\`<${valid_element}>\``).join(', ')
);
}
@ -67,11 +67,15 @@ export function BindDirective(node, context) {
}
} else {
if (node.name === 'checked' && type?.value[0].data !== 'checkbox') {
e.bind_invalid_target(node, node.name, '<input type="checkbox">');
e.bind_invalid_target(
node,
node.name,
`\`<input type="checkbox">\`${type?.value[0].data === 'radio' ? ` — for \`<input type="radio">\`, use \`bind:group\`` : ''}`
);
}
if (node.name === 'files' && type?.value[0].data !== 'file') {
e.bind_invalid_target(node, node.name, '<input type="file">');
e.bind_invalid_target(node, node.name, '`<input type="file">`');
}
}
}
@ -94,7 +98,7 @@ export function BindDirective(node, context) {
e.bind_invalid_target(
node,
node.name,
`non-<svg> elements. Use 'clientWidth' for <svg> instead`
`non-\`<svg>\` elements. Use \`bind:clientWidth\` for \`<svg>\` instead`
);
}

@ -46,6 +46,21 @@ export function VariableDeclarator(node, context) {
: path.is_rest
? 'rest_prop'
: 'prop';
if (rune === '$props' && binding.kind === 'rest_prop' && node.id.type === 'ObjectPattern') {
const { properties } = node.id;
/** @type {string[]} */
const exclude_props = [];
for (const property of properties) {
if (property.type === 'RestElement') {
continue;
}
const key = /** @type {Identifier | Literal & { value: string | number }} */ (
property.key
);
exclude_props.push(key.type === 'Identifier' ? key.name : key.value.toString());
}
(binding.metadata ??= {}).exclude_props = exclude_props;
}
}
}

@ -382,7 +382,10 @@ export function check_element(node, context) {
}
// element-specific checks
const is_labelled = attribute_map.has('aria-label') || attribute_map.has('aria-labelledby');
const is_labelled =
attribute_map.has('aria-label') ||
attribute_map.has('aria-labelledby') ||
attribute_map.has('title');
switch (node.name) {
case 'a':

@ -192,17 +192,18 @@ function build_assignment(operator, left, right, context) {
path.at(-1) === 'Component' ||
path.at(-1) === 'SvelteComponent' ||
(path.at(-1) === 'ArrowFunctionExpression' &&
path.at(-2) === 'SequenceExpression' &&
(path.at(-3) === 'Component' ||
path.at(-3) === 'SvelteComponent' ||
path.at(-3) === 'BindDirective'))
(path.at(-2) === 'BindDirective' ||
(path.at(-2) === 'Component' && path.at(-3) === 'Fragment') ||
(path.at(-2) === 'SequenceExpression' &&
(path.at(-3) === 'Component' ||
path.at(-3) === 'SvelteComponent' ||
path.at(-3) === 'BindDirective'))))
) {
should_transform = false;
}
if (left.type === 'MemberExpression' && should_transform) {
const callee = callees[operator];
return /** @type {Expression} */ (
context.visit(
b.call(

@ -32,7 +32,11 @@ export function Identifier(node, context) {
grand_parent?.type !== 'AssignmentExpression' &&
grand_parent?.type !== 'UpdateExpression'
) {
return b.id('$$props');
const key = /** @type {Identifier} */ (parent.property);
if (!binding.metadata?.exclude_props?.includes(key.name)) {
return b.id('$$props');
}
}
}

@ -12,8 +12,19 @@ export function TitleElement(node, context) {
/** @type {any} */ (node.fragment.nodes),
context
);
const evaluated = context.state.scope.evaluate(value);
const statement = b.stmt(b.assignment('=', b.id('$.document.title'), value));
const statement = b.stmt(
b.assignment(
'=',
b.id('$.document.title'),
evaluated.is_known
? b.literal(evaluated.value)
: evaluated.is_defined
? value
: b.logical('??', value, b.literal(''))
)
);
if (has_state) {
context.state.update.push(statement);

@ -99,7 +99,14 @@ export function process_children(nodes, initial, is_element, context) {
if (is_static_element(node, context.state)) {
skipped += 1;
} else if (node.type === 'EachBlock' && nodes.length === 1 && is_element) {
} else if (
node.type === 'EachBlock' &&
nodes.length === 1 &&
is_element &&
// In case it's wrapped in async the async logic will want to skip sibling nodes up until the end, hence we cannot make this controlled
// TODO switch this around and instead optimize for elements with a single block child and not require extra comments (neither for async nor normally)
!(node.body.metadata.has_await || node.metadata.expression.has_await)
) {
node.metadata.is_controlled = true;
} else {
const id = flush_node(false, node.type === 'RegularElement' ? node.name : 'node');

@ -209,10 +209,8 @@ export function parse_directive_name(name) {
* @param {import('zimmerframe').Context<AST.SvelteNode, ComponentClientTransformState>} context
*/
export function build_bind_this(expression, value, { state, visit }) {
if (expression.type === 'SequenceExpression') {
const [get, set] = /** @type {SequenceExpression} */ (visit(expression)).expressions;
return b.call('$.bind_this', value, set, get);
}
const [getter, setter] =
expression.type === 'SequenceExpression' ? expression.expressions : [null, null];
/** @type {Identifier[]} */
const ids = [];
@ -229,7 +227,7 @@ export function build_bind_this(expression, value, { state, visit }) {
// Note that we only do this for each context variables, the consequence is that the value might be stale in
// some scenarios where the value is a member expression with changing computed parts or using a combination of multiple
// variables, but that was the same case in Svelte 4, too. Once legacy mode is gone completely, we can revisit this.
walk(expression, null, {
walk(getter ?? expression, null, {
Identifier(node, { path }) {
if (seen.includes(node.name)) return;
seen.push(node.name);
@ -260,9 +258,17 @@ export function build_bind_this(expression, value, { state, visit }) {
const child_state = { ...state, transform };
const get = /** @type {Expression} */ (visit(expression, child_state));
const set = /** @type {Expression} */ (
visit(b.assignment('=', expression, b.id('$$value')), child_state)
let get = /** @type {Expression} */ (visit(getter ?? expression, child_state));
let set = /** @type {Expression} */ (
visit(
setter ??
b.assignment(
'=',
/** @type {Identifier | MemberExpression} */ (expression),
b.id('$$value')
),
child_state
)
);
// If we're mutating a property, then it might already be non-existent.
@ -275,13 +281,25 @@ export function build_bind_this(expression, value, { state, visit }) {
node = node.object;
}
return b.call(
'$.bind_this',
value,
b.arrow([b.id('$$value'), ...ids], set),
b.arrow([...ids], get),
values.length > 0 && b.thunk(b.array(values))
);
get =
get.type === 'ArrowFunctionExpression'
? b.arrow([...ids], get.body)
: get.type === 'FunctionExpression'
? b.function(null, [...ids], get.body)
: getter
? get
: b.arrow([...ids], get);
set =
set.type === 'ArrowFunctionExpression'
? b.arrow([set.params[0] ?? b.id('_'), ...ids], set.body)
: set.type === 'FunctionExpression'
? b.function(null, [set.params[0] ?? b.id('_'), ...ids], set.body)
: setter
? set
: b.arrow([b.id('$$value'), ...ids], set);
return b.call('$.bind_this', value, set, get, values.length > 0 && b.thunk(b.array(values)));
}
/**

@ -32,7 +32,9 @@ export function EachBlock(node, context) {
each.push(b.let(node.index, index));
}
each.push(.../** @type {BlockStatement} */ (context.visit(node.body)).body);
const new_body = /** @type {BlockStatement} */ (context.visit(node.body)).body;
each.push(...(node.body.metadata.has_await ? [create_async_block(b.block(new_body))] : new_body));
const for_loop = b.for(
b.declaration('let', [
@ -55,7 +57,7 @@ export function EachBlock(node, context) {
b.if(
b.binary('!==', b.member(array_id, 'length'), b.literal(0)),
b.block([open, for_loop]),
fallback
node.fallback.metadata.has_await ? create_async_block(fallback) : fallback
)
);
} else {

@ -23,7 +23,11 @@ export function IfBlock(node, context) {
/** @type {Statement} */
let statement = b.if(test, consequent, alternate);
if (node.metadata.expression.has_await) {
if (
node.metadata.expression.has_await ||
node.consequent.metadata.has_await ||
node.alternate?.metadata.has_await
) {
statement = create_async_block(b.block([statement]));
}

@ -7,11 +7,7 @@ import * as b from '#compiler/builders';
* @param {Context} context
*/
export function MemberExpression(node, context) {
if (
context.state.analysis.runes &&
node.object.type === 'ThisExpression' &&
node.property.type === 'PrivateIdentifier'
) {
if (context.state.analysis.runes && node.property.type === 'PrivateIdentifier') {
const field = context.state.state_fields?.get(`#${node.property.name}`);
if (field?.type === '$derived' || field?.type === '$derived.by') {

@ -2,7 +2,14 @@
/** @import { AST } from '#compiler' */
/** @import { ComponentContext } from '../types' */
import * as b from '#compiler/builders';
import { block_close, block_open, block_open_else, build_attribute_value } from './shared/utils.js';
import {
block_close,
block_open,
block_open_else,
build_attribute_value,
build_template,
create_async_block
} from './shared/utils.js';
/**
* @param {AST.SvelteBoundary} node
@ -13,6 +20,11 @@ export function SvelteBoundary(node, context) {
const pending_attribute = /** @type {AST.Attribute} */ (
node.attributes.find((node) => node.type === 'Attribute' && node.name === 'pending')
);
const is_pending_attr_nullish =
pending_attribute &&
typeof pending_attribute.value === 'object' &&
!Array.isArray(pending_attribute.value) &&
!context.state.scope.evaluate(pending_attribute.value.expression).is_defined;
const pending_snippet = /** @type {AST.SnippetBlock} */ (
node.fragment.nodes.find(
@ -21,22 +33,46 @@ export function SvelteBoundary(node, context) {
);
if (pending_attribute || pending_snippet) {
const pending = pending_attribute
? b.call(
build_attribute_value(
pending_attribute.value,
context,
(expression) => expression,
false,
true
),
b.id('$$renderer')
if (pending_attribute && is_pending_attr_nullish && !pending_snippet) {
const callee = build_attribute_value(
pending_attribute.value,
context,
(expression) => expression,
false,
true
);
const pending = b.call(callee, b.id('$$renderer'));
const block = /** @type {BlockStatement} */ (context.visit(node.fragment));
const statement = node.fragment.metadata.has_await
? create_async_block(b.block([block]))
: block;
context.state.template.push(
b.if(
callee,
b.block([b.stmt(pending)]),
b.block(build_template([block_open, statement, block_close]))
)
: /** @type {BlockStatement} */ (context.visit(pending_snippet.body));
context.state.template.push(block_open_else, pending, block_close);
);
} else {
const pending = pending_attribute
? b.call(
build_attribute_value(
pending_attribute.value,
context,
(expression) => expression,
false,
true
),
b.id('$$renderer')
)
: /** @type {BlockStatement} */ (context.visit(pending_snippet.body));
context.state.template.push(block_open_else, pending, block_close);
}
} else {
const block = /** @type {BlockStatement} */ (context.visit(node.fragment));
context.state.template.push(block_open, block, block_close);
const statement = node.fragment.metadata.has_await
? create_async_block(b.block([block]))
: block;
context.state.template.push(block_open, statement, block_close);
}
}

@ -242,7 +242,12 @@ export function build_inline_component(node, expression, context) {
params.push(pattern);
}
const slot_fn = b.arrow(params, b.block(block.body));
const slot_fn = b.arrow(
params,
node.fragment.metadata.has_await
? b.block([create_async_block(b.block(block.body))])
: b.block(block.body)
);
if (slot_name === 'default' && !has_children_prop) {
if (

@ -122,7 +122,7 @@ export class Binding {
/**
* Additional metadata, varies per binding type
* @type {null | { inside_rest?: boolean; is_template_declaration?: boolean }}
* @type {null | { inside_rest?: boolean; is_template_declaration?: boolean; exclude_props?: string[] }}
*/
metadata = null;
@ -260,6 +260,13 @@ class Evaluation {
break;
}
if (binding.initial?.type === 'SnippetBlock') {
this.is_defined = true;
this.is_known = false;
this.values.add(UNKNOWN);
break;
}
if (!binding.updated && binding.initial !== null && !is_prop) {
binding.scope.evaluate(/** @type {Expression} */ (binding.initial), this.values);
break;

@ -174,11 +174,11 @@ export function a11y_click_events_have_key_events(node) {
}
/**
* Buttons and links should either contain text or have an `aria-label` or `aria-labelledby` attribute
* Buttons and links should either contain text or have an `aria-label`, `aria-labelledby` or `title` attribute
* @param {null | NodeLike} node
*/
export function a11y_consider_explicit_label(node) {
w(node, 'a11y_consider_explicit_label', `Buttons and links should either contain text or have an \`aria-label\` or \`aria-labelledby\` attribute\nhttps://svelte.dev/e/a11y_consider_explicit_label`);
w(node, 'a11y_consider_explicit_label', `Buttons and links should either contain text or have an \`aria-label\`, \`aria-labelledby\` or \`title\` attribute\nhttps://svelte.dev/e/a11y_consider_explicit_label`);
}
/**

@ -292,6 +292,13 @@ export class Boundary {
this.#anchor.before(this.#offscreen_fragment);
this.#offscreen_fragment = null;
}
// TODO this feels like a little bit of a kludge, but until we
// overhaul the boundary/batch relationship it's probably
// the most pragmatic solution available to us
queue_micro_task(() => {
Batch.ensure().flush();
});
}
}

@ -52,8 +52,6 @@ export function flatten(sync, async, fn) {
Promise.all(async.map((expression) => async_derived(expression)))
.then((result) => {
batch?.activate();
restore();
try {

@ -1,4 +1,4 @@
/** @import { Derived, Effect, Source } from '#client' */
/** @import { Derived, Effect, Source, Value } from '#client' */
import {
BLOCK_EFFECT,
BRANCH_EFFECT,
@ -10,10 +10,11 @@ import {
INERT,
RENDER_EFFECT,
ROOT_EFFECT,
MAYBE_DIRTY
MAYBE_DIRTY,
DERIVED
} from '#client/constants';
import { async_mode_flag } from '../../flags/index.js';
import { deferred, define_property } from '../../shared/utils.js';
import { deferred, define_property, noop } from '../../shared/utils.js';
import {
active_effect,
is_dirty,
@ -97,22 +98,8 @@ export class Batch {
#deferred = null;
/**
* True if an async effect inside this batch resolved and
* its parent branch was already deleted
*/
#neutered = false;
/**
* Async effects (created inside `async_derived`) encountered during processing.
* These run after the rest of the batch has updated, since they should
* always have the latest values
* @type {Effect[]}
*/
#async_effects = [];
/**
* The same as `#async_effects`, but for effects inside a newly-created
* `<svelte:boundary>` these do not prevent the batch from committing
* Async effects inside a newly-created `<svelte:boundary>`
* these do not prevent the batch from committing
* @type {Effect[]}
*/
#boundary_async_effects = [];
@ -165,32 +152,7 @@ export class Batch {
previous_batch = null;
/** @type {Map<Source, { v: unknown, wv: number }> | null} */
var current_values = null;
// if there are multiple batches, we are 'time travelling' —
// we need to undo the changes belonging to any batch
// other than the current one
if (async_mode_flag && batches.size > 1) {
current_values = new Map();
batch_deriveds = new Map();
for (const [source, current] of this.current) {
current_values.set(source, { v: source.v, wv: source.wv });
source.v = current;
}
for (const batch of batches) {
if (batch === this) continue;
for (const [source, previous] of batch.#previous) {
if (!current_values.has(source)) {
current_values.set(source, { v: source.v, wv: source.wv });
source.v = previous;
}
}
}
}
var revert = Batch.apply(this);
for (const root of root_effects) {
this.#traverse_effect_tree(root);
@ -198,7 +160,7 @@ export class Batch {
// if we didn't start any new async work, and no async work
// is outstanding from a previous flush, commit
if (this.#async_effects.length === 0 && this.#pending === 0) {
if (this.#pending === 0) {
this.#commit();
var render_effects = this.#render_effects;
@ -210,12 +172,14 @@ export class Batch {
// If sources are written to, then work needs to happen in a separate batch, else prior sources would be mixed with
// newly updated sources, which could lead to infinite loops when effects run over and over again.
previous_batch = current_batch;
previous_batch = this;
current_batch = null;
flush_queued_effects(render_effects);
flush_queued_effects(effects);
previous_batch = null;
this.#deferred?.resolve();
} else {
this.#defer_effects(this.#render_effects);
@ -223,27 +187,12 @@ export class Batch {
this.#defer_effects(this.#block_effects);
}
if (current_values) {
for (const [source, { v, wv }] of current_values) {
// reset the source to the current value (unless
// it got a newer value as a result of effects running)
if (source.wv <= wv) {
source.v = v;
}
}
batch_deriveds = null;
}
for (const effect of this.#async_effects) {
update_effect(effect);
}
revert();
for (const effect of this.#boundary_async_effects) {
update_effect(effect);
}
this.#async_effects = [];
this.#boundary_async_effects = [];
}
@ -272,12 +221,8 @@ export class Batch {
} else if (async_mode_flag && (flags & RENDER_EFFECT) !== 0) {
this.#render_effects.push(effect);
} else if ((flags & CLEAN) === 0) {
if ((flags & ASYNC) !== 0) {
var effects = effect.b?.is_pending()
? this.#boundary_async_effects
: this.#async_effects;
effects.push(effect);
if ((flags & ASYNC) !== 0 && effect.b?.is_pending()) {
this.#boundary_async_effects.push(effect);
} else if (is_dirty(effect)) {
if ((effect.f & BLOCK_EFFECT) !== 0) this.#block_effects.push(effect);
update_effect(effect);
@ -337,21 +282,6 @@ export class Batch {
deactivate() {
current_batch = null;
previous_batch = null;
for (const update of effect_pending_updates) {
effect_pending_updates.delete(update);
update();
if (current_batch !== null) {
// only do one at a time
break;
}
}
}
neuter() {
this.#neutered = true;
}
flush() {
@ -368,19 +298,74 @@ export class Batch {
}
this.deactivate();
for (const update of effect_pending_updates) {
effect_pending_updates.delete(update);
update();
if (current_batch !== null) {
// only do one at a time
break;
}
}
}
/**
* Append and remove branches to/from the DOM
*/
#commit() {
if (!this.#neutered) {
for (const fn of this.#callbacks) {
fn();
}
for (const fn of this.#callbacks) {
fn();
}
this.#callbacks.clear();
// If there are other pending batches, they now need to be 'rebased' —
// in other words, we re-run block/async effects with the newly
// committed state, unless the batch in question has a more
// recent value for a given source
if (batches.size > 1) {
this.#previous.clear();
let is_earlier = true;
for (const batch of batches) {
if (batch === this) {
is_earlier = false;
continue;
}
for (const [source, value] of this.current) {
if (batch.current.has(source)) {
if (is_earlier) {
// bring the value up to date
batch.current.set(source, value);
} else {
// later batch has more recent value,
// no need to re-run these effects
continue;
}
}
mark_effects(source);
}
if (queued_root_effects.length > 0) {
current_batch = batch;
const revert = Batch.apply(batch);
for (const root of queued_root_effects) {
batch.#traverse_effect_tree(root);
}
queued_root_effects = [];
revert();
}
}
current_batch = null;
}
batches.delete(this);
}
@ -391,24 +376,17 @@ export class Batch {
decrement() {
this.#pending -= 1;
if (this.#pending === 0) {
for (const e of this.#dirty_effects) {
set_signal_status(e, DIRTY);
schedule_effect(e);
}
for (const e of this.#maybe_dirty_effects) {
set_signal_status(e, MAYBE_DIRTY);
schedule_effect(e);
}
this.#render_effects = [];
this.#effects = [];
for (const e of this.#dirty_effects) {
set_signal_status(e, DIRTY);
schedule_effect(e);
}
this.flush();
} else {
this.deactivate();
for (const e of this.#maybe_dirty_effects) {
set_signal_status(e, MAYBE_DIRTY);
schedule_effect(e);
}
this.flush();
}
/** @param {() => void} fn */
@ -444,6 +422,51 @@ export class Batch {
static enqueue(task) {
queue_micro_task(task);
}
/**
* @param {Batch} current_batch
*/
static apply(current_batch) {
if (!async_mode_flag || batches.size === 1) {
return noop;
}
// if there are multiple batches, we are 'time travelling' —
// we need to undo the changes belonging to any batch
// other than the current one
/** @type {Map<Source, { v: unknown, wv: number }>} */
var current_values = new Map();
batch_deriveds = new Map();
for (const [source, current] of current_batch.current) {
current_values.set(source, { v: source.v, wv: source.wv });
source.v = current;
}
for (const batch of batches) {
if (batch === current_batch) continue;
for (const [source, previous] of batch.#previous) {
if (!current_values.has(source)) {
current_values.set(source, { v: source.v, wv: source.wv });
source.v = previous;
}
}
}
return () => {
for (const [source, { v, wv }] of current_values) {
// reset the source to the current value (unless
// it got a newer value as a result of effects running)
if (source.wv <= wv) {
source.v = v;
}
}
batch_deriveds = null;
};
}
}
/**
@ -615,6 +638,26 @@ function flush_queued_effects(effects) {
eager_block_effects = null;
}
/**
* This is similar to `mark_reactions`, but it only marks async/block effects
* so that these can re-run after another batch has been committed
* @param {Value} value
*/
function mark_effects(value) {
if (value.reactions !== null) {
for (const reaction of value.reactions) {
const flags = reaction.f;
if ((flags & DERIVED) !== 0) {
mark_effects(/** @type {Derived} */ (reaction));
} else if ((flags & (ASYNC | BLOCK_EFFECT)) !== 0) {
set_signal_status(reaction, DIRTY);
schedule_effect(/** @type {Effect} */ (reaction));
}
}
}
}
/**
* @param {Effect} signal
* @returns {void}

@ -26,7 +26,7 @@ import {
import { equals, safe_equals } from './equality.js';
import * as e from '../errors.js';
import * as w from '../warnings.js';
import { async_effect, destroy_effect } from './effects.js';
import { async_effect, destroy_effect, teardown } from './effects.js';
import { inspect_effects, internal_set, set_inspect_effects, source } from './sources.js';
import { get_stack } from '../dev/tracing.js';
import { async_mode_flag, tracing_mode_flag } from '../../flags/index.js';
@ -35,6 +35,7 @@ import { component_context } from '../context.js';
import { UNINITIALIZED } from '../../../constants.js';
import { batch_deriveds, current_batch } from './batch.js';
import { unset_context } from './async.js';
import { deferred } from '../../shared/utils.js';
/** @type {Effect | null} */
export let current_async_effect = null;
@ -109,37 +110,40 @@ export function async_derived(fn, location) {
var promise = /** @type {Promise<V>} */ (/** @type {unknown} */ (undefined));
var signal = source(/** @type {V} */ (UNINITIALIZED));
/** @type {Promise<V> | null} */
var prev = null;
// only suspend in async deriveds created on initialisation
var should_suspend = !active_reaction;
/** @type {Map<Batch, ReturnType<typeof deferred<V>>>} */
var deferreds = new Map();
async_effect(() => {
if (DEV) current_async_effect = active_effect;
/** @type {ReturnType<typeof deferred<V>>} */
var d = deferred();
promise = d.promise;
try {
var p = fn();
// Make sure to always access the then property to read any signals
// it might access, so that we track them as dependencies.
if (prev) Promise.resolve(p).catch(() => {}); // avoid unhandled rejection
// If this code is changed at some point, make sure to still access the then property
// of fn() to read any signals it might access, so that we track them as dependencies.
Promise.resolve(fn()).then(d.resolve, d.reject);
} catch (error) {
p = Promise.reject(error);
d.reject(error);
}
if (DEV) current_async_effect = null;
var r = () => p;
promise = prev?.then(r, r) ?? Promise.resolve(p);
prev = promise;
var batch = /** @type {Batch} */ (current_batch);
var pending = boundary.is_pending();
if (should_suspend) {
boundary.update_pending_count(1);
if (!pending) batch.increment();
if (!pending) {
batch.increment();
deferreds.get(batch)?.reject(STALE_REACTION);
deferreds.set(batch, d);
}
}
/**
@ -147,8 +151,6 @@ export function async_derived(fn, location) {
* @param {unknown} error
*/
const handler = (value, error = undefined) => {
prev = null;
current_async_effect = null;
if (!pending) batch.activate();
@ -187,12 +189,12 @@ export function async_derived(fn, location) {
unset_context();
};
promise.then(handler, (e) => handler(null, e || 'unknown'));
d.promise.then(handler, (e) => handler(null, e || 'unknown'));
});
if (batch) {
return () => {
queueMicrotask(() => batch.neuter());
};
teardown(() => {
for (const d of deferreds.values()) {
d.reject(STALE_REACTION);
}
});

@ -4,5 +4,5 @@
* The current version, as set in package.json.
* @type {string}
*/
export const VERSION = '5.39.6';
export const VERSION = '5.39.11';
export const PUBLIC_VERSION = '5';

@ -3,6 +3,6 @@ import { test } from '../../test';
export default test({
error: {
code: 'bind_invalid_target',
message: '`bind:value` can only be used with <input>, <textarea>, <select>'
message: '`bind:value` can only be used with `<input>`, `<textarea>`, `<select>`'
}
});

@ -0,0 +1,11 @@
import { tick } from 'svelte';
import { test } from '../../test';
export default test({
mode: ['client', 'hydrate'],
async test({ assert, target }) {
await tick();
assert.htmlEqual(target.innerHTML, '<div>attachment ran</div>');
}
});

@ -0,0 +1,9 @@
{#if await true}
<div
{@attach (node) => {
node.textContent = 'attachment ran';
}}
>
attachment did not run
</div>
{/if}

@ -14,17 +14,6 @@ export default test({
const [reset, a, b, increment] = target.querySelectorAll('button');
a.click();
// TODO why is this necessary? why isn't `await tick()` enough?
await Promise.resolve();
await Promise.resolve();
await Promise.resolve();
await Promise.resolve();
await Promise.resolve();
await Promise.resolve();
await Promise.resolve();
await Promise.resolve();
flushSync();
await tick();
assert.htmlEqual(
target.innerHTML,

@ -0,0 +1,16 @@
import { tick } from 'svelte';
import { test } from '../../test';
export default test({
mode: ['async-server', 'hydrate', 'client'],
ssrHtml: `<ul><li>1</li></ul> <button>add</button>`,
async test({ assert, target }) {
await tick();
const [add] = target.querySelectorAll('button');
add.click();
await tick();
assert.htmlEqual(target.innerHTML, `<ul><li>1</li><li>2</li></ul> <button>add</button>`);
}
});

@ -0,0 +1,11 @@
<script>
let array = $state(Promise.resolve([1]));
</script>
<ul>
{#each await array as item}
<li>{item}</li>
{/each}
</ul>
<button onclick={() => array = Promise.resolve([1, 2])}>add</button>

@ -2,30 +2,46 @@ import { tick } from 'svelte';
import { test } from '../../test';
export default test({
html: `<button>reset</button><button>true</button><button>false</button><p>pending</p>`,
html: `<button>shift</button><button>true</button><button>false</button><p>pending</p>`,
async test({ assert, target }) {
const [reset, t, f] = target.querySelectorAll('button');
const [shift, t, f] = target.querySelectorAll('button');
shift.click();
await tick();
assert.htmlEqual(
target.innerHTML,
'<button>shift</button><button>true</button><button>false</button><h1>yes</h1>'
);
f.click();
await tick();
t.click();
await tick();
f.click();
await tick();
shift.click();
await tick();
assert.htmlEqual(
target.innerHTML,
'<button>reset</button><button>true</button><button>false</button><h1>yes</h1>'
'<button>shift</button><button>true</button><button>false</button><h1>no</h1>'
);
reset.click();
shift.click();
await tick();
assert.htmlEqual(
target.innerHTML,
'<button>reset</button><button>true</button><button>false</button><h1>yes</h1>'
'<button>shift</button><button>true</button><button>false</button><h1>yes</h1>'
);
f.click();
shift.click();
await tick();
assert.htmlEqual(
target.innerHTML,
'<button>reset</button><button>true</button><button>false</button><h1>no</h1>'
'<button>shift</button><button>true</button><button>false</button><h1>no</h1>'
);
}
});

@ -1,13 +1,24 @@
<script>
let deferred = $state(Promise.withResolvers());
let condition = $state(true);
let deferreds = [];
function push(value) {
const deferred = Promise.withResolvers();
deferreds.push({ deferred, value });
return deferred.promise;
}
</script>
<button onclick={() => deferred = Promise.withResolvers()}>reset</button>
<button onclick={() => deferred.resolve(true)}>true</button>
<button onclick={() => deferred.resolve(false)}>false</button>
<button onclick={() => {
const d = deferreds.shift();
d?.deferred.resolve(d.value);
}}>shift</button>
<button onclick={() => condition = true}>true</button>
<button onclick={() => condition = false}>false</button>
<svelte:boundary>
{#if await deferred.promise}
{#if await push(condition)}
<h1>yes</h1>
{:else}
<h1>no</h1>

@ -0,0 +1,57 @@
import { tick } from 'svelte';
import { test } from '../../test';
export default test({
async test({ assert, target }) {
const shift = document.querySelector('button');
shift?.click();
await tick();
shift?.click();
await tick();
assert.htmlEqual(
target.innerHTML,
`
<p>true</p>
<button>toggle</button>
<button>shift</button>
`
);
const toggle = target.querySelector('button');
toggle?.click();
await tick();
assert.htmlEqual(
target.innerHTML,
`
<p>true</p>
<button>toggle</button>
<button>shift</button>
`
);
shift?.click();
await tick();
assert.htmlEqual(
target.innerHTML,
`
<p>true</p>
<button>toggle</button>
<button>shift</button>
`
);
shift?.click();
await tick();
assert.htmlEqual(
target.innerHTML,
`
<button>toggle</button>
<button>shift</button>
`
);
}
});

@ -0,0 +1,41 @@
<script>
let show = $state(true);
let count = $state(0);
let queue = [];
function foo() {
const {promise, resolve} = Promise.withResolvers();
const s = show;
queue.push(() => resolve(s));
return promise;
}
function bar() {
const {promise, resolve} = Promise.withResolvers();
const s = show;
queue.push(() => {
// This will create a new batch while the other batch is still in flight
count++
resolve(s);
});
return promise;
}
$effect(() => { count; });
</script>
<svelte:boundary>
{#if await foo()}
<p>{await bar()}</p>
{/if}
<button onclick={() => {
show = !show
}}>toggle</button>
{#snippet pending()}
<p>loading...</p>
{/snippet}
</svelte:boundary>
<button onclick={() => queue.shift()()}>shift</button>

@ -3,26 +3,24 @@ import { test } from '../../test';
export default test({
async test({ assert, target }) {
const [a, b, reset1, reset2, resolve1, resolve2] = target.querySelectorAll('button');
const [a, b, shift, pop] = target.querySelectorAll('button');
resolve1.click();
shift.click();
await tick();
const p = /** @type {HTMLElement} */ (target.querySelector('#test'));
assert.htmlEqual(p.innerHTML, '1 + 2 = 3');
flushSync(() => reset1.click());
flushSync(() => a.click());
flushSync(() => reset2.click());
flushSync(() => b.click());
resolve2.click();
pop.click();
await tick();
assert.htmlEqual(p.innerHTML, '1 + 2 = 3');
assert.htmlEqual(p.innerHTML, '1 + 3 = 4');
resolve1.click();
pop.click();
await tick();
assert.htmlEqual(p.innerHTML, '2 + 3 = 5');

@ -1,14 +1,14 @@
<script>
let delay = 1000;
let deferreds = [];
let a = $state(1);
let b = $state(2);
let d1 = Promise.withResolvers();
let d2 = Promise.withResolvers();
let deferred = d1;
async function push(a, b) {
var d = Promise.withResolvers();
deferreds.push(d);
await d.promise;
async function add(a, b) {
await deferred.promise;
return a + b;
}
</script>
@ -16,14 +16,11 @@
<button onclick={() => a++}>a++</button>
<button onclick={() => b++}>b++</button>
<button onclick={() => deferred = d1 = Promise.withResolvers()}>reset 1</button>
<button onclick={() => deferred = d2 = Promise.withResolvers()}>reset 2</button>
<button onclick={() => d1.resolve()}>resolve 1</button>
<button onclick={() => d2.resolve()}>resolve 2</button>
<button onclick={() => deferreds.shift()?.resolve()}>shift</button>
<button onclick={() => deferreds.pop()?.resolve()}>pop</button>
<svelte:boundary>
<p id="test">{a} + {b} = {await add(a, b)}</p>
<p id="test">{a} + {b} = {await push(a, b)}</p>
{#snippet pending()}
<p>loading...</p>

@ -0,0 +1,16 @@
import { flushSync } from 'svelte';
import { test } from '../../test';
export default test({
async test({ assert, target }) {
const [btn] = target.querySelectorAll('button');
flushSync(() => {
btn.click();
});
assert.htmlEqual(
target.innerHTML,
'<button>Shuffle</button> <br> <b>5</b><b>1</b><b>4</b><b>2</b><b>3</b> <br> 51423'
);
}
});

@ -0,0 +1,13 @@
<script>
let arr = $state([1, 2, 3, 4, 5]);
let elements = $state([]);
</script>
<button onclick={() => arr = [5, 1, 4, 2, 3]}>Shuffle</button><br>
{#each arr as item, i (item)}
<b bind:this={() => elements[i], (v) => elements[i] = v }>{item}</b>
{/each}
<br>
{#each elements as elem}
{elem.textContent}
{/each}

@ -9,7 +9,8 @@
}
get embiggened1() {
return this.#doubled;
const self = this;
return self.#doubled;
}
get embiggened2() {

@ -0,0 +1,5 @@
<script>
let { test } = $props();
</script>
{@render test?.()}

@ -0,0 +1,10 @@
import { flushSync } from 'svelte';
import { test } from '../../test';
export default test({
async test({ assert, target }) {
const btn = target.querySelector('button');
flushSync(() => btn?.click());
assert.htmlEqual(target.innerHTML, `<button></button><p>snip</p>`);
}
});

@ -0,0 +1,15 @@
<script>
import Component from "./Component.svelte";
let count = $state(0);
</script>
{#snippet snip()}
<p>snip</p>
{/snippet}
<button onclick={() => count++}></button>
{#if true}
{@const test = count % 2 === 0 ? undefined: snip}
<Component {test} />
{/if}

@ -0,0 +1,7 @@
import { test } from '../../test';
export default test({
async test({ assert, target }) {
assert.equal(target.textContent, ' false');
}
});

@ -0,0 +1,4 @@
<script>
const { name, ...rest } = $props();
</script>
{rest.name} {'name' in rest}

@ -0,0 +1,4 @@
<script>
import Component from './component.svelte';
</script>
<Component name='world' />

@ -0,0 +1,7 @@
import { test } from '../../test';
export default test({
test({ assert, target }) {
assert.equal(target.ownerDocument.title, '');
}
});

@ -0,0 +1,6 @@
<script>
const thing = {};
</script>
<svelte:head>
<title>{thing.thing}</title>
</svelte:head>

@ -0,0 +1,4 @@
<script>
const { children } = $props();
</script>
{@render children()}

@ -0,0 +1,7 @@
<script>
import Component from "./component.svelte";
</script>
<Component>
{@const one = await 1}
{one}
</Component>

@ -0,0 +1,10 @@
{#if false}
{@const one = await 1}
{one}
{:else if false}
{@const two = await 2}
{two}
{:else}
{@const three = await 3}
{three}
{/if}

@ -0,0 +1,6 @@
<script>
let pending = null;
</script>
<svelte:boundary {pending}>
{await 'awaited'}
</svelte:boundary>

@ -0,0 +1,4 @@
<svelte:boundary>
{@const x = await 'this should work'}
<div>{x}</div>
</svelte:boundary>

@ -0,0 +1,3 @@
import { test } from '../../test';
export default test({});

@ -0,0 +1,8 @@
{#snippet pending()}
Loading...
{/snippet}
<svelte:boundary pending={pending}>
{@const data = await Promise.resolve('hello')}
<p>{data}</p>
</svelte:boundary>

@ -0,0 +1,5 @@
import { test } from '../../test';
export default test({
mode: ['async']
});

@ -0,0 +1 @@
<!--[--><!--[--><!--[--><!---->each<!--]--><!--]--> <!--[--><!--[!--><!---->else<!--]--><!--]--><!--]-->

@ -0,0 +1,11 @@
{#each { length: 1 }}
{@const data = await Promise.resolve("each")}
{data}
{/each}
{#each { length: 0 }}
should not see this
{:else}
{@const data = await Promise.resolve("else")}
{data}
{/each}

@ -1,7 +1,7 @@
[
{
"code": "a11y_consider_explicit_label",
"message": "Buttons and links should either contain text or have an `aria-label` or `aria-labelledby` attribute",
"message": "Buttons and links should either contain text or have an `aria-label`, `aria-labelledby` or `title` attribute",
"start": {
"line": 1,
"column": 0

@ -4,6 +4,9 @@
<button aria-label="Valid empty button"></button>
<a href="/#" aria-label="Valid empty link"></a>
<button title="Valid empty button"></button>
<a href="/#" title="Valid empty link"></a>
<button aria-hidden='true'></button>
<button inert></button>
<a href="/#" aria-hidden='true'><b></b></a>

@ -1,7 +1,7 @@
[
{
"code": "a11y_consider_explicit_label",
"message": "Buttons and links should either contain text or have an `aria-label` or `aria-labelledby` attribute",
"message": "Buttons and links should either contain text or have an `aria-label`, `aria-labelledby` or `title` attribute",
"start": {
"line": 1,
"column": 0
@ -13,7 +13,7 @@
},
{
"code": "a11y_consider_explicit_label",
"message": "Buttons and links should either contain text or have an `aria-label` or `aria-labelledby` attribute",
"message": "Buttons and links should either contain text or have an `aria-label`, `aria-labelledby` or `title` attribute",
"start": {
"line": 2,
"column": 0

@ -1,7 +1,7 @@
[
{
"code": "bind_invalid_target",
"message": "`bind:offsetWidth` can only be used with non-<svg> elements. Use 'clientWidth' for <svg> instead",
"message": "`bind:offsetWidth` can only be used with non-`<svg>` elements. Use `bind:clientWidth` for `<svg>` instead",
"start": {
"line": 5,
"column": 5

@ -1,7 +1,7 @@
[
{
"code": "bind_invalid_target",
"message": "`bind:checked` can only be used with <input type=\"checkbox\">",
"message": "`bind:checked` can only be used with `<input type=\"checkbox\">`",
"start": {
"line": 5,
"column": 7

@ -1,7 +1,7 @@
[
{
"code": "bind_invalid_target",
"message": "`bind:open` can only be used with <details>",
"message": "`bind:open` can only be used with `<details>`",
"start": {
"line": 5,
"column": 5

@ -1,7 +1,7 @@
[
{
"code": "bind_invalid_target",
"message": "`bind:value` can only be used with <input>, <textarea>, <select>",
"message": "`bind:value` can only be used with `<input>`, `<textarea>`, `<select>`",
"start": {
"line": 5,
"column": 5

@ -0,0 +1,14 @@
[
{
"code": "bind_invalid_target",
"message": "`bind:checked` can only be used with `<input type=\"checkbox\">` — for `<input type=\"radio\">`, use `bind:group`",
"start": {
"line": 5,
"column": 20
},
"end": {
"line": 5,
"column": 38
}
}
]

@ -0,0 +1,5 @@
<script>
let foo;
</script>
<input type="radio" bind:checked={foo}>

@ -3171,13 +3171,15 @@ declare namespace $state {
? NonReactive<T>
: T extends { toJSON(): infer R }
? R
: T extends Array<infer U>
? Array<Snapshot<U>>
: T extends object
? T extends { [key: string]: any }
? { [K in keyof T]: Snapshot<T[K]> }
: never
: never;
: T extends readonly unknown[]
? { [K in keyof T]: Snapshot<T[K]> }
: T extends Array<infer U>
? Array<Snapshot<U>>
: T extends object
? T extends { [key: string]: any }
? { [K in keyof T]: Snapshot<T[K]> }
: never
: never;
/**
* Declares state that is _not_ made deeply reactive instead of mutating it,

File diff suppressed because it is too large Load Diff
Loading…
Cancel
Save