Merge remote-tracking branch 'origin/main' into fix-inspect-trace-highlight

pull/14811/head
paoloricciuti 2 years ago
commit ba9b56ba99

@ -0,0 +1,5 @@
---
'svelte': patch
---
fix: ensure $inspect.trace works correctly with null values

@ -1,23 +0,0 @@
---
title: class:
---
The `class:` directive is a convenient way to conditionally set classes on elements, as an alternative to using conditional expressions inside `class` attributes:
```svelte
<!-- These are equivalent -->
<div class={isCool ? 'cool' : ''}>...</div>
<div class:cool={isCool}>...</div>
```
As with other directives, we can use a shorthand when the name of the class coincides with the value:
```svelte
<div class:cool>...</div>
```
Multiple `class:` directives can be added to a single element:
```svelte
<div class:cool class:lame={!cool} class:potato>...</div>
```

@ -0,0 +1,90 @@
---
title: class
---
There are two ways to set classes on elements: the `class` attribute, and the `class:` directive.
## Attributes
Primitive values are treated like any other attribute:
```svelte
<div class={large ? 'large' : 'small'}>...</div>
```
> [!NOTE]
> For historical reasons, falsy values (like `false` and `NaN`) are stringified (`class="false"`), though `class={undefined}` (or `null`) cause the attribute to be omitted altogether. In a future version of Svelte, all falsy values will cause `class` to be omitted.
### Objects and arrays
Since Svelte 5.16, `class` can be an object or array, and is converted to a string using [clsx](https://github.com/lukeed/clsx).
If the value is an object, the truthy keys are added:
```svelte
<script>
let { cool } = $props();
</script>
<!-- results in `class="cool"` if `cool` is truthy,
`class="lame"` otherwise -->
<div class={{ cool, lame: !cool }}>...</div>
```
If the value is an array, the truthy values are combined:
```svelte
<!-- if `faded` and `large` are both truthy, results in
`class="saturate-0 opacity-50 scale-200"` -->
<div class={[faded && 'saturate-0 opacity-50', large && 'scale-200']}>...</div>
```
Note that whether we're using the array or object form, we can set multiple classes simultaneously with a single condition, which is particularly useful if you're using things like Tailwind.
Arrays can contain arrays and objects, and clsx will flatten them. This is useful for combining local classes with props, for example:
```svelte
<!--- file: Button.svelte --->
<script>
let props = $props();
</script>
<button {...props} class={['cool-button', props.class]}>
{@render props.children?.()}
</button>
```
The user of this component has the same flexibility to use a mixture of objects, arrays and strings:
```svelte
<!--- file: App.svelte --->
<script>
import Button from './Button.svelte';
let useTailwind = $state(false);
</script>
<Button
onclick={() => useTailwind = true}
class={{ 'bg-blue-700 sm:w-1/2': useTailwind }}
>
Accept the inevitability of Tailwind
</Button>
```
## The `class:` directive
Prior to Svelte 5.16, the `class:` directive was the most convenient way to set classes on elements conditionally.
```svelte
<!-- These are equivalent -->
<div class={{ cool, lame: !cool }}>...</div>
<div class:cool={cool} class:lame={!cool}>...</div>
```
As with other directives, we can use a shorthand when the name of the class coincides with the value:
```svelte
<div class:cool class:lame={!cool}>...</div>
```
> [!NOTE] Unless you're using an older version of Svelte, consider avoiding `class:`, since the attribute is more powerful and composable.

@ -21,15 +21,19 @@ A component is attempting to bind to a non-bindable property `%key%` belonging t
### component_api_changed ### component_api_changed
``` ```
%parent% called `%method%` on an instance of %component%, which is no longer valid in Svelte 5. See https://svelte.dev/docs/svelte/v5-migration-guide#Components-are-no-longer-classes for more information %parent% called `%method%` on an instance of %component%, which is no longer valid in Svelte 5
``` ```
See the [migration guide](/docs/svelte/v5-migration-guide#Components-are-no-longer-classes) for more information.
### component_api_invalid_new ### component_api_invalid_new
``` ```
Attempted to instantiate %component% with `new %name%`, which is no longer valid in Svelte 5. If this component is not under your control, set the `compatibility.componentApi` compiler option to `4` to keep it working. See https://svelte.dev/docs/svelte/v5-migration-guide#Components-are-no-longer-classes for more information Attempted to instantiate %component% with `new %name%`, which is no longer valid in Svelte 5. If this component is not under your control, set the `compatibility.componentApi` compiler option to `4` to keep it working.
``` ```
See the [migration guide](/docs/svelte/v5-migration-guide#Components-are-no-longer-classes) for more information.
### derived_references_self ### derived_references_self
``` ```

@ -52,7 +52,7 @@ Your `console.%method%` contained `$state` proxies. Consider using `$inspect(...
When logging a [proxy](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Proxy), browser devtools will log the proxy itself rather than the value it represents. In the case of Svelte, the 'target' of a `$state` proxy might not resemble its current value, which can be confusing. When logging a [proxy](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Proxy), browser devtools will log the proxy itself rather than the value it represents. In the case of Svelte, the 'target' of a `$state` proxy might not resemble its current value, which can be confusing.
The easiest way to log a value as it changes over time is to use the [`$inspect`](https://svelte.dev/docs/svelte/$inspect) rune. Alternatively, to log things on a one-off basis (for example, inside an event handler) you can use [`$state.snapshot`](https://svelte.dev/docs/svelte/$state#$state.snapshot) to take a snapshot of the current value. The easiest way to log a value as it changes over time is to use the [`$inspect`](/docs/svelte/$inspect) rune. Alternatively, to log things on a one-off basis (for example, inside an event handler) you can use [`$state.snapshot`](/docs/svelte/$state#$state.snapshot) to take a snapshot of the current value.
### event_handler_invalid ### event_handler_invalid

@ -62,7 +62,7 @@ Enforce that `autofocus` is not used on elements. Autofocusing elements can caus
### a11y_click_events_have_key_events ### a11y_click_events_have_key_events
``` ```
Visible, non-interactive elements with a click event must be accompanied by a keyboard event handler. Consider whether an interactive element such as `<button type="button">` or `<a>` might be more appropriate. See https://svelte.dev/docs/accessibility-warnings#a11y-click-events-have-key-events for more details Visible, non-interactive elements with a click event must be accompanied by a keyboard event handler. Consider whether an interactive element such as `<button type="button">` or `<a>` might be more appropriate
``` ```
Enforce that visible, non-interactive elements with an `onclick` event are accompanied by a keyboard event handler. Enforce that visible, non-interactive elements with an `onclick` event are accompanied by a keyboard event handler.

@ -1,5 +1,15 @@
# svelte # svelte
## 5.16.0
### Minor Changes
- feat: allow `class` attribute to be an object or array, using `clsx` ([#14714](https://github.com/sveltejs/svelte/pull/14714))
### Patch Changes
- fix: don't include keyframes in global scope in the keyframes to rename ([#14822](https://github.com/sveltejs/svelte/pull/14822))
## 5.15.0 ## 5.15.0
### Minor Changes ### Minor Changes

@ -741,7 +741,7 @@ export interface HTMLAttributes<T extends EventTarget> extends AriaAttributes, D
accesskey?: string | undefined | null; accesskey?: string | undefined | null;
autocapitalize?: 'characters' | 'off' | 'on' | 'none' | 'sentences' | 'words' | undefined | null; autocapitalize?: 'characters' | 'off' | 'on' | 'none' | 'sentences' | 'words' | undefined | null;
autofocus?: boolean | undefined | null; autofocus?: boolean | undefined | null;
class?: string | undefined | null; class?: string | import('clsx').ClassArray | import('clsx').ClassDictionary | undefined | null;
contenteditable?: Booleanish | 'inherit' | 'plaintext-only' | undefined | null; contenteditable?: Booleanish | 'inherit' | 'plaintext-only' | undefined | null;
contextmenu?: string | undefined | null; contextmenu?: string | undefined | null;
dir?: 'ltr' | 'rtl' | 'auto' | undefined | null; dir?: 'ltr' | 'rtl' | 'auto' | undefined | null;
@ -1522,7 +1522,7 @@ export interface SvelteWindowAttributes extends HTMLAttributes<Window> {
export interface SVGAttributes<T extends EventTarget> extends AriaAttributes, DOMAttributes<T> { export interface SVGAttributes<T extends EventTarget> extends AriaAttributes, DOMAttributes<T> {
// Attributes which also defined in HTMLAttributes // Attributes which also defined in HTMLAttributes
className?: string | undefined | null; className?: string | undefined | null;
class?: string | undefined | null; class?: string | import('clsx').ClassArray | import('clsx').ClassDictionary | undefined | null;
color?: string | undefined | null; color?: string | undefined | null;
height?: number | string | undefined | null; height?: number | string | undefined | null;
id?: string | undefined | null; id?: string | undefined | null;

@ -12,11 +12,15 @@
## component_api_changed ## component_api_changed
> %parent% called `%method%` on an instance of %component%, which is no longer valid in Svelte 5. See https://svelte.dev/docs/svelte/v5-migration-guide#Components-are-no-longer-classes for more information > %parent% called `%method%` on an instance of %component%, which is no longer valid in Svelte 5
See the [migration guide](/docs/svelte/v5-migration-guide#Components-are-no-longer-classes) for more information.
## component_api_invalid_new ## component_api_invalid_new
> Attempted to instantiate %component% with `new %name%`, which is no longer valid in Svelte 5. If this component is not under your control, set the `compatibility.componentApi` compiler option to `4` to keep it working. See https://svelte.dev/docs/svelte/v5-migration-guide#Components-are-no-longer-classes for more information > Attempted to instantiate %component% with `new %name%`, which is no longer valid in Svelte 5. If this component is not under your control, set the `compatibility.componentApi` compiler option to `4` to keep it working.
See the [migration guide](/docs/svelte/v5-migration-guide#Components-are-no-longer-classes) for more information.
## derived_references_self ## derived_references_self

@ -42,7 +42,7 @@ function add() {
When logging a [proxy](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Proxy), browser devtools will log the proxy itself rather than the value it represents. In the case of Svelte, the 'target' of a `$state` proxy might not resemble its current value, which can be confusing. When logging a [proxy](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Proxy), browser devtools will log the proxy itself rather than the value it represents. In the case of Svelte, the 'target' of a `$state` proxy might not resemble its current value, which can be confusing.
The easiest way to log a value as it changes over time is to use the [`$inspect`](https://svelte.dev/docs/svelte/$inspect) rune. Alternatively, to log things on a one-off basis (for example, inside an event handler) you can use [`$state.snapshot`](https://svelte.dev/docs/svelte/$state#$state.snapshot) to take a snapshot of the current value. The easiest way to log a value as it changes over time is to use the [`$inspect`](/docs/svelte/$inspect) rune. Alternatively, to log things on a one-off basis (for example, inside an event handler) you can use [`$state.snapshot`](/docs/svelte/$state#$state.snapshot) to take a snapshot of the current value.
## event_handler_invalid ## event_handler_invalid

@ -49,7 +49,7 @@ Enforce that `autofocus` is not used on elements. Autofocusing elements can caus
## a11y_click_events_have_key_events ## a11y_click_events_have_key_events
> Visible, non-interactive elements with a click event must be accompanied by a keyboard event handler. Consider whether an interactive element such as `<button type="button">` or `<a>` might be more appropriate. See https://svelte.dev/docs/accessibility-warnings#a11y-click-events-have-key-events for more details > Visible, non-interactive elements with a click event must be accompanied by a keyboard event handler. Consider whether an interactive element such as `<button type="button">` or `<a>` might be more appropriate
Enforce that visible, non-interactive elements with an `onclick` event are accompanied by a keyboard event handler. Enforce that visible, non-interactive elements with an `onclick` event are accompanied by a keyboard event handler.

@ -2,7 +2,7 @@
"name": "svelte", "name": "svelte",
"description": "Cybernetically enhanced web apps", "description": "Cybernetically enhanced web apps",
"license": "MIT", "license": "MIT",
"version": "5.15.0", "version": "5.16.0",
"type": "module", "type": "module",
"types": "./types/index.d.ts", "types": "./types/index.d.ts",
"engines": { "engines": {
@ -153,6 +153,7 @@
"acorn-typescript": "^1.4.13", "acorn-typescript": "^1.4.13",
"aria-query": "^5.3.1", "aria-query": "^5.3.1",
"axobject-query": "^4.1.0", "axobject-query": "^4.1.0",
"clsx": "^2.1.1",
"esm-env": "^1.2.1", "esm-env": "^1.2.1",
"esrap": "^1.3.2", "esrap": "^1.3.2",
"is-reference": "^3.0.3", "is-reference": "^3.0.3",

@ -28,11 +28,19 @@ function is_global_block_selector(simple_selector) {
); );
} }
/**
*
* @param {Array<AST.CSS.Node>} path
*/
function is_in_global_block(path) {
return path.some((node) => node.type === 'Rule' && node.metadata.is_global_block);
}
/** @type {CssVisitors} */ /** @type {CssVisitors} */
const css_visitors = { const css_visitors = {
Atrule(node, context) { Atrule(node, context) {
if (is_keyframes_node(node)) { if (is_keyframes_node(node)) {
if (!node.prelude.startsWith('-global-')) { if (!node.prelude.startsWith('-global-') && !is_in_global_block(context.path)) {
context.state.keyframes.push(node.prelude); context.state.keyframes.push(node.prelude);
} }
} }

@ -731,7 +731,7 @@ function attribute_matches(node, name, expected_value, operator, case_insensitiv
/** @type {string[]} */ /** @type {string[]} */
let prev_values = []; let prev_values = [];
for (const chunk of chunks) { for (const chunk of chunks) {
const current_possible_values = get_possible_values(chunk); const current_possible_values = get_possible_values(chunk, name === 'class');
// impossible to find out all combinations // impossible to find out all combinations
if (!current_possible_values) return true; if (!current_possible_values) return true;
@ -784,7 +784,7 @@ function attribute_matches(node, name, expected_value, operator, case_insensitiv
prev_values.push(current_possible_value); prev_values.push(current_possible_value);
} }
}); });
if (prev_values.length < current_possible_values.size) { if (prev_values.length < current_possible_values.length) {
prev_values.push(' '); prev_values.push(' ');
} }
if (prev_values.length > 20) { if (prev_values.length > 20) {

@ -4,14 +4,74 @@ const UNKNOWN = {};
/** /**
* @param {Node} node * @param {Node} node
* @param {boolean} is_class
* @param {Set<any>} set * @param {Set<any>} set
* @param {boolean} is_nested
*/ */
function gather_possible_values(node, set) { function gather_possible_values(node, is_class, set, is_nested = false) {
if (set.has(UNKNOWN)) {
// no point traversing any further
return;
}
if (node.type === 'Literal') { if (node.type === 'Literal') {
set.add(String(node.value)); set.add(String(node.value));
} else if (node.type === 'ConditionalExpression') { } else if (node.type === 'ConditionalExpression') {
gather_possible_values(node.consequent, set); gather_possible_values(node.consequent, is_class, set, is_nested);
gather_possible_values(node.alternate, set); gather_possible_values(node.alternate, is_class, set, is_nested);
} else if (node.type === 'LogicalExpression') {
if (node.operator === '&&') {
// && is a special case, because the only way the left
// hand value can be included is if it's falsy. this is
// a bit of extra work but it's worth it because
// `class={[condition && 'blah']}` is common,
// and we don't want to deopt on `condition`
const left = new Set();
gather_possible_values(node.left, is_class, left, is_nested);
if (left.has(UNKNOWN)) {
// add all non-nullish falsy values, unless this is a `class` attribute that
// will be processed by cslx, in which case falsy values are removed, unless
// they're not inside an array/object (TODO 6.0 remove that last part)
if (!is_class || !is_nested) {
set.add('');
set.add(false);
set.add(NaN);
set.add(0); // -0 and 0n are also falsy, but stringify to '0'
}
} else {
for (const value of left) {
if (!value && value != undefined && (!is_class || !is_nested)) {
set.add(value);
}
}
}
gather_possible_values(node.right, is_class, set, is_nested);
} else {
gather_possible_values(node.left, is_class, set, is_nested);
gather_possible_values(node.right, is_class, set, is_nested);
}
} else if (is_class && node.type === 'ArrayExpression') {
for (const entry of node.elements) {
if (entry) {
gather_possible_values(entry, is_class, set, true);
}
}
} else if (is_class && node.type === 'ObjectExpression') {
for (const property of node.properties) {
if (
property.type === 'Property' &&
!property.computed &&
(property.key.type === 'Identifier' || property.key.type === 'Literal')
) {
set.add(
property.key.type === 'Identifier' ? property.key.name : String(property.key.value)
);
} else {
set.add(UNKNOWN);
}
}
} else { } else {
set.add(UNKNOWN); set.add(UNKNOWN);
} }
@ -19,19 +79,20 @@ function gather_possible_values(node, set) {
/** /**
* @param {AST.Text | AST.ExpressionTag} chunk * @param {AST.Text | AST.ExpressionTag} chunk
* @returns {Set<string> | null} * @param {boolean} is_class
* @returns {string[] | null}
*/ */
export function get_possible_values(chunk) { export function get_possible_values(chunk, is_class) {
const values = new Set(); const values = new Set();
if (chunk.type === 'Text') { if (chunk.type === 'Text') {
values.add(chunk.data); values.add(chunk.data);
} else { } else {
gather_possible_values(chunk.expression, values); gather_possible_values(chunk.expression, is_class, values);
} }
if (values.has(UNKNOWN)) return null; if (values.has(UNKNOWN)) return null;
return values; return [...values].map((value) => String(value));
} }
/** /**

@ -773,6 +773,8 @@ export function analyze_component(root, source, options) {
if (attribute.type !== 'Attribute') continue; if (attribute.type !== 'Attribute') continue;
if (attribute.name.toLowerCase() !== 'class') continue; if (attribute.name.toLowerCase() !== 'class') continue;
// The dynamic class method appends the hash to the end of the class attribute on its own
if (attribute.metadata.needs_clsx) continue outer;
class_attribute = attribute; class_attribute = attribute;
} }

@ -38,6 +38,19 @@ export function Attribute(node, context) {
mark_subtree_dynamic(context.path); mark_subtree_dynamic(context.path);
} }
// class={[...]} or class={{...}} or `class={x}` need clsx to resolve the classes
if (
node.name === 'class' &&
!Array.isArray(node.value) &&
node.value !== true &&
node.value.expression.type !== 'Literal' &&
node.value.expression.type !== 'TemplateLiteral' &&
node.value.expression.type !== 'BinaryExpression'
) {
mark_subtree_dynamic(context.path);
node.metadata.needs_clsx = true;
}
if (node.value !== true) { if (node.value !== true) {
for (const chunk of get_attribute_chunks(node.value)) { for (const chunk of get_attribute_chunks(node.value)) {
if (chunk.type !== 'ExpressionTag') continue; if (chunk.type !== 'ExpressionTag') continue;

@ -553,6 +553,10 @@ function build_element_attribute_update_assignment(
let update; let update;
if (name === 'class') { if (name === 'class') {
if (attribute.metadata.needs_clsx) {
value = b.call('$.clsx', value);
}
if (attribute.metadata.expression.has_state && has_call) { if (attribute.metadata.expression.has_state && has_call) {
// ensure we're not creating a separate template effect for this so that // ensure we're not creating a separate template effect for this so that
// potential class directives are added to the same effect and therefore always apply // potential class directives are added to the same effect and therefore always apply
@ -561,11 +565,13 @@ function build_element_attribute_update_assignment(
value = b.call('$.get', id); value = b.call('$.get', id);
has_call = false; has_call = false;
} }
update = b.stmt( update = b.stmt(
b.call( b.call(
is_svg ? '$.set_svg_class' : is_mathml ? '$.set_mathml_class' : '$.set_class', is_svg ? '$.set_svg_class' : is_mathml ? '$.set_mathml_class' : '$.set_class',
node_id, node_id,
value value,
attribute.metadata.needs_clsx ? b.literal(context.state.analysis.css.hash) : undefined
) )
); );
} else if (name === 'value') { } else if (name === 'value') {

@ -86,10 +86,35 @@ export function build_element_attributes(node, context) {
} else if (attribute.name !== 'defaultValue' && attribute.name !== 'defaultChecked') { } else if (attribute.name !== 'defaultValue' && attribute.name !== 'defaultChecked') {
if (attribute.name === 'class') { if (attribute.name === 'class') {
class_index = attributes.length; class_index = attributes.length;
} else if (attribute.name === 'style') {
style_index = attributes.length; if (attribute.metadata.needs_clsx) {
const clsx_value = b.call(
'$.clsx',
/** @type {AST.ExpressionTag} */ (attribute.value).expression
);
attributes.push({
...attribute,
value: {
.../** @type {AST.ExpressionTag} */ (attribute.value),
expression: context.state.analysis.css.hash
? b.binary(
'+',
b.binary('+', clsx_value, b.literal(' ')),
b.literal(context.state.analysis.css.hash)
)
: clsx_value
}
});
} else {
attributes.push(attribute);
}
} else {
if (attribute.name === 'style') {
style_index = attributes.length;
}
attributes.push(attribute);
} }
attributes.push(attribute);
} }
} else if (attribute.type === 'BindDirective') { } else if (attribute.type === 'BindDirective') {
if (attribute.name === 'value' && node.name === 'select') continue; if (attribute.name === 'value' && node.name === 'select') continue;

@ -45,7 +45,8 @@ export function create_attribute(name, start, end, value) {
value, value,
metadata: { metadata: {
expression: create_expression_metadata(), expression: create_expression_metadata(),
delegated: null delegated: null,
needs_clsx: false
} }
}; };
} }

@ -482,6 +482,8 @@ export namespace AST {
expression: ExpressionMetadata; expression: ExpressionMetadata;
/** May be set if this is an event attribute */ /** May be set if this is an event attribute */
delegated: null | DelegatedEvent; delegated: null | DelegatedEvent;
/** May be `true` if this is a `class` attribute that needs `clsx` */
needs_clsx: boolean;
}; };
} }

@ -168,11 +168,11 @@ export function a11y_autofocus(node) {
} }
/** /**
* Visible, non-interactive elements with a click event must be accompanied by a keyboard event handler. Consider whether an interactive element such as `<button type="button">` or `<a>` might be more appropriate. See https://svelte.dev/docs/accessibility-warnings#a11y-click-events-have-key-events for more details * Visible, non-interactive elements with a click event must be accompanied by a keyboard event handler. Consider whether an interactive element such as `<button type="button">` or `<a>` might be more appropriate
* @param {null | NodeLike} node * @param {null | NodeLike} node
*/ */
export function a11y_click_events_have_key_events(node) { export function a11y_click_events_have_key_events(node) {
w(node, "a11y_click_events_have_key_events", `Visible, non-interactive elements with a click event must be accompanied by a keyboard event handler. Consider whether an interactive element such as \`<button type="button">\` or \`<a>\` might be more appropriate. See https://svelte.dev/docs/accessibility-warnings#a11y-click-events-have-key-events for more details\nhttps://svelte.dev/e/a11y_click_events_have_key_events`); w(node, "a11y_click_events_have_key_events", `Visible, non-interactive elements with a click event must be accompanied by a keyboard event handler. Consider whether an interactive element such as \`<button type="button">\` or \`<a>\` might be more appropriate\nhttps://svelte.dev/e/a11y_click_events_have_key_events`);
} }
/** /**

@ -51,7 +51,9 @@ function log_entry(signal, entry) {
status !== 'clean' status !== 'clean'
? 'color: CornflowerBlue; font-weight: bold' ? 'color: CornflowerBlue; font-weight: bold'
: 'color: grey; font-weight: bold', : 'color: grey; font-weight: bold',
typeof value === 'object' && STATE_SYMBOL in value ? snapshot(value, true) : value typeof value === 'object' && value !== null && STATE_SYMBOL in value
? snapshot(value, true)
: value
); );
if (type === '$derived') { if (type === '$derived') {

@ -13,6 +13,7 @@ import {
set_active_effect, set_active_effect,
set_active_reaction set_active_reaction
} from '../../runtime.js'; } from '../../runtime.js';
import { clsx } from '../../../shared/attributes.js';
/** /**
* The value/checked attribute in the template actually corresponds to the defaultValue property, so we need * The value/checked attribute in the template actually corresponds to the defaultValue property, so we need
@ -267,6 +268,10 @@ export function set_attributes(
} }
} }
if (next.class) {
next.class = clsx(next.class);
}
if (css_hash !== undefined) { if (css_hash !== undefined) {
next.class = next.class ? next.class + ' ' + css_hash : css_hash; next.class = next.class ? next.class + ' ' + css_hash : css_hash;
} }

@ -3,12 +3,13 @@ import { hydrating } from '../hydration.js';
/** /**
* @param {SVGElement} dom * @param {SVGElement} dom
* @param {string} value * @param {string} value
* @param {string} [hash]
* @returns {void} * @returns {void}
*/ */
export function set_svg_class(dom, value) { export function set_svg_class(dom, value, hash) {
// @ts-expect-error need to add __className to patched prototype // @ts-expect-error need to add __className to patched prototype
var prev_class_name = dom.__className; var prev_class_name = dom.__className;
var next_class_name = to_class(value); var next_class_name = to_class(value, hash);
if (hydrating && dom.getAttribute('class') === next_class_name) { if (hydrating && dom.getAttribute('class') === next_class_name) {
// In case of hydration don't reset the class as it's already correct. // In case of hydration don't reset the class as it's already correct.
@ -32,12 +33,13 @@ export function set_svg_class(dom, value) {
/** /**
* @param {MathMLElement} dom * @param {MathMLElement} dom
* @param {string} value * @param {string} value
* @param {string} [hash]
* @returns {void} * @returns {void}
*/ */
export function set_mathml_class(dom, value) { export function set_mathml_class(dom, value, hash) {
// @ts-expect-error need to add __className to patched prototype // @ts-expect-error need to add __className to patched prototype
var prev_class_name = dom.__className; var prev_class_name = dom.__className;
var next_class_name = to_class(value); var next_class_name = to_class(value, hash);
if (hydrating && dom.getAttribute('class') === next_class_name) { if (hydrating && dom.getAttribute('class') === next_class_name) {
// In case of hydration don't reset the class as it's already correct. // In case of hydration don't reset the class as it's already correct.
@ -61,12 +63,13 @@ export function set_mathml_class(dom, value) {
/** /**
* @param {HTMLElement} dom * @param {HTMLElement} dom
* @param {string} value * @param {string} value
* @param {string} [hash]
* @returns {void} * @returns {void}
*/ */
export function set_class(dom, value) { export function set_class(dom, value, hash) {
// @ts-expect-error need to add __className to patched prototype // @ts-expect-error need to add __className to patched prototype
var prev_class_name = dom.__className; var prev_class_name = dom.__className;
var next_class_name = to_class(value); var next_class_name = to_class(value, hash);
if (hydrating && dom.className === next_class_name) { if (hydrating && dom.className === next_class_name) {
// In case of hydration don't reset the class as it's already correct. // In case of hydration don't reset the class as it's already correct.
@ -79,7 +82,7 @@ export function set_class(dom, value) {
// Removing the attribute when the value is only an empty string causes // Removing the attribute when the value is only an empty string causes
// peformance issues vs simply making the className an empty string. So // peformance issues vs simply making the className an empty string. So
// we should only remove the class if the the value is nullish. // we should only remove the class if the the value is nullish.
if (value == null) { if (value == null && !hash) {
dom.removeAttribute('class'); dom.removeAttribute('class');
} else { } else {
dom.className = next_class_name; dom.className = next_class_name;
@ -93,10 +96,11 @@ export function set_class(dom, value) {
/** /**
* @template V * @template V
* @param {V} value * @param {V} value
* @param {string} [hash]
* @returns {string | V} * @returns {string | V}
*/ */
function to_class(value) { function to_class(value, hash) {
return value == null ? '' : value; return (value == null ? '' : value) + (hash ? ' ' + hash : '');
} }
/** /**

@ -54,7 +54,7 @@ export function bind_not_bindable(key, component, name) {
} }
/** /**
* %parent% called `%method%` on an instance of %component%, which is no longer valid in Svelte 5. See https://svelte.dev/docs/svelte/v5-migration-guide#Components-are-no-longer-classes for more information * %parent% called `%method%` on an instance of %component%, which is no longer valid in Svelte 5
* @param {string} parent * @param {string} parent
* @param {string} method * @param {string} method
* @param {string} component * @param {string} component
@ -62,7 +62,7 @@ export function bind_not_bindable(key, component, name) {
*/ */
export function component_api_changed(parent, method, component) { export function component_api_changed(parent, method, component) {
if (DEV) { if (DEV) {
const error = new Error(`component_api_changed\n${parent} called \`${method}\` on an instance of ${component}, which is no longer valid in Svelte 5. See https://svelte.dev/docs/svelte/v5-migration-guide#Components-are-no-longer-classes for more information\nhttps://svelte.dev/e/component_api_changed`); const error = new Error(`component_api_changed\n${parent} called \`${method}\` on an instance of ${component}, which is no longer valid in Svelte 5\nhttps://svelte.dev/e/component_api_changed`);
error.name = 'Svelte error'; error.name = 'Svelte error';
throw error; throw error;
@ -72,14 +72,14 @@ export function component_api_changed(parent, method, component) {
} }
/** /**
* Attempted to instantiate %component% with `new %name%`, which is no longer valid in Svelte 5. If this component is not under your control, set the `compatibility.componentApi` compiler option to `4` to keep it working. See https://svelte.dev/docs/svelte/v5-migration-guide#Components-are-no-longer-classes for more information * Attempted to instantiate %component% with `new %name%`, which is no longer valid in Svelte 5. If this component is not under your control, set the `compatibility.componentApi` compiler option to `4` to keep it working.
* @param {string} component * @param {string} component
* @param {string} name * @param {string} name
* @returns {never} * @returns {never}
*/ */
export function component_api_invalid_new(component, name) { export function component_api_invalid_new(component, name) {
if (DEV) { if (DEV) {
const error = new Error(`component_api_invalid_new\nAttempted to instantiate ${component} with \`new ${name}\`, which is no longer valid in Svelte 5. If this component is not under your control, set the \`compatibility.componentApi\` compiler option to \`4\` to keep it working. See https://svelte.dev/docs/svelte/v5-migration-guide#Components-are-no-longer-classes for more information\nhttps://svelte.dev/e/component_api_invalid_new`); const error = new Error(`component_api_invalid_new\nAttempted to instantiate ${component} with \`new ${name}\`, which is no longer valid in Svelte 5. If this component is not under your control, set the \`compatibility.componentApi\` compiler option to \`4\` to keep it working.\nhttps://svelte.dev/e/component_api_invalid_new`);
error.name = 'Svelte error'; error.name = 'Svelte error';
throw error; throw error;

@ -161,7 +161,7 @@ export {
$window as window, $window as window,
$document as document $document as document
} from './dom/operations.js'; } from './dom/operations.js';
export { attr } from '../shared/attributes.js'; export { attr, clsx } from '../shared/attributes.js';
export { snapshot } from '../shared/clone.js'; export { snapshot } from '../shared/clone.js';
export { noop, fallback } from '../shared/utils.js'; export { noop, fallback } from '../shared/utils.js';
export { export {

@ -2,7 +2,7 @@
/** @import { Component, Payload, RenderOutput } from '#server' */ /** @import { Component, Payload, RenderOutput } from '#server' */
/** @import { Store } from '#shared' */ /** @import { Store } from '#shared' */
export { FILENAME, HMR } from '../../constants.js'; export { FILENAME, HMR } from '../../constants.js';
import { attr } from '../shared/attributes.js'; import { attr, clsx } from '../shared/attributes.js';
import { is_promise, noop } from '../shared/utils.js'; import { is_promise, noop } from '../shared/utils.js';
import { subscribe_to_store } from '../../store/utils.js'; import { subscribe_to_store } from '../../store/utils.js';
import { import {
@ -195,6 +195,10 @@ export function spread_attributes(attrs, classes, styles, flags = 0) {
: style_object_to_string(styles); : style_object_to_string(styles);
} }
if (attrs.class) {
attrs.class = clsx(attrs.class);
}
if (classes) { if (classes) {
const classlist = attrs.class ? [attrs.class] : []; const classlist = attrs.class ? [attrs.class] : [];
@ -522,7 +526,7 @@ export function once(get_value) {
}; };
} }
export { attr }; export { attr, clsx };
export { html } from './blocks/html.js'; export { html } from './blocks/html.js';

@ -1,4 +1,5 @@
import { escape_html } from '../../escaping.js'; import { escape_html } from '../../escaping.js';
import { clsx as _clsx } from 'clsx';
/** /**
* `<div translate={false}>` should be rendered as `<div translate="no">` and _not_ * `<div translate={false}>` should be rendered as `<div translate="no">` and _not_
@ -26,3 +27,16 @@ export function attr(name, value, is_boolean = false) {
const assignment = is_boolean ? '' : `="${escape_html(normalized, true)}"`; const assignment = is_boolean ? '' : `="${escape_html(normalized, true)}"`;
return ` ${name}${assignment}`; return ` ${name}${assignment}`;
} }
/**
* Small wrapper around clsx to preserve Svelte's (weird) handling of falsy values.
* TODO Svelte 6 revisit this, and likely turn all falsy values into the empty string (what clsx also does)
* @param {any} value
*/
export function clsx(value) {
if (typeof value === 'object') {
return _clsx(value);
} else {
return value ?? '';
}
}

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

@ -0,0 +1,20 @@
import { test } from '../../test';
export default test({
warnings: [
{
code: 'css_unused_selector',
message: 'Unused CSS selector ".unused"\nhttps://svelte.dev/e/css_unused_selector',
start: {
line: 24,
column: 1,
character: 548
},
end: {
line: 24,
column: 8,
character: 555
}
}
]
});

@ -0,0 +1,12 @@
.used1.svelte-xyz { color: green; }
.used2.svelte-xyz { color: green; }
.used3.svelte-xyz { color: green; }
.used4.svelte-xyz { color: green; }
.used5.svelte-xyz { color: green; }
.used6.svelte-xyz { color: green; }
.used7.svelte-xyz { color: green; }
.used8.svelte-xyz { color: green; }
.used9.svelte-xyz { color: green; }
/* (unused) .unused { color: red; }*/

@ -0,0 +1,25 @@
<script>
let condition = Math.random() < 0.5;
</script>
<p class={['used1']}></p>
<p class={[{ used2: true }]}></p>
<p class={{ used3: true }}></p>
<p class={{ 'used4 used5': true }}></p>
<p class={{ used6 }}></p>
<p class={[condition ? 'used7' : 'used8']}></p>
<p class={[condition && 'used9']}></p>
<style>
.used1 { color: green; }
.used2 { color: green; }
.used3 { color: green; }
.used4 { color: green; }
.used5 { color: green; }
.used6 { color: green; }
.used7 { color: green; }
.used8 { color: green; }
.used9 { color: green; }
.unused { color: red; }
</style>

@ -0,0 +1,5 @@
<h1 class={[foo]}>hello world</h1>
<style>
.x { color: green; }
</style>

@ -0,0 +1,5 @@
<h1 class={{ foo: true, ...rest }}>hello world</h1>
<style>
.x { color: green; }
</style>

@ -0,0 +1,5 @@
<h1 class={{ [foo]: true }}>hello world</h1>
<style>
.x { color: green; }
</style>

@ -74,6 +74,10 @@
animation: svelte-xyz-test 1s; animation: svelte-xyz-test 1s;
} }
.y{
animation: test-in 1s;
}
@keyframes test-in{ @keyframes test-in{
to{ to{
opacity: 1; opacity: 1;

@ -76,6 +76,10 @@
animation: test 1s; animation: test 1s;
} }
.y{
animation: test-in 1s;
}
@keyframes test-in{ @keyframes test-in{
to{ to{
opacity: 1; opacity: 1;

@ -40,7 +40,7 @@ export default test({
assert.equal(div.className, 'true'); assert.equal(div.className, 'true');
component.testName = {}; component.testName = {};
assert.equal(div.className, '[object Object]'); assert.equal(div.className, '');
component.testName = ''; component.testName = '';
assert.equal(div.className, ''); assert.equal(div.className, '');

@ -32,7 +32,7 @@ export default test({
assert.equal(div.className, 'true svelte-x1o6ra'); assert.equal(div.className, 'true svelte-x1o6ra');
component.testName = {}; component.testName = {};
assert.equal(div.className, '[object Object] svelte-x1o6ra'); assert.equal(div.className, ' svelte-x1o6ra');
component.testName = ''; component.testName = '';
assert.equal(div.className, ' svelte-x1o6ra'); assert.equal(div.className, ' svelte-x1o6ra');

@ -40,7 +40,7 @@ export default test({
assert.equal(div.className, 'true'); assert.equal(div.className, 'true');
component.testName = {}; component.testName = {};
assert.equal(div.className, '[object Object]'); assert.equal(div.className, '');
component.testName = ''; component.testName = '';
assert.equal(div.className, ''); assert.equal(div.className, '');

@ -40,7 +40,7 @@ export default test({
assert.equal(div.className, 'true svelte-x1o6ra'); assert.equal(div.className, 'true svelte-x1o6ra');
component.testName = {}; component.testName = {};
assert.equal(div.className, '[object Object] svelte-x1o6ra'); assert.equal(div.className, ' svelte-x1o6ra');
component.testName = ''; component.testName = '';
assert.equal(div.className, ' svelte-x1o6ra'); assert.equal(div.className, ' svelte-x1o6ra');

@ -0,0 +1,43 @@
import { test } from '../../test';
export default test({
html: `
<div class="foo svelte-owbekl"></div>
<div class="foo svelte-owbekl"></div>
<div class="foo svelte-owbekl"></div>
<div class="foo svelte-owbekl"></div>
<div class="foo svelte-owbekl"></div>
<div class="foo">child</div>
<div class="foo">child</div>
<div class="foo">child</div>
<div class="foo">child</div>
<div class="foo">child</div>
<button>update</button>
`,
test({ assert, target }) {
const button = target.querySelector('button');
button?.click();
assert.htmlEqual(
target.innerHTML,
`
<div class="foo svelte-owbekl"></div>
<div class="foo svelte-owbekl"></div>
<div class="foo svelte-owbekl"></div>
<div class="foo svelte-owbekl"></div>
<div class="foo svelte-owbekl"></div>
<div class="foo">child</div>
<div class="foo">child</div>
<div class="foo">child</div>
<div class="foo">child</div>
<div class="foo">child</div>
<button>update</button>
`
);
}
});

@ -0,0 +1,5 @@
<script>
let { class: cls } = $props();
</script>
<div class={cls}>child</div>

@ -0,0 +1,33 @@
<script>
import Child from "./child.svelte";
let foo = $state('foo');
let bar = $state(null);
let spread = { class: { foo: true, bar: false } };
</script>
<div class={{ foo: true, bar: false }}></div>
<div class={['foo', false && 'bar']}></div>
<div class={{ foo, bar }}></div>
<div class={[ foo, bar ]}></div>
<div {...spread}></div>
<Child class={{ foo: true, bar: false }} />
<Child class={['foo', false && 'bar']} />
<Child class={{ foo, bar }} />
<Child class={[ foo, bar ]} />
<Child {...spread} />
<button onclick={() => {
foo = null;
bar = 'bar';
}}>update</button>
<style>
.foo {
color: red;
}
.bar {
color: blue;
}
</style>

@ -1,7 +1,7 @@
[ [
{ {
"code": "a11y_click_events_have_key_events", "code": "a11y_click_events_have_key_events",
"message": "Visible, non-interactive elements with a click event must be accompanied by a keyboard event handler. Consider whether an interactive element such as `<button type=\"button\">` or `<a>` might be more appropriate. See https://svelte.dev/docs/accessibility-warnings#a11y-click-events-have-key-events for more details", "message": "Visible, non-interactive elements with a click event must be accompanied by a keyboard event handler. Consider whether an interactive element such as `<button type=\"button\">` or `<a>` might be more appropriate",
"start": { "start": {
"line": 13, "line": 13,
"column": 0 "column": 0
@ -13,7 +13,7 @@
}, },
{ {
"code": "a11y_click_events_have_key_events", "code": "a11y_click_events_have_key_events",
"message": "Visible, non-interactive elements with a click event must be accompanied by a keyboard event handler. Consider whether an interactive element such as `<button type=\"button\">` or `<a>` might be more appropriate. See https://svelte.dev/docs/accessibility-warnings#a11y-click-events-have-key-events for more details", "message": "Visible, non-interactive elements with a click event must be accompanied by a keyboard event handler. Consider whether an interactive element such as `<button type=\"button\">` or `<a>` might be more appropriate",
"start": { "start": {
"line": 15, "line": 15,
"column": 0 "column": 0
@ -25,7 +25,7 @@
}, },
{ {
"code": "a11y_click_events_have_key_events", "code": "a11y_click_events_have_key_events",
"message": "Visible, non-interactive elements with a click event must be accompanied by a keyboard event handler. Consider whether an interactive element such as `<button type=\"button\">` or `<a>` might be more appropriate. See https://svelte.dev/docs/accessibility-warnings#a11y-click-events-have-key-events for more details", "message": "Visible, non-interactive elements with a click event must be accompanied by a keyboard event handler. Consider whether an interactive element such as `<button type=\"button\">` or `<a>` might be more appropriate",
"start": { "start": {
"line": 18, "line": 18,
"column": 0 "column": 0
@ -37,7 +37,7 @@
}, },
{ {
"code": "a11y_click_events_have_key_events", "code": "a11y_click_events_have_key_events",
"message": "Visible, non-interactive elements with a click event must be accompanied by a keyboard event handler. Consider whether an interactive element such as `<button type=\"button\">` or `<a>` might be more appropriate. See https://svelte.dev/docs/accessibility-warnings#a11y-click-events-have-key-events for more details", "message": "Visible, non-interactive elements with a click event must be accompanied by a keyboard event handler. Consider whether an interactive element such as `<button type=\"button\">` or `<a>` might be more appropriate",
"start": { "start": {
"line": 20, "line": 20,
"column": 0 "column": 0
@ -49,7 +49,7 @@
}, },
{ {
"code": "a11y_click_events_have_key_events", "code": "a11y_click_events_have_key_events",
"message": "Visible, non-interactive elements with a click event must be accompanied by a keyboard event handler. Consider whether an interactive element such as `<button type=\"button\">` or `<a>` might be more appropriate. See https://svelte.dev/docs/accessibility-warnings#a11y-click-events-have-key-events for more details", "message": "Visible, non-interactive elements with a click event must be accompanied by a keyboard event handler. Consider whether an interactive element such as `<button type=\"button\">` or `<a>` might be more appropriate",
"start": { "start": {
"line": 22, "line": 22,
"column": 0 "column": 0
@ -61,7 +61,7 @@
}, },
{ {
"code": "a11y_click_events_have_key_events", "code": "a11y_click_events_have_key_events",
"message": "Visible, non-interactive elements with a click event must be accompanied by a keyboard event handler. Consider whether an interactive element such as `<button type=\"button\">` or `<a>` might be more appropriate. See https://svelte.dev/docs/accessibility-warnings#a11y-click-events-have-key-events for more details", "message": "Visible, non-interactive elements with a click event must be accompanied by a keyboard event handler. Consider whether an interactive element such as `<button type=\"button\">` or `<a>` might be more appropriate",
"start": { "start": {
"line": 24, "line": 24,
"column": 0 "column": 0
@ -73,7 +73,7 @@
}, },
{ {
"code": "a11y_click_events_have_key_events", "code": "a11y_click_events_have_key_events",
"message": "Visible, non-interactive elements with a click event must be accompanied by a keyboard event handler. Consider whether an interactive element such as `<button type=\"button\">` or `<a>` might be more appropriate. See https://svelte.dev/docs/accessibility-warnings#a11y-click-events-have-key-events for more details", "message": "Visible, non-interactive elements with a click event must be accompanied by a keyboard event handler. Consider whether an interactive element such as `<button type=\"button\">` or `<a>` might be more appropriate",
"start": { "start": {
"line": 26, "line": 26,
"column": 0 "column": 0
@ -85,7 +85,7 @@
}, },
{ {
"code": "a11y_click_events_have_key_events", "code": "a11y_click_events_have_key_events",
"message": "Visible, non-interactive elements with a click event must be accompanied by a keyboard event handler. Consider whether an interactive element such as `<button type=\"button\">` or `<a>` might be more appropriate. See https://svelte.dev/docs/accessibility-warnings#a11y-click-events-have-key-events for more details", "message": "Visible, non-interactive elements with a click event must be accompanied by a keyboard event handler. Consider whether an interactive element such as `<button type=\"button\">` or `<a>` might be more appropriate",
"start": { "start": {
"line": 28, "line": 28,
"column": 0 "column": 0

@ -1,7 +1,7 @@
[ [
{ {
"code": "a11y_click_events_have_key_events", "code": "a11y_click_events_have_key_events",
"message": "Visible, non-interactive elements with a click event must be accompanied by a keyboard event handler. Consider whether an interactive element such as `<button type=\"button\">` or `<a>` might be more appropriate. See https://svelte.dev/docs/accessibility-warnings#a11y-click-events-have-key-events for more details", "message": "Visible, non-interactive elements with a click event must be accompanied by a keyboard event handler. Consider whether an interactive element such as `<button type=\"button\">` or `<a>` might be more appropriate",
"start": { "start": {
"column": 1, "column": 1,
"line": 7 "line": 7

@ -1,7 +1,7 @@
[ [
{ {
"code": "a11y_click_events_have_key_events", "code": "a11y_click_events_have_key_events",
"message": "Visible, non-interactive elements with a click event must be accompanied by a keyboard event handler. Consider whether an interactive element such as `<button type=\"button\">` or `<a>` might be more appropriate. See https://svelte.dev/docs/accessibility-warnings#a11y-click-events-have-key-events for more details", "message": "Visible, non-interactive elements with a click event must be accompanied by a keyboard event handler. Consider whether an interactive element such as `<button type=\"button\">` or `<a>` might be more appropriate",
"start": { "start": {
"column": 1, "column": 1,
"line": 8 "line": 8

@ -80,6 +80,9 @@ importers:
axobject-query: axobject-query:
specifier: ^4.1.0 specifier: ^4.1.0
version: 4.1.0 version: 4.1.0
clsx:
specifier: ^2.1.1
version: 2.1.1
esm-env: esm-env:
specifier: ^1.2.1 specifier: ^1.2.1
version: 1.2.1 version: 1.2.1
@ -890,6 +893,10 @@ packages:
resolution: {integrity: sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ==} resolution: {integrity: sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ==}
engines: {node: '>=8'} engines: {node: '>=8'}
clsx@2.1.1:
resolution: {integrity: sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==}
engines: {node: '>=6'}
color-convert@2.0.1: color-convert@2.0.1:
resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==}
engines: {node: '>=7.0.0'} engines: {node: '>=7.0.0'}
@ -3055,6 +3062,8 @@ snapshots:
ci-info@3.9.0: {} ci-info@3.9.0: {}
clsx@2.1.1: {}
color-convert@2.0.1: color-convert@2.0.1:
dependencies: dependencies:
color-name: 1.1.4 color-name: 1.1.4

Loading…
Cancel
Save