Merge branch 'main' into allow-recursive-derived

allow-recursive-derived
Rich Harris 5 months ago
commit 3d34b0c444

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

@ -4,47 +4,32 @@ on:
types: [opened, synchronize]
push:
branches: [main]
workflow_dispatch:
inputs:
sha:
description: 'Commit SHA to build'
required: true
type: string
permissions: {}
jobs:
# This job determines the environment to use for the build job. It ensures that:
# - For pushes to main, we use the "Publish pkg.pr.new (maintainers)" environment.
# - For PRs from the same repository, we also use the "Publish pkg.pr.new (maintainers)" environment, since these are trusted.
# - For PRs from forks, we use the "Publish pkg.pr.new (external contributors)" environment, which requires manual approval by a maintainer before the build job can run.
# This protects us from running untrusted code while still allowing external contributors to use pkg.pr.new.
resolve-env:
runs-on: ubuntu-latest
outputs:
environment: ${{ steps.resolve.outputs.environment }}
steps:
- name: Determine environment
id: resolve
run: |
if [[ "${{ github.event_name }}" == "push" ]]; then
echo "environment=Publish pkg.pr.new (maintainers)" >> "$GITHUB_OUTPUT"
elif [[ "${{ github.event.pull_request.head.repo.full_name }}" == "${{ github.repository }}" ]]; then
echo "environment=Publish pkg.pr.new (maintainers)" >> "$GITHUB_OUTPUT"
else
echo "environment=Publish pkg.pr.new (external contributors)" >> "$GITHUB_OUTPUT"
fi
build:
needs: resolve-env
# Skip pull_request_target events from forks — maintainers can use workflow_dispatch instead
if: >
github.event_name != 'pull_request_target' ||
github.event.pull_request.head.repo.full_name == github.repository
runs-on: ubuntu-latest
# This is the line that ensures forks require manual approval before running the build job
environment: ${{ needs.resolve-env.outputs.environment }}
# No permissions — this job runs user-controlled code
permissions: {}
steps:
- uses: actions/checkout@v6
with:
# For pull_request_target, we must explicitly check out the PR head.
# This is safe because the environment gate above has already fired —
# an org member has approved this specific commit for external PRs.
ref: ${{ github.event.pull_request.head.sha || github.sha }}
# For pull_request_target, check out the PR head.
# For workflow_dispatch, check out the manually specified SHA.
# For push, fall back to the push SHA.
ref: ${{ github.event.pull_request.head.sha || inputs.sha || github.sha }}
- uses: pnpm/action-setup@v4
- uses: actions/setup-node@v6
@ -119,10 +104,11 @@ jobs:
comment:
needs: sanitize
if: github.event_name == 'pull_request_target'
if: github.event_name == 'pull_request_target' || github.event_name == 'workflow_dispatch'
runs-on: ubuntu-latest
permissions:
contents: read
pull-requests: write
steps:
@ -131,6 +117,37 @@ jobs:
with:
name: sanitized-output
- name: Resolve PR number
id: pr
uses: actions/github-script@v8
with:
script: |
if (context.eventName === 'pull_request_target') {
core.setOutput('number', context.issue.number);
return;
}
const { data: pulls } = await github.rest.repos.listPullRequestsAssociatedWithCommit({
owner: context.repo.owner,
repo: context.repo.repo,
commit_sha: '${{ inputs.sha }}',
});
const open = pulls.filter(p => p.state === 'open');
if (open.length === 0) {
core.setFailed(`No open PR found for commit ${{ inputs.sha }}`);
return;
}
if (open.length > 1) {
const nums = open.map(p => `#${p.number}`).join(', ');
core.setFailed(`Multiple open PRs found for commit ${{ inputs.sha }}: ${nums}`);
return;
}
core.setOutput('number', open[0].number);
- name: Post or update comment
uses: actions/github-script@v8
with:
@ -144,8 +161,7 @@ jobs:
return;
}
// Issue number from trusted event context, never from the artifact
const issue_number = context.issue.number;
const issue_number = parseInt('${{ steps.pr.outputs.number }}', 10);
const bot_comment_identifier = `<!-- pkg.pr.new comment -->`;

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

@ -33,7 +33,7 @@ const no_compiler_imports = {
}
};
/** @type {import('eslint').Linter.FlatConfig[]} */
/** @type {import('eslint').Linter.Config[]} */
export default [
...svelte_config,
{

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

@ -1,5 +1,47 @@
# svelte
## 5.51.0
### Minor Changes
- feat: Use `TrustedTypes` for HTML handling where supported ([#16271](https://github.com/sveltejs/svelte/pull/16271))
### Patch Changes
- fix: sanitize template-literal-special-characters in SSR attribute values ([#17692](https://github.com/sveltejs/svelte/pull/17692))
- fix: follow-up formatting in `print()` — flush block-level elements into separate sequences ([#17699](https://github.com/sveltejs/svelte/pull/17699))
- fix: preserve delegated event handlers as long as one or more root components are using them ([#17695](https://github.com/sveltejs/svelte/pull/17695))
## 5.50.3
### Patch Changes
- fix: take into account `nodeName` case sensitivity on XHTML pages ([#17689](https://github.com/sveltejs/svelte/pull/17689))
- fix: render `multiple` and `selected` attributes as empty strings for XHTML compliance ([#17689](https://github.com/sveltejs/svelte/pull/17689))
- fix: always lowercase HTML elements, for XHTML compliance ([#17664](https://github.com/sveltejs/svelte/pull/17664))
- fix: freeze effects-inside-deriveds when disconnecting, unfreeze on reconnect ([#17682](https://github.com/sveltejs/svelte/pull/17682))
- fix: propagate `$effect` errors to `<svelte:boundary>` ([#17684](https://github.com/sveltejs/svelte/pull/17684))
## 5.50.2
### Patch Changes
- fix: resolve `effect_update_depth_exceeded` when using `bind:value` on `<select>` with derived state in legacy mode ([#17645](https://github.com/sveltejs/svelte/pull/17645))
- fix: don't swallow `DOMException` when `media.play()` fails in `bind:paused` ([#17656](https://github.com/sveltejs/svelte/pull/17656))
- chore: provide proper public type for `parseCss` result ([#17654](https://github.com/sveltejs/svelte/pull/17654))
- fix: robustify blocker calculation ([#17676](https://github.com/sveltejs/svelte/pull/17676))
- fix: reduce if block nesting ([#17662](https://github.com/sveltejs/svelte/pull/17662))
## 5.50.1
### Patch Changes

@ -2,7 +2,7 @@
"name": "svelte",
"description": "Cybernetically enhanced web apps",
"license": "MIT",
"version": "5.50.1",
"version": "5.51.0",
"type": "module",
"types": "./types/index.d.ts",
"engines": {
@ -170,6 +170,7 @@
"@jridgewell/sourcemap-codec": "^1.5.0",
"@sveltejs/acorn-typescript": "^1.0.5",
"@types/estree": "^1.0.5",
"@types/trusted-types": "^2.0.7",
"acorn": "^8.12.1",
"aria-query": "^5.3.1",
"axobject-query": "^4.1.0",

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

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

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

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

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

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

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

@ -8,7 +8,7 @@ import {
is_event_attribute
} from '../../../../utils/ast.js';
import { dev, locate_node } from '../../../../state.js';
import { should_proxy } from '../utils.js';
import { build_getter, should_proxy } from '../utils.js';
import { visit_assignment_expression } from '../../shared/assignments.js';
import { validate_mutation } from './shared/utils.js';
import { get_rune } from '../../../scope.js';
@ -147,7 +147,7 @@ function build_assignment(operator, left, right, context) {
// mutation
if (transform?.mutate) {
return transform.mutate(
let mutation = transform.mutate(
object,
b.assignment(
operator,
@ -155,6 +155,25 @@ function build_assignment(operator, left, right, context) {
/** @type {Expression} */ (context.visit(right))
)
);
if (binding.legacy_indirect_bindings.size > 0) {
mutation = b.sequence([
mutation,
b.call(
'$.invalidate_inner_signals',
b.arrow(
[],
b.block(
Array.from(binding.legacy_indirect_bindings).map((binding) =>
b.stmt(build_getter({ ...binding.node }, context.state))
)
)
)
)
]);
}
return mutation;
}
// in cases like `(object.items ??= []).push(value)`, we may need to warn

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

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

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

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

@ -1,4 +1,4 @@
/** @import { BlockStatement, Expression, Statement } from 'estree' */
/** @import { BlockStatement, Expression, IfStatement, Statement } from 'estree' */
/** @import { AST } from '#compiler' */
/** @import { ComponentContext } from '../types.js' */
import * as b from '#compiler/builders';
@ -9,23 +9,37 @@ import { block_close, block_open, block_open_else, create_child_block } from './
* @param {ComponentContext} context
*/
export function IfBlock(node, context) {
const test = /** @type {Expression} */ (context.visit(node.test));
const consequent = /** @type {BlockStatement} */ (context.visit(node.consequent));
consequent.body.unshift(b.stmt(b.call(b.id('$$renderer.push'), block_open)));
const alternate = node.alternate
? /** @type {BlockStatement} */ (context.visit(node.alternate))
: b.block([]);
/** @type {IfStatement} */
let if_statement = b.if(/** @type {Expression} */ (context.visit(node.test)), consequent);
consequent.body.unshift(b.stmt(b.call(b.id('$$renderer.push'), block_open)));
let index = 1;
let current_if = if_statement;
let alt = node.alternate;
// 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++}-->`))));
current_if = current_if.alternate = b.if(
/** @type {Expression} */ (context.visit(elseif.test)),
branch
);
alt = elseif.alternate;
}
alternate.body.unshift(b.stmt(b.call(b.id('$$renderer.push'), block_open_else)));
// Handle final else (or remaining async chain)
const final_alternate = alt ? /** @type {BlockStatement} */ (context.visit(alt)) : b.block([]);
/** @type {Statement} */
let statement = b.if(test, consequent, alternate);
final_alternate.body.unshift(b.stmt(b.call(b.id('$$renderer.push'), block_open_else)));
current_if.alternate = final_alternate;
context.state.template.push(
...create_child_block(
[statement],
[if_statement],
node.metadata.expression.blockers(),
node.metadata.expression.has_await
),

@ -15,6 +15,7 @@ import { is_customizable_select_element } from '../../../nodes.js';
* @param {ComponentContext} context
*/
export function RegularElement(node, context) {
const name = context.state.namespace === 'html' ? node.name.toLowerCase() : node.name;
const namespace = determine_namespace_for_children(node, context.state.namespace);
/** @type {ComponentServerTransformState} */
@ -27,7 +28,7 @@ export function RegularElement(node, context) {
template: []
};
const node_is_void = is_void(node.name);
const node_is_void = is_void(name);
const optimiser = new PromiseOptimiser();
@ -35,28 +36,28 @@ export function RegularElement(node, context) {
// avoid calling build_element_attributes here to prevent evaluating/awaiting
// attribute expressions twice. We'll handle attributes in the special branch.
const is_select_special =
node.name === 'select' &&
name === 'select' &&
node.attributes.some(
(attribute) =>
((attribute.type === 'Attribute' || attribute.type === 'BindDirective') &&
attribute.name === 'value') ||
attribute.type === 'SpreadAttribute'
);
const is_option_special = node.name === 'option';
const is_option_special = name === 'option';
const is_special = is_select_special || is_option_special;
let body = /** @type {Expression | null} */ (null);
if (!is_special) {
// only open the tag in the non-special path
state.template.push(b.literal(`<${node.name}`));
state.template.push(b.literal(`<${name}`));
body = build_element_attributes(node, { ...context, state }, optimiser.transform);
state.template.push(b.literal(node_is_void ? '/>' : '>')); // add `/>` for XHTML compliance
}
if ((node.name === 'script' || node.name === 'style') && node.fragment.nodes.length === 1) {
if ((name === 'script' || name === 'style') && node.fragment.nodes.length === 1) {
state.template.push(
b.literal(/** @type {AST.Text} */ (node.fragment.nodes[0]).data),
b.literal(`</${node.name}>`)
b.literal(`</${name}>`)
);
context.state.template.push(
@ -90,7 +91,7 @@ export function RegularElement(node, context) {
b.call(
'$.push_element',
b.id('$$renderer'),
b.literal(node.name),
b.literal(name),
b.literal(location.line),
b.literal(location.column)
)
@ -142,7 +143,7 @@ export function RegularElement(node, context) {
b.call(
'$.push_element',
b.id('$$renderer'),
b.literal(node.name),
b.literal(name),
b.literal(location.line),
b.literal(location.column)
)
@ -191,16 +192,13 @@ export function RegularElement(node, context) {
} else {
// For optgroup or select with rich content, add hydration marker at the start
process_children(trimmed, { ...context, state });
if (
(node.name === 'optgroup' || node.name === 'select') &&
is_customizable_select_element(node)
) {
if ((name === 'optgroup' || name === 'select') && is_customizable_select_element(node)) {
state.template.push(b.literal('<!>'));
}
}
if (!node_is_void) {
state.template.push(b.literal(`</${node.name}>`));
state.template.push(b.literal(`</${name}>`));
}
if (dev) {

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

@ -225,7 +225,7 @@ export function build_attribute_value(
const node = value[i];
if (node.type === 'Text') {
quasi.value.raw += trim_whitespace
quasi.value.cooked += trim_whitespace
? node.data.replace(regex_whitespaces_strict, ' ')
: node.data;
} else {
@ -244,6 +244,10 @@ export function build_attribute_value(
}
}
for (const quasi of quasis) {
quasi.value.raw = sanitize_template_string(/** @type {string} */ (quasi.value.cooked));
}
return b.template(quasis, expressions);
}

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

@ -120,6 +120,12 @@ export class Binding {
*/
legacy_dependencies = [];
/**
* Bindings that should be invalidated when this binding is invalidated
* @type {Set<Binding>}
*/
legacy_indirect_bindings = new Set();
/**
* Legacy props: the `class` in `{ export klass as class}`. $props(): The `class` in { class: klass } = $props()
* @type {string | null}

@ -371,9 +371,23 @@ const svelte_visitors = {
}
}
} else {
const is_block_element =
child_node.type === 'RegularElement' ||
child_node.type === 'Component' ||
child_node.type === 'SvelteHead' ||
child_node.type === 'SvelteFragment' ||
child_node.type === 'SvelteBoundary' ||
child_node.type === 'SvelteDocument' ||
child_node.type === 'SvelteSelf' ||
child_node.type === 'SvelteWindow' ||
child_node.type === 'SvelteComponent' ||
child_node.type === 'SvelteElement' ||
child_node.type === 'SlotElement' ||
child_node.type === 'TitleElement';
if (is_block_element && sequence.length > 0) flush();
sequence.push(child_node);
if (child_node.type === 'RegularElement') flush();
if (is_block_element) flush();
}
}

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

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

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

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

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

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

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

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

@ -5,7 +5,7 @@ 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';
import * as w from '../../warnings.js';
import { LOADING_ATTR_SYMBOL } from '#client/constants';
import { IS_XHTML, LOADING_ATTR_SYMBOL } from '#client/constants';
import { queue_micro_task } from '../task.js';
import { is_capture_event, can_delegate_event, normalize_attribute } from '../../../../utils.js';
import {
@ -30,6 +30,12 @@ export const STYLE = Symbol('style');
const IS_CUSTOM_ELEMENT = Symbol('is custom element');
const IS_HTML = Symbol('is html');
const LINK_TAG = IS_XHTML ? 'link' : 'LINK';
const INPUT_TAG = IS_XHTML ? 'input' : 'INPUT';
const OPTION_TAG = IS_XHTML ? 'option' : 'OPTION';
const SELECT_TAG = IS_XHTML ? 'select' : 'SELECT';
const PROGRESS_TAG = IS_XHTML ? 'progress' : 'PROGRESS';
/**
* The value/checked attribute in the template actually corresponds to the defaultValue property, so we need
* to remove it upon hydration to avoid a bug when someone resets the form value.
@ -83,7 +89,7 @@ export function set_value(element, value) {
value ?? undefined) ||
// @ts-expect-error
// `progress` elements always need their value set when it's `0`
(element.value === value && (value !== 0 || element.nodeName !== 'PROGRESS'))
(element.value === value && (value !== 0 || element.nodeName !== PROGRESS_TAG))
) {
return;
}
@ -168,7 +174,7 @@ export function set_attribute(element, attribute, value, skip_warning) {
if (
attribute === 'src' ||
attribute === 'srcset' ||
(attribute === 'href' && element.nodeName === 'LINK')
(attribute === 'href' && element.nodeName === LINK_TAG)
) {
if (!skip_warning) {
check_src_in_dev_hydration(element, attribute, value ?? '');
@ -241,7 +247,7 @@ export function set_custom_element_data(node, prop, value) {
(setters_cache.has(node.getAttribute('is') || node.nodeName) ||
// customElements may not be available in browser extension contexts
!customElements ||
customElements.get(node.getAttribute('is') || node.tagName.toLowerCase())
customElements.get(node.getAttribute('is') || node.nodeName.toLowerCase())
? get_setters(node).includes(prop)
: value && typeof value === 'object')
) {
@ -280,7 +286,7 @@ function set_attributes(
should_remove_defaults = false,
skip_warning = false
) {
if (hydrating && should_remove_defaults && element.tagName === 'INPUT') {
if (hydrating && should_remove_defaults && element.nodeName === INPUT_TAG) {
var input = /** @type {HTMLInputElement} */ (element);
var attribute = input.type === 'checkbox' ? 'defaultChecked' : 'defaultValue';
@ -302,7 +308,7 @@ function set_attributes(
}
var current = prev || {};
var is_option_element = element.tagName === 'OPTION';
var is_option_element = element.nodeName === OPTION_TAG;
for (var key in prev) {
if (!(key in next)) {
@ -505,7 +511,7 @@ export function attribute_effect(
/** @type {Record<symbol, Effect>} */
var effects = {};
var is_select = element.nodeName === 'SELECT';
var is_select = element.nodeName === SELECT_TAG;
var inited = false;
managed(() => {

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

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

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

@ -5,7 +5,7 @@ import { active_effect, untrack } from '../../runtime.js';
import { loop } from '../../loop.js';
import { should_intro } from '../../render.js';
import { TRANSITION_GLOBAL, TRANSITION_IN, TRANSITION_OUT } from '../../../../constants.js';
import { BLOCK_EFFECT, EFFECT_RAN, EFFECT_TRANSPARENT } from '#client/constants';
import { BLOCK_EFFECT, REACTION_RAN, EFFECT_TRANSPARENT } from '#client/constants';
import { queue_micro_task } from '../task.js';
import { without_reactive_context } from './bindings/shared.js';
@ -289,7 +289,7 @@ export function transition(flags, element, get_fn, get_params) {
}
}
run = !block || (block.f & EFFECT_RAN) !== 0;
run = !block || (block.f & REACTION_RAN) !== 0;
}
if (run) {

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

@ -5,8 +5,9 @@ import { init_array_prototype_warnings } from '../dev/equality.js';
import { get_descriptor, is_extensible } from '../../shared/utils.js';
import { active_effect } from '../runtime.js';
import { async_mode_flag } from '../../flags/index.js';
import { TEXT_NODE, EFFECT_RAN } from '#client/constants';
import { TEXT_NODE, REACTION_RAN } from '#client/constants';
import { eager_block_effects } from '../reactivity/batch.js';
import { NAMESPACE_HTML } from '../../../constants.js';
// export these for reference in the compiled code, making global name deduplication unnecessary
/** @type {Window} */
@ -227,22 +228,21 @@ export function should_defer_append() {
if (eager_block_effects !== null) return false;
var flags = /** @type {Effect} */ (active_effect).f;
return (flags & EFFECT_RAN) !== 0;
return (flags & REACTION_RAN) !== 0;
}
/**
*
* @param {string} tag
* @template {keyof HTMLElementTagNameMap | string} T
* @param {T} tag
* @param {string} [namespace]
* @param {string} [is]
* @returns
* @returns {T extends keyof HTMLElementTagNameMap ? HTMLElementTagNameMap[T] : Element}
*/
export function create_element(tag, namespace, is) {
let options = is ? { is } : undefined;
if (namespace) {
return document.createElementNS(namespace, tag, options);
}
return document.createElement(tag, options);
return /** @type {T extends keyof HTMLElementTagNameMap ? HTMLElementTagNameMap[T] : Element} */ (
document.createElementNS(namespace ?? NAMESPACE_HTML, tag, options)
);
}
export function create_fragment() {

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

@ -22,7 +22,16 @@ import {
TEMPLATE_USE_MATHML,
TEMPLATE_USE_SVG
} from '../../../constants.js';
import { COMMENT_NODE, DOCUMENT_FRAGMENT_NODE, EFFECT_RAN, TEXT_NODE } from '#client/constants';
import {
COMMENT_NODE,
DOCUMENT_FRAGMENT_NODE,
IS_XHTML,
REACTION_RAN,
TEXT_NODE
} from '#client/constants';
const TEMPLATE_TAG = IS_XHTML ? 'template' : 'TEMPLATE';
const SCRIPT_TAG = IS_XHTML ? 'script' : 'SCRIPT';
/**
* @param {TemplateNode} start
@ -61,7 +70,7 @@ export function from_html(content, flags) {
}
if (node === undefined) {
node = create_fragment_from_html(has_start ? content : '<!>' + content);
node = create_fragment_from_html(has_start ? content : '<!>' + content, true);
if (!is_fragment) node = /** @type {TemplateNode} */ (get_first_child(node));
}
@ -109,7 +118,7 @@ function from_namespace(content, flags, ns = 'svg') {
}
if (!node) {
var fragment = /** @type {DocumentFragment} */ (create_fragment_from_html(wrapped));
var fragment = /** @type {DocumentFragment} */ (create_fragment_from_html(wrapped, true));
var root = /** @type {Element} */ (get_first_child(fragment));
if (is_fragment) {
@ -186,12 +195,12 @@ function fragment_from_tree(structure, ns) {
if (children.length > 0) {
var target =
element.tagName === 'TEMPLATE'
element.nodeName === TEMPLATE_TAG
? /** @type {HTMLTemplateElement} */ (element).content
: element;
target.append(
fragment_from_tree(children, element.tagName === 'foreignObject' ? undefined : namespace)
fragment_from_tree(children, element.nodeName === 'foreignObject' ? undefined : namespace)
);
}
@ -268,14 +277,14 @@ function run_scripts(node) {
const is_fragment = node.nodeType === DOCUMENT_FRAGMENT_NODE;
const scripts =
/** @type {HTMLElement} */ (node).tagName === 'SCRIPT'
/** @type {HTMLElement} */ (node).nodeName === SCRIPT_TAG
? [/** @type {HTMLScriptElement} */ (node)]
: node.querySelectorAll('script');
const effect = /** @type {Effect & { nodes: EffectNodes }} */ (active_effect);
for (const script of scripts) {
const clone = document.createElement('script');
const clone = create_element('script');
for (var attribute of script.attributes) {
clone.setAttribute(attribute.name, attribute.value);
}
@ -353,7 +362,7 @@ export function append(anchor, dom) {
// When hydrating and outer component and an inner component is async, i.e. blocked on a promise,
// then by the time the inner resolves we have already advanced to the end of the hydrated nodes
// of the parent component. Check for defined for that reason to avoid rewinding the parent's end marker.
if ((effect.f & EFFECT_RAN) === 0 || effect.nodes.end === null) {
if ((effect.f & REACTION_RAN) === 0 || effect.nodes.end === null) {
effect.nodes.end = hydrate_node;
}

@ -3,7 +3,7 @@
import { DEV } from 'esm-env';
import { FILENAME } from '../../constants.js';
import { is_firefox } from './dom/operations.js';
import { ERROR_VALUE, BOUNDARY_EFFECT, EFFECT_RAN } from './constants.js';
import { ERROR_VALUE, BOUNDARY_EFFECT, REACTION_RAN, EFFECT } from './constants.js';
import { define_property, get_descriptor } from '../shared/utils.js';
import { active_effect, active_reaction } from './runtime.js';
@ -25,22 +25,19 @@ export function handle_error(error) {
adjustments.set(error, get_adjustments(error, effect));
}
if ((effect.f & EFFECT_RAN) === 0) {
// if the error occurred while creating this subtree, we let it
// bubble up until it hits a boundary that can handle it
if ((effect.f & BOUNDARY_EFFECT) === 0) {
if (DEV && !effect.parent && error instanceof Error) {
apply_adjustments(error);
}
throw error;
// if the error occurred while creating this subtree, we let it
// bubble up until it hits a boundary that can handle it, unless
// it's an $effect in which case it doesn't run immediately
if ((effect.f & REACTION_RAN) === 0 && (effect.f & EFFECT) === 0) {
if (DEV && !effect.parent && error instanceof Error) {
apply_adjustments(error);
}
/** @type {Boundary} */ (effect.b).error(error);
} else {
// otherwise we bubble up the effect tree ourselves
invoke_error_boundary(error, effect);
throw error;
}
// otherwise we bubble up the effect tree ourselves
invoke_error_boundary(error, effect);
}
/**
@ -50,6 +47,11 @@ export function handle_error(error) {
export function invoke_error_boundary(error, effect) {
while (effect !== null) {
if ((effect.f & BOUNDARY_EFFECT) !== 0) {
if ((effect.f & REACTION_RAN) === 0) {
// we are still creating the boundary effect
throw error;
}
try {
/** @type {Boundary} */ (effect.b).error(error);
return;

@ -126,7 +126,7 @@ export function proxy(value) {
}
var s = sources.get(prop);
if (s === undefined) {
s = with_parent(() => {
with_parent(() => {
var s = source(descriptor.value, stack);
sources.set(prop, s);
if (DEV && typeof prop === 'string') {

@ -701,16 +701,15 @@ function flush_queued_effects(effects) {
// don't know if we need to keep them until they are executed. Doing the check
// here (rather than in `update_effect`) allows us to skip the work for
// immediate effects.
if (effect.deps === null && effect.first === null && effect.nodes === null) {
// if there's no teardown or abort controller we completely unlink
// the effect from the graph
if (effect.teardown === null && effect.ac === null) {
// remove this effect from the graph
unlink_effect(effect);
} else {
// keep the effect in the graph, but free up some memory
effect.fn = null;
}
if (
effect.deps === null &&
effect.first === null &&
effect.nodes === null &&
effect.teardown === null &&
effect.ac === null
) {
// remove this effect from the graph
unlink_effect(effect);
}
// If update_effect() has a flushSync() in it, we may have flushed another flush_queued_effects(),

@ -19,12 +19,20 @@ import {
increment_write_version,
set_active_effect,
push_reaction_value,
is_destroying_effect
is_destroying_effect,
update_effect,
remove_reactions
} from '../runtime.js';
import { equals, safe_equals } from './equality.js';
import * as e from '../errors.js';
import * as w from '../warnings.js';
import { async_effect, destroy_effect, effect_tracking, teardown } from './effects.js';
import {
async_effect,
destroy_effect,
destroy_effect_children,
effect_tracking,
teardown
} from './effects.js';
import { eager_effects, internal_set, set_eager_effects, source } from './sources.js';
import { get_error } from '../../shared/dev.js';
import { async_mode_flag, tracing_mode_flag } from '../../flags/index.js';
@ -33,7 +41,7 @@ import { component_context } from '../context.js';
import { UNINITIALIZED } from '../../../constants.js';
import { batch_values, current_batch } from './batch.js';
import { unset_context } from './async.js';
import { deferred, includes } from '../../shared/utils.js';
import { deferred, includes, noop } from '../../shared/utils.js';
import { set_signal_status, update_derived_status } from './status.js';
/** @type {Effect | null} */
@ -362,3 +370,43 @@ export function update_derived(derived) {
update_derived_status(derived);
}
}
/**
* @param {Derived} derived
*/
export function freeze_derived_effects(derived) {
if (derived.effects === null) return;
for (const e of derived.effects) {
// if the effect has a teardown function or abort signal, call it
if (e.teardown || e.ac) {
e.teardown?.();
e.ac?.abort(STALE_REACTION);
// make it a noop so it doesn't get called again if the derived
// is unfrozen. we don't set it to `null`, because the existence
// of a teardown function is what determines whether the
// effect runs again during unfreezing
e.teardown = noop;
e.ac = null;
remove_reactions(e, 0);
destroy_effect_children(e);
}
}
}
/**
* @param {Derived} derived
*/
export function unfreeze_derived_effects(derived) {
if (derived.effects === null) return;
for (const e of derived.effects) {
// if the effect was previously frozen — indicated by the presence
// of a teardown function — unfreeze it
if (e.teardown) {
update_effect(e);
}
}
}

@ -19,7 +19,7 @@ import {
EFFECT,
DESTROYED,
INERT,
EFFECT_RAN,
REACTION_RAN,
BLOCK_EFFECT,
ROOT_EFFECT,
EFFECT_TRANSPARENT,
@ -122,7 +122,6 @@ function create_effect(type, fn, sync) {
if (sync) {
try {
update_effect(effect);
effect.f |= EFFECT_RAN;
} catch (e) {
destroy_effect(effect);
throw e;
@ -206,7 +205,7 @@ export function user_effect(fn) {
// Non-nested `$effect(...)` in a component should be deferred
// until the component is mounted
var flags = /** @type {Effect} */ (active_effect).f;
var defer = !active_reaction && (flags & BRANCH_EFFECT) !== 0 && (flags & EFFECT_RAN) === 0;
var defer = !active_reaction && (flags & BRANCH_EFFECT) !== 0 && (flags & REACTION_RAN) === 0;
if (defer) {
// Top-level `$effect(...)` in an unmounted component — defer until mount

@ -302,6 +302,7 @@ export function update_pre(source, d = 1) {
var value = get(source);
// @ts-expect-error
// eslint-disable-next-line no-useless-assignment -- `++`/`--` used for return value, not side effect on `value`
return set(source, d === 1 ? ++value : --value);
}

@ -149,8 +149,8 @@ export function hydrate(component, options) {
}
}
/** @type {Map<string, number>} */
const document_listeners = new Map();
/** @type {Map<EventTarget, Map<string, number>>} */
const listeners = new Map();
/**
* @template {Record<string, any>} Exports
@ -177,17 +177,25 @@ function _mount(Component, { target, anchor, props = {}, events, context, intro
// Add the event listener to both the container and the document.
// The container listener ensures we catch events from within in case
// the outer content stops propagation of the event.
target.addEventListener(event_name, handle_event_propagation, { passive });
//
// The document listener ensures we catch events that originate from elements that were
// manually moved outside of the container (e.g. via manual portals).
for (const node of [target, document]) {
var counts = listeners.get(node);
if (counts === undefined) {
counts = new Map();
listeners.set(node, counts);
}
var n = document_listeners.get(event_name);
var count = counts.get(event_name);
if (n === undefined) {
// The document listener ensures we catch events that originate from elements that were
// manually moved outside of the container (e.g. via manual portals).
document.addEventListener(event_name, handle_event_propagation, { passive });
document_listeners.set(event_name, 1);
} else {
document_listeners.set(event_name, n + 1);
if (count === undefined) {
node.addEventListener(event_name, handle_event_propagation, { passive });
counts.set(event_name, 1);
} else {
counts.set(event_name, count + 1);
}
}
}
};
@ -245,15 +253,20 @@ function _mount(Component, { target, anchor, props = {}, events, context, intro
return () => {
for (var event_name of registered_events) {
target.removeEventListener(event_name, handle_event_propagation);
var n = /** @type {number} */ (document_listeners.get(event_name));
if (--n === 0) {
document.removeEventListener(event_name, handle_event_propagation);
document_listeners.delete(event_name);
} else {
document_listeners.set(event_name, n);
for (const node of [target, document]) {
var counts = /** @type {Map<string, number>} */ (listeners.get(node));
var count = /** @type {number} */ (counts.get(event_name));
if (--count == 0) {
node.removeEventListener(event_name, handle_event_propagation);
counts.delete(event_name);
if (counts.size === 0) {
listeners.delete(node);
}
} else {
counts.set(event_name, count);
}
}
}

@ -22,13 +22,16 @@ import {
STALE_REACTION,
ERROR_VALUE,
WAS_MARKED,
MANAGED_EFFECT
MANAGED_EFFECT,
REACTION_RAN
} from './constants.js';
import { old_values } from './reactivity/sources.js';
import {
destroy_derived_effects,
execute_derived,
freeze_derived_effects,
recent_async_deriveds,
unfreeze_derived_effects,
update_derived
} from './reactivity/deriveds.js';
import { async_mode_flag, tracing_mode_flag } from '../flags/index.js';
@ -256,6 +259,7 @@ export function update_reaction(reaction) {
reaction.f |= REACTION_IS_UPDATING;
var fn = /** @type {Function} */ (reaction.fn);
var result = fn();
reaction.f |= REACTION_RAN;
var deps = reaction.deps;
// Don't remove reactions during fork;
@ -399,8 +403,10 @@ function remove_reaction(signal, dependency) {
update_derived_status(derived);
// freeze any effects inside this derived
freeze_derived_effects(derived);
// Disconnect any reactions owned by this reaction
destroy_derived_effects(derived);
remove_reactions(derived, 0);
}
}
@ -650,7 +656,7 @@ export function get(signal) {
active_reaction !== null &&
(is_updating_effect || (active_reaction.f & CONNECTED) !== 0);
var is_new = derived.deps === null;
var is_new = (derived.f & REACTION_RAN) === 0;
if (is_dirty(derived)) {
if (should_connect) {
@ -663,6 +669,7 @@ export function get(signal) {
}
if (should_connect && !is_new) {
unfreeze_derived_effects(derived);
reconnect(derived);
}
}
@ -684,14 +691,15 @@ export function get(signal) {
* @param {Derived} derived
*/
function reconnect(derived) {
if (derived.deps === null) return;
derived.f |= CONNECTED;
if (derived.deps === null) return;
for (const dep of derived.deps) {
(dep.reactions ??= []).push(derived);
if ((dep.f & DERIVED) !== 0 && (dep.f & CONNECTED) === 0) {
unfreeze_derived_effects(/** @type {Derived} */ (dep));
reconnect(/** @type {Derived} */ (dep));
}
}

@ -272,7 +272,7 @@ export class Renderer {
}
if (value === this.local.select_value) {
renderer.#out.push(' selected');
renderer.#out.push(' selected=""');
}
renderer.#out.push(`>${body}${is_rich ? '<!>' : ''}</option>`);

@ -187,7 +187,7 @@ test('selects an option with an explicit value', () => {
const { head, body } = Renderer.render(component as unknown as Component);
expect(head).toBe('');
expect(body).toBe(
'<!--[--><select><option value="1">one</option><option value="2" selected>two</option><option value="3">three</option></select><!--]-->'
'<!--[--><select><option value="1">one</option><option value="2" selected="">two</option><option value="3">three</option></select><!--]-->'
);
});
@ -203,7 +203,7 @@ test('selects an option with an implicit value', () => {
const { head, body } = Renderer.render(component as unknown as Component);
expect(head).toBe('');
expect(body).toBe(
'<!--[--><select><option>one</option><option selected>two</option><option>three</option></select><!--]-->'
'<!--[--><select><option>one</option><option selected="">two</option><option>three</option></select><!--]-->'
);
});
@ -221,7 +221,7 @@ test('select merges scoped css hash with static class', () => {
const { head, body } = Renderer.render(component as unknown as Component);
expect(head).toBe('');
expect(body).toBe(
'<!--[--><select class="foo svelte-hash"><option value="foo" selected>foo</option></select><!--]-->'
'<!--[--><select class="foo svelte-hash"><option value="foo" selected="">foo</option></select><!--]-->'
);
});

@ -28,7 +28,7 @@ export function attr(name, value, is_boolean = false) {
}
if (value == null || (!value && is_boolean)) return '';
const normalized = (name in replacements && replacements[name].get(value)) || value;
const assignment = is_boolean ? '' : `="${escape_html(normalized, true)}"`;
const assignment = is_boolean ? `=""` : `="${escape_html(normalized, true)}"`;
return ` ${name}${assignment}`;
}

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

@ -4,7 +4,7 @@ import { parseCss } from 'svelte/compiler';
describe('parseCss', () => {
it('parses a simple rule', () => {
const ast = parseCss('div { color: red; }');
assert.equal(ast.type, 'StyleSheet');
assert.equal(ast.type, 'StyleSheetFile');
assert.equal(ast.children.length, 1);
assert.equal(ast.children[0].type, 'Rule');
});
@ -57,7 +57,7 @@ describe('parseCss', () => {
it('parses empty stylesheet', () => {
const ast = parseCss('');
assert.equal(ast.type, 'StyleSheet');
assert.equal(ast.type, 'StyleSheetFile');
assert.equal(ast.children.length, 0);
assert.equal(ast.start, 0);
assert.equal(ast.end, 0);
@ -138,7 +138,7 @@ describe('parseCss', () => {
it('parses escaped characters', () => {
const ast = parseCss("div { background: url('./example.png?\\''); }");
assert.equal(ast.type, 'StyleSheet');
assert.equal(ast.type, 'StyleSheetFile');
assert.equal(ast.children.length, 1);
const rule = ast.children[0];
assert.equal(rule.type, 'Rule');

@ -0,0 +1,25 @@
<svelte:head><title>Page Title</title></svelte:head><div>no space</div>
<Component /><Component />
<Component><span>child</span></Component><div>after component</div>
<svelte:boundary><div>boundary content</div></svelte:boundary><div>after boundary</div>
<!--comment--><div>after comment</div>
<div>before comment</div><!--comment-->
{#each items as item}<div>{item}</div>{/each}<div>after each</div>
{@render children()}<div>after render</div>
<div>before render</div>{@render children()}
<Component /> <div>with spaces</div>
<Component><span>child</span></Component> <div>spaces after component</div>
<!--comment--> <div>spaces after comment</div>
{@render children()} <div>spaces after render</div>
<Component />
<div>newline between</div>
<!--comment-->
<div>newline after comment</div>
{#each items as item}
<div>{item}</div>
{/each}
<div>newline after each</div>
{@render children()}
<div>after render</div>

@ -0,0 +1,42 @@
<svelte:head><title>Page Title</title></svelte:head>
<div>no space</div>
<Component />
<Component />
<Component><span>child</span></Component>
<div>after component</div>
<svelte:boundary><div>boundary content</div></svelte:boundary>
<div>after boundary</div>
<!--comment-->
<div>after comment</div>
<div>before comment</div>
<!--comment-->
{#each items as item}
<div>{item}</div>
{/each}
<div>after each</div>
{@render children()}
<div>after render</div>
<div>before render</div>
{@render children()}
<Component />
<div>with spaces</div>
<Component><span>child</span></Component>
<div>spaces after component</div>
<!--comment-->
<div>spaces after comment</div>
{@render children()}
<div>spaces after render</div>
<Component />
<div>newline between</div>
<!--comment-->
<div>newline after comment</div>
{#each items as item}
<div>{item}</div>
{/each}
<div>newline after each</div>
{@render children()}
<div>after render</div>

@ -0,0 +1,37 @@
import { test } from '../../test';
export default test({
ssrHtml: `
<select>
<option selected="" value="">Select</option>
<option value="us">US</option>
<option value="uk">UK</option>
</select>
`,
async test({ assert, target, window, variant }) {
assert.htmlEqual(
target.innerHTML,
`
<select>
<option${variant === 'hydrate' ? ' selected=""' : ''} value="">Select</option>
<option value="us">US</option>
<option value="uk">UK</option>
</select>
`
);
const [select] = target.querySelectorAll('select');
const options = target.querySelectorAll('option');
assert.equal(select.value, '');
const change = new window.Event('change');
// Select "UK"
options[2].selected = true;
await select.dispatchEvent(change);
assert.equal(select.value, 'uk');
}
});

@ -0,0 +1,21 @@
<script>
const default_details = {
country: '',
}
$: data = {
locked: false,
details: null,
}
$: details = data.details ?? default_details
</script>
<select
bind:value={details.country}
disabled={data.locked}
>
<option value="">Select</option>
<option value="us">US</option>
<option value="uk">UK</option>
</select>

@ -7,9 +7,8 @@ export default test({
assert.equal(options[0].selected, true);
assert.equal(options[1].selected, false);
// Shouldn't change the value because the value is not bound.
component.value = ['2'];
assert.equal(options[0].selected, true);
assert.equal(options[1].selected, false);
component.attrs = { value: ['2'] };
assert.equal(options[0].selected, false);
assert.equal(options[1].selected, true);
}
});

@ -0,0 +1,14 @@
import { tick } from 'svelte';
import { test } from '../../test';
export default test({
mode: ['async-server', 'client', 'hydrate'],
ssrHtml: 'true true true true true',
async test({ assert, target }) {
await new Promise((resolve) => setTimeout(resolve, 10));
await tick();
assert.htmlEqual(target.innerHTML, 'true true true true true');
}
});

@ -0,0 +1,41 @@
<script>
let checked = $derived(await new Promise((r) => setTimeout(() => r(true)), 10));
const checkedFactory = () => {
return () => checked;
}
function indirectCheckedFactory() {
return checkedFactory();
}
function callFactory(factory) {
return factory();
}
function indirectCallFactory() {
return callFactory(indirectCheckedFactory);
}
function indirectChecked2() {
const indirect = () => checkedFactory()();
return indirect;
}
</script>
<!-- force into separate effects -->
{#if true}
{checkedFactory()()}
{/if}
{#if true}
{indirectCheckedFactory()()}
{/if}
{#if true}
{callFactory(checkedFactory)()}
{/if}
{#if true}
{indirectCallFactory()()}
{/if}
{#if true}
{indirectChecked2()()}
{/if}

@ -0,0 +1,12 @@
import { tick } from 'svelte';
import { test } from '../../test';
export default test({
mode: ['async-server', 'hydrate', 'client'],
ssrHtml: `bar blocking`,
async test({ assert, target }) {
await tick();
assert.htmlEqual(target.innerHTML, 'bar blocking');
}
});

@ -0,0 +1,21 @@
<script>
let foo = $state(false);
let blocking = $derived(await foo);
let bar = Promise.resolve(true);
</script>
{#if foo}
foo
{:else if await bar}
bar
{:else}
else
{/if}
{#if foo}
foo
{:else if !blocking}
blocking
{:else}
else
{/if}

@ -0,0 +1,7 @@
<script>
let { x } = $props();
console.log(x);
$effect(() => console.log('$effect: '+ x))
</script>
{x}

@ -0,0 +1,47 @@
import { tick } from 'svelte';
import { test } from '../../test';
export default test({
skip: true, // this fails on main, too; skip for now
async test({ assert, target, logs }) {
const [x, y, resolve] = target.querySelectorAll('button');
x.click();
await tick();
assert.deepEqual(logs, ['universe']);
y.click();
await tick();
assert.deepEqual(logs, ['universe', 'world', '$effect: world']);
assert.htmlEqual(
target.innerHTML,
`
<button>x</button>
<button>y++</button>
<button>resolve</button>
world
`
);
resolve.click();
await tick();
assert.deepEqual(logs, [
'universe',
'world',
'$effect: world',
'$effect: universe',
'$effect: universe'
]);
assert.htmlEqual(
target.innerHTML,
`
<button>x</button>
<button>y++</button>
<button>resolve</button>
universe
universe
universe
`
);
}
});

@ -0,0 +1,28 @@
<script>
import Child from './Child.svelte';
let x = $state('world');
let y = $state(0);
let deferred = [];
function delay(s) {
const d = Promise.withResolvers();
deferred.push(() => d.resolve(s))
return d.promise;
}
</script>
<button onclick={() => x = 'universe'}>x</button>
<button onclick={() => y++}>y++</button>
<button onclick={() => deferred.shift()()}>resolve</button>
{#if x === 'universe'}
{await delay(x)}
<Child {x} />
{/if}
{#if y > 0}
<Child {x} />
{/if}

@ -0,0 +1,5 @@
<script>
$effect(() => {
throw new Error('boom');
});
</script>

@ -0,0 +1,10 @@
import { test } from '../../test';
export default test({
async test({ assert, target }) {
// allow effects to run / microtasks to flush
await Promise.resolve();
assert.htmlEqual(target.innerHTML, '<p>caught: boom</p>');
}
});

@ -0,0 +1,11 @@
<script>
import Test from './Test.svelte';
</script>
<svelte:boundary>
<Test />
{#snippet failed(e)}
<p>caught: {e.message}</p>
{/snippet}
</svelte:boundary>

@ -0,0 +1,42 @@
import { flushSync } from 'svelte';
import { test } from '../../test';
export default test({
test({ assert, target, logs }) {
const [toggle, run] = target.querySelectorAll('button');
assert.htmlEqual(
target.innerHTML,
'<button>toggle</button><button>run</button><p>hello: 0</p>'
);
flushSync(() => run.click());
assert.deepEqual(logs, []);
assert.htmlEqual(
target.innerHTML,
'<button>toggle</button><button>run</button><p>hello: 1</p>'
);
flushSync(() => toggle.click());
assert.deepEqual(logs, ['aborted']);
assert.htmlEqual(target.innerHTML, '<button>toggle</button><button>run</button>');
flushSync(() => run.click());
flushSync(() => run.click());
flushSync(() => run.click());
flushSync(() => toggle.click());
assert.htmlEqual(
target.innerHTML,
'<button>toggle</button><button>run</button><p>hello: 1</p>'
);
flushSync(() => run.click());
assert.htmlEqual(
target.innerHTML,
'<button>toggle</button><button>run</button><p>hello: 2</p>'
);
assert.deepEqual(logs, ['aborted']);
}
});

@ -0,0 +1,53 @@
<script>
import { getAbortSignal } from 'svelte';
const callbacks = new Map();
// similar semantics to setInterval, but simpler to test
function add(fn) {
const id = crypto.randomUUID();
callbacks.set(id, fn);
return id;
}
function remove(id) {
callbacks.delete(id);
}
function run() {
for (const fn of callbacks.values()) {
fn();
}
}
class Timer {
constructor(text) {
this.elapsed = $state(0);
this.text = $derived(text + ': ' + this.elapsed);
$effect(() => {
const id = add(() => {
this.elapsed += 1;
});
getAbortSignal().onabort = () => {
console.log('aborted');
};
return () => remove(id);
});
}
}
let timer = $derived(new Timer('hello'));
let visible = $state(true);
</script>
<button onclick={() => visible = !visible}>toggle</button>
<button onclick={run}>run</button>
{#if visible}
<p>{timer.text}</p>
{/if}

@ -0,0 +1,72 @@
import { tick } from 'svelte';
import { test, ok } from '../../test';
export default test({
html: `
<input type="text"/>
<input type="text"/>
<p>x / y</p>
<button>change to text</button>
<button>change to number</button>
<button>change to range</button>
`,
ssrHtml: `
<input type="text" value="x"/>
<input type="text" value="y"/>
<p>x / y</p>
<button>change to text</button>
<button>change to number</button>
<button>change to range</button>
`,
async test({ assert, target }) {
const [in1, in2] = target.querySelectorAll('input');
const [btn1, btn2, btn3] = target.querySelectorAll('button');
const p = target.querySelector('p');
ok(p);
in1.value = '0';
in2.value = '1';
in1.dispatchEvent(new window.Event('input', { bubbles: true }));
in2.dispatchEvent(new window.Event('input', { bubbles: true }));
await tick();
btn2?.click();
await tick();
assert.htmlEqual(p.innerHTML, '0 / 1');
in1.stepUp();
in1.dispatchEvent(new window.Event('input', { bubbles: true }));
in2.stepUp();
in2.dispatchEvent(new window.Event('input', { bubbles: true }));
await tick();
assert.htmlEqual(p.innerHTML, '1 / 2');
btn1?.click();
await tick();
try {
in1.stepUp();
assert.fail();
} catch (e) {
// expected
}
btn3?.click();
await tick();
in1.stepUp();
in1.dispatchEvent(new window.Event('input', { bubbles: true }));
in2.stepUp();
in2.dispatchEvent(new window.Event('input', { bubbles: true }));
await tick();
assert.htmlEqual(p.innerHTML, '2 / 3');
btn1?.click();
await tick();
in1.value = 'a';
in2.value = 'b';
in1.dispatchEvent(new window.Event('input', { bubbles: true }));
in2.dispatchEvent(new window.Event('input', { bubbles: true }));
await tick();
assert.htmlEqual(p.innerHTML, 'a / b');
}
});

@ -0,0 +1,14 @@
<script>
let dynamic = $state('x');
let spread = $state('y');
let inputType = $state('text');
let props = $derived({type: inputType});
</script>
<input bind:value={dynamic} type={inputType}>
<input bind:value={spread} {...props}>
<p>{dynamic} / {spread}</p>
<button onclick={() => inputType = 'text'}>change to text</button>
<button onclick={() => inputType = 'number'}>change to number</button>
<button onclick={() => inputType = 'range'}>change to range</button>

@ -0,0 +1,263 @@
import { test } from '../../test';
import { flushSync } from 'svelte';
export default test({
async test({ assert, target }) {
/**
* @param {NodeListOf<any>} inputs
* @param {string} field
* @param {any | any[]} value
*/
function check_inputs(inputs, field, value) {
for (let i = 0; i < inputs.length; i++) {
assert.equal(inputs[i][field], Array.isArray(value) ? value[i] : value, `field ${i}`);
}
}
/**
* @param {any} input
* @param {string} field
* @param {any} value
*/
function set_input(input, field, value) {
input[field] = value;
input.dispatchEvent(
new Event(typeof value === 'boolean' ? 'change' : 'input', { bubbles: true })
);
}
/**
* @param {HTMLOptionElement} option
*/
function select_option(option) {
option.selected = true;
option.dispatchEvent(new Event('change', { bubbles: true }));
}
const after_reset = [];
const reset = /** @type {HTMLInputElement} */ (target.querySelector('input[type=reset]'));
const [test1, test2, test3, test4, test5, test6, test7, test14] =
target.querySelectorAll('div');
const [test8, test9, test10, test11] = target.querySelectorAll('select');
const [
test1_span,
test2_span,
test3_span,
test4_span,
test5_span,
test6_span,
test7_span,
test8_span,
test9_span,
test10_span,
test11_span
] = target.querySelectorAll('span');
{
/** @type {NodeListOf<HTMLInputElement | HTMLTextAreaElement>} */
const inputs = test1.querySelectorAll('input, textarea');
check_inputs(inputs, 'value', 'x');
assert.htmlEqual(test1_span.innerHTML, 'x x x x');
for (const input of inputs) {
set_input(input, 'value', 'foo');
}
flushSync();
check_inputs(inputs, 'value', 'foo');
assert.htmlEqual(test1_span.innerHTML, 'foo foo foo foo');
after_reset.push(() => {
check_inputs(inputs, 'value', 'x');
assert.htmlEqual(test1_span.innerHTML, 'x x x x');
});
}
{
/** @type {NodeListOf<HTMLInputElement | HTMLTextAreaElement>} */
const inputs = test2.querySelectorAll('input, textarea');
check_inputs(inputs, 'value', 'x');
assert.htmlEqual(test2_span.innerHTML, 'x x x x');
for (const input of inputs) {
set_input(input, 'value', 'foo');
}
flushSync();
check_inputs(inputs, 'value', 'foo');
assert.htmlEqual(test2_span.innerHTML, 'foo foo foo foo');
after_reset.push(() => {
check_inputs(inputs, 'value', 'x');
assert.htmlEqual(test2_span.innerHTML, 'x x x x');
});
}
{
/** @type {NodeListOf<HTMLInputElement | HTMLTextAreaElement>} */
const inputs = test3.querySelectorAll('input, textarea');
check_inputs(inputs, 'value', 'y');
assert.htmlEqual(test3_span.innerHTML, 'y y y y');
for (const input of inputs) {
set_input(input, 'value', 'foo');
}
flushSync();
check_inputs(inputs, 'value', 'foo');
assert.htmlEqual(test3_span.innerHTML, 'foo foo foo foo');
after_reset.push(() => {
check_inputs(inputs, 'value', 'x');
assert.htmlEqual(test3_span.innerHTML, 'x x x x');
});
}
{
/** @type {NodeListOf<HTMLInputElement>} */
const inputs = test4.querySelectorAll('input');
check_inputs(inputs, 'checked', true);
assert.htmlEqual(test4_span.innerHTML, 'true true');
for (const input of inputs) {
set_input(input, 'checked', false);
}
flushSync();
check_inputs(inputs, 'checked', false);
assert.htmlEqual(test4_span.innerHTML, 'false false');
after_reset.push(() => {
check_inputs(inputs, 'checked', true);
assert.htmlEqual(test4_span.innerHTML, 'true true');
});
}
{
/** @type {NodeListOf<HTMLInputElement>} */
const inputs = test5.querySelectorAll('input');
check_inputs(inputs, 'checked', true);
assert.htmlEqual(test5_span.innerHTML, 'true true');
for (const input of inputs) {
set_input(input, 'checked', false);
}
flushSync();
check_inputs(inputs, 'checked', false);
assert.htmlEqual(test5_span.innerHTML, 'false false');
after_reset.push(() => {
check_inputs(inputs, 'checked', true);
assert.htmlEqual(test5_span.innerHTML, 'true true');
});
}
{
/** @type {NodeListOf<HTMLInputElement>} */
const inputs = test6.querySelectorAll('input');
check_inputs(inputs, 'checked', false);
assert.htmlEqual(test6_span.innerHTML, 'false false');
after_reset.push(() => {
check_inputs(inputs, 'checked', true);
assert.htmlEqual(test6_span.innerHTML, 'true true');
});
}
{
/** @type {NodeListOf<HTMLInputElement>} */
const inputs = test7.querySelectorAll('input');
check_inputs(inputs, 'checked', true);
assert.htmlEqual(test7_span.innerHTML, 'true');
after_reset.push(() => {
check_inputs(inputs, 'checked', false);
assert.htmlEqual(test7_span.innerHTML, 'false');
});
}
{
/** @type {NodeListOf<HTMLOptionElement>} */
const options = test8.querySelectorAll('option');
check_inputs(options, 'selected', [false, true, false]);
assert.htmlEqual(test8_span.innerHTML, 'b');
select_option(options[2]);
flushSync();
check_inputs(options, 'selected', [false, false, true]);
assert.htmlEqual(test8_span.innerHTML, 'c');
after_reset.push(() => {
check_inputs(options, 'selected', [false, true, false]);
assert.htmlEqual(test8_span.innerHTML, 'b');
});
}
{
/** @type {NodeListOf<HTMLOptionElement>} */
const options = test9.querySelectorAll('option');
check_inputs(options, 'selected', [false, true, false]);
assert.htmlEqual(test9_span.innerHTML, 'b');
select_option(options[2]);
flushSync();
check_inputs(options, 'selected', [false, false, true]);
assert.htmlEqual(test9_span.innerHTML, 'c');
after_reset.push(() => {
check_inputs(options, 'selected', [false, true, false]);
assert.htmlEqual(test9_span.innerHTML, 'b');
});
}
{
/** @type {NodeListOf<HTMLOptionElement>} */
const options = test10.querySelectorAll('option');
check_inputs(options, 'selected', [false, false, true]);
assert.htmlEqual(test10_span.innerHTML, 'c');
select_option(options[0]);
flushSync();
check_inputs(options, 'selected', [true, false, false]);
assert.htmlEqual(test10_span.innerHTML, 'a');
after_reset.push(() => {
check_inputs(options, 'selected', [false, true, false]);
assert.htmlEqual(test10_span.innerHTML, 'b');
});
}
{
/** @type {NodeListOf<HTMLOptionElement>} */
const options = test11.querySelectorAll('option');
check_inputs(options, 'selected', [false, false, true]);
assert.htmlEqual(test11_span.innerHTML, 'c');
select_option(options[0]);
flushSync();
check_inputs(options, 'selected', [true, false, false]);
assert.htmlEqual(test11_span.innerHTML, 'a');
after_reset.push(() => {
check_inputs(options, 'selected', [false, true, false]);
assert.htmlEqual(test11_span.innerHTML, 'b');
});
}
{
/** @type {NodeListOf<HTMLInputElement | HTMLTextAreaElement>} */
const inputs = test14.querySelectorAll('input, textarea');
assert.equal(inputs[0].value, 'x');
assert.equal(/** @type {HTMLInputElement} */ (inputs[1]).checked, true);
// this is still missing...i have no idea how to fix this lol
// assert.equal(inputs[2].value, 'x');
after_reset.push(() => {
assert.equal(inputs[0].value, 'y');
assert.equal(/** @type {HTMLInputElement} */ (inputs[1]).checked, false);
assert.equal(inputs[2].value, 'y');
});
}
reset.click();
await Promise.resolve();
flushSync();
after_reset.forEach((fn) => fn());
}
});

@ -0,0 +1,199 @@
<script>
let spread = {};
let value1 = $state();
let value2 = $state();
let value3 = $state();
let value4 = $state();
let value5 = $state();
let value6 = $state();
let value7 = $state();
let value8 = $state();
let value9 = $state(null);
let value10 = $state(null);
let value11 = $state(null);
let value12 = $state(null);
let value13 = $state(null);
let value14 = $state(null);
let value15 = $state(null);
let value16 = $state(null);
let value17 = $state('y');
let value18 = $state('y');
let value19 = $state('y');
let value20 = $state('y');
let value21 = $state('y');
let value22 = $state('y');
let value23 = $state('y');
let value24 = $state('y');
let checked1 = $state();
let checked2 = $state();
let checked3 = $state();
let checked4 = $state();
let checked5 = $state(null);
let checked6 = $state(null);
let checked7 = $state(null);
let checked8 = $state(null);
let checked9 = $state(false);
let checked10 = $state(false);
let checked11 = $state(false);
let checked12 = $state(false);
let checked13 = $state(true);
let checked14 = $state(true);
let selected1 = $state();
let selected2 = $state();
let selected3 = $state('c');
let selected4 = $state('c');
let selected5 = $state(['c']);
let selected6 = $state(['c']);
let defaultValue = $state('x');
let defaultChecked = $state(true);
</script>
<form>
<p>Input/Textarea value</p>
<!-- defaultValue=x, value=undefined -->
<div class="test-1">
<input {defaultValue} bind:value={value1} {...spread} />
<input {defaultValue} value={value2} {...spread} />
<input defaultValue="x" bind:value={value3} {...spread} />
<input defaultValue="x" value={value4} {...spread} />
<textarea {defaultValue} value={value5} {...spread}></textarea>
<textarea {defaultValue} bind:value={value6} {...spread}></textarea>
<textarea defaultValue="x" value={value7} {...spread}></textarea>
<textarea defaultValue="x" bind:value={value8} {...spread}></textarea>
</div>
<!-- defaultValue=x, value=null -->
<div class="test-2">
<input {defaultValue} bind:value={value9} {...spread} />
<input {defaultValue} value={value10} {...spread} />
<input defaultValue="x" value={value11} {...spread} />
<input defaultValue="x" bind:value={value12} {...spread} />
<textarea {defaultValue} value={value13} {...spread}></textarea>
<textarea {defaultValue} bind:value={value14} {...spread}></textarea>
<textarea defaultValue="x" value={value15} {...spread}></textarea>
<textarea defaultValue="x" bind:value={value16} {...spread}></textarea>
</div>
<!-- defaultValue=x, value=y -->
<div class="test-3">
<input {defaultValue} bind:value={value17} {...spread} />
<input {defaultValue} value={value18} {...spread} />
<input defaultValue="x" value={value19} {...spread} />
<input defaultValue="x" bind:value={value20} {...spread} />
<textarea {defaultValue} value={value21} {...spread}></textarea>
<textarea {defaultValue} bind:value={value22} {...spread}></textarea>
<textarea defaultValue="x" value={value23} {...spread}></textarea>
<textarea defaultValue="x" bind:value={value24} {...spread}></textarea>
</div>
<p>Input checked</p>
<!-- defaultChecked=true, checked=undefined -->
<div class="test-4">
<input type="checkbox" {defaultChecked} checked={checked1} {...spread} />
<input type="checkbox" {defaultChecked} bind:checked={checked2} {...spread} />
<input type="checkbox" defaultChecked checked={checked3} {...spread} />
<input type="checkbox" defaultChecked bind:checked={checked4} {...spread} />
</div>
<!-- defaultChecked=true, checked=null -->
<div class="test-5">
<input type="checkbox" {defaultChecked} checked={checked5} {...spread} />
<input type="checkbox" {defaultChecked} bind:checked={checked6} {...spread} />
<input type="checkbox" defaultChecked checked={checked7} {...spread} />
<input type="checkbox" defaultChecked bind:checked={checked8} {...spread} />
</div>
<!-- defaultChecked=true, checked=false -->
<div class="test-6">
<input type="checkbox" {defaultChecked} checked={checked9} {...spread} />
<input type="checkbox" {defaultChecked} bind:checked={checked10} {...spread} />
<input type="checkbox" defaultChecked checked={checked11} {...spread} />
<input type="checkbox" defaultChecked bind:checked={checked12} {...spread} />
</div>
<!-- defaultChecked=false, checked=true -->
<div class="test-7">
<input type="checkbox" defaultChecked={false} checked={checked13} {...spread} />
<input type="checkbox" defaultChecked={false} bind:checked={checked14} {...spread} />
</div>
<!-- no support for bind:group; too complex + we may deprecate it in favor of bind:checked={get,set} -->
<p>Select (single)</p>
<!-- select with static checked, value=undefined-->
<select bind:value={selected1}>
<option value="a">A</option>
<option value="b" selected {...spread}>B</option>
<option value="c">C</option>
</select>
<!-- select with dynamic checked, value=undefined-->
<select bind:value={selected2}>
<option value="a">A</option>
<option value="b" selected={defaultChecked}>B</option>
<option value="c">C</option>
</select>
<!-- select with static checked, value=something else than default-->
<select bind:value={selected3}>
<option value="a">A</option>
<option value="b" selected {...spread}>B</option>
<option value="c">C</option>
</select>
<!-- select with dynamic checked, value=something else than default-->
<select bind:value={selected4}>
<option value="a">A</option>
<option value="b" selected={defaultChecked}>B</option>
<option value="c">C</option>
</select>
<p>Select (multiple)</p>
<!-- There's no possibility to have the selected attribute influence a multi select initially,
because we require the value to be an array -->
<!-- select with static checked, value=something else than default-->
<select multiple bind:value={selected5}>
<option value="a">A</option>
<option value="b" selected {...spread}>B</option>
<option value="c">C</option>
</select>
<!-- select with dynamic checked, value=something else than default-->
<select multiple bind:value={selected6}>
<option value="a">A</option>
<option value="b" selected={defaultChecked}>B</option>
<option value="c">C</option>
</select>
<p>Static values</p>
<div class="test-14">
<input value="x" defaultValue="y" {...spread} />
<input type="checkbox" checked defaultChecked={false} {...spread} />
<textarea defaultValue="y" {...spread}>x</textarea>
</div>
<input type="reset" value="Reset" />
</form>
<p>
Bound values:
<span class="test-1">{value1} {value3} {value6} {value8}</span>
<span class="test-2">{value9} {value12} {value14} {value16}</span>
<span class="test-3">{value17} {value20} {value22} {value24}</span>
<span class="test-4">{checked2} {checked4}</span>
<span class="test-5">{checked6} {checked8}</span>
<span class="test-6">{checked10} {checked12}</span>
<span class="test-7">{checked14}</span>
<span class="test-8">{selected1}</span>
<span class="test-9">{selected2}</span>
<span class="test-10">{selected3}</span>
<span class="test-11">{selected4}</span>
<span class="test-12">{selected5}</span>
<span class="test-13">{selected6}</span>
</p>

@ -0,0 +1,21 @@
import { test } from '../../test';
export default test({
mode: ['hydrate'],
server_props: {
browser: false
},
props: {
browser: true
},
test({ assert, target }) {
assert.equal(target.querySelector('link')?.getAttribute('href'), '/bar');
},
warnings: [
'The `href` attribute on `<link xmlns="http://www.w3.org/1999/xhtml" href="/bar" />` changed its value between server and client renders. The client value, `/foo`, will be ignored in favour of the server value'
]
});

@ -0,0 +1,5 @@
<script>
let { browser } = $props();
</script>
<link href={browser ? '/foo' : '/bar'} />

@ -0,0 +1,9 @@
import { test } from '../../test';
export default test({
test({ assert, target }) {
const [input] = target.querySelectorAll('input');
assert.equal(input.disabled, true);
}
});

@ -0,0 +1,32 @@
import { test } from '../../test';
// <option value> is special because falsy values should result in an empty string value attribute
export default test({
mode: ['client'],
test({ assert, target }) {
assert.htmlEqual(
target.innerHTML,
`
<select>
<option value="">Default</option>
</select>
<select>
<option value="">Default</option>
</select>
<select>
<option value="">Default</option>
</select>
<select>
<option value="">Default</option>
</select>
<select>
<option value="">Default</option>
</select>
`
);
}
});

@ -0,0 +1,26 @@
<script>
let nonreactive = undefined;
let reactive = $state();
let nonreactive_spread = { value: undefined };
let reactive_spread = $state({ value: undefined });
</script>
<select>
<option value={undefined}>Default</option>
</select>
<select>
<option value={nonreactive}>Default</option>
</select>
<select>
<option value={reactive}>Default</option>
</select>
<select>
<option {...nonreactive_spread}>Default</option>
</select>
<select>
<option {...reactive_spread}>Default</option>
</select>

@ -0,0 +1,18 @@
import { flushSync } from 'svelte';
import { test } from '../../test';
export default test({
test({ assert, component, target }) {
const options = target.querySelectorAll('option');
assert.equal(options[0].selected, true);
assert.equal(options[1].selected, false);
// Shouldn't change the value because the value is not bound.
component.attrs = { value: ['2'] };
flushSync();
assert.equal(options[0].selected, false);
assert.equal(options[1].selected, true);
}
});

@ -0,0 +1,7 @@
<script>
import Select from './select.svelte';
let { attrs = { value: ['1'] } } = $props();
</script>
<Select {attrs} />

@ -0,0 +1,8 @@
<script>
let { attrs } = $props();
</script>
<select multiple {...attrs}>
<option value="1">option 1</option>
<option value="2">option 2</option>
</select>

@ -0,0 +1,46 @@
import { flushSync } from 'svelte';
import { ok, test } from '../../test';
export default test({
test({ assert, component, target, window }) {
const [input1, input2] = target.querySelectorAll('input');
const select = target.querySelector('select');
ok(select);
const [option1, option2] = /** @type {NodeListOf<HTMLOptionElement>} */ (select.childNodes);
let selections = Array.from(select.selectedOptions);
assert.equal(selections.length, 2);
assert.ok(selections.includes(option1));
assert.ok(selections.includes(option2));
const event = new window.Event('change');
input1.checked = false;
input1.dispatchEvent(event);
flushSync();
selections = Array.from(select.selectedOptions);
assert.equal(selections.length, 1);
assert.ok(!selections.includes(option1));
assert.ok(selections.includes(option2));
input2.checked = false;
input2.dispatchEvent(event);
flushSync();
input1.checked = true;
input1.dispatchEvent(event);
flushSync();
selections = Array.from(select.selectedOptions);
assert.equal(selections.length, 1);
assert.ok(selections.includes(option1));
assert.ok(!selections.includes(option2));
component.spread = { value: ['Hello', 'World'] };
flushSync();
selections = Array.from(select.selectedOptions);
assert.equal(selections.length, 2);
assert.ok(selections.includes(option1));
assert.ok(selections.includes(option2));
}
});

@ -0,0 +1,12 @@
<script>
let { spread } = $props();
let value = $state(['Hello', 'World']);
</script>
<select multiple {value} {...spread}>
<option>Hello</option>
<option>World</option>
</select>
<input type="checkbox" value="Hello" bind:group={value}>
<input type="checkbox" value="World" bind:group={value}>

@ -0,0 +1,3 @@
<div title="${inject} world"></div>
<div title="`backtick world"></div>
<div title="back\slash world"></div>

@ -0,0 +1,7 @@
<script>
export let value = 'world';
</script>
<div title="&#36;&#123;inject&#125; {value}"></div>
<div title="&#96;backtick {value}"></div>
<div title="back\slash {value}"></div>

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

@ -0,0 +1,201 @@
import 'svelte/internal/disclose-version';
import 'svelte/internal/flags/async';
import * as $ from 'svelte/internal/client';
var root = $.from_html(`<!> <!> <!> <!> <!>`, 1);
export default function Async_if_chain($$anchor) {
function complex1() {
return 1;
}
let foo = true;
var blocking;
var $$promises = $.run([async () => blocking = await $.async_derived(() => foo)]);
var fragment = root();
var node = $.first_child(fragment);
$.async(node, [$$promises[0]], void 0, (node) => {
var consequent = ($$anchor) => {
var text = $.text('foo');
$.append($$anchor, text);
};
var consequent_1 = ($$anchor) => {
var text_1 = $.text('bar');
$.append($$anchor, text_1);
};
var alternate = ($$anchor) => {
var text_2 = $.text('else');
$.append($$anchor, text_2);
};
$.if(node, ($$render) => {
if (foo) $$render(consequent); else if (bar) $$render(consequent_1, 1); else $$render(alternate, false);
});
});
var node_1 = $.sibling(node, 2);
$.async(node_1, [$$promises[0]], [() => foo], (node_1, $$condition) => {
var consequent_2 = ($$anchor) => {
var text_3 = $.text('foo');
$.append($$anchor, text_3);
};
var consequent_3 = ($$anchor) => {
var text_4 = $.text('bar');
$.append($$anchor, text_4);
};
var alternate_2 = ($$anchor) => {
var fragment_1 = $.comment();
var node_2 = $.first_child(fragment_1);
$.async(node_2, [], [() => baz], (node_2, $$condition) => {
var consequent_4 = ($$anchor) => {
var text_5 = $.text('baz');
$.append($$anchor, text_5);
};
var alternate_1 = ($$anchor) => {
var text_6 = $.text('else');
$.append($$anchor, text_6);
};
$.if(
node_2,
($$render) => {
if ($.get($$condition)) $$render(consequent_4); else $$render(alternate_1, false);
},
true
);
});
$.append($$anchor, fragment_1);
};
$.if(node_1, ($$render) => {
if ($.get($$condition)) $$render(consequent_2); else if (bar) $$render(consequent_3, 1); else $$render(alternate_2, false);
});
});
var node_3 = $.sibling(node_1, 2);
$.async(node_3, [$$promises[0]], [async () => (await $.save(foo))() > 10], (node_3, $$condition) => {
var consequent_5 = ($$anchor) => {
var text_7 = $.text('foo');
$.append($$anchor, text_7);
};
var consequent_6 = ($$anchor) => {
var text_8 = $.text('bar');
$.append($$anchor, text_8);
};
var alternate_4 = ($$anchor) => {
var fragment_2 = $.comment();
var node_4 = $.first_child(fragment_2);
$.async(node_4, [$$promises[0]], [async () => (await $.save(foo))() > 5], (node_4, $$condition) => {
var consequent_7 = ($$anchor) => {
var text_9 = $.text('baz');
$.append($$anchor, text_9);
};
var alternate_3 = ($$anchor) => {
var text_10 = $.text('else');
$.append($$anchor, text_10);
};
$.if(
node_4,
($$render) => {
if ($.get($$condition)) $$render(consequent_7); else $$render(alternate_3, false);
},
true
);
});
$.append($$anchor, fragment_2);
};
$.if(node_3, ($$render) => {
if ($.get($$condition)) $$render(consequent_5); else if (bar) $$render(consequent_6, 1); else $$render(alternate_4, false);
});
});
var node_5 = $.sibling(node_3, 2);
{
var consequent_8 = ($$anchor) => {
var text_11 = $.text('foo');
$.append($$anchor, text_11);
};
var consequent_9 = ($$anchor) => {
var text_12 = $.text('bar');
$.append($$anchor, text_12);
};
var consequent_10 = ($$anchor) => {
var text_13 = $.text('baz');
$.append($$anchor, text_13);
};
var d = $.derived(() => complex1() * complex2 > 100);
var alternate_5 = ($$anchor) => {
var text_14 = $.text('else');
$.append($$anchor, text_14);
};
$.if(node_5, ($$render) => {
if (simple1) $$render(consequent_8); else if (simple2 > 10) $$render(consequent_9, 1); else if ($.get(d)) $$render(consequent_10, 2); else $$render(alternate_5, false);
});
}
var node_6 = $.sibling(node_5, 2);
$.async(node_6, [$$promises[0]], void 0, (node_6) => {
var consequent_11 = ($$anchor) => {
var text_15 = $.text('foo');
$.append($$anchor, text_15);
};
var consequent_12 = ($$anchor) => {
var text_16 = $.text('bar');
$.append($$anchor, text_16);
};
var alternate_6 = ($$anchor) => {
var text_17 = $.text('else');
$.append($$anchor, text_17);
};
$.if(node_6, ($$render) => {
if ($.get(blocking) > 10) $$render(consequent_11); else if ($.get(blocking) > 5) $$render(consequent_12, 1); else $$render(alternate_6, false);
});
});
$.append($$anchor, fragment);
}

@ -0,0 +1,110 @@
import 'svelte/internal/flags/async';
import * as $ from 'svelte/internal/server';
export default function Async_if_chain($$renderer) {
function complex1() {
return 1;
}
let foo = true;
var blocking;
var $$promises = $$renderer.run([async () => blocking = await foo]);
$$renderer.async_block([$$promises[0]], ($$renderer) => {
if (foo) {
$$renderer.push('<!--[-->');
$$renderer.push(`foo`);
} else if (bar) {
$$renderer.push('<!--[1-->');
$$renderer.push(`bar`);
} else {
$$renderer.push('<!--[!-->');
$$renderer.push(`else`);
}
});
$$renderer.push(`<!--]--> `);
$$renderer.async_block([$$promises[0]], async ($$renderer) => {
if ((await $.save(foo))()) {
$$renderer.push('<!--[-->');
$$renderer.push(`foo`);
} else if (bar) {
$$renderer.push('<!--[1-->');
$$renderer.push(`bar`);
} else {
$$renderer.push('<!--[!-->');
$$renderer.child_block(async ($$renderer) => {
if ((await $.save(baz))()) {
$$renderer.push('<!--[-->');
$$renderer.push(`baz`);
} else {
$$renderer.push('<!--[!-->');
$$renderer.push(`else`);
}
});
$$renderer.push(`<!--]-->`);
}
});
$$renderer.push(`<!--]--> `);
$$renderer.async_block([$$promises[0]], async ($$renderer) => {
if ((await $.save(foo))() > 10) {
$$renderer.push('<!--[-->');
$$renderer.push(`foo`);
} else if (bar) {
$$renderer.push('<!--[1-->');
$$renderer.push(`bar`);
} else {
$$renderer.push('<!--[!-->');
$$renderer.async_block([$$promises[0]], async ($$renderer) => {
if ((await $.save(foo))() > 5) {
$$renderer.push('<!--[-->');
$$renderer.push(`baz`);
} else {
$$renderer.push('<!--[!-->');
$$renderer.push(`else`);
}
});
$$renderer.push(`<!--]-->`);
}
});
$$renderer.push(`<!--]--> `);
if (simple1) {
$$renderer.push('<!--[-->');
$$renderer.push(`foo`);
} else if (simple2 > 10) {
$$renderer.push('<!--[1-->');
$$renderer.push(`bar`);
} else if (complex1() * complex2 > 100) {
$$renderer.push('<!--[2-->');
$$renderer.push(`baz`);
} else {
$$renderer.push('<!--[!-->');
$$renderer.push(`else`);
}
$$renderer.push(`<!--]--> `);
$$renderer.async_block([$$promises[0]], ($$renderer) => {
if (blocking > 10) {
$$renderer.push('<!--[-->');
$$renderer.push(`foo`);
} else if (blocking > 5) {
$$renderer.push('<!--[1-->');
$$renderer.push(`bar`);
} else {
$$renderer.push('<!--[!-->');
$$renderer.push(`else`);
}
});
$$renderer.push(`<!--]-->`);
}

@ -0,0 +1,59 @@
<script>
function complex1() {
return 1;
}
let foo = $state(true);
let blocking = $derived(await foo);
</script>
<!-- simple chain - should have no nested $.if() -->
{#if foo}
foo
{:else if bar}
bar
{:else}
else
{/if}
<!-- simple chain with await expressions - should have $.if() at each await expression -->
{#if await foo}
foo
{:else if bar}
bar
{:else if await baz}
baz
{:else}
else
{/if}
<!-- simple chain with await expressions #2 - should have $.if() at each await expression (ideally we can detect that await foo is unnecessary to await multiple times and this is one $.if()) -->
{#if await foo > 10}
foo
{:else if bar}
bar
{:else if await foo > 5}
baz
{:else}
else
{/if}
<!-- simple chain with some expressions that cause a $.derived - should be one $.if() -->
{#if simple1}
foo
{:else if simple2 > 10}
bar
{:else if complex1() * complex2 > 100}
baz
{:else}
else
{/if}
<!-- simple chain with blocking expressions - should be one $.if() -->
{#if blocking > 10}
foo
{:else if blocking > 5}
bar
{:else}
else
{/if}

@ -889,7 +889,7 @@ declare module 'svelte/compiler' {
*
* @param source The CSS source code
* */
export function parseCss(source: string): Omit<AST.CSS.StyleSheet, "attributes" | "content">;
export function parseCss(source: string): AST.CSS.StyleSheetFile;
/**
* @deprecated Replace this with `import { walk } from 'estree-walker'`
* */
@ -1673,10 +1673,17 @@ declare module 'svelte/compiler' {
end: number;
}
export interface StyleSheet extends BaseNode {
export interface StyleSheetBase extends BaseNode {
children: Array<Atrule | Rule>;
}
export interface StyleSheetFile extends StyleSheetBase {
type: 'StyleSheetFile';
}
export interface StyleSheet extends StyleSheetBase {
type: 'StyleSheet';
attributes: any[]; // TODO
children: Array<Atrule | Rule>;
content: {
start: number;
end: number;

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