Merge branch 'main' into gh-13270

gh-13270
Rich Harris 23 hours ago
commit e709646182

@ -0,0 +1,5 @@
---
'svelte': patch
---
fix: only set attribute as property if element has setter

@ -0,0 +1,5 @@
---
'svelte': patch
---
fix: only warn on context="module" in runes mode

@ -0,0 +1,5 @@
---
'svelte': patch
---
feat: deprecate `<svelte:self>` in runes mode

@ -0,0 +1,5 @@
---
'svelte': patch
---
fix: set strings as attributes, non-strings as properties if property exists

@ -19,6 +19,7 @@
"angry-plums-punch",
"angry-wasps-help",
"angry-weeks-design",
"beige-clocks-notice",
"beige-cobras-smoke",
"beige-flies-wash",
"beige-gifts-appear",
@ -61,6 +62,7 @@
"brown-insects-float",
"brown-months-flow",
"brown-months-fry",
"brown-pens-repeat",
"brown-radios-itch",
"brown-spoons-boil",
"brown-turkeys-tap",
@ -517,6 +519,8 @@
"old-mails-sneeze",
"old-oranges-compete",
"old-planets-bow",
"old-planets-kiss",
"old-spoons-pull",
"olive-apples-lick",
"olive-cobras-wonder",
"olive-forks-grin",
@ -536,6 +540,7 @@
"orange-yaks-protect",
"orange-zoos-heal",
"perfect-actors-bake",
"perfect-ants-allow",
"perfect-cooks-shop",
"perfect-hairs-matter",
"perfect-hats-dance",
@ -876,6 +881,7 @@
"two-keys-watch",
"unlucky-boxes-obey",
"unlucky-points-clap",
"unlucky-spies-flow",
"unlucky-steaks-warn",
"unlucky-trees-lick",
"violet-bats-brake",

@ -1,5 +1,29 @@
# svelte
## 5.0.0-next.257
### Patch Changes
- fix: only set attribute as property if element has setter ([#13341](https://github.com/sveltejs/svelte/pull/13341))
## 5.0.0-next.256
### Patch Changes
- fix: only warn on context="module" in runes mode ([#13332](https://github.com/sveltejs/svelte/pull/13332))
- feat: deprecate `<svelte:self>` in runes mode ([#13333](https://github.com/sveltejs/svelte/pull/13333))
- fix: set strings as attributes, non-strings as properties if property exists ([#13327](https://github.com/sveltejs/svelte/pull/13327))
## 5.0.0-next.255
### Patch Changes
- fix: keep bound inputs in sync in runes mode ([#13328](https://github.com/sveltejs/svelte/pull/13328))
- fix: silence snapshot warnings inside `$inspect` ([#13334](https://github.com/sveltejs/svelte/pull/13334))
## 5.0.0-next.254
### Patch Changes

@ -97,3 +97,7 @@ A derived value may be used in other contexts:
## svelte_element_invalid_this
> `this` should be an `{expression}`. Using a string attribute value will cause an error in future versions of Svelte
## svelte_self_deprecated
> `<svelte:self>` is deprecated — use self-imports (e.g. `import %name% from './%basename%'`) instead

@ -2,7 +2,7 @@
"name": "svelte",
"description": "Cybernetically enhanced web apps",
"license": "MIT",
"version": "5.0.0-next.254",
"version": "5.0.0-next.257",
"type": "module",
"types": "./types/index.d.ts",
"engines": {

@ -74,8 +74,6 @@ export function read_script(parser, start, attributes) {
e.script_invalid_context(attribute);
}
w.script_context_deprecated(attribute);
context = 'module';
}
}

@ -359,6 +359,13 @@ export function analyze_component(root, source, options) {
const runes = options.runes ?? Array.from(module.scope.references.keys()).some(is_rune);
if (runes && root.module) {
const context = root.module.attributes.find((attribute) => attribute.name === 'context');
if (context) {
w.script_context_deprecated(context);
}
}
// TODO remove all the ?? stuff, we don't need it now that we're validating the config
/** @type {ComponentAnalysis} */
const analysis = {

@ -2,6 +2,8 @@
/** @import { Context } from '../types' */
import { visit_component } from './shared/component.js';
import * as e from '../../../errors.js';
import * as w from '../../../warnings.js';
import { filename } from '../../../state.js';
/**
* @param {AST.SvelteSelf} node
@ -20,5 +22,15 @@ export function SvelteSelf(node, context) {
e.svelte_self_invalid_placement(node);
}
if (context.state.analysis.runes) {
const name = filename === '(unknown)' ? 'Self' : context.state.analysis.name;
const basename =
filename === '(unknown)'
? 'Self.svelte'
: /** @type {string} */ (filename.split(/[/\\]/).pop());
w.svelte_self_deprecated(node, name, basename);
}
visit_component(node, context);
}

@ -511,7 +511,8 @@ function build_element_spread_attributes(
b.object(values),
context.state.analysis.css.hash !== '' && b.literal(context.state.analysis.css.hash),
preserve_attribute_case && b.true,
is_ignored(element, 'hydration_attribute_changed') && b.true
is_ignored(element, 'hydration_attribute_changed') && b.true,
element.name.includes('-') && b.true
)
)
);

@ -1,7 +1,7 @@
/** @import { BlockStatement, Expression, ExpressionStatement, Identifier, ObjectExpression, Statement } from 'estree' */
/** @import { AST } from '#compiler' */
/** @import { ComponentContext } from '../types' */
import { dev, locator } from '../../../../state.js';
import { dev, is_ignored, locator } from '../../../../state.js';
import {
get_attribute_expression,
is_event_attribute,
@ -84,7 +84,7 @@ export function SvelteElement(node, context) {
// Always use spread because we don't know whether the element is a custom element or not,
// therefore we need to do the "how to set an attribute" logic at runtime.
const is_attributes_reactive =
build_dynamic_element_attributes(attributes, inner_context, element_id) !== null;
build_dynamic_element_attributes(node, attributes, inner_context, element_id) !== null;
// class/style directives must be applied last since they could override class/style attributes
build_class_directives(class_directives, element_id, inner_context, is_attributes_reactive);
@ -137,12 +137,13 @@ export function SvelteElement(node, context) {
/**
* Serializes dynamic element attribute assignments.
* Returns the `true` if spread is deemed reactive.
* @param {AST.SvelteElement} element
* @param {Array<AST.Attribute | AST.SpreadAttribute>} attributes
* @param {ComponentContext} context
* @param {Identifier} element_id
* @returns {boolean}
*/
function build_dynamic_element_attributes(attributes, context, element_id) {
function build_dynamic_element_attributes(element, attributes, context, element_id) {
if (attributes.length === 0) {
if (context.state.analysis.css.hash) {
context.state.init.push(
@ -197,11 +198,14 @@ function build_dynamic_element_attributes(attributes, context, element_id) {
'=',
b.id(id),
b.call(
'$.set_dynamic_element_attributes',
'$.set_attributes',
element_id,
b.id(id),
b.object(values),
context.state.analysis.css.hash !== '' && b.literal(context.state.analysis.css.hash)
context.state.analysis.css.hash !== '' && b.literal(context.state.analysis.css.hash),
b.binary('!==', b.member(element_id, 'namespaceURI'), b.id('$.NAMESPACE_SVG')),
is_ignored(element, 'hydration_attribute_changed') && b.true,
b.call(b.member(b.member(element_id, 'nodeName'), 'includes'), b.literal('-'))
)
)
);
@ -218,11 +222,14 @@ function build_dynamic_element_attributes(attributes, context, element_id) {
context.state.init.push(
b.stmt(
b.call(
'$.set_dynamic_element_attributes',
'$.set_attributes',
element_id,
b.literal(null),
b.object(values),
context.state.analysis.css.hash !== '' && b.literal(context.state.analysis.css.hash)
context.state.analysis.css.hash !== '' && b.literal(context.state.analysis.css.hash),
b.binary('!==', b.member(element_id, 'namespaceURI'), b.id('$.NAMESPACE_SVG')),
is_ignored(element, 'hydration_attribute_changed') && b.true,
b.call(b.member(b.member(element_id, 'nodeName'), 'includes'), b.literal('-'))
)
)
);

@ -121,7 +121,8 @@ export const codes = [
"script_unknown_attribute",
"slot_element_deprecated",
"svelte_component_deprecated",
"svelte_element_invalid_this"
"svelte_element_invalid_this",
"svelte_self_deprecated"
];
/**
@ -809,4 +810,14 @@ export function svelte_component_deprecated(node) {
*/
export function svelte_element_invalid_this(node) {
w(node, "svelte_element_invalid_this", "`this` should be an `{expression}`. Using a string attribute value will cause an error in future versions of Svelte");
}
/**
* `<svelte:self>` is deprecated use self-imports (e.g. `import %name% from './%basename%'`) instead
* @param {null | NodeLike} node
* @param {string} name
* @param {string} basename
*/
export function svelte_self_deprecated(node, name, basename) {
w(node, "svelte_self_deprecated", `\`<svelte:self>\` is deprecated — use self-imports (e.g. \`import ${name} from './${basename}'\`) instead`);
}

@ -81,8 +81,6 @@ export function set_checked(element, checked) {
* @param {boolean} [skip_warning]
*/
export function set_attribute(element, attribute, value, skip_warning) {
value = value == null ? null : value + '';
// @ts-expect-error
var attributes = (element.__attributes ??= {});
@ -95,7 +93,7 @@ export function set_attribute(element, attribute, value, skip_warning) {
(attribute === 'href' && element.nodeName === 'LINK')
) {
if (!skip_warning) {
check_src_in_dev_hydration(element, attribute, value);
check_src_in_dev_hydration(element, attribute, value ?? '');
}
// If we reset these attributes, they would result in another network request, which we want to avoid.
@ -113,8 +111,11 @@ export function set_attribute(element, attribute, value, skip_warning) {
element[LOADING_ATTR_SYMBOL] = value;
}
if (value === null) {
if (value == null) {
element.removeAttribute(attribute);
} else if (typeof value !== 'string' && get_setters(element).includes(attribute)) {
// @ts-ignore
element[attribute] = value;
} else {
element.setAttribute(attribute, value);
}
@ -135,18 +136,8 @@ export function set_xlink_attribute(dom, attribute, value) {
* @param {any} value
*/
export function set_custom_element_data(node, prop, value) {
if (prop in node) {
// Reading the prop could cause an error, we don't want this to fail everything else
try {
var curr_val = node[prop];
} catch {
set_attribute(node, prop, value);
return;
}
var next_val = typeof curr_val === 'boolean' && value === '' ? true : value;
if (typeof curr_val !== 'object' || curr_val !== next_val) {
node[prop] = next_val;
}
if (get_setters(node).includes(prop)) {
node[prop] = value;
} else {
set_attribute(node, prop, value);
}
@ -160,6 +151,7 @@ export function set_custom_element_data(node, prop, value) {
* @param {string} [css_hash]
* @param {boolean} preserve_attribute_case
* @param {boolean} [skip_warning]
* @param {boolean} [is_custom_element]
* @returns {Record<string, any>}
*/
export function set_attributes(
@ -168,7 +160,8 @@ export function set_attributes(
next,
css_hash,
preserve_attribute_case = false,
skip_warning
skip_warning = false,
is_custom_element = false
) {
var current = prev || {};
var is_option_element = element.tagName === 'OPTION';
@ -183,8 +176,7 @@ export function set_attributes(
next.class = next.class ? next.class + ' ' + css_hash : css_hash;
}
var setters = setters_cache.get(element.nodeName);
if (!setters) setters_cache.set(element.nodeName, (setters = get_setters(element)));
var setters = get_setters(element);
// @ts-expect-error
var attributes = /** @type {Record<string, unknown>} **/ (element.__attributes ??= {});
@ -271,14 +263,11 @@ export function set_attributes(
delegate([event_name]);
}
}
} else if (value == null) {
attributes[key] = null;
element.removeAttribute(key);
} else if (key === 'style') {
} else if (key === 'style' && value != null) {
element.style.cssText = value + '';
} else if (key === 'autofocus') {
autofocus(/** @type {HTMLElement} */ (element), Boolean(value));
} else if (key === '__value' || key === 'value') {
} else if (key === '__value' || (key === 'value' && value != null)) {
// @ts-ignore
element.value = element[key] = element.__value = value;
} else {
@ -287,15 +276,18 @@ export function set_attributes(
name = normalize_attribute(name);
}
if (setters.includes(name)) {
if (value == null && !is_custom_element) {
attributes[key] = null;
element.removeAttribute(key);
} else if (setters.includes(name) && (is_custom_element || typeof value !== 'string')) {
// @ts-ignore
element[name] = value;
} else if (typeof value !== 'function') {
if (hydrating && (name === 'src' || name === 'href' || name === 'srcset')) {
if (!skip_warning) check_src_in_dev_hydration(element, name, value);
if (!skip_warning) check_src_in_dev_hydration(element, name, value ?? '');
} else {
// @ts-ignore
element[name] = value;
set_attribute(element, name, value);
}
} else if (typeof value !== 'function') {
set_attribute(element, name, value);
}
}
}
@ -316,57 +308,14 @@ export function set_attributes(
return current;
}
/**
* @param {Element} node
* @param {Record<string, any> | undefined} prev
* @param {Record<string, any>} next The new attributes - this function mutates this object
* @param {string} [css_hash]
*/
export function set_dynamic_element_attributes(node, prev, next, css_hash) {
if (node.tagName.includes('-')) {
for (var key in prev) {
if (!(key in next)) {
next[key] = null;
}
}
if (css_hash !== undefined) {
next.class = next.class ? next.class + ' ' + css_hash : css_hash;
}
for (key in next) {
set_custom_element_data(node, key, next[key]);
}
return next;
}
return set_attributes(
/** @type {Element & ElementCSSInlineStyle} */ (node),
prev,
next,
css_hash,
node.namespaceURI !== NAMESPACE_SVG
);
}
/**
* List of attributes that should always be set through the attr method,
* because updating them through the property setter doesn't work reliably.
* In the example of `width`/`height`, the problem is that the setter only
* accepts numeric values, but the attribute can also be set to a string like `50%`.
* In case of draggable trying to set `element.draggable='false'` will actually set
* draggable to `true`. If this list becomes too big, rethink this approach.
*/
var always_set_through_set_attribute = ['width', 'height', 'draggable'];
/** @type {Map<string, string[]>} */
var setters_cache = new Map();
/** @param {Element} element */
function get_setters(element) {
/** @type {string[]} */
var setters = [];
var setters = setters_cache.get(element.nodeName);
if (setters) return setters;
setters_cache.set(element.nodeName, (setters = []));
var descriptors;
var proto = get_prototype_of(element);
@ -375,7 +324,7 @@ function get_setters(element) {
descriptors = get_descriptors(proto);
for (var key in descriptors) {
if (descriptors[key].set && !always_set_through_set_attribute.includes(key)) {
if (descriptors[key].set) {
setters.push(key);
}
}
@ -389,12 +338,12 @@ function get_setters(element) {
/**
* @param {any} element
* @param {string} attribute
* @param {string | null} value
* @param {string} value
*/
function check_src_in_dev_hydration(element, attribute, value) {
if (!DEV) return;
if (attribute === 'srcset' && srcset_url_equal(element, value)) return;
if (src_url_equal(element.getAttribute(attribute) ?? '', value ?? '')) return;
if (src_url_equal(element.getAttribute(attribute) ?? '', value)) return;
w.hydration_attribute_changed(
attribute,
@ -420,12 +369,12 @@ function split_srcset(srcset) {
/**
* @param {HTMLSourceElement | HTMLImageElement} element
* @param {string | undefined | null} srcset
* @param {string} srcset
* @returns {boolean}
*/
function srcset_url_equal(element, srcset) {
var element_urls = split_srcset(element.srcset);
var urls = split_srcset(srcset ?? '');
var urls = split_srcset(srcset);
return (
urls.length === element_urls.length &&

@ -1,4 +1,4 @@
export { FILENAME, HMR } from '../../constants.js';
export { FILENAME, HMR, NAMESPACE_SVG } from '../../constants.js';
export { cleanup_styles } from './dev/css.js';
export { add_locations } from './dev/elements.js';
export { hmr } from './dev/hmr.js';
@ -30,7 +30,6 @@ export {
set_attribute,
set_attributes,
set_custom_element_data,
set_dynamic_element_attributes,
set_xlink_attribute,
handle_lazy_img,
set_value,

@ -147,6 +147,19 @@ export function head(payload, fn) {
head_payload.out += BLOCK_CLOSE;
}
/**
* `<div translate={false}>` should be rendered as `<div translate="no">` and _not_
* `<div translate="false">`, which is equivalent to `<div translate="yes">`. There
* may be other odd cases that need to be added to this list in future
* @type {Record<string, Map<any, string>>}
*/
const replacements = {
translate: new Map([
[true, 'yes'],
[false, 'no']
])
};
/**
* @template V
* @param {string} name
@ -156,7 +169,8 @@ export function head(payload, fn) {
*/
export function attr(name, value, is_boolean = false) {
if (value == null || (!value && is_boolean) || (value === '' && name === 'class')) return '';
const assignment = is_boolean ? '' : `="${escape_html(value, true)}"`;
const normalized = (name in replacements && replacements[name].get(value)) || value;
const assignment = is_boolean ? '' : `="${escape_html(normalized, true)}"`;
return ` ${name}${assignment}`;
}

@ -6,5 +6,5 @@
* https://svelte.dev/docs/svelte-compiler#svelte-version
* @type {string}
*/
export const VERSION = '5.0.0-next.254';
export const VERSION = '5.0.0-next.257';
export const PUBLIC_VERSION = '5';

@ -0,0 +1,24 @@
import { test } from '../../test';
export default test({
test({ assert, target, variant, hydrate }) {
function check(/** @type {boolean} */ condition) {
const divs = /** @type {NodeListOf<HTMLDivElement>} */ (
target.querySelectorAll(`.translate-${condition} div`)
);
divs.forEach((div, i) => {
assert.equal(div.translate, condition, `${i + 1} of ${divs.length}: ${div.outerHTML}`);
});
}
check(false);
check(true);
if (variant === 'hydrate') {
hydrate();
check(false);
check(true);
}
}
});

@ -0,0 +1,19 @@
<div class="translate-false">
<div translate={false}></div>
<div translate="no"></div>
<div {...{ translate: false }}></div>
<div {...{ translate: 'no' }}></div>
</div>
<div class="translate-true">
<div></div>
<div translate={true}></div>
<div translate="yes"></div>
<div {...{ translate: true }}></div>
<div {...{ translate: 'yes' }}></div>
<div translate="false"></div>
<div translate="banana"></div>
<div {...{ translate: 'false' }}></div>
<div {...{ translate: 'banana' }}></div>
</div>

@ -15,6 +15,7 @@
<svelte:component this={foo} class="{foo}" />
<!-- prettier-ignore -->
{#if foo}
<!-- svelte-ignore svelte_self_deprecated -->
<svelte:self class="{foo}" />
{/if}
<!-- prettier-ignore -->

@ -40,11 +40,11 @@
"message": "Quoted attributes on components and custom elements will be stringified in a future version of Svelte. If this isn't what you want, remove the quotes",
"start": {
"column": 14,
"line": 18
"line": 19
},
"end": {
"column": 27,
"line": 18
"line": 19
}
},
{
@ -52,11 +52,11 @@
"message": "Quoted attributes on components and custom elements will be stringified in a future version of Svelte. If this isn't what you want, remove the quotes",
"start": {
"column": 16,
"line": 21
"line": 22
},
"end": {
"column": 29,
"line": 21
"line": 22
}
}
]

@ -0,0 +1,5 @@
<svelte:options runes />
<script context="module">
let num = 2;
</script>

@ -4,11 +4,11 @@
"message": "`context=\"module\"` is deprecated, use the `module` attribute instead",
"start": {
"column": 8,
"line": 1
"line": 3
},
"end": {
"column": 24,
"line": 1
"line": 3
}
}
]

@ -0,0 +1,10 @@
<script>
let { n = 5 } = $props();
</script>
{#if n === 0}
<p>lift-off!</p>
{:else}
<p>{n}</p>
<svelte:self n={n - 1} />
{/if}

@ -0,0 +1,14 @@
[
{
"code": "svelte_self_deprecated",
"message": "`<svelte:self>` is deprecated — use self-imports (e.g. `import Self from './Self.svelte'`) instead",
"start": {
"line": 9,
"column": 1
},
"end": {
"line": 9,
"column": 26
}
}
]
Loading…
Cancel
Save