Merge branch 'main' into svelte-custom-renderer

pull/18244/head
Paolo Ricciuti 2 months ago committed by GitHub
commit e2c81d69f8
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -1,5 +0,0 @@
---
'svelte': patch
---
fix: leave stale promises to wait for a later resolution, instead of rejecting

@ -1,5 +0,0 @@
---
'svelte': patch
---
fix: reapply context after transforming error during SSR

@ -1,5 +0,0 @@
---
'svelte': patch
---
fix: don't rebase just-created batches

@ -1,5 +0,0 @@
---
'svelte': patch
---
chore: allow `null` for `pending` in typings

@ -1,5 +0,0 @@
---
'svelte': patch
---
fix: flush eager effects in production

@ -1,5 +0,0 @@
---
'svelte': patch
---
fix: rethrow error of failed iterable after calling `return()`

@ -1,5 +0,0 @@
---
'svelte': patch
---
fix: account for proxified instance when updating `bind:this`

@ -1,5 +0,0 @@
---
'svelte': patch
---
fix: ensure scheduled batch is flushed if not obsolete

@ -1,5 +0,0 @@
---
'svelte': patch
---
fix: resolve stale deriveds with latest value

@ -1,5 +0,0 @@
---
'svelte': patch
---
chore: remove unnecessary `increment_pending` calls

@ -1,5 +0,0 @@
---
'svelte': patch
---
fix: correctly compile component member expressions for SSR

@ -1,5 +0,0 @@
---
'svelte': patch
---
fix: reset `source.updated` stack traces after `flush`

@ -1,5 +0,0 @@
---
"svelte": patch
---
fix: replacing async 'blocking' strategy with 'merging'

@ -1,5 +0,0 @@
---
'svelte': patch
---
fix: allow `@debug` tags to reference awaited variables

@ -1,5 +0,0 @@
---
'svelte': patch
---
fix: re-run fallback props if dependencies update

@ -1,5 +0,0 @@
---
'svelte': patch
---
fix: abort running obsolete async branches

@ -1,5 +0,0 @@
---
'svelte': patch
---
fix: ignore comments when reading CSS values

@ -1,5 +0,0 @@
---
'svelte': patch
---
fix: wrap `Promise.all` in `save` during SSR

@ -1,5 +0,0 @@
---
'svelte': patch
---
fix: ignore false-positive errors of `$inspect` dependencies

@ -1,5 +1,63 @@
# svelte
## 5.55.7
### Patch Changes
- fix: prevent XSS on `hydratable` from user contents ([`a16ebc67bbcf8f708360195687e1b2719463e1a4`](https://github.com/sveltejs/svelte/commit/a16ebc67bbcf8f708360195687e1b2719463e1a4))
- chore: bump devalue ([#18219](https://github.com/sveltejs/svelte/pull/18219))
- fix: disallow empty attribute names during SSR ([`547853e2406a2147ad7fb5ffeba95b01bd9642da`](https://github.com/sveltejs/svelte/commit/547853e2406a2147ad7fb5ffeba95b01bd9642da))
- fix: harden regex ([`d2375e2ebcab5c88feb5652f1a9d621b8f06b259`](https://github.com/sveltejs/svelte/commit/d2375e2ebcab5c88feb5652f1a9d621b8f06b259))
- fix: move Svelte runtime properties to symbols ([`e1cbbd96441e82c9eb8a23a2903c0d06d3cda991`](https://github.com/sveltejs/svelte/commit/e1cbbd96441e82c9eb8a23a2903c0d06d3cda991))
## 5.55.6
### Patch Changes
- fix: leave stale promises to wait for a later resolution, instead of rejecting ([#18180](https://github.com/sveltejs/svelte/pull/18180))
- fix: keep dependencies of `$state.eager/pending` ([#18218](https://github.com/sveltejs/svelte/pull/18218))
- fix: reapply context after transforming error during SSR ([#18099](https://github.com/sveltejs/svelte/pull/18099))
- fix: don't rebase just-created batches ([#18117](https://github.com/sveltejs/svelte/pull/18117))
- chore: allow `null` for `pending` in typings ([#18201](https://github.com/sveltejs/svelte/pull/18201))
- fix: flush eager effects in production ([#18107](https://github.com/sveltejs/svelte/pull/18107))
- fix: rethrow error of failed iterable after calling `return()` ([#18169](https://github.com/sveltejs/svelte/pull/18169))
- fix: account for proxified instance when updating `bind:this` ([#18147](https://github.com/sveltejs/svelte/pull/18147))
- fix: ensure scheduled batch is flushed if not obsolete ([#18131](https://github.com/sveltejs/svelte/pull/18131))
- fix: resolve stale deriveds with latest value ([#18167](https://github.com/sveltejs/svelte/pull/18167))
- chore: remove unnecessary `increment_pending` calls ([#18183](https://github.com/sveltejs/svelte/pull/18183))
- fix: correctly compile component member expressions for SSR ([#18192](https://github.com/sveltejs/svelte/pull/18192))
- fix: reset `source.updated` stack traces after `flush` ([#18196](https://github.com/sveltejs/svelte/pull/18196))
- fix: replacing async 'blocking' strategy with 'merging' ([#18205](https://github.com/sveltejs/svelte/pull/18205))
- fix: allow `@debug` tags to reference awaited variables ([#18138](https://github.com/sveltejs/svelte/pull/18138))
- fix: re-run fallback props if dependencies update ([#18146](https://github.com/sveltejs/svelte/pull/18146))
- fix: abort running obsolete async branches ([#18118](https://github.com/sveltejs/svelte/pull/18118))
- fix: ignore comments when reading CSS values ([#18153](https://github.com/sveltejs/svelte/pull/18153))
- fix: wrap `Promise.all` in `save` during SSR ([#18178](https://github.com/sveltejs/svelte/pull/18178))
- fix: ignore false-positive errors of `$inspect` dependencies ([#18106](https://github.com/sveltejs/svelte/pull/18106))
## 5.55.5
### Patch Changes

@ -2,7 +2,7 @@
"name": "svelte",
"description": "Cybernetically enhanced web apps",
"license": "MIT",
"version": "5.55.5",
"version": "5.55.7",
"type": "module",
"types": "./types/index.d.ts",
"engines": {
@ -183,7 +183,7 @@
"aria-query": "5.3.1",
"axobject-query": "^4.1.0",
"clsx": "^2.1.1",
"devalue": "^5.6.4",
"devalue": "^5.8.1",
"esm-env": "^1.2.1",
"esrap": "^2.2.4",
"is-reference": "^3.0.3",

@ -63,6 +63,11 @@ export const STATE_SYMBOL = Symbol('$state');
export const LEGACY_PROPS = Symbol('legacy props');
export const LOADING_ATTR_SYMBOL = Symbol('');
export const PROXY_PATH_SYMBOL = Symbol('proxy path');
export const ATTRIBUTES_CACHE = Symbol('attributes');
export const CLASS_CACHE = Symbol('class');
export const STYLE_CACHE = Symbol('style');
export const TEXT_CACHE = Symbol('text');
export const FORM_RESET_HANDLER = Symbol('form reset');
/** An anchor might change, via this symbol on the original anchor we can tell HMR about the updated anchor */
export const HMR_ANCHOR = Symbol('hmr anchor');

@ -5,7 +5,12 @@ import { get_descriptors, get_prototype_of } from '../../../shared/utils.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 { IS_XHTML, LOADING_ATTR_SYMBOL } from '#client/constants';
import {
ATTRIBUTES_CACHE,
FORM_RESET_HANDLER,
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 {
@ -84,8 +89,7 @@ export function remove_input_defaults(input) {
}
};
// @ts-expect-error
input.__on_r = remove_defaults;
/** @type {any} */ (input)[FORM_RESET_HANDLER] = remove_defaults;
queue_micro_task(remove_defaults);
add_form_reset_listener();
}
@ -596,8 +600,7 @@ export function attribute_effect(
*/
function get_attributes(element) {
return /** @type {Record<string | symbol, unknown>} **/ (
// @ts-expect-error
element.__attributes ??= {
/** @type {any} */ (element)[ATTRIBUTES_CACHE] ??= {
[IS_CUSTOM_ELEMENT]: (node_name(element) ?? '').includes('-'),
[IS_HTML]: element.namespaceURI === NAMESPACE_HTML
}
@ -620,13 +623,19 @@ function get_setters(element) {
var proto = element; // In the case of custom elements there might be setters on the instance
var element_proto = Element.prototype;
// Stop at Element, from there on there's only unnecessary setters we're not interested in
// Stop at Element, from there on there's only unnecessary (and dangerous, like innerHTML) setters we're not interested in
// Do not use constructor.name here as that's unreliable in some browser environments
while (element_proto !== proto) {
descriptors = get_descriptors(proto);
for (var key in descriptors) {
if (descriptors[key].set) {
if (
descriptors[key].set &&
// better safe than sorry, we don't want spread attributes to mess with HTML content
key !== 'innerHTML' &&
key !== 'textContent' &&
key !== 'innerText'
) {
setters.push(key);
}
}

@ -5,6 +5,7 @@ import {
set_active_effect,
set_active_reaction
} from '../../../runtime.js';
import { FORM_RESET_HANDLER } from '../../../constants.js';
import { add_form_reset_listener } from '../misc.js';
/**
@ -58,18 +59,15 @@ export function without_reactive_context(fn) {
*/
export function listen_to_event_and_reset_event(element, event, handler, on_reset = handler) {
element.addEventListener(event, () => without_reactive_context(handler));
// @ts-expect-error
const prev = element.__on_r;
const prev = /** @type {any} */ (element)[FORM_RESET_HANDLER];
if (prev) {
// special case for checkbox that can have multiple binds (group & checked)
// @ts-expect-error
element.__on_r = () => {
/** @type {any} */ (element)[FORM_RESET_HANDLER] = () => {
prev();
on_reset(true);
};
} else {
// @ts-expect-error
element.__on_r = () => on_reset(true);
/** @type {any} */ (element)[FORM_RESET_HANDLER] = () => on_reset(true);
}
add_form_reset_listener();

@ -1,4 +1,5 @@
import { to_class } from '../../../shared/attributes.js';
import { CLASS_CACHE } from '../../constants.js';
import { hydrating } from '../hydration.js';
import {
class_list_toggle,
@ -17,8 +18,7 @@ import {
* @returns {Record<string, boolean> | undefined}
*/
export function set_class(dom, is_html, value, hash, prev_classes, next_classes) {
// @ts-expect-error need to add __className to patched prototype
var prev = dom.__className;
var prev = /** @type {any} */ (dom)[CLASS_CACHE];
if (
hydrating ||
@ -41,8 +41,7 @@ export function set_class(dom, is_html, value, hash, prev_classes, next_classes)
}
}
// @ts-expect-error need to add __className to patched prototype
dom.__className = value;
/** @type {any} */ (dom)[CLASS_CACHE] = value;
} else if (next_classes && prev_classes !== next_classes) {
for (var key in next_classes) {
var is_present = !!next_classes[key];

@ -261,9 +261,9 @@ export function handle_event_propagation(event) {
});
// This started because of Chromium issue https://chromestatus.com/feature/5128696823545856,
// where removal or moving of of the DOM can cause sync `blur` events to fire, which can cause logic
// where removal or moving of the DOM can cause sync `blur` events to fire, which can cause logic
// to run inside the current `active_reaction`, which isn't what we want at all. However, on reflection,
// it's probably best that all event handled by Svelte have this behaviour, as we don't really want
// it's probably best that all events handled by Svelte have this behaviour, as we don't really want
// an event handler to run in the context of another reaction or effect.
var previous_reaction = active_reaction;
var previous_effect = active_effect;

@ -1,6 +1,7 @@
import { hydrating } from '../hydration.js';
import { clear_text_content, get_first_child, add_event_listener } from '../operations.js';
import { queue_micro_task } from '../task.js';
import { FORM_RESET_HANDLER } from '../../constants.js';
/**
* @param {HTMLElement} dom
@ -46,8 +47,7 @@ export function add_form_reset_listener() {
Promise.resolve().then(() => {
if (!evt.defaultPrevented) {
for (const e of /**@type {HTMLFormElement} */ (evt.target).elements) {
// @ts-expect-error
e.__on_r?.();
/** @type {any} */ (e)[FORM_RESET_HANDLER]?.();
}
}
});

@ -1,4 +1,5 @@
import { to_style } from '../../../shared/attributes.js';
import { STYLE_CACHE } from '../../constants.js';
import { hydrating } from '../hydration.js';
import {
style_remove_property,
@ -35,8 +36,7 @@ function update_styles(dom, prev = {}, next, priority) {
* @param {Record<string, any> | [Record<string, any>, Record<string, any>]} [next_styles]
*/
export function set_style(dom, value, prev_styles, next_styles) {
// @ts-expect-error
var prev = dom.__style;
var prev = /** @type {any} */ (dom)[STYLE_CACHE];
if (hydrating || prev !== value) {
var next_style_attr = to_style(value, next_styles);
@ -49,8 +49,7 @@ export function set_style(dom, value, prev_styles, next_styles) {
}
}
// @ts-expect-error
dom.__style = value;
/** @type {any} */ (dom)[STYLE_CACHE] = value;
} else if (next_styles) {
if (Array.isArray(next_styles)) {
update_styles(dom, prev_styles?.[0], next_styles[0]);

@ -6,10 +6,14 @@ 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,
ATTRIBUTES_CACHE,
CLASS_CACHE,
REACTION_RAN,
CUSTOM_RENDERER_NODE_TYPE_MAP,
COMMENT_NODE
STYLE_CACHE,
TEXT_CACHE,
TEXT_NODE,
COMMENT_NODE,
CUSTOM_RENDERER_NODE_TYPE_MAP
} from '#client/constants';
import { eager_block_effects } from '../reactivity/batch.js';
import { NAMESPACE_HTML } from '../../../constants.js';
@ -54,21 +58,15 @@ export function init_operations() {
if (is_extensible(element_prototype)) {
// the following assignments improve perf of lookups on DOM nodes
// @ts-expect-error
element_prototype.__click = undefined;
// @ts-expect-error
element_prototype.__className = undefined;
// @ts-expect-error
element_prototype.__attributes = null;
// @ts-expect-error
element_prototype.__style = undefined;
/** @type {any} */ (element_prototype)[CLASS_CACHE] = undefined;
/** @type {any} */ (element_prototype)[ATTRIBUTES_CACHE] = null;
/** @type {any} */ (element_prototype)[STYLE_CACHE] = undefined;
// @ts-expect-error
element_prototype.__e = undefined;
}
if (is_extensible(text_prototype)) {
// @ts-expect-error
text_prototype.__t = undefined;
/** @type {any} */ (text_prototype)[TEXT_CACHE] = undefined;
}
if (DEV) {

@ -16,7 +16,8 @@ import {
EAGER_EFFECT,
ERROR_VALUE,
MANAGED_EFFECT,
REACTION_RAN
REACTION_RAN,
DESTROYING
} from '#client/constants';
import { async_mode_flag } from '../../flags/index.js';
import { deferred, define_property, includes } from '../../shared/utils.js';
@ -33,7 +34,7 @@ import { flush_tasks, queue_micro_task } from '../dom/task.js';
import { DEV } from 'esm-env';
import { invoke_error_boundary } from '../error-handling.js';
import { flush_eager_effects, old_values, set_eager_effects, source, update } from './sources.js';
import { eager_effect, unlink_effect } from './effects.js';
import { eager_effect, teardown, unlink_effect } from './effects.js';
import { defer_effect } from './utils.js';
import { UNINITIALIZED } from '../../../constants.js';
import { set_signal_status } from './status.js';
@ -1234,6 +1235,9 @@ function eager_flush() {
});
}
/** @type {Map<Reaction, Source<number>>} */
var version_map = new Map();
/**
* Implementation of `$state.eager(fn())`
* @template T
@ -1241,10 +1245,22 @@ function eager_flush() {
* @returns {T}
*/
export function eager(fn) {
var version = source(0);
var initial = true;
var value = /** @type {T} */ (undefined);
if (active_reaction === null) {
return fn();
}
let parent = active_reaction;
let version = version_map.get(parent) ?? source(0);
version_map.set(parent, version);
teardown(() => {
if (parent.f & DESTROYING) version_map.delete(parent);
});
get(version);
eager_effect(() => {

@ -30,7 +30,7 @@ import * as w from './warnings.js';
import * as e from './errors.js';
import { assign_nodes } from './dom/template.js';
import { is_passive_event } from '../../utils.js';
import { COMMENT_NODE, STATE_SYMBOL } from './constants.js';
import { COMMENT_NODE, STATE_SYMBOL, TEXT_CACHE } from './constants.js';
import { boundary } from './dom/blocks/boundary.js';
/**
@ -53,10 +53,9 @@ export function set_should_intro(value) {
export function set_text(text, value) {
// For objects, we apply string coercion (which might make things like $state array references in the template reactive) before diffing
var str = value == null ? '' : typeof value === 'object' ? `${value}` : value;
// @ts-expect-error
if (str !== (text.__t ??= get_node_value(text))) {
// @ts-expect-error
text.__t = str;
// prettier-ignore
if (str !== (/** @type {any} */ (text)[TEXT_CACHE] ??= get_node_value(text))) {
/** @type {any} */ (text)[TEXT_CACHE] = str;
set_node_value(text, `${str}`);
}
}

@ -3,7 +3,6 @@ import { async_mode_flag } from '../flags/index.js';
import { get_render_context } from './render-context.js';
import * as e from './errors.js';
import * as devalue from 'devalue';
import { get_stack } from '../shared/dev.js';
import { DEV } from 'esm-env';
import { get_user_code_location } from './dev.js';
@ -65,7 +64,13 @@ function encode(key, value, unresolved) {
const placeholder = `"${uid++}"`;
const p = value
.then((v) => {
entry.serialized = entry.serialized.replace(placeholder, `r(${uneval(v)})`);
entry.serialized = entry.serialized.replace(
placeholder,
// use the function form here to prevent any string replacement characters from being interpreted
// in `v`, as it's potentially user-controlled and therefore potentially malicious.
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/replace#specifying_a_string_as_the_replacement
() => `r(${uneval(v)})`
);
})
.catch((devalue_error) =>
e.hydratable_serialization_failed(

@ -0,0 +1,33 @@
import { afterAll, beforeAll, expect, test } from 'vitest';
import { Renderer } from './renderer.js';
import type { Component } from 'svelte';
import { disable_async_mode_flag, enable_async_mode_flag } from '../flags/index.js';
import { hydratable } from './hydratable.js';
beforeAll(() => {
enable_async_mode_flag();
});
afterAll(() => {
disable_async_mode_flag();
});
test('treats replacement tokens in hydratable promise values as literals', async () => {
const component = (renderer: Renderer) => {
hydratable('key', () => Promise.resolve(`$'`));
renderer.child(async () => {
await Promise.resolve();
});
renderer.push('ok');
};
const { head } = await Renderer.render(component as unknown as Component);
const script_match = head.match(/<script(?:\s[^>]*)?>([\s\S]*)<\/script>/);
expect(script_match, 'expected hydratable script in head output').toBeTruthy();
const script_content = script_match![1];
expect(script_content).toContain('const h = (window.__svelte ??= {}).h ??= new Map();');
expect(script_content).toContain('r("$\'")');
expect(script_content).toMatch(/\[\s*"key"\s*,\s*r\("\$'"\)\s*\]/);
});

@ -151,7 +151,7 @@ export function attributes(attrs, css_hash, classes, styles, flags = 0) {
// 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('$$')
if (INVALID_ATTR_NAME_CHAR_REGEX.test(name)) continue;
if (name === '' || INVALID_ATTR_NAME_CHAR_REGEX.test(name)) continue;
var value = attrs[name];
var lower = name.toLowerCase();

@ -491,7 +491,7 @@ export function is_raw_text_element(name) {
// 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;
/^[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

@ -0,0 +1,50 @@
import { expect, test } from 'vitest';
import { REGEX_VALID_TAG_NAME } from './utils';
test('REGEX_VALID_TAG_NAME accepts common HTML tag names', () => {
const common_html_tag_names = ['div', 'span', 'button', 'input', 'svg', 'math', 'a'];
for (const tag_name of common_html_tag_names) {
expect(REGEX_VALID_TAG_NAME.test(tag_name)).toBe(true);
}
});
test('REGEX_VALID_TAG_NAME accepts basic custom element names', () => {
const valid_custom_tag_names = ['my-element', 'x-foo', 'todo-item', 'my-element2'];
for (const tag_name of valid_custom_tag_names) {
expect(REGEX_VALID_TAG_NAME.test(tag_name)).toBe(true);
}
});
test('REGEX_VALID_TAG_NAME accepts spec-allowed custom element characters', () => {
const valid_custom_tag_names = [
'x-foo.bar',
'x-foo_bar',
'x-foo\u00B7bar',
'x-foo\u00FCbar',
'x-foo\u{1F600}bar',
'x-'
];
for (const tag_name of valid_custom_tag_names) {
expect(REGEX_VALID_TAG_NAME.test(tag_name)).toBe(true);
}
});
test('REGEX_VALID_TAG_NAME rejects invalid tag names', () => {
const invalid_tag_names = ['', '1', 'x\u00FC', '-x-foo', '1foo', 'x-foo bar', 'x-foo/', 'x-foo>'];
for (const tag_name of invalid_tag_names) {
expect(REGEX_VALID_TAG_NAME.test(tag_name)).toBe(false);
}
});
test('REGEX_VALID_TAG_NAME no ReDoS', () => {
const before = performance.now();
REGEX_VALID_TAG_NAME.test('a-----------------------------------!');
const after = performance.now();
if (after - before > 10) {
throw new Error(`REGEX_VALID_TAG_NAME is vulnerable to ReDoS`);
}
});

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

@ -0,0 +1,23 @@
import { assert_ok, test } from '../../assert';
export default test({
async test({ assert, target, waitUntil, window }) {
const form = target.querySelector('form');
const button = target.querySelector('button');
const [i1, i2, i3] = target.querySelectorAll('input');
assert_ok(form);
assert_ok(button);
assert.equal(form.id, 'initial-form');
assert.equal(form.className, 'first');
assert.equal(window.getComputedStyle(form).backgroundColor, 'rgb(255, 0, 0)');
button.click();
await waitUntil(() => form.id === 'updated-form');
assert.equal(form.id, 'updated-form');
assert.equal(form.className, 'second');
assert.equal(i3.id, '', 'input clobbered form');
assert.equal(window.getComputedStyle(form).backgroundColor, 'rgb(0, 0, 255)');
}
});

@ -0,0 +1,20 @@
<script>
let form_attributes = $state({ id: 'initial-form' });
let class_name = $state('first');
let background_color = $state('rgb(255, 0, 0)');
function update() {
form_attributes = { id: 'updated-form' };
class_name = 'second';
background_color = 'rgb(0, 0, 255)';
}
</script>
<button onclick={update}>update</button>
<form {...form_attributes} class={class_name} style:background-color={background_color}>
<!-- the once non-symbol-ified hidden properties we used -->
<input {...{ name: '__className' }} value="x" />
<input {...{ name: '__style' }} value="y" />
<input {...{ name: '__attributes' }} value="z" />
</form>

@ -0,0 +1,23 @@
import { tick } from 'svelte';
import { test } from '../../test';
export default test({
async test({ assert, target }) {
await tick();
const [increment, resolve] = target.querySelectorAll('button');
increment.click();
await tick();
assert.htmlEqual(
target.innerHTML,
'<button>increment</button> <button>resolve</button> 0 <p>loading...</p>'
);
resolve.click();
await tick();
assert.htmlEqual(
target.innerHTML,
'<button>increment</button> <button>resolve</button> 1 <p>1</p>'
);
}
});

@ -0,0 +1,20 @@
<script>
let count = $state(0);
const queued = [];
async function delay(v) {
if (!v) return v;
return new Promise(r => queued.push(() => r(v)));
}
</script>
<button onclick={() => count++}>increment</button>
<button onclick={() => queued.shift()?.()}>resolve</button>
{await delay(count)}
{#if $state.eager(count) !== count}
<p>loading...</p>
{:else}
<p>{count}</p>
{/if}

@ -0,0 +1,32 @@
import { tick } from 'svelte';
import { test } from '../../test';
export default test({
async test({ assert, target, logs }) {
await tick();
const [increment, resolve] = target.querySelectorAll('button');
logs.length = 0;
increment.click();
await tick();
assert.htmlEqual(
target.innerHTML,
`<button>increment</button> <button>resolve</button>
<ul><li>0 / 0</li><li>0 / loading...</li><li>0 / 0</li></ul>`
);
resolve.click();
await tick();
assert.htmlEqual(
target.innerHTML,
`<button>increment</button> <button>resolve</button>
<ul><li>0 / 0</li><li>1 / 1</li><li>0 / 0</li></ul>`
);
assert.equal(
logs.some((l) => l.toString().includes('0 ') || l.toString().includes('2')),
false,
'only the second $state.eager should have been evaluated'
);
}
});

@ -0,0 +1,24 @@
<script>
let counts = $state([0, 0, 0]);
const queued = [];
async function delay(v) {
if (!v) return v;
return new Promise(r => queued.push(() => r(v)));
}
</script>
<button onclick={() => counts[1]++}>increment</button>
<button onclick={() => queued.shift()?.()}>resolve</button>
<ul>
{#each counts as count, i}
<li>
{await delay(count)} /
{#if console.log(i) || $state.eager(count) !== count}
loading...
{:else}
{count}
{/if}
</li>
{/each}
</ul>

@ -5,7 +5,12 @@
onclick: 'alert(1)',
onerror: 'alert(1)',
onfocus: 'alert(1)',
onmouseover: 'alert(1)'
onmouseover: 'alert(1)',
' onload': 'alert(1)',
'\tonload': 'alert(1)',
'\u00a0onload': 'alert(1)',
"\t": "/onmouseover=alert(1)//",
"": "/onmouseover=alert(1)//"
};
</script>

@ -29,8 +29,9 @@ polka()
const { head, body } = await render(App);
const html = transformed_template
.replace(`<!--ssr-head-->`, head)
.replace(`<!--ssr-body-->`, body)
// use function form to prevent any string replacement characters from being interpreted
.replace(`<!--ssr-head-->`, () => head)
.replace(`<!--ssr-body-->`, () => body)
// check that Safari doesn't break hydration
.replaceAll('+636-555-3226', '<a href="tel:+636-555-3226">+636-555-3226</a>');

@ -9,8 +9,9 @@ const { head, body } = await render(App);
const rendered = fs
.readFileSync(path.resolve('./dist/client/index.html'), 'utf-8')
.replace(`<!--ssr-body-->`, body)
.replace(`<!--ssr-head-->`, head);
// use function form to prevent any string replacement characters from being interpreted
.replace(`<!--ssr-body-->`, () => body)
.replace(`<!--ssr-head-->`, () => head);
const types = {
'.js': 'application/javascript',

@ -96,8 +96,8 @@ importers:
specifier: ^2.1.1
version: 2.1.1
devalue:
specifier: ^5.6.4
version: 5.6.4
specifier: ^5.8.1
version: 5.8.1
esm-env:
specifier: ^1.2.1
version: 1.2.1
@ -1418,8 +1418,8 @@ packages:
engines: {node: '>=0.10'}
hasBin: true
devalue@5.6.4:
resolution: {integrity: sha512-Gp6rDldRsFh/7XuouDbxMH3Mx8GMCcgzIb1pDTvNyn8pZGQ22u+Wa+lGV9dQCltFQ7uVw0MhRyb8XDskNFOReA==}
devalue@5.8.1:
resolution: {integrity: sha512-4CXDYRBGqN+57wVJkuXBYmpAVUSg3L6JAQa/DFqm238G73E1wuyc/JhGQJzN7vUf/CMphYau2zXbfWzDR5aTEw==}
dir-glob@3.0.1:
resolution: {integrity: sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==}
@ -3786,7 +3786,7 @@ snapshots:
detect-libc@1.0.3:
optional: true
devalue@5.6.4: {}
devalue@5.8.1: {}
dir-glob@3.0.1:
dependencies:

Loading…
Cancel
Save