diff --git a/.changeset/clever-dodos-jam.md b/.changeset/clever-dodos-jam.md new file mode 100644 index 0000000000..bdeb979184 --- /dev/null +++ b/.changeset/clever-dodos-jam.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: untrack `$inspect.with` and add check for unsafe mutation diff --git a/documentation/docs/98-reference/.generated/client-errors.md b/documentation/docs/98-reference/.generated/client-errors.md index b9268636b2..cd68ae704b 100644 --- a/documentation/docs/98-reference/.generated/client-errors.md +++ b/documentation/docs/98-reference/.generated/client-errors.md @@ -131,7 +131,7 @@ Cannot set prototype of `$state` object ### state_unsafe_mutation ``` -Updating state inside a derived or a template expression is forbidden. If the value should not be reactive, declare it without `$state` +Updating state inside `$derived(...)`, `$inspect(...)` or a template expression is forbidden. If the value should not be reactive, declare it without `$state` ``` This error occurs when state is updated while evaluating a `$derived`. You might encounter it while trying to 'derive' two pieces of state in one go: diff --git a/packages/svelte/CHANGELOG.md b/packages/svelte/CHANGELOG.md index 618a25c638..b29ecc3fe3 100644 --- a/packages/svelte/CHANGELOG.md +++ b/packages/svelte/CHANGELOG.md @@ -1,5 +1,17 @@ # svelte +## 5.34.7 + +### Patch Changes + +- fix: address css class matching regression ([#16204](https://github.com/sveltejs/svelte/pull/16204)) + +## 5.34.6 + +### Patch Changes + +- fix: match class and style directives against attribute selector ([#16179](https://github.com/sveltejs/svelte/pull/16179)) + ## 5.34.5 ### Patch Changes diff --git a/packages/svelte/messages/client-errors/errors.md b/packages/svelte/messages/client-errors/errors.md index 8748bf8978..f9e86dcd50 100644 --- a/packages/svelte/messages/client-errors/errors.md +++ b/packages/svelte/messages/client-errors/errors.md @@ -86,7 +86,7 @@ See the [migration guide](/docs/svelte/v5-migration-guide#Components-are-no-long ## state_unsafe_mutation -> Updating state inside a derived or a template expression is forbidden. If the value should not be reactive, declare it without `$state` +> Updating state inside `$derived(...)`, `$inspect(...)` or a template expression is forbidden. If the value should not be reactive, declare it without `$state` This error occurs when state is updated while evaluating a `$derived`. You might encounter it while trying to 'derive' two pieces of state in one go: diff --git a/packages/svelte/package.json b/packages/svelte/package.json index e01691ff63..ebab2158de 100644 --- a/packages/svelte/package.json +++ b/packages/svelte/package.json @@ -2,7 +2,7 @@ "name": "svelte", "description": "Cybernetically enhanced web apps", "license": "MIT", - "version": "5.34.5", + "version": "5.34.7", "type": "module", "types": "./types/index.d.ts", "engines": { diff --git a/packages/svelte/src/compiler/phases/2-analyze/css/css-prune.js b/packages/svelte/src/compiler/phases/2-analyze/css/css-prune.js index b9a5688a87..79e8fbb02c 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/css/css-prune.js +++ b/packages/svelte/src/compiler/phases/2-analyze/css/css-prune.js @@ -628,10 +628,11 @@ function attribute_matches(node, name, expected_value, operator, case_insensitiv if (attribute.type === 'SpreadAttribute') return true; if (attribute.type === 'BindDirective' && attribute.name === name) return true; + const name_lower = name.toLowerCase(); // match attributes against the corresponding directive but bail out on exact matching - if (attribute.type === 'StyleDirective' && name.toLowerCase() === 'style') return true; - if (attribute.type === 'ClassDirective' && name.toLowerCase() === 'class') { - if (operator == '~=') { + if (attribute.type === 'StyleDirective' && name_lower === 'style') return true; + if (attribute.type === 'ClassDirective' && name_lower === 'class') { + if (operator === '~=') { if (attribute.name === expected_value) return true; } else { return true; @@ -639,13 +640,21 @@ function attribute_matches(node, name, expected_value, operator, case_insensitiv } if (attribute.type !== 'Attribute') continue; - if (attribute.name.toLowerCase() !== name.toLowerCase()) continue; + if (attribute.name.toLowerCase() !== name_lower) continue; if (attribute.value === true) return operator === null; if (expected_value === null) return true; if (is_text_attribute(attribute)) { - return test_attribute(operator, expected_value, case_insensitive, attribute.value[0].data); + const matches = test_attribute( + operator, + expected_value, + case_insensitive, + attribute.value[0].data + ); + // continue if we still may match against a class/style directive + if (!matches && (name_lower === 'class' || name_lower === 'style')) continue; + return matches; } const chunks = get_attribute_chunks(attribute.value); @@ -654,7 +663,7 @@ function attribute_matches(node, name, expected_value, operator, case_insensitiv /** @type {string[]} */ let prev_values = []; for (const chunk of chunks) { - const current_possible_values = get_possible_values(chunk, name === 'class'); + const current_possible_values = get_possible_values(chunk, name_lower === 'class'); // impossible to find out all combinations if (!current_possible_values) return true; diff --git a/packages/svelte/src/internal/client/constants.js b/packages/svelte/src/internal/client/constants.js index 47cf48b366..0ee27f4c70 100644 --- a/packages/svelte/src/internal/client/constants.js +++ b/packages/svelte/src/internal/client/constants.js @@ -35,3 +35,8 @@ export const LEGACY_PROPS = Symbol('legacy props'); export const LOADING_ATTR_SYMBOL = Symbol(''); export const STALE_REACTION = Symbol('stale reaction'); + +export const ELEMENT_NODE = 1; +export const TEXT_NODE = 3; +export const COMMENT_NODE = 8; +export const DOCUMENT_FRAGMENT_NODE = 11; diff --git a/packages/svelte/src/internal/client/dev/elements.js b/packages/svelte/src/internal/client/dev/elements.js index 62ac09d784..f70f893d1e 100644 --- a/packages/svelte/src/internal/client/dev/elements.js +++ b/packages/svelte/src/internal/client/dev/elements.js @@ -1,4 +1,5 @@ /** @import { SourceLocation } from '#client' */ +import { COMMENT_NODE, DOCUMENT_FRAGMENT_NODE, ELEMENT_NODE } from '#client/constants'; import { HYDRATION_END, HYDRATION_START, HYDRATION_START_ELSE } from '../../../constants.js'; import { hydrating } from '../dom/hydration.js'; @@ -12,7 +13,7 @@ export function add_locations(fn, filename, locations) { return (/** @type {any[]} */ ...args) => { const dom = fn(...args); - var node = hydrating ? dom : dom.nodeType === 11 ? dom.firstChild : dom; + var node = hydrating ? dom : dom.nodeType === DOCUMENT_FRAGMENT_NODE ? dom.firstChild : dom; assign_locations(node, filename, locations); return dom; @@ -45,13 +46,13 @@ function assign_locations(node, filename, locations) { var depth = 0; while (node && i < locations.length) { - if (hydrating && node.nodeType === 8) { + if (hydrating && node.nodeType === COMMENT_NODE) { var comment = /** @type {Comment} */ (node); if (comment.data === HYDRATION_START || comment.data === HYDRATION_START_ELSE) depth += 1; else if (comment.data[0] === HYDRATION_END) depth -= 1; } - if (depth === 0 && node.nodeType === 1) { + if (depth === 0 && node.nodeType === ELEMENT_NODE) { assign_location(/** @type {Element} */ (node), filename, locations[i++]); } diff --git a/packages/svelte/src/internal/client/dev/inspect.js b/packages/svelte/src/internal/client/dev/inspect.js index e13ef470cf..e15c66901c 100644 --- a/packages/svelte/src/internal/client/dev/inspect.js +++ b/packages/svelte/src/internal/client/dev/inspect.js @@ -1,6 +1,7 @@ import { UNINITIALIZED } from '../../../constants.js'; import { snapshot } from '../../shared/clone.js'; import { inspect_effect, validate_effect } from '../reactivity/effects.js'; +import { untrack } from '../runtime.js'; /** * @param {() => any[]} get_value @@ -28,7 +29,10 @@ export function inspect(get_value, inspector = console.log) { } if (value !== UNINITIALIZED) { - inspector(initial ? 'init' : 'update', ...snapshot(value, true)); + var snap = snapshot(value, true); + untrack(() => { + inspector(initial ? 'init' : 'update', ...snap); + }); } initial = false; diff --git a/packages/svelte/src/internal/client/dom/blocks/each.js b/packages/svelte/src/internal/client/dom/blocks/each.js index 0a8179958e..a6e4f9b6c6 100644 --- a/packages/svelte/src/internal/client/dom/blocks/each.js +++ b/packages/svelte/src/internal/client/dom/blocks/each.js @@ -36,7 +36,7 @@ import { } from '../../reactivity/effects.js'; import { source, mutable_source, internal_set } from '../../reactivity/sources.js'; import { array_from, is_array } from '../../../shared/utils.js'; -import { INERT } from '#client/constants'; +import { COMMENT_NODE, INERT } from '#client/constants'; import { queue_micro_task } from '../task.js'; import { active_effect, get } from '../../runtime.js'; import { DEV } from 'esm-env'; @@ -227,7 +227,7 @@ export function each(node, flags, get_collection, get_key, render_fn, fallback_f for (var i = 0; i < length; i++) { if ( - hydrate_node.nodeType === 8 && + hydrate_node.nodeType === COMMENT_NODE && /** @type {Comment} */ (hydrate_node).data === HYDRATION_END ) { // The server rendered fewer items than expected, diff --git a/packages/svelte/src/internal/client/dom/blocks/html.js b/packages/svelte/src/internal/client/dom/blocks/html.js index 92c8243478..d7190abc66 100644 --- a/packages/svelte/src/internal/client/dom/blocks/html.js +++ b/packages/svelte/src/internal/client/dom/blocks/html.js @@ -10,6 +10,7 @@ import { DEV } from 'esm-env'; import { dev_current_component_function } from '../../context.js'; import { get_first_child, get_next_sibling } from '../operations.js'; import { active_effect } from '../../runtime.js'; +import { COMMENT_NODE } from '#client/constants'; /** * @param {Element} element @@ -67,7 +68,10 @@ export function html(node, get_value, svg = false, mathml = false, skip_warning var next = hydrate_next(); var last = next; - while (next !== null && (next.nodeType !== 8 || /** @type {Comment} */ (next).data !== '')) { + while ( + next !== null && + (next.nodeType !== COMMENT_NODE || /** @type {Comment} */ (next).data !== '') + ) { last = next; next = /** @type {TemplateNode} */ (get_next_sibling(next)); } diff --git a/packages/svelte/src/internal/client/dom/blocks/snippet.js b/packages/svelte/src/internal/client/dom/blocks/snippet.js index c6dce26bfe..32d88d4c60 100644 --- a/packages/svelte/src/internal/client/dom/blocks/snippet.js +++ b/packages/svelte/src/internal/client/dom/blocks/snippet.js @@ -1,7 +1,7 @@ /** @import { Snippet } from 'svelte' */ /** @import { Effect, TemplateNode } from '#client' */ /** @import { Getters } from '#shared' */ -import { EFFECT_TRANSPARENT } from '#client/constants'; +import { EFFECT_TRANSPARENT, ELEMENT_NODE } from '#client/constants'; import { branch, block, destroy_effect, teardown } from '../../reactivity/effects.js'; import { dev_current_component_function, @@ -102,7 +102,7 @@ export function createRawSnippet(fn) { var fragment = create_fragment_from_html(html); element = /** @type {Element} */ (get_first_child(fragment)); - if (DEV && (get_next_sibling(element) !== null || element.nodeType !== 1)) { + if (DEV && (get_next_sibling(element) !== null || element.nodeType !== ELEMENT_NODE)) { w.invalid_raw_snippet_render(); } diff --git a/packages/svelte/src/internal/client/dom/blocks/svelte-element.js b/packages/svelte/src/internal/client/dom/blocks/svelte-element.js index 43f669e844..ffa57b2d8b 100644 --- a/packages/svelte/src/internal/client/dom/blocks/svelte-element.js +++ b/packages/svelte/src/internal/client/dom/blocks/svelte-element.js @@ -20,7 +20,7 @@ import { current_each_item, set_current_each_item } from './each.js'; import { active_effect } from '../../runtime.js'; import { component_context } from '../../context.js'; import { DEV } from 'esm-env'; -import { EFFECT_TRANSPARENT } from '#client/constants'; +import { EFFECT_TRANSPARENT, ELEMENT_NODE } from '#client/constants'; import { assign_nodes } from '../template.js'; import { is_raw_text_element } from '../../../../utils.js'; @@ -51,7 +51,7 @@ export function element(node, get_tag, is_svg, render_fn, get_namespace, locatio /** @type {null | Element} */ var element = null; - if (hydrating && hydrate_node.nodeType === 1) { + if (hydrating && hydrate_node.nodeType === ELEMENT_NODE) { element = /** @type {Element} */ (hydrate_node); hydrate_next(); } diff --git a/packages/svelte/src/internal/client/dom/blocks/svelte-head.js b/packages/svelte/src/internal/client/dom/blocks/svelte-head.js index db2a0c4ef1..66d3371836 100644 --- a/packages/svelte/src/internal/client/dom/blocks/svelte-head.js +++ b/packages/svelte/src/internal/client/dom/blocks/svelte-head.js @@ -2,7 +2,7 @@ import { hydrate_node, hydrating, set_hydrate_node, set_hydrating } from '../hydration.js'; import { create_text, get_first_child, get_next_sibling } from '../operations.js'; import { block } from '../../reactivity/effects.js'; -import { HEAD_EFFECT } from '#client/constants'; +import { COMMENT_NODE, HEAD_EFFECT } from '#client/constants'; import { HYDRATION_START } from '../../../../constants.js'; /** @@ -37,7 +37,8 @@ export function head(render_fn) { while ( head_anchor !== null && - (head_anchor.nodeType !== 8 || /** @type {Comment} */ (head_anchor).data !== HYDRATION_START) + (head_anchor.nodeType !== COMMENT_NODE || + /** @type {Comment} */ (head_anchor).data !== HYDRATION_START) ) { head_anchor = /** @type {TemplateNode} */ (get_next_sibling(head_anchor)); } diff --git a/packages/svelte/src/internal/client/dom/hydration.js b/packages/svelte/src/internal/client/dom/hydration.js index ab3256da82..1f80b7922b 100644 --- a/packages/svelte/src/internal/client/dom/hydration.js +++ b/packages/svelte/src/internal/client/dom/hydration.js @@ -1,5 +1,6 @@ /** @import { TemplateNode } from '#client' */ +import { COMMENT_NODE } from '#client/constants'; import { HYDRATION_END, HYDRATION_ERROR, @@ -87,7 +88,7 @@ export function remove_nodes() { var node = hydrate_node; while (true) { - if (node.nodeType === 8) { + if (node.nodeType === COMMENT_NODE) { var data = /** @type {Comment} */ (node).data; if (data === HYDRATION_END) { @@ -109,7 +110,7 @@ export function remove_nodes() { * @param {TemplateNode} node */ export function read_hydration_instruction(node) { - if (!node || node.nodeType !== 8) { + if (!node || node.nodeType !== COMMENT_NODE) { w.hydration_mismatch(); throw HYDRATION_ERROR; } diff --git a/packages/svelte/src/internal/client/dom/operations.js b/packages/svelte/src/internal/client/dom/operations.js index a4325fce5a..fb269e47e0 100644 --- a/packages/svelte/src/internal/client/dom/operations.js +++ b/packages/svelte/src/internal/client/dom/operations.js @@ -4,8 +4,8 @@ import { DEV } from 'esm-env'; import { init_array_prototype_warnings } from '../dev/equality.js'; import { get_descriptor, is_extensible } from '../../shared/utils.js'; import { active_effect } from '../runtime.js'; -import { EFFECT_RAN } from '../constants.js'; import { async_mode_flag } from '../../flags/index.js'; +import { TEXT_NODE, EFFECT_RAN } from '#client/constants'; // export these for reference in the compiled code, making global name deduplication unnecessary /** @type {Window} */ @@ -116,7 +116,7 @@ export function child(node, is_text) { // Child can be null if we have an element with a single child, like `

{text}

`, where `text` is empty if (child === null) { child = hydrate_node.appendChild(create_text()); - } else if (is_text && child.nodeType !== 3) { + } else if (is_text && child.nodeType !== TEXT_NODE) { var text = create_text(); child?.before(text); set_hydrate_node(text); @@ -146,7 +146,7 @@ export function first_child(fragment, is_text) { // if an {expression} is empty during SSR, there might be no // text node to hydrate — we must therefore create one - if (is_text && hydrate_node?.nodeType !== 3) { + if (is_text && hydrate_node?.nodeType !== TEXT_NODE) { var text = create_text(); hydrate_node?.before(text); @@ -177,11 +177,9 @@ export function sibling(node, count = 1, is_text = false) { return next_sibling; } - var type = next_sibling?.nodeType; - // if a sibling {expression} is empty during SSR, there might be no // text node to hydrate — we must therefore create one - if (is_text && type !== 3) { + if (is_text && next_sibling?.nodeType !== TEXT_NODE) { var text = create_text(); // If the next sibling is `null` and we're handling text then it's because // the SSR content was empty for the text, so we need to generate a new text diff --git a/packages/svelte/src/internal/client/dom/template.js b/packages/svelte/src/internal/client/dom/template.js index 0b77ab1396..ebbf0039b2 100644 --- a/packages/svelte/src/internal/client/dom/template.js +++ b/packages/svelte/src/internal/client/dom/template.js @@ -20,6 +20,7 @@ import { TEMPLATE_USE_MATHML, TEMPLATE_USE_SVG } from '../../../constants.js'; +import { COMMENT_NODE, DOCUMENT_FRAGMENT_NODE, TEXT_NODE } from '#client/constants'; /** * @param {TemplateNode} start @@ -264,7 +265,7 @@ function run_scripts(node) { // scripts were SSR'd, in which case they will run if (hydrating) return node; - const is_fragment = node.nodeType === 11; + const is_fragment = node.nodeType === DOCUMENT_FRAGMENT_NODE; const scripts = /** @type {HTMLElement} */ (node).tagName === 'SCRIPT' ? [/** @type {HTMLScriptElement} */ (node)] @@ -305,7 +306,7 @@ export function text(value = '') { var node = hydrate_node; - if (node.nodeType !== 3) { + if (node.nodeType !== TEXT_NODE) { // if an {expression} is empty during SSR, we need to insert an empty text node node.before((node = create_text())); set_hydrate_node(node); @@ -360,7 +361,7 @@ export function props_id() { if ( hydrating && hydrate_node && - hydrate_node.nodeType === 8 && + hydrate_node.nodeType === COMMENT_NODE && hydrate_node.textContent?.startsWith(`#`) ) { const id = hydrate_node.textContent.substring(1); diff --git a/packages/svelte/src/internal/client/errors.js b/packages/svelte/src/internal/client/errors.js index 5beae00aa1..063595884e 100644 --- a/packages/svelte/src/internal/client/errors.js +++ b/packages/svelte/src/internal/client/errors.js @@ -322,12 +322,12 @@ export function state_prototype_fixed() { } /** - * Updating state inside a derived or a template expression is forbidden. If the value should not be reactive, declare it without `$state` + * Updating state inside `$derived(...)`, `$inspect(...)` or a template expression is forbidden. If the value should not be reactive, declare it without `$state` * @returns {never} */ export function state_unsafe_mutation() { if (DEV) { - const error = new Error(`state_unsafe_mutation\nUpdating state inside a derived or a template expression is forbidden. If the value should not be reactive, declare it without \`$state\`\nhttps://svelte.dev/e/state_unsafe_mutation`); + const error = new Error(`state_unsafe_mutation\nUpdating state inside \`$derived(...)\`, \`$inspect(...)\` or a template expression is forbidden. If the value should not be reactive, declare it without \`$state\`\nhttps://svelte.dev/e/state_unsafe_mutation`); error.name = 'Svelte error'; throw error; diff --git a/packages/svelte/src/internal/client/reactivity/sources.js b/packages/svelte/src/internal/client/reactivity/sources.js index 44185e118f..2300baed91 100644 --- a/packages/svelte/src/internal/client/reactivity/sources.js +++ b/packages/svelte/src/internal/client/reactivity/sources.js @@ -140,9 +140,11 @@ export function mutate(source, value) { export function set(source, value, should_proxy = false) { if ( active_reaction !== null && - !untracking && + // since we are untracking the function inside `$inspect.with` we need to add this check + // to ensure we error if state is set inside an inspect effect + (!untracking || (active_reaction.f & INSPECT_EFFECT) !== 0) && is_runes() && - (active_reaction.f & (DERIVED | BLOCK_EFFECT | EFFECT_ASYNC)) !== 0 && + (active_reaction.f & (DERIVED | BLOCK_EFFECT | EFFECT_ASYNC | INSPECT_EFFECT)) !== 0 && !(reaction_sources?.[1].includes(source) && reaction_sources[0] === active_reaction) ) { e.state_unsafe_mutation(); diff --git a/packages/svelte/src/internal/client/render.js b/packages/svelte/src/internal/client/render.js index 222b971bdf..40936ab7df 100644 --- a/packages/svelte/src/internal/client/render.js +++ b/packages/svelte/src/internal/client/render.js @@ -31,6 +31,7 @@ import * as e from './errors.js'; import { assign_nodes } from './dom/template.js'; import { is_passive_event } from '../../utils.js'; import { async_mode_flag } from '../flags/index.js'; +import { COMMENT_NODE } from './constants.js'; /** * This is normally true — block effects should run their intro transitions — @@ -108,7 +109,7 @@ export function hydrate(component, options) { var anchor = /** @type {TemplateNode} */ (get_first_child(target)); while ( anchor && - (anchor.nodeType !== 8 || /** @type {Comment} */ (anchor).data !== HYDRATION_START) + (anchor.nodeType !== COMMENT_NODE || /** @type {Comment} */ (anchor).data !== HYDRATION_START) ) { anchor = /** @type {TemplateNode} */ (get_next_sibling(anchor)); } @@ -125,7 +126,7 @@ export function hydrate(component, options) { if ( hydrate_node === null || - hydrate_node.nodeType !== 8 || + hydrate_node.nodeType !== COMMENT_NODE || /** @type {Comment} */ (hydrate_node).data !== HYDRATION_END ) { w.hydration_mismatch(); diff --git a/packages/svelte/src/version.js b/packages/svelte/src/version.js index bffca48eec..748cc3ddd6 100644 --- a/packages/svelte/src/version.js +++ b/packages/svelte/src/version.js @@ -4,5 +4,5 @@ * The current version, as set in package.json. * @type {string} */ -export const VERSION = '5.34.5'; +export const VERSION = '5.34.7'; export const PUBLIC_VERSION = '5'; diff --git a/packages/svelte/tests/css/samples/class-directive/_config.js b/packages/svelte/tests/css/samples/class-directive/_config.js index 28e9fbc815..abeb8b6329 100644 --- a/packages/svelte/tests/css/samples/class-directive/_config.js +++ b/packages/svelte/tests/css/samples/class-directive/_config.js @@ -4,16 +4,16 @@ export default test({ warnings: [ { code: 'css_unused_selector', - message: 'Unused CSS selector ".third"\nhttps://svelte.dev/e/css_unused_selector', + message: 'Unused CSS selector ".forth"\nhttps://svelte.dev/e/css_unused_selector', start: { - line: 6, + line: 8, column: 2, - character: 115 + character: 190 }, end: { - line: 6, + line: 8, column: 8, - character: 121 + character: 196 } } ] diff --git a/packages/svelte/tests/css/samples/class-directive/expected.css b/packages/svelte/tests/css/samples/class-directive/expected.css index 1d7d3d4dee..b3a74baee5 100644 --- a/packages/svelte/tests/css/samples/class-directive/expected.css +++ b/packages/svelte/tests/css/samples/class-directive/expected.css @@ -1,3 +1,5 @@ - .first.svelte-xyz { color: green } + + .zero.first.svelte-xyz { color: green } .second.svelte-xyz { color: green } - /* (unused) .third { color: red }*/ + .third.svelte-xyz { color: green } + /* (unused) .forth { color: red }*/ diff --git a/packages/svelte/tests/css/samples/class-directive/input.svelte b/packages/svelte/tests/css/samples/class-directive/input.svelte index cf00335964..60e1f53171 100644 --- a/packages/svelte/tests/css/samples/class-directive/input.svelte +++ b/packages/svelte/tests/css/samples/class-directive/input.svelte @@ -1,7 +1,9 @@ -
+
+
\ No newline at end of file + .third { color: green } + .forth { color: red } + diff --git a/packages/svelte/tests/html_equal.js b/packages/svelte/tests/html_equal.js index 6948d8db32..b637e4d538 100644 --- a/packages/svelte/tests/html_equal.js +++ b/packages/svelte/tests/html_equal.js @@ -1,3 +1,4 @@ +import { COMMENT_NODE, ELEMENT_NODE, TEXT_NODE } from '#client/constants'; import { assert } from 'vitest'; /** @@ -35,7 +36,7 @@ function clean_children(node, opts) { }); for (let child of [...node.childNodes]) { - if (child.nodeType === 3) { + if (child.nodeType === TEXT_NODE) { let text = /** @type {Text} */ (child); if ( @@ -49,7 +50,7 @@ function clean_children(node, opts) { text.data = text.data.replace(/[^\S]+/g, ' '); - if (previous && previous.nodeType === 3) { + if (previous && previous.nodeType === TEXT_NODE) { const prev = /** @type {Text} */ (previous); prev.data += text.data; @@ -62,22 +63,22 @@ function clean_children(node, opts) { } } - if (child.nodeType === 8 && !opts.preserveComments) { + if (child.nodeType === COMMENT_NODE && !opts.preserveComments) { // comment child.remove(); continue; } // add newlines for better readability and potentially recurse into children - if (child.nodeType === 1 || child.nodeType === 8) { - if (previous?.nodeType === 3) { + if (child.nodeType === ELEMENT_NODE || child.nodeType === COMMENT_NODE) { + if (previous?.nodeType === TEXT_NODE) { const prev = /** @type {Text} */ (previous); prev.data = prev.data.replace(/^[^\S]+$/, '\n'); - } else if (previous?.nodeType === 1 || previous?.nodeType === 8) { + } else if (previous?.nodeType === ELEMENT_NODE || previous?.nodeType === COMMENT_NODE) { node.insertBefore(document.createTextNode('\n'), child); } - if (child.nodeType === 1) { + if (child.nodeType === ELEMENT_NODE) { has_element_children = true; clean_children(/** @type {Element} */ (child), opts); } @@ -87,12 +88,12 @@ function clean_children(node, opts) { } // collapse whitespace - if (node.firstChild && node.firstChild.nodeType === 3) { + if (node.firstChild && node.firstChild.nodeType === TEXT_NODE) { const text = /** @type {Text} */ (node.firstChild); text.data = text.data.trimStart(); } - if (node.lastChild && node.lastChild.nodeType === 3) { + if (node.lastChild && node.lastChild.nodeType === TEXT_NODE) { const text = /** @type {Text} */ (node.lastChild); text.data = text.data.trimEnd(); } diff --git a/packages/svelte/tests/runtime-browser/assert.js b/packages/svelte/tests/runtime-browser/assert.js index fb460c722a..48bde01410 100644 --- a/packages/svelte/tests/runtime-browser/assert.js +++ b/packages/svelte/tests/runtime-browser/assert.js @@ -1,5 +1,8 @@ /** @import { assert } from 'vitest' */ /** @import { CompileOptions, Warning } from '#compiler' */ + +import { ELEMENT_NODE } from '#client/constants'; + /** * @param {any} a * @param {any} b @@ -102,7 +105,7 @@ function normalize_children(node) { } for (let child of [...node.childNodes]) { - if (child.nodeType === 1 /* Element */) { + if (child.nodeType === ELEMENT_NODE) { normalize_children(child); } } diff --git a/packages/svelte/tests/runtime-legacy/samples/template/_config.js b/packages/svelte/tests/runtime-legacy/samples/template/_config.js index f827168542..7576b6fbb8 100644 --- a/packages/svelte/tests/runtime-legacy/samples/template/_config.js +++ b/packages/svelte/tests/runtime-legacy/samples/template/_config.js @@ -1,3 +1,4 @@ +import { COMMENT_NODE } from '#client/constants'; import { ok, test } from '../../test'; export default test({ @@ -41,7 +42,7 @@ export default test({ // get all childNodes of template3 except comments let childNodes = []; for (const node of template3.content.childNodes) { - if (node.nodeType !== 8) { + if (node.nodeType !== COMMENT_NODE) { childNodes.push(/** @type {Element} */ (node)); } } diff --git a/packages/svelte/tests/runtime-runes/samples/inspect-state-unsafe-mutation/_config.js b/packages/svelte/tests/runtime-runes/samples/inspect-state-unsafe-mutation/_config.js new file mode 100644 index 0000000000..7e8fcd2d48 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/inspect-state-unsafe-mutation/_config.js @@ -0,0 +1,9 @@ +import { flushSync } from 'svelte'; +import { test } from '../../test'; + +export default test({ + compileOptions: { + dev: true + }, + error: 'state_unsafe_mutation' +}); diff --git a/packages/svelte/tests/runtime-runes/samples/inspect-state-unsafe-mutation/main.svelte b/packages/svelte/tests/runtime-runes/samples/inspect-state-unsafe-mutation/main.svelte new file mode 100644 index 0000000000..3361087ff7 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/inspect-state-unsafe-mutation/main.svelte @@ -0,0 +1,10 @@ + \ No newline at end of file diff --git a/packages/svelte/tests/runtime-runes/samples/inspect-with-untracked/_config.js b/packages/svelte/tests/runtime-runes/samples/inspect-with-untracked/_config.js new file mode 100644 index 0000000000..cdb242c416 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/inspect-with-untracked/_config.js @@ -0,0 +1,20 @@ +import { flushSync } from 'svelte'; +import { test } from '../../test'; + +export default test({ + compileOptions: { + dev: true + }, + async test({ assert, target, logs }) { + const [a, b] = target.querySelectorAll('button'); + assert.deepEqual(logs, ['init', 0]); + flushSync(() => { + b?.click(); + }); + assert.deepEqual(logs, ['init', 0]); + flushSync(() => { + a?.click(); + }); + assert.deepEqual(logs, ['init', 0, 'update', 1]); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/inspect-with-untracked/main.svelte b/packages/svelte/tests/runtime-runes/samples/inspect-with-untracked/main.svelte new file mode 100644 index 0000000000..5bcf2bd348 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/inspect-with-untracked/main.svelte @@ -0,0 +1,13 @@ + + + + \ No newline at end of file diff --git a/packages/svelte/tests/signals/test.ts b/packages/svelte/tests/signals/test.ts index 8155aedcb0..0e952db7ec 100644 --- a/packages/svelte/tests/signals/test.ts +++ b/packages/svelte/tests/signals/test.ts @@ -1076,6 +1076,41 @@ describe('signals', () => { }; }); + test('nested effects depend on state of upper effects', () => { + const logs: number[] = []; + + user_effect(() => { + const raw = state(0); + const proxied = proxy({ current: 0 }); + + // We need those separate, else one working and rerunning the effect + // could mask the other one not rerunning + user_effect(() => { + logs.push($.get(raw)); + }); + + user_effect(() => { + logs.push(proxied.current); + }); + + // Important so that the updating effect is not running + // together with the reading effects + flushSync(); + + user_effect(() => { + $.untrack(() => { + set(raw, $.get(raw) + 1); + proxied.current += 1; + }); + }); + }); + + return () => { + flushSync(); + assert.deepEqual(logs, [0, 0, 1, 1]); + }; + }); + test('proxy version state does not trigger self-dependency guard', () => { return () => { const s = proxy({ a: { b: 1 } });