From 64d2a2e20cae31acf2a318f9be22bef846b1abcf Mon Sep 17 00:00:00 2001 From: Paolo Ricciuti Date: Wed, 31 Jul 2024 04:45:59 +0200 Subject: [PATCH] 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 --- .changeset/large-emus-cough.md | 5 + .../phases/3-transform/client/utils.js | 70 ++++++++----- .../3-transform/client/visitors/global.js | 16 ++- .../client/visitors/javascript-runes.js | 20 ++-- .../3-transform/client/visitors/template.js | 99 ++++++++++++------- .../server/visitors/CallExpression.js | 9 +- packages/svelte/src/compiler/state.js | 10 ++ .../compiler/utils/extract_svelte_ignore.js | 13 ++- packages/svelte/src/constants.js | 11 +++ .../src/internal/client/dev/ownership.js | 23 ++++- .../src/internal/client/dom/blocks/html.js | 5 +- .../client/dom/elements/attributes.js | 12 ++- packages/svelte/src/internal/client/index.js | 3 +- packages/svelte/src/internal/shared/clone.js | 7 +- .../_config.js | 11 +++ .../main.svelte | 6 ++ .../_config.js | 11 +++ .../main.svelte | 6 ++ .../_config.js | 16 +++ .../main.svelte | 6 ++ .../hydration-html-changed-ignored/_config.js | 16 +++ .../main.svelte | 6 ++ .../Child.svelte | 5 + .../Parent.svelte | 8 ++ .../_config.js | 11 +++ .../main.svelte | 6 ++ .../Child.svelte | 45 +++++++++ .../_config.js | 19 ++++ .../main.svelte | 9 ++ .../_config.js | 11 +++ .../main.svelte | 11 +++ 31 files changed, 419 insertions(+), 87 deletions(-) create mode 100644 .changeset/large-emus-cough.md create mode 100644 packages/svelte/tests/runtime-runes/samples/binding-property-non-reactive-ignored-2/_config.js create mode 100644 packages/svelte/tests/runtime-runes/samples/binding-property-non-reactive-ignored-2/main.svelte create mode 100644 packages/svelte/tests/runtime-runes/samples/binding-property-non-reactive-ignored/_config.js create mode 100644 packages/svelte/tests/runtime-runes/samples/binding-property-non-reactive-ignored/main.svelte create mode 100644 packages/svelte/tests/runtime-runes/samples/hydration-attribute-changed-ignored/_config.js create mode 100644 packages/svelte/tests/runtime-runes/samples/hydration-attribute-changed-ignored/main.svelte create mode 100644 packages/svelte/tests/runtime-runes/samples/hydration-html-changed-ignored/_config.js create mode 100644 packages/svelte/tests/runtime-runes/samples/hydration-html-changed-ignored/main.svelte create mode 100644 packages/svelte/tests/runtime-runes/samples/ownership-invalid-binding-ignored/Child.svelte create mode 100644 packages/svelte/tests/runtime-runes/samples/ownership-invalid-binding-ignored/Parent.svelte create mode 100644 packages/svelte/tests/runtime-runes/samples/ownership-invalid-binding-ignored/_config.js create mode 100644 packages/svelte/tests/runtime-runes/samples/ownership-invalid-binding-ignored/main.svelte create mode 100644 packages/svelte/tests/runtime-runes/samples/ownership-invalid-mutation-ignored/Child.svelte create mode 100644 packages/svelte/tests/runtime-runes/samples/ownership-invalid-mutation-ignored/_config.js create mode 100644 packages/svelte/tests/runtime-runes/samples/ownership-invalid-mutation-ignored/main.svelte create mode 100644 packages/svelte/tests/runtime-runes/samples/state-snapshot-uncloneable-ignored/_config.js create mode 100644 packages/svelte/tests/runtime-runes/samples/state-snapshot-uncloneable-ignored/main.svelte diff --git a/.changeset/large-emus-cough.md b/.changeset/large-emus-cough.md new file mode 100644 index 0000000000..b282b5faec --- /dev/null +++ b/.changeset/large-emus-cough.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +feat: allow ignoring runtime warnings diff --git a/packages/svelte/src/compiler/phases/3-transform/client/utils.js b/packages/svelte/src/compiler/phases/3-transform/client/utils.js index 825452fec8..704ccbe6d8 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/utils.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/utils.js @@ -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( - '$.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)) + 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( - left, - b.assignment(node.operator, /** @type {Pattern} */ (visit(node.left)), value), - b.true + return maybe_skip_ownership_validation( + b.call( + left, + b.assignment(node.operator, /** @type {Pattern} */ (visit(node.left)), value), + b.true + ) ); } else { - return b.call( - '$.mutate', - b.id(left_name), - b.assignment(node.operator, /** @type {Pattern} */ (visit(node.left)), value) + 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( - node.operator === '+=' ? '++' : '--', - /** @type {Expression} */ (visit(node.left)), - prefix + return maybe_skip_ownership_validation( + b.update( + node.operator === '+=' ? '++' : '--', + /** @type {Expression} */ (visit(node.left)), + prefix + ) ); } else { - return b.assignment( - node.operator, - /** @type {Pattern} */ (visit(node.left)), - /** @type {Expression} */ (visit(node.right)) + return maybe_skip_ownership_validation( + b.assignment( + node.operator, + /** @type {Pattern} */ (visit(node.left)), + /** @type {Expression} */ (visit(node.right)) + ) ); } } diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/global.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/global.js index 8d4698c663..ac4fcc16c0 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/global.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/global.js @@ -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)))); } } } diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/javascript-runes.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/javascript-runes.js index e8b8a20de4..77c889ac8e 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/javascript-runes.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/javascript-runes.js @@ -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') { diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/template.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/template.js index dacb810b47..7d2de190e9 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/template.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/template.js @@ -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( diff --git a/packages/svelte/src/compiler/phases/3-transform/server/visitors/CallExpression.js b/packages/svelte/src/compiler/phases/3-transform/server/visitors/CallExpression.js index 91ff0f6771..4b15a772c9 100644 --- a/packages/svelte/src/compiler/phases/3-transform/server/visitors/CallExpression.js +++ b/packages/svelte/src/compiler/phases/3-transform/server/visitors/CallExpression.js @@ -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') { diff --git a/packages/svelte/src/compiler/state.js b/packages/svelte/src/compiler/state.js index 13b6f95545..3359b899bf 100644 --- a/packages/svelte/src/compiler/state.js +++ b/packages/svelte/src/compiler/state.js @@ -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 diff --git a/packages/svelte/src/compiler/utils/extract_svelte_ignore.js b/packages/svelte/src/compiler/utils/extract_svelte_ignore.js index ff440d4c41..2f0d387307 100644 --- a/packages/svelte/src/compiler/utils/extract_svelte_ignore.js +++ b/packages/svelte/src/compiler/utils/extract_svelte_ignore.js @@ -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); } } diff --git a/packages/svelte/src/constants.js b/packages/svelte/src/constants.js index 0cc2102925..12b17ecb30 100644 --- a/packages/svelte/src/constants.js +++ b/packages/svelte/src/constants.js @@ -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'; diff --git a/packages/svelte/src/internal/client/dev/ownership.js b/packages/svelte/src/internal/client/dev/ownership.js index 7d10118788..62d4a43e94 100644 --- a/packages/svelte/src/internal/client/dev/ownership.js +++ b/packages/svelte/src/internal/client/dev/ownership.js @@ -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)) { diff --git a/packages/svelte/src/internal/client/dom/blocks/html.js b/packages/svelte/src/internal/client/dom/blocks/html.js index 3c4051261e..63ca704c0f 100644 --- a/packages/svelte/src/internal/client/dom/blocks/html.js +++ b/packages/svelte/src/internal/client/dom/blocks/html.js @@ -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); } diff --git a/packages/svelte/src/internal/client/dom/elements/attributes.js b/packages/svelte/src/internal/client/dom/elements/attributes.js index 1321470062..6288aac197 100644 --- a/packages/svelte/src/internal/client/dom/elements/attributes.js +++ b/packages/svelte/src/internal/client/dom/elements/attributes.js @@ -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') { - check_src_in_dev_hydration(element, attribute, value); + 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} next New attributes - this function mutates this object * @param {boolean} lowercase_attributes * @param {string} css_hash + * @param {boolean} [skip_warning] * @returns {Record} */ -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; diff --git a/packages/svelte/src/internal/client/index.js b/packages/svelte/src/internal/client/index.js index c70cb6674b..edd50bff32 100644 --- a/packages/svelte/src/internal/client/index.js +++ b/packages/svelte/src/internal/client/index.js @@ -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'; diff --git a/packages/svelte/src/internal/shared/clone.js b/packages/svelte/src/internal/shared/clone.js index 9983ef619a..9027c15c4f 100644 --- a/packages/svelte/src/internal/shared/clone.js +++ b/packages/svelte/src/internal/shared/clone.js @@ -14,18 +14,19 @@ const empty = []; /** * @template T * @param {T} value + * @param {boolean} [skip_warning] * @returns {Snapshot} */ -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; diff --git a/packages/svelte/tests/runtime-runes/samples/binding-property-non-reactive-ignored-2/_config.js b/packages/svelte/tests/runtime-runes/samples/binding-property-non-reactive-ignored-2/_config.js new file mode 100644 index 0000000000..e93067eb9d --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/binding-property-non-reactive-ignored-2/_config.js @@ -0,0 +1,11 @@ +import { test } from '../../test'; + +export default test({ + mode: ['client'], + compileOptions: { + dev: true + }, + async test({ warnings, assert }) { + assert.deepEqual(warnings, []); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/binding-property-non-reactive-ignored-2/main.svelte b/packages/svelte/tests/runtime-runes/samples/binding-property-non-reactive-ignored-2/main.svelte new file mode 100644 index 0000000000..218cfe7506 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/binding-property-non-reactive-ignored-2/main.svelte @@ -0,0 +1,6 @@ + + + + diff --git a/packages/svelte/tests/runtime-runes/samples/binding-property-non-reactive-ignored/_config.js b/packages/svelte/tests/runtime-runes/samples/binding-property-non-reactive-ignored/_config.js new file mode 100644 index 0000000000..e93067eb9d --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/binding-property-non-reactive-ignored/_config.js @@ -0,0 +1,11 @@ +import { test } from '../../test'; + +export default test({ + mode: ['client'], + compileOptions: { + dev: true + }, + async test({ warnings, assert }) { + assert.deepEqual(warnings, []); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/binding-property-non-reactive-ignored/main.svelte b/packages/svelte/tests/runtime-runes/samples/binding-property-non-reactive-ignored/main.svelte new file mode 100644 index 0000000000..4cbd69d06b --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/binding-property-non-reactive-ignored/main.svelte @@ -0,0 +1,6 @@ + + + + diff --git a/packages/svelte/tests/runtime-runes/samples/hydration-attribute-changed-ignored/_config.js b/packages/svelte/tests/runtime-runes/samples/hydration-attribute-changed-ignored/_config.js new file mode 100644 index 0000000000..6bbad1bf64 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/hydration-attribute-changed-ignored/_config.js @@ -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, []); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/hydration-attribute-changed-ignored/main.svelte b/packages/svelte/tests/runtime-runes/samples/hydration-attribute-changed-ignored/main.svelte new file mode 100644 index 0000000000..c06bf95341 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/hydration-attribute-changed-ignored/main.svelte @@ -0,0 +1,6 @@ + + + + diff --git a/packages/svelte/tests/runtime-runes/samples/hydration-html-changed-ignored/_config.js b/packages/svelte/tests/runtime-runes/samples/hydration-html-changed-ignored/_config.js new file mode 100644 index 0000000000..6bbad1bf64 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/hydration-html-changed-ignored/_config.js @@ -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, []); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/hydration-html-changed-ignored/main.svelte b/packages/svelte/tests/runtime-runes/samples/hydration-html-changed-ignored/main.svelte new file mode 100644 index 0000000000..fc1e4f4e50 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/hydration-html-changed-ignored/main.svelte @@ -0,0 +1,6 @@ + + + +{@html browser ? 'a' : 'b'} diff --git a/packages/svelte/tests/runtime-runes/samples/ownership-invalid-binding-ignored/Child.svelte b/packages/svelte/tests/runtime-runes/samples/ownership-invalid-binding-ignored/Child.svelte new file mode 100644 index 0000000000..78b82caed9 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/ownership-invalid-binding-ignored/Child.svelte @@ -0,0 +1,5 @@ + + +{test} diff --git a/packages/svelte/tests/runtime-runes/samples/ownership-invalid-binding-ignored/Parent.svelte b/packages/svelte/tests/runtime-runes/samples/ownership-invalid-binding-ignored/Parent.svelte new file mode 100644 index 0000000000..1a724a3832 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/ownership-invalid-binding-ignored/Parent.svelte @@ -0,0 +1,8 @@ + + + + diff --git a/packages/svelte/tests/runtime-runes/samples/ownership-invalid-binding-ignored/_config.js b/packages/svelte/tests/runtime-runes/samples/ownership-invalid-binding-ignored/_config.js new file mode 100644 index 0000000000..e93067eb9d --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/ownership-invalid-binding-ignored/_config.js @@ -0,0 +1,11 @@ +import { test } from '../../test'; + +export default test({ + mode: ['client'], + compileOptions: { + dev: true + }, + async test({ warnings, assert }) { + assert.deepEqual(warnings, []); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/ownership-invalid-binding-ignored/main.svelte b/packages/svelte/tests/runtime-runes/samples/ownership-invalid-binding-ignored/main.svelte new file mode 100644 index 0000000000..e0da1dfcab --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/ownership-invalid-binding-ignored/main.svelte @@ -0,0 +1,6 @@ + + + diff --git a/packages/svelte/tests/runtime-runes/samples/ownership-invalid-mutation-ignored/Child.svelte b/packages/svelte/tests/runtime-runes/samples/ownership-invalid-mutation-ignored/Child.svelte new file mode 100644 index 0000000000..32bcef5250 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/ownership-invalid-mutation-ignored/Child.svelte @@ -0,0 +1,45 @@ + + + + + + + + + diff --git a/packages/svelte/tests/runtime-runes/samples/ownership-invalid-mutation-ignored/_config.js b/packages/svelte/tests/runtime-runes/samples/ownership-invalid-mutation-ignored/_config.js new file mode 100644 index 0000000000..44ac85aa28 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/ownership-invalid-mutation-ignored/_config.js @@ -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, []); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/ownership-invalid-mutation-ignored/main.svelte b/packages/svelte/tests/runtime-runes/samples/ownership-invalid-mutation-ignored/main.svelte new file mode 100644 index 0000000000..b642dd32cf --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/ownership-invalid-mutation-ignored/main.svelte @@ -0,0 +1,9 @@ + + + diff --git a/packages/svelte/tests/runtime-runes/samples/state-snapshot-uncloneable-ignored/_config.js b/packages/svelte/tests/runtime-runes/samples/state-snapshot-uncloneable-ignored/_config.js new file mode 100644 index 0000000000..e93067eb9d --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/state-snapshot-uncloneable-ignored/_config.js @@ -0,0 +1,11 @@ +import { test } from '../../test'; + +export default test({ + mode: ['client'], + compileOptions: { + dev: true + }, + async test({ warnings, assert }) { + assert.deepEqual(warnings, []); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/state-snapshot-uncloneable-ignored/main.svelte b/packages/svelte/tests/runtime-runes/samples/state-snapshot-uncloneable-ignored/main.svelte new file mode 100644 index 0000000000..4bc1c1d1a0 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/state-snapshot-uncloneable-ignored/main.svelte @@ -0,0 +1,11 @@ + + + +
a