Merge remote-tracking branch 'origin/main' into state-onchange

pull/15579/head
paoloricciuti 7 months ago
commit 19acec417e

@ -0,0 +1,5 @@
---
'svelte': patch
---
fix: value/checked not correctly set using spread

@ -0,0 +1,5 @@
---
'svelte': minor
---
feat: SSR-safe ID generation with `$props.id()`

@ -0,0 +1,5 @@
---
'svelte': patch
---
fix: use `importNode` to clone templates for Firefox

@ -0,0 +1,5 @@
---
'svelte': patch
---
fix: recurse into `$derived` for ownership validation

@ -1,26 +0,0 @@
# https://github.com/sveltejs/svelte.dev/blob/main/apps/svelte.dev/scripts/sync-docs/README.md
name: Docs preview create request
on:
pull_request_target:
branches:
- main
jobs:
dispatch:
runs-on: ubuntu-latest
steps:
- name: Repository Dispatch
uses: peter-evans/repository-dispatch@v3
with:
token: ${{ secrets.SYNC_REQUEST_TOKEN }}
repository: sveltejs/svelte.dev
event-type: docs-preview-create
client-payload: |-
{
"package": "svelte",
"repo": "${{ github.repository }}",
"owner": "${{ github.event.pull_request.head.repo.owner.login }}",
"branch": "${{ github.event.pull_request.head.ref }}",
"pr": ${{ github.event.pull_request.number }}
}

@ -1,27 +0,0 @@
# https://github.com/sveltejs/svelte.dev/blob/main/apps/svelte.dev/scripts/sync-docs/README.md
name: Docs preview delete request
on:
pull_request_target:
branches:
- main
types: [closed]
jobs:
dispatch:
runs-on: ubuntu-latest
steps:
- name: Repository Dispatch
uses: peter-evans/repository-dispatch@v3
with:
token: ${{ secrets.SYNC_REQUEST_TOKEN }}
repository: sveltejs/svelte.dev
event-type: docs-preview-delete
client-payload: |-
{
"package": "svelte",
"repo": "${{ github.repository }}",
"owner": "${{ github.event.pull_request.head.repo.owner.login }}",
"branch": "${{ github.event.pull_request.head.ref }}",
"pr": ${{ github.event.pull_request.number }}
}

@ -9,6 +9,9 @@ jobs:
- name: Checkout code
uses: actions/checkout@v4
- name: install corepack
run: npm i -g corepack@0.31.0
- run: corepack enable
- uses: actions/setup-node@v4
with:

@ -1,22 +0,0 @@
# https://github.com/sveltejs/svelte.dev/blob/main/apps/svelte.dev/scripts/sync-docs/README.md
name: Sync request
on:
push:
branches:
- main
jobs:
dispatch:
runs-on: ubuntu-latest
steps:
- name: Repository Dispatch
uses: peter-evans/repository-dispatch@v3
with:
token: ${{ secrets.SYNC_REQUEST_TOKEN }}
repository: sveltejs/svelte.dev
event-type: sync-request
client-payload: |-
{
"package": "svelte"
}

@ -51,7 +51,7 @@ We use [GitHub issues](https://github.com/sveltejs/svelte/issues) for our public
If you have questions about using Svelte, contact us on Discord at [svelte.dev/chat](https://svelte.dev/chat), and we will do our best to answer your questions.
If you see anything you'd like to be implemented, create a [feature request issue](https://github.com/sveltejs/svelte/issues/new?template=feature_request.yml)
If you see anything you'd like to be implemented, create a [feature request issue](https://github.com/sveltejs/svelte/issues/new?template=feature_request.yml).
### Reporting new issues

@ -66,7 +66,7 @@ You can return a function from `$effect`, which will run immediately before the
### Understanding dependencies
`$effect` automatically picks up any reactive values (`$state`, `$derived`, `$props`) that are _synchronously_ read inside its function body and registers them as dependencies. When those dependencies change, the `$effect` schedules a rerun.
`$effect` automatically picks up any reactive values (`$state`, `$derived`, `$props`) that are _synchronously_ read inside its function body (including indirectly, via function calls) and registers them as dependencies. When those dependencies change, the `$effect` schedules a rerun.
Values that are read _asynchronously_ — after an `await` or inside a `setTimeout`, for example — will not be tracked. Here, the canvas will be repainted when `color` changes, but not when `size` changes ([demo](/playground/untitled#H4sIAAAAAAAAE31T246bMBD9lZF3pWSlBEirfaEQqdo_2PatVIpjBrDkGGQPJGnEv1e2IZfVal-wfHzmzJyZ4cIqqdCy9M-F0blDlnqArZjmB3f72XWRHVCRw_bc4me4aDWhJstSlllhZEfbQhekkMDKfwg5PFvihMvX5OXH_CJa1Zrb0-Kpqr5jkiwC48rieuDWQbqgZ6wqFLRcvkC-hYvnkWi1dWqa8ESQTxFRjfQWsOXiWzmr0sSLhEJu3p1YsoJkNUcdZUnN9dagrBu6FVRQHAM10sJRKgUG16bXcGxQ44AGdt7SDkTDdY02iqLHnJVU6hedlWuIp94JW6Tf8oBt_8GdTxlF0b4n0C35ZLBzXb3mmYn3ae6cOW74zj0YVzDNYXRHFt9mprNgHfZSl6mzml8CMoLvTV6wTZIUDEJv5us2iwMtiJRyAKG4tXnhl8O0yhbML0Wm-B7VNlSSSd31BG7z8oIZZ6dgIffAVY_5xdU9Qrz1Bnx8fCfwtZ7v8Qc9j3nB8PqgmMWlHIID6-bkVaPZwDySfWtKNGtquxQ23Qlsq2QJT0KIqb8dL0up6xQ2eIBkAg_c1FI_YqW0neLnFCqFpwmreedJYT7XX8FVOBfwWRhXstZrSXiwKQjUhOZeMIleb5JZfHWn2Yq5pWEpmR7Hv-N_wEqT8hEEAAA=)):

@ -199,3 +199,24 @@ You can, of course, separate the type declaration from the annotation:
> [!NOTE] Interfaces for native DOM elements are provided in the `svelte/elements` module (see [Typing wrapper components](typescript#Typing-wrapper-components))
Adding types is recommended, as it ensures that people using your component can easily discover which props they should provide.
## `$props.id()`
This rune, added in version 5.20.0, generates an ID that is unique to the current component instance. When hydrating a server-rendered component, the value will be consistent between server and client.
This is useful for linking elements via attributes like `for` and `aria-labelledby`.
```svelte
<script>
const uid = $props.id();
</script>
<form>
<label for="{uid}-firstname">First Name: </label>
<input id="{uid}-firstname" type="text" />
<label for="{uid}-lastname">Last Name: </label>
<input id="{uid}-lastname" type="text" />
</form>
```

@ -11,4 +11,4 @@ The `{@const ...}` tag defines a local constant.
{/each}
```
`{@const}` is only allowed as an immediate child of a block — `{#if ...}`, `{#each ...}`, `{#snippet ...}` and so on — a `<Component />` or a `<svelte:boundary`.
`{@const}` is only allowed as an immediate child of a block — `{#if ...}`, `{#each ...}`, `{#snippet ...}` and so on — a `<Component />` or a `<svelte:boundary>`.

@ -219,11 +219,10 @@ You can give the `<select>` a default value by adding a `selected` attribute to
- [`volume`](https://developer.mozilla.org/en-US/docs/Web/API/HTMLMediaElement/volume)
- [`muted`](https://developer.mozilla.org/en-US/docs/Web/API/HTMLMediaElement/muted)
...and seven readonly ones:
...and six readonly ones:
- [`duration`](https://developer.mozilla.org/en-US/docs/Web/API/HTMLMediaElement/duration)
- [`buffered`](https://developer.mozilla.org/en-US/docs/Web/API/HTMLMediaElement/buffered)
- [`paused`](https://developer.mozilla.org/en-US/docs/Web/API/HTMLMediaElement/paused)
- [`seekable`](https://developer.mozilla.org/en-US/docs/Web/API/HTMLMediaElement/seekable)
- [`seeking`](https://developer.mozilla.org/en-US/docs/Web/API/HTMLMediaElement/seeking_event)
- [`ended`](https://developer.mozilla.org/en-US/docs/Web/API/HTMLMediaElement/ended)
@ -235,7 +234,7 @@ You can give the `<select>` a default value by adding a `selected` attribute to
## `<video>`
`<video>` elements have all the same bindings as [#audio] elements, plus readonly [`videoWidth`](https://developer.mozilla.org/en-US/docs/Web/API/HTMLVideoElement/videoWidth) and [`videoHeight`](https://developer.mozilla.org/en-US/docs/Web/API/HTMLVideoElement/videoHeight) bindings.
`<video>` elements have all the same bindings as [`<audio>`](#audio) elements, plus readonly [`videoWidth`](https://developer.mozilla.org/en-US/docs/Web/API/HTMLVideoElement/videoWidth) and [`videoHeight`](https://developer.mozilla.org/en-US/docs/Web/API/HTMLVideoElement/videoHeight) bindings.
## `<img>`

@ -33,10 +33,7 @@ If a component defines `@keyframes`, the name is scoped to the component using t
/* these keyframes are only accessible inside this component */
@keyframes bounce {
/* ... *.
/* ... */
}
</style>
```

@ -13,7 +13,7 @@ Boundaries allow you to guard against errors in part of your app from breaking t
If an error occurs while rendering or updating the children of a `<svelte:boundary>`, or running any [`$effect`]($effect) functions contained therein, the contents will be removed.
Errors occurring outside the rendering process (for example, in event handlers) are _not_ caught by error boundaries.
Errors occurring outside the rendering process (for example, in event handlers or after a `setTimeout` or async work) are _not_ caught by error boundaries.
## Properties

@ -45,8 +45,6 @@ If a function is returned from `onMount`, it will be called when the component i
## `onDestroy`
> EXPORT_SNIPPET: svelte#onDestroy
Schedules a callback to run immediately before the component is unmounted.
Out of `onMount`, `beforeUpdate`, `afterUpdate` and `onDestroy`, this is the only one that runs inside a server-side component.

@ -722,7 +722,39 @@ If a bindable property has a default value (e.g. `let { foo = $bindable('bar') }
### `accessors` option is ignored
Setting the `accessors` option to `true` makes properties of a component directly accessible on the component instance. In runes mode, properties are never accessible on the component instance. You can use component exports instead if you need to expose them.
Setting the `accessors` option to `true` makes properties of a component directly accessible on the component instance.
```svelte
<svelte:options accessors={true} />
<script>
// available via componentInstance.name
export let name;
</script>
```
In runes mode, properties are never accessible on the component instance. You can use component exports instead if you need to expose them.
```svelte
<script>
let { name } = $props();
// available via componentInstance.getName()
export const getName = () => name;
</script>
```
Alternatively, if the place where they are instantiated is under your control, you can also make use of runes inside `.js/.ts` files by adjusting their ending to include `.svelte`, i.e. `.svelte.js` or `.svelte.ts`, and then use `$state`:
```js
+++import { mount } from 'svelte';+++
import App from './App.svelte'
---const app = new App({ target: document.getElementById("app"), props: { foo: 'bar' } });
app.foo = 'baz'---
+++const props = $state({ foo: 'bar' });
const app = mount(App, { target: document.getElementById("app"), props });
props.foo = 'baz';+++
```
### `immutable` option is ignored

@ -573,7 +573,13 @@ Unrecognised compiler option %keypath%
### props_duplicate
```
Cannot use `$props()` more than once
Cannot use `%rune%()` more than once
```
### props_id_invalid_placement
```
`$props.id()` can only be used at the top level of components as a variable declaration initializer
```
### props_illegal_name

@ -7,7 +7,7 @@
"license": "MIT",
"packageManager": "pnpm@9.4.0",
"engines": {
"pnpm": "^9.0.0"
"pnpm": ">=9.0.0"
},
"repository": {
"type": "git",
@ -44,6 +44,6 @@
"typescript": "^5.5.4",
"typescript-eslint": "^8.2.0",
"v8-natives": "^1.2.5",
"vitest": "^2.0.5"
"vitest": "^2.1.9"
}
}

@ -1,5 +1,97 @@
# svelte
## 5.19.10
### Patch Changes
- fix: when re-connecting unowned deriveds, remove their unowned flag ([#15255](https://github.com/sveltejs/svelte/pull/15255))
- fix: allow mutation of private derived state ([#15228](https://github.com/sveltejs/svelte/pull/15228))
## 5.19.9
### Patch Changes
- fix: ensure unowned derived dependencies are not duplicated when reactions are skipped ([#15232](https://github.com/sveltejs/svelte/pull/15232))
- fix: hydrate `href` that is part of spread attributes ([#15226](https://github.com/sveltejs/svelte/pull/15226))
## 5.19.8
### Patch Changes
- fix: properly set `value` property of custom elements ([#15206](https://github.com/sveltejs/svelte/pull/15206))
- fix: ensure custom element updates don't run in hydration mode ([#15217](https://github.com/sveltejs/svelte/pull/15217))
- fix: ensure tracking returns true, even if in unowned ([#15214](https://github.com/sveltejs/svelte/pull/15214))
## 5.19.7
### Patch Changes
- chore: remove unused code from signal logic ([#15195](https://github.com/sveltejs/svelte/pull/15195))
- fix: encounter svelte:element in blocks as sibling during pruning css ([#15165](https://github.com/sveltejs/svelte/pull/15165))
## 5.19.6
### Patch Changes
- fix: do not prune selectors like `:global(.foo):has(.scoped)` ([#15140](https://github.com/sveltejs/svelte/pull/15140))
- fix: don't error on slot prop inside block inside other component ([#15148](https://github.com/sveltejs/svelte/pull/15148))
- fix: ensure reactions are correctly attached for unowned deriveds ([#15158](https://github.com/sveltejs/svelte/pull/15158))
- fix: silence a11y attribute warnings when spread attributes present ([#15150](https://github.com/sveltejs/svelte/pull/15150))
- fix: prevent false-positive ownership validations due to hot reload ([#15154](https://github.com/sveltejs/svelte/pull/15154))
- fix: widen ownership when calling setContext ([#15153](https://github.com/sveltejs/svelte/pull/15153))
## 5.19.5
### Patch Changes
- fix: improve derived connection to ownership graph ([#15137](https://github.com/sveltejs/svelte/pull/15137))
- fix: correctly look for sibling elements inside blocks and components when pruning CSS ([#15106](https://github.com/sveltejs/svelte/pull/15106))
## 5.19.4
### Patch Changes
- fix: Add `bind:focused` property to `HTMLAttributes` type ([#15122](https://github.com/sveltejs/svelte/pull/15122))
- fix: lazily connect derievds (in deriveds) to their parent ([#15129](https://github.com/sveltejs/svelte/pull/15129))
- fix: disallow $state/$derived in const tags ([#15115](https://github.com/sveltejs/svelte/pull/15115))
## 5.19.3
### Patch Changes
- fix: don't throw for `undefined` non delegated event handlers ([#15087](https://github.com/sveltejs/svelte/pull/15087))
- fix: consistently set value to blank string when value attribute is undefined ([#15057](https://github.com/sveltejs/svelte/pull/15057))
- fix: optimise || expressions in template ([#15092](https://github.com/sveltejs/svelte/pull/15092))
- fix: correctly handle `novalidate` attribute casing ([#15083](https://github.com/sveltejs/svelte/pull/15083))
- fix: expand boolean attribute support ([#15095](https://github.com/sveltejs/svelte/pull/15095))
- fix: avoid double deriveds in component props ([#15089](https://github.com/sveltejs/svelte/pull/15089))
- fix: add check for `is` attribute to correctly detect custom elements ([#15086](https://github.com/sveltejs/svelte/pull/15086))
## 5.19.2
### Patch Changes
- fix: address regression with untrack ([#15079](https://github.com/sveltejs/svelte/pull/15079))
## 5.19.1
### Patch Changes

@ -839,6 +839,7 @@ export interface HTMLAttributes<T extends EventTarget> extends AriaAttributes, D
readonly 'bind:contentBoxSize'?: Array<ResizeObserverSize> | undefined | null;
readonly 'bind:borderBoxSize'?: Array<ResizeObserverSize> | undefined | null;
readonly 'bind:devicePixelContentBoxSize'?: Array<ResizeObserverSize> | undefined | null;
readonly 'bind:focused'?: boolean | undefined | null;
// SvelteKit
'data-sveltekit-keepfocus'?: true | '' | 'off' | undefined | null;

@ -120,7 +120,11 @@ This turned out to be buggy and unpredictable, particularly when working with de
## props_duplicate
> Cannot use `$props()` more than once
> Cannot use `%rune%()` more than once
## props_id_invalid_placement
> `$props.id()` can only be used at the top level of components as a variable declaration initializer
## props_illegal_name

@ -2,7 +2,7 @@
"name": "svelte",
"description": "Cybernetically enhanced web apps",
"license": "MIT",
"version": "5.19.1",
"version": "5.19.10",
"type": "module",
"types": "./types/index.d.ts",
"engines": {
@ -143,7 +143,7 @@
"source-map": "^0.7.4",
"tiny-glob": "^0.2.9",
"typescript": "^5.5.4",
"vitest": "^2.0.5"
"vitest": "^2.1.9"
},
"dependencies": {
"@ampproject/remapping": "^2.3.0",

@ -349,6 +349,15 @@ declare namespace $effect {
declare function $props(): any;
declare namespace $props {
/**
* Generates an ID that is unique to the current component instance. When hydrating a server-rendered component,
* the value will be consistent between server and client.
*
* This is useful for linking elements via attributes like `for` and `aria-labelledby`.
* @since 5.20.0
*/
export function id(): string;
// prevent intellisense from being unhelpful
/** @deprecated */
export const apply: never;

@ -279,12 +279,22 @@ export function module_illegal_default_export(node) {
}
/**
* Cannot use `$props()` more than once
* Cannot use `%rune%()` more than once
* @param {null | number | NodeLike} node
* @param {string} rune
* @returns {never}
*/
export function props_duplicate(node, rune) {
e(node, 'props_duplicate', `Cannot use \`${rune}()\` more than once\nhttps://svelte.dev/e/props_duplicate`);
}
/**
* `$props.id()` can only be used at the top level of components as a variable declaration initializer
* @param {null | number | NodeLike} node
* @returns {never}
*/
export function props_duplicate(node) {
e(node, 'props_duplicate', `Cannot use \`$props()\` more than once\nhttps://svelte.dev/e/props_duplicate`);
export function props_id_invalid_placement(node) {
e(node, 'props_id_invalid_placement', `\`$props.id()\` can only be used at the top level of components as a variable declaration initializer\nhttps://svelte.dev/e/props_id_invalid_placement`);
}
/**

@ -706,7 +706,7 @@ function special(parser) {
expression: /** @type {AST.RenderTag['expression']} */ (expression),
metadata: {
dynamic: false,
args_with_call_expression: new Set(),
arguments: [],
path: [],
snippets: new Set()
}

@ -339,13 +339,18 @@ function relative_selector_might_apply_to_node(relative_selector, rule, element)
let sibling_elements; // do them lazy because it's rarely used and expensive to calculate
// If this is a :has inside a global selector, we gotta include the element itself, too,
// because the global selector might be for an element that's outside the component (e.g. :root).
// because the global selector might be for an element that's outside the component,
// e.g. :root:has(.scoped), :global(.foo):has(.scoped), or :root { &:has(.scoped) {} }
const rules = get_parent_rules(rule);
const include_self =
rules.some((r) => r.prelude.children.some((c) => c.children.some((s) => is_global(s, r)))) ||
rules[rules.length - 1].prelude.children.some((c) =>
c.children.some((r) =>
r.selectors.some((s) => s.type === 'PseudoClassSelector' && s.name === 'root')
r.selectors.some(
(s) =>
s.type === 'PseudoClassSelector' &&
(s.name === 'root' || (s.name === 'global' && s.args))
)
)
);
if (include_self) {
@ -638,19 +643,30 @@ function get_following_sibling_elements(element, include_self) {
/** @type {Array<Compiler.AST.RegularElement | Compiler.AST.SvelteElement>} */
const siblings = [];
// ...then walk them, starting from the node after the one
// containing the element in question
// ...then walk them, starting from the node containing the element in question
// skipping nodes that appears before the element
const seen = new Set();
let skip = true;
/** @param {Compiler.AST.SvelteNode} node */
function get_siblings(node) {
walk(node, null, {
RegularElement(node) {
siblings.push(node);
if (node === element) {
skip = false;
if (include_self) siblings.push(node);
} else if (!skip) {
siblings.push(node);
}
},
SvelteElement(node) {
siblings.push(node);
if (node === element) {
skip = false;
if (include_self) siblings.push(node);
} else if (!skip) {
siblings.push(node);
}
},
RenderTag(node) {
for (const snippet of node.metadata.snippets) {
@ -663,14 +679,10 @@ function get_following_sibling_elements(element, include_self) {
});
}
for (const node of nodes.slice(nodes.indexOf(start) + 1)) {
for (const node of nodes.slice(nodes.indexOf(start))) {
get_siblings(node);
}
if (include_self) {
siblings.push(element);
}
return siblings;
}
@ -921,11 +933,9 @@ function get_possible_element_siblings(node, adjacent_only, seen = new Set()) {
/**
* @param {Compiler.AST.EachBlock | Compiler.AST.IfBlock | Compiler.AST.AwaitBlock | Compiler.AST.KeyBlock | Compiler.AST.SlotElement} node
* @param {boolean} adjacent_only
* @returns {Map<Compiler.AST.RegularElement, NodeExistsValue>}
* @returns {Map<Compiler.AST.RegularElement | Compiler.AST.SvelteElement, NodeExistsValue>}
*/
function get_possible_last_child(node, adjacent_only) {
/** @typedef {Map<Compiler.AST.RegularElement, NodeExistsValue>} NodeMap */
/** @type {Array<Compiler.AST.Fragment | undefined | null>} */
let fragments = [];
@ -948,7 +958,7 @@ function get_possible_last_child(node, adjacent_only) {
break;
}
/** @type {NodeMap} */
/** @type {Map<Compiler.AST.RegularElement | Compiler.AST.SvelteElement, NodeExistsValue>} NodeMap */
const result = new Map();
let exhaustive = node.type !== 'SlotElement';
@ -989,9 +999,10 @@ function has_definite_elements(result) {
}
/**
* @template T
* @param {Map<T, NodeExistsValue>} from
* @param {Map<T, NodeExistsValue>} to
* @template T2
* @template {T2} T1
* @param {Map<T1, NodeExistsValue>} from
* @param {Map<T2, NodeExistsValue>} to
* @returns {void}
*/
function add_to_map(from, to) {
@ -1016,7 +1027,7 @@ function higher_existence(exist1, exist2) {
* @param {boolean} adjacent_only
*/
function loop_child(children, adjacent_only) {
/** @type {Map<Compiler.AST.RegularElement, NodeExistsValue>} */
/** @type {Map<Compiler.AST.RegularElement | Compiler.AST.SvelteElement, NodeExistsValue>} */
const result = new Map();
let i = children.length;
@ -1029,6 +1040,8 @@ function loop_child(children, adjacent_only) {
if (adjacent_only) {
break;
}
} else if (child.type === 'SvelteElement') {
result.set(child, NODE_PROBABLY_EXISTS);
} else if (is_block(child)) {
const child_result = get_possible_last_child(child, adjacent_only);
add_to_map(child_result, result);

@ -243,7 +243,15 @@ export function analyze_module(ast, options) {
}
}
const analysis = { runes: true, tracing: false };
/** @type {Analysis} */
const analysis = {
module: { ast, scope, scopes },
name: options.filename,
accessors: false,
runes: true,
immutable: true,
tracing: false
};
walk(
/** @type {Node} */ (ast),
@ -256,14 +264,7 @@ export function analyze_module(ast, options) {
visitors
);
return {
module: { ast, scope, scopes },
name: options.filename,
accessors: false,
runes: true,
immutable: true,
tracing: analysis.tracing
};
return analysis;
}
/**
@ -415,6 +416,7 @@ export function analyze_component(root, source, options) {
immutable: runes || options.immutable,
exports: [],
uses_props: false,
props_id: null,
uses_rest_props: false,
uses_slots: false,
uses_component_bindings: false,
@ -604,8 +606,7 @@ export function analyze_component(root, source, options) {
has_props_rune: false,
component_slots: new Set(),
expression: null,
render_tag: null,
private_derived_state: [],
derived_state: [],
function_depth: scope.function_depth,
instance_scope: instance.scope,
reactive_statement: null,
@ -676,8 +677,7 @@ export function analyze_component(root, source, options) {
reactive_statements: analysis.reactive_statements,
component_slots: new Set(),
expression: null,
render_tag: null,
private_derived_state: [],
derived_state: [],
function_depth: scope.function_depth
};

@ -19,9 +19,7 @@ export interface AnalysisState {
component_slots: Set<string>;
/** Information about the current expression/directive/block value */
expression: ExpressionMetadata | null;
/** The current {@render ...} tag, if any */
render_tag: null | AST.RenderTag;
private_derived_state: string[];
derived_state: string[];
function_depth: number;
// legacy stuff

@ -23,11 +23,6 @@ export function Attribute(node, context) {
if (node.name === 'value' && parent.name === 'option') {
mark_subtree_dynamic(context.path);
}
// special case <img loading="lazy" />
if (node.name === 'loading' && parent.name === 'img') {
mark_subtree_dynamic(context.path);
}
}
if (is_event_attribute(node)) {

@ -55,7 +55,7 @@ export function CallExpression(node, context) {
case '$props':
if (context.state.has_props_rune) {
e.props_duplicate(node);
e.props_duplicate(node, rune);
}
context.state.has_props_rune = true;
@ -74,12 +74,39 @@ export function CallExpression(node, context) {
break;
case '$props.id': {
const grand_parent = get_parent(context.path, -2);
if (context.state.analysis.props_id) {
e.props_duplicate(node, rune);
}
if (
parent.type !== 'VariableDeclarator' ||
parent.id.type !== 'Identifier' ||
context.state.ast_type !== 'instance' ||
context.state.scope !== context.state.analysis.instance.scope ||
grand_parent.type !== 'VariableDeclaration'
) {
e.props_id_invalid_placement(node);
}
if (node.arguments.length > 0) {
e.rune_invalid_arguments(node, rune);
}
context.state.analysis.props_id = parent.id;
break;
}
case '$state':
case '$state.raw':
case '$derived':
case '$derived.by':
if (
parent.type !== 'VariableDeclarator' &&
(parent.type !== 'VariableDeclarator' ||
get_parent(context.path, -3).type === 'ConstTag') &&
!(parent.type === 'PropertyDefinition' && !parent.static && !parent.computed)
) {
e.state_invalid_placement(node, rune);
@ -191,18 +218,6 @@ export function CallExpression(node, context) {
break;
}
if (context.state.render_tag) {
// Find out which of the render tag arguments contains this call expression
const arg_idx = unwrap_optional(context.state.render_tag.expression).arguments.findIndex(
(arg) => arg === node || context.path.includes(arg)
);
// -1 if this is the call expression of the render tag itself
if (arg_idx !== -1) {
context.state.render_tag.metadata.args_with_call_expression.add(arg_idx);
}
}
if (node.callee.type === 'Identifier') {
const binding = context.state.scope.get(node.callee.name);

@ -8,20 +8,20 @@ import { get_rune } from '../../scope.js';
*/
export function ClassBody(node, context) {
/** @type {string[]} */
const private_derived_state = [];
const derived_state = [];
for (const definition of node.body) {
if (
definition.type === 'PropertyDefinition' &&
definition.key.type === 'PrivateIdentifier' &&
(definition.key.type === 'PrivateIdentifier' || definition.key.type === 'Identifier') &&
definition.value?.type === 'CallExpression'
) {
const rune = get_rune(definition.value, context.state.scope);
if (rune === '$derived' || rune === '$derived.by') {
private_derived_state.push(definition.key.name);
derived_state.push(definition.key.name);
}
}
}
context.next({ ...context.state, private_derived_state });
context.next({ ...context.state, derived_state });
}

@ -5,6 +5,7 @@ import * as e from '../../../errors.js';
import { validate_opening_tag } from './shared/utils.js';
import { mark_subtree_dynamic } from './shared/fragment.js';
import { is_resolved_snippet } from './shared/snippets.js';
import { create_expression_metadata } from '../../nodes.js';
/**
* @param {AST.RenderTag} node
@ -15,7 +16,8 @@ export function RenderTag(node, context) {
node.metadata.path = [...context.path];
const callee = unwrap_optional(node.expression).callee;
const expression = unwrap_optional(node.expression);
const callee = expression.callee;
const binding = callee.type === 'Identifier' ? context.state.scope.get(callee.name) : null;
@ -52,5 +54,15 @@ export function RenderTag(node, context) {
mark_subtree_dynamic(context.path);
context.next({ ...context.state, render_tag: node });
context.visit(callee);
for (const arg of expression.arguments) {
const metadata = create_expression_metadata();
node.metadata.arguments.push(metadata);
context.visit(arg, {
...context.state,
expression: metadata
});
}
}

@ -756,7 +756,8 @@ export function check_element(node, context) {
name === 'aria-activedescendant' &&
!is_dynamic_element &&
!is_interactive_element(node.name, attribute_map) &&
!attribute_map.has('tabindex')
!attribute_map.has('tabindex') &&
!has_spread
) {
w.a11y_aria_activedescendant_has_tabindex(attribute);
}
@ -810,9 +811,9 @@ export function check_element(node, context) {
const role = roles_map.get(current_role);
if (role) {
const required_role_props = Object.keys(role.requiredProps);
const has_missing_props = required_role_props.some(
(prop) => !attributes.find((a) => a.name === prop)
);
const has_missing_props =
!has_spread &&
required_role_props.some((prop) => !attributes.find((a) => a.name === prop));
if (has_missing_props) {
w.a11y_role_has_required_aria_props(
attribute,
@ -828,6 +829,7 @@ export function check_element(node, context) {
// interactive-supports-focus
if (
!has_spread &&
!has_disabled_attribute(attribute_map) &&
!is_hidden_from_screen_reader(node.name, attribute_map) &&
!is_presentation_role(current_role) &&
@ -845,6 +847,7 @@ export function check_element(node, context) {
// no-interactive-element-to-noninteractive-role
if (
!has_spread &&
is_interactive_element(node.name, attribute_map) &&
(is_non_interactive_roles(current_role) || is_presentation_role(current_role))
) {
@ -853,6 +856,7 @@ export function check_element(node, context) {
// no-noninteractive-element-to-interactive-role
if (
!has_spread &&
is_non_interactive_element(node.name, attribute_map) &&
is_interactive_roles(current_role) &&
!a11y_non_interactive_element_to_interactive_role_exceptions[node.name]?.includes(
@ -947,6 +951,7 @@ export function check_element(node, context) {
// no-noninteractive-element-interactions
if (
!has_spread &&
!has_contenteditable_attr &&
!is_hidden_from_screen_reader(node.name, attribute_map) &&
!is_presentation_role(role_static_value) &&
@ -964,6 +969,7 @@ export function check_element(node, context) {
// no-static-element-interactions
if (
!has_spread &&
(!role || role_static_value !== null) &&
!is_hidden_from_screen_reader(node.name, attribute_map) &&
!is_presentation_role(role_static_value) &&
@ -981,11 +987,11 @@ export function check_element(node, context) {
}
}
if (handlers.has('mouseover') && !handlers.has('focus')) {
if (!has_spread && handlers.has('mouseover') && !handlers.has('focus')) {
w.a11y_mouse_events_have_key_events(node, 'mouseover', 'focus');
}
if (handlers.has('mouseout') && !handlers.has('blur')) {
if (!has_spread && handlers.has('mouseout') && !handlers.has('blur')) {
w.a11y_mouse_events_have_key_events(node, 'mouseout', 'blur');
}
@ -995,7 +1001,7 @@ export function check_element(node, context) {
if (node.name === 'a' || node.name === 'button') {
const is_hidden = get_static_value(attribute_map.get('aria-hidden')) === 'true';
if (!is_hidden && !is_labelled && !has_content(node)) {
if (!has_spread && !is_hidden && !is_labelled && !has_content(node)) {
w.a11y_consider_explicit_label(node);
}
}
@ -1054,7 +1060,7 @@ export function check_element(node, context) {
if (node.name === 'img') {
const alt_attribute = get_static_text_value(attribute_map.get('alt'));
const aria_hidden = get_static_value(attribute_map.get('aria-hidden'));
if (alt_attribute && !aria_hidden) {
if (alt_attribute && !aria_hidden && !has_spread) {
if (/\b(image|picture|photo)\b/i.test(alt_attribute)) {
w.a11y_img_redundant_alt(node);
}
@ -1087,7 +1093,7 @@ export function check_element(node, context) {
);
return has;
};
if (!attribute_map.has('for') && !has_input_child(node)) {
if (!has_spread && !attribute_map.has('for') && !has_input_child(node)) {
w.a11y_label_has_associated_control(node);
}
}
@ -1095,7 +1101,7 @@ export function check_element(node, context) {
if (node.name === 'video') {
const aria_hidden_attribute = attribute_map.get('aria-hidden');
const aria_hidden_exist = aria_hidden_attribute && get_static_value(aria_hidden_attribute);
if (attribute_map.has('muted') || aria_hidden_exist === 'true') {
if (attribute_map.has('muted') || aria_hidden_exist === 'true' || has_spread) {
return;
}
let has_caption = false;
@ -1141,6 +1147,7 @@ export function check_element(node, context) {
// Check content
if (
!has_spread &&
!is_labelled &&
!has_contenteditable_binding &&
a11y_required_content.includes(node.name) &&

@ -80,40 +80,42 @@ export function validate_slot_attribute(context, attribute, is_component = false
}
if (owner) {
if (!is_text_attribute(attribute)) {
e.slot_attribute_invalid(attribute);
}
if (
owner.type === 'Component' ||
owner.type === 'SvelteComponent' ||
owner.type === 'SvelteSelf'
) {
if (owner !== parent) {
e.slot_attribute_invalid_placement(attribute);
}
if (!is_component) {
e.slot_attribute_invalid_placement(attribute);
}
} else {
if (!is_text_attribute(attribute)) {
e.slot_attribute_invalid(attribute);
}
const name = attribute.value[0].data;
const name = attribute.value[0].data;
if (context.state.component_slots.has(name)) {
e.slot_attribute_duplicate(attribute, name, owner.name);
}
context.state.component_slots.add(name);
if (context.state.component_slots.has(name)) {
e.slot_attribute_duplicate(attribute, name, owner.name);
}
if (name === 'default') {
for (const node of owner.fragment.nodes) {
if (node.type === 'Text' && regex_only_whitespaces.test(node.data)) {
continue;
}
context.state.component_slots.add(name);
if (node.type === 'RegularElement' || node.type === 'SvelteFragment') {
if (node.attributes.some((a) => a.type === 'Attribute' && a.name === 'slot')) {
if (name === 'default') {
for (const node of owner.fragment.nodes) {
if (node.type === 'Text' && regex_only_whitespaces.test(node.data)) {
continue;
}
}
e.slot_default_duplicate(node);
if (node.type === 'RegularElement' || node.type === 'SvelteFragment') {
if (node.attributes.some((a) => a.type === 'Attribute' && a.name === 'slot')) {
continue;
}
}
e.slot_default_duplicate(node);
}
}
}
}

@ -25,6 +25,10 @@ export function validate_assignment(node, argument, state) {
e.constant_assignment(node, 'derived state');
}
if (binding?.node === state.analysis.props_id) {
e.constant_assignment(node, '$props.id()');
}
if (binding?.kind === 'each') {
e.each_item_invalid_assignment(node);
}
@ -35,20 +39,17 @@ export function validate_assignment(node, argument, state) {
}
}
let object = /** @type {Expression | Super} */ (argument);
/** @type {Expression | PrivateIdentifier | null} */
let property = null;
while (object.type === 'MemberExpression') {
property = object.property;
object = object.object;
}
if (object.type === 'ThisExpression' && property?.type === 'PrivateIdentifier') {
if (state.private_derived_state.includes(property.name)) {
e.constant_assignment(node, 'derived state');
}
if (
argument.type === 'MemberExpression' &&
argument.object.type === 'ThisExpression' &&
(((argument.property.type === 'PrivateIdentifier' || argument.property.type === 'Identifier') &&
state.derived_state.includes(argument.property.name)) ||
(argument.property.type === 'Literal' &&
argument.property.value &&
typeof argument.property.value === 'string' &&
state.derived_state.includes(argument.property.value)))
) {
e.constant_assignment(node, 'derived state');
}
}

@ -565,6 +565,11 @@ export function client_component(analysis, options) {
component_block.body.unshift(b.stmt(b.call('$.check_target', b.id('new.target'))));
}
if (analysis.props_id) {
// need to be placed on first line of the component for hydration
component_block.body.unshift(b.const(analysis.props_id, b.call('$.props_id')));
}
if (state.events.size > 0) {
body.push(
b.stmt(b.call('$.delegate', b.array(Array.from(state.events).map((name) => b.literal(name)))))

@ -261,41 +261,6 @@ export function should_proxy(node, scope) {
return true;
}
/**
* @param {Pattern} node
* @param {import('zimmerframe').Context<AST.SvelteNode, ComponentClientTransformState>} context
* @returns {{ id: Pattern, declarations: null | Statement[] }}
*/
export function create_derived_block_argument(node, context) {
if (node.type === 'Identifier') {
context.state.transform[node.name] = { read: get_value };
return { id: node, declarations: null };
}
const pattern = /** @type {Pattern} */ (context.visit(node));
const identifiers = extract_identifiers(node);
const id = b.id('$$source');
const value = b.id('$$value');
const block = b.block([
b.var(pattern, b.call('$.get', id)),
b.return(b.object(identifiers.map((identifier) => b.prop('init', identifier, identifier))))
]);
const declarations = [b.var(value, create_derived(context.state, b.thunk(block)))];
for (const id of identifiers) {
context.state.transform[id.name] = { read: get_value };
declarations.push(
b.var(id, create_derived(context.state, b.thunk(b.member(b.call('$.get', value), id))))
);
}
return { id, declarations };
}
/**
* Svelte legacy mode should use safe equals in most places, runes mode shouldn't
* @param {ComponentClientTransformState} state

@ -1,8 +1,10 @@
/** @import { BlockStatement, Expression, Pattern, Statement } from 'estree' */
/** @import { AST } from '#compiler' */
/** @import { ComponentContext } from '../types' */
/** @import { ComponentClientTransformState, ComponentContext } from '../types' */
import { extract_identifiers } from '../../../../utils/ast.js';
import * as b from '../../../../utils/builders.js';
import { create_derived_block_argument } from '../utils.js';
import { create_derived } from '../utils.js';
import { get_value } from './shared/declarations.js';
/**
* @param {AST.AwaitBlock} node
@ -65,3 +67,38 @@ export function AwaitBlock(node, context) {
)
);
}
/**
* @param {Pattern} node
* @param {import('zimmerframe').Context<AST.SvelteNode, ComponentClientTransformState>} context
* @returns {{ id: Pattern, declarations: null | Statement[] }}
*/
function create_derived_block_argument(node, context) {
if (node.type === 'Identifier') {
context.state.transform[node.name] = { read: get_value };
return { id: node, declarations: null };
}
const pattern = /** @type {Pattern} */ (context.visit(node));
const identifiers = extract_identifiers(node);
const id = b.id('$$source');
const value = b.id('$$value');
const block = b.block([
b.var(pattern, b.call('$.get', id)),
b.return(b.object(identifiers.map((identifier) => b.prop('init', identifier, identifier))))
]);
const declarations = [b.var(value, create_derived(context.state, b.thunk(block)))];
for (const id of identifiers) {
context.state.transform[id.name] = { read: get_value };
declarations.push(
b.var(id, create_derived(context.state, b.thunk(b.member(b.call('$.get', value), id))))
);
}
return { id, declarations };
}

@ -198,22 +198,21 @@ export function ClassBody(node, context) {
'method',
b.id('$.ADD_OWNER'),
[b.id('owner')],
Array.from(public_state)
// Only run ownership addition on $state fields.
// Theoretically someone could create a `$state` while creating `$state.raw` or inside a `$derived.by`,
// but that feels so much of an edge case that it doesn't warrant a perf hit for the common case.
.filter(([_, { kind }]) => kind === 'state')
.map(([name]) =>
b.stmt(
b.call(
'$.add_owner',
b.call('$.get', b.member(b.this, b.private_id(name))),
b.id('owner'),
b.literal(false),
is_ignored(node, 'ownership_invalid_binding') && b.true
)
[
b.stmt(
b.call(
'$.add_owner_to_class',
b.this,
b.id('owner'),
b.array(
Array.from(public_state).map(([name]) =>
b.thunk(b.call('$.get', b.member(b.this, b.private_id(name))))
)
),
is_ignored(node, 'ownership_invalid_binding') && b.true
)
),
)
],
true
)
);

@ -35,10 +35,6 @@ export function EachBlock(node, context) {
context.state.template.push('<!>');
}
if (each_node_meta.array_name !== null) {
context.state.init.push(b.const(each_node_meta.array_name, b.thunk(collection)));
}
let flags = 0;
if (node.metadata.keyed && node.index) {
@ -120,8 +116,21 @@ export function EachBlock(node, context) {
return [array, ...transitive_dependencies];
});
if (each_node_meta.array_name) {
indirect_dependencies.push(b.call(each_node_meta.array_name));
/** @type {Identifier | null} */
let collection_id = null;
// Check if inner scope shadows something from outer scope.
// This is necessary because we need access to the array expression of the each block
// in the inner scope if bindings are used, in order to invalidate the array.
for (const [name] of context.state.scope.declarations) {
if (context.state.scope.parent?.get(name) != null) {
collection_id = context.state.scope.root.unique('$$array');
break;
}
}
if (collection_id) {
indirect_dependencies.push(b.call(collection_id));
} else {
indirect_dependencies.push(collection);
@ -195,7 +204,7 @@ export function EachBlock(node, context) {
// TODO 6.0 this only applies in legacy mode, reassignments are
// forbidden in runes mode
return b.member(
each_node_meta.array_name ? b.call(each_node_meta.array_name) : collection,
collection_id ? b.call(collection_id) : collection,
(flags & EACH_INDEX_REACTIVE) !== 0 ? get_value(index) : index,
true
);
@ -207,7 +216,7 @@ export function EachBlock(node, context) {
uses_index = true;
const left = b.member(
each_node_meta.array_name ? b.call(each_node_meta.array_name) : collection,
collection_id ? b.call(collection_id) : collection,
(flags & EACH_INDEX_REACTIVE) !== 0 ? get_value(index) : index,
true
);
@ -283,16 +292,17 @@ export function EachBlock(node, context) {
);
}
const render_args = [b.id('$$anchor'), item];
if (uses_index || collection_id) render_args.push(index);
if (collection_id) render_args.push(collection_id);
/** @type {Expression[]} */
const args = [
context.state.node,
b.literal(flags),
each_node_meta.array_name ? each_node_meta.array_name : b.thunk(collection),
b.thunk(collection),
key_function,
b.arrow(
uses_index ? [b.id('$$anchor'), item, index] : [b.id('$$anchor'), item],
b.block(declarations.concat(block.body))
)
b.arrow(render_args, b.block(declarations.concat(block.body)))
];
if (node.fallback) {

@ -227,7 +227,7 @@ export function RegularElement(node, context) {
node_id,
attributes_id,
(node.metadata.svg || node.metadata.mathml || is_custom_element_node(node)) && b.true,
node.name.includes('-') && b.true,
is_custom_element_node(node) && b.true,
context.state
);
@ -300,11 +300,6 @@ export function RegularElement(node, context) {
build_class_directives(class_directives, node_id, context, is_attributes_reactive);
build_style_directives(style_directives, node_id, context, is_attributes_reactive);
// Apply the src and loading attributes for <img> elements after the element is appended to the document
if (node.name === 'img' && (has_spread || lookup.has('loading'))) {
context.state.after_update.push(b.stmt(b.call('$.handle_lazy_img', node_id)));
}
if (
is_load_error_element(node.name) &&
(has_spread || has_use || lookup.has('onload') || lookup.has('onerror'))
@ -537,8 +532,8 @@ function build_element_attribute_update_assignment(
const is_svg = context.state.metadata.namespace === 'svg' || element.name === 'svg';
const is_mathml = context.state.metadata.namespace === 'mathml';
let { value, has_state } = build_attribute_value(attribute.value, context, (value) =>
get_expression_id(state, value)
let { value, has_state } = build_attribute_value(attribute.value, context, (value, metadata) =>
metadata.has_call ? get_expression_id(state, value) : value
);
if (name === 'autofocus') {
@ -665,8 +660,8 @@ function build_custom_element_attribute_update_assignment(node_id, attribute, co
*/
function build_element_special_value_attribute(element, node_id, attribute, context) {
const state = context.state;
const { value, has_state } = build_attribute_value(attribute.value, context, (value) =>
get_expression_id(state, value)
const { value, has_state } = build_attribute_value(attribute.value, context, (value, metadata) =>
metadata.has_call ? get_expression_id(state, value) : value
);
const inner_assignment = b.assignment(

@ -10,20 +10,24 @@ import * as b from '../../../../utils/builders.js';
*/
export function RenderTag(node, context) {
context.state.template.push('<!>');
const callee = unwrap_optional(node.expression).callee;
const raw_args = unwrap_optional(node.expression).arguments;
const expression = unwrap_optional(node.expression);
const callee = expression.callee;
const raw_args = expression.arguments;
/** @type {Expression[]} */
let args = [];
for (let i = 0; i < raw_args.length; i++) {
const raw = raw_args[i];
const arg = /** @type {Expression} */ (context.visit(raw));
if (node.metadata.args_with_call_expression.has(i)) {
let thunk = b.thunk(/** @type {Expression} */ (context.visit(raw_args[i])));
const { has_call } = node.metadata.arguments[i];
if (has_call) {
const id = b.id(context.state.scope.generate('render_arg'));
context.state.init.push(b.var(id, b.call('$.derived_safe_equal', b.thunk(arg))));
context.state.init.push(b.var(id, b.call('$.derived_safe_equal', thunk)));
args.push(b.thunk(b.call('$.get', id)));
} else {
args.push(b.thunk(arg));
args.push(thunk);
}
}

@ -30,8 +30,10 @@ export function SlotElement(node, context) {
if (attribute.type === 'SpreadAttribute') {
spreads.push(b.thunk(/** @type {Expression} */ (context.visit(attribute))));
} else if (attribute.type === 'Attribute') {
const { value, has_state } = build_attribute_value(attribute.value, context, (value) =>
memoize_expression(context.state, value)
const { value, has_state } = build_attribute_value(
attribute.value,
context,
(value, metadata) => (metadata.has_call ? memoize_expression(context.state, value) : value)
);
if (attribute.name === 'name') {

@ -42,6 +42,11 @@ export function VariableDeclaration(node, context) {
continue;
}
if (rune === '$props.id') {
// skip
continue;
}
if (rune === '$props') {
/** @type {string[]} */
const seen = ['$$slots', '$$events', '$$legacy'];

@ -4,7 +4,6 @@
import { dev, is_ignored } from '../../../../../state.js';
import { get_attribute_chunks, object } from '../../../../../utils/ast.js';
import * as b from '../../../../../utils/builders.js';
import { create_derived } from '../../utils.js';
import { build_bind_this, memoize_expression, validate_binding } from '../shared/utils.js';
import { build_attribute_value } from '../shared/element.js';
import { build_event_handler } from './events.js';
@ -134,9 +133,9 @@ export function build_component(node, component_name, context, anchor = context.
custom_css_props.push(
b.init(
attribute.name,
build_attribute_value(attribute.value, context, (value) =>
build_attribute_value(attribute.value, context, (value, metadata) =>
// TODO put the derived in the local block
memoize_expression(context.state, value)
metadata.has_call ? memoize_expression(context.state, value) : value
).value
)
);
@ -151,31 +150,29 @@ export function build_component(node, component_name, context, anchor = context.
has_children_prop = true;
}
const { value, has_state } = build_attribute_value(attribute.value, context, (value) =>
memoize_expression(context.state, value)
);
if (has_state) {
let arg = value;
// When we have a non-simple computation, anything other than an Identifier or Member expression,
// then there's a good chance it needs to be memoized to avoid over-firing when read within the
// child component.
const should_wrap_in_derived = get_attribute_chunks(attribute.value).some((n) => {
return (
n.type === 'ExpressionTag' &&
n.expression.type !== 'Identifier' &&
n.expression.type !== 'MemberExpression'
);
});
const { value, has_state } = build_attribute_value(
attribute.value,
context,
(value, metadata) => {
if (!metadata.has_state) return value;
// When we have a non-simple computation, anything other than an Identifier or Member expression,
// then there's a good chance it needs to be memoized to avoid over-firing when read within the
// child component (e.g. `active={i === index}`)
const should_wrap_in_derived = get_attribute_chunks(attribute.value).some((n) => {
return (
n.type === 'ExpressionTag' &&
n.expression.type !== 'Identifier' &&
n.expression.type !== 'MemberExpression'
);
});
if (should_wrap_in_derived) {
const id = b.id(context.state.scope.generate(attribute.name));
context.state.init.push(b.var(id, create_derived(context.state, b.thunk(value))));
arg = b.call('$.get', id);
return should_wrap_in_derived ? memoize_expression(context.state, value) : value;
}
);
push_prop(b.get(attribute.name, [b.return(arg)]));
if (has_state) {
push_prop(b.get(attribute.name, [b.return(value)]));
} else {
push_prop(b.init(attribute.name, value));
}
@ -183,37 +180,18 @@ export function build_component(node, component_name, context, anchor = context.
const expression = /** @type {Expression} */ (context.visit(attribute.expression));
if (dev && attribute.name !== 'this') {
let should_add_owner = true;
if (attribute.expression.type !== 'SequenceExpression') {
const left = object(attribute.expression);
if (left?.type === 'Identifier') {
const binding = context.state.scope.get(left.name);
// Only run ownership addition on $state fields.
// Theoretically someone could create a `$state` while creating `$state.raw` or inside a `$derived.by`,
// but that feels so much of an edge case that it doesn't warrant a perf hit for the common case.
if (binding?.kind === 'derived' || binding?.kind === 'raw_state') {
should_add_owner = false;
}
}
}
if (should_add_owner) {
binding_initializers.push(
b.stmt(
b.call(
b.id('$.add_owner_effect'),
expression.type === 'SequenceExpression'
? expression.expressions[0]
: b.thunk(expression),
b.id(component_name),
is_ignored(node, 'ownership_invalid_binding') && b.true
)
binding_initializers.push(
b.stmt(
b.call(
b.id('$.add_owner_effect'),
expression.type === 'SequenceExpression'
? expression.expressions[0]
: b.thunk(expression),
b.id(component_name),
is_ignored(node, 'ownership_invalid_binding') && b.true
)
);
}
)
);
}
if (expression.type === 'SequenceExpression') {

@ -1,11 +1,11 @@
/** @import { Expression, Identifier, ObjectExpression } from 'estree' */
/** @import { AST } from '#compiler' */
/** @import { AST, ExpressionMetadata } from '#compiler' */
/** @import { ComponentClientTransformState, ComponentContext } from '../../types' */
import { normalize_attribute } from '../../../../../../utils.js';
import { is_ignored } from '../../../../../state.js';
import { is_event_attribute } from '../../../../../utils/ast.js';
import * as b from '../../../../../utils/builders.js';
import { build_getter, create_derived } from '../../utils.js';
import { build_getter } from '../../utils.js';
import { build_template_chunk, get_expression_id } from './utils.js';
/**
@ -35,8 +35,10 @@ export function build_set_attributes(
for (const attribute of attributes) {
if (attribute.type === 'Attribute') {
const { value, has_state } = build_attribute_value(attribute.value, context, (value) =>
get_expression_id(context.state, value)
const { value, has_state } = build_attribute_value(
attribute.value,
context,
(value, metadata) => (metadata.has_call ? get_expression_id(context.state, value) : value)
);
if (
@ -59,10 +61,9 @@ export function build_set_attributes(
let value = /** @type {Expression} */ (context.visit(attribute));
if (attribute.metadata.expression.has_call) {
const id = b.id(state.scope.generate('spread_with_call'));
state.init.push(b.const(id, create_derived(state, b.thunk(value))));
value = b.call('$.get', id);
value = get_expression_id(context.state, value);
}
values.push(b.spread(value));
}
}
@ -111,8 +112,8 @@ export function build_style_directives(
let value =
directive.value === true
? build_getter({ name: directive.name, type: 'Identifier' }, context.state)
: build_attribute_value(directive.value, context, (value) =>
get_expression_id(context.state, value)
: build_attribute_value(directive.value, context, (value, metadata) =>
metadata.has_call ? get_expression_id(context.state, value) : value
).value;
const update = b.stmt(
@ -169,7 +170,7 @@ export function build_class_directives(
/**
* @param {AST.Attribute['value']} value
* @param {ComponentContext} context
* @param {(value: Expression) => Expression} memoize
* @param {(value: Expression, metadata: ExpressionMetadata) => Expression} memoize
* @returns {{ value: Expression, has_state: boolean }}
*/
export function build_attribute_value(value, context, memoize = (value) => value) {
@ -187,7 +188,7 @@ export function build_attribute_value(value, context, memoize = (value) => value
let expression = /** @type {Expression} */ (context.visit(chunk.expression));
return {
value: chunk.metadata.expression.has_call ? memoize(expression) : expression,
value: memoize(expression, chunk.metadata.expression),
has_state: chunk.metadata.expression.has_state
};
}

@ -4,6 +4,7 @@
import { cannot_be_set_statically } from '../../../../../../utils.js';
import { is_event_attribute, is_text_attribute } from '../../../../../utils/ast.js';
import * as b from '../../../../../utils/builders.js';
import { is_custom_element_node } from '../../../../nodes.js';
import { build_template_chunk } from './utils.js';
/**
@ -128,7 +129,7 @@ export function process_children(nodes, initial, is_element, { visit, state }) {
function is_static_element(node, state) {
if (node.type !== 'RegularElement') return false;
if (node.fragment.metadata.dynamic) return false;
if (node.name.includes('-')) return false; // we're setting all attributes on custom elements through properties
if (is_custom_element_node(node)) return false; // we're setting all attributes on custom elements through properties
for (const attribute of node.attributes) {
if (attribute.type !== 'Attribute') {

@ -1,5 +1,5 @@
/** @import { Expression, ExpressionStatement, Identifier, MemberExpression, SequenceExpression, Statement, Super } from 'estree' */
/** @import { AST } from '#compiler' */
/** @import { AST, ExpressionMetadata } from '#compiler' */
/** @import { ComponentClientTransformState } from '../../types' */
import { walk } from 'zimmerframe';
import { object } from '../../../../../utils/ast.js';
@ -79,14 +79,14 @@ function compare_expressions(a, b) {
* @param {Array<AST.Text | AST.ExpressionTag>} values
* @param {(node: AST.SvelteNode, state: any) => any} visit
* @param {ComponentClientTransformState} state
* @param {(value: Expression) => Expression} memoize
* @param {(value: Expression, metadata: ExpressionMetadata) => Expression} memoize
* @returns {{ value: Expression, has_state: boolean }}
*/
export function build_template_chunk(
values,
visit,
state,
memoize = (value) => get_expression_id(state, value)
memoize = (value, metadata) => (metadata.has_call ? get_expression_id(state, value) : value)
) {
/** @type {Expression[]} */
const expressions = [];
@ -106,35 +106,40 @@ export function build_template_chunk(
quasi.value.cooked += node.expression.value + '';
}
} else {
let value = /** @type {Expression} */ (visit(node.expression, state));
let value = memoize(
/** @type {Expression} */ (visit(node.expression, state)),
node.metadata.expression
);
has_state ||= node.metadata.expression.has_state;
if (node.metadata.expression.has_call) {
value = memoize(value);
}
if (values.length === 1) {
// If we have a single expression, then pass that in directly to possibly avoid doing
// extra work in the template_effect (instead we do the work in set_text).
return { value, has_state };
} else {
let expression = value;
// only add nullish coallescence if it hasn't been added already
if (value.type === 'LogicalExpression' && value.operator === '??') {
const { right } = value;
// `undefined` isn't a Literal (due to pre-ES5 shenanigans), so the only nullish literal is `null`
// however, you _can_ make a variable called `undefined` in a Svelte component, so we can't just treat it the same way
if (right.type !== 'Literal') {
expression = b.logical('??', value, b.literal(''));
} else if (right.value === null) {
// if they do something weird like `stuff ?? null`, replace `null` with empty string
value.right = b.literal('');
// add `?? ''` where necessary (TODO optimise more cases)
if (
value.type === 'LogicalExpression' &&
value.right.type === 'Literal' &&
(value.operator === '??' || value.operator === '||')
) {
// `foo ?? null` -=> `foo ?? ''`
// otherwise leave the expression untouched
if (value.right.value === null) {
value = { ...value, right: b.literal('') };
}
} else if (
state.analysis.props_id &&
value.type === 'Identifier' &&
value.name === state.analysis.props_id.name
) {
// do nothing ($props.id() is never null/undefined)
} else {
expression = b.logical('??', value, b.literal(''));
value = b.logical('??', value, b.literal(''));
}
expressions.push(expression);
expressions.push(value);
}
quasi = b.quasi('', i + 1 === values.length);

@ -244,6 +244,13 @@ export function server_component(analysis, options) {
.../** @type {Statement[]} */ (template.body)
]);
if (analysis.props_id) {
// need to be placed on first line of the component for hydration
component_block.body.unshift(
b.const(analysis.props_id, b.call('$.props_id', b.id('$$payload')))
);
}
let should_inject_context = dev || analysis.needs_context;
if (should_inject_context) {

@ -24,6 +24,11 @@ export function VariableDeclaration(node, context) {
continue;
}
if (rune === '$props.id') {
// skip
continue;
}
if (rune === '$props') {
let has_rest = false;
// remove $bindable() from props declaration
@ -156,6 +161,10 @@ export function VariableDeclaration(node, context) {
}
}
if (declarations.length === 0) {
return b.empty;
}
return {
...node,
declarations

@ -23,10 +23,14 @@ export function is_element_node(node) {
/**
* @param {AST.RegularElement | AST.SvelteElement} node
* @returns {node is AST.RegularElement}
* @returns {boolean}
*/
export function is_custom_element_node(node) {
return node.type === 'RegularElement' && node.name.includes('-');
return (
node.type === 'RegularElement' &&
(node.name.includes('-') ||
node.attributes.some((attr) => attr.type === 'Attribute' && attr.name === 'is'))
);
}
/**

@ -575,21 +575,10 @@ export function create_scopes(ast, root, allow_reactive_declarations, parent) {
}
if (node.fallback) visit(node.fallback, { scope });
// Check if inner scope shadows something from outer scope.
// This is necessary because we need access to the array expression of the each block
// in the inner scope if bindings are used, in order to invalidate the array.
let needs_array_deduplication = false;
for (const [name] of scope.declarations) {
if (state.scope.get(name) !== null) {
needs_array_deduplication = true;
}
}
node.metadata = {
expression: create_expression_metadata(),
keyed: false,
contains_group_binding: false,
array_name: needs_array_deduplication ? state.scope.root.unique('$$array') : null,
index: scope.root.unique('$$index'),
declarations: scope.declarations,
is_controlled: false

@ -44,6 +44,8 @@ export interface ComponentAnalysis extends Analysis {
exports: Array<{ name: string; alias: string | null }>;
/** Whether the component uses `$$props` */
uses_props: boolean;
/** The component ID variable name, if any */
props_id: Identifier | null;
/** Whether the component uses `$$restProps` */
uses_rest_props: boolean;
/** Whether the component uses `$$slots` */

@ -166,7 +166,7 @@ export namespace AST {
/** @internal */
metadata: {
dynamic: boolean;
args_with_call_expression: Set<number>;
arguments: ExpressionMetadata[];
path: SvelteNode[];
/** The set of locally-defined snippets that this render tag could correspond to,
* used for CSS pruning purposes */
@ -414,8 +414,6 @@ export namespace AST {
expression: ExpressionMetadata;
keyed: boolean;
contains_group_binding: boolean;
/** Set if something in the array expression is shadowed within the each block */
array_name: Identifier | null;
index: Identifier;
declarations: Map<string, Binding>;
/**

@ -1,12 +1,48 @@
/** @import { ComponentContext, ComponentContextLegacy } from '#client' */
/** @import { EventDispatcher } from './index.js' */
/** @import { NotFunction } from './internal/types.js' */
import { component_context, flush_sync, untrack } from './internal/client/runtime.js';
import { flush_sync, untrack } from './internal/client/runtime.js';
import { is_array } from './internal/shared/utils.js';
import { user_effect } from './internal/client/index.js';
import * as e from './internal/client/errors.js';
import { lifecycle_outside_component } from './internal/shared/errors.js';
import { legacy_mode_flag } from './internal/flags/index.js';
import { component_context } from './internal/client/context.js';
import { DEV } from 'esm-env';
if (DEV) {
/**
* @param {string} rune
*/
function throw_rune_error(rune) {
if (!(rune in globalThis)) {
// TODO if people start adjusting the "this can contain runes" config through v-p-s more, adjust this message
/** @type {any} */
let value; // let's hope noone modifies this global, but belts and braces
Object.defineProperty(globalThis, rune, {
configurable: true,
// eslint-disable-next-line getter-return
get: () => {
if (value !== undefined) {
return value;
}
e.rune_outside_svelte(rune);
},
set: (v) => {
value = v;
}
});
}
}
throw_rune_error('$state');
throw_rune_error('$effect');
throw_rune_error('$derived');
throw_rune_error('$inspect');
throw_rune_error('$props');
throw_rune_error('$bindable');
}
/**
* The `onMount` function schedules a callback to run as soon as the component has been mounted to the DOM.
@ -179,15 +215,7 @@ export function flushSync(fn) {
flush_sync(fn);
}
export { getContext, getAllContexts, hasContext, setContext } from './internal/client/context.js';
export { hydrate, mount, unmount } from './internal/client/render.js';
export {
getContext,
getAllContexts,
hasContext,
setContext,
tick,
untrack
} from './internal/client/runtime.js';
export { tick, untrack } from './internal/client/runtime.js';
export { createRawSnippet } from './internal/client/dom/blocks/snippet.js';

@ -0,0 +1,211 @@
/** @import { ComponentContext } from '#client' */
import { DEV } from 'esm-env';
import { add_owner } from './dev/ownership.js';
import { lifecycle_outside_component } from '../shared/errors.js';
import { source } from './reactivity/sources.js';
import {
active_effect,
active_reaction,
set_active_effect,
set_active_reaction,
untrack
} from './runtime.js';
import { effect } from './reactivity/effects.js';
import { legacy_mode_flag } from '../flags/index.js';
/** @type {ComponentContext | null} */
export let component_context = null;
/** @param {ComponentContext | null} context */
export function set_component_context(context) {
component_context = context;
}
/**
* The current component function. Different from current component context:
* ```html
* <!-- App.svelte -->
* <Foo>
* <Bar /> <!-- context == Foo.svelte, function == App.svelte -->
* </Foo>
* ```
* @type {ComponentContext['function']}
*/
export let dev_current_component_function = null;
/** @param {ComponentContext['function']} fn */
export function set_dev_current_component_function(fn) {
dev_current_component_function = fn;
}
/**
* Retrieves the context that belongs to the closest parent component with the specified `key`.
* Must be called during component initialisation.
*
* @template T
* @param {any} key
* @returns {T}
*/
export function getContext(key) {
const context_map = get_or_init_context_map('getContext');
const result = /** @type {T} */ (context_map.get(key));
return result;
}
/**
* Associates an arbitrary `context` object with the current component and the specified `key`
* and returns that object. The context is then available to children of the component
* (including slotted content) with `getContext`.
*
* Like lifecycle functions, this must be called during component initialisation.
*
* @template T
* @param {any} key
* @param {T} context
* @returns {T}
*/
export function setContext(key, context) {
const context_map = get_or_init_context_map('setContext');
if (DEV) {
// When state is put into context, we treat as if it's global from now on.
// We do for performance reasons (it's for example very expensive to call
// getContext on a big object many times when part of a list component)
// and danger of false positives.
untrack(() => add_owner(context, null, true));
}
context_map.set(key, context);
return context;
}
/**
* Checks whether a given `key` has been set in the context of a parent component.
* Must be called during component initialisation.
*
* @param {any} key
* @returns {boolean}
*/
export function hasContext(key) {
const context_map = get_or_init_context_map('hasContext');
return context_map.has(key);
}
/**
* Retrieves the whole context map that belongs to the closest parent component.
* Must be called during component initialisation. Useful, for example, if you
* programmatically create a component and want to pass the existing context to it.
*
* @template {Map<any, any>} [T=Map<any, any>]
* @returns {T}
*/
export function getAllContexts() {
const context_map = get_or_init_context_map('getAllContexts');
return /** @type {T} */ (context_map);
}
/**
* @param {Record<string, unknown>} props
* @param {any} runes
* @param {Function} [fn]
* @returns {void}
*/
export function push(props, runes = false, fn) {
component_context = {
p: component_context,
c: null,
e: null,
m: false,
s: props,
x: null,
l: null
};
if (legacy_mode_flag && !runes) {
component_context.l = {
s: null,
u: null,
r1: [],
r2: source(false)
};
}
if (DEV) {
// component function
component_context.function = fn;
dev_current_component_function = fn;
}
}
/**
* @template {Record<string, any>} T
* @param {T} [component]
* @returns {T}
*/
export function pop(component) {
const context_stack_item = component_context;
if (context_stack_item !== null) {
if (component !== undefined) {
context_stack_item.x = component;
}
const component_effects = context_stack_item.e;
if (component_effects !== null) {
var previous_effect = active_effect;
var previous_reaction = active_reaction;
context_stack_item.e = null;
try {
for (var i = 0; i < component_effects.length; i++) {
var component_effect = component_effects[i];
set_active_effect(component_effect.effect);
set_active_reaction(component_effect.reaction);
effect(component_effect.fn);
}
} finally {
set_active_effect(previous_effect);
set_active_reaction(previous_reaction);
}
}
component_context = context_stack_item.p;
if (DEV) {
dev_current_component_function = context_stack_item.p?.function ?? null;
}
context_stack_item.m = true;
}
// Micro-optimization: Don't set .a above to the empty object
// so it can be garbage-collected when the return here is unused
return component || /** @type {T} */ ({});
}
/** @returns {boolean} */
export function is_runes() {
return !legacy_mode_flag || (component_context !== null && component_context.l === null);
}
/**
* @param {string} name
* @returns {Map<unknown, unknown>}
*/
function get_or_init_context_map(name) {
if (component_context === null) {
lifecycle_outside_component(name);
}
return (component_context.c ??= new Map(get_parent_context(component_context) || undefined));
}
/**
* @param {ComponentContext} component_context
* @returns {Map<unknown, unknown> | null}
*/
function get_parent_context(component_context) {
let parent = component_context.p;
while (parent !== null) {
const context_map = parent.c;
if (context_map !== null) {
return context_map;
}
parent = parent.p;
}
return null;
}

@ -1,5 +1,5 @@
import * as e from '../errors.js';
import { component_context } from '../runtime.js';
import { component_context } from '../context.js';
import { FILENAME } from '../../../constants.js';
import { get_component } from './ownership.js';

@ -3,10 +3,10 @@
import { STATE_SYMBOL_METADATA } from '../constants.js';
import { render_effect, user_pre_effect } from '../reactivity/effects.js';
import { dev_current_component_function } from '../runtime.js';
import { dev_current_component_function } from '../context.js';
import { get_prototype_of } from '../../shared/utils.js';
import * as w from '../warnings.js';
import { FILENAME } from '../../../constants.js';
import { FILENAME, UNINITIALIZED } from '../../../constants.js';
/** @type {Record<string, Array<{ start: Location, end: Location, component: Function }>>} */
const boundaries = {};
@ -109,7 +109,7 @@ export function mark_module_end(component) {
/**
* @param {any} object
* @param {any} owner
* @param {any | null} owner
* @param {boolean} [global]
* @param {boolean} [skip_warning]
*/
@ -120,7 +120,7 @@ export function add_owner(object, owner, global = false, skip_warning = false) {
if (metadata && !has_owner(metadata, component)) {
let original = get_owner(metadata);
if (owner[FILENAME] !== component[FILENAME] && !skip_warning) {
if (owner && owner[FILENAME] !== component[FILENAME] && !skip_warning) {
w.ownership_invalid_binding(component[FILENAME], owner[FILENAME], original[FILENAME]);
}
}
@ -140,6 +140,25 @@ export function add_owner_effect(get_object, Component, skip_warning = false) {
});
}
/**
* @param {any} _this
* @param {Function} owner
* @param {Array<() => any>} getters
* @param {boolean} skip_warning
*/
export function add_owner_to_class(_this, owner, getters, skip_warning) {
_this[ADD_OWNER].current ||= getters.map(() => UNINITIALIZED);
for (let i = 0; i < getters.length; i += 1) {
const current = getters[i]();
// For performance reasons we only re-add the owner if the state has changed
if (current !== _this[ADD_OWNER][i]) {
_this[ADD_OWNER].current[i] = current;
add_owner(current, owner, false, skip_warning);
}
}
}
/**
* @param {ProxyMetadata | null} from
* @param {ProxyMetadata} to
@ -165,7 +184,7 @@ export function widen_ownership(from, to) {
/**
* @param {any} object
* @param {Function} owner
* @param {Function | null} owner If `null`, then the object is globally owned and will not be checked
* @param {Set<any>} seen
*/
function add_owner_to_object(object, owner, seen) {
@ -174,7 +193,11 @@ function add_owner_to_object(object, owner, seen) {
if (metadata) {
// this is a state proxy, add owner directly, if not globally shared
if ('owners' in metadata && metadata.owners != null) {
metadata.owners.add(owner);
if (owner) {
metadata.owners.add(owner);
} else {
metadata.owners = null;
}
}
} else if (object && typeof object === 'object') {
if (seen.has(object)) return;
@ -192,7 +215,19 @@ function add_owner_to_object(object, owner, seen) {
if (proto === Object.prototype) {
// recurse until we find a state proxy
for (const key in object) {
add_owner_to_object(object[key], owner, seen);
if (Object.getOwnPropertyDescriptor(object, key)?.get) {
// Similar to the class case; the getter could update with a new state
let current = UNINITIALIZED;
render_effect(() => {
const next = object[key];
if (current !== next) {
current = next;
add_owner_to_object(next, owner, seen);
}
});
} else {
add_owner_to_object(object[key], owner, seen);
}
}
} else if (proto === Array.prototype) {
// recurse until we find a state proxy
@ -216,6 +251,11 @@ function has_owner(metadata, component) {
return (
metadata.owners.has(component) ||
// This helps avoid false positives when using HMR, where the component function is replaced
(FILENAME in component &&
[...metadata.owners].some(
(owner) => /** @type {any} */ (owner)[FILENAME] === component[FILENAME]
)) ||
(metadata.parent !== null && has_owner(metadata.parent, component))
);
}

@ -3,18 +3,16 @@ import { DEV } from 'esm-env';
import { is_promise } from '../../../shared/utils.js';
import { block, branch, pause_effect, resume_effect } from '../../reactivity/effects.js';
import { internal_set, mutable_source, source } from '../../reactivity/sources.js';
import { flush_sync, set_active_effect, set_active_reaction } from '../../runtime.js';
import { hydrate_next, hydrate_node, hydrating } from '../hydration.js';
import { queue_micro_task } from '../task.js';
import { UNINITIALIZED } from '../../../../constants.js';
import {
component_context,
flush_sync,
is_runes,
set_active_effect,
set_active_reaction,
set_component_context,
set_dev_current_component_function
} from '../../runtime.js';
import { hydrate_next, hydrate_node, hydrating } from '../hydration.js';
import { queue_micro_task } from '../task.js';
import { UNINITIALIZED } from '../../../../constants.js';
} from '../../context.js';
const PENDING = 0;
const THEN = 1;

@ -1,15 +1,14 @@
/** @import { Effect, TemplateNode, } from '#client' */
import { BOUNDARY_EFFECT, EFFECT_TRANSPARENT } from '../../constants.js';
import { component_context, set_component_context } from '../../context.js';
import { block, branch, destroy_effect, pause_effect } from '../../reactivity/effects.js';
import {
active_effect,
active_reaction,
component_context,
handle_error,
set_active_effect,
set_active_reaction,
set_component_context,
reset_is_throwing_error
} from '../../runtime.js';
import {

@ -219,17 +219,7 @@ export function each(node, flags, get_collection, get_key, render_fn, fallback_f
}
if (!hydrating) {
var effect = /** @type {Effect} */ (active_reaction);
reconcile(
array,
state,
anchor,
render_fn,
flags,
(effect.f & INERT) !== 0,
get_key,
get_collection
);
reconcile(array, state, anchor, render_fn, flags, get_key, get_collection);
}
if (fallback_fn !== null) {
@ -271,14 +261,13 @@ export function each(node, flags, get_collection, get_key, render_fn, fallback_f
* @param {Array<V>} array
* @param {EachState} state
* @param {Element | Comment | Text} anchor
* @param {(anchor: Node, item: MaybeSource<V>, index: number | Source<number>) => void} render_fn
* @param {(anchor: Node, item: MaybeSource<V>, index: number | Source<number>, collection: () => V[]) => void} render_fn
* @param {number} flags
* @param {boolean} is_inert
* @param {(value: V, index: number) => any} get_key
* @param {() => V[]} get_collection
* @returns {void}
*/
function reconcile(array, state, anchor, render_fn, flags, is_inert, get_key, get_collection) {
function reconcile(array, state, anchor, render_fn, flags, get_key, get_collection) {
var is_animated = (flags & EACH_IS_ANIMATED) !== 0;
var should_update = (flags & (EACH_ITEM_REACTIVE | EACH_INDEX_REACTIVE)) !== 0;
@ -420,7 +409,7 @@ function reconcile(array, state, anchor, render_fn, flags, is_inert, get_key, ge
while (current !== null && current.k !== key) {
// If the each block isn't inert and an item has an effect that is already inert,
// skip over adding it to our seen Set as the item is already being handled
if (is_inert || (current.e.f & INERT) === 0) {
if ((current.e.f & INERT) === 0) {
(seen ??= new Set()).add(current);
}
stashed.push(current);
@ -444,7 +433,7 @@ function reconcile(array, state, anchor, render_fn, flags, is_inert, get_key, ge
while (current !== null) {
// If the each block isn't inert, then inert effects are currently outroing and will be removed once the transition is finished
if (is_inert || (current.e.f & INERT) === 0) {
if ((current.e.f & INERT) === 0) {
to_destroy.push(current);
}
current = current.next;
@ -510,7 +499,7 @@ function update_item(item, value, index, type) {
* @param {V} value
* @param {unknown} key
* @param {number} index
* @param {(anchor: Node, item: V | Source<V>, index: number | Value<number>) => void} render_fn
* @param {(anchor: Node, item: V | Source<V>, index: number | Value<number>, collection: () => V[]) => void} render_fn
* @param {number} flags
* @param {() => V[]} get_collection
* @returns {EachItem}
@ -559,7 +548,7 @@ function create_item(
current_each_item = item;
try {
item.e = branch(() => render_fn(anchor, v, i), hydrating);
item.e = branch(() => render_fn(anchor, v, i, get_collection), hydrating);
item.e.prev = prev && prev.e;
item.e.next = next && next.e;

@ -7,7 +7,7 @@ import { assign_nodes } from '../template.js';
import * as w from '../../warnings.js';
import { hash, sanitize_location } from '../../../../utils.js';
import { DEV } from 'esm-env';
import { dev_current_component_function } from '../../runtime.js';
import { dev_current_component_function } from '../../context.js';
import { get_first_child, get_next_sibling } from '../operations.js';
/**

@ -2,7 +2,7 @@
import { UNINITIALIZED } from '../../../../constants.js';
import { block, branch, pause_effect } from '../../reactivity/effects.js';
import { not_equal, safe_not_equal } from '../../reactivity/equality.js';
import { is_runes } from '../../runtime.js';
import { is_runes } from '../../context.js';
import { hydrate_next, hydrate_node, hydrating } from '../hydration.js';
/**

@ -6,7 +6,7 @@ import { branch, block, destroy_effect, teardown } from '../../reactivity/effect
import {
dev_current_component_function,
set_dev_current_component_function
} from '../../runtime.js';
} from '../../context.js';
import { hydrate_next, hydrate_node, hydrating } from '../hydration.js';
import { create_fragment_from_html } from '../reconciler.js';
import { assign_nodes } from '../template.js';

@ -17,7 +17,8 @@ import {
} from '../../reactivity/effects.js';
import { set_should_intro } from '../../render.js';
import { current_each_item, set_current_each_item } from './each.js';
import { component_context, active_effect } from '../../runtime.js';
import { active_effect } from '../../runtime.js';
import { component_context } from '../../context.js';
import { DEV } from 'esm-env';
import { EFFECT_TRANSPARENT } from '../../constants.js';
import { assign_nodes } from '../template.js';

@ -1,5 +1,5 @@
import { DEV } from 'esm-env';
import { hydrating } from '../hydration.js';
import { hydrating, set_hydrating } from '../hydration.js';
import { get_descriptors, get_prototype_of } from '../../../shared/utils.js';
import { create_event, delegate } from './events.js';
import { add_form_reset_listener, autofocus } from './misc.js';
@ -68,14 +68,14 @@ export function set_value(element, value) {
// treat null and undefined the same for the initial value
value ?? undefined) ||
// @ts-expect-error
// `progress` elements always need their value set when its `0`
// `progress` elements always need their value set when it's `0`
(element.value === value && (value !== 0 || element.nodeName !== 'PROGRESS'))
) {
return;
}
// @ts-expect-error
element.value = value;
element.value = value ?? '';
}
/**
@ -213,6 +213,12 @@ export function set_custom_element_data(node, prop, value) {
// or effect
var previous_reaction = active_reaction;
var previous_effect = active_effect;
// If we're hydrating but the custom element is from Svelte, and it already scaffolded,
// then it might run block logic in hydration mode, which we have to prevent.
let was_hydrating = hydrating;
if (hydrating) {
set_hydrating(false);
}
set_active_reaction(null);
set_active_effect(null);
@ -239,6 +245,9 @@ export function set_custom_element_data(node, prop, value) {
} finally {
set_active_reaction(previous_reaction);
set_active_effect(previous_effect);
if (was_hydrating) {
set_hydrating(true);
}
}
}
@ -262,6 +271,13 @@ export function set_attributes(
is_custom_element = false,
skip_warning = false
) {
// If we're hydrating but the custom element is from Svelte, and it already scaffolded,
// then it might run block logic in hydration mode, which we have to prevent.
let is_hydrating_custom_element = hydrating && is_custom_element;
if (is_hydrating_custom_element) {
set_hydrating(false);
}
var current = prev || {};
var is_option_element = element.tagName === 'OPTION';
@ -363,9 +379,10 @@ export function set_attributes(
element.style.cssText = value + '';
} else if (key === 'autofocus') {
autofocus(/** @type {HTMLElement} */ (element), Boolean(value));
} else if (key === '__value' || (key === 'value' && value != null)) {
// @ts-ignore
element.value = element[key] = element.__value = value;
} else if (!is_custom_element && (key === '__value' || (key === 'value' && value != null))) {
// @ts-ignore We're not running this for custom elements because __value is actually
// how Lit stores the current value on the element, and messing with that would break things.
element.value = element.__value = value;
} else if (key === 'selected' && is_option_element) {
set_selected(/** @type {HTMLOptionElement} */ (element), value);
} else {
@ -382,15 +399,18 @@ export function set_attributes(
if (name === 'value' || name === 'checked') {
// removing value/checked also removes defaultValue/defaultChecked — preserve
let input = /** @type {HTMLInputElement} */ (element);
const use_default = prev === undefined;
if (name === 'value') {
let prev = input.defaultValue;
let previous = input.defaultValue;
input.removeAttribute(name);
input.defaultValue = prev;
input.defaultValue = previous;
// @ts-ignore
input.value = input.__value = use_default ? previous : null;
} else {
let prev = input.defaultChecked;
let previous = input.defaultChecked;
input.removeAttribute(name);
input.defaultChecked = prev;
input.defaultChecked = previous;
input.checked = use_default ? previous : false;
}
} else {
element.removeAttribute(key);
@ -402,11 +422,7 @@ export function set_attributes(
// @ts-ignore
element[name] = value;
} else if (typeof value !== 'function') {
if (hydrating && (name === 'src' || name === 'href' || name === 'srcset')) {
if (!skip_warning) check_src_in_dev_hydration(element, name, value ?? '');
} else {
set_attribute(element, name, value);
}
set_attribute(element, name, value);
}
}
if (key === 'style' && '__styles' in element) {
@ -415,6 +431,10 @@ export function set_attributes(
}
}
if (is_hydrating_custom_element) {
set_hydrating(true);
}
return current;
}
@ -503,28 +523,3 @@ function srcset_url_equal(element, srcset) {
)
);
}
/**
* @param {HTMLImageElement} element
* @returns {void}
*/
export function handle_lazy_img(element) {
// If we're using an image that has a lazy loading attribute, we need to apply
// the loading and src after the img element has been appended to the document.
// Otherwise the lazy behaviour will not work due to our cloneNode heuristic for
// templates.
if (!hydrating && element.loading === 'lazy') {
var src = element.src;
// @ts-expect-error
element[LOADING_ATTR_SYMBOL] = null;
element.loading = 'eager';
element.removeAttribute('src');
requestAnimationFrame(() => {
// @ts-expect-error
if (element[LOADING_ATTR_SYMBOL] !== 'eager') {
element.loading = 'lazy';
}
element.src = src;
});
}
}

@ -5,7 +5,8 @@ import * as e from '../../../errors.js';
import { is } from '../../../proxy.js';
import { queue_micro_task } from '../../task.js';
import { hydrating } from '../../hydration.js';
import { is_runes, untrack } from '../../../runtime.js';
import { untrack } from '../../../runtime.js';
import { is_runes } from '../../../context.js';
/**
* @param {HTMLInputElement} input

@ -49,10 +49,10 @@ export function replay_events(dom) {
/**
* @param {string} event_name
* @param {EventTarget} dom
* @param {EventListener} handler
* @param {AddEventListenerOptions} options
* @param {EventListener} [handler]
* @param {AddEventListenerOptions} [options]
*/
export function create_event(event_name, dom, handler, options) {
export function create_event(event_name, dom, handler, options = {}) {
/**
* @this {EventTarget}
*/
@ -63,7 +63,7 @@ export function create_event(event_name, dom, handler, options) {
}
if (!event.cancelBubble) {
return without_reactive_context(() => {
return handler.call(this, event);
return handler?.call(this, event);
});
}
}
@ -108,8 +108,8 @@ export function on(element, type, handler, options = {}) {
/**
* @param {string} event_name
* @param {Element} dom
* @param {EventListener} handler
* @param {boolean} capture
* @param {EventListener} [handler]
* @param {boolean} [capture]
* @param {boolean} [passive]
* @returns {void}
*/

@ -1,8 +1,9 @@
/** @import { ComponentContextLegacy } from '#client' */
import { run, run_all } from '../../../shared/utils.js';
import { component_context } from '../../context.js';
import { derived } from '../../reactivity/deriveds.js';
import { user_pre_effect, user_effect } from '../../reactivity/effects.js';
import { component_context, deep_read_state, get, untrack } from '../../runtime.js';
import { deep_read_state, get, untrack } from '../../runtime.js';
/**
* Legacy-mode only: Call `onMount` callbacks and set up `beforeUpdate`/`afterUpdate` effects

@ -11,6 +11,9 @@ export var $window;
/** @type {Document} */
export var $document;
/** @type {boolean} */
export var is_firefox;
/** @type {() => Node | null} */
var first_child_getter;
/** @type {() => Node | null} */
@ -27,6 +30,7 @@ export function init_operations() {
$window = window;
$document = document;
is_firefox = /Firefox/.test(navigator.userAgent);
var element_prototype = Element.prototype;
var node_prototype = Node.prototype;

@ -1,6 +1,6 @@
/** @import { Effect, TemplateNode } from '#client' */
import { hydrate_next, hydrate_node, hydrating, set_hydrate_node } from './hydration.js';
import { create_text, get_first_child } from './operations.js';
import { create_text, get_first_child, is_firefox } from './operations.js';
import { create_fragment_from_html } from './reconciler.js';
import { active_effect } from '../runtime.js';
import { TEMPLATE_FRAGMENT, TEMPLATE_USE_IMPORT_NODE } from '../../../constants.js';
@ -48,7 +48,7 @@ export function template(content, flags) {
}
var clone = /** @type {TemplateNode} */ (
use_import_node ? document.importNode(node, true) : node.cloneNode(true)
use_import_node || is_firefox ? document.importNode(node, true) : node.cloneNode(true)
);
if (is_fragment) {
@ -249,3 +249,23 @@ export function append(anchor, dom) {
anchor.before(/** @type {Node} */ (dom));
}
let uid = 1;
/**
* Create (or hydrate) an unique UID for the component instance.
*/
export function props_id() {
if (
hydrating &&
hydrate_node &&
hydrate_node.nodeType === 8 &&
hydrate_node.textContent?.startsWith('#s')
) {
const id = hydrate_node.textContent.substring(1);
hydrate_next();
return id;
}
return 'c' + uid++;
}

@ -1,4 +1,5 @@
export { FILENAME, HMR, NAMESPACE_SVG } from '../../constants.js';
export { push, pop } from './context.js';
export { assign, assign_and, assign_or, assign_nullish } from './dev/assign.js';
export { cleanup_styles } from './dev/css.js';
export { add_locations } from './dev/elements.js';
@ -9,6 +10,7 @@ export {
mark_module_start,
mark_module_end,
add_owner_effect,
add_owner_to_class,
skip_ownership_validation
} from './dev/ownership.js';
export { check_target, legacy_api } from './dev/legacy.js';
@ -33,7 +35,6 @@ export {
set_attributes,
set_custom_element_data,
set_xlink_attribute,
handle_lazy_img,
set_value,
set_checked,
set_selected,
@ -95,7 +96,8 @@ export {
mathml_template,
template,
template_with_script,
text
text,
props_id
} from './dom/template.js';
export { derived, derived_safe_equal } from './reactivity/deriveds.js';
export {
@ -115,6 +117,8 @@ export {
set,
simple_set,
state,
update,
update_pre,
get_options
} from './reactivity/sources.js';
export {
@ -145,17 +149,9 @@ export {
flush_sync,
tick,
untrack,
update,
update_pre,
exclude_from_object,
pop,
push,
deep_read,
deep_read_state,
getAllContexts,
getContext,
setContext,
hasContext
deep_read_state
} from './runtime.js';
export { validate_binding, validate_each_keys } from './validate.js';
export { raf } from './timing.js';

@ -1,7 +1,8 @@
/** @import { ProxyMetadata, ProxyStateObject, Source, ValueOptions } from '#client' */
/** @import { ProxyMetadata, Source, ValueOptions } from '#client' */
import { DEV } from 'esm-env';
import { UNINITIALIZED } from '../../constants.js';
import { tracing_mode_flag } from '../flags/index.js';
import { component_context } from './context.js';
import {
array_prototype,
get_descriptor,
@ -14,7 +15,7 @@ import { check_ownership, widen_ownership } from './dev/ownership.js';
import { get_stack } from './dev/tracing.js';
import * as e from './errors.js';
import { batch_onchange, set, source, state } from './reactivity/sources.js';
import { active_effect, component_context, get } from './runtime.js';
import { active_effect, get } from './runtime.js';
const array_methods = ['push', 'pop', 'shift', 'unshift', 'splice', 'reverse', 'sort'];

@ -1,24 +1,14 @@
/** @import { Derived, Effect } from '#client' */
import { DEV } from 'esm-env';
import {
CLEAN,
DERIVED,
DESTROYED,
DIRTY,
EFFECT_HAS_DERIVED,
MAYBE_DIRTY,
UNOWNED
} from '../constants.js';
import { CLEAN, DERIVED, DIRTY, EFFECT_HAS_DERIVED, MAYBE_DIRTY, UNOWNED } from '../constants.js';
import {
active_reaction,
active_effect,
remove_reactions,
set_signal_status,
skip_reaction,
update_reaction,
increment_write_version,
set_active_effect,
component_context
set_active_effect
} from '../runtime.js';
import { equals, safe_equals } from './equality.js';
import * as e from '../errors.js';
@ -26,6 +16,7 @@ import { destroy_effect } from './effects.js';
import { inspect_effects, set_inspect_effects } from './sources.js';
import { get_stack } from '../dev/tracing.js';
import { tracing_mode_flag } from '../../flags/index.js';
import { component_context } from '../context.js';
/**
* @template V
@ -35,8 +26,12 @@ import { tracing_mode_flag } from '../../flags/index.js';
/*#__NO_SIDE_EFFECTS__*/
export function derived(fn) {
var flags = DERIVED | DIRTY;
var parent_derived =
active_reaction !== null && (active_reaction.f & DERIVED) !== 0
? /** @type {Derived} */ (active_reaction)
: null;
if (active_effect === null) {
if (active_effect === null || (parent_derived !== null && (parent_derived.f & UNOWNED) !== 0)) {
flags |= UNOWNED;
} else {
// Since deriveds are evaluated lazily, any effects created inside them are
@ -44,16 +39,11 @@ export function derived(fn) {
active_effect.f |= EFFECT_HAS_DERIVED;
}
var parent_derived =
active_reaction !== null && (active_reaction.f & DERIVED) !== 0
? /** @type {Derived} */ (active_reaction)
: null;
/** @type {Derived<V>} */
const signal = {
children: null,
ctx: component_context,
deps: null,
effects: null,
equals,
f: flags,
fn,
@ -68,10 +58,6 @@ export function derived(fn) {
signal.created = get_stack('CreatedAt');
}
if (parent_derived !== null) {
(parent_derived.children ??= []).push(signal);
}
return signal;
}
@ -91,19 +77,14 @@ export function derived_safe_equal(fn) {
* @param {Derived} derived
* @returns {void}
*/
function destroy_derived_children(derived) {
var children = derived.children;
if (children !== null) {
derived.children = null;
for (var i = 0; i < children.length; i += 1) {
var child = children[i];
if ((child.f & DERIVED) !== 0) {
destroy_derived(/** @type {Derived} */ (child));
} else {
destroy_effect(/** @type {Effect} */ (child));
}
export function destroy_derived_effects(derived) {
var effects = derived.effects;
if (effects !== null) {
derived.effects = null;
for (var i = 0; i < effects.length; i += 1) {
destroy_effect(/** @type {Effect} */ (effects[i]));
}
}
}
@ -151,7 +132,7 @@ export function execute_derived(derived) {
stack.push(derived);
destroy_derived_children(derived);
destroy_derived_effects(derived);
value = update_reaction(derived);
} finally {
set_active_effect(prev_active_effect);
@ -160,7 +141,7 @@ export function execute_derived(derived) {
}
} else {
try {
destroy_derived_children(derived);
destroy_derived_effects(derived);
value = update_reaction(derived);
} finally {
set_active_effect(prev_active_effect);
@ -186,15 +167,3 @@ export function update_derived(derived) {
derived.wv = increment_write_version();
}
}
/**
* @param {Derived} derived
* @returns {void}
*/
export function destroy_derived(derived) {
destroy_derived_children(derived);
remove_reactions(derived, 0);
set_signal_status(derived, DESTROYED);
derived.v = derived.children = derived.deps = derived.ctx = derived.reactions = null;
}

@ -1,10 +1,8 @@
/** @import { ComponentContext, ComponentContextLegacy, Derived, Effect, TemplateNode, TransitionManager } from '#client' */
import {
check_dirtiness,
component_context,
active_effect,
active_reaction,
dev_current_component_function,
update_effect,
get,
is_destroying_effect,
@ -44,7 +42,8 @@ import * as e from '../errors.js';
import { DEV } from 'esm-env';
import { define_property } from '../../shared/utils.js';
import { get_next_sibling } from '../dom/operations.js';
import { derived, destroy_derived } from './deriveds.js';
import { derived } from './deriveds.js';
import { component_context, dev_current_component_function } from '../context.js';
/**
* @param {'$effect' | '$effect.pre' | '$inspect'} rune
@ -54,7 +53,7 @@ export function validate_effect(rune) {
e.effect_orphan(rune);
}
if (active_reaction !== null && (active_reaction.f & UNOWNED) !== 0) {
if (active_reaction !== null && (active_reaction.f & UNOWNED) !== 0 && active_effect === null) {
e.effect_in_unowned_derived();
}
@ -100,7 +99,6 @@ function create_effect(type, fn, sync, push = true) {
var effect = {
ctx: component_context,
deps: null,
deriveds: null,
nodes_start: null,
nodes_end: null,
f: type | DIRTY,
@ -154,7 +152,7 @@ function create_effect(type, fn, sync, push = true) {
// if we're in a derived, add the effect there too
if (active_reaction !== null && (active_reaction.f & DERIVED) !== 0) {
var derived = /** @type {Derived} */ (active_reaction);
(derived.children ??= []).push(effect);
(derived.effects ??= []).push(effect);
}
}
@ -166,13 +164,7 @@ function create_effect(type, fn, sync, push = true) {
* @returns {boolean}
*/
export function effect_tracking() {
if (active_reaction === null || untracking) {
return false;
}
// If it's skipped, that's because we're inside an unowned
// that is not being tracked by another reaction
return !skip_reaction;
return active_reaction !== null && !untracking;
}
/**
@ -396,22 +388,6 @@ export function execute_effect_teardown(effect) {
}
}
/**
* @param {Effect} signal
* @returns {void}
*/
export function destroy_effect_deriveds(signal) {
var deriveds = signal.deriveds;
if (deriveds !== null) {
signal.deriveds = null;
for (var i = 0; i < deriveds.length; i += 1) {
destroy_derived(deriveds[i]);
}
}
}
/**
* @param {Effect} signal
* @param {boolean} remove_dom
@ -469,7 +445,6 @@ export function destroy_effect(effect, remove_dom = true) {
}
destroy_effect_children(effect, remove_dom && !removed);
destroy_effect_deriveds(effect);
remove_reactions(effect, 0);
set_signal_status(effect, DESTROYED);

@ -8,7 +8,7 @@ import {
PROPS_IS_UPDATED
} from '../../../constants.js';
import { get_descriptor, is_function } from '../../shared/utils.js';
import { mutable_source, set, source } from './sources.js';
import { mutable_source, set, source, update } from './sources.js';
import { derived, derived_safe_equal } from './deriveds.js';
import {
active_effect,
@ -16,7 +16,8 @@ import {
captured_signals,
set_active_effect,
untrack,
update
active_reaction,
set_active_reaction
} from '../runtime.js';
import { safe_equals } from './equality.js';
import * as e from '../errors.js';
@ -248,26 +249,6 @@ export function spread_props(...props) {
return new Proxy({ props }, spread_props_handler);
}
/**
* @template T
* @param {() => T} fn
* @returns {T}
*/
function with_parent_branch(fn) {
var effect = active_effect;
var previous_effect = active_effect;
while (effect !== null && (effect.f & (BRANCH_EFFECT | ROOT_EFFECT)) === 0) {
effect = effect.parent;
}
try {
set_active_effect(effect);
return fn();
} finally {
set_active_effect(previous_effect);
}
}
/**
* This function is responsible for synchronizing a possibly bound prop with the inner component state.
* It is used whenever the compiler sees that the component writes to the prop, or when it has a default prop_value.
@ -342,8 +323,8 @@ export function prop(props, key, flags, fallback) {
} else {
// Svelte 4 did not trigger updates when a primitive value was updated to the same value.
// Replicate that behavior through using a derived
var derived_getter = with_parent_branch(() =>
(immutable ? derived : derived_safe_equal)(() => /** @type {V} */ (props[key]))
var derived_getter = (immutable ? derived : derived_safe_equal)(
() => /** @type {V} */ (props[key])
);
derived_getter.f |= LEGACY_DERIVED_PROP;
getter = () => {
@ -387,21 +368,19 @@ export function prop(props, key, flags, fallback) {
// The derived returns the current value. The underlying mutable
// source is written to from various places to persist this value.
var inner_current_value = mutable_source(prop_value);
var current_value = with_parent_branch(() =>
derived(() => {
var parent_value = getter();
var child_value = get(inner_current_value);
if (from_child) {
from_child = false;
was_from_child = true;
return child_value;
}
var current_value = derived(() => {
var parent_value = getter();
var child_value = get(inner_current_value);
if (from_child) {
from_child = false;
was_from_child = true;
return child_value;
}
was_from_child = false;
return (inner_current_value.v = parent_value);
})
);
was_from_child = false;
return (inner_current_value.v = parent_value);
});
if (!immutable) current_value.equals = safe_equals;

@ -1,12 +1,10 @@
/** @import { Derived, Effect, Reaction, Source, Value, ValueOptions } from '#client' */
import { DEV } from 'esm-env';
import {
component_context,
active_reaction,
active_effect,
untracked_writes,
get,
is_runes,
schedule_effect,
set_untracked_writes,
set_signal_status,
@ -36,6 +34,7 @@ import * as e from '../errors.js';
import { legacy_mode_flag, tracing_mode_flag } from '../../flags/index.js';
import { get_stack } from '../dev/tracing.js';
import { proxy } from '../proxy.js';
import { component_context, is_runes } from '../context.js';
export let inspect_effects = new Set();
@ -154,7 +153,7 @@ export function mutable_state(v, immutable = false) {
*/
/*#__NO_SIDE_EFFECTS__*/
function push_derived_source(source) {
if (active_reaction !== null && (active_reaction.f & DERIVED) !== 0) {
if (active_reaction !== null && !untracking && (active_reaction.f & DERIVED) !== 0) {
if (derived_sources === null) {
set_derived_sources([source]);
} else {
@ -303,6 +302,35 @@ export function internal_set(source, value) {
return value;
}
/**
* @template {number | bigint} T
* @param {Source<T>} source
* @param {1 | -1} [d]
* @returns {T}
*/
export function update(source, d = 1) {
var value = get(source);
var result = d === 1 ? value++ : value--;
set(source, value);
// @ts-expect-error
return result;
}
/**
* @template {number | bigint} T
* @param {Source<T>} source
* @param {1 | -1} [d]
* @returns {T}
*/
export function update_pre(source, d = 1) {
var value = get(source);
// @ts-expect-error
return set(source, d === 1 ? ++value : --value);
}
/**
* @param {Value} signal
* @param {number} status should be DIRTY or MAYBE_DIRTY

@ -42,8 +42,8 @@ export interface Reaction extends Signal {
export interface Derived<V = unknown> extends Value<V>, Reaction {
/** The derived function */
fn: () => V;
/** Reactions created inside this signal */
children: null | Reaction[];
/** Effects created inside this signal */
effects: null | Effect[];
/** Parent effect or derived */
parent: Effect | Derived | null;
}
@ -57,8 +57,6 @@ export interface Effect extends Reaction {
*/
nodes_start: null | TemplateNode;
nodes_end: null | TemplateNode;
/** Reactions created inside this signal */
deriveds: null | Derived[];
/** The effect function */
fn: null | (() => void | (() => void));
/** The teardown function returned from the effect function */

@ -9,7 +9,8 @@ import {
init_operations
} from './dom/operations.js';
import { HYDRATION_END, HYDRATION_ERROR, HYDRATION_START } from '../../constants.js';
import { push, pop, component_context, active_effect } from './runtime.js';
import { active_effect } from './runtime.js';
import { push, pop, component_context } from './context.js';
import { component_root, branch } from './reactivity/effects.js';
import {
hydrate_next,
@ -54,7 +55,7 @@ export function set_text(text, value) {
if (str !== (text.__t ??= text.nodeValue)) {
// @ts-expect-error
text.__t = str;
text.nodeValue = str == null ? '' : str + '';
text.nodeValue = str + '';
}
}

@ -4,8 +4,6 @@ import { define_property, get_descriptors, get_prototype_of, index_of } from '..
import {
destroy_block_effect_children,
destroy_effect_children,
destroy_effect_deriveds,
effect,
execute_effect_teardown,
unlink_effect
} from './reactivity/effects.js';
@ -28,14 +26,20 @@ import {
BOUNDARY_EFFECT
} from './constants.js';
import { flush_tasks } from './dom/task.js';
import { add_owner } from './dev/ownership.js';
import { internal_set, set, source } from './reactivity/sources.js';
import { destroy_derived, execute_derived, update_derived } from './reactivity/deriveds.js';
import { internal_set } from './reactivity/sources.js';
import { destroy_derived_effects, update_derived } from './reactivity/deriveds.js';
import * as e from './errors.js';
import { lifecycle_outside_component } from '../shared/errors.js';
import { FILENAME } from '../../constants.js';
import { legacy_mode_flag, tracing_mode_flag } from '../flags/index.js';
import { tracing_mode_flag } from '../flags/index.js';
import { tracing_expressions, get_stack } from './dev/tracing.js';
import {
component_context,
dev_current_component_function,
is_runes,
set_component_context,
set_dev_current_component_function
} from './context.js';
import { is_firefox } from './dom/operations.js';
const FLUSH_MICROTASK = 0;
const FLUSH_SYNC = 1;
@ -150,41 +154,10 @@ export function set_captured_signals(value) {
captured_signals = value;
}
// Handling runtime component context
/** @type {ComponentContext | null} */
export let component_context = null;
/** @param {ComponentContext | null} context */
export function set_component_context(context) {
component_context = context;
}
/**
* The current component function. Different from current component context:
* ```html
* <!-- App.svelte -->
* <Foo>
* <Bar /> <!-- context == Foo.svelte, function == App.svelte -->
* </Foo>
* ```
* @type {ComponentContext['function']}
*/
export let dev_current_component_function = null;
/** @param {ComponentContext['function']} fn */
export function set_dev_current_component_function(fn) {
dev_current_component_function = fn;
}
export function increment_write_version() {
return ++write_version;
}
/** @returns {boolean} */
export function is_runes() {
return !legacy_mode_flag || (component_context !== null && component_context.l === null);
}
/**
* Determines whether a derived or effect is dirty.
* If it is MAYBE_DIRTY, will set the status to CLEAN
@ -212,18 +185,28 @@ export function check_dirtiness(reaction) {
// If we are working with a disconnected or an unowned signal that is now connected (due to an active effect)
// then we need to re-connect the reaction to the dependency
if (is_disconnected || is_unowned_connected) {
var derived = /** @type {Derived} */ (reaction);
var parent = derived.parent;
for (i = 0; i < length; i++) {
dependency = dependencies[i];
// We always re-add all reactions (even duplicates) if the derived was
// previously disconnected
if (is_disconnected || !dependency?.reactions?.includes(reaction)) {
(dependency.reactions ??= []).push(reaction);
// previously disconnected, however we don't if it was unowned as we
// de-duplicate dependencies in that case
if (is_disconnected || !dependency?.reactions?.includes(derived)) {
(dependency.reactions ??= []).push(derived);
}
}
if (is_disconnected) {
reaction.f ^= DISCONNECTED;
derived.f ^= DISCONNECTED;
}
// If the unowned derived is now fully connected to the graph again (it's unowned and reconnected, has a parent
// and the parent is not unowned), then we can mark it as connected again, removing the need for the unowned
// flag
if (is_unowned_connected && parent !== null && (parent.f & UNOWNED) === 0) {
derived.f ^= UNOWNED;
}
}
@ -351,7 +334,7 @@ export function handle_error(error, effect, previous_effect, component_context)
current_context = current_context.p;
}
const indent = /Firefox/.test(navigator.userAgent) ? ' ' : '\t';
const indent = is_firefox ? ' ' : '\t';
define_property(error, 'message', {
value: error.message + `\n${component_stack.map((name) => `\n${indent}in ${name}`).join('')}\n`
});
@ -432,9 +415,12 @@ export function update_reaction(reaction) {
skipped_deps = 0;
untracked_writes = null;
active_reaction = (flags & (BRANCH_EFFECT | ROOT_EFFECT)) === 0 ? reaction : null;
skip_reaction = !is_flushing_effect && (flags & UNOWNED) !== 0;
skip_reaction =
(flags & UNOWNED) !== 0 &&
(!is_flushing_effect || previous_reaction === null || previous_untracking);
derived_sources = null;
component_context = reaction.ctx;
set_component_context(reaction.ctx);
untracking = false;
read_version++;
@ -498,7 +484,7 @@ export function update_reaction(reaction) {
active_reaction = previous_reaction;
skip_reaction = previous_skip_reaction;
derived_sources = prev_derived_sources;
component_context = previous_component_context;
set_component_context(previous_component_context);
untracking = previous_untracking;
}
}
@ -540,6 +526,8 @@ function remove_reaction(signal, dependency) {
if ((dependency.f & (UNOWNED | DISCONNECTED)) === 0) {
dependency.f ^= DISCONNECTED;
}
// Disconnect any reactions owned by this reaction
destroy_derived_effects(/** @type {Derived} **/ (dependency));
remove_reactions(/** @type {Derived} **/ (dependency), 0);
}
}
@ -578,7 +566,7 @@ export function update_effect(effect) {
if (DEV) {
var previous_component_fn = dev_current_component_function;
dev_current_component_function = effect.component_function;
set_dev_current_component_function(effect.component_function);
}
try {
@ -587,7 +575,6 @@ export function update_effect(effect) {
} else {
destroy_effect_children(effect);
}
destroy_effect_deriveds(effect);
execute_effect_teardown(effect);
var teardown = update_reaction(effect);
@ -620,7 +607,7 @@ export function update_effect(effect) {
active_effect = previous_effect;
if (DEV) {
dev_current_component_function = previous_component_fn;
set_dev_current_component_function(previous_component_fn);
}
}
}
@ -693,10 +680,7 @@ function flush_queued_root_effects(root_effects) {
effect.f ^= CLEAN;
}
/** @type {Effect[]} */
var collected_effects = [];
process_effects(effect, collected_effects);
var collected_effects = process_effects(effect);
flush_queued_effects(collected_effects);
}
} finally {
@ -797,13 +781,14 @@ export function schedule_effect(signal) {
* effects to be flushed.
*
* @param {Effect} effect
* @param {Effect[]} collected_effects
* @returns {void}
* @returns {Effect[]}
*/
function process_effects(effect, collected_effects) {
var current_effect = effect.first;
function process_effects(effect) {
/** @type {Effect[]} */
var effects = [];
var current_effect = effect.first;
main_loop: while (current_effect !== null) {
var flags = current_effect.f;
var is_branch = (flags & BRANCH_EFFECT) !== 0;
@ -811,27 +796,32 @@ function process_effects(effect, collected_effects) {
var sibling = current_effect.next;
if (!is_skippable_branch && (flags & INERT) === 0) {
if ((flags & RENDER_EFFECT) !== 0) {
if (is_branch) {
current_effect.f ^= CLEAN;
} else {
try {
if (check_dirtiness(current_effect)) {
update_effect(current_effect);
}
} catch (error) {
handle_error(error, current_effect, null, current_effect.ctx);
if ((flags & EFFECT) !== 0) {
effects.push(current_effect);
} else if (is_branch) {
current_effect.f ^= CLEAN;
} else {
// Ensure we set the effect to be the active reaction
// to ensure that unowned deriveds are correctly tracked
// because we're flushing the current effect
var previous_active_reaction = active_reaction;
try {
active_reaction = current_effect;
if (check_dirtiness(current_effect)) {
update_effect(current_effect);
}
} catch (error) {
handle_error(error, current_effect, null, current_effect.ctx);
} finally {
active_reaction = previous_active_reaction;
}
}
var child = current_effect.first;
var child = current_effect.first;
if (child !== null) {
current_effect = child;
continue;
}
} else if ((flags & EFFECT) !== 0) {
effects.push(current_effect);
if (child !== null) {
current_effect = child;
continue;
}
}
@ -854,13 +844,7 @@ function process_effects(effect, collected_effects) {
current_effect = sibling;
}
// We might be dealing with many effects here, far more than can be spread into
// an array push call (callstack overflow). So let's deal with each effect in a loop.
for (var i = 0; i < effects.length; i++) {
child = effects[i];
collected_effects.push(child);
process_effects(child, collected_effects);
}
return effects;
}
/**
@ -925,15 +909,6 @@ export function get(signal) {
var flags = signal.f;
var is_derived = (flags & DERIVED) !== 0;
// If the derived is destroyed, just execute it again without retaining
// its memoisation properties as the derived is stale
if (is_derived && (flags & DESTROYED) !== 0) {
var value = execute_derived(/** @type {Derived} */ (signal));
// Ensure the derived remains destroyed
destroy_derived(/** @type {Derived} */ (signal));
return value;
}
if (captured_signals !== null) {
captured_signals.add(signal);
}
@ -953,31 +928,26 @@ export function get(signal) {
skipped_deps++;
} else if (new_deps === null) {
new_deps = [signal];
} else {
} else if (!skip_reaction || !new_deps.includes(signal)) {
// Normally we can push duplicated dependencies to `new_deps`, but if we're inside
// an unowned derived because skip_reaction is true, then we need to ensure that
// we don't have duplicates
new_deps.push(signal);
}
}
} else if (is_derived && /** @type {Derived} */ (signal).deps === null) {
} else if (
is_derived &&
/** @type {Derived} */ (signal).deps === null &&
/** @type {Derived} */ (signal).effects === null
) {
var derived = /** @type {Derived} */ (signal);
var parent = derived.parent;
var target = derived;
while (parent !== null) {
// Attach the derived to the nearest parent effect, if there are deriveds
// in between then we also need to attach them too
if ((parent.f & DERIVED) !== 0) {
var parent_derived = /** @type {Derived} */ (parent);
target = parent_derived;
parent = parent_derived.parent;
} else {
var parent_effect = /** @type {Effect} */ (parent);
if (!parent_effect.deriveds?.includes(target)) {
(parent_effect.deriveds ??= []).push(target);
}
break;
}
if (parent !== null && (parent.f & UNOWNED) === 0) {
// If the derived is owned by another derived then mark it as unowned
// as the derived value might have been referenced in a different context
// since and thus its parent might not be its true owner anymore
derived.f ^= UNOWNED;
}
}
@ -1110,138 +1080,6 @@ export function set_signal_status(signal, status) {
signal.f = (signal.f & STATUS_MASK) | status;
}
/**
* Retrieves the context that belongs to the closest parent component with the specified `key`.
* Must be called during component initialisation.
*
* @template T
* @param {any} key
* @returns {T}
*/
export function getContext(key) {
const context_map = get_or_init_context_map('getContext');
const result = /** @type {T} */ (context_map.get(key));
if (DEV) {
const fn = /** @type {ComponentContext} */ (component_context).function;
if (fn) {
add_owner(result, fn, true);
}
}
return result;
}
/**
* Associates an arbitrary `context` object with the current component and the specified `key`
* and returns that object. The context is then available to children of the component
* (including slotted content) with `getContext`.
*
* Like lifecycle functions, this must be called during component initialisation.
*
* @template T
* @param {any} key
* @param {T} context
* @returns {T}
*/
export function setContext(key, context) {
const context_map = get_or_init_context_map('setContext');
context_map.set(key, context);
return context;
}
/**
* Checks whether a given `key` has been set in the context of a parent component.
* Must be called during component initialisation.
*
* @param {any} key
* @returns {boolean}
*/
export function hasContext(key) {
const context_map = get_or_init_context_map('hasContext');
return context_map.has(key);
}
/**
* Retrieves the whole context map that belongs to the closest parent component.
* Must be called during component initialisation. Useful, for example, if you
* programmatically create a component and want to pass the existing context to it.
*
* @template {Map<any, any>} [T=Map<any, any>]
* @returns {T}
*/
export function getAllContexts() {
const context_map = get_or_init_context_map('getAllContexts');
if (DEV) {
const fn = component_context?.function;
if (fn) {
for (const value of context_map.values()) {
add_owner(value, fn, true);
}
}
}
return /** @type {T} */ (context_map);
}
/**
* @param {string} name
* @returns {Map<unknown, unknown>}
*/
function get_or_init_context_map(name) {
if (component_context === null) {
lifecycle_outside_component(name);
}
return (component_context.c ??= new Map(get_parent_context(component_context) || undefined));
}
/**
* @param {ComponentContext} component_context
* @returns {Map<unknown, unknown> | null}
*/
function get_parent_context(component_context) {
let parent = component_context.p;
while (parent !== null) {
const context_map = parent.c;
if (context_map !== null) {
return context_map;
}
parent = parent.p;
}
return null;
}
/**
* @template {number | bigint} T
* @param {Value<T>} signal
* @param {1 | -1} [d]
* @returns {T}
*/
export function update(signal, d = 1) {
var value = get(signal);
var result = d === 1 ? value++ : value--;
set(signal, value);
// @ts-expect-error
return result;
}
/**
* @template {number | bigint} T
* @param {Value<T>} signal
* @param {1 | -1} [d]
* @returns {T}
*/
export function update_pre(signal, d = 1) {
var value = get(signal);
// @ts-expect-error
return set(signal, d === 1 ? ++value : --value);
}
/**
* @param {Record<string, unknown>} obj
* @param {string[]} keys
@ -1260,78 +1098,6 @@ export function exclude_from_object(obj, keys) {
return result;
}
/**
* @param {Record<string, unknown>} props
* @param {any} runes
* @param {Function} [fn]
* @returns {void}
*/
export function push(props, runes = false, fn) {
component_context = {
p: component_context,
c: null,
e: null,
m: false,
s: props,
x: null,
l: null
};
if (legacy_mode_flag && !runes) {
component_context.l = {
s: null,
u: null,
r1: [],
r2: source(false)
};
}
if (DEV) {
// component function
component_context.function = fn;
dev_current_component_function = fn;
}
}
/**
* @template {Record<string, any>} T
* @param {T} [component]
* @returns {T}
*/
export function pop(component) {
const context_stack_item = component_context;
if (context_stack_item !== null) {
if (component !== undefined) {
context_stack_item.x = component;
}
const component_effects = context_stack_item.e;
if (component_effects !== null) {
var previous_effect = active_effect;
var previous_reaction = active_reaction;
context_stack_item.e = null;
try {
for (var i = 0; i < component_effects.length; i++) {
var component_effect = component_effects[i];
set_active_effect(component_effect.effect);
set_active_reaction(component_effect.reaction);
effect(component_effect.fn);
}
} finally {
set_active_effect(previous_effect);
set_active_reaction(previous_reaction);
}
}
component_context = context_stack_item.p;
if (DEV) {
dev_current_component_function = context_stack_item.p?.function ?? null;
}
context_stack_item.m = true;
}
// Micro-optimization: Don't set .a above to the empty object
// so it can be garbage-collected when the return here is unused
return component || /** @type {T} */ ({});
}
/**
* Possibly traverse an object and read all its properties so that they're all reactive in case this is `$state`.
* Does only check first level of an object for performance reasons (heuristic should be good for 99% of all cases).
@ -1405,37 +1171,3 @@ export function deep_read(value, visited = new Set()) {
}
}
}
if (DEV) {
/**
* @param {string} rune
*/
function throw_rune_error(rune) {
if (!(rune in globalThis)) {
// TODO if people start adjusting the "this can contain runes" config through v-p-s more, adjust this message
/** @type {any} */
let value; // let's hope noone modifies this global, but belts and braces
Object.defineProperty(globalThis, rune, {
configurable: true,
// eslint-disable-next-line getter-return
get: () => {
if (value !== undefined) {
return value;
}
e.rune_outside_svelte(rune);
},
set: (v) => {
value = v;
}
});
}
}
throw_rune_error('$state');
throw_rune_error('$effect');
throw_rune_error('$derived');
throw_rune_error('$inspect');
throw_rune_error('$props');
throw_rune_error('$bindable');
}

@ -1,4 +1,4 @@
import { dev_current_component_function } from './runtime.js';
import { dev_current_component_function } from './context.js';
import { is_array } from '../shared/utils.js';
import * as e from './errors.js';
import { FILENAME } from '../../constants.js';

@ -28,14 +28,15 @@ const INVALID_ATTR_NAME_CHAR_REGEX =
* @param {Payload} to_copy
* @returns {Payload}
*/
export function copy_payload({ out, css, head }) {
export function copy_payload({ out, css, head, uid }) {
return {
out,
css: new Set(css),
head: {
title: head.title,
out: head.out
}
},
uid
};
}
@ -48,6 +49,7 @@ export function copy_payload({ out, css, head }) {
export function assign_payload(p1, p2) {
p1.out = p2.out;
p1.head = p2.head;
p1.uid = p2.uid;
}
/**
@ -83,17 +85,27 @@ export function element(payload, tag, attributes_fn = noop, children_fn = noop)
*/
export let on_destroy = [];
function props_id_generator() {
let uid = 1;
return () => 's' + uid++;
}
/**
* Only available on the server and when compiling with the `server` option.
* Takes a component and returns an object with `body` and `head` properties on it, which you can use to populate the HTML when server-rendering your app.
* @template {Record<string, any>} Props
* @param {import('svelte').Component<Props> | ComponentType<SvelteComponent<Props>>} component
* @param {{ props?: Omit<Props, '$$slots' | '$$events'>; context?: Map<any, any> }} [options]
* @param {{ props?: Omit<Props, '$$slots' | '$$events'>; context?: Map<any, any>, uid?: () => string }} [options]
* @returns {RenderOutput}
*/
export function render(component, options = {}) {
/** @type {Payload} */
const payload = { out: '', css: new Set(), head: { title: '', out: '' } };
const payload = {
out: '',
css: new Set(),
head: { title: '', out: '' },
uid: options.uid ?? props_id_generator()
};
const prev_on_destroy = on_destroy;
on_destroy = [];
@ -526,6 +538,17 @@ export function once(get_value) {
};
}
/**
* Create an unique ID
* @param {Payload} payload
* @returns {string}
*/
export function props_id(payload) {
const uid = payload.uid();
payload.out += '<!--#' + uid + '-->';
return uid;
}
export { attr, clsx };
export { html } from './blocks/html.js';

@ -18,6 +18,8 @@ export interface Payload {
title: string;
out: string;
};
/** Function that generates a unique ID */
uid: () => string;
}
export interface RenderOutput {

@ -3,19 +3,13 @@ import { DIRTY, LEGACY_PROPS, MAYBE_DIRTY } from '../internal/client/constants.j
import { user_pre_effect } from '../internal/client/reactivity/effects.js';
import { mutable_source, set } from '../internal/client/reactivity/sources.js';
import { hydrate, mount, unmount } from '../internal/client/render.js';
import {
active_effect,
component_context,
dev_current_component_function,
flush_sync,
get,
set_signal_status
} from '../internal/client/runtime.js';
import { active_effect, flush_sync, get, set_signal_status } from '../internal/client/runtime.js';
import { lifecycle_outside_component } from '../internal/shared/errors.js';
import { define_property, is_array } from '../internal/shared/utils.js';
import * as w from '../internal/client/warnings.js';
import { DEV } from 'esm-env';
import { FILENAME } from '../constants.js';
import { component_context, dev_current_component_function } from '../internal/client/context.js';
/**
* Takes the same options as a Svelte 4 component and the component function and returns a Svelte 4 compatible component.

@ -170,7 +170,10 @@ const DOM_BOOLEAN_ATTRIBUTES = [
'reversed',
'seamless',
'selected',
'webkitdirectory'
'webkitdirectory',
'defer',
'disablepictureinpicture',
'disableremoteplayback'
];
/**
@ -196,7 +199,11 @@ const ATTRIBUTE_ALIASES = {
readonly: 'readOnly',
defaultvalue: 'defaultValue',
defaultchecked: 'defaultChecked',
srcobject: 'srcObject'
srcobject: 'srcObject',
novalidate: 'noValidate',
allowfullscreen: 'allowFullscreen',
disablepictureinpicture: 'disablePictureInPicture',
disableremoteplayback: 'disableRemotePlayback'
};
/**
@ -218,7 +225,11 @@ const DOM_PROPERTIES = [
'volume',
'defaultValue',
'defaultChecked',
'srcObject'
'srcObject',
'noValidate',
'allowFullscreen',
'disablePictureInPicture',
'disableRemotePlayback'
];
/**
@ -422,6 +433,7 @@ const RUNES = /** @type {const} */ ([
'$state.raw',
'$state.snapshot',
'$props',
'$props.id',
'$bindable',
'$derived',
'$derived.by',

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

@ -5,15 +5,15 @@ export default test({
{
code: 'css_unused_selector',
end: {
character: 496,
character: 627,
column: 10,
line: 26
line: 32
},
message: 'Unused CSS selector ".x + .bar"',
start: {
character: 487,
character: 618,
column: 1,
line: 26
line: 32
}
}
]

@ -9,5 +9,8 @@
.x.svelte-xyz ~ .foo:where(.svelte-xyz) span:where(.svelte-xyz) { color: green; }
.x.svelte-xyz ~ .bar:where(.svelte-xyz) { color: green; }
.z.svelte-xyz + .z:where(.svelte-xyz) { color: green; }
.z.svelte-xyz ~ .z:where(.svelte-xyz) { color: green; }
/* no match */
/* (unused) .x + .bar { color: green; }*/

@ -10,6 +10,9 @@
</p>
<p class="bar">bar</p>
</div>
{#each [1]}
<svelte:element class="z" this={tag}></svelte:element>
{/each}
<style>
.before + .foo { color: green; }
@ -22,6 +25,9 @@
.x ~ .foo span { color: green; }
.x ~ .bar { color: green; }
.z + .z { color: green; }
.z ~ .z { color: green; }
/* no match */
.x + .bar { color: green; }
</style>

@ -6,182 +6,210 @@ export default test({
code: 'css_unused_selector',
message: 'Unused CSS selector ".unused:has(y)"',
start: {
line: 31,
line: 33,
column: 1,
character: 308
character: 330
},
end: {
line: 31,
line: 33,
column: 15,
character: 322
character: 344
}
},
{
code: 'css_unused_selector',
message: 'Unused CSS selector ".unused:has(:global(y))"',
start: {
line: 34,
line: 36,
column: 1,
character: 343
character: 365
},
end: {
line: 34,
line: 36,
column: 24,
character: 366
character: 388
}
},
{
code: 'css_unused_selector',
message: 'Unused CSS selector "x:has(.unused)"',
start: {
line: 37,
line: 39,
column: 1,
character: 387
character: 409
},
end: {
line: 37,
line: 39,
column: 15,
character: 401
character: 423
}
},
{
code: 'css_unused_selector',
message: 'Unused CSS selector ":global(.foo):has(.unused)"',
start: {
line: 40,
line: 42,
column: 1,
character: 422
character: 444
},
end: {
line: 40,
line: 42,
column: 27,
character: 448
character: 470
}
},
{
code: 'css_unused_selector',
message: 'Unused CSS selector "x:has(y):has(.unused)"',
start: {
line: 50,
line: 52,
column: 1,
character: 556
character: 578
},
end: {
line: 50,
line: 52,
column: 22,
character: 577
character: 599
}
},
{
code: 'css_unused_selector',
message: 'Unused CSS selector ".unused"',
start: {
line: 69,
line: 71,
column: 2,
character: 782
character: 804
},
end: {
line: 69,
line: 71,
column: 9,
character: 789
character: 811
}
},
{
code: 'css_unused_selector',
message: 'Unused CSS selector ".unused x:has(y)"',
start: {
line: 85,
line: 87,
column: 1,
character: 936
character: 958
},
end: {
line: 85,
line: 87,
column: 17,
character: 952
character: 974
}
},
{
code: 'css_unused_selector',
message: 'Unused CSS selector ".unused:has(.unused)"',
start: {
line: 88,
line: 90,
column: 1,
character: 973
character: 995
},
end: {
line: 88,
line: 90,
column: 21,
character: 993
character: 1015
}
},
{
code: 'css_unused_selector',
message: 'Unused CSS selector "x:has(> z)"',
start: {
line: 98,
line: 100,
column: 1,
character: 1093
character: 1115
},
end: {
line: 98,
line: 100,
column: 11,
character: 1103
character: 1125
}
},
{
code: 'css_unused_selector',
message: 'Unused CSS selector "x:has(> d)"',
start: {
line: 101,
line: 103,
column: 1,
character: 1124
character: 1146
},
end: {
line: 101,
line: 103,
column: 11,
character: 1134
character: 1156
}
},
{
code: 'css_unused_selector',
message: 'Unused CSS selector "x:has(~ y)"',
start: {
line: 121,
line: 123,
column: 1,
character: 1326
character: 1348
},
end: {
line: 121,
line: 123,
column: 11,
character: 1336
character: 1358
}
},
{
code: 'css_unused_selector',
message: 'Unused CSS selector "f:has(~ d)"',
start: {
line: 133,
column: 1,
character: 1446
},
end: {
line: 133,
column: 11,
character: 1456
}
},
{
code: 'css_unused_selector',
message: 'Unused CSS selector ":has(.unused)"',
start: {
line: 129,
line: 141,
column: 2,
character: 1409
character: 1529
},
end: {
line: 129,
line: 141,
column: 15,
character: 1422
character: 1542
}
},
{
code: 'css_unused_selector',
message: 'Unused CSS selector "&:has(.unused)"',
start: {
line: 135,
line: 147,
column: 2,
character: 1480
character: 1600
},
end: {
line: 135,
line: 147,
column: 16,
character: 1494
character: 1614
}
},
{
code: 'css_unused_selector',
message: 'Unused CSS selector ":global(.foo):has(.unused)"',
start: {
line: 155,
column: 1,
character: 1684
},
end: {
line: 155,
column: 27,
character: 1710
}
}
]

@ -112,6 +112,16 @@
color: red;
}*/
d.svelte-xyz:has(+ e:where(.svelte-xyz)) {
color: green;
}
d.svelte-xyz:has(~ f:where(.svelte-xyz)) {
color: green;
}
/* (unused) f:has(~ d) {
color: red;
}*/
.foo {
.svelte-xyz:has(x:where(.svelte-xyz)) {
color: green;
@ -126,3 +136,10 @@
color: red;
}*/
}
.foo:has(x.svelte-xyz) {
color: green;
}
/* (unused) :global(.foo):has(.unused) {
color: red;
}*/

@ -3,6 +3,8 @@
<z></z>
{#if foo}
<d></d>
<e></e>
<f></f>
{/if}
</y>
</x>
@ -122,6 +124,16 @@
color: red;
}
d:has(+ e) {
color: green;
}
d:has(~ f) {
color: green;
}
f:has(~ d) {
color: red;
}
:global(.foo) {
:has(x) {
color: green;
@ -136,4 +148,11 @@
color: red;
}
}
:global(.foo):has(x) {
color: green;
}
:global(.foo):has(.unused) {
color: red;
}
</style>

@ -1 +1 @@
<!--[--><a href="/foo">foo</a><!--]-->
<!--[--><a href="/foo">foo</a> <a href="/foo">foo</a><!--]-->

@ -3,3 +3,4 @@
</script>
<a href={browser ? '/foo' : '/bar'}>foo</a>
<a {...{href: browser ? '/foo' : '/bar'}}>foo</a>

@ -0,0 +1,60 @@
import { test } from '../../test';
export default test({
// JSDOM lacks support for some of these attributes, so we'll skip it for now.
//
// See:
// - `async`: https://github.com/jsdom/jsdom/issues/1564
// - `nomodule`: https://github.com/jsdom/jsdom/issues/2475
// - `autofocus`: https://github.com/jsdom/jsdom/issues/3041
// - `inert`: https://github.com/jsdom/jsdom/issues/3605
// - etc...: https://github.com/jestjs/jest/issues/139#issuecomment-592673550
skip_mode: ['client'],
html: `
<script nomodule async defer></script>
<form novalidate></form>
<input readonly required checked webkitdirectory>
<select multiple disabled></select>
<button formnovalidate></button>
<img ismap>
<video autoplay controls loop muted playsinline disablepictureinpicture disableremoteplayback></video>
<audio disableremoteplayback></audio>
<track default>
<iframe allowfullscreen></iframe>
<details open></details>
<ol reversed></ol>
<div autofocus></div>
<span inert></span>
<script nomodule async defer></script>
<form novalidate></form>
<input readonly required checked webkitdirectory>
<select multiple disabled></select>
<button formnovalidate></button>
<img ismap>
<video autoplay controls loop muted playsinline disablepictureinpicture disableremoteplayback></video>
<audio disableremoteplayback></audio>
<track default>
<iframe allowfullscreen></iframe>
<details open></details>
<ol reversed></ol>
<div autofocus></div>
<span inert></span>
<script></script>
<form></form>
<input>
<select></select>
<button></button>
<img>
<video></video>
<audio></audio>
<track>
<iframe></iframe>
<details></details>
<ol></ol>
<div></div>
<span></span>
`
});

@ -0,0 +1,22 @@
<script>
let runesMode = $state('using a rune so that we trigger runes mode');
const attributeValues = [true, 'test', false];
</script>
{#each attributeValues as val}
<script NOMODULE={val} ASYNC={val} DEFER={val}></script>
<form NOVALIDATE={val}></form>
<input READONLY={val} REQUIRED={val} CHECKED={val} WEBKITDIRECTORY={val} />
<select MULTIPLE={val} DISABLED={val}></select>
<button FORMNOVALIDATE={val}></button>
<img ISMAP={val} />
<video AUTOPLAY={val} CONTROLS={val} LOOP={val} MUTED={val} PLAYSINLINE={val} DISABLEPICTUREINPICTURE={val} DISABLEREMOTEPLAYBACK={val}></video>
<audio DISABLEREMOTEPLAYBACK={val}></audio>
<track DEFAULT={val} />
<iframe ALLOWFULLSCREEN={val}></iframe>
<details OPEN={val}></details>
<ol REVERSED={val}></ol>
<div AUTOFOCUS={val}></div>
<span INERT={val}></span>
{/each}

@ -0,0 +1,33 @@
import { flushSync } from 'svelte';
import { test } from '../../test';
export default test({
async test({ target, assert }) {
// Test for https://github.com/sveltejs/svelte/issues/15237
const [setValues, clearValue] = target.querySelectorAll('button');
const [text1, text2, check1, check2] = target.querySelectorAll('input');
assert.equal(text1.value, '');
assert.equal(text2.value, '');
assert.equal(check1.checked, false);
assert.equal(check2.checked, false);
flushSync(() => {
setValues.click();
});
assert.equal(text1.value, 'message');
assert.equal(text2.value, 'message');
assert.equal(check1.checked, true);
assert.equal(check2.checked, true);
flushSync(() => {
clearValue.click();
});
assert.equal(text1.value, '');
assert.equal(text2.value, '');
assert.equal(check1.checked, false);
assert.equal(check2.checked, false);
}
});

@ -0,0 +1,22 @@
<script>
let value = $state();
let checked = $state(false);
function setValues() {
value = 'message';
checked = true;
}
function clearValues() {
value = null;
checked = null;
}
</script>
<button onclick={setValues}>setValues</button>
<button onclick={clearValues}>clearValues</button>
<input type="text" {value} />
<input type="text" {value} {...{}} />
<input type="checkbox" {checked} />
<input type="checkbox" {checked} {...{}} />

@ -0,0 +1,24 @@
import { test } from '../../test';
export default test({
mode: ['client'],
async test({ assert, target }) {
const my_element = /** @type HTMLElement & { object: { test: true }; } */ (
target.querySelector('my-element')
);
assert.equal(my_element.getAttribute('string'), 'test');
assert.equal(my_element.hasAttribute('object'), false);
assert.deepEqual(my_element.object, { test: true });
const my_link = /** @type HTMLAnchorElement & { object: { test: true }; } */ (
target.querySelector('a')
);
assert.equal(my_link.getAttribute('string'), 'test');
assert.equal(my_link.hasAttribute('object'), false);
assert.deepEqual(my_link.object, { test: true });
const [value1, value2] = target.querySelectorAll('value-element');
assert.equal(value1.shadowRoot?.innerHTML, '<span>test</span>');
assert.equal(value2.shadowRoot?.innerHTML, '<span>test</span>');
}
});

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

Loading…
Cancel
Save