Merge branch 'main' into comments-in-tags

pull/17671/head
Rich Harris 5 months ago committed by GitHub
commit 7d324e0c80
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -1,5 +0,0 @@
---
'svelte': patch
---
fix: don't swallow `DOMException` when `media.play()` fails in `bind:paused`

@ -1,5 +0,0 @@
---
'svelte': patch
---
fix: reduce if block nesting

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

@ -62,6 +62,14 @@ Keyed each block has duplicate key at indexes %a% and %b%
Keyed each block has duplicate key `%value%` at indexes %a% and %b%
```
### each_key_volatile
```
Keyed each block has key that is not idempotent — the key for item at index %index% was `%a%` but is now `%b%`. Keys must be the same each time for a given item
```
The key expression in a keyed each block must return the same value when called multiple times for the same item. Using expressions like `[item.a, item.b]` creates a new array each time, which will never be equal to itself. Instead, use a primitive value or create a stable key like `item.a + '-' + item.b`.
### effect_in_teardown
```

@ -16,6 +16,14 @@ Encountered asynchronous work while rendering synchronously.
You (or the framework you're using) called [`render(...)`](svelte-server#render) with a component containing an `await` expression. Either `await` the result of `render` or wrap the `await` (or the component containing it) in a [`<svelte:boundary>`](svelte-boundary) with a `pending` snippet.
### dynamic_element_invalid_tag
```
`<svelte:element this="%tag%">` is not a valid element name — the element will not be rendered
```
The value passed to the `this` prop of `<svelte:element>` must be a valid HTML element, SVG element, MathML element, or custom element name. A value containing invalid characters (such as whitespace or special characters) was provided, which could be a security risk. Ensure only valid tag names are passed.
### html_deprecated
```

@ -1,5 +1,99 @@
# svelte
## 5.51.5
### Patch Changes
- fix: check to make sure `svelte:element` tags are valid during SSR ([`73098bb26c6f06e7fd1b0746d817d2c5ee90755f`](https://github.com/sveltejs/svelte/commit/73098bb26c6f06e7fd1b0746d817d2c5ee90755f))
- fix: misc option escaping and backwards compatibility ([#17741](https://github.com/sveltejs/svelte/pull/17741))
- fix: strip event handlers during SSR ([`a0c7f289156e9fafaeaf5ca14af6c06fe9b9eae5`](https://github.com/sveltejs/svelte/commit/a0c7f289156e9fafaeaf5ca14af6c06fe9b9eae5))
- fix: replace usage of `for in` with `for of Object.keys` ([`f89c7ddd7eebaa1ef3cc540400bec2c9140b330c`](https://github.com/sveltejs/svelte/commit/f89c7ddd7eebaa1ef3cc540400bec2c9140b330c))
- fix: always escape option body in SSR ([`f7c80da18c215e3727c2a611b0b8744cc6e504c5`](https://github.com/sveltejs/svelte/commit/f7c80da18c215e3727c2a611b0b8744cc6e504c5))
- chore: upgrade `devalue` ([#17739](https://github.com/sveltejs/svelte/pull/17739))
## 5.51.4
### Patch Changes
- chore: proactively defer effects in pending boundary ([#17734](https://github.com/sveltejs/svelte/pull/17734))
- fix: detect and error on non-idempotent each block keys in dev mode ([#17732](https://github.com/sveltejs/svelte/pull/17732))
## 5.51.3
### Patch Changes
- fix: prevent event delegation logic conflicting between svelte instances ([#17728](https://github.com/sveltejs/svelte/pull/17728))
- fix: treat CSS attribute selectors as case-insensitive for HTML enumerated attributes ([#17712](https://github.com/sveltejs/svelte/pull/17712))
- fix: locate Rollup annontaion friendly to JS downgraders ([#17724](https://github.com/sveltejs/svelte/pull/17724))
- fix: run effects in pending snippets ([#17719](https://github.com/sveltejs/svelte/pull/17719))
## 5.51.2
### Patch Changes
- fix: take async into consideration for dev delegated handlers ([#17710](https://github.com/sveltejs/svelte/pull/17710))
- fix: emit state_referenced_locally warning for non-destructured props ([#17708](https://github.com/sveltejs/svelte/pull/17708))
## 5.51.1
### Patch Changes
- fix: don't crash on undefined `document.contentType` ([#17707](https://github.com/sveltejs/svelte/pull/17707))
- fix: use symbols for encapsulated event delegation ([#17703](https://github.com/sveltejs/svelte/pull/17703))
## 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

@ -42,6 +42,12 @@ See the [migration guide](/docs/svelte/v5-migration-guide#Components-are-no-long
> Keyed each block has duplicate key `%value%` at indexes %a% and %b%
## each_key_volatile
> Keyed each block has key that is not idempotent — the key for item at index %index% was `%a%` but is now `%b%`. Keys must be the same each time for a given item
The key expression in a keyed each block must return the same value when called multiple times for the same item. Using expressions like `[item.a, item.b]` creates a new array each time, which will never be equal to itself. Instead, use a primitive value or create a stable key like `item.a + '-' + item.b`.
## effect_in_teardown
> `%rune%` cannot be used inside an effect cleanup function

@ -10,6 +10,12 @@ Some platforms require configuration flags to enable this API. Consult your plat
You (or the framework you're using) called [`render(...)`](svelte-server#render) with a component containing an `await` expression. Either `await` the result of `render` or wrap the `await` (or the component containing it) in a [`<svelte:boundary>`](svelte-boundary) with a `pending` snippet.
## dynamic_element_invalid_tag
> `<svelte:element this="%tag%">` is not a valid element name — the element will not be rendered
The value passed to the `this` prop of `<svelte:element>` must be a valid HTML element, SVG element, MathML element, or custom element name. A value containing invalid characters (such as whitespace or special characters) was provided, which could be a security risk. Ensure only valid tag names are passed.
## html_deprecated
> The `html` property of server render results has been deprecated. Use `body` instead.

@ -2,7 +2,7 @@
"name": "svelte",
"description": "Cybernetically enhanced web apps",
"license": "MIT",
"version": "5.50.1",
"version": "5.51.5",
"type": "module",
"types": "./types/index.d.ts",
"engines": {
@ -170,11 +170,12 @@
"@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",
"clsx": "^2.1.1",
"devalue": "^5.6.2",
"devalue": "^5.6.3",
"esm-env": "^1.2.1",
"esrap": "^2.2.2",
"is-reference": "^3.0.3",

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

@ -2,7 +2,7 @@
/** @import { Location } from 'locate-character' */
/** @import { AST } from '#compiler' */
/** @import { Parser } from '../index.js' */
import { is_void } from '../../../../utils.js';
import { is_void, REGEX_VALID_TAG_NAME } from '../../../../utils.js';
import read_expression from '../read/expression.js';
import { read_script } from '../read/script.js';
import read_style from '../read/style.js';
@ -24,8 +24,15 @@ const regex_whitespace_or_slash_or_closing_tag = /(\s|\/|>)/;
const regex_token_ending_character = /[\s=/>"']/;
const regex_starts_with_quote_characters = /^["']/;
const regex_attribute_value = /^(?:"([^"]*)"|'([^'])*'|([^>\s]+))/;
const regex_valid_element_name =
/^(?:![a-zA-Z]+|[a-zA-Z](?:[a-zA-Z0-9-]*[a-zA-Z0-9])?|[a-zA-Z][a-zA-Z0-9]*:[a-zA-Z][a-zA-Z0-9-]*[a-zA-Z0-9])$/;
/** @param {string} name */
function is_valid_element_name(name) {
// DOCTYPE (e.g. !DOCTYPE)
if (/^![a-zA-Z]+$/.test(name)) return true;
// svelte:* meta tags (e.g. svelte:element, svelte:head)
if (/^[a-zA-Z][a-zA-Z0-9]*:[a-zA-Z][a-zA-Z0-9-]*[a-zA-Z0-9]$/.test(name)) return true;
// standard HTML/SVG/MathML elements and custom elements
return REGEX_VALID_TAG_NAME.test(name);
}
export const regex_valid_component_name =
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Lexical_grammar#identifiers adjusted for our needs
// (must start with uppercase letter if no dots, can contain dots)
@ -134,7 +141,7 @@ export default function element(parser) {
e.svelte_meta_invalid_tag(bounds, list(Array.from(meta_tags.keys())));
}
if (!regex_valid_element_name.test(tag.name) && !regex_valid_component_name.test(tag.name)) {
if (!is_valid_element_name(tag.name) && !regex_valid_component_name.test(tag.name)) {
// <div. -> in the middle of typing -> allow in loose mode
if (!parser.loose || !tag.name.endsWith('.')) {
const bounds = { start: start + 1, end: start + 1 + tag.name.length };

@ -22,6 +22,50 @@ const whitelist_attribute_selector = new Map([
['dialog', ['open']]
]);
/**
* HTML attributes whose enumerated values are case-insensitive per the HTML spec.
* CSS attribute selectors match these values case-insensitively in HTML documents.
* @see {@link https://html.spec.whatwg.org/multipage/semantics-other.html#case-sensitivity-of-selectors HTML spec}
*/
const case_insensitive_attributes = new Set([
'accept-charset',
'autocapitalize',
'autocomplete',
'behavior',
'charset',
'crossorigin',
'decoding',
'dir',
'direction',
'draggable',
'enctype',
'enterkeyhint',
'fetchpriority',
'formenctype',
'formmethod',
'formtarget',
'hidden',
'http-equiv',
'inputmode',
'kind',
'loading',
'method',
'preload',
'referrerpolicy',
'rel',
'rev',
'role',
'rules',
'scope',
'shape',
'spellcheck',
'target',
'translate',
'type',
'valign',
'wrap'
]);
/** @type {Compiler.AST.CSS.Combinator} */
const descendant_combinator = {
type: 'Combinator',
@ -523,7 +567,9 @@ function relative_selector_might_apply_to_node(relative_selector, rule, element,
selector.name,
selector.value && unquote(selector.value),
selector.matcher,
selector.flags?.includes('i') ?? false
(selector.flags?.includes('i') ?? false) ||
(!selector.flags?.includes('s') &&
case_insensitive_attributes.has(selector.name.toLowerCase()))
)
) {
return false;

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

@ -115,7 +115,8 @@ export function Identifier(node, context) {
!should_proxy(binding.initial.arguments[0], context.state.scope)))) ||
binding.kind === 'raw_state' ||
binding.kind === 'derived' ||
binding.kind === 'prop') &&
binding.kind === 'prop' ||
binding.kind === 'rest_prop') &&
// We're only concerned with reads here
(parent.type !== 'AssignmentExpression' || parent.left !== node) &&
parent.type !== 'UpdateExpression'

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

@ -34,5 +34,5 @@ export function OnDirective(node, context) {
node.modifiers.includes('passive') ||
(node.modifiers.includes('nonpassive') ? false : undefined);
return build_event(node.name, context.state.node, handler, capture, passive);
return build_event(context, node.name, handler, capture, passive, false);
}

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

@ -27,55 +27,59 @@ export function visit_event_attribute(node, context) {
let handler = build_event_handler(tag.expression, tag.metadata.expression, context);
if (node.metadata.delegated) {
if (!context.state.events.has(event_name)) {
context.state.events.add(event_name);
}
context.state.events.add(event_name);
}
context.state.init.push(
b.stmt(
b.assignment(
'=',
b.member(context.state.node, b.id('__' + event_name, node.name_loc)),
handler
)
)
);
} else {
const statement = b.stmt(
build_event(
event_name,
context.state.node,
handler,
capture,
is_passive_event(event_name) ? true : undefined
)
);
const statement = b.stmt(
build_event(
context,
event_name,
handler,
capture,
is_passive_event(event_name) ? true : undefined,
node.metadata.delegated
)
);
const type = /** @type {AST.SvelteNode} */ (context.path.at(-1)).type;
const type = /** @type {AST.SvelteNode} */ (context.path.at(-1)).type;
if (type === 'SvelteDocument' || type === 'SvelteWindow' || type === 'SvelteBody') {
// These nodes are above the component tree, and its events should run parent first
context.state.init.push(statement);
} else {
context.state.after_update.push(statement);
}
if (type === 'SvelteDocument' || type === 'SvelteWindow' || type === 'SvelteBody') {
// These nodes are above the component tree, and its events should run parent first
context.state.init.push(statement);
} else {
context.state.after_update.push(statement);
}
}
/**
* Creates a `$.event(...)` call for non-delegated event handlers
* @param {ComponentContext} context
* @param {string} event_name
* @param {Expression} node
* @param {Expression} handler
* @param {boolean} capture
* @param {boolean | undefined} passive
* @param {boolean | undefined} delegated
*/
export function build_event(event_name, node, handler, capture, passive) {
export function build_event(context, event_name, handler, capture, passive, delegated) {
let fn = handler;
if (dev && handler.type === 'ArrowFunctionExpression') {
// create a named function for better debugging
const name = context.state.scope.generate(event_name);
fn = b.function(
b.id(name),
handler.params,
handler.body.type === 'BlockStatement' ? handler.body : b.block([b.return(handler.body)]),
handler.async
);
}
return b.call(
'$.event',
delegated ? '$.delegated' : '$.event',
b.literal(event_name),
node,
handler,
context.state.node,
fn,
capture && b.true,
passive === undefined ? undefined : b.literal(passive)
);

@ -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)}"`)
);
}

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

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

@ -414,9 +414,23 @@ const svelte_visitors = (comments) => ({
}
}
} 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;

@ -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,10 @@ export const STALE_REACTION = new (class StaleReactionError extends Error {
message = 'The reaction that called `getAbortSignal()` was re-run or destroyed';
})();
export const IS_XHTML =
// We gotta write it like this because after downleveling the pure comment may end up in the wrong location
!!globalThis.document?.contentType &&
/* @__PURE__ */ globalThis.document.contentType.includes('xml');
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;

@ -1,6 +1,5 @@
/** @import { Blocker, TemplateNode, Value } from '#client' */
import { flatten } from '../../reactivity/async.js';
import { Batch, current_batch } from '../../reactivity/batch.js';
import { flatten, increment_pending } from '../../reactivity/async.js';
import { get } from '../../runtime.js';
import {
hydrate_next,
@ -10,7 +9,6 @@ import {
set_hydrating,
skip_nodes
} from '../hydration.js';
import { get_boundary } from './boundary.js';
/**
* @param {TemplateNode} node
@ -44,12 +42,7 @@ export function async(node, blockers = [], expressions = [], fn) {
return;
}
var boundary = get_boundary();
var batch = /** @type {Batch} */ (current_batch);
var blocking = boundary.is_rendered();
boundary.update_pending_count(1);
batch.increment(blocking);
const decrement_pending = increment_pending();
if (was_hydrating) {
var previous_hydrate_node = hydrate_node;
@ -72,8 +65,7 @@ export function async(node, blockers = [], expressions = [], fn) {
set_hydrating(false);
}
boundary.update_pending_count(-1);
batch.decrement(blocking);
decrement_pending();
}
});
}

@ -1,8 +1,6 @@
/** @import { Effect, Source, TemplateNode, } from '#client' */
import {
BLOCK_EFFECT,
BOUNDARY_EFFECT,
COMMENT_NODE,
DIRTY,
EFFECT_PRESERVED,
EFFECT_TRANSPARENT,
@ -53,7 +51,7 @@ import { set_signal_status } from '../../reactivity/status.js';
* }} BoundaryProps
*/
var flags = EFFECT_TRANSPARENT | EFFECT_PRESERVED | BOUNDARY_EFFECT;
var flags = EFFECT_TRANSPARENT | EFFECT_PRESERVED;
/**
* @param {TemplateNode} node
@ -98,15 +96,10 @@ export class Boundary {
/** @type {DocumentFragment | null} */
#offscreen_fragment = null;
/** @type {TemplateNode | null} */
#pending_anchor = null;
#local_pending_count = 0;
#pending_count = 0;
#pending_count_update_queued = false;
#is_creating_fallback = false;
/** @type {Set<Effect>} */
#dirty_effects = new Set();
@ -142,51 +135,31 @@ export class Boundary {
constructor(node, props, children) {
this.#anchor = node;
this.#props = props;
this.#children = children;
this.parent = /** @type {Effect} */ (active_effect).b;
this.#children = (anchor) => {
var effect = /** @type {Effect} */ (active_effect);
this.is_pending = !!this.#props.pending;
effect.b = this;
effect.f |= BOUNDARY_EFFECT;
this.#effect = block(() => {
/** @type {Effect} */ (active_effect).b = this;
children(anchor);
};
this.parent = /** @type {Effect} */ (active_effect).b;
this.#effect = block(() => {
if (hydrating) {
const comment = this.#hydrate_open;
const comment = /** @type {Comment} */ (this.#hydrate_open);
hydrate_next();
const server_rendered_pending =
/** @type {Comment} */ (comment).nodeType === COMMENT_NODE &&
/** @type {Comment} */ (comment).data === HYDRATION_START_ELSE;
if (server_rendered_pending) {
if (comment.data === HYDRATION_START_ELSE) {
this.#hydrate_pending_content();
} else {
this.#hydrate_resolved_content();
if (this.#pending_count === 0) {
this.is_pending = false;
}
}
} else {
var anchor = this.#get_anchor();
try {
this.#main_effect = branch(() => children(anchor));
} catch (error) {
this.error(error);
}
if (this.#pending_count > 0) {
this.#show_pending_snippet();
} else {
this.is_pending = false;
}
this.#render();
}
return () => {
this.#pending_anchor?.remove();
};
}, flags);
if (hydrating) {
@ -206,39 +179,75 @@ export class Boundary {
const pending = this.#props.pending;
if (!pending) return;
this.is_pending = true;
this.#pending_effect = branch(() => pending(this.#anchor));
queue_micro_task(() => {
var anchor = this.#get_anchor();
var fragment = (this.#offscreen_fragment = document.createDocumentFragment());
var anchor = create_text();
fragment.append(anchor);
this.#main_effect = this.#run(() => {
Batch.ensure();
return branch(() => this.#children(anchor));
});
if (this.#pending_count > 0) {
this.#show_pending_snippet();
} else {
if (this.#pending_count === 0) {
this.#anchor.before(fragment);
this.#offscreen_fragment = null;
pause_effect(/** @type {Effect} */ (this.#pending_effect), () => {
this.#pending_effect = null;
});
this.is_pending = false;
this.#resolve();
}
});
}
#get_anchor() {
var anchor = this.#anchor;
#render() {
try {
this.is_pending = this.has_pending_snippet();
this.#pending_count = 0;
this.#local_pending_count = 0;
this.#main_effect = branch(() => {
this.#children(this.#anchor);
});
if (this.is_pending) {
this.#pending_anchor = create_text();
this.#anchor.before(this.#pending_anchor);
if (this.#pending_count > 0) {
var fragment = (this.#offscreen_fragment = document.createDocumentFragment());
move_effect(this.#main_effect, fragment);
anchor = this.#pending_anchor;
const pending = /** @type {(anchor: Node) => void} */ (this.#props.pending);
this.#pending_effect = branch(() => pending(this.#anchor));
} else {
this.#resolve();
}
} catch (error) {
this.error(error);
}
}
#resolve() {
this.is_pending = false;
// any effects that were previously deferred should be rescheduled —
// after the next traversal (which will happen immediately, due to the
// same update that brought us here) the effects will be flushed
for (const e of this.#dirty_effects) {
set_signal_status(e, DIRTY);
schedule_effect(e);
}
for (const e of this.#maybe_dirty_effects) {
set_signal_status(e, MAYBE_DIRTY);
schedule_effect(e);
}
return anchor;
this.#dirty_effects.clear();
this.#maybe_dirty_effects.clear();
}
/**
@ -262,7 +271,8 @@ export class Boundary {
}
/**
* @param {() => Effect | null} fn
* @template T
* @param {() => T} fn
*/
#run(fn) {
var previous_effect = active_effect;
@ -285,20 +295,6 @@ export class Boundary {
}
}
#show_pending_snippet() {
const pending = /** @type {(anchor: Node) => void} */ (this.#props.pending);
if (this.#main_effect !== null) {
this.#offscreen_fragment = document.createDocumentFragment();
this.#offscreen_fragment.append(/** @type {TemplateNode} */ (this.#pending_anchor));
move_effect(this.#main_effect, this.#offscreen_fragment);
}
if (this.#pending_effect === null) {
this.#pending_effect = branch(() => pending(this.#anchor));
}
}
/**
* Updates the pending count associated with the currently visible pending snippet,
* if any, such that we can replace the snippet with content once work is done
@ -317,24 +313,7 @@ export class Boundary {
this.#pending_count += d;
if (this.#pending_count === 0) {
this.is_pending = false;
// any effects that were encountered and deferred during traversal
// should be rescheduled — after the next traversal (which will happen
// immediately, due to the same update that brought us here)
// the effects will be flushed
for (const e of this.#dirty_effects) {
set_signal_status(e, DIRTY);
schedule_effect(e);
}
for (const e of this.#maybe_dirty_effects) {
set_signal_status(e, MAYBE_DIRTY);
schedule_effect(e);
}
this.#dirty_effects.clear();
this.#maybe_dirty_effects.clear();
this.#resolve();
if (this.#pending_effect) {
pause_effect(this.#pending_effect, () => {
@ -383,7 +362,7 @@ export class Boundary {
// If we have nothing to capture the error, or if we hit an error while
// rendering the fallback, re-throw for another boundary to handle
if (this.#is_creating_fallback || (!onerror && !failed)) {
if (!onerror && !failed) {
throw error;
}
@ -423,31 +402,18 @@ export class Boundary {
e.svelte_boundary_reset_onerror();
}
// If the failure happened while flushing effects, current_batch can be null
Batch.ensure();
this.#local_pending_count = 0;
if (this.#failed_effect !== null) {
pause_effect(this.#failed_effect, () => {
this.#failed_effect = null;
});
}
// we intentionally do not try to find the nearest pending boundary. If this boundary has one, we'll render it on reset
// but it would be really weird to show the parent's boundary on a child reset.
this.is_pending = this.has_pending_snippet();
this.#run(() => {
// If the failure happened while flushing effects, current_batch can be null
Batch.ensure();
this.#main_effect = this.#run(() => {
this.#is_creating_fallback = false;
return branch(() => this.#children(this.#anchor));
this.#render();
});
if (this.#pending_count > 0) {
this.#show_pending_snippet();
} else {
this.is_pending = false;
}
};
queue_micro_task(() => {
@ -462,10 +428,16 @@ export class Boundary {
if (failed) {
this.#failed_effect = this.#run(() => {
Batch.ensure();
this.#is_creating_fallback = true;
try {
return branch(() => {
// errors in `failed` snippets cause the boundary to error again
// TODO Svelte 6: revisit this decision, most likely better to go to parent boundary instead
var effect = /** @type {Effect} */ (active_effect);
effect.b = this;
effect.f |= BOUNDARY_EFFECT;
failed(
this.#anchor,
() => error,
@ -475,8 +447,6 @@ export class Boundary {
} catch (error) {
invoke_error_boundary(error, /** @type {Effect} */ (this.#effect.parent));
return null;
} finally {
this.#is_creating_fallback = false;
}
});
}
@ -484,10 +454,6 @@ export class Boundary {
}
}
export function get_boundary() {
return /** @type {Boundary} */ (/** @type {Effect} */ (active_effect).b);
}
export function pending() {
if (active_effect === null) {
e.effect_pending_outside_reaction();

@ -250,6 +250,14 @@ export function each(node, flags, get_collection, get_key, render_fn, fallback_f
var value = array[index];
var key = get_key(value, index);
if (DEV) {
// Check that the key function is idempotent (returns the same value when called twice)
var key_again = get_key(value, index);
if (key !== key_again) {
e.each_key_volatile(String(index), String(key), String(key_again));
}
}
var item = first_run ? null : items.get(key);
if (item) {

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

@ -2,10 +2,10 @@
import { DEV } from 'esm-env';
import { hydrating, set_hydrating } from '../hydration.js';
import { get_descriptors, get_prototype_of } from '../../../shared/utils.js';
import { create_event, delegate } from './events.js';
import { create_event, delegate, delegated, event, event_symbol } 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)) {
@ -378,14 +384,14 @@ function set_attributes(
const opts = {};
const event_handle_key = '$$' + key;
let event_name = key.slice(2);
var delegated = can_delegate_event(event_name);
var is_delegated = can_delegate_event(event_name);
if (is_capture_event(event_name)) {
event_name = event_name.slice(0, -7);
opts.capture = true;
}
if (!delegated && prev_value) {
if (!is_delegated && prev_value) {
// Listening to same event but different handler -> our handle function below takes care of this
// If we were to remove and add listeners in this case, it could happen that the event is "swallowed"
// (the browser seems to not know yet that a new one exists now) and doesn't reach the handler
@ -396,25 +402,19 @@ function set_attributes(
current[event_handle_key] = null;
}
if (value != null) {
if (!delegated) {
/**
* @this {any}
* @param {Event} evt
*/
function handle(evt) {
current[key].call(this, evt);
}
current[event_handle_key] = create_event(event_name, element, handle, opts);
} else {
// @ts-ignore
element[`__${event_name}`] = value;
delegate([event_name]);
if (is_delegated) {
delegated(event_name, element, value);
delegate([event_name]);
} else if (value != null) {
/**
* @this {any}
* @param {Event} evt
*/
function handle(evt) {
current[key].call(this, evt);
}
} else if (delegated) {
// @ts-ignore
element[`__${event_name}`] = undefined;
current[event_handle_key] = create_event(event_name, element, handle, opts);
}
} else if (key === 'style') {
// avoid using the setter
@ -505,7 +505,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(() => {

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

@ -12,6 +12,12 @@ import {
} from '../../runtime.js';
import { without_reactive_context } from './bindings/shared.js';
/**
* Used on elements, as a map of event type -> event handler,
* and on events themselves to track which element handled an event
*/
export const event_symbol = Symbol('events');
/** @type {Set<string>} */
export const all_registered_events = new Set();
@ -127,6 +133,17 @@ export function event(event_name, dom, handler, capture, passive) {
}
}
/**
* @param {string} event_name
* @param {Element} element
* @param {EventListener} [handler]
* @returns {void}
*/
export function delegated(event_name, element, handler) {
// @ts-expect-error
(element[event_symbol] ??= {})[event_name] = handler;
}
/**
* @param {Array<string>} events
* @returns {void}
@ -163,8 +180,8 @@ export function handle_event_propagation(event) {
last_propagated_event = event;
// composedPath contains list of nodes the event has propagated through.
// We check __root to skip all nodes below it in case this is a
// parent of the __root node, which indicates that there's nested
// We check `event_symbol` to skip all nodes below it in case this is a
// parent of the `event_symbol` node, which indicates that there's nested
// mounted apps. In this case we don't want to trigger events multiple times.
var path_idx = 0;
@ -172,7 +189,7 @@ export function handle_event_propagation(event) {
// without it the variable will be DCE'd and things will
// fail mysteriously in Firefox
// @ts-expect-error is added below
var handled_at = last_propagated_event === event && event.__root;
var handled_at = last_propagated_event === event && event[event_symbol];
if (handled_at) {
var at_idx = path.indexOf(handled_at);
@ -184,7 +201,7 @@ export function handle_event_propagation(event) {
// -> ignore, but set handle_at to document/window so that we're resetting the event
// chain in case someone manually dispatches the same event object again.
// @ts-expect-error
event.__root = handler_element;
event[event_symbol] = handler_element;
return;
}
@ -249,7 +266,7 @@ export function handle_event_propagation(event) {
try {
// @ts-expect-error
var delegated = current_target['__' + event_name];
var delegated = current_target[event_symbol]?.[event_name];
if (
delegated != null &&
@ -284,7 +301,7 @@ export function handle_event_propagation(event) {
}
} finally {
// @ts-expect-error is used above
event.__root = handler_element;
event[event_symbol] = handler_element;
// @ts-ignore remove proxy on currentTarget
delete event.currentTarget;
set_active_reaction(previous_reaction);

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

@ -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 =
// We gotta write it like this because after downleveling the pure comment may end up in the wrong location
globalThis?.window?.trustedTypes &&
/* @__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;

@ -147,6 +147,25 @@ export function each_key_duplicate(a, b, value) {
}
}
/**
* Keyed each block has key that is not idempotent the key for item at index %index% was `%a%` but is now `%b%`. Keys must be the same each time for a given item
* @param {string} index
* @param {string} a
* @param {string} b
* @returns {never}
*/
export function each_key_volatile(index, a, b) {
if (DEV) {
const error = new Error(`each_key_volatile\nKeyed each block has key that is not idempotent — the key for item at index ${index} was \`${a}\` but is now \`${b}\`. Keys must be the same each time for a given item\nhttps://svelte.dev/e/each_key_volatile`);
error.name = 'Svelte error';
throw error;
} else {
throw new Error(`https://svelte.dev/e/each_key_volatile`);
}
}
/**
* `%rune%` cannot be used inside an effect cleanup function
* @param {string} rune

@ -40,7 +40,7 @@ export {
STYLE
} from './dom/elements/attributes.js';
export { set_class } from './dom/elements/class.js';
export { apply, event, delegate, replay_events } from './dom/elements/events.js';
export { apply, event, delegated, delegate, replay_events } from './dom/elements/events.js';
export { autofocus, remove_textarea_child } from './dom/elements/misc.js';
export { customizable_select, selectedcontent } from './dom/elements/customizable-select.js';
export { set_style } from './dom/elements/style.js';

@ -8,7 +8,7 @@ import {
set_component_context,
set_dev_stack
} from '../context.js';
import { get_boundary } from '../dom/blocks/boundary.js';
import { Boundary } from '../dom/blocks/boundary.js';
import { invoke_error_boundary } from '../error-handling.js';
import {
active_effect,
@ -224,12 +224,7 @@ export function unset_context() {
export function run(thunks) {
const restore = capture();
var boundary = get_boundary();
var batch = /** @type {Batch} */ (current_batch);
var blocking = boundary.is_rendered();
boundary.update_pending_count(1);
batch.increment(blocking);
const decrement_pending = increment_pending();
var active = /** @type {Effect} */ (active_effect);
@ -286,10 +281,7 @@ export function run(thunks) {
// wait one more tick, so that template effects are
// guaranteed to run before `$effect(...)`
.then(() => Promise.resolve())
.finally(() => {
boundary.update_pending_count(-1);
batch.decrement(blocking);
});
.finally(decrement_pending);
return blockers;
}
@ -300,3 +292,17 @@ export function run(thunks) {
export function wait(blockers) {
return Promise.all(blockers.map((b) => b.promise));
}
export function increment_pending() {
var boundary = /** @type {Boundary} */ (/** @type {Effect} */ (active_effect).b);
var batch = /** @type {Batch} */ (current_batch);
var blocking = boundary.is_rendered();
boundary.update_pending_count(1);
batch.increment(blocking);
return () => {
boundary.update_pending_count(-1);
batch.decrement(blocking);
};
}

@ -18,7 +18,8 @@ import {
EAGER_EFFECT,
HEAD_EFFECT,
ERROR_VALUE,
MANAGED_EFFECT
MANAGED_EFFECT,
REACTION_RAN
} from '#client/constants';
import { async_mode_flag } from '../../flags/index.js';
import { deferred, define_property, includes } from '../../shared/utils.js';
@ -142,7 +143,7 @@ export class Batch {
#decrement_queued = false;
is_deferred() {
#is_deferred() {
return this.is_fork || this.#blocking_pending > 0;
}
@ -202,7 +203,7 @@ export class Batch {
// log_inconsistent_branches(root);
}
if (this.is_deferred()) {
if (this.#is_deferred()) {
this.#defer_effects(render_effects);
this.#defer_effects(effects);
@ -246,9 +247,6 @@ export class Batch {
var effect = root.first;
/** @type {Effect | null} */
var pending_boundary = null;
while (effect !== null) {
var flags = effect.f;
var is_branch = (flags & (BRANCH_EFFECT | ROOT_EFFECT)) !== 0;
@ -256,26 +254,9 @@ export class Batch {
var skip = is_skippable_branch || (flags & INERT) !== 0 || this.#skipped_branches.has(effect);
// Inside a `<svelte:boundary>` with a pending snippet,
// all effects are deferred until the boundary resolves
// (except block/async effects, which run immediately)
if (
async_mode_flag &&
pending_boundary === null &&
(flags & BOUNDARY_EFFECT) !== 0 &&
effect.b?.is_pending
) {
pending_boundary = effect;
}
if (!skip && effect.fn !== null) {
if (is_branch) {
effect.f ^= CLEAN;
} else if (
pending_boundary !== null &&
(flags & (EFFECT | RENDER_EFFECT | MANAGED_EFFECT)) !== 0
) {
/** @type {Boundary} */ (pending_boundary.b).defer_effect(effect);
} else if ((flags & EFFECT) !== 0) {
effects.push(effect);
} else if (async_mode_flag && (flags & (RENDER_EFFECT | MANAGED_EFFECT)) !== 0) {
@ -293,16 +274,15 @@ export class Batch {
}
}
var parent = effect.parent;
effect = effect.next;
while (effect !== null) {
var next = effect.next;
while (effect === null && parent !== null) {
if (parent === pending_boundary) {
pending_boundary = null;
if (next !== null) {
effect = next;
break;
}
effect = parent.next;
parent = parent.parent;
effect = effect.parent;
}
}
}
@ -472,7 +452,7 @@ export class Batch {
queue_micro_task(() => {
this.#decrement_queued = false;
if (!this.is_deferred()) {
if (!this.#is_deferred()) {
// we only reschedule previously-deferred effects if we expect
// to be able to run them after processing the batch
this.revive();
@ -701,16 +681,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(),
@ -837,6 +816,19 @@ function depends_on(reaction, sources, checked) {
export function schedule_effect(signal) {
var effect = (last_scheduled_effect = signal);
var boundary = effect.b;
// defer render effects inside a pending boundary
// TODO the `REACTION_RAN` check is only necessary because of legacy `$:` effects AFAICT — we can remove later
if (
boundary?.is_pending &&
(signal.f & (EFFECT | RENDER_EFFECT | MANAGED_EFFECT)) !== 0 &&
(signal.f & REACTION_RAN) === 0
) {
boundary.defer_effect(signal);
return;
}
while (effect.parent !== null) {
effect = effect.parent;
var flags = effect.f;
@ -848,13 +840,18 @@ export function schedule_effect(signal) {
is_flushing &&
effect === active_effect &&
(flags & BLOCK_EFFECT) !== 0 &&
(flags & HEAD_EFFECT) === 0
(flags & HEAD_EFFECT) === 0 &&
(flags & REACTION_RAN) !== 0
) {
return;
}
if ((flags & (ROOT_EFFECT | BRANCH_EFFECT)) !== 0) {
if ((flags & CLEAN) === 0) return;
if ((flags & CLEAN) === 0) {
// branch is already dirty, bail
return;
}
effect.f ^= CLEAN;
}
}

@ -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';
@ -32,8 +40,8 @@ import { Boundary } from '../dom/blocks/boundary.js';
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 { increment_pending, unset_context } from './async.js';
import { deferred, includes, noop } from '../../shared/utils.js';
import { set_signal_status, update_derived_status } from './status.js';
/** @type {Effect | null} */
@ -103,8 +111,6 @@ export function async_derived(fn, label, location) {
e.async_derived_orphan();
}
var boundary = /** @type {Boundary} */ (parent.b);
var promise = /** @type {Promise<V>} */ (/** @type {unknown} */ (undefined));
var signal = source(/** @type {V} */ (UNINITIALIZED));
@ -148,10 +154,7 @@ export function async_derived(fn, label, location) {
var batch = /** @type {Batch} */ (current_batch);
if (should_suspend) {
var blocking = boundary.is_rendered();
boundary.update_pending_count(1);
batch.increment(blocking);
var decrement_pending = increment_pending();
deferreds.get(batch)?.reject(STALE_REACTION);
deferreds.delete(batch); // delete to ensure correct order in Map iteration below
@ -200,9 +203,8 @@ export function async_derived(fn, label, location) {
}
}
if (should_suspend) {
boundary.update_pending_count(-1);
batch.decrement(blocking);
if (decrement_pending) {
decrement_pending();
}
};
@ -392,3 +394,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,
@ -40,8 +40,8 @@ import { DEV } from 'esm-env';
import { define_property } from '../../shared/utils.js';
import { get_next_sibling } from '../dom/operations.js';
import { component_context, dev_current_component_function, dev_stack } from '../context.js';
import { Batch, current_batch, schedule_effect } from './batch.js';
import { flatten } from './async.js';
import { Batch, schedule_effect } from './batch.js';
import { flatten, increment_pending } from './async.js';
import { without_reactive_context } from '../dom/elements/bindings/shared.js';
import { set_signal_status } from './status.js';
@ -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
@ -377,14 +376,16 @@ export function template_effect(fn, sync = [], async = [], blockers = []) {
* @param {Blocker[]} blockers
*/
export function deferred_template_effect(fn, sync = [], async = [], blockers = []) {
var batch = /** @type {Batch} */ (current_batch);
var is_async = async.length > 0 || blockers.length > 0;
if (is_async) batch.increment(true);
if (async.length > 0 || blockers.length > 0) {
var decrement_pending = increment_pending();
}
flatten(blockers, sync, async, (values) => {
create_effect(EFFECT, () => fn(...values.map(get)), false);
if (is_async) batch.decrement(true);
if (decrement_pending) {
decrement_pending();
}
});
}

@ -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
@ -161,40 +161,6 @@ const document_listeners = new Map();
function _mount(Component, { target, anchor, props = {}, events, context, intro = true }) {
init_operations();
/** @type {Set<string>} */
var registered_events = new Set();
/** @param {Array<string>} events */
var event_handle = (events) => {
for (var i = 0; i < events.length; i++) {
var event_name = events[i];
if (registered_events.has(event_name)) continue;
registered_events.add(event_name);
var passive = is_passive_event(event_name);
// 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 });
var n = document_listeners.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);
}
}
};
event_handle(array_from(all_registered_events));
root_event_handles.add(event_handle);
/** @type {Exports} */
// @ts-expect-error will be defined because the render effect runs synchronously
var component = undefined;
@ -243,17 +209,65 @@ function _mount(Component, { target, anchor, props = {}, events, context, intro
}
);
return () => {
for (var event_name of registered_events) {
target.removeEventListener(event_name, handle_event_propagation);
// Setup event delegation _after_ component is mounted - if an error would happen during mount, it would otherwise not be cleaned up
/** @type {Set<string>} */
var registered_events = new Set();
/** @param {Array<string>} events */
var event_handle = (events) => {
for (var i = 0; i < events.length; i++) {
var event_name = events[i];
if (registered_events.has(event_name)) continue;
registered_events.add(event_name);
var n = /** @type {number} */ (document_listeners.get(event_name));
var passive = is_passive_event(event_name);
if (--n === 0) {
document.removeEventListener(event_name, handle_event_propagation);
document_listeners.delete(event_name);
} else {
document_listeners.set(event_name, n);
// 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.
//
// 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 count = counts.get(event_name);
if (count === undefined) {
node.addEventListener(event_name, handle_event_propagation, { passive });
counts.set(event_name, 1);
} else {
counts.set(event_name, count + 1);
}
}
}
};
event_handle(array_from(all_registered_events));
root_event_handles.add(event_handle);
return () => {
for (var event_name of registered_events) {
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';
@ -253,6 +256,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;
@ -396,8 +400,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);
}
}
@ -643,7 +649,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) {
@ -656,6 +662,7 @@ export function get(signal) {
}
if (should_connect && !is_new) {
unfreeze_derived_effects(derived);
reconnect(derived);
}
}
@ -677,14 +684,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));
}
}

@ -26,6 +26,19 @@ export function await_invalid() {
throw error;
}
/**
* `<svelte:element this="%tag%">` is not a valid element name the element will not be rendered
* @param {string} tag
* @returns {never}
*/
export function dynamic_element_invalid_tag(tag) {
const error = new Error(`dynamic_element_invalid_tag\n\`<svelte:element this="${tag}">\` is not a valid element name — the element will not be rendered\nhttps://svelte.dev/e/dynamic_element_invalid_tag`);
error.name = 'Svelte error';
throw error;
}
/**
* The `html` property of server render results has been deprecated. Use `body` instead.
* @returns {never}

@ -13,9 +13,14 @@ import {
} from '../../constants.js';
import { escape_html } from '../../escaping.js';
import { DEV } from 'esm-env';
import { EMPTY_COMMENT, BLOCK_CLOSE, BLOCK_OPEN, BLOCK_OPEN_ELSE } from './hydration.js';
import { EMPTY_COMMENT, BLOCK_OPEN, BLOCK_OPEN_ELSE } from './hydration.js';
import { validate_store } from '../shared/validate.js';
import { is_boolean_attribute, is_raw_text_element, is_void } from '../../utils.js';
import {
is_boolean_attribute,
is_raw_text_element,
is_void,
REGEX_VALID_TAG_NAME
} from '../../utils.js';
import { Renderer } from './renderer.js';
import * as e from './errors.js';
@ -35,6 +40,9 @@ export function element(renderer, tag, attributes_fn = noop, children_fn = noop)
renderer.push('<!---->');
if (tag) {
if (!REGEX_VALID_TAG_NAME.test(tag)) {
e.dynamic_element_invalid_tag(tag);
}
renderer.push(`<${tag}`);
attributes_fn();
renderer.push(`>`);
@ -138,7 +146,7 @@ export function attributes(attrs, css_hash, classes, styles, flags = 0) {
const lowercase = (flags & ELEMENT_PRESERVE_ATTRIBUTE_CASE) === 0;
const is_input = (flags & ELEMENT_IS_INPUT) !== 0;
for (name in attrs) {
for (name of Object.keys(attrs)) {
// omit functions, internal svelte properties and invalid attribute names
if (typeof attrs[name] === 'function') continue;
if (name[0] === '$' && name[1] === '$') continue; // faster than name.startsWith('$$')
@ -150,6 +158,9 @@ export function attributes(attrs, css_hash, classes, styles, flags = 0) {
name = name.toLowerCase();
}
// omit event handler attributes
if (name.length > 2 && name.startsWith('on')) continue;
if (is_input) {
if (name === 'defaultvalue' || name === 'defaultchecked') {
name = name === 'defaultvalue' ? 'value' : 'checked';
@ -174,7 +185,8 @@ export function spread_props(props) {
for (let i = 0; i < props.length; i++) {
const obj = props[i];
for (key in obj) {
if (obj == null) continue;
for (key of Object.keys(obj)) {
const desc = Object.getOwnPropertyDescriptor(obj, key);
if (desc) {
Object.defineProperty(merged_props, key, desc);
@ -302,7 +314,7 @@ export function update_store_pre(store_values, store_name, store, d = 1) {
/** @param {Record<string, [any, any, any]>} store_values */
export function unsubscribe_stores(store_values) {
for (const store_name in store_values) {
for (const store_name of Object.keys(store_values)) {
store_values[store_name][1]();
}
}
@ -338,7 +350,7 @@ export function rest_props(props, rest) {
/** @type {Record<string, unknown>} */
const rest_props = {};
let key;
for (key in props) {
for (key of Object.keys(props)) {
if (!rest.includes(key)) {
rest_props[key] = props[key];
}
@ -363,7 +375,7 @@ export function sanitize_slots(props) {
/** @type {Record<string, boolean>} */
const sanitized = {};
if (props.children) sanitized.default = true;
for (const key in props.$$slots) {
for (const key of Object.keys(props.$$slots || {})) {
sanitized[key] = true;
}
return sanitized;
@ -376,7 +388,7 @@ export function sanitize_slots(props) {
* @param {Record<string, unknown>} props_now
*/
export function bind_props(props_parent, props_now) {
for (const key in props_now) {
for (const key of Object.keys(props_now)) {
const initial_value = props_parent[key];
const value = props_now[key];
if (

@ -11,7 +11,8 @@ import { attributes } from './index.js';
import { get_render_context, with_render_context, init_render_context } from './render-context.js';
import { sha256 } from './crypto.js';
import * as devalue from 'devalue';
import { noop } from '../shared/utils.js';
import { has_own_property, noop } from '../shared/utils.js';
import { escape_html } from '../../escaping.js';
/** @typedef {'head' | 'body'} RendererType */
/** @typedef {{ [key in RendererType]: string }} AccumulatedContent */
@ -267,12 +268,12 @@ export class Renderer {
* @param {{ head?: string, body: any }} content
*/
const close = (renderer, value, { head, body }) => {
if ('value' in attrs) {
if (has_own_property.call(attrs, 'value')) {
value = attrs.value;
}
if (value === this.local.select_value) {
renderer.#out.push(' selected');
renderer.#out.push(' selected=""');
}
renderer.#out.push(`>${body}${is_rich ? '<!>' : ''}</option>`);
@ -298,7 +299,7 @@ export class Renderer {
}
});
} else {
close(this, body, { body });
close(this, body, { body: escape_html(body) });
}
}

@ -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><!--]-->'
);
});

@ -1,5 +1,6 @@
import { escape_html } from '../../escaping.js';
import { clsx as _clsx } from 'clsx';
import { has_own_property } from './utils.js';
/**
* `<div translate={false}>` should be rendered as `<div translate="no">` and _not_
@ -27,8 +28,9 @@ export function attr(name, value, is_boolean = false) {
is_boolean = true;
}
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 normalized =
(has_own_property.call(replacements, name) && replacements[name].get(value)) || value;
const assignment = is_boolean ? `=""` : `="${escape_html(normalized, true)}"`;
return ` ${name}${assignment}`;
}
@ -61,7 +63,7 @@ export function to_class(value, hash, directives) {
}
if (directives) {
for (var key in directives) {
for (var key of Object.keys(directives)) {
if (directives[key]) {
classname = classname ? classname + ' ' + key : key;
} else if (classname.length) {
@ -96,7 +98,7 @@ function append_styles(styles, important = false) {
var separator = important ? ' !important;' : ';';
var css = '';
for (var key in styles) {
for (var key of Object.keys(styles)) {
var value = styles[key];
if (value != null && value !== '') {
css += ' ' + key + ': ' + value + separator;

@ -89,7 +89,7 @@ function clone(value, cloned, path, paths, original = null, no_tojson = false) {
cloned.set(original, copy);
}
for (var key in value) {
for (var key of Object.keys(value)) {
copy[key] = clone(
// @ts-expect-error
value[key],

@ -12,6 +12,7 @@ export var object_prototype = Object.prototype;
export var array_prototype = Array.prototype;
export var get_prototype_of = Object.getPrototypeOf;
export var is_extensible = Object.isExtensible;
export var has_own_property = Object.prototype.hasOwnProperty;
/**
* @param {any} thing

@ -480,6 +480,19 @@ export function is_raw_text_element(name) {
return RAW_TEXT_ELEMENTS.includes(/** @type {typeof RAW_TEXT_ELEMENTS[number]} */ (name));
}
// Matches valid HTML/SVG/MathML element names and custom element names.
// https://html.spec.whatwg.org/multipage/custom-elements.html#valid-custom-element-name
//
// Standard elements: ASCII alpha start, followed by ASCII alphanumerics.
// Custom elements: ASCII alpha start, followed by any mix of PCENChar (which
// includes ASCII alphanumerics, `-`, `.`, `_`, and specified Unicode ranges),
// with at least one hyphen required somewhere after the first character.
//
// Rejects strings containing whitespace, quotes, angle brackets, slashes, equals,
// or other characters that could break out of a tag-name token and enable markup injection.
export const REGEX_VALID_TAG_NAME =
/^[a-zA-Z][a-zA-Z0-9]*(-[a-zA-Z0-9.\-_\u00B7\u00C0-\u00D6\u00D8-\u00F6\u00F8-\u037D\u037F-\u1FFF\u200C-\u200D\u203F-\u2040\u2070-\u218F\u2C00-\u2FEF\u3001-\uD7FF\uF900-\uFDCF\uFDF0-\uFFFD\u{10000}-\u{EFFFF}]+)*$/u;
/**
* Prevent devtools trying to make `location` a clickable link by inserting a zero-width space
* @template {string | undefined} T

@ -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.5';
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,11 @@
form[method="get"].svelte-xyz h1:where(.svelte-xyz) {
color: red;
}
form[method="post"].svelte-xyz h1:where(.svelte-xyz) {
color: blue;
}
input[type="text"].svelte-xyz {
color: green;
}

@ -0,0 +1 @@
<form class="svelte-xyz" method="GET"><h1 class="svelte-xyz">Hello</h1></form> <form class="svelte-xyz" method="POST"><h1 class="svelte-xyz">World</h1></form> <input class="svelte-xyz" type="Text" />

@ -0,0 +1,23 @@
<form method="GET">
<h1>Hello</h1>
</form>
<form method="POST">
<h1>World</h1>
</form>
<input type="Text" />
<style>
form[method="get"] h1 {
color: red;
}
form[method="post"] h1 {
color: blue;
}
input[type="text"] {
color: green;
}
</style>

@ -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,38 @@
import { tick } from 'svelte';
import { test } from '../../test';
export default test({
html: `
<button>increment</button>
<button>shift</button>
<p>0</p>
`,
async test({ assert, target }) {
const [increment, shift] = target.querySelectorAll('button');
increment.click();
await tick();
assert.htmlEqual(
target.innerHTML,
`
<button>increment</button>
<button>shift</button>
<p>1</p>
`
);
shift.click();
await tick();
assert.htmlEqual(
target.innerHTML,
`
<button>increment</button>
<button>shift</button>
<p>resolved</p>
`
);
}
});

@ -0,0 +1,31 @@
<script>
let resolvers = [];
function push(value) {
const deferred = Promise.withResolvers();
resolvers.push(() => deferred.resolve(value));
return deferred.promise;
}
function shift() {
resolvers.shift()?.();
}
let count = $state(0);
</script>
<button onclick={() => count += 1}>
increment
</button>
<button onclick={shift}>
shift
</button>
<svelte:boundary>
<p>{await push('resolved')}</p>
{#snippet pending()}
<p>{count}</p>
{/snippet}
</svelte:boundary>

@ -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,11 @@
import { test } from '../../test';
export default test({
compileOptions: {
dev: true
},
mode: ['client'],
error: 'each_key_volatile'
});

@ -0,0 +1,10 @@
<script>
let things = $state([
{ group: 'a', id: 1 },
{ group: 'b', id: 2 }
]);
</script>
{#each things as thing ([thing.group, thing.id])}
<p>{thing.group}-{thing.id}</p>
{/each}

@ -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,9 @@
import { flushSync } from 'svelte';
import { test } from '../../test';
export default test({
compileOptions: {
dev: true
},
async test() {}
});

@ -0,0 +1,8 @@
<button
type="button"
onclick={async () => {
await Promise.resolve();
}}
>
Button
</button>

@ -30,7 +30,7 @@ export default test({
},
derived: [1]
},
'at HTMLButtonElement.Main.button.__click'
'at HTMLButtonElement.click'
]);
}
});

@ -17,10 +17,10 @@ export default test({
0,
1,
0,
'at HTMLButtonElement.<anonymous>',
'at HTMLButtonElement.click',
1,
1,
'at HTMLButtonElement.<anonymous>'
'at HTMLButtonElement.click_1'
]);
}
});

@ -16,7 +16,7 @@ export default test({
[{ count: 0 }],
{ x: { count: 1 } },
[{ count: 1 }],
'at HTMLButtonElement.<anonymous>'
'at HTMLButtonElement.click'
]);
}
});

@ -15,9 +15,9 @@ export default test({
{},
[],
{ x: 'hello' },
'at HTMLButtonElement.Main.button.__click',
'at HTMLButtonElement.click',
['hello'],
'at HTMLButtonElement.Main.button.__click'
'at HTMLButtonElement.click'
]);
}
});

@ -15,9 +15,9 @@ export default test({
assert.deepEqual(normalise_inspect_logs(logs), [
[],
[{}],
'at HTMLButtonElement.Main.button.__click',
'at HTMLButtonElement.click',
[{}, {}],
'at HTMLButtonElement.Main.button.__click'
'at HTMLButtonElement.click'
]);
}
});

@ -12,6 +12,6 @@ export default test({
b2.click();
await Promise.resolve();
assert.deepEqual(normalise_inspect_logs(logs), [0, 1, 'at HTMLButtonElement.<anonymous>']);
assert.deepEqual(normalise_inspect_logs(logs), [0, 1, 'at HTMLButtonElement.click']);
}
});

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

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

Loading…
Cancel
Save