feat: allow ignoring runtime warnings (#12608)

* feat: allow ignoring binding_property_non_reactive

* chore: add comments before `to_ignore`

* chore: fix warnings regeneration

* chore: include client warnings code in svelte ignore extract

* feat: allow ignoring state_snapshot_uncloneable

* chore: abstract ignore into function

* feat: allow skipping of `hydration_attribute_changed`

* feat: allow skip of `hydration_html_changed`

* feat: allow skipping `ownership_invalid_binding`

* chore: revert extracting codes and use hardcoded list

* chore: update changeset

* feat: allow skipping `ownership_invalid_mutation`

* is_to_ignore -> is_ignored

* make is_ignored type safe

* tweak

* tweak naming

* tweak

* remove extra args

* comment is redundant, code contains enough information

* remove more unwanted args

* lint

---------

Co-authored-by: Rich Harris <rich.harris@vercel.com>
pull/12671/head
Paolo Ricciuti 2 months ago committed by GitHub
parent 4b1b886855
commit 64d2a2e20c
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -0,0 +1,5 @@
---
'svelte': patch
---
feat: allow ignoring runtime warnings

@ -16,7 +16,7 @@ import {
PROPS_IS_RUNES,
PROPS_IS_UPDATED
} from '../../../../constants.js';
import { dev } from '../../../state.js';
import { is_ignored, dev } from '../../../state.js';
/**
* @template {ClientTransformState} State
@ -282,6 +282,22 @@ export function serialize_set_binding(node, context, fallback, prefix, options)
);
}
/**
* @param {any} serialized
* @returns
*/
function maybe_skip_ownership_validation(serialized) {
if (is_ignored(node, 'ownership_invalid_mutation')) {
return b.call('$.skip_ownership_validation', b.thunk(serialized));
}
return serialized;
}
if (binding.kind === 'derived') {
return maybe_skip_ownership_validation(fallback());
}
const is_store = binding.kind === 'store_sub';
const left_name = is_store ? left.name.slice(1) : left.name;
@ -382,11 +398,13 @@ export function serialize_set_binding(node, context, fallback, prefix, options)
return /** @type {Expression} */ (visit(node));
}
return b.call(
return maybe_skip_ownership_validation(
b.call(
'$.store_mutate',
serialize_get_binding(b.id(left_name), state),
b.assignment(node.operator, /** @type {Pattern}} */ (visit_node(node.left)), value),
b.call('$.untrack', b.id('$' + left_name))
)
);
} else if (
!state.analysis.runes ||
@ -394,16 +412,20 @@ export function serialize_set_binding(node, context, fallback, prefix, options)
(binding.mutated && binding.kind === 'bindable_prop')
) {
if (binding.kind === 'bindable_prop') {
return b.call(
return maybe_skip_ownership_validation(
b.call(
left,
b.assignment(node.operator, /** @type {Pattern} */ (visit(node.left)), value),
b.true
)
);
} else {
return b.call(
return maybe_skip_ownership_validation(
b.call(
'$.mutate',
b.id(left_name),
b.assignment(node.operator, /** @type {Pattern} */ (visit(node.left)), value)
)
);
}
} else if (
@ -411,16 +433,20 @@ export function serialize_set_binding(node, context, fallback, prefix, options)
prefix != null &&
(node.operator === '+=' || node.operator === '-=')
) {
return b.update(
return maybe_skip_ownership_validation(
b.update(
node.operator === '+=' ? '++' : '--',
/** @type {Expression} */ (visit(node.left)),
prefix
)
);
} else {
return b.assignment(
return maybe_skip_ownership_validation(
b.assignment(
node.operator,
/** @type {Pattern} */ (visit(node.left)),
/** @type {Expression} */ (visit(node.right))
)
);
}
}

@ -3,6 +3,7 @@
import is_reference from 'is-reference';
import { serialize_get_binding, serialize_set_binding } from '../utils.js';
import * as b from '../../../../utils/builders.js';
import { is_ignored } from '../../../../state.js';
/** @type {Visitors} */
export const global_visitors = {
@ -115,6 +116,15 @@ export const global_visitors = {
return b.call(fn, ...args);
} else {
/** @param {any} serialized */
function maybe_skip_ownership_validation(serialized) {
if (is_ignored(node, 'ownership_invalid_mutation')) {
return b.call('$.skip_ownership_validation', b.thunk(serialized));
}
return serialized;
}
// turn it into an IIFEE assignment expression: i++ -> (() => { const $$value = i; i+=1; return $$value; })
const assignment = b.assignment(
node.operator === '++' ? '+=' : '-=',
@ -130,9 +140,9 @@ export const global_visitors = {
const value = /** @type {Expression} */ (visit(argument));
if (serialized_assignment === assignment) {
// No change to output -> nothing to transform -> we can keep the original update expression
return next();
return maybe_skip_ownership_validation(next());
} else if (context.state.analysis.runes) {
return serialized_assignment;
return maybe_skip_ownership_validation(serialized_assignment);
} else {
/** @type {Statement[]} */
let statements;
@ -146,7 +156,7 @@ export const global_visitors = {
b.return(b.id(tmp_id))
];
}
return b.call(b.thunk(b.block(statements)));
return maybe_skip_ownership_validation(b.call(b.thunk(b.block(statements))));
}
}
}

@ -1,10 +1,13 @@
/** @import { CallExpression, Expression, Identifier, Literal, MethodDefinition, PrivateIdentifier, PropertyDefinition, VariableDeclarator } from 'estree' */
/** @import { Binding } from '#compiler' */
/** @import { ComponentVisitors, StateField } from '../types.js' */
import { dev, is_ignored } from '../../../../state.js';
import * as assert from '../../../../utils/assert.js';
import { extract_paths } from '../../../../utils/ast.js';
import * as b from '../../../../utils/builders.js';
import { regex_invalid_identifier_chars } from '../../../patterns.js';
import { get_rune } from '../../../scope.js';
import { is_hoistable_function, transform_inspect_rune } from '../../utils.js';
import * as b from '../../../../utils/builders.js';
import * as assert from '../../../../utils/assert.js';
import {
get_prop_source,
is_prop_source,
@ -12,9 +15,6 @@ import {
serialize_proxy_reassignment,
should_proxy_or_freeze
} from '../utils.js';
import { extract_paths } from '../../../../utils/ast.js';
import { regex_invalid_identifier_chars } from '../../../patterns.js';
import { dev } from '../../../../state.js';
/** @type {ComponentVisitors} */
export const javascript_visitors_runes = {
@ -197,7 +197,9 @@ export const javascript_visitors_runes = {
b.call(
'$.add_owner',
b.call('$.get', b.member(b.this, b.private_id(name))),
b.id('owner')
b.id('owner'),
b.literal(false),
is_ignored(node, 'ownership_invalid_binding') && b.true
)
)
),
@ -446,7 +448,11 @@ export const javascript_visitors_runes = {
}
if (rune === '$state.snapshot') {
return b.call('$.snapshot', /** @type {Expression} */ (context.visit(node.arguments[0])));
return b.call(
'$.snapshot',
/** @type {Expression} */ (context.visit(node.arguments[0])),
is_ignored(node, 'state_snapshot_uncloneable') && b.true
);
}
if (rune === '$state.is') {

@ -3,6 +3,26 @@
/** @import { SourceLocation } from '#shared' */
/** @import { Scope } from '../../../scope.js' */
/** @import { ComponentClientTransformState, ComponentContext, ComponentVisitors } from '../types.js' */
import is_reference from 'is-reference';
import { walk } from 'zimmerframe';
import {
AttributeAliases,
DOMBooleanAttributes,
EACH_INDEX_REACTIVE,
EACH_IS_ANIMATED,
EACH_IS_CONTROLLED,
EACH_IS_STRICT_EQUALS,
EACH_ITEM_REACTIVE,
EACH_KEYED,
is_capture_event,
TEMPLATE_FRAGMENT,
TEMPLATE_USE_IMPORT_NODE,
TRANSITION_GLOBAL,
TRANSITION_IN,
TRANSITION_OUT
} from '../../../../../constants.js';
import { escape_html } from '../../../../../escaping.js';
import { dev, is_ignored, locator } from '../../../../state.js';
import {
extract_identifiers,
extract_paths,
@ -13,8 +33,9 @@ import {
object,
unwrap_optional
} from '../../../../utils/ast.js';
import * as b from '../../../../utils/builders.js';
import { sanitize_template_string } from '../../../../utils/sanitize_template_string.js';
import { binding_properties } from '../../../bindings.js';
import { clean_nodes, determine_namespace_for_children, infer_namespace } from '../../utils.js';
import {
DOMProperties,
LoadErrorElements,
@ -22,39 +43,18 @@ import {
VoidElements
} from '../../../constants.js';
import { is_custom_element_node, is_element_node } from '../../../nodes.js';
import * as b from '../../../../utils/builders.js';
import { regex_is_valid_identifier } from '../../../patterns.js';
import { clean_nodes, determine_namespace_for_children, infer_namespace } from '../../utils.js';
import {
with_loc,
create_derived,
create_derived_block_argument,
function_visitor,
get_assignment_value,
serialize_get_binding,
serialize_set_binding,
create_derived,
create_derived_block_argument
with_loc
} from '../utils.js';
import {
AttributeAliases,
DOMBooleanAttributes,
EACH_INDEX_REACTIVE,
EACH_IS_ANIMATED,
EACH_IS_CONTROLLED,
EACH_IS_STRICT_EQUALS,
EACH_ITEM_REACTIVE,
EACH_KEYED,
is_capture_event,
TEMPLATE_FRAGMENT,
TEMPLATE_USE_IMPORT_NODE,
TRANSITION_GLOBAL,
TRANSITION_IN,
TRANSITION_OUT
} from '../../../../../constants.js';
import { escape_html } from '../../../../../escaping.js';
import { regex_is_valid_identifier } from '../../../patterns.js';
import { javascript_visitors_runes } from './javascript-runes.js';
import { sanitize_template_string } from '../../../../utils/sanitize_template_string.js';
import { walk } from 'zimmerframe';
import { dev, locator } from '../../../../state.js';
import is_reference from 'is-reference';
/**
* @param {RegularElement | SvelteElement} element
@ -324,7 +324,8 @@ function serialize_element_spread_attributes(
b.id(id),
b.object(values),
lowercase_attributes,
b.literal(context.state.analysis.css.hash)
b.literal(context.state.analysis.css.hash),
is_ignored(element, 'hydration_attribute_changed') && b.true
)
)
);
@ -489,7 +490,15 @@ function serialize_element_attribute_update_assignment(element, node_id, attribu
// The foreign namespace doesn't have any special handling, everything goes through the attr function
if (context.state.metadata.namespace === 'foreign') {
const statement = b.stmt(b.call('$.set_attribute', node_id, b.literal(name), value));
const statement = b.stmt(
b.call(
'$.set_attribute',
node_id,
b.literal(name),
value,
is_ignored(element, 'hydration_attribute_changed') && b.true
)
);
if (attribute.metadata.expression.has_state) {
const id = state.scope.generate(`${node_id.name}_${name}`);
@ -525,7 +534,15 @@ function serialize_element_attribute_update_assignment(element, node_id, attribu
update = b.stmt(b.assignment('=', b.member(node_id, b.id(name)), value));
} else {
const callee = name.startsWith('xlink') ? '$.set_xlink_attribute' : '$.set_attribute';
update = b.stmt(b.call(callee, node_id, b.literal(name), value));
update = b.stmt(
b.call(
callee,
node_id,
b.literal(name),
value,
is_ignored(element, 'hydration_attribute_changed') && b.true
)
);
}
if (attribute.metadata.expression.has_state) {
@ -780,7 +797,12 @@ function serialize_inline_component(node, component_name, context, anchor = cont
} else if (attribute.type === 'BindDirective') {
const expression = /** @type {Expression} */ (context.visit(attribute.expression));
if (dev && expression.type === 'MemberExpression' && context.state.analysis.runes) {
if (
dev &&
expression.type === 'MemberExpression' &&
context.state.analysis.runes &&
!is_ignored(node, 'binding_property_non_reactive')
) {
context.state.init.push(serialize_validate_binding(context.state, attribute, expression));
}
@ -789,7 +811,14 @@ function serialize_inline_component(node, component_name, context, anchor = cont
} else {
if (dev) {
binding_initializers.push(
b.stmt(b.call(b.id('$.add_owner_effect'), b.thunk(expression), b.id(component_name)))
b.stmt(
b.call(
b.id('$.add_owner_effect'),
b.thunk(expression),
b.id(component_name),
is_ignored(node, 'ownership_invalid_binding') && b.true
)
)
);
}
@ -1811,7 +1840,8 @@ export const template_visitors = {
context.state.node,
b.thunk(/** @type {Expression} */ (context.visit(node.expression))),
b.literal(context.state.metadata.namespace === 'svg'),
b.literal(context.state.metadata.namespace === 'mathml')
b.literal(context.state.metadata.namespace === 'mathml'),
is_ignored(node, 'hydration_html_changed') && b.true
)
)
);
@ -2903,7 +2933,8 @@ export const template_visitors = {
type === 'KeyBlock'
)) &&
dev &&
context.state.analysis.runes
context.state.analysis.runes &&
!is_ignored(node, 'binding_property_non_reactive')
) {
context.state.init.push(
serialize_validate_binding(

@ -1,7 +1,8 @@
/** @import { CallExpression, Expression } from 'estree' */
/** @import { Context } from '../types.js' */
import { get_rune } from '../../../scope.js';
import { is_ignored } from '../../../../state.js';
import * as b from '../../../../utils/builders.js';
import { get_rune } from '../../../scope.js';
import { transform_inspect_rune } from '../../utils.js';
/**
@ -25,7 +26,11 @@ export function CallExpression(node, context) {
}
if (rune === '$state.snapshot') {
return b.call('$.snapshot', /** @type {Expression} */ (context.visit(node.arguments[0])));
return b.call(
'$.snapshot',
/** @type {Expression} */ (context.visit(node.arguments[0])),
is_ignored(node, 'state_snapshot_uncloneable') && b.true
);
}
if (rune === '$state.is') {

@ -65,6 +65,16 @@ export function reset_warning_filter(fn = () => true) {
warning_filter = fn;
}
/**
*
* @param {SvelteNode | NodeLike} node
* @param {import('../constants.js').IGNORABLE_RUNTIME_WARNINGS[number]} code
* @returns
*/
export function is_ignored(node, code) {
return dev && !!ignore_map.get(node)?.some((codes) => codes.has(code));
}
/**
* @param {string} _source
* @param {{ dev?: boolean; filename?: string; rootDir?: string }} options

@ -1,3 +1,4 @@
import { IGNORABLE_RUNTIME_WARNINGS } from '../../constants.js';
import fuzzymatch from '../phases/1-parse/utils/fuzzymatch.js';
import * as w from '../warnings.js';
@ -16,6 +17,8 @@ const replacements = {
'unused-export-let': 'export_let_unused'
};
const codes = w.codes.concat(IGNORABLE_RUNTIME_WARNINGS);
/**
* @param {number} offset
* @param {string} text
@ -37,7 +40,7 @@ export function extract_svelte_ignore(offset, text, runes) {
for (const match of text.slice(length).matchAll(/([\w$-]+)(,)?/gm)) {
const code = match[1];
if (w.codes.includes(code)) {
if (codes.includes(code)) {
ignores.push(code);
} else {
const replacement = replacements[code] ?? code.replace(/-/g, '_');
@ -46,10 +49,10 @@ export function extract_svelte_ignore(offset, text, runes) {
const start = offset + /** @type {number} */ (match.index);
const end = start + code.length;
if (w.codes.includes(replacement)) {
if (codes.includes(replacement)) {
w.legacy_code({ start, end }, code, replacement);
} else {
const suggestion = fuzzymatch(code, w.codes);
const suggestion = fuzzymatch(code, codes);
w.unknown_code({ start, end }, code, suggestion);
}
}
@ -65,10 +68,10 @@ export function extract_svelte_ignore(offset, text, runes) {
ignores.push(code);
if (!w.codes.includes(code)) {
if (!codes.includes(code)) {
const replacement = replacements[code] ?? code.replace(/-/g, '_');
if (w.codes.includes(replacement)) {
if (codes.includes(replacement)) {
ignores.push(replacement);
}
}

@ -199,6 +199,17 @@ const void_element_names = [
'wbr'
];
// we use a list of ignorable runtime warnings because not every runtime warning
// can be ignored and we want to keep the validation for svelte-ignore in place
export const IGNORABLE_RUNTIME_WARNINGS = /** @type {const} */ ([
'state_snapshot_uncloneable',
'binding_property_non_reactive',
'hydration_attribute_changed',
'hydration_html_changed',
'ownership_invalid_binding',
'ownership_invalid_mutation'
]);
/** @param {string} name */
export function is_void(name) {
return void_element_names.includes(name) || name.toLowerCase() === '!doctype';

@ -108,15 +108,16 @@ export function mark_module_end(component) {
* @param {any} object
* @param {any} owner
* @param {boolean} [global]
* @param {boolean} [skip_warning]
*/
export function add_owner(object, owner, global = false) {
export function add_owner(object, owner, global = false, skip_warning = false) {
if (object && !global) {
const component = dev_current_component_function;
const metadata = object[STATE_SYMBOL];
if (metadata && !has_owner(metadata, component)) {
let original = get_owner(metadata);
if (owner[FILENAME] !== component[FILENAME]) {
if (owner[FILENAME] !== component[FILENAME] && !skip_warning) {
w.ownership_invalid_binding(component[FILENAME], owner[FILENAME], original[FILENAME]);
}
}
@ -128,10 +129,11 @@ export function add_owner(object, owner, global = false) {
/**
* @param {() => unknown} get_object
* @param {any} Component
* @param {boolean} [skip_warning]
*/
export function add_owner_effect(get_object, Component) {
export function add_owner_effect(get_object, Component, skip_warning = false) {
user_pre_effect(() => {
add_owner(get_object(), Component);
add_owner(get_object(), Component, false, skip_warning);
});
}
@ -227,10 +229,23 @@ function get_owner(metadata) {
);
}
let skip = false;
/**
* @param {() => any} fn
*/
export function skip_ownership_validation(fn) {
skip = true;
fn();
skip = false;
}
/**
* @param {ProxyMetadata} metadata
*/
export function check_ownership(metadata) {
if (skip) return;
const component = get_component();
if (component && !has_owner(metadata, component)) {

@ -37,9 +37,10 @@ function check_hash(element, server_hash, value) {
* @param {() => string} get_value
* @param {boolean} svg
* @param {boolean} mathml
* @param {boolean} [skip_warning]
* @returns {void}
*/
export function html(node, get_value, svg, mathml) {
export function html(node, get_value, svg, mathml, skip_warning) {
var anchor = node;
var value = '';
@ -78,7 +79,7 @@ export function html(node, get_value, svg, mathml) {
throw HYDRATION_ERROR;
}
if (DEV) {
if (DEV && !skip_warning) {
check_hash(/** @type {Element} */ (next.parentNode), hash, value);
}

@ -82,8 +82,9 @@ export function set_checked(element, checked) {
* @param {Element} element
* @param {string} attribute
* @param {string | null} value
* @param {boolean} [skip_warning]
*/
export function set_attribute(element, attribute, value) {
export function set_attribute(element, attribute, value, skip_warning) {
value = value == null ? null : value + '';
// @ts-expect-error
@ -93,7 +94,9 @@ export function set_attribute(element, attribute, value) {
attributes[attribute] = element.getAttribute(attribute);
if (attribute === 'src' || attribute === 'href' || attribute === 'srcset') {
if (!skip_warning) {
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.
// We assume they are the same between client and server as checking if they are equal is expensive
@ -150,9 +153,10 @@ export function set_custom_element_data(node, prop, value) {
* @param {Record<string, any>} next New attributes - this function mutates this object
* @param {boolean} lowercase_attributes
* @param {string} css_hash
* @param {boolean} [skip_warning]
* @returns {Record<string, any>}
*/
export function set_attributes(element, prev, next, lowercase_attributes, css_hash) {
export function set_attributes(element, prev, next, lowercase_attributes, css_hash, skip_warning) {
var has_hash = css_hash.length !== 0;
var current = prev || {};
var is_option_element = element.tagName === 'OPTION';
@ -274,7 +278,7 @@ export function set_attributes(element, prev, next, lowercase_attributes, css_ha
if (setters.includes(name)) {
if (hydrating && (name === 'src' || name === 'href' || name === 'srcset')) {
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;

@ -6,7 +6,8 @@ export {
add_owner,
mark_module_start,
mark_module_end,
add_owner_effect
add_owner_effect,
skip_ownership_validation
} from './dev/ownership.js';
export { check_target, legacy_api } from './dev/legacy.js';
export { inspect } from './dev/inspect.js';

@ -14,18 +14,19 @@ const empty = [];
/**
* @template T
* @param {T} value
* @param {boolean} [skip_warning]
* @returns {Snapshot<T>}
*/
export function snapshot(value) {
export function snapshot(value, skip_warning = false) {
if (DEV) {
/** @type {string[]} */
const paths = [];
const copy = clone(value, new Map(), '', paths);
if (paths.length === 1 && paths[0] === '') {
if (paths.length === 1 && paths[0] === '' && !skip_warning) {
// value could not be cloned
w.state_snapshot_uncloneable();
} else if (paths.length > 0) {
} else if (paths.length > 0 && !skip_warning) {
// some properties could not be cloned
const slice = paths.length > 10 ? paths.slice(0, 7) : paths.slice(0, 10);
const excess = paths.length - slice.length;

@ -0,0 +1,11 @@
import { test } from '../../test';
export default test({
mode: ['client'],
compileOptions: {
dev: true
},
async test({ warnings, assert }) {
assert.deepEqual(warnings, []);
}
});

@ -0,0 +1,6 @@
<script>
let arr = [];
</script>
<!-- svelte-ignore binding_property_non_reactive -->
<input bind:value={arr[0]} />

@ -0,0 +1,11 @@
import { test } from '../../test';
export default test({
mode: ['client'],
compileOptions: {
dev: true
},
async test({ warnings, assert }) {
assert.deepEqual(warnings, []);
}
});

@ -0,0 +1,6 @@
<script>
let arr = [];
</script>
<!-- svelte-ignore binding_property_non_reactive -->
<svelte:component this={undefined} bind:this={arr[0]} />

@ -0,0 +1,16 @@
import { test } from '../../test';
export default test({
server_props: {
browser: false
},
props: {
browser: true
},
compileOptions: {
dev: true
},
async test({ warnings, assert }) {
assert.deepEqual(warnings, []);
}
});

@ -0,0 +1,6 @@
<script>
let { browser } = $props();
</script>
<!-- svelte-ignore hydration_attribute_changed -->
<img src={browser ? 'a' : 'b'} alt="" />

@ -0,0 +1,16 @@
import { test } from '../../test';
export default test({
server_props: {
browser: false
},
props: {
browser: true
},
compileOptions: {
dev: true
},
async test({ warnings, assert }) {
assert.deepEqual(warnings, []);
}
});

@ -0,0 +1,6 @@
<script>
let { browser } = $props();
</script>
<!-- svelte-ignore hydration_html_changed -->
{@html browser ? 'a' : 'b'}

@ -0,0 +1,5 @@
<script>
const { test = $bindable() } = $props();
</script>
{test}

@ -0,0 +1,8 @@
<script>
import Child from './Child.svelte';
let { test } = $props();
</script>
<!-- svelte-ignore ownership_invalid_binding -->
<Child bind:test />

@ -0,0 +1,11 @@
import { test } from '../../test';
export default test({
mode: ['client'],
compileOptions: {
dev: true
},
async test({ warnings, assert }) {
assert.deepEqual(warnings, []);
}
});

@ -0,0 +1,6 @@
<script>
import Parent from './Parent.svelte';
let test = $state({ test: '' });
</script>
<Parent {test} />

@ -0,0 +1,45 @@
<script>
let { test, store } = $props();
let der = $derived(test);
let state = $state(test);
</script>
<button
onclick={() => {
//svelte-ignore ownership_invalid_mutation
test.test = Math.random();
//svelte-ignore ownership_invalid_mutation
test.test++;
}}
>
</button>
<button
onclick={() => {
//svelte-ignore ownership_invalid_mutation
der.test = Math.random();
//svelte-ignore ownership_invalid_mutation
der.test++;
}}
>
</button>
<button
onclick={() => {
//svelte-ignore ownership_invalid_mutation
state.test = Math.random();
//svelte-ignore ownership_invalid_mutation
state.test++;
}}
>
</button>
<button
onclick={() => {
//svelte-ignore ownership_invalid_mutation
$store.test = Math.random();
//svelte-ignore ownership_invalid_mutation
$store.test++;
}}
>
</button>

@ -0,0 +1,19 @@
import { flushSync } from 'svelte';
import { test } from '../../test';
export default test({
mode: ['client'],
compileOptions: {
dev: true
},
async test({ warnings, assert, target }) {
const [btn1, btn2, btn3, btn4] = target.querySelectorAll('button');
flushSync(() => {
btn1.click();
btn2.click();
btn3.click();
btn4.click();
});
assert.deepEqual(warnings, []);
}
});

@ -0,0 +1,9 @@
<script>
import { writable } from 'svelte/store';
import Child from './Child.svelte';
let test = $state({ test: 'a' });
const store = writable(test);
</script>
<Child {test} {store} />

@ -0,0 +1,11 @@
import { test } from '../../test';
export default test({
mode: ['client'],
compileOptions: {
dev: true
},
async test({ warnings, assert }) {
assert.deepEqual(warnings, []);
}
});

@ -0,0 +1,11 @@
<script>
let arr = $state({
test: () => {}
});
// svelte-ignore state_snapshot_uncloneable
$state.snapshot(arr);
</script>
<!-- svelte-ignore state_snapshot_uncloneable -->
<div {...$state.snapshot(arr)}>a</div>
Loading…
Cancel
Save