Merge remote-tracking branch 'origin' into elliott/resources

elliott/resources
Elliott Johnson 1 week ago
commit 61d02fe850

@ -11,14 +11,13 @@ jobs:
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: |
@ -57,7 +56,7 @@ jobs:
}
- name: Get PR Data
uses: actions/github-script@v7
uses: actions/github-script@v8
id: get-pr-data
with:
script: |
@ -67,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,
@ -85,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()

@ -85,8 +85,9 @@ Derived expressions are recalculated when their dependencies change, but you can
Unlike `$state`, which converts objects and arrays to [deeply reactive proxies]($state#Deep-state), `$derived` values are left as-is. For example, [in a case like this](/playground/untitled#H4sIAAAAAAAAE4VU22rjMBD9lUHd3aaQi9PdstS1A3t5XvpQ2Ic4D7I1iUUV2UjjNMX431eS7TRdSosxgjMzZ45mjt0yzffIYibvy0ojFJWqDKCQVBk2ZVup0LJ43TJ6rn2aBxw-FP2o67k9oCKP5dziW3hRaUJNjoYltjCyplWmM1JIIAn3FlL4ZIkTTtYez6jtj4w8WwyXv9GiIXiQxLVs9pfTMR7EuoSLIuLFbX7Z4930bZo_nBrD1bs834tlfvsBz9_SyX6PZXu9XaL4gOWn4sXjeyzftv4ZWfyxubpzxzg6LfD4MrooxELEosKCUPigQCMPKCZh0OtQE1iSxcsmdHuBvCiHZXALLXiN08EL3RRkaJ_kDVGle0HcSD5TPEeVtj67O4Nrg9aiSNtBY5oODJkrL5QsHtN2cgXp6nSJMWzpWWGasdlsGEMbzi5jPr5KFr0Ep7pdeM2-TCelCddIhDxAobi1jqF3cMaC1RKp64bAW9iFAmXGIHfd4wNXDabtOLN53w8W53VvJoZLh7xk4Rr3CoL-UNoLhWHrT1JQGcM17u96oES5K-kc2XOzkzqGCKL5De79OUTyyrg1zgwXsrEx3ESfx4Bz0M5UjVMHB24mw9SuXtXFoN13fYKOM1tyUT3FbvbWmSWCZX2Er-41u5xPoml45svRahl9Wb9aasbINJixDZwcPTbyTLZSUsAvrg_cPuCR7s782_WU8343Y72Qtlb8OYatwuOQvuN13M_hJKNfxann1v1U_B1KZ_D_mzhzhz24fw85CSz2irtN9w9HshBK7AQAAA==)...
```svelte
let items = $state([...]);
```js
// @errors: 7005
let items = $state([ /*...*/ ]);
let index = $state(0);
let selected = $derived(items[index]);

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

@ -24,7 +24,7 @@ For the boundary to do anything, one or more of the following must be provided.
### `pending`
As of Svelte 5.36, boundaries with a `pending` snippet can contain [`await`](await-expressions) expressions. This snippet will be shown when the boundary is first created, and will remain visible until all the `await` expressions inside the boundary have resolved ([demo](/playground/untitled#H4sIAAAAAAAAE21QQW6DQAz8ytY9BKQVpFdKkPqDHnorPWzAaSwt3tWugUaIv1eE0KpKD5as8YxnNBOw6RAKKOOAVrA4up5bEy6VGknOyiO3xJ8qMnmPAhpOZDFC8T6BXPyiXADQ258X77P1FWg4moj_4Y1jQZZ49W0CealqruXUcyPkWLVozQXbZDC2R606spYiNo7bqA7qab_fp2paFLUElD6wYhzVa3AdRUySgNHZAVN1qDZaLRHljTp0vSTJ9XJjrSbpX5f0eZXN6zLXXOa_QfmurIVU-moyoyH5ib87o7XuYZfOZe6vnGWmx1uZW7lJOq9upa-sMwuUZdkmmfIbfQ1xZwwaBL8ECgk9zh8axJAdiVsoTsZGnL8Bg4tX_OMBAAA=)):
This snippet will be shown when the boundary is first created, and will remain visible until all the [`await`](await-expressions) expressions inside the boundary have resolved ([demo](/playground/untitled#H4sIAAAAAAAAE21QQW6DQAz8ytY9BKQVpFdKkPqDHnorPWzAaSwt3tWugUaIv1eE0KpKD5as8YxnNBOw6RAKKOOAVrA4up5bEy6VGknOyiO3xJ8qMnmPAhpOZDFC8T6BXPyiXADQ258X77P1FWg4moj_4Y1jQZZ49W0CealqruXUcyPkWLVozQXbZDC2R606spYiNo7bqA7qab_fp2paFLUElD6wYhzVa3AdRUySgNHZAVN1qDZaLRHljTp0vSTJ9XJjrSbpX5f0eZXN6zLXXOa_QfmurIVU-moyoyH5ib87o7XuYZfOZe6vnGWmx1uZW7lJOq9upa-sMwuUZdkmmfIbfQ1xZwwaBL8ECgk9zh8axJAdiVsoTsZGnL8Bg4tX_OMBAAA=)):
```svelte
<svelte:boundary>

@ -83,27 +83,18 @@ Svelte will warn you if you get it wrong.
## Type-safe context
A useful pattern is to wrap the calls to `setContext` and `getContext` inside helper functions that let you preserve type safety:
As an alternative to using `setContext` and `getContext` directly, you can use them via `createContext`. This gives you type safety and makes it unnecessary to use a key:
```js
/// file: context.js
```ts
/// file: context.ts
// @filename: ambient.d.ts
interface User {}
// @filename: index.js
// @filename: index.ts
// ---cut---
import { getContext, setContext } from 'svelte';
const key = {};
/** @param {User} user */
export function setUserContext(user) {
setContext(key, user);
}
import { createContext } from 'svelte';
export function getUserContext() {
return /** @type {User} */ (getContext(key));
}
export const [getUserContext, setUserContext] = createContext<User>();
```
## Replacing global state

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

@ -1,9 +0,0 @@
<!-- This file is generated by scripts/process-messages/index.js. Do not edit! -->
### experimental_async_ssr
```
Attempted to use asynchronous rendering without `experimental.async` enabled
```
Set `experimental.async: true` in your compiler options (usually in `svelte.config.js`) to use async server rendering. This render ran synchronously.

@ -60,6 +60,14 @@ Certain lifecycle methods can only be used during component initialisation. To f
<button onclick={handleClick}>click me</button>
```
### missing_context
```
Context was not set in a parent component
```
The [`createContext()`](svelte#createContext) utility returns a `[get, set]` pair of functions. `get` will throw an error if `set` was not used to set the context in a parent component.
### snippet_without_render_tag
```

@ -1,5 +1,69 @@
# svelte
## 5.40.1
### Patch Changes
- chore: Remove sync-in-async warning for server rendering ([#16949](https://github.com/sveltejs/svelte/pull/16949))
## 5.40.0
### Minor Changes
- feat: add `createContext` utility for type-safe context ([#16948](https://github.com/sveltejs/svelte/pull/16948))
### Patch Changes
- chore: simplify `batch.apply()` ([#16945](https://github.com/sveltejs/svelte/pull/16945))
- fix: don't rerun async effects unnecessarily ([#16944](https://github.com/sveltejs/svelte/pull/16944))
## 5.39.13
### Patch Changes
- fix: add missing type for `fr` attribute for `radialGradient` tags in svg ([#16943](https://github.com/sveltejs/svelte/pull/16943))
- fix: unset context on stale promises ([#16935](https://github.com/sveltejs/svelte/pull/16935))
## 5.39.12
### Patch Changes
- fix: better input cursor restoration for `bind:value` ([#16925](https://github.com/sveltejs/svelte/pull/16925))
- fix: track the user's getter of `bind:this` ([#16916](https://github.com/sveltejs/svelte/pull/16916))
- fix: generate correct SSR code for the case where `pending` is an attribute ([#16919](https://github.com/sveltejs/svelte/pull/16919))
- fix: generate correct code for `each` blocks with async body ([#16923](https://github.com/sveltejs/svelte/pull/16923))
## 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

@ -1658,6 +1658,7 @@ export interface SVGAttributes<T extends EventTarget> extends AriaAttributes, DO
'font-variant'?: number | string | undefined | null;
'font-weight'?: number | string | undefined | null;
format?: number | string | undefined | null;
fr?: number | string | undefined | null;
from?: number | string | undefined | null;
fx?: number | string | undefined | null;
fy?: number | string | undefined | null;

@ -1,5 +0,0 @@
## experimental_async_ssr
> Attempted to use asynchronous rendering without `experimental.async` enabled
Set `experimental.async: true` in your compiler options (usually in `svelte.config.js`) to use async server rendering. This render ran synchronously.

@ -52,6 +52,12 @@ Certain lifecycle methods can only be used during component initialisation. To f
<button onclick={handleClick}>click me</button>
```
## missing_context
> Context was not set in a parent component
The [`createContext()`](svelte#createContext) utility returns a `[get, set]` pair of functions. `get` will throw an error if `set` was not used to set the context in a parent component.
## snippet_without_render_tag
> Attempted to render a snippet without a `{@render}` block. This would cause the snippet code to be stringified instead of its content being rendered to the DOM. To fix this, change `{snippet}` to `{@render snippet()}`.

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

@ -100,7 +100,7 @@ declare namespace $state {
* you must reassign it.
*
* Example:
* ```ts
* ```svelte
* <script>
* let items = $state.raw([0]);
*
@ -109,7 +109,7 @@ declare namespace $state {
* };
* </script>
*
* <button on:click={addItem}>
* <button onclick={addItem}>
* {items.join(', ')}
* </button>
* ```
@ -124,7 +124,7 @@ declare namespace $state {
* To take a static snapshot of a deeply reactive `$state` proxy, use `$state.snapshot`:
*
* Example:
* ```ts
* ```svelte
* <script>
* let counter = $state({ count: 0 });
*

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

@ -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(-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'))
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,9 +32,13 @@ export function Identifier(node, context) {
grand_parent?.type !== 'AssignmentExpression' &&
grand_parent?.type !== 'UpdateExpression'
) {
const key = /** @type {Identifier} */ (parent.property);
if (!binding.metadata?.exclude_props?.includes(key.name)) {
return b.id('$$props');
}
}
}
return build_getter(node, context.state);
}

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

@ -43,11 +43,14 @@ export function SvelteBoundary(node, context) {
);
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(build_template([block_open_else, pending, block_close])),
b.block(build_template([block_open, block, block_close]))
b.block([b.stmt(pending)]),
b.block(build_template([block_open, statement, block_close]))
)
);
} else {

@ -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;
@ -263,6 +263,7 @@ class Evaluation {
if (binding.initial?.type === 'SnippetBlock') {
this.is_defined = true;
this.is_known = false;
this.values.add(UNKNOWN);
break;
}

@ -243,6 +243,7 @@ function init_update_callbacks(context) {
export { flushSync } from './internal/client/reactivity/batch.js';
export {
createContext,
getContext,
getAllContexts,
hasContext,

@ -40,6 +40,7 @@ export async function settled() {}
export { getAbortSignal } from './internal/server/abort-signal.js';
export {
createContext,
getAllContexts,
getContext,
hasContext,

@ -71,10 +71,36 @@ export function set_dev_current_component_function(fn) {
dev_current_component_function = fn;
}
/**
* Returns a `[get, set]` pair of functions for working with context in a type-safe way.
*
* `get` will throw an error if no parent component called `set`.
*
* @template T
* @returns {[() => T, (context: T) => T]}
* @since 5.40.0
*/
export function createContext() {
const key = {};
return [
() => {
if (!hasContext(key)) {
e.missing_context();
}
return getContext(key);
},
(context) => setContext(key, context)
];
}
/**
* Retrieves the context that belongs to the closest parent component with the specified `key`.
* Must be called during component initialisation.
*
* [`createContext`](https://svelte.dev/docs/svelte/svelte#createContext) is a type-safe alternative.
*
* @template T
* @param {any} key
* @returns {T}
@ -92,6 +118,8 @@ export function getContext(key) {
*
* Like lifecycle functions, this must be called during component initialisation.
*
* [`createContext`](https://svelte.dev/docs/svelte/svelte#createContext) is a type-safe alternative.
*
* @template T
* @param {any} key
* @param {T} context

@ -29,7 +29,7 @@ import { queue_micro_task } from '../task.js';
import * as e from '../../errors.js';
import * as w from '../../warnings.js';
import { DEV } from 'esm-env';
import { Batch, current_batch, effect_pending_updates } from '../../reactivity/batch.js';
import { Batch, effect_pending_updates } from '../../reactivity/batch.js';
import { internal_set, source } from '../../reactivity/sources.js';
import { tag } from '../../dev/tracing.js';
import { createSubscriber } from '../../../../reactivity/create-subscriber.js';
@ -285,6 +285,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();
});
}
}

@ -43,14 +43,22 @@ export function bind_value(input, get, set = get) {
if (value !== (value = get())) {
var start = input.selectionStart;
var end = input.selectionEnd;
var length = input.value.length;
// the value is coerced on assignment
input.value = value ?? '';
// Restore selection
if (end !== null) {
var new_length = input.value.length;
// If cursor was at end and new input is longer, move cursor to new end
if (start === end && end === length && new_length > length) {
input.selectionStart = new_length;
input.selectionEnd = new_length;
} else {
input.selectionStart = start;
input.selectionEnd = Math.min(end, input.value.length);
input.selectionEnd = Math.min(end, new_length);
}
}
}
});

@ -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, Value } from '#client' */
/** @import { Derived, Effect, Reaction, Source, Value } from '#client' */
import {
BLOCK_EFFECT,
BRANCH_EFFECT,
@ -14,7 +14,7 @@ import {
DERIVED
} from '#client/constants';
import { async_mode_flag } from '../../flags/index.js';
import { deferred, define_property, noop } from '../../shared/utils.js';
import { deferred, define_property } from '../../shared/utils.js';
import {
active_effect,
is_dirty,
@ -44,12 +44,12 @@ export let current_batch = null;
export let previous_batch = null;
/**
* When time travelling, we re-evaluate deriveds based on the temporary
* values of their dependencies rather than their actual values, and cache
* the results in this map rather than on the deriveds themselves
* @type {Map<Derived, any> | null}
* When time travelling (i.e. working in one batch, while other batches
* still have ongoing work), we ignore the real values of affected
* signals in favour of their values within the batch
* @type {Map<Value, any> | null}
*/
export let batch_deriveds = null;
export let batch_values = null;
/** @type {Set<() => void>} */
export let effect_pending_updates = new Set();
@ -152,7 +152,7 @@ export class Batch {
previous_batch = null;
var revert = Batch.apply(this);
this.apply();
for (const root of root_effects) {
this.#traverse_effect_tree(root);
@ -161,6 +161,10 @@ 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.#pending === 0) {
// TODO we need this because we commit _then_ flush effects...
// maybe there's a way we can reverse the order?
var previous_batch_sources = batch_values;
this.#commit();
var render_effects = this.#render_effects;
@ -175,9 +179,12 @@ export class Batch {
previous_batch = this;
current_batch = null;
batch_values = previous_batch_sources;
flush_queued_effects(render_effects);
flush_queued_effects(effects);
previous_batch = null;
this.#deferred?.resolve();
} else {
this.#defer_effects(this.#render_effects);
@ -185,7 +192,7 @@ export class Batch {
this.#defer_effects(this.#block_effects);
}
revert();
batch_values = null;
for (const effect of this.#boundary_async_effects) {
update_effect(effect);
@ -272,6 +279,7 @@ export class Batch {
}
this.current.set(source, source.v);
batch_values?.set(source, source.v);
}
activate() {
@ -280,17 +288,7 @@ 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;
}
}
batch_values = null;
}
flush() {
@ -307,6 +305,16 @@ 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;
}
}
}
/**
@ -334,31 +342,46 @@ export class Batch {
continue;
}
/** @type {Source[]} */
const sources = [];
for (const [source, value] of this.current) {
if (batch.current.has(source)) {
if (is_earlier) {
if (is_earlier && value !== batch.current.get(source)) {
// bring the value up to date
batch.current.set(source, value);
} else {
// later batch has more recent value,
// same value or later batch has more recent value,
// no need to re-run these effects
continue;
}
}
mark_effects(source);
sources.push(source);
}
if (sources.length === 0) {
continue;
}
// Re-run async/block effects that depend on distinct values changed in both batches
const others = [...batch.current.keys()].filter((s) => !this.current.has(s));
if (others.length > 0) {
for (const source of sources) {
mark_effects(source, others);
}
if (queued_root_effects.length > 0) {
current_batch = batch;
const revert = Batch.apply(batch);
batch.apply();
for (const root of queued_root_effects) {
batch.#traverse_effect_tree(root);
}
queued_root_effects = [];
revert();
batch.deactivate();
}
}
}
@ -375,7 +398,6 @@ 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);
@ -387,9 +409,6 @@ export class Batch {
}
this.flush();
} else {
this.deactivate();
}
}
/** @param {() => void} fn */
@ -426,49 +445,23 @@ export class Batch {
queue_micro_task(task);
}
/**
* @param {Batch} current_batch
*/
static apply(current_batch) {
if (!async_mode_flag || batches.size === 1) {
return noop;
}
apply() {
if (!async_mode_flag || batches.size === 1) return;
// 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;
}
// we need to override values with the ones in this batch...
batch_values = new Map(this.current);
// ...and undo changes belonging to other batches
for (const batch of batches) {
if (batch === current_batch) continue;
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;
if (!batch_values.has(source)) {
batch_values.set(source, 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;
};
}
}
@ -643,17 +636,19 @@ function flush_queued_effects(effects) {
/**
* 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
* depending on `value` and at least one of the other `sources`, so that
* these effects can re-run after another batch has been committed
* @param {Value} value
* @param {Source[]} sources
*/
function mark_effects(value) {
function mark_effects(value, sources) {
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) {
mark_effects(/** @type {Derived} */ (reaction), sources);
} else if ((flags & (ASYNC | BLOCK_EFFECT)) !== 0 && depends_on(reaction, sources)) {
set_signal_status(reaction, DIRTY);
schedule_effect(/** @type {Effect} */ (reaction));
}
@ -661,6 +656,26 @@ function mark_effects(value) {
}
}
/**
* @param {Reaction} reaction
* @param {Source[]} sources
*/
function depends_on(reaction, sources) {
if (reaction.deps !== null) {
for (const dep of reaction.deps) {
if (sources.includes(dep)) {
return true;
}
if ((dep.f & DERIVED) !== 0 && depends_on(/** @type {Derived} */ (dep), sources)) {
return true;
}
}
}
return false;
}
/**
* @param {Effect} signal
* @returns {void}

@ -33,7 +33,7 @@ import { async_mode_flag, tracing_mode_flag } from '../../flags/index.js';
import { Boundary } from '../dom/blocks/boundary.js';
import { component_context } from '../context.js';
import { UNINITIALIZED } from '../../../constants.js';
import { batch_deriveds, current_batch } from './batch.js';
import { batch_values, current_batch } from './batch.js';
import { unset_context } from './async.js';
import { deferred } from '../../shared/utils.js';
@ -126,9 +126,11 @@ export function async_derived(fn, location) {
try {
// 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);
// We call `unset_context` to undo any `save` calls that happen inside `fn()`
Promise.resolve(fn()).then(d.resolve, d.reject).then(unset_context);
} catch (error) {
d.reject(error);
unset_context();
}
if (DEV) current_async_effect = null;
@ -142,6 +144,7 @@ export function async_derived(fn, location) {
batch.increment();
deferreds.get(batch)?.reject(STALE_REACTION);
deferreds.delete(batch); // delete to ensure correct order in Map iteration below
deferreds.set(batch, d);
}
}
@ -169,6 +172,13 @@ export function async_derived(fn, location) {
internal_set(signal, value);
// All prior async derived runs are now stale
for (const [b, d] of deferreds) {
deferreds.delete(b);
if (b === batch) break;
d.reject(STALE_REACTION);
}
if (DEV && location !== undefined) {
recent_async_deriveds.add(signal);
@ -185,8 +195,6 @@ export function async_derived(fn, location) {
boundary.update_pending_count(-1);
if (!pending) batch.decrement();
}
unset_context();
};
d.promise.then(handler, (e) => handler(null, e || 'unknown'));
@ -336,6 +344,8 @@ export function update_derived(derived) {
var value = execute_derived(derived);
if (!derived.equals(value)) {
// TODO can we avoid setting `derived.v` when `batch_values !== null`,
// without causing the value to be stale later?
derived.v = value;
derived.wv = increment_write_version();
}
@ -346,8 +356,8 @@ export function update_derived(derived) {
return;
}
if (batch_deriveds !== null) {
batch_deriveds.set(derived, derived.v);
if (batch_values !== null) {
batch_values.set(derived, derived.v);
} else {
var status =
(skip_reaction || (derived.f & UNOWNED) !== 0) && derived.deps !== null ? MAYBE_DIRTY : CLEAN;

@ -42,7 +42,7 @@ import {
set_dev_stack
} from './context.js';
import * as w from './warnings.js';
import { Batch, batch_deriveds, flushSync, schedule_effect } from './reactivity/batch.js';
import { Batch, batch_values, flushSync, schedule_effect } from './reactivity/batch.js';
import { handle_error } from './error-handling.js';
import { UNINITIALIZED } from '../../constants.js';
import { captured_signals } from './legacy.js';
@ -671,8 +671,8 @@ export function get(signal) {
} else if (is_derived) {
derived = /** @type {Derived} */ (signal);
if (batch_deriveds?.has(derived)) {
return batch_deriveds.get(derived);
if (batch_values?.has(derived)) {
return batch_values.get(derived);
}
if (is_dirty(derived)) {
@ -680,6 +680,10 @@ export function get(signal) {
}
}
if (batch_values?.has(signal)) {
return batch_values.get(signal);
}
if ((signal.f & ERROR_VALUE) !== 0) {
throw signal.v;
}

@ -12,6 +12,16 @@ export function set_ssr_context(v) {
ssr_context = v;
}
/**
* @template T
* @returns {[() => T, (context: T) => T]}
* @since 5.40.0
*/
export function createContext() {
const key = {};
return [() => getContext(key), (context) => setContext(key, context)];
}
/**
* @template T
* @param {any} key

@ -14,7 +14,6 @@ import {
with_render_store
} from './context.js';
import * as e from './errors.js';
import * as w from './warnings.js';
import { BLOCK_CLOSE, BLOCK_OPEN } from './hydration.js';
import { attributes } from './index.js';
import { uneval } from 'devalue';
@ -368,7 +367,6 @@ export class Renderer {
*/
(onfulfilled, onrejected) => {
if (!async_mode_flag) {
w.experimental_async_ssr();
const result = (sync ??= Renderer.#render(component, options));
const user_result = onfulfilled({
head: result.head,

@ -4,14 +4,3 @@ import { DEV } from 'esm-env';
var bold = 'font-weight: bold';
var normal = 'font-weight: normal';
/**
* Attempted to use asynchronous rendering without `experimental.async` enabled
*/
export function experimental_async_ssr() {
if (DEV) {
console.warn(`%c[svelte] experimental_async_ssr\n%cAttempted to use asynchronous rendering without \`experimental.async\` enabled\nhttps://svelte.dev/e/experimental_async_ssr`, bold, normal);
} else {
console.warn(`https://svelte.dev/e/experimental_async_ssr`);
}
}

@ -51,6 +51,22 @@ export function lifecycle_outside_component(name) {
}
}
/**
* Context was not set in a parent component
* @returns {never}
*/
export function missing_context() {
if (DEV) {
const error = new Error(`missing_context\nContext was not set in a parent component\nhttps://svelte.dev/e/missing_context`);
error.name = 'Svelte error';
throw error;
} else {
throw new Error(`https://svelte.dev/e/missing_context`);
}
}
/**
* Attempted to render a snippet without a `{@render}` block. This would cause the snippet code to be stringified instead of its content being rendered to the DOM. To fix this, change `{snippet}` to `{@render snippet()}`.
* @returns {never}

@ -53,7 +53,6 @@ export function asClassComponent(component) {
*/
value: (onfulfilled, onrejected) => {
if (!async_mode_flag) {
w.experimental_async_ssr();
const user_result = onfulfilled({
css: munged.css,
head: munged.head,

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

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

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

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

@ -0,0 +1,25 @@
import { test } from '../../test';
export default test({
async test({ assert, target }) {
// We gotta wait a bit more in this test because of the macrotasks in App.svelte
function macrotask(t = 3) {
return new Promise((r) => setTimeout(r, t));
}
await macrotask();
assert.htmlEqual(target.innerHTML, '<input> 1 | ');
const [input] = target.querySelectorAll('input');
input.value = '1';
input.dispatchEvent(new Event('input', { bubbles: true }));
await macrotask();
assert.htmlEqual(target.innerHTML, '<input> 1 | ');
input.value = '12';
input.dispatchEvent(new Event('input', { bubbles: true }));
await macrotask(6);
assert.htmlEqual(target.innerHTML, '<input> 3 | 12');
}
});

@ -0,0 +1,34 @@
<script>
let count = $state(0);
let value = $state('');
let prev;
function asd(v) {
const r = Promise.withResolvers();
if (prev || v === '') {
Promise.resolve().then(async () => {
count++;
r.resolve(v);
await new Promise(r => setTimeout(r, 0));
// TODO with a microtask like below it still throws a mutation error
// await Promise.resolve();
prev?.resolve();
});
} else {
prev = Promise.withResolvers();
prev.promise.then(() => {
count++;
r.resolve(v)
});
}
return r.promise;
}
const x = $derived(await asd(value))
</script>
<input bind:value />
{count} | {x}

@ -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 @@
<script>
import { get } from './main.svelte';
const message = get();
</script>
<h1>{message}</h1>

@ -0,0 +1,5 @@
import { test } from '../../test';
export default test({
html: `<h1>hello</h1>`
});

@ -0,0 +1,16 @@
<script module>
import { createContext } from 'svelte';
/** @type {ReturnType<typeof createContext<string>>} */
const [get, set] = createContext();
export { get };
</script>
<script>
import Child from './Child.svelte';
set('hello');
</script>
<Child />

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

@ -450,10 +450,20 @@ declare module 'svelte' {
type NotFunction<T> = T extends Function ? never : T;
type MaybePromise<T> = T | Promise<T>;
/**
* Returns a `[get, set]` pair of functions for working with context in a type-safe way.
*
* `get` will throw an error if no parent component called `set`.
*
* @since 5.40.0
*/
export function createContext<T>(): [() => T, (context: T) => T];
/**
* Retrieves the context that belongs to the closest parent component with the specified `key`.
* Must be called during component initialisation.
*
* [`createContext`](https://svelte.dev/docs/svelte/svelte#createContext) is a type-safe alternative.
*
* */
export function getContext<T>(key: any): T;
/**
@ -463,6 +473,8 @@ declare module 'svelte' {
*
* Like lifecycle functions, this must be called during component initialisation.
*
* [`createContext`](https://svelte.dev/docs/svelte/svelte#createContext) is a type-safe alternative.
*
* */
export function setContext<T>(key: any, context: T): T;
/**
@ -3230,7 +3242,7 @@ declare namespace $state {
* you must reassign it.
*
* Example:
* ```ts
* ```svelte
* <script>
* let items = $state.raw([0]);
*
@ -3239,7 +3251,7 @@ declare namespace $state {
* };
* </script>
*
* <button on:click={addItem}>
* <button onclick={addItem}>
* {items.join(', ')}
* </button>
* ```
@ -3254,7 +3266,7 @@ declare namespace $state {
* To take a static snapshot of a deeply reactive `$state` proxy, use `$state.snapshot`:
*
* Example:
* ```ts
* ```svelte
* <script>
* let counter = $state({ count: 0 });
*

Loading…
Cancel
Save