diff --git a/.changeset/empty-geckos-pretend.md b/.changeset/empty-geckos-pretend.md new file mode 100644 index 0000000000..defe2fb419 --- /dev/null +++ b/.changeset/empty-geckos-pretend.md @@ -0,0 +1,5 @@ +--- +"svelte": patch +--- + +fix: ensure reactions are kept dirty when marking them again diff --git a/.changeset/fast-penguins-matter.md b/.changeset/fast-penguins-matter.md new file mode 100644 index 0000000000..523aaf8a12 --- /dev/null +++ b/.changeset/fast-penguins-matter.md @@ -0,0 +1,5 @@ +--- +"svelte": patch +--- + +fix: apply modifiers to bubbled events diff --git a/.changeset/healthy-ants-film.md b/.changeset/healthy-ants-film.md new file mode 100644 index 0000000000..f4fa0a19b1 --- /dev/null +++ b/.changeset/healthy-ants-film.md @@ -0,0 +1,5 @@ +--- +"svelte": patch +--- + +fix: allow `bind:this` on `` with dynamic `multiple` attribute ([#11378](https://github.com/sveltejs/svelte/pull/11378)) + +- feat: allow for literal property definition with state on classes ([#11326](https://github.com/sveltejs/svelte/pull/11326)) + +- fix: disallow mounting a snippet ([#11347](https://github.com/sveltejs/svelte/pull/11347)) + +- feat: only inject push/init/pop when necessary ([#11319](https://github.com/sveltejs/svelte/pull/11319)) + +- feat: provide migration helper ([#11334](https://github.com/sveltejs/svelte/pull/11334)) + +- fix: ensure store from props is hoisted correctly ([#11367](https://github.com/sveltejs/svelte/pull/11367)) + ## 5.0.0-next.115 ### Patch Changes diff --git a/packages/svelte/package.json b/packages/svelte/package.json index b6087b6a0f..8c6a1f01a8 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.0.0-next.115", + "version": "5.0.0-next.116", "type": "module", "types": "./types/index.d.ts", "engines": { diff --git a/packages/svelte/src/compiler/phases/1-parse/read/style.js b/packages/svelte/src/compiler/phases/1-parse/read/style.js index 358baea77a..ebd3b1a715 100644 --- a/packages/svelte/src/compiler/phases/1-parse/read/style.js +++ b/packages/svelte/src/compiler/phases/1-parse/read/style.js @@ -182,8 +182,7 @@ function read_selector(parser, inside_pseudo_class = false) { end: -1, metadata: { is_global: false, - is_host: false, - is_root: false, + is_global_like: false, scoped: false } }; diff --git a/packages/svelte/src/compiler/phases/2-analyze/css/css-analyze.js b/packages/svelte/src/compiler/phases/2-analyze/css/css-analyze.js index dfbdb98ef2..b8f5b34bb0 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/css/css-analyze.js +++ b/packages/svelte/src/compiler/phases/2-analyze/css/css-analyze.js @@ -42,7 +42,7 @@ const analysis_visitors = { node.metadata.rule = context.state.rule; node.metadata.used = node.children.every( - ({ metadata }) => metadata.is_global || metadata.is_host || metadata.is_root + ({ metadata }) => metadata.is_global || metadata.is_global_like ); }, RelativeSelector(node, context) { @@ -57,10 +57,19 @@ const analysis_visitors = { if (node.selectors.length === 1) { const first = node.selectors[0]; - node.metadata.is_host = first.type === 'PseudoClassSelector' && first.name === 'host'; + node.metadata.is_global_like ||= + (first.type === 'PseudoClassSelector' && first.name === 'host') || + (first.type === 'PseudoElementSelector' && + [ + 'view-transition', + 'view-transition-group', + 'view-transition-old', + 'view-transition-new', + 'view-transition-image-pair' + ].includes(first.name)); } - node.metadata.is_root = !!node.selectors.find( + node.metadata.is_global_like ||= !!node.selectors.find( (child) => child.type === 'PseudoClassSelector' && child.name === 'root' ); @@ -87,7 +96,7 @@ const analysis_visitors = { node.metadata.has_local_selectors = node.prelude.children.some((selector) => { return selector.children.some( - ({ metadata }) => !metadata.is_global && !metadata.is_host && !metadata.is_root + ({ metadata }) => !metadata.is_global && !metadata.is_global_like ); }); } 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 1726a4fab1..25438e8d5e 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 @@ -42,8 +42,7 @@ const nesting_selector = { ], metadata: { is_global: false, - is_host: false, - is_root: false, + is_global_like: false, scoped: false } }; @@ -109,7 +108,7 @@ const visitors = { */ function truncate(node) { const i = node.children.findLastIndex(({ metadata }) => { - return !metadata.is_global && !metadata.is_host && !metadata.is_root; + return !metadata.is_global && !metadata.is_global_like; }); return node.children.slice(0, i + 1); @@ -229,14 +228,14 @@ function mark(relative_selector, element) { /** * Returns `true` if the relative selector is global, meaning - * it's a `:global(...)` or `:host` or `:root` selector, or + * it's a `:global(...)` or unscopeable selector, or * is an `:is(...)` or `:where(...)` selector that contains * a global selector * @param {import('#compiler').Css.RelativeSelector} selector * @param {import('#compiler').Css.Rule} rule */ function is_global(selector, rule) { - if (selector.metadata.is_global || selector.metadata.is_host || selector.metadata.is_root) { + if (selector.metadata.is_global || selector.metadata.is_global_like) { return true; } diff --git a/packages/svelte/src/compiler/phases/2-analyze/validation.js b/packages/svelte/src/compiler/phases/2-analyze/validation.js index ab793bf1a7..c2c7dece31 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/validation.js +++ b/packages/svelte/src/compiler/phases/2-analyze/validation.js @@ -413,7 +413,7 @@ const validation = { } } - if (parent.name === 'select') { + if (parent.name === 'select' && node.name !== 'this') { const multiple = parent.attributes.find( (a) => a.type === 'Attribute' && 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 481caf6561..e6fdb902f0 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/utils.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/utils.js @@ -456,27 +456,30 @@ function get_hoistable_params(node, context) { /** @type {import('estree').Identifier[]} */ const params = []; - let added_props = false; /** - * we only want to push if it's not already present to avoid name clashing + * We only want to push if it's not already present to avoid name clashing * @param {import('estree').Identifier} id */ - function safe_push(id) { + function push_unique(id) { if (!params.find((param) => param.name === id.name)) { params.push(id); } } for (const [reference] of scope.references) { - const binding = scope.get(reference); + let binding = scope.get(reference); if (binding !== null && !scope.declarations.has(reference) && binding.initial !== node) { if (binding.kind === 'store_sub') { // We need both the subscription for getting the value and the store for updating - safe_push(b.id(binding.node.name.slice(1))); - safe_push(b.id(binding.node.name)); - } else if ( + push_unique(b.id(binding.node.name)); + binding = /** @type {import('#compiler').Binding} */ ( + scope.get(binding.node.name.slice(1)) + ); + } + + if ( // If it's a destructured derived binding, then we can extract the derived signal reference and use that. binding.expression !== null && typeof binding.expression !== 'function' && @@ -486,7 +489,7 @@ function get_hoistable_params(node, context) { binding.expression.object.callee.name === '$.get' && binding.expression.object.arguments[0].type === 'Identifier' ) { - safe_push(b.id(binding.expression.object.arguments[0].name)); + push_unique(b.id(binding.expression.object.arguments[0].name)); } else if ( // If we are referencing a simple $$props value, then we need to reference the object property instead (binding.kind === 'prop' || binding.kind === 'bindable_prop') && @@ -494,14 +497,10 @@ function get_hoistable_params(node, context) { binding.initial === null && !context.state.analysis.accessors ) { - // Handle $$props.something use-cases - if (!added_props) { - added_props = true; - safe_push(b.id('$$props')); - } + push_unique(b.id('$$props')); } else { // create a copy to remove start/end tags which would mess up source maps - safe_push(b.id(binding.node.name)); + push_unique(b.id(binding.node.name)); } } } 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 5c5b177917..9f3192c638 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 @@ -4,6 +4,7 @@ import * as b from '../../../../utils/builders.js'; import * as assert from '../../../../utils/assert.js'; import { get_prop_source, is_state_source, should_proxy_or_freeze } from '../utils.js'; import { extract_paths } from '../../../../utils/ast.js'; +import { regex_invalid_identifier_chars } from '../../../patterns.js'; /** @type {import('../types.js').ComponentVisitors} */ export const javascript_visitors_runes = { @@ -20,9 +21,13 @@ export const javascript_visitors_runes = { for (const definition of node.body) { if ( definition.type === 'PropertyDefinition' && - (definition.key.type === 'Identifier' || definition.key.type === 'PrivateIdentifier') + (definition.key.type === 'Identifier' || + definition.key.type === 'PrivateIdentifier' || + definition.key.type === 'Literal') ) { - const { type, name } = definition.key; + const type = definition.key.type; + const name = get_name(definition.key); + if (!name) continue; const is_private = type === 'PrivateIdentifier'; if (is_private) private_ids.push(name); @@ -79,9 +84,12 @@ export const javascript_visitors_runes = { for (const definition of node.body) { if ( definition.type === 'PropertyDefinition' && - (definition.key.type === 'Identifier' || definition.key.type === 'PrivateIdentifier') + (definition.key.type === 'Identifier' || + definition.key.type === 'PrivateIdentifier' || + definition.key.type === 'Literal') ) { - const name = definition.key.name; + const name = get_name(definition.key); + if (!name) continue; const is_private = definition.key.type === 'PrivateIdentifier'; const field = (is_private ? private_state : public_state).get(name); @@ -160,7 +168,6 @@ export const javascript_visitors_runes = { ); } } - continue; } } @@ -437,3 +444,14 @@ export const javascript_visitors_runes = { context.next(); } }; + +/** + * @param {import('estree').Identifier | import('estree').PrivateIdentifier | import('estree').Literal} node + */ +function get_name(node) { + if (node.type === 'Literal') { + return node.value?.toString().replace(regex_invalid_identifier_chars, '_'); + } else { + return node.name; + } +} 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 0046dc9cc9..667d36b707 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 @@ -1186,8 +1186,11 @@ function serialize_render_stmt(state) { * @param {import('../types.js').ComponentContext} context */ function serialize_event_handler(node, { state, visit }) { + /** @type {import('estree').Expression} */ + let handler; + if (node.expression) { - let handler = node.expression; + handler = node.expression; // Event handlers can be dynamic (source/store/prop/conditional etc) const dynamic_handler = () => @@ -1225,34 +1228,34 @@ function serialize_event_handler(node, { state, visit }) { } else { handler = /** @type {import('estree').Expression} */ (visit(handler)); } - - if (node.modifiers.includes('stopPropagation')) { - handler = b.call('$.stopPropagation', handler); - } - if (node.modifiers.includes('stopImmediatePropagation')) { - handler = b.call('$.stopImmediatePropagation', handler); - } - if (node.modifiers.includes('preventDefault')) { - handler = b.call('$.preventDefault', handler); - } - if (node.modifiers.includes('self')) { - handler = b.call('$.self', handler); - } - if (node.modifiers.includes('trusted')) { - handler = b.call('$.trusted', handler); - } - - return handler; } else { state.analysis.needs_props = true; // Function + .call to preserve "this" context as much as possible - return b.function( + handler = b.function( null, [b.id('$$arg')], b.block([b.stmt(b.call('$.bubble_event.call', b.this, b.id('$$props'), b.id('$$arg')))]) ); } + + if (node.modifiers.includes('stopPropagation')) { + handler = b.call('$.stopPropagation', handler); + } + if (node.modifiers.includes('stopImmediatePropagation')) { + handler = b.call('$.stopImmediatePropagation', handler); + } + if (node.modifiers.includes('preventDefault')) { + handler = b.call('$.preventDefault', handler); + } + if (node.modifiers.includes('self')) { + handler = b.call('$.self', handler); + } + if (node.modifiers.includes('trusted')) { + handler = b.call('$.trusted', handler); + } + + return handler; } /** diff --git a/packages/svelte/src/compiler/phases/patterns.js b/packages/svelte/src/compiler/phases/patterns.js index 4f737adb3c..bda299de9e 100644 --- a/packages/svelte/src/compiler/phases/patterns.js +++ b/packages/svelte/src/compiler/phases/patterns.js @@ -15,6 +15,8 @@ export const regex_only_whitespaces = /^[ \t\n\r\f]+$/; export const regex_not_newline_characters = /[^\n]/g; export const regex_is_valid_identifier = /^[a-zA-Z_$][a-zA-Z_$0-9]*$/; +// used in replace all to remove all invalid chars from a literal identifier +export const regex_invalid_identifier_chars = /(^[^a-zA-Z_$]|[^a-zA-Z0-9_$])/g; export const regex_starts_with_vowel = /^[aeiou]/; export const regex_heading_tags = /^h[1-6]$/; diff --git a/packages/svelte/src/compiler/types/css.d.ts b/packages/svelte/src/compiler/types/css.d.ts index ee323ad6d1..89a739713a 100644 --- a/packages/svelte/src/compiler/types/css.d.ts +++ b/packages/svelte/src/compiler/types/css.d.ts @@ -56,9 +56,10 @@ export namespace Css { combinator: null | Combinator; selectors: SimpleSelector[]; metadata: { + /** :global(..) */ is_global: boolean; - is_host: boolean; - is_root: boolean; + /** :root, :host, ::view-transition */ + is_global_like: boolean; scoped: boolean; }; } diff --git a/packages/svelte/src/internal/client/runtime.js b/packages/svelte/src/internal/client/runtime.js index 9e268a3966..4364a52199 100644 --- a/packages/svelte/src/internal/client/runtime.js +++ b/packages/svelte/src/internal/client/runtime.js @@ -780,7 +780,7 @@ export function invalidate_inner_signals(fn) { /** * @param {import('#client').Value} signal - * @param {number} to_status + * @param {number} to_status should be DIRTY or MAYBE_DIRTY * @param {boolean} force_schedule * @returns {void} */ @@ -793,15 +793,15 @@ export function mark_reactions(signal, to_status, force_schedule) { for (var i = 0; i < length; i++) { var reaction = reactions[i]; + var flags = reaction.f; - // We skip any effects that are already dirty (but not unowned). Additionally, we also + // We skip any effects that are already dirty. Additionally, we also // skip if the reaction is the same as the current effect (except if we're not in runes or we // are in force schedule mode). - if ((!force_schedule || !runes) && reaction === current_effect) { + if ((flags & DIRTY) !== 0 || ((!force_schedule || !runes) && reaction === current_effect)) { continue; } - var flags = reaction.f; set_signal_status(reaction, to_status); // If the signal is not clean, then skip over it – with the exception of unowned signals that diff --git a/packages/svelte/src/version.js b/packages/svelte/src/version.js index 1d4da8571b..0715ea6b89 100644 --- a/packages/svelte/src/version.js +++ b/packages/svelte/src/version.js @@ -6,5 +6,5 @@ * https://svelte.dev/docs/svelte-compiler#svelte-version * @type {string} */ -export const VERSION = '5.0.0-next.115'; +export const VERSION = '5.0.0-next.116'; export const PUBLIC_VERSION = '5'; diff --git a/packages/svelte/tests/css/samples/at-rule-nested-class/_config.js b/packages/svelte/tests/css/samples/at-rule-nested-class/_config.js new file mode 100644 index 0000000000..f47bee71df --- /dev/null +++ b/packages/svelte/tests/css/samples/at-rule-nested-class/_config.js @@ -0,0 +1,3 @@ +import { test } from '../../test'; + +export default test({}); diff --git a/packages/svelte/tests/css/samples/at-rule-nested-class/expected.css b/packages/svelte/tests/css/samples/at-rule-nested-class/expected.css new file mode 100644 index 0000000000..d5307f9c71 --- /dev/null +++ b/packages/svelte/tests/css/samples/at-rule-nested-class/expected.css @@ -0,0 +1,9 @@ + + @starting-style { + .card.svelte-xyz{ + height: 0; + } + } + .card.svelte-xyz { + color: red; + } diff --git a/packages/svelte/tests/css/samples/at-rule-nested-class/input.svelte b/packages/svelte/tests/css/samples/at-rule-nested-class/input.svelte new file mode 100644 index 0000000000..9b5b5fc849 --- /dev/null +++ b/packages/svelte/tests/css/samples/at-rule-nested-class/input.svelte @@ -0,0 +1,12 @@ +
+ + diff --git a/packages/svelte/tests/css/samples/view-transition/expected.css b/packages/svelte/tests/css/samples/view-transition/expected.css new file mode 100644 index 0000000000..afc84d52eb --- /dev/null +++ b/packages/svelte/tests/css/samples/view-transition/expected.css @@ -0,0 +1,16 @@ + + ::view-transition { + animation-duration: 0.5s; + } + ::view-transition-group(foo) { + animation-duration: 0.5s; + } + ::view-transition-old { + animation-duration: 0.5s; + } + ::view-transition-new { + animation-duration: 0.5s; + } + ::view-transition-image-pair { + animation-duration: 0.5s; + } diff --git a/packages/svelte/tests/css/samples/view-transition/input.svelte b/packages/svelte/tests/css/samples/view-transition/input.svelte new file mode 100644 index 0000000000..ebb2b3fd88 --- /dev/null +++ b/packages/svelte/tests/css/samples/view-transition/input.svelte @@ -0,0 +1,17 @@ + diff --git a/packages/svelte/tests/runtime-legacy/samples/event-handler-modifier-bubble/_config.js b/packages/svelte/tests/runtime-legacy/samples/event-handler-modifier-bubble/_config.js new file mode 100644 index 0000000000..c30af43043 --- /dev/null +++ b/packages/svelte/tests/runtime-legacy/samples/event-handler-modifier-bubble/_config.js @@ -0,0 +1,12 @@ +import { ok, test } from '../../test'; + +export default test({ + async test({ assert, component, target }) { + const button = target.querySelector('button'); + ok(button); + + await button.click(); + + assert.ok(component.default_was_prevented); + } +}); diff --git a/packages/svelte/tests/runtime-legacy/samples/event-handler-modifier-bubble/button.svelte b/packages/svelte/tests/runtime-legacy/samples/event-handler-modifier-bubble/button.svelte new file mode 100644 index 0000000000..d94a505c0c --- /dev/null +++ b/packages/svelte/tests/runtime-legacy/samples/event-handler-modifier-bubble/button.svelte @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/packages/svelte/tests/runtime-legacy/samples/event-handler-modifier-bubble/main.svelte b/packages/svelte/tests/runtime-legacy/samples/event-handler-modifier-bubble/main.svelte new file mode 100644 index 0000000000..d5134e28fa --- /dev/null +++ b/packages/svelte/tests/runtime-legacy/samples/event-handler-modifier-bubble/main.svelte @@ -0,0 +1,11 @@ + + +`, + + async test({ assert, target }) { + const btn = target.querySelector('button'); + + await btn?.click(); + assert.htmlEqual(target.innerHTML, ``); + + await btn?.click(); + assert.htmlEqual(target.innerHTML, ``); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/class-state-with-literal/main.svelte b/packages/svelte/tests/runtime-runes/samples/class-state-with-literal/main.svelte new file mode 100644 index 0000000000..3ad90deb6a --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/class-state-with-literal/main.svelte @@ -0,0 +1,12 @@ + + + diff --git a/packages/svelte/tests/runtime-runes/samples/derived-write-read-write-read/_config.js b/packages/svelte/tests/runtime-runes/samples/derived-write-read-write-read/_config.js new file mode 100644 index 0000000000..fe8d5b732f --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/derived-write-read-write-read/_config.js @@ -0,0 +1,8 @@ +import { test } from '../../test'; + +export default test({ + async test({ assert, target }) { + await target.querySelector('button')?.click(); + assert.htmlEqual(target.innerHTML, ``); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/derived-write-read-write-read/main.svelte b/packages/svelte/tests/runtime-runes/samples/derived-write-read-write-read/main.svelte new file mode 100644 index 0000000000..1e449198be --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/derived-write-read-write-read/main.svelte @@ -0,0 +1,13 @@ + + + \ No newline at end of file diff --git a/packages/svelte/tests/runtime-runes/samples/store-from-props-hoisting/_config.js b/packages/svelte/tests/runtime-runes/samples/store-from-props-hoisting/_config.js new file mode 100644 index 0000000000..bd5593eb08 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/store-from-props-hoisting/_config.js @@ -0,0 +1,18 @@ +import { test } from '../../test'; + +export default test({ + compileOptions: { + dev: true + }, + async test({ assert, target }) { + const button = target.querySelector('button'); + await button?.click(); + + assert.htmlEqual( + target.innerHTML, + ` + + ` + ); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/store-from-props-hoisting/child.svelte b/packages/svelte/tests/runtime-runes/samples/store-from-props-hoisting/child.svelte new file mode 100644 index 0000000000..77a04f3e9f --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/store-from-props-hoisting/child.svelte @@ -0,0 +1,8 @@ + + + diff --git a/packages/svelte/tests/runtime-runes/samples/store-from-props-hoisting/main.svelte b/packages/svelte/tests/runtime-runes/samples/store-from-props-hoisting/main.svelte new file mode 100644 index 0000000000..0e7390d164 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/store-from-props-hoisting/main.svelte @@ -0,0 +1,7 @@ + + + \ No newline at end of file diff --git a/packages/svelte/tests/validator/samples/binding-select-multiple-dynamic/errors.json b/packages/svelte/tests/validator/samples/binding-select-multiple-dynamic/errors.json index 9506d800ee..8430d40d61 100644 --- a/packages/svelte/tests/validator/samples/binding-select-multiple-dynamic/errors.json +++ b/packages/svelte/tests/validator/samples/binding-select-multiple-dynamic/errors.json @@ -3,11 +3,11 @@ "code": "attribute_invalid_multiple", "message": "'multiple' attribute must be static if select uses two-way binding", "start": { - "line": 6, + "line": 14, "column": 19 }, "end": { - "line": 6, + "line": 14, "column": 29 } } diff --git a/packages/svelte/tests/validator/samples/binding-select-multiple-dynamic/input.svelte b/packages/svelte/tests/validator/samples/binding-select-multiple-dynamic/input.svelte index 6fb0df4d47..4908672985 100644 --- a/packages/svelte/tests/validator/samples/binding-select-multiple-dynamic/input.svelte +++ b/packages/svelte/tests/validator/samples/binding-select-multiple-dynamic/input.svelte @@ -3,6 +3,14 @@ let multiple; + + + +