Merge branch 'main' into main

pull/18732/head
Utkarsh Yadav 2 weeks ago committed by GitHub
commit dd3cfda172
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -0,0 +1,5 @@
---
'svelte': patch
---
perf: reuse the cached value in the `<option>`/`<select>` value guard

@ -0,0 +1,5 @@
---
'svelte': patch
---
fix: apply ownership mutation ignores to binding assignments

@ -0,0 +1,5 @@
---
'svelte': patch
---
perf: fold SSR block-open markers into the branch's first push

@ -0,0 +1,5 @@
---
'svelte': patch
---
fix: report `derived_invalid_export` for `export let x = $derived(...)` in runes mode

@ -0,0 +1,5 @@
---
'svelte': patch
---
fix: keep `defaultChecked` on hydrated radio inputs with spread attributes

@ -0,0 +1,5 @@
---
'svelte': patch
---
fix: measure nested transitions before applying their starting styles

@ -0,0 +1,5 @@
---
'svelte': patch
---
perf: emit `$.only_child` for elements with a single child

@ -0,0 +1,5 @@
---
'svelte': patch
---
fix: omit `bind:focused` from SSR output (it has no HTML attribute)

@ -0,0 +1,5 @@
---
'svelte': patch
---
fix: more robust rendering of Svelte custom element slots

@ -0,0 +1,5 @@
---
'svelte': patch
---
fix: properly apply static textarea value attribute during CSR

@ -0,0 +1,5 @@
---
'svelte': patch
---
fix: keep the dependencies of a reaction that throws, so deriveds it read are neither leaked nor stuck in their error

@ -0,0 +1,5 @@
---
'svelte': patch
---
perf: use `$.comment()` for single-comment templates

@ -0,0 +1,5 @@
---
'svelte': patch
---
chore: move `@types/trusted-types` to devDependencies

@ -0,0 +1,5 @@
---
'svelte': minor
---
feat: add `has` function to `createContext`

@ -0,0 +1,5 @@
---
'svelte': patch
---
fix: transform derived assignments and select function bindings correctly during server-side rendering

@ -0,0 +1,5 @@
---
'svelte': patch
---
fix: keep boolean attributes with an empty string value when rendering attribute objects on the server

@ -0,0 +1,5 @@
---
'svelte': patch
---
fix: sync `SvelteURL` port signal when the protocol setter clears the port

@ -0,0 +1,5 @@
---
'svelte': minor
---
feat: support `defaultValue` on `<select>`

@ -0,0 +1,5 @@
---
'svelte': minor
---
feat: add getOrInsert/getOrInsertComputed to SvelteMap

@ -0,0 +1,5 @@
---
'svelte': patch
---
fix: block declaration tags and `{@const}` on async values read inside closures

@ -0,0 +1,5 @@
---
'svelte': patch
---
perf: treat `<img loading>` as a static element again

@ -0,0 +1,5 @@
---
'svelte': patch
---
fix: preserve line feed character references in attribute values

@ -0,0 +1,5 @@
---
'svelte': patch
---
fix: decode uppercase-`X` hex numeric character references (`&#X...;`)

@ -251,6 +251,19 @@ You can give the `<select>` a default value by adding a `selected` attribute to
</select>
```
Since 5.57.0, if a `<select>` has a `defaultValue` and is part of a form, it will revert to that value instead of the empty string when the form is reset. Note that for the initial render the value of the binding takes precedence unless it is `null` or `undefined`.
```svelte
<form>
<select bind:value defaultValue="b">
<option>a</option>
<option>b</option>
<option>c</option>
</select>
<input type="reset" value="Reset">
</form>
```
## `<audio>`
`<audio>` elements have their own set of bindings — five two-way ones...

@ -4,7 +4,7 @@ title: Context
Context allows components to access values owned by parent components without passing them down as props (potentially through many layers of intermediate components, known as 'prop-drilling').
By creating a `[get, set]` pair of functions with `createContext`, you can set the context in a parent component and get it in a child component:
By creating a `[get, set, has]` triplet of functions with `createContext`, you can set the context in a parent component and get it in a child component:
<!-- codeblock:start {"title":"Context","selected":"context.ts"} -->
```svelte
@ -165,9 +165,9 @@ Svelte will warn you if you get it wrong.
Similarly, to pass primitive values through context, use functions as described in [Passing state into functions]($state#Passing-state-into-functions).
## Component testing
## Mounting components with context
When writing [component tests](testing#Unit-and-component-tests-with-Vitest-Component-testing), it can be useful to create a wrapper component that sets the context in order to check the behaviour of a component that uses it. As of version 5.49, you can do this sort of thing:
To mount a component with specific context, create a wrapper component that sets the context before rendering the component. This is useful for [component tests](testing#Unit-and-component-tests-with-Vitest-Component-testing), or any other scenario that needs to provide context through `mount`. As of version 5.49, you can do this sort of thing:
```js
import { mount, unmount } from 'svelte';
@ -193,6 +193,8 @@ test('MyComponent', () => {
This approach also works with [`hydrate`](imperative-component-api#hydrate) and [`render`](imperative-component-api#render).
The context set by the wrapper only applies to that mounted component tree. Each call to `mount`, `hydrate` or `render` creates a separate wrapper instance, so the context does not leak into other mounted components.
## Replacing global state
When you have state shared by many different components, you might be tempted to put it in its own module and just import it wherever it's needed:

@ -78,7 +78,7 @@ Certain lifecycle methods can only be used during component initialisation. To f
Context was not set in the current component or any of its ancestors
```
The [`createContext()`](svelte#createContext) utility returns a `[get, set]` pair of functions. `get` will throw an error if `set` was not used to set the context in the current component or any of its ancestors.
The [`createContext()`](svelte#createContext) utility returns a `[get, set, has]` triplet of functions. `get` will throw an error if `set` was not used to set the context in the current component or any of its ancestors.
### snippet_without_render_tag

@ -37,7 +37,7 @@
"eslint-plugin-lube": "^0.5.1",
"eslint-plugin-svelte": "^3.15.0",
"jsdom": "25.0.1",
"playwright": "^1.60.0",
"playwright": "^1.62.0",
"prettier": "^3.2.4",
"prettier-plugin-svelte": "^3.4.0",
"svelte": "workspace:^",

@ -1352,6 +1352,9 @@ export interface HTMLSelectAttributes extends HTMLAttributes<HTMLSelectElement>
required?: boolean | undefined | null;
size?: number | undefined | null;
value?: any;
// needs both casing variants because language tools does lowercase names of non-shorthand attributes
defaultValue?: any;
defaultvalue?: any;
'on:change'?: ChangeEventHandler<HTMLSelectElement> | undefined | null;
onchange?: ChangeEventHandler<HTMLSelectElement> | undefined | null;

@ -64,7 +64,7 @@ Certain lifecycle methods can only be used during component initialisation. To f
> Context was not set in the current component or any of its ancestors
The [`createContext()`](svelte#createContext) utility returns a `[get, set]` pair of functions. `get` will throw an error if `set` was not used to set the context in the current component or any of its ancestors.
The [`createContext()`](svelte#createContext) utility returns a `[get, set, has]` triplet of functions. `get` will throw an error if `set` was not used to set the context in the current component or any of its ancestors.
## snippet_without_render_tag

@ -152,13 +152,14 @@
},
"devDependencies": {
"@jridgewell/trace-mapping": "^0.3.25",
"@playwright/test": "^1.60.0",
"@playwright/test": "^1.62.0",
"@rollup/plugin-commonjs": "^28.0.1",
"@rollup/plugin-node-resolve": "^15.3.0",
"@rollup/plugin-terser": "^0.4.4",
"@rollup/plugin-virtual": "^3.0.2",
"@types/aria-query": "^5.0.4",
"@types/node": "^20.11.5",
"@types/trusted-types": "^2.0.7",
"baseline-browser-mapping": "^2.10.32",
"dts-buddy": "^0.5.5",
"esbuild": "^0.28.1",
@ -174,7 +175,6 @@
"@jridgewell/sourcemap-codec": "^1.5.0",
"@sveltejs/acorn-typescript": "^1.0.10",
"@types/estree": "^1.0.5",
"@types/trusted-types": "^2.0.7",
"acorn": "^8.12.1",
"aria-query": "5.3.1",
"axobject-query": "^4.1.0",

@ -20,7 +20,7 @@ function reg_exp_entity(entity_name, is_attribute_value) {
/** @param {boolean} is_attribute_value */
function get_entity_pattern(is_attribute_value) {
const reg_exp_num = '#(?:x[a-fA-F\\d]+|\\d+)(?:;)?';
const reg_exp_num = '#(?:[xX][a-fA-F\\d]+|\\d+)(?:;)?';
const reg_exp_entities = Object.keys(entities).map(
/** @param {any} entity_name */ (entity_name) => reg_exp_entity(entity_name, is_attribute_value)
);
@ -50,7 +50,7 @@ export function decode_character_references(html, is_attribute_value) {
// Handle named entities
if (entity[0] !== '#') {
code = entities[entity];
} else if (entity[1] === 'x') {
} else if (entity[1] === 'x' || entity[1] === 'X') {
code = parseInt(entity.substring(2), 16);
} else {
code = parseInt(entity.substring(1), 10);
@ -60,7 +60,7 @@ export function decode_character_references(html, is_attribute_value) {
return match;
}
return String.fromCodePoint(validate_code(code));
return String.fromCodePoint(validate_code(code, is_attribute_value));
}
);
}
@ -75,10 +75,15 @@ const NUL = 0;
// Also see: https://en.wikipedia.org/wiki/Plane_(Unicode)
// Also see: https://html.spec.whatwg.org/multipage/parsing.html#preprocessing-the-input-stream
/** @param {number} code */
function validate_code(code) {
// line feed becomes generic whitespace
if (code === 10) {
/**
* @param {number} code
* @param {boolean} is_attribute_value
*/
function validate_code(code, is_attribute_value) {
// line feed becomes generic whitespace, since it is collapsed along with the
// surrounding whitespace anyway. In an attribute value it is significant, so it
// is left alone there
if (code === 10 && !is_attribute_value) {
return 32;
}

@ -43,7 +43,8 @@ export function DeclarationTag(node, context) {
*/
export function mark_async_declaration(context, metadata, declarations) {
const has_await = metadata.expression.has_await;
const blockers = [...metadata.expression.dependencies]
// reads inside closures must block too, like they do in template expressions
const blockers = [...metadata.expression.references]
.map((dep) => dep.blocker)
.filter((b) => b !== null && b.object !== context.state.async_consts?.id);

@ -1,4 +1,4 @@
/** @import { ExportNamedDeclaration, Identifier } from 'estree' */
/** @import { ExportNamedDeclaration, Identifier, VariableDeclaration } from 'estree' */
/** @import { Context } from '../types' */
import * as e from '../../../errors.js';
import { extract_identifiers } from '../../../utils/ast.js';
@ -23,15 +23,6 @@ export function ExportNamedDeclaration(node, context) {
}
if (node.declaration?.type === 'VariableDeclaration') {
// in runes mode, forbid `export let`
if (
context.state.analysis.runes &&
context.state.ast_type === 'instance' &&
node.declaration.kind === 'let'
) {
e.legacy_export_invalid(node);
}
for (const declarator of node.declaration.declarations) {
for (const id of extract_identifiers(declarator.id)) {
const binding = context.state.scope.get(id.name);
@ -46,6 +37,15 @@ export function ExportNamedDeclaration(node, context) {
}
}
}
// in runes mode, forbid `export let`
if (
context.state.analysis.runes &&
context.state.ast_type === 'instance' &&
node.declaration.kind === 'let'
) {
e.legacy_export_invalid(node);
}
}
if (context.state.analysis.runes) {

@ -39,6 +39,12 @@ export function transform_template(state, name, flags = 0) {
const namespace = state.metadata.namespace;
const tree = state.options.fragments === 'tree';
const { nodes } = state.template;
const is_lone_anchor = nodes.length === 1 && nodes[0].type === 'comment';
// special case - `$.comment` creates the anchor more cheaply than cloning a template
if (is_lone_anchor) return b.id('$.comment');
const expression = tree ? state.template.as_tree() : state.template.as_html();
const key =

@ -1,7 +1,7 @@
/** @import { CallExpression, Expression, Pattern } from 'estree' */
/** @import { AST } from '#compiler' */
/** @import { ComponentContext } from '../types' */
import { dev, is_ignored } from '../../../../state.js';
import { dev, ignore_map, is_ignored } from '../../../../state.js';
import { is_text_attribute } from '../../../../utils/ast.js';
import * as b from '#compiler/builders';
import { binding_properties } from '../../../bindings.js';
@ -40,9 +40,15 @@ export function BindDirective(node, context) {
validate_binding(context.state, node, expression);
}
const assignment = /** @type {Expression} */ (
context.visit(b.assignment('=', /** @type {Pattern} */ (node.expression), b.id('$$value')))
const raw_assignment = b.assignment(
'=',
/** @type {Pattern} */ (node.expression),
b.id('$$value')
);
// The assignment is generated, so inherit any ignores attached to the binding
ignore_map.set(raw_assignment, ignore_map.get(node) ?? []);
const assignment = /** @type {Expression} */ (context.visit(raw_assignment));
if (dev) {
// in dev, create named functions, so that `$inspect(...)` delivers
@ -58,16 +64,7 @@ export function BindDirective(node, context) {
get = b.thunk(expression);
/** @type {Expression | undefined} */
set = b.unthunk(
b.arrow(
[b.id('$$value')],
/** @type {Expression} */ (
context.visit(
b.assignment('=', /** @type {Pattern} */ (node.expression), b.id('$$value'))
)
)
)
);
set = b.unthunk(b.arrow([b.id('$$value')], assignment));
if (get === set) {
set = undefined;

@ -71,7 +71,7 @@ export function add_async_declaration(context, metadata, ids, assignments, kind
context.state.consts.push(kind === 'var' ? b.var(id.name) : b.let(id.name));
}
const blockers = [...metadata.expression.dependencies]
const blockers = [...metadata.expression.references]
.map((dep) => dep.blocker)
.filter((b) => b !== null && b.object !== context.state.async_consts?.id);

@ -141,14 +141,9 @@ export function Fragment(node, context) {
flags |= TEMPLATE_USE_IMPORT_NODE;
}
if (state.template.nodes.length === 1 && state.template.nodes[0].type === 'comment') {
// special case — we can use `$.comment` instead of creating a unique template
state.init.unshift(b.var(id, b.call('$.comment')));
} else {
const template_name = transform_template(state, 'root', flags);
state.init.unshift(b.var(id, b.call(template_name)));
}
close = b.stmt(b.call('$.append', b.id('$$anchor'), id));
}

@ -1,4 +1,4 @@
/** @import { ArrayExpression, Expression, ExpressionStatement, Identifier, MemberExpression, ObjectExpression } from 'estree' */
/** @import { ArrayExpression, Expression, ExpressionStatement, Identifier, MemberExpression, ObjectExpression, Statement } from 'estree' */
/** @import { AST } from '#compiler' */
/** @import { ComponentClientTransformState, ComponentContext } from '../types' */
/** @import { Scope } from '../../../scope' */
@ -229,11 +229,18 @@ export function RegularElement(node, context) {
continue;
}
// `<select defaultValue>` needs the options to exist before it can mark one
// as selected, so it is handled after the children, alongside `value`
if (node.name === 'select' && get_attribute_name(node, attribute) === 'defaultValue') {
continue;
}
const name = get_attribute_name(node, attribute);
if (
!is_custom_element &&
!cannot_be_set_statically(attribute.name) &&
(name !== 'value' || node.name !== 'textarea') &&
(attribute.value === true || is_text_attribute(attribute)) &&
(name !== 'class' || class_directives.length === 0) &&
(name !== 'style' || style_directives.length === 0)
@ -432,7 +439,7 @@ export function RegularElement(node, context) {
state: child_state
});
if (needs_reset) {
if (needs_reset && !fold_reset_into_child(child_state.init, context.state.node)) {
child_state.init.push(b.stmt(b.call('$.reset', context.state.node)));
}
}
@ -511,6 +518,34 @@ export function RegularElement(node, context) {
}
}
// deferred from the attribute loop above, so that the options it selects from
// have been created and had their values assigned
if (!has_spread && name === 'select') {
const default_value = /** @type {AST.Attribute[]} */ (attributes).find(
(attribute) => get_attribute_name(node, attribute) === 'defaultValue'
);
if (default_value) {
const { value, has_state } = build_attribute_value(default_value.value, context, (v, m) =>
context.state.memoizer.add(v, m)
);
(has_state ? context.state.update : context.state.init).push(
b.stmt(b.call('$.set_default_select_value', node_id, value))
);
}
const value_attribute = lookup.get('value');
const dynamic_value =
value_attribute !== undefined &&
value_attribute.value !== true &&
!is_text_attribute(value_attribute);
if (default_value || dynamic_value || bindings.has('value')) {
context.state.init.push(b.stmt(b.call('$.init_select', node_id)));
}
}
context.state.template.pop_element();
}
@ -701,6 +736,9 @@ function build_element_special_value_attribute(
);
const evaluated = context.state.scope.evaluate(value);
/** @param {Expression} value */
const build_update = (value) => {
const assignment = b.assignment('=', b.member(node_id, '__value'), value);
const set_value_assignment = b.assignment(
@ -709,7 +747,7 @@ function build_element_special_value_attribute(
evaluated.is_defined ? assignment : b.logical('??', assignment, b.literal(''))
);
const update = b.stmt(
return b.stmt(
is_select_with_value
? b.sequence([
set_value_assignment,
@ -723,6 +761,7 @@ function build_element_special_value_attribute(
? assignment
: set_value_assignment
);
};
if (has_state) {
const id = b.id(state.scope.generate(`${node_id.name}_value`));
@ -733,12 +772,49 @@ function build_element_special_value_attribute(
const init = element === 'option' ? b.object([]) : undefined;
state.init.push(b.var(id, init));
state.update.push(b.if(b.binary('!==', id, b.assignment('=', id, value)), b.block([update])));
// the guard already evaluated `value` into `id`, so read that back rather than
// evaluating the same expression (and its signal reads) a second time
state.update.push(
b.if(b.binary('!==', id, b.assignment('=', id, value)), b.block([build_update(id)]))
);
} else {
state.init.push(update);
state.init.push(build_update(value));
}
}
/**
* `<p>{text}</p>` and friends produce `var x = $.child(p, true); $.reset(p);`. That pair is
* by far the most common shape in compiled output, and `$.only_child` does both, so fold the
* two together when the `$.child(...)` is the last thing we emitted for this element.
* @param {Statement[]} init
* @param {Expression} node_id
* @returns {boolean} whether the reset was folded in
*/
function fold_reset_into_child(init, node_id) {
const last = init.at(-1);
if (is_select_with_value) {
state.init.push(b.stmt(b.call('$.init_select', node_id)));
if (
node_id?.type !== 'Identifier' ||
last?.type !== 'VariableDeclaration' ||
last.declarations.length !== 1
) {
return false;
}
const call = last.declarations[0].init;
if (
call?.type !== 'CallExpression' ||
call.callee.type !== 'Identifier' ||
call.callee.name !== '$.child' ||
call.arguments[0]?.type !== 'Identifier' ||
call.arguments[0].name !== node_id.name
) {
return false;
}
call.callee = b.id('$.only_child');
return true;
}

@ -30,6 +30,7 @@ export function build_attribute_effect(
) {
/** @type {ObjectExpression['properties']} */
const values = [];
const is_select = element.type === 'RegularElement' && element.name === 'select';
const memoizer = new Memoizer();
@ -48,7 +49,11 @@ export function build_attribute_effect(
context.state.init.push(b.var(id, value));
values.push(b.init(attribute.name, b.id(id)));
} else {
values.push(b.init(attribute.name, value));
const name =
is_select && normalize_attribute(attribute.name) === 'defaultValue'
? 'defaultValue'
: attribute.name;
values.push(b.init(name, value));
}
} else {
let value = /** @type {Expression} */ (context.visit(attribute));

@ -171,11 +171,6 @@ export function is_static_element(node) {
return false;
}
// We need to apply src and loading after appending the img to the DOM for lazy loading to work
if (node.name === 'img' && attribute.name === 'loading') {
return false;
}
if (attribute.value !== true && !is_text_attribute(attribute)) {
return false;
}

@ -110,7 +110,7 @@ function build_assignment(operator, left, right, context) {
context.visit(build_assignment_value(operator, left, right))
);
return b.call(binding.node, value);
return b.call(object, value);
}
return null;

@ -66,7 +66,7 @@ export function add_async_declaration(context, metadata, ids, assignments, kind
context.state.init.push(kind === 'var' ? b.var(id.name) : b.let(id.name));
}
const blockers = [...metadata.expression.dependencies]
const blockers = [...metadata.expression.references]
.map((dep) => dep.blocker)
.filter((b) => b !== null && b.object !== context.state.async_consts?.id);

@ -2,7 +2,13 @@
/** @import { AST } from '#compiler' */
/** @import { ComponentContext } from '../types.js' */
import * as b from '#compiler/builders';
import { block_close, block_open, block_open_else, create_child_block } from './shared/utils.js';
import {
block_close,
block_open,
block_open_else,
create_child_block,
prepend_block_marker
} from './shared/utils.js';
/**
* @param {AST.EachBlock} node
@ -51,7 +57,7 @@ export function EachBlock(node, context) {
const fallback = /** @type {BlockStatement} */ (context.visit(node.fallback));
fallback.body.unshift(b.stmt(b.call(b.id('$$renderer.push'), block_open_else)));
prepend_block_marker(fallback, /** @type {string} */ (block_open_else.value));
statements.push(
b.if(

@ -2,7 +2,7 @@
/** @import { AST } from '#compiler' */
/** @import { ComponentContext } from '../types.js' */
import * as b from '#compiler/builders';
import { block_close, create_child_block } from './shared/utils.js';
import { block_close, create_child_block, prepend_block_marker } from './shared/utils.js';
/**
* @param {AST.IfBlock} node
@ -10,7 +10,7 @@ import { block_close, create_child_block } from './shared/utils.js';
*/
export function IfBlock(node, context) {
const consequent = /** @type {BlockStatement} */ (context.visit(node.consequent));
consequent.body.unshift(b.stmt(b.call(b.id('$$renderer.push'), b.literal(`<!--[0-->`))));
prepend_block_marker(consequent, `<!--[0-->`);
/** @type {IfStatement} */
let if_statement = b.if(/** @type {Expression} */ (context.visit(node.test)), consequent);
@ -22,7 +22,7 @@ export function IfBlock(node, context) {
// Walk the else-if chain, flattening branches
for (const elseif of node.metadata.flattened ?? []) {
const branch = /** @type {BlockStatement} */ (context.visit(elseif.consequent));
branch.body.unshift(b.stmt(b.call(b.id('$$renderer.push'), b.literal(`<!--[${index++}-->`))));
prepend_block_marker(branch, `<!--[${index++}-->`);
current_if = current_if.alternate = b.if(
/** @type {Expression} */ (context.visit(elseif.test)),
@ -34,7 +34,7 @@ export function IfBlock(node, context) {
// Handle final else (or remaining async chain)
const final_alternate = alt ? /** @type {BlockStatement} */ (context.visit(alt)) : b.block([]);
final_alternate.body.unshift(b.stmt(b.call(b.id('$$renderer.push'), b.literal(`<!--[-1-->`))));
prepend_block_marker(final_alternate, `<!--[-1-->`);
current_if.alternate = final_alternate;
context.state.template.push(

@ -46,7 +46,7 @@ export function RegularElement(node, context) {
node.attributes.some(
(attribute) =>
((attribute.type === 'Attribute' || attribute.type === 'BindDirective') &&
attribute.name === 'value') ||
(attribute.name === 'value' || attribute.name.toLowerCase() === 'defaultvalue')) ||
attribute.type === 'SpreadAttribute'
);
const is_option_special = name === 'option';

@ -306,12 +306,14 @@ function get_attribute_name(element, attribute) {
* @param {(expression: Expression, metadata: ExpressionMetadata) => Expression} transform
*/
export function build_spread_object(element, attributes, context, transform) {
const is_select = element.type === 'RegularElement' && element.name === 'select';
const object = b.object(
attributes.map((attribute) => {
if (attribute.type === 'transformed') {
return b.prop('init', b.key(attribute.name), attribute.expression);
} else if (attribute.type === 'Attribute') {
const name = get_attribute_name(element, attribute);
let name = get_attribute_name(element, attribute);
if (is_select && name === 'defaultvalue') name = 'defaultValue';
const value = build_attribute_value(
attribute.value,
context,
@ -322,10 +324,9 @@ export function build_spread_object(element, attributes, context, transform) {
return b.prop('init', b.key(name), value);
} else if (attribute.type === 'BindDirective') {
const name = get_attribute_name(element, attribute);
const expression = /** @type {Expression} */ (context.visit(attribute.expression));
const value =
attribute.expression.type === 'SequenceExpression'
? b.call(attribute.expression.expressions[0])
: /** @type {Expression} */ (context.visit(attribute.expression));
expression.type === 'SequenceExpression' ? b.call(expression.expressions[0]) : expression;
return b.prop('init', b.key(name), value);
}

@ -180,6 +180,36 @@ export function build_template(template) {
return statements;
}
/**
* Prepends a hydration marker (e.g. `<!--[0-->`) to a branch. The branch has already been
* turned into statements by `build_template`, so if it happens to start with a static
* `$$renderer.push(...)` we fold the marker into that call rather than emitting a second one.
* @param {BlockStatement} block
* @param {string} marker
*/
export function prepend_block_marker(block, marker) {
const first = block.body[0];
if (
first?.type === 'ExpressionStatement' &&
first.expression.type === 'CallExpression' &&
first.expression.callee.type === 'Identifier' &&
first.expression.callee.name === '$$renderer.push' &&
first.expression.arguments.length === 1 &&
first.expression.arguments[0].type === 'TemplateLiteral'
) {
const quasi = first.expression.arguments[0].quasis[0];
// markers never contain characters that need escaping in a template literal
quasi.value.cooked = marker + quasi.value.cooked;
quasi.value.raw = marker + quasi.value.raw;
return;
}
block.body.unshift(b.stmt(b.call(b.id('$$renderer.push'), b.literal(marker))));
}
/**
*
* @param {AST.Attribute['value']} value

@ -23,7 +23,9 @@ export const binding_properties = {
event: 'durationchange',
omit_in_ssr: true
},
focused: {},
focused: {
omit_in_ssr: true // no corresponding HTML attribute
},
paused: {
valid_elements: ['audio', 'video'],
omit_in_ssr: true,

@ -72,17 +72,17 @@ export function set_dev_current_component_function(fn) {
}
/**
* Returns a `[get, set]` pair of functions for working with context in a type-safe way.
* Returns a `[get, set, has]` triplet of functions for working with context in a type-safe way.
*
* `get` will throw an error if `set` has not yet been called in the current component or any of
* its ancestors.
*
* @template T
* @returns {[() => T, (context: T) => T]}
* @returns {[() => T, (context: T) => T, () => boolean]}
* @since 5.40.0
*/
export function createContext() {
return /** @type {[() => T, (context: T) => T]} */ (
return /** @type {[() => T, (context: T) => T, () => boolean]} */ (
create_context(getContext, setContext, hasContext)
);
}

@ -1,14 +1,8 @@
/** @import { Effect, Source, TemplateNode, } from '#client' */
import {
BOUNDARY_EFFECT,
DIRTY,
EFFECT_PRESERVED,
EFFECT_TRANSPARENT,
MAYBE_DIRTY
} from '#client/constants';
import { BOUNDARY_EFFECT, EFFECT_PRESERVED, EFFECT_TRANSPARENT } from '#client/constants';
import { HYDRATION_START_ELSE, HYDRATION_START_FAILED } from '../../../../constants.js';
import { component_context, set_component_context } from '../../context.js';
import { handle_error, invoke_error_boundary } from '../../error-handling.js';
import { invoke_error_boundary } from '../../error-handling.js';
import {
block,
branch,
@ -271,13 +265,31 @@ export class Boundary {
queue_micro_task(() => {
var fragment = (this.#offscreen_fragment = document.createDocumentFragment());
var anchor = create_text();
var handled = false;
fragment.append(anchor);
this.#main_effect = this.#run(() => {
try {
return branch(() => this.#children(anchor));
} catch (error) {
try {
this.error(error);
handled = true;
} catch (error) {
invoke_error_boundary(error, this.#effect.parent);
}
return null;
}
});
if (this.#main_effect === null) {
this.#offscreen_fragment = null;
if (handled) this.#resolve(/** @type {Batch} */ (current_batch));
return;
}
if (this.#pending_count === 0) {
this.#anchor.before(fragment);
this.#offscreen_fragment = null;
@ -362,9 +374,6 @@ export class Boundary {
try {
Batch.ensure();
return fn();
} catch (e) {
handle_error(e);
return null;
} finally {
set_active_effect(previous_effect);
set_active_reaction(previous_reaction);

@ -1,4 +1,6 @@
import { hydrate_next, hydrating } from '../hydration.js';
import { create_element, create_text } from '../operations.js';
import { append } from '../template.js';
/**
* @param {Comment} anchor
@ -12,6 +14,23 @@ export function slot(anchor, $$props, name, slot_props, fallback_fn) {
hydrate_next();
}
// Custom element slots are native DOM slots.
// Use the stored reference because the shadow root may be closed.
if ($$props.$$host?.$$shadowRoot) {
const element = create_element('slot');
if (name !== 'default') element.name = name;
append(anchor, element);
if (fallback_fn !== null) {
const fallback_anchor = create_text();
element.append(fallback_anchor);
fallback_fn(fallback_anchor);
}
return;
}
var slot_fn = $$props.$$slots?.[name];
// Interop: Can use snippets to fill slots
var is_interop = false;

@ -26,7 +26,12 @@ import { set_class } from './class.js';
import { set_style } from './style.js';
import { ATTACHMENT_KEY, NAMESPACE_HTML, UNINITIALIZED } from '../../../../constants.js';
import { branch, destroy_effect, effect, managed } from '../../reactivity/effects.js';
import { init_select, select_option } from './bindings/select.js';
import {
init_select,
select_option,
set_default_select_value,
set_selected
} from './bindings/select.js';
import { flatten } from '../../reactivity/async.js';
export const CLASS = Symbol('class');
@ -122,25 +127,6 @@ export function set_checked(element, checked) {
element.checked = checked;
}
/**
* Sets the `selected` attribute on an `option` element.
* Not set through the property because that doesn't reflect to the DOM,
* which means it wouldn't be taken into account when a form is reset.
* @param {HTMLOptionElement} element
* @param {boolean} selected
*/
export function set_selected(element, selected) {
if (selected) {
// The selected option could've changed via user selection, and
// setting the value without this check would set it back.
if (!element.hasAttribute('selected')) {
element.setAttribute('selected', '');
}
} else {
element.removeAttribute('selected');
}
}
/**
* Applies the default checked property without influencing the current checked property.
* @param {HTMLInputElement} element
@ -291,11 +277,8 @@ function set_attributes(
skip_warning = false
) {
if (hydrating && should_remove_defaults && element.nodeName === INPUT_TAG) {
var input = /** @type {HTMLInputElement} */ (element);
var attribute = input.type === 'checkbox' ? 'defaultChecked' : 'defaultValue';
if (!(attribute in next)) {
remove_input_defaults(input);
if (!('defaultValue' in next || 'defaultChecked' in next)) {
remove_input_defaults(/** @type {HTMLInputElement} */ (element));
}
}
@ -313,6 +296,7 @@ function set_attributes(
var current = prev || {};
var is_option_element = element.nodeName === OPTION_TAG;
var is_select_element = element.nodeName === SELECT_TAG;
for (var key in prev) {
// don't null our internal $$onX listeners
@ -449,6 +433,9 @@ function set_attributes(
var is_default = name === 'defaultValue' || name === 'defaultChecked';
// A select's default value is represented by selected options, not a property.
if (is_select_element && name === 'defaultValue') continue;
if (value == null && !is_custom_element && !is_default) {
attributes[key] = null;
@ -534,8 +521,16 @@ export function attribute_effect(
skip_warning
);
if (inited && is_select && 'value' in next) {
select_option(/** @type {HTMLSelectElement} */ (element), next.value);
if (inited && is_select) {
var select = /** @type {HTMLSelectElement} */ (element);
if ('defaultValue' in next) {
set_default_select_value(select, next.defaultValue);
}
if ('value' in next) {
select_option(select, next.value);
}
}
for (let symbol of Object.getOwnPropertySymbols(effects)) {
@ -560,7 +555,13 @@ export function attribute_effect(
var select = /** @type {HTMLSelectElement} */ (element);
effect(() => {
select_option(select, /** @type {Record<string | symbol, any>} */ (prev).value, true);
var attrs = /** @type {Record<string | symbol, any>} */ (prev);
if ('defaultValue' in attrs) {
set_default_select_value(select, attrs.defaultValue);
}
select_option(select, attrs.value, true);
init_select(select);
});
}

@ -6,6 +6,71 @@ import * as w from '../../../warnings.js';
import { Batch, current_batch, previous_batch } from '../../../reactivity/batch.js';
import { async_mode_flag } from '../../../../flags/index.js';
/**
* Sets the `selected` attribute on an option so form reset can restore it.
* @param {HTMLOptionElement} option
* @param {boolean} selected
*/
export function set_selected(option, selected) {
if (selected) {
if (!option.hasAttribute('selected')) option.setAttribute('selected', '');
} else {
option.removeAttribute('selected');
}
}
/**
* Sets the options a form reset should restore. The first call selects
* them if nothing has set a value, later calls leave the current selection alone.
* @param {HTMLSelectElement} select
* @param {any} value
*/
export function set_default_select_value(select, value) {
var mounting = !('__defaultValue' in select);
// @ts-expect-error
if (!mounting && select.__defaultValue === value) return;
// @ts-expect-error
select.__defaultValue = value;
apply_default_select_value(select, !mounting || '__value' in select);
}
/**
* Marks the options matching `__defaultValue` as selected. Without `preserve`
* a newly matching option gets selected, as an inserted `<option selected>` would.
* @param {HTMLSelectElement} select
* @param {boolean} preserve
*/
function apply_default_select_value(select, preserve) {
// @ts-expect-error
var value = select.__defaultValue;
var multiple = select.multiple;
var values = multiple ? value ?? [] : null;
if (multiple && !is_array(values)) return;
var index = select.selectedIndex;
var selected = preserve && multiple ? new Set(select.selectedOptions) : null;
for (var option of select.options) {
var option_value = get_option_value(option);
set_selected(
option,
multiple ? /** @type {any[]} */ (values).includes(option_value) : is(option_value, value)
);
}
if (!preserve) return;
if (selected !== null) {
for (option of select.options) {
var was_selected = selected.has(option);
if (option.selected !== was_selected) option.selected = was_selected;
}
} else if (select.selectedIndex !== index) {
select.selectedIndex = index;
}
}
/**
* Selects the correct option(s) (depending on whether this is a multiple select)
* @template V
@ -47,11 +112,10 @@ export function select_option(select, value, mounting = false) {
}
/**
* Selects the correct option(s) if `value` is given,
* and then sets up a mutation observer to sync the
* current selection to the dom when it changes. Such
* changes could for example occur when options are
* inside an `#each` block.
* Sets up a mutation observer to sync the current selection
* and default to the dom when the options change, for example
* when they are inside an `#each` block. Called once per `<select>`,
* by the compiled output or by `attribute_effect` for spreads.
* @param {HTMLSelectElement} select
*/
export function init_select(select) {
@ -60,9 +124,15 @@ export function init_select(select) {
// Reacting to them could revert a user-initiated selection change, because the
// records are delivered as soon as any listener returns (e.g. a delegated `input`
// handler), which can happen before the `change` handler has updated `__value`
if (entries.every(is_selectedcontent_mutation) || !('__value' in select)) return;
// @ts-ignore
if (entries.every(is_selectedcontent_mutation)) return;
if ('__defaultValue' in select) {
apply_default_select_value(select, false);
}
if ('__value' in select) {
select_option(select, select.__value);
}
// Deliberately don't update the potential binding value,
// the model should be preserved unless explicitly changed
});
@ -154,8 +224,6 @@ export function bind_select_value(select, get, set = get) {
select.__value = value;
mounting = false;
});
init_select(select);
}
/** @param {HTMLOptionElement} option */

@ -337,6 +337,7 @@ export function transition(flags, element, get_fn, get_params) {
*/
function animate(element, options, counterpart, t2, on_begin, on_finish) {
var is_intro = t2 === 1;
var aborted = false;
if (is_function(options)) {
// In the case of a deferred transition (such as `crossfade`), `option` will be
@ -344,7 +345,6 @@ function animate(element, options, counterpart, t2, on_begin, on_finish) {
// once the DOM has been updated...
/** @type {Animation} */
var a;
var aborted = false;
queue_micro_task(() => {
if (aborted) return;
@ -381,6 +381,18 @@ function animate(element, options, counterpart, t2, on_begin, on_finish) {
const { delay = 0, css, tick, easing = linear } = options;
/** @type {globalThis.Animation} */
var animation;
var get_t = () => 1 - t2;
// wait a microtask before applying the initial styles and creating the dummy animation,
// so that transitions created in the same batch (e.g. on nested elements) all measure
// the DOM first (#18421). this still happens before the next paint, so the element
// won't be rendered without styles applied (#14732)
queue_micro_task(() => {
if (aborted) return;
var keyframes = [];
if (is_intro && counterpart === undefined) {
@ -394,15 +406,13 @@ function animate(element, options, counterpart, t2, on_begin, on_finish) {
}
}
var get_t = () => 1 - t2;
// create a dummy animation that lasts as long as the delay (but with whatever devtools
// multiplier is in effect). in the common case that it is `0`, we keep it anyway so that
// the CSS keyframes aren't created until the DOM is updated
//
// fill forwards to prevent the element from rendering without styles applied
// see https://github.com/sveltejs/svelte/issues/14732
var animation = element.animate(keyframes, { duration: delay, fill: 'forwards' });
animation = element.animate(keyframes, { duration: delay, fill: 'forwards' });
animation.onfinish = () => {
// remove dummy animation from the stack to prevent conflict with main animation
@ -471,9 +481,12 @@ function animate(element, options, counterpart, t2, on_begin, on_finish) {
on_finish();
};
};
});
return {
abort: () => {
aborted = true;
if (animation) {
animation.cancel();
// This prevents memory leaks in Chromium

@ -1,5 +1,5 @@
/** @import { Effect, TemplateNode } from '#client' */
import { hydrate_node, hydrating, set_hydrate_node } from './hydration.js';
import { hydrate_node, hydrating, reset, set_hydrate_node } from './hydration.js';
import { DEV } from 'esm-env';
import { init_array_prototype_warnings } from '../dev/equality.js';
import { get_descriptor, is_extensible } from '../../shared/utils.js';
@ -165,6 +165,26 @@ export function first_child(node, is_text = false) {
return hydrate_node;
}
/**
* `child`, for the very common case of an element with exactly one child. Resetting the
* hydration cursor is part of the same step, so the compiler doesn't have to emit a
* separate `reset` call for every `<p>{text}</p>` in an app.
* Don't mark this as side-effect-free, hydration needs to walk all nodes
* @param {TemplateNode} node
* @param {boolean} [is_text]
* @returns {TemplateNode | null}
*/
export function only_child(node, is_text = false) {
if (!hydrating) {
return get_first_child(node);
}
var first = child(node, is_text);
reset(node);
return first;
}
/**
* Don't mark this as side-effect-free, hydration needs to walk all nodes
* @param {TemplateNode} node

@ -33,7 +33,6 @@ export {
set_xlink_attribute,
set_value,
set_checked,
set_selected,
set_default_checked,
set_default_value,
CLASS,
@ -62,7 +61,13 @@ export {
} from './dom/elements/bindings/media.js';
export { bind_online } from './dom/elements/bindings/navigator.js';
export { bind_prop } from './dom/elements/bindings/props.js';
export { bind_select_value, init_select, select_option } from './dom/elements/bindings/select.js';
export {
bind_select_value,
init_select,
select_option,
set_selected,
set_default_select_value
} from './dom/elements/bindings/select.js';
export { bind_element_size, bind_resize_observer } from './dom/elements/bindings/size.js';
export { bind_this } from './dom/elements/bindings/this.js';
export {
@ -164,6 +169,7 @@ export { proxy } from './proxy.js';
export { create_custom_element } from './dom/elements/custom-element.js';
export {
child,
only_child,
first_child,
sibling,
$window as window,

@ -258,37 +258,7 @@ export function update_reaction(reaction) {
var fn = /** @type {Function} */ (reaction.fn);
var result = fn();
reaction.f |= REACTION_RAN;
var deps = reaction.deps;
// Don't remove reactions during fork;
// they must remain for when fork is discarded
var is_fork = current_batch?.is_fork;
if (new_deps !== null) {
var i;
if (!is_fork) {
remove_reactions(reaction, skipped_deps);
}
if (deps !== null && skipped_deps > 0) {
deps.length = skipped_deps + new_deps.length;
for (i = 0; i < new_deps.length; i++) {
deps[skipped_deps + i] = new_deps[i];
}
} else {
reaction.deps = deps = new_deps;
}
if (effect_tracking() && (reaction.f & CONNECTED) !== 0) {
for (i = skipped_deps; i < deps.length; i++) {
(deps[i].reactions ??= []).push(reaction);
}
}
} else if (!is_fork && deps !== null && skipped_deps < deps.length) {
remove_reactions(reaction, skipped_deps);
deps.length = skipped_deps;
}
var deps = update_dependencies(reaction);
// If we're inside an effect and we have untracked writes, then we need to
// ensure that if any of those untracked writes result in re-invalidation
@ -300,7 +270,7 @@ export function update_reaction(reaction) {
deps !== null &&
(reaction.f & (DERIVED | MAYBE_DIRTY | DIRTY)) === 0
) {
for (i = 0; i < /** @type {Source[]} */ (untracked_writes).length; i++) {
for (var i = 0; i < /** @type {Source[]} */ (untracked_writes).length; i++) {
schedule_possible_effect_self_invalidation(
untracked_writes[i],
/** @type {Effect} */ (reaction)
@ -344,6 +314,9 @@ export function update_reaction(reaction) {
return result;
} catch (error) {
// still commit the deps read before the throw, otherwise deriveds connected by this run keep no reader and the reaction never re-runs when they change
update_dependencies(reaction);
return handle_error(error);
} finally {
reaction.f ^= REACTION_IS_UPDATING;
@ -358,6 +331,45 @@ export function update_reaction(reaction) {
}
}
/**
* @param {Reaction} reaction
*/
function update_dependencies(reaction) {
var deps = reaction.deps;
// Don't remove reactions during fork;
// they must remain for when fork is discarded
var is_fork = current_batch?.is_fork;
if (new_deps !== null) {
var i;
if (!is_fork) {
remove_reactions(reaction, skipped_deps);
}
if (deps !== null && skipped_deps > 0) {
deps.length = skipped_deps + new_deps.length;
for (i = 0; i < new_deps.length; i++) {
deps[skipped_deps + i] = new_deps[i];
}
} else {
reaction.deps = deps = new_deps;
}
if (effect_tracking() && (reaction.f & CONNECTED) !== 0) {
for (i = skipped_deps; i < deps.length; i++) {
(deps[i].reactions ??= []).push(reaction);
}
}
} else if (!is_fork && deps !== null && skipped_deps < deps.length) {
remove_reactions(reaction, skipped_deps);
deps.length = skipped_deps;
}
return deps;
}
/**
* @template V
* @param {Reaction} signal

@ -12,11 +12,11 @@ export function set_ssr_context(v) {
/**
* @template T
* @returns {[() => T, (context: T) => T]}
* @returns {[() => T, (context: T) => T, () => boolean]}
* @since 5.40.0
*/
export function createContext() {
return /** @type {[() => T, (context: T) => T]} */ (
return /** @type {[() => T, (context: T) => T, () => boolean]} */ (
create_context(getContext, setContext, hasContext)
);
}

@ -13,7 +13,7 @@ import { attributes } from './index.js';
import { get_render_context, with_render_context, init_render_context } from './render-context.js';
import { sha256 } from './crypto.js';
import * as devalue from 'devalue';
import { has_own_property, noop } from '../shared/utils.js';
import { has_own_property, is_array, noop } from '../shared/utils.js';
import { escape_html } from '../../escaping.js';
/** @typedef {'head' | 'body'} RendererType */
@ -89,7 +89,7 @@ export class Renderer {
* State that is local to the branch it is declared in.
* It will be shallow-copied to all children.
*
* @type {{ select_value: string | undefined }}
* @type {{ select_value: any, multiple: boolean }}
*/
local;
@ -101,7 +101,7 @@ export class Renderer {
this.#parent = parent;
this.global = global;
this.local = parent ? { ...parent.local } : { select_value: undefined };
this.local = parent ? { ...parent.local } : { select_value: undefined, multiple: false };
this.type = parent ? parent.type : 'body';
}
@ -338,11 +338,13 @@ export class Renderer {
* @returns {void}
*/
select(attrs, fn, css_hash, classes, styles, flags, is_rich) {
const { value, ...select_attrs } = attrs;
const { value, defaultValue, ...select_attrs } = attrs;
if (select_attrs.multiple === '') select_attrs.multiple = true;
this.push(`<select${attributes(select_attrs, css_hash, classes, styles, flags)}>`);
this.child((renderer) => {
renderer.local.select_value = value;
renderer.local.select_value = value === undefined ? defaultValue : value;
renderer.local.multiple = !!select_attrs.multiple;
fn(renderer);
});
this.push(`${is_rich ? '<!>' : ''}</select>`);
@ -370,7 +372,15 @@ export class Renderer {
value = attrs.value;
}
if (value === this.local.select_value) {
var select_value = this.local.select_value;
if (
// Super edge-case, but theoretically someone could use arrays with non-multiple selects,
// so we gotta check for the multiple attribute presence, too.
this.local.multiple && is_array(select_value)
? select_value.includes(value)
: value === select_value
) {
renderer.#out.push(' selected=""');
}

@ -27,7 +27,8 @@ export function attr(name, value, is_boolean = false) {
if (name === 'hidden' && value !== 'until-found') {
is_boolean = true;
}
if (value == null || (!value && is_boolean)) return '';
// `''` is a present boolean attribute, as it is in markup and on the client
if (value == null || (is_boolean && !value && value !== '')) return '';
const normalized =
(has_own_property.call(replacements, name) && replacements[name].get(value)) || value;
const assignment = is_boolean ? `=""` : `="${escape_html(normalized, true)}"`;

@ -5,7 +5,7 @@ import { lifecycle_outside_component, missing_context } from './errors.js';
* @param {(key: object) => T} get_context
* @param {(key: object, context: T) => T} set_context
* @param {(key: object) => boolean} has_context
* @returns {[() => T, (context: T) => T]}
* @returns {[() => T, (context: T) => T, () => boolean]}
*/
export function create_context(get_context, set_context, has_context) {
const key = {};
@ -18,7 +18,8 @@ export function create_context(get_context, set_context, has_context) {
return get_context(key);
},
(context) => set_context(key, context)
(context) => set_context(key, context),
() => has_context(key)
];
}

@ -153,6 +153,28 @@ export class SvelteMap extends Map {
return super.get(key);
}
/**
* @param {K} key
* @param {V} value
* */
getOrInsert(key, value) {
if (!super.has(key)) {
this.set(key, value);
}
return /** @type {V} */ (this.get(key));
}
/**
* @param {K} key
* @param {(key: K) => V} callbackFn
*/
getOrInsertComputed(key, callbackFn) {
if (!super.has(key)) {
this.set(key, callbackFn(key));
}
return /** @type {V} */ (this.get(key));
}
/**
* @param {K} key
* @param {V} value

@ -100,6 +100,85 @@ test('map.get(...)', () => {
cleanup();
});
test('map.getOrInsert(...)', () => {
const map = new SvelteMap([
[2, 2],
[3, 3]
]);
const log: any = [];
const cleanup = effect_root(() => {
render_effect(() => {
log.push('get 1', map.getOrInsert(1, 1));
});
render_effect(() => {
log.push('get 2', map.getOrInsert(2, 2));
});
render_effect(() => {
log.push('get 3', map.getOrInsert(3, 4));
});
});
flushSync(() => {
map.delete(2);
});
flushSync(() => {
map.set(2, 2);
});
assert.deepEqual(log, ['get 1', 1, 'get 2', 2, 'get 3', 3, 'get 2', 2]);
cleanup();
});
test('map.getOrInsertComputed(...)', () => {
const map = new SvelteMap([
[2, 2],
[3, 3]
]);
const log: any = [];
const cleanup = effect_root(() => {
render_effect(() => {
log.push(
'get 1',
map.getOrInsertComputed(1, (k) => k)
);
});
render_effect(() => {
log.push(
'get 2',
map.getOrInsertComputed(2, (k) => k)
);
});
render_effect(() => {
log.push(
'get 3',
map.getOrInsertComputed(3, () => 4)
);
});
});
flushSync(() => {
map.delete(2);
});
flushSync(() => {
map.set(2, 2);
});
assert.deepEqual(log, ['get 1', 1, 'get 2', 2, 'get 3', 3, 'get 2', 2]);
cleanup();
});
test('map.has(...)', () => {
const map = new SvelteMap([
[1, 1],

@ -163,6 +163,8 @@ export class SvelteURL extends URL {
set protocol(value) {
super.protocol = value;
set(this.#protocol, super.protocol);
// changing the protocol can clear the port when it matches the new scheme's default
set(this.#port, super.port);
}
get search() {

@ -240,3 +240,25 @@ test('url.searchParams.forEach re-runs when the search string changes via the UR
cleanup();
});
test('url.port is updated when the protocol change clears the port', () => {
const url = new SvelteURL('http://example.com:443/');
const log: any = [];
const cleanup = effect_root(() => {
render_effect(() => {
log.push(url.port);
});
});
flushSync(() => {
// 443 is the default port for https, so it gets stripped
url.protocol = 'https:';
});
assert.equal(url.port, '');
assert.equal(url.href, 'https://example.com/');
assert.deepEqual(log, ['443', '']);
cleanup();
});

@ -0,0 +1,9 @@
import { test } from '../../test';
export default test({
error: {
code: 'derived_invalid_export',
message:
'Cannot export derived state from a module. To expose the current derived value, export a function returning its value'
}
});

@ -0,0 +1,4 @@
<script>
let count = $state(0);
export let double = $derived(count * 2);
</script>

@ -0,0 +1,53 @@
{
"html": {
"type": "Fragment",
"start": 0,
"end": 32,
"children": [
{
"type": "Element",
"start": 0,
"end": 32,
"name": "p",
"attributes": [
{
"type": "Attribute",
"start": 3,
"end": 19,
"name": "title",
"name_loc": {
"start": {
"line": 1,
"column": 3,
"character": 3
},
"end": {
"line": 1,
"column": 8,
"character": 8
}
},
"value": [
{
"start": 10,
"end": 18,
"type": "Text",
"raw": "A&#x0A;B",
"data": "A\nB"
}
]
}
],
"children": [
{
"type": "Text",
"start": 20,
"end": 28,
"raw": "A&#x0A;B",
"data": "A B"
}
]
}
]
}
}

@ -14,8 +14,8 @@ export default test({
assert.htmlEqual(
ce.shadowRoot.innerHTML,
`
<slot></slot>
<p>named fallback</p>
<slot>fallback</slot>
<slot name="named"><p>named fallback</p></slot>
`
);
@ -23,8 +23,8 @@ export default test({
assert.htmlEqual(
ce.shadowRoot.innerHTML,
`
<slot></slot>
<p>named fallback</p>
<slot>fallback</slot>
<slot name="named"><p>named fallback</p></slot>
`
);
}

@ -3,10 +3,7 @@ const tick = () => Promise.resolve();
export default test({
async test({ assert, target }) {
target.innerHTML = `
<custom-element>
<strong>slotted</strong>
</custom-element>`;
target.innerHTML = '<custom-element></custom-element>';
await tick();
await tick();
@ -16,7 +13,26 @@ export default test({
const div = el.shadowRoot.children[0];
const [slot0, slot1] = div.children;
assert.equal(slot0.assignedNodes()[1], target.querySelector('strong'));
assert.equal(slot1.innerHTML, 'foo fallback content');
assert.equal(slot0.localName, 'slot');
assert.equal(slot0.assignedNodes().length, 0);
assert.equal(slot0.innerHTML, '<p>default fallback content</p>');
assert.equal(slot1.localName, 'slot');
assert.equal(slot1.name, 'foo');
assert.equal(slot1.assignedNodes().length, 0);
assert.equal(slot1.innerHTML, '<p>foo fallback content</p>');
const default_content = document.createElement('strong');
default_content.textContent = 'default content';
el.append(default_content);
const named_content = document.createElement('strong');
named_content.slot = 'foo';
named_content.textContent = 'named content';
el.append(named_content);
assert.equal(slot0.assignedNodes().length, 1);
assert.equal(slot0.assignedNodes()[0], default_content);
assert.equal(slot1.assignedNodes().length, 1);
assert.equal(slot1.assignedNodes()[0], named_content);
}
});

@ -3,7 +3,7 @@ const tick = () => Promise.resolve();
export default test({
async test({ assert, target }) {
target.innerHTML = '<custom-element name="world"></custom-element>';
target.innerHTML = '<custom-element name="world"><span>slotted</span></custom-element>';
await tick();
await tick();
@ -15,5 +15,6 @@ export default test({
assert.equal(el.shadowRoot, null);
assert.equal(h1.innerHTML, 'Hello world!');
assert.equal(getComputedStyle(h1).color, 'rgb(255, 0, 0)');
assert.equal(el.querySelector('slot').innerHTML, '');
}
});

@ -5,6 +5,7 @@
</script>
<h1>Hello {name}!</h1>
<slot>fallback</slot>
<style>
h1 {

@ -0,0 +1,14 @@
<script>
import Nested from './Nested.svelte';
import { slide } from 'svelte/transition';
let { depth } = $props();
</script>
<div class="level-{depth}" in:slide|global={{ duration: 100 }}>
{#if depth > 0}
<Nested depth={depth - 1} />
{:else}
<div style="height: 100px">leaf</div>
{/if}
</div>

@ -0,0 +1,36 @@
import { test } from '../../assert';
export default test({
async test({ assert, target }) {
const button = target.querySelector('button');
button?.click();
// wait for the transition's keyframes to be created
const animation = await new Promise((resolve, reject) => {
const start = performance.now();
function check() {
const outer = target.querySelector('.level-2');
const animation = outer
?.getAnimations()
.find((a) => a.effect?.getTiming().duration === 100);
if (animation) {
resolve(animation);
} else if (performance.now() - start > 2000) {
reject(new Error('timed out waiting for the transition to start'));
} else {
requestAnimationFrame(check);
}
}
check();
});
// the outermost `slide` must have measured the element with its
// descendants at their natural size, not collapsed to zero by their
// own starting styles (#18421)
const keyframes = animation.effect?.getKeyframes() ?? [];
assert.equal(keyframes[keyframes.length - 1].height, '100px');
}
});

@ -0,0 +1,11 @@
<script>
import Nested from './Nested.svelte';
let visible = $state(false);
</script>
<button onclick={() => (visible = !visible)}>toggle</button>
{#if visible}
<Nested depth={2} />
{/if}

@ -6,17 +6,30 @@ export default test({
return { selected: ['two', 'three'] };
},
html: `
ssrHtml: `
<select multiple>
<option>one</option>
<option>two</option>
<option>three</option>
<option selected>two</option>
<option selected>three</option>
</select>
<p>selected: two, three</p>
`,
test({ assert, component, target, window }) {
test({ assert, component, target, window, variant }) {
const selected = variant === 'hydrate' ? ' selected' : '';
assert.htmlEqual(
target.innerHTML,
`
<select multiple>
<option>one</option>
<option${selected}>two</option>
<option${selected}>three</option>
</select>
<p>selected: two, three</p>
`
);
const select = target.querySelector('select');
ok(select);
const options = [...target.querySelectorAll('option')];
@ -33,8 +46,8 @@ export default test({
`
<select multiple>
<option>one</option>
<option>two</option>
<option>three</option>
<option${selected}>two</option>
<option${selected}>three</option>
</select>
<p>selected: three</p>
@ -51,8 +64,8 @@ export default test({
`
<select multiple>
<option>one</option>
<option>two</option>
<option>three</option>
<option${selected}>two</option>
<option${selected}>three</option>
</select>
<p>selected: one, three</p>
@ -70,8 +83,8 @@ export default test({
`
<select multiple>
<option>one</option>
<option>two</option>
<option>three</option>
<option${selected}>two</option>
<option${selected}>three</option>
</select>
<p>selected: one, two</p>

@ -7,6 +7,7 @@ export default test({
<span>*</span>
<span>*</span>
<span>*</span>
<span>*</span>
<span></span>
<span>A</span>

@ -2,6 +2,7 @@
<span>&midast;</span>
<span>&#x0002A;</span>
<span>&#x0002A</span>
<span>&#X0002A;</span>
<span>&#42;</span>
<span>&#10;</span>

@ -5,14 +5,19 @@ export default test({
return { foo: 42 };
},
html: '<textarea></textarea>',
ssrHtml: '<textarea>42</textarea>',
ssrHtml: '<textarea>42</textarea> <textarea>static</textarea>',
test({ assert, component, target }) {
const textarea = /** @type {HTMLTextAreaElement} */ (target.querySelector('textarea'));
assert.strictEqual(textarea.value, '42');
test({ assert, component, target, variant }) {
assert.htmlEqual(
target.innerHTML,
`<textarea></textarea> <textarea>${variant === 'hydrate' ? 'static' : ''}</textarea>`
);
const [textarea1, textarea2] = target.querySelectorAll('textarea');
assert.strictEqual(textarea1.value, '42');
assert.strictEqual(textarea2.value, 'static');
component.foo = 43;
assert.strictEqual(textarea.value, '43');
assert.strictEqual(textarea1.value, '43');
}
});

@ -3,3 +3,4 @@
</script>
<textarea value='{foo}'/>
<textarea value="static"></textarea>

@ -177,11 +177,11 @@ export function runtime_suite(runes: boolean) {
['dom', 'hydrate', 'ssr', 'async-ssr'],
(variant, config, test_name) => {
if (!async_mode && (config.skip_no_async || test_name.startsWith('async-'))) {
return true;
return 'no-test';
}
if (async_mode && config.skip_async) {
return true;
return 'no-test';
}
if (variant === 'hydrate') {
@ -195,9 +195,9 @@ export function runtime_suite(runes: boolean) {
) {
return 'no-test';
}
if (variant === 'ssr') {
if (
(test_name.startsWith('async-') && !config.mode?.includes('server')) ||
(config.mode && !config.mode.includes('server')) ||
(!config.test_ssr &&
config.html === undefined &&

@ -0,0 +1,10 @@
import { tick } from 'svelte';
import { test } from '../../test';
// #18469 — a @const in a nested snippet reading an async declaration through a closure must block on it
export default test({
async test({ assert, target }) {
await tick();
assert.htmlEqual(target.innerHTML, '<p>true</p> <p>false</p>');
}
});

@ -0,0 +1,12 @@
<script>
async function getValue() {
return new Set(['a', 'b', 'c']);
}
const value = await getValue();
</script>
{#each [['a', 'b'], ['a', 'x']] as keys}
{@const all_present = keys.every((k) => value.has(k))}
<p>{all_present}</p>
{/each}

@ -0,0 +1,11 @@
import { tick } from 'svelte';
import { test } from '../../test';
// #18469 — a sync $derived in a nested snippet reading an async declaration through a closure must block on it
export default test({
ssrHtml: '<p>true</p> <p>false</p>',
async test({ assert, target }) {
await tick();
assert.htmlEqual(target.innerHTML, '<p>true</p> <p>false</p>');
}
});

@ -0,0 +1,17 @@
<script>
async function getValue() {
return new Set(['a', 'b', 'c']);
}
</script>
{#snippet outer()}
{const value = $derived(await getValue())}
{#snippet inner(keys)}
{const all_present = $derived(keys.every((k) => value.has(k)))}
<p>{all_present}</p>
{/snippet}
{@render inner(['a', 'b'])}
{@render inner(['a', 'x'])}
{/snippet}
{@render outer()}

@ -0,0 +1,7 @@
<script>
const { environment } = $props();
if (environment === 'client') {
throw new Error('oops');
}
</script>

@ -0,0 +1,14 @@
import { tick } from 'svelte';
import { test } from '../../test';
export default test({
mode: ['hydrate'],
server_props: { environment: 'server' },
props: { environment: 'client' },
ssrHtml: 'loading inner loading nested',
async test({ assert, target }) {
await tick();
assert.htmlEqual(target.innerHTML, 'inner failed: oops outer failed: oops');
}
});

@ -0,0 +1,24 @@
<script>
import Child from './Child.svelte';
const { environment } = $props();
</script>
<svelte:boundary>
<Child {environment} />
{await new Promise(() => {})}
{#snippet pending()}loading inner{/snippet}
{#snippet failed(error)}inner failed: {error.message}{/snippet}
</svelte:boundary>
<svelte:boundary>
<svelte:boundary>
<Child {environment} />
{await new Promise(() => {})}
{#snippet pending()}loading nested{/snippet}
</svelte:boundary>
{#snippet failed(error)}outer failed: {error.message}{/snippet}
</svelte:boundary>

@ -1,7 +1,13 @@
<script>
import { get } from './main.svelte';
import { get, has, has_unset } from './main.svelte';
const message = get();
</script>
<h1>{message}</h1>
{#if has()}
<h2>it's me</h2>
{/if}
{#if !has_unset()}
<h2>or not</h2>
{/if}

@ -2,7 +2,7 @@ import { test } from '../../test';
export default test({
ssrHtml: `<div></div>`,
html: `<div><h1>hello</h1></div>`,
html: `<div><h1>hello</h1><h2>it's me</h2><h2>or not</h2></div>`,
test() {}
});

@ -3,9 +3,11 @@
import Child from './Child.svelte';
/** @type {ReturnType<typeof createContext<string>>} */
const [get, set] = createContext();
const [get, set, has] = createContext();
/** @type {ReturnType<typeof createContext<string>>} */
const [, , has_unset] = createContext();
export { get };
export { get, has, has_unset };
function Wrapper(Component) {
return (...args) => {
@ -15,6 +17,8 @@
}
</script>
<div {@attach (target) => {
<div
{@attach (target) => {
mount(Wrapper(Child), { target });
}}></div>
}}
></div>

@ -1,7 +1,13 @@
<script>
import { get } from './main.svelte';
import { get, has, has_unset } from './main.svelte';
const message = get();
</script>
<h1>{message}</h1>
{#if has()}
<h2>it's me</h2>
{/if}
{#if !has_unset()}
<h2>or not</h2>
{/if}

@ -1,5 +1,5 @@
import { test } from '../../test';
export default test({
html: `<h1>hello</h1>`
html: `<h1>hello</h1><h2>it's me</h2><h2>or not</h2>`
});

@ -2,9 +2,11 @@
import { createContext } from 'svelte';
/** @type {ReturnType<typeof createContext<string>>} */
const [get, set] = createContext();
const [get, set, has] = createContext();
/** @type {ReturnType<typeof createContext<string>>} */
const [, , has_unset] = createContext();
export { get };
export { get, has, has_unset };
</script>
<script>

@ -1,6 +0,0 @@
import { test } from '../../test';
export default test({
ssrHtml: '<p>LATER</p> <input value="LATER">',
html: '<p>LATER</p> <input>'
});

@ -0,0 +1,6 @@
import { test } from '../../test';
export default test({
ssrHtml: '<p>y:y</p> <p>LATER</p> <input value="LATER">',
html: '<p>y:y</p> <p>LATER</p> <input>'
});

@ -1,4 +1,13 @@
<script>
function write(value) {
return foo = value;
}
// a leading comment on the declaration
// that spans more than one line
let foo = $derived('x');
let bar = write('y');
const ctx = {
get later() {
return later;
@ -10,5 +19,6 @@
let later = $derived.by(() => 'LATER');
</script>
<p>{foo}:{bar}</p>
<p>{ctx.later}</p>
<input bind:value={later} />

@ -0,0 +1,23 @@
import { flushSync } from 'svelte';
import { test } from '../../test';
export default test({
mode: ['hydrate'],
async test({ assert, target }) {
const [a, b, reset] = target.querySelectorAll('input');
// let the deferred hydration cleanup run
await Promise.resolve();
flushSync();
b.checked = true;
reset.click();
await Promise.resolve();
flushSync();
assert.equal(a.defaultChecked, true);
assert.equal(a.checked, true);
assert.equal(b.checked, false);
}
});

@ -0,0 +1,9 @@
<script>
let spread = { defaultChecked: true, checked: true };
</script>
<form>
<input type="radio" name="option" value="a" {...spread} />
<input type="radio" name="option" value="b" />
<input type="reset" value="Reset" />
</form>

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

Loading…
Cancel
Save