Merge commit from fork

* fix: move Svelte runtime properties to symbols

Prevents DOM clobbering

* shorten names

---------

Co-authored-by: Simon Holthausen <simon.holthausen@vercel.com>
pull/18220/head
Elliott Johnson 2 months ago committed by GitHub
parent a16ebc67bb
commit e1cbbd9644
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -0,0 +1,5 @@
---
'svelte': patch
---
fix: move Svelte runtime properties to symbols

@ -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 {
@ -69,8 +74,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();
}
@ -561,8 +565,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]: element.nodeName.includes('-'),
[IS_HTML]: element.namespaceURI === NAMESPACE_HTML
}
@ -583,13 +586,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';
/**
@ -11,8 +12,7 @@ import { hydrating } from '../hydration.js';
* @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 ||
@ -35,8 +35,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];

@ -1,6 +1,7 @@
import { hydrating } from '../hydration.js';
import { clear_text_content, get_first_child } from '../operations.js';
import { queue_micro_task } from '../task.js';
import { FORM_RESET_HANDLER } from '../../constants.js';
/**
* @param {HTMLElement} dom
@ -45,8 +46,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';
/**
@ -28,8 +29,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);
@ -42,8 +42,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]);

@ -5,7 +5,14 @@ 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, REACTION_RAN } from '#client/constants';
import {
ATTRIBUTES_CACHE,
CLASS_CACHE,
REACTION_RAN,
STYLE_CACHE,
TEXT_CACHE,
TEXT_NODE
} from '#client/constants';
import { eager_block_effects } from '../reactivity/batch.js';
import { NAMESPACE_HTML } from '../../../constants.js';
@ -48,21 +55,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) {

@ -23,7 +23,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';
/**
@ -46,10 +46,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 ??= text.nodeValue)) {
// @ts-expect-error
text.__t = str;
// prettier-ignore
if (str !== (/** @type {any} */ (text)[TEXT_CACHE] ??= text.nodeValue)) {
/** @type {any} */ (text)[TEXT_CACHE] = str;
text.nodeValue = `${str}`;
}
}

@ -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>
Loading…
Cancel
Save