diff --git a/.changeset/chilly-rocks-hug.md b/.changeset/chilly-rocks-hug.md new file mode 100644 index 0000000000..cccc87da32 --- /dev/null +++ b/.changeset/chilly-rocks-hug.md @@ -0,0 +1,5 @@ +--- +"svelte": patch +--- + +feat: introduce `$host` rune, deprecate `createEventDispatcher` diff --git a/.changeset/cuddly-points-tickle.md b/.changeset/cuddly-points-tickle.md new file mode 100644 index 0000000000..4a75553b42 --- /dev/null +++ b/.changeset/cuddly-points-tickle.md @@ -0,0 +1,5 @@ +--- +"svelte": patch +--- + +fix: execute sole static script tag diff --git a/.changeset/eight-hornets-punch.md b/.changeset/eight-hornets-punch.md new file mode 100644 index 0000000000..e4ae2217f4 --- /dev/null +++ b/.changeset/eight-hornets-punch.md @@ -0,0 +1,5 @@ +--- +"svelte": patch +--- + +fix: make static `element` property available for the SvelteComponent type diff --git a/.changeset/four-mice-hammer.md b/.changeset/four-mice-hammer.md new file mode 100644 index 0000000000..22d1af5925 --- /dev/null +++ b/.changeset/four-mice-hammer.md @@ -0,0 +1,5 @@ +--- +"svelte": patch +--- + +fix: improve internal proxied state signal heuristic diff --git a/.changeset/giant-plants-grin.md b/.changeset/giant-plants-grin.md new file mode 100644 index 0000000000..4f5db1712a --- /dev/null +++ b/.changeset/giant-plants-grin.md @@ -0,0 +1,5 @@ +--- +"svelte": patch +--- + +fix: keep sibling selectors when dealing with slots/render tags/`svelte:element` tags diff --git a/.changeset/heavy-ducks-leave.md b/.changeset/heavy-ducks-leave.md new file mode 100644 index 0000000000..6adf79c667 --- /dev/null +++ b/.changeset/heavy-ducks-leave.md @@ -0,0 +1,5 @@ +--- +"svelte": patch +--- + +breaking: robustify interop of exports and props in runes mode diff --git a/.changeset/mighty-frogs-obey.md b/.changeset/mighty-frogs-obey.md new file mode 100644 index 0000000000..0321c60463 --- /dev/null +++ b/.changeset/mighty-frogs-obey.md @@ -0,0 +1,5 @@ +--- +"svelte": patch +--- + +fix: ensure deep mutation ownership widening diff --git a/.changeset/popular-walls-hunt.md b/.changeset/popular-walls-hunt.md new file mode 100644 index 0000000000..9dd06c2d4e --- /dev/null +++ b/.changeset/popular-walls-hunt.md @@ -0,0 +1,5 @@ +--- +"svelte": patch +--- + +fix: improve compiled output of multiple call expression in single text node diff --git a/.changeset/pre.json b/.changeset/pre.json index 9236431537..9feb3674db 100644 --- a/.changeset/pre.json +++ b/.changeset/pre.json @@ -132,6 +132,7 @@ "happy-suits-film", "healthy-planes-vanish", "heavy-comics-move", + "heavy-ducks-leave", "heavy-ears-rule", "hip-balloons-begin", "honest-buses-add", @@ -250,6 +251,7 @@ "red-feet-worry", "red-poets-study", "rich-cobras-exist", + "rich-garlics-laugh", "rich-olives-yell", "rich-sheep-burn", "rich-tables-sing", @@ -366,6 +368,7 @@ "three-icons-trade", "three-lions-visit", "three-papayas-buy", + "three-rice-tie", "three-suits-grin", "tidy-buses-whisper", "tidy-starfishes-allow", diff --git a/.changeset/proud-queens-sniff.md b/.changeset/proud-queens-sniff.md new file mode 100644 index 0000000000..e2970fd4bc --- /dev/null +++ b/.changeset/proud-queens-sniff.md @@ -0,0 +1,5 @@ +--- +"svelte": patch +--- + +fix: improve hydration of svelte head blocks diff --git a/.changeset/rich-garlics-laugh.md b/.changeset/rich-garlics-laugh.md new file mode 100644 index 0000000000..c9230dfb80 --- /dev/null +++ b/.changeset/rich-garlics-laugh.md @@ -0,0 +1,5 @@ +--- +"svelte": patch +--- + +fix: improve handled of unowned derived signals diff --git a/packages/svelte/CHANGELOG.md b/packages/svelte/CHANGELOG.md index 19bd7bc66d..ef939c54e5 100644 --- a/packages/svelte/CHANGELOG.md +++ b/packages/svelte/CHANGELOG.md @@ -1,5 +1,15 @@ # svelte +## 5.0.0-next.95 + +### Patch Changes + +- breaking: robustify interop of exports and props in runes mode ([#11064](https://github.com/sveltejs/svelte/pull/11064)) + +- fix: improve handled of unowned derived signals ([#11077](https://github.com/sveltejs/svelte/pull/11077)) + +- fix: bundle CSS types ([#11067](https://github.com/sveltejs/svelte/pull/11067)) + ## 5.0.0-next.94 ### Patch Changes diff --git a/packages/svelte/package.json b/packages/svelte/package.json index 3f6f68577d..f164de0cb2 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.94", + "version": "5.0.0-next.95", "type": "module", "types": "./types/index.d.ts", "engines": { diff --git a/packages/svelte/src/ambient.d.ts b/packages/svelte/src/ambient.d.ts index a2ad6d63af..f923aace5a 100644 --- a/packages/svelte/src/ambient.d.ts +++ b/packages/svelte/src/ambient.d.ts @@ -211,3 +211,24 @@ declare function $bindable(t?: T): T; declare function $inspect( ...values: T ): { with: (fn: (type: 'init' | 'update', ...values: T) => void) => void }; + +/** + * Retrieves the `this` reference of the custom element that contains this component. Example: + * + * ```svelte + * + * + * + * + * + * ``` + * + * Only available inside custom element components, and only on the client-side. + * + * https://svelte-5-preview.vercel.app/docs/runes#$host + */ +declare function $host(): El; diff --git a/packages/svelte/src/compiler/errors.js b/packages/svelte/src/compiler/errors.js index 318225be95..6a867ce12c 100644 --- a/packages/svelte/src/compiler/errors.js +++ b/packages/svelte/src/compiler/errors.js @@ -187,6 +187,8 @@ const runes = { 'invalid-state-location': (rune) => `${rune}(...) can only be used as a variable declaration initializer or a class field`, 'invalid-effect-location': () => `$effect() can only be used as an expression statement`, + 'invalid-host-location': () => + `$host() can only be used inside custom element component instances`, /** * @param {boolean} is_binding * @param {boolean} show_details @@ -212,7 +214,9 @@ const runes = { 'duplicate-props-rune': () => `Cannot use $props() more than once`, 'invalid-each-assignment': () => `Cannot reassign or bind to each block argument in runes mode. Use the array and index variables instead (e.g. 'array[i] = value' instead of 'entry = value')`, - 'invalid-derived-call': () => `$derived.call(...) has been replaced with $derived.by(...)` + 'invalid-derived-call': () => `$derived.call(...) has been replaced with $derived.by(...)`, + 'conflicting-property-name': () => + `Cannot have a property and a component export with the same name` }; /** @satisfies {Errors} */ 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 ee5c8395f8..ef64a97953 100644 --- a/packages/svelte/src/compiler/phases/1-parse/read/style.js +++ b/packages/svelte/src/compiler/phases/1-parse/read/style.js @@ -36,7 +36,8 @@ export default function read_style(parser, start, attributes) { content: { start: content_start, end: content_end, - styles: parser.template.slice(content_start, content_end) + styles: parser.template.slice(content_start, content_end), + comment: null } }; } diff --git a/packages/svelte/src/compiler/phases/1-parse/state/element.js b/packages/svelte/src/compiler/phases/1-parse/state/element.js index 16960e9721..42c48a89ad 100644 --- a/packages/svelte/src/compiler/phases/1-parse/state/element.js +++ b/packages/svelte/src/compiler/phases/1-parse/state/element.js @@ -283,25 +283,26 @@ export default function tag(parser) { if (is_top_level_script_or_style) { parser.eat('>', true); - if (name === 'script') { - const content = read_script(parser, start, element.attributes); - /** @type {import('#compiler').Comment | null} */ - let prev_comment = null; - for (let i = current.fragment.nodes.length - 1; i >= 0; i--) { - const node = current.fragment.nodes[i]; + /** @type {import('#compiler').Comment | null} */ + let prev_comment = null; + for (let i = current.fragment.nodes.length - 1; i >= 0; i--) { + const node = current.fragment.nodes[i]; - if (i === current.fragment.nodes.length - 1 && node.end !== start) { - break; - } + if (i === current.fragment.nodes.length - 1 && node.end !== start) { + break; + } - if (node.type === 'Comment') { - prev_comment = node; - break; - } else if (node.type !== 'Text' || node.data.trim()) { - break; - } + if (node.type === 'Comment') { + prev_comment = node; + break; + } else if (node.type !== 'Text' || node.data.trim()) { + break; } + } + + if (name === 'script') { + const content = read_script(parser, start, element.attributes); if (prev_comment) { // We take advantage of the fact that the root will never have leadingComments set, // and set the previous comment to it so that the warning mechanism can later @@ -318,6 +319,7 @@ export default function tag(parser) { } } else { const content = read_style(parser, start, element.attributes); + content.content.comment = prev_comment; if (current.css) error(start, 'duplicate-style-element'); current.css = content; 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 c299612fd1..a5010c543a 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 @@ -175,7 +175,13 @@ function apply_selector(relative_selectors, rule, element, stylesheet) { let sibling_matched = false; for (const possible_sibling of siblings.keys()) { - if (apply_selector(parent_selectors, rule, possible_sibling, stylesheet)) { + if (possible_sibling.type === 'RenderTag' || possible_sibling.type === 'SlotElement') { + // `{@render foo()}

foo

` with `:global(.x) + p` is a match + if (parent_selectors.length === 1 && parent_selectors[0].metadata.is_global) { + mark(relative_selector, element); + sibling_matched = true; + } + } else if (apply_selector(parent_selectors, rule, possible_sibling, stylesheet)) { mark(relative_selector, element); sibling_matched = true; } @@ -564,38 +570,39 @@ function get_element_parent(node) { function find_previous_sibling(node) { /** @type {import('#compiler').SvelteNode} */ let current_node = node; - do { - if (current_node.type === 'SlotElement') { - const slot_children = current_node.fragment.nodes; - if (slot_children.length > 0) { - current_node = slot_children.slice(-1)[0]; // go to its last child first - continue; - } - } - while ( - // @ts-expect-error TODO - !current_node.prev && - // @ts-expect-error TODO - current_node.parent && - // @ts-expect-error TODO - current_node.parent.type === 'SlotElement' - ) { - // @ts-expect-error TODO - current_node = current_node.parent; + + while ( + // @ts-expect-error TODO + !current_node.prev && + // @ts-expect-error TODO + current_node.parent?.type === 'SlotElement' + ) { + // @ts-expect-error TODO + current_node = current_node.parent; + } + + // @ts-expect-error + current_node = current_node.prev; + + while (current_node?.type === 'SlotElement') { + const slot_children = current_node.fragment.nodes; + if (slot_children.length > 0) { + current_node = slot_children.slice(-1)[0]; + } else { + break; } - // @ts-expect-error - current_node = current_node.prev; - } while (current_node && current_node.type === 'SlotElement'); + } + return current_node; } /** * @param {import('#compiler').SvelteNode} node * @param {boolean} adjacent_only - * @returns {Map} + * @returns {Map} */ function get_possible_element_siblings(node, adjacent_only) { - /** @type {Map} */ + /** @type {Map} */ const result = new Map(); /** @type {import('#compiler').SvelteNode} */ @@ -618,6 +625,14 @@ function get_possible_element_siblings(node, adjacent_only) { if (adjacent_only && has_definite_elements(possible_last_child)) { return result; } + } else if ( + prev.type === 'SlotElement' || + prev.type === 'RenderTag' || + prev.type === 'SvelteElement' + ) { + result.set(prev, NODE_PROBABLY_EXISTS); + // Special case: slots, render tags and svelte:element tags could resolve to no siblings, + // so we want to continue until we find a definite sibling even with the adjacent-only combinator } } @@ -720,7 +735,7 @@ function get_possible_last_child(relative_selector, adjacent_only) { } /** - * @param {Map} result + * @param {Map} result * @returns {boolean} */ function has_definite_elements(result) { @@ -734,8 +749,9 @@ function has_definite_elements(result) { } /** - * @param {Map} from - * @param {Map} to + * @template T + * @param {Map} from + * @param {Map} to * @returns {void} */ function add_to_map(from, to) { diff --git a/packages/svelte/src/compiler/phases/2-analyze/css/css-warn.js b/packages/svelte/src/compiler/phases/2-analyze/css/css-warn.js new file mode 100644 index 0000000000..8d7fef873f --- /dev/null +++ b/packages/svelte/src/compiler/phases/2-analyze/css/css-warn.js @@ -0,0 +1,34 @@ +import { walk } from 'zimmerframe'; +import { warn } from '../../../warnings.js'; +import { is_keyframes_node } from '../../css.js'; + +/** + * @param {import('#compiler').Css.StyleSheet} stylesheet + * @param {import('../../types.js').RawWarning[]} warnings + */ +export function warn_unused(stylesheet, warnings) { + walk(stylesheet, { warnings, stylesheet }, visitors); +} + +/** @type {import('zimmerframe').Visitors} */ +const visitors = { + Atrule(node, context) { + if (!is_keyframes_node(node)) { + context.next(); + } + }, + PseudoClassSelector(node, context) { + if (node.name === 'is' || node.name === 'where') { + context.next(); + } + }, + ComplexSelector(node, context) { + if (!node.metadata.used) { + const content = context.state.stylesheet.content; + const text = content.styles.substring(node.start - content.start, node.end - content.start); + warn(context.state.warnings, node, context.path, 'css-unused-selector', text); + } + + context.next(); + } +}; diff --git a/packages/svelte/src/compiler/phases/2-analyze/index.js b/packages/svelte/src/compiler/phases/2-analyze/index.js index 5773a562ee..d9c6dfe64a 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/index.js +++ b/packages/svelte/src/compiler/phases/2-analyze/index.js @@ -23,6 +23,7 @@ import { should_proxy_or_freeze } from '../3-transform/client/utils.js'; import { analyze_css } from './css/css-analyze.js'; import { prune } from './css/css-prune.js'; import { hash } from './utils.js'; +import { warn_unused } from './css/css-warn.js'; /** * @param {import('#compiler').Script | null} script @@ -437,6 +438,20 @@ export function analyze_component(root, source, options) { merge(set_scope(scopes), validation_runes, runes_scope_tweaker, common_visitors) ); } + + if (analysis.exports.length > 0) { + for (const [_, binding] of instance.scope.declarations) { + if (binding.kind === 'prop' || binding.kind === 'bindable_prop') { + if ( + analysis.exports.some( + ({ alias, name }) => (binding.prop_alias ?? binding.node.name) === (alias ?? name) + ) + ) { + error(binding.node, 'conflicting-property-name'); + } + } + } + } } else { instance.scope.declare(b.id('$$props'), 'bindable_prop', 'synthetic'); instance.scope.declare(b.id('$$restProps'), 'rest_prop', 'synthetic'); @@ -534,6 +549,7 @@ export function analyze_component(root, source, options) { for (const element of analysis.elements) { prune(analysis.css.ast, element); } + warn_unused(analysis.css.ast, analysis.warnings); outer: for (const element of analysis.elements) { if (element.metadata.scoped) { diff --git a/packages/svelte/src/compiler/phases/2-analyze/validation.js b/packages/svelte/src/compiler/phases/2-analyze/validation.js index 69d879d021..ed840f5303 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/validation.js +++ b/packages/svelte/src/compiler/phases/2-analyze/validation.js @@ -896,6 +896,9 @@ export const validation_runes_js = { } }, CallExpression(node, { state, path }) { + if (get_rune(node, state.scope) === '$host') { + error(node, 'invalid-host-location'); + } validate_call_expression(node, state.scope, path); }, VariableDeclarator(node, { state }) { @@ -1063,9 +1066,17 @@ export const validation_runes = merge(validation, a11y_validators, { } }, CallExpression(node, { state, path }) { - if (get_rune(node, state.scope) === '$bindable' && node.arguments.length > 1) { + const rune = get_rune(node, state.scope); + if (rune === '$bindable' && node.arguments.length > 1) { error(node, 'invalid-rune-args-length', '$bindable', [0, 1]); + } else if (rune === '$host') { + if (node.arguments.length > 0) { + error(node, 'invalid-rune-args-length', '$host', [0]); + } else if (state.ast_type === 'module' || !state.analysis.custom_element) { + error(node, 'invalid-host-location'); + } } + validate_call_expression(node, state.scope, path); }, EachBlock(node, { next, state }) { diff --git a/packages/svelte/src/compiler/phases/3-transform/client/transform-client.js b/packages/svelte/src/compiler/phases/3-transform/client/transform-client.js index 8c88a405f1..f77766a8e7 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/transform-client.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/transform-client.js @@ -255,8 +255,7 @@ export function client_component(source, analysis, options) { ); if (analysis.runes && options.dev) { - /** @type {import('estree').Literal[]} */ - const bindable = []; + const bindable = analysis.exports.map(({ name, alias }) => b.literal(alias ?? name)); for (const [name, binding] of properties) { if (binding.kind === 'bindable_prop') { bindable.push(b.literal(binding.prop_alias ?? name)); @@ -382,7 +381,6 @@ export function client_component(source, analysis, options) { ); if (analysis.uses_rest_props) { - /** @type {string[]} */ const named_props = analysis.exports.map(({ name, alias }) => alias ?? name); for (const [name, binding] of analysis.instance.scope.declarations) { if (binding.kind === 'bindable_prop') named_props.push(binding.prop_alias ?? name); @@ -401,15 +399,12 @@ export function client_component(source, analysis, options) { } if (analysis.uses_props || analysis.uses_rest_props) { + const to_remove = [b.literal('children'), b.literal('$$slots'), b.literal('$$events')]; + if (analysis.custom_element) { + to_remove.push(b.literal('$$host')); + } component_block.body.unshift( - b.const( - '$$sanitized_props', - b.call( - '$.rest_props', - b.id('$$props'), - b.array([b.literal('children'), b.literal('$$slots'), b.literal('$$events')]) - ) - ) + b.const('$$sanitized_props', b.call('$.rest_props', b.id('$$props'), b.array(to_remove))) ); } 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 25a2564453..5c4169b409 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 @@ -195,8 +195,7 @@ export const javascript_visitors_runes = { if (rune === '$props') { assert.equal(declarator.id.type, 'ObjectPattern'); - /** @type {string[]} */ - const seen = []; + const seen = state.analysis.exports.map(({ name, alias }) => alias ?? name); for (const property of declarator.id.properties) { if (property.type === 'Property') { @@ -381,6 +380,10 @@ export const javascript_visitors_runes = { CallExpression(node, context) { const rune = get_rune(node, context.state.scope); + if (rune === '$host') { + return b.id('$$props.$$host'); + } + if (rune === '$effect.active') { return b.call('$.effect_active'); } 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 1d8b7acb71..2ff6fe1715 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 @@ -1396,7 +1396,7 @@ function process_children(nodes, expression, is_element, { visit, state }) { state.template.push(' '); - const [contains_call_expression, value] = serialize_template_literal(sequence, visit); + const [contains_call_expression, value] = serialize_template_literal(sequence, visit, state); const update = b.stmt(b.call('$.set_text', text_id, value)); @@ -1511,25 +1511,39 @@ function serialize_attribute_value(attribute_value, context) { } } - return serialize_template_literal(attribute_value, context.visit); + return serialize_template_literal(attribute_value, context.visit, context.state); } /** * @param {Array} values * @param {(node: import('#compiler').SvelteNode) => any} visit + * @param {import("../types.js").ComponentClientTransformState} state * @returns {[boolean, import('estree').TemplateLiteral]} */ -function serialize_template_literal(values, visit) { +function serialize_template_literal(values, visit, state) { /** @type {import('estree').TemplateElement[]} */ const quasis = []; /** @type {import('estree').Expression[]} */ const expressions = []; let contains_call_expression = false; + let contains_multiple_call_expression = false; quasis.push(b.quasi('')); for (let i = 0; i < values.length; i++) { const node = values[i]; + + if (node.type === 'ExpressionTag' && node.metadata.contains_call_expression) { + if (contains_call_expression) { + contains_multiple_call_expression = true; + } + contains_call_expression = true; + } + } + + for (let i = 0; i < values.length; i++) { + const node = values[i]; + if (node.type === 'Text') { const last = /** @type {import('estree').TemplateElement} */ (quasis.at(-1)); last.value.raw += sanitize_template_string(node.data); @@ -1539,11 +1553,23 @@ function serialize_template_literal(values, visit) { last.value.raw += sanitize_template_string(node.expression.value + ''); } } else { - if (node.type === 'ExpressionTag' && node.metadata.contains_call_expression) { - contains_call_expression = true; - } + if (contains_multiple_call_expression) { + const id = b.id(state.scope.generate('stringified_text')); - expressions.push(b.call('$.stringify', visit(node.expression))); + state.init.push( + b.const( + id, + b.call( + // In runes mode, we want things to be fine-grained - but not in legacy mode + state.analysis.runes ? '$.derived' : '$.derived_safe_equal', + b.thunk(/** @type {import('estree').Expression} */ (visit(node.expression))) + ) + ) + ); + expressions.push(b.call('$.get', id)); + } else { + expressions.push(b.call('$.stringify', visit(node.expression))); + } quasis.push(b.quasi('', i + 1 === values.length)); } } @@ -1586,7 +1612,7 @@ export const template_visitors = { declaration.id, b.call( // In runes mode, we want things to be fine-grained - but not in legacy mode - state.options.runes ? '$.derived' : '$.derived_safe_equal', + state.analysis.runes ? '$.derived' : '$.derived_safe_equal', b.thunk(/** @type {import('estree').Expression} */ (visit(declaration.init))) ) ) @@ -1623,7 +1649,7 @@ export const template_visitors = { state.init.push( // In runes mode, we want things to be fine-grained - but not in legacy mode - b.const(tmp, b.call(state.options.runes ? '$.derived' : '$.derived_safe_equal', fn)) + b.const(tmp, b.call(state.analysis.runes ? '$.derived' : '$.derived_safe_equal', fn)) ); // we need to eagerly evaluate the expression in order to hit any @@ -2972,7 +2998,7 @@ export const template_visitors = { b.assignment( '=', b.member(b.id('$.document'), b.id('title')), - serialize_template_literal(/** @type {any} */ (node.fragment.nodes), visit)[1] + serialize_template_literal(/** @type {any} */ (node.fragment.nodes), visit, state)[1] ) ) ); diff --git a/packages/svelte/src/compiler/phases/3-transform/server/transform-server.js b/packages/svelte/src/compiler/phases/3-transform/server/transform-server.js index 19c626ae58..3f07e4dbfb 100644 --- a/packages/svelte/src/compiler/phases/3-transform/server/transform-server.js +++ b/packages/svelte/src/compiler/phases/3-transform/server/transform-server.js @@ -691,7 +691,8 @@ const javascript_visitors_runes = { } if (rune === '$props') { - // remove $bindable() from props declaration + // remove $bindable() from props declaration and handle rest props + let uses_rest_props = false; const id = walk(declarator.id, null, { AssignmentPattern(node) { if ( @@ -703,9 +704,26 @@ const javascript_visitors_runes = { : b.id('undefined'); return b.assignment_pattern(node.left, right); } + }, + RestElement(node, { path }) { + if (path.at(-1) === declarator.id) { + uses_rest_props = true; + } } }); - declarations.push(b.declarator(id, b.id('$$props'))); + + const exports = /** @type {import('../../types').ComponentAnalysis} */ ( + state.analysis + ).exports.map(({ name, alias }) => b.literal(alias ?? name)); + + declarations.push( + b.declarator( + id, + uses_rest_props && exports.length > 0 + ? b.call('$.rest_props', b.id('$$props'), b.array(exports)) + : b.id('$$props') + ) + ); continue; } @@ -767,6 +785,10 @@ const javascript_visitors_runes = { CallExpression(node, context) { const rune = get_rune(node, context.state.scope); + if (rune === '$host') { + return b.id('undefined'); + } + if (rune === '$effect.active') { return b.literal(false); } diff --git a/packages/svelte/src/compiler/phases/constants.js b/packages/svelte/src/compiler/phases/constants.js index 822174d4dd..c61015e92a 100644 --- a/packages/svelte/src/compiler/phases/constants.js +++ b/packages/svelte/src/compiler/phases/constants.js @@ -40,7 +40,8 @@ export const Runes = /** @type {const} */ ([ '$effect.active', '$effect.root', '$inspect', - '$inspect().with' + '$inspect().with', + '$host' ]); /** diff --git a/packages/svelte/src/compiler/types/css.d.ts b/packages/svelte/src/compiler/types/css.d.ts index efa33e9e5a..4bce904425 100644 --- a/packages/svelte/src/compiler/types/css.d.ts +++ b/packages/svelte/src/compiler/types/css.d.ts @@ -1,3 +1,5 @@ +import type { Comment } from '#compiler'; + export namespace Css { export interface BaseNode { start: number; @@ -12,6 +14,8 @@ export namespace Css { start: number; end: number; styles: string; + /** Possible comment atop the style tag */ + comment: Comment | null; }; } diff --git a/packages/svelte/src/compiler/warnings.js b/packages/svelte/src/compiler/warnings.js index 7b863ef2b1..8f0e81d9c0 100644 --- a/packages/svelte/src/compiler/warnings.js +++ b/packages/svelte/src/compiler/warnings.js @@ -7,7 +7,8 @@ import { /** @satisfies {Warnings} */ const css = { - 'unused-selector': () => 'Unused CSS selector' + /** @param {string} name */ + 'css-unused-selector': (name) => `Unused CSS selector "${name}"` }; /** @satisfies {Warnings} */ @@ -300,6 +301,11 @@ export function warn(array, node, path, code, ...args) { ) ); } + + // Style nodes + if (current.type === 'StyleSheet' && current.content.comment) { + ignores.push(...current.content.comment.ignores); + } } if (ignores.includes(code)) return; diff --git a/packages/svelte/src/index-client.js b/packages/svelte/src/index-client.js index 2b22037457..215a3379ff 100644 --- a/packages/svelte/src/index-client.js +++ b/packages/svelte/src/index-client.js @@ -80,6 +80,7 @@ function create_custom_event(type, detail, { bubbles = false, cancelable = false * ``` * * https://svelte.dev/docs/svelte#createeventdispatcher + * @deprecated Use callback props and/or the `$host()` rune instead — see https://svelte-5-preview.vercel.app/docs/deprecations#createeventdispatcher * @template {Record} [EventMap = any] * @returns {import('./index.js').EventDispatcher} */ diff --git a/packages/svelte/src/index.d.ts b/packages/svelte/src/index.d.ts index a9e7ce8477..afe3e87f09 100644 --- a/packages/svelte/src/index.d.ts +++ b/packages/svelte/src/index.d.ts @@ -74,6 +74,9 @@ export class SvelteComponent< Events extends Record = any, Slots extends Record = any > { + /** The custom element version of the component. Only present if compiled with the `customElement` compiler option */ + static element?: typeof HTMLElement; + [prop: string]: any; /** * @deprecated This constructor only exists when using the `asClassComponent` compatibility helper, which diff --git a/packages/svelte/src/internal/client/dev/ownership.js b/packages/svelte/src/internal/client/dev/ownership.js index 358b56df22..fde316c840 100644 --- a/packages/svelte/src/internal/client/dev/ownership.js +++ b/packages/svelte/src/internal/client/dev/ownership.js @@ -2,6 +2,7 @@ import { STATE_SYMBOL } from '../constants.js'; import { untrack } from '../runtime.js'; +import { get_descriptors } from '../utils.js'; /** @type {Record>} */ const boundaries = {}; @@ -91,49 +92,107 @@ export function mark_module_end() { } } +let add_owner_visited = new Set(); + /** * * @param {any} object * @param {any} owner */ export function add_owner(object, owner) { - untrack(() => { - add_owner_to_object(object, owner); - }); + // Needed because ownership addition can invoke getters on a proxy, + // calling add_owner anew, so just keeping the set as part of + // add_owner_to_object would not be enough. + const prev = add_owner_visited; + try { + add_owner_visited = new Set(add_owner_visited); + untrack(() => { + add_owner_to_object(object, owner, add_owner_visited); + }); + } finally { + add_owner_visited = prev; + } } /** * @param {any} object * @param {Function} owner + * @param {Set} visited */ -function add_owner_to_object(object, owner) { +function add_owner_to_object(object, owner, visited) { + if (visited.has(object)) return; + visited.add(object); + if (object?.[STATE_SYMBOL]?.o && !object[STATE_SYMBOL].o.has(owner)) { object[STATE_SYMBOL].o.add(owner); - - for (const key in object) { - add_owner_to_object(object[key], owner); - } } + // Not inside previous if-block; there could be normal objects in-between + traverse_for_owners(object, (nested) => add_owner_to_object(nested, owner, visited)); } +let strip_owner_visited = new Set(); + /** * @param {any} object */ export function strip_owner(object) { - untrack(() => { - strip_owner_from_object(object); - }); + // Needed because ownership stripping can invoke getters on a proxy, + // calling strip_owner anew, so just keeping the set as part of + // strip_owner_from_object would not be enough. + const prev = strip_owner_visited; + try { + untrack(() => { + strip_owner_from_object(object, strip_owner_visited); + }); + } finally { + strip_owner_visited = prev; + } } /** * @param {any} object + * @param {Set} visited */ -function strip_owner_from_object(object) { +function strip_owner_from_object(object, visited) { + if (visited.has(object)) return; + visited.add(object); + if (object?.[STATE_SYMBOL]?.o) { object[STATE_SYMBOL].o = null; + } + // Not inside previous if-block; there could be normal objects in-between + traverse_for_owners(object, (nested) => strip_owner_from_object(nested, visited)); +} +/** + * @param {any} object + * @param {(obj: any) => void} cb + */ +function traverse_for_owners(object, cb) { + if (typeof object === 'object' && object !== null && !(object instanceof EventTarget)) { for (const key in object) { - strip_owner(object[key]); + cb(object[key]); + } + // deal with state on classes + const proto = Object.getPrototypeOf(object); + if ( + proto !== Object.prototype && + proto !== Array.prototype && + proto !== Map.prototype && + proto !== Set.prototype && + proto !== Date.prototype + ) { + const descriptors = get_descriptors(proto); + for (let key in descriptors) { + const get = descriptors[key].get; + if (get) { + try { + cb(object[key]); + } catch (e) { + // continue + } + } + } } } } 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 70606cd32f..b00a3a242b 100644 --- a/packages/svelte/src/internal/client/dom/blocks/svelte-head.js +++ b/packages/svelte/src/internal/client/dom/blocks/svelte-head.js @@ -1,7 +1,16 @@ import { hydrate_anchor, hydrate_nodes, hydrating, set_hydrate_nodes } from '../hydration.js'; import { empty } from '../operations.js'; import { block } from '../../reactivity/effects.js'; -import { HYDRATION_START } from '../../../../constants.js'; +import { HYDRATION_END, HYDRATION_START } from '../../../../constants.js'; + +/** + * @type {Node | undefined} + */ +let head_anchor; + +export function reset_head_anchor() { + head_anchor = undefined; +} /** * @param {(anchor: Node) => import('#client').Dom | void} render_fn @@ -19,12 +28,20 @@ export function head(render_fn) { if (hydrating) { previous_hydrate_nodes = hydrate_nodes; - let anchor = /** @type {import('#client').TemplateNode} */ (document.head.firstChild); - while (anchor.nodeType !== 8 || /** @type {Comment} */ (anchor).data !== HYDRATION_START) { - anchor = /** @type {import('#client').TemplateNode} */ (anchor.nextSibling); + // There might be multiple head blocks in our app, so we need to account for each one needing independent hydration. + if (head_anchor === undefined) { + head_anchor = /** @type {import('#client').TemplateNode} */ (document.head.firstChild); + } + + while ( + head_anchor.nodeType !== 8 || + /** @type {Comment} */ (head_anchor).data !== HYDRATION_START + ) { + head_anchor = /** @type {import('#client').TemplateNode} */ (head_anchor.nextSibling); } - anchor = /** @type {import('#client').TemplateNode} */ (hydrate_anchor(anchor)); + head_anchor = /** @type {import('#client').TemplateNode} */ (hydrate_anchor(head_anchor)); + head_anchor = /** @type {import('#client').TemplateNode} */ (head_anchor.nextSibling); } else { anchor = document.head.appendChild(empty()); } diff --git a/packages/svelte/src/internal/client/dom/elements/custom-element.js b/packages/svelte/src/internal/client/dom/elements/custom-element.js index 5ea42a9628..eb47d674c2 100644 --- a/packages/svelte/src/internal/client/dom/elements/custom-element.js +++ b/packages/svelte/src/internal/client/dom/elements/custom-element.js @@ -138,7 +138,8 @@ if (typeof HTMLElement === 'function') { target: this.shadowRoot || this, props: { ...this.$$d, - $$slots + $$slots, + $$host: this } }); diff --git a/packages/svelte/src/internal/client/dom/template.js b/packages/svelte/src/internal/client/dom/template.js index 827508b739..8e1f51c8eb 100644 --- a/packages/svelte/src/internal/client/dom/template.js +++ b/packages/svelte/src/internal/client/dom/template.js @@ -3,6 +3,7 @@ import { clone_node, empty } from './operations.js'; import { create_fragment_from_html } from './reconciler.js'; import { current_effect } from '../runtime.js'; import { TEMPLATE_FRAGMENT, TEMPLATE_USE_IMPORT_NODE } from '../../../constants.js'; +import { effect } from '../reactivity/effects.js'; /** * @param {string} content @@ -120,14 +121,29 @@ export function svg_template_with_script(content, flags) { * @param {Element | DocumentFragment} node */ function run_scripts(node) { - for (const script of node.querySelectorAll('script')) { + // scripts were SSR'd, in which case they will run + if (hydrating) return; + + const scripts = + /** @type {HTMLElement} */ (node).tagName === 'SCRIPT' + ? [/** @type {HTMLScriptElement} */ (node)] + : node.querySelectorAll('script'); + for (const script of scripts) { var clone = document.createElement('script'); for (var attribute of script.attributes) { clone.setAttribute(attribute.name, attribute.value); } clone.textContent = script.textContent; - script.replaceWith(clone); + // If node === script tag, replaceWith will do nothing because there's no parent yet, + // waiting until that's the case using an effect solves this. + // Don't do it in other circumstances or we could accidentally execute scripts + // in an adjacent @html tag that was instantiated in the meantime. + if (script === node) { + effect(() => script.replaceWith(clone)); + } else { + script.replaceWith(clone); + } } } diff --git a/packages/svelte/src/internal/client/proxy.js b/packages/svelte/src/internal/client/proxy.js index bf4e15c5ff..6c69262418 100644 --- a/packages/svelte/src/internal/client/proxy.js +++ b/packages/svelte/src/internal/client/proxy.js @@ -1,6 +1,11 @@ import { DEV } from 'esm-env'; -import { get, batch_inspect, current_component_context, untrack } from './runtime.js'; -import { effect_active } from './reactivity/effects.js'; +import { + get, + batch_inspect, + current_component_context, + untrack, + current_effect +} from './runtime.js'; import { array_prototype, define_property, @@ -206,7 +211,7 @@ const state_proxy_handler = { // but only if it's an own property and not a prototype property if ( s === undefined && - (effect_active() || updating_derived) && + (current_effect !== null || updating_derived) && (!(prop in target) || get_descriptor(target, prop)?.writable) ) { s = (metadata.i ? source : mutable_source)(proxy(target[prop], metadata.i, metadata.o)); @@ -250,7 +255,10 @@ const state_proxy_handler = { const has = Reflect.has(target, prop); let s = metadata.s.get(prop); - if (s !== undefined || (effect_active() && (!has || get_descriptor(target, prop)?.writable))) { + if ( + s !== undefined || + (current_effect !== null && (!has || get_descriptor(target, prop)?.writable)) + ) { if (s === undefined) { s = (metadata.i ? source : mutable_source)( has ? proxy(target[prop], metadata.i, metadata.o) : UNINITIALIZED @@ -273,7 +281,7 @@ const state_proxy_handler = { // we do so otherwise if we read it later, then the write won't be tracked and // the heuristics of effects will be different vs if we had read the proxied // object property before writing to that property. - if (s === undefined && effect_active()) { + if (s === undefined && current_effect !== null) { // the read creates a signal untrack(() => receiver[prop]); s = metadata.s.get(prop); diff --git a/packages/svelte/src/internal/client/render.js b/packages/svelte/src/internal/client/render.js index 1bfcd74f53..b121d49991 100644 --- a/packages/svelte/src/internal/client/render.js +++ b/packages/svelte/src/internal/client/render.js @@ -18,6 +18,7 @@ import { } from './dom/hydration.js'; import { array_from } from './utils.js'; import { handle_event_propagation } from './dom/elements/events.js'; +import { reset_head_anchor } from './dom/blocks/svelte-head.js'; /** @type {Set} */ export const all_registered_events = new Set(); @@ -175,6 +176,7 @@ export function hydrate(component, options) { } finally { set_hydrating(!!previous_hydrate_nodes); set_hydrate_nodes(previous_hydrate_nodes); + reset_head_anchor(); } } diff --git a/packages/svelte/src/internal/client/runtime.js b/packages/svelte/src/internal/client/runtime.js index 28428c1fb7..c1b8ee3e44 100644 --- a/packages/svelte/src/internal/client/runtime.js +++ b/packages/svelte/src/internal/client/runtime.js @@ -194,9 +194,21 @@ export function check_dirtiness(reaction) { // is also dirty. var version = dependency.version; - if (is_unowned && version > /** @type {import('#client').Derived} */ (reaction).version) { - /** @type {import('#client').Derived} */ (reaction).version = version; - return true; + if (is_unowned) { + if (version > /** @type {import('#client').Derived} */ (reaction).version) { + /** @type {import('#client').Derived} */ (reaction).version = version; + return true; + } else if (!current_skip_reaction && !dependency?.reactions?.includes(reaction)) { + // If we are working with an unowned signal as part of an effect (due to !current_skip_reaction) + // and the version hasn't changed, we still need to check that this reaction + // if linked to the dependency source – otherwise future updates will not be caught. + var reactions = dependency.reactions; + if (reactions === null) { + dependency.reactions = [reaction]; + } else { + reactions.push(reaction); + } + } } } } diff --git a/packages/svelte/src/internal/server/index.js b/packages/svelte/src/internal/server/index.js index b6d10cfca8..6a5e8aa177 100644 --- a/packages/svelte/src/internal/server/index.js +++ b/packages/svelte/src/internal/server/index.js @@ -207,10 +207,7 @@ export function render(component, options) { on_destroy = prev_on_destroy; return { - head: - payload.head.out || payload.head.title - ? payload.head.title + BLOCK_OPEN + payload.head.out + BLOCK_CLOSE - : '', + head: payload.head.out || payload.head.title ? payload.head.out + payload.head.title : '', html: payload.out }; } @@ -247,7 +244,9 @@ export function escape(value, is_attr = false) { */ export function head(payload, fn) { const head_payload = payload.head; + payload.head.out += BLOCK_OPEN; fn(head_payload); + payload.head.out += BLOCK_CLOSE; } /** diff --git a/packages/svelte/src/version.js b/packages/svelte/src/version.js index 728aa720a6..4d2bdaa4eb 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.94'; +export const VERSION = '5.0.0-next.95'; export const PUBLIC_VERSION = '5'; diff --git a/packages/svelte/tests/compiler-errors/samples/runes-prop-export-conflict/_config.js b/packages/svelte/tests/compiler-errors/samples/runes-prop-export-conflict/_config.js new file mode 100644 index 0000000000..a6e4a4c018 --- /dev/null +++ b/packages/svelte/tests/compiler-errors/samples/runes-prop-export-conflict/_config.js @@ -0,0 +1,8 @@ +import { test } from '../../test'; + +export default test({ + error: { + code: 'conflicting-property-name', + message: 'Cannot have a property and a component export with the same name' + } +}); diff --git a/packages/svelte/tests/compiler-errors/samples/runes-prop-export-conflict/main.svelte b/packages/svelte/tests/compiler-errors/samples/runes-prop-export-conflict/main.svelte new file mode 100644 index 0000000000..0ea9ef8fbb --- /dev/null +++ b/packages/svelte/tests/compiler-errors/samples/runes-prop-export-conflict/main.svelte @@ -0,0 +1,4 @@ + diff --git a/packages/svelte/tests/compiler-errors/samples/runes-wrong-host-placement/_config.js b/packages/svelte/tests/compiler-errors/samples/runes-wrong-host-placement/_config.js new file mode 100644 index 0000000000..f848528c6c --- /dev/null +++ b/packages/svelte/tests/compiler-errors/samples/runes-wrong-host-placement/_config.js @@ -0,0 +1,8 @@ +import { test } from '../../test'; + +export default test({ + error: { + code: 'invalid-host-location', + message: '$host() can only be used inside custom element component instances' + } +}); diff --git a/packages/svelte/tests/compiler-errors/samples/runes-wrong-host-placement/main.svelte b/packages/svelte/tests/compiler-errors/samples/runes-wrong-host-placement/main.svelte new file mode 100644 index 0000000000..d4ac5ced65 --- /dev/null +++ b/packages/svelte/tests/compiler-errors/samples/runes-wrong-host-placement/main.svelte @@ -0,0 +1,3 @@ + diff --git a/packages/svelte/tests/compiler-errors/samples/runes-wrong-host-placement/main.svelte.js b/packages/svelte/tests/compiler-errors/samples/runes-wrong-host-placement/main.svelte.js new file mode 100644 index 0000000000..b7e4b4aee3 --- /dev/null +++ b/packages/svelte/tests/compiler-errors/samples/runes-wrong-host-placement/main.svelte.js @@ -0,0 +1 @@ +$host(); diff --git a/packages/svelte/tests/css/samples/attribute-selector-case-sensitive/_config.js b/packages/svelte/tests/css/samples/attribute-selector-case-sensitive/_config.js new file mode 100644 index 0000000000..4be32cf365 --- /dev/null +++ b/packages/svelte/tests/css/samples/attribute-selector-case-sensitive/_config.js @@ -0,0 +1,20 @@ +import { test } from '../../test'; + +export default test({ + warnings: [ + { + code: 'css-unused-selector', + end: { + character: 44, + column: 14, + line: 4 + }, + message: 'Unused CSS selector "p[type=\'B\' s]"', + start: { + character: 31, + column: 1, + line: 4 + } + } + ] +}); diff --git a/packages/svelte/tests/css/samples/descendant-selector-unmatched/_config.js b/packages/svelte/tests/css/samples/descendant-selector-unmatched/_config.js new file mode 100644 index 0000000000..509e5faec9 --- /dev/null +++ b/packages/svelte/tests/css/samples/descendant-selector-unmatched/_config.js @@ -0,0 +1,20 @@ +import { test } from '../../test'; + +export default test({ + warnings: [ + { + code: 'css-unused-selector', + end: { + character: 33, + column: 6, + line: 6 + }, + message: 'Unused CSS selector "x y z"', + start: { + character: 28, + column: 1, + line: 6 + } + } + ] +}); diff --git a/packages/svelte/tests/css/samples/general-siblings-combinator-each-else/_config.js b/packages/svelte/tests/css/samples/general-siblings-combinator-each-else/_config.js index c733bcfbde..e0059c8eb5 100644 --- a/packages/svelte/tests/css/samples/general-siblings-combinator-each-else/_config.js +++ b/packages/svelte/tests/css/samples/general-siblings-combinator-each-else/_config.js @@ -5,8 +5,8 @@ export default test({ { code: 'css-unused-selector', message: 'Unused CSS selector ".b ~ .c"', - start: { character: 199, column: 1, line: 13 }, - end: { character: 206, column: 8, line: 13 } + start: { character: 198, column: 1, line: 13 }, + end: { character: 205, column: 8, line: 13 } } ] }); diff --git a/packages/svelte/tests/css/samples/general-siblings-combinator-rendertag-global/_config.js b/packages/svelte/tests/css/samples/general-siblings-combinator-rendertag-global/_config.js new file mode 100644 index 0000000000..2038788f67 --- /dev/null +++ b/packages/svelte/tests/css/samples/general-siblings-combinator-rendertag-global/_config.js @@ -0,0 +1,20 @@ +import { test } from '../../test'; + +export default test({ + warnings: [ + { + code: 'css-unused-selector', + end: { + character: 479, + column: 19, + line: 22 + }, + message: 'Unused CSS selector ":global(.x) + .bar"', + start: { + character: 461, + column: 1, + line: 22 + } + } + ] +}); diff --git a/packages/svelte/tests/css/samples/general-siblings-combinator-rendertag-global/expected.css b/packages/svelte/tests/css/samples/general-siblings-combinator-rendertag-global/expected.css new file mode 100644 index 0000000000..5495a803ef --- /dev/null +++ b/packages/svelte/tests/css/samples/general-siblings-combinator-rendertag-global/expected.css @@ -0,0 +1,13 @@ + + .before.svelte-xyz + .foo:where(.svelte-xyz) { color: green; } + .before.svelte-xyz ~ .foo:where(.svelte-xyz) { color: green; } + .before.svelte-xyz ~ .bar:where(.svelte-xyz) { color: green; } + + .x + .foo.svelte-xyz { color: green; } + .x + .foo.svelte-xyz span:where(.svelte-xyz) { color: green; } + .x ~ .foo.svelte-xyz { color: green; } + .x ~ .foo.svelte-xyz span:where(.svelte-xyz) { color: green; } + .x ~ .bar.svelte-xyz { color: green; } + + /* no match */ + /* (unused) :global(.x) + .bar { color: green; }*/ diff --git a/packages/svelte/tests/css/samples/general-siblings-combinator-rendertag-global/input.svelte b/packages/svelte/tests/css/samples/general-siblings-combinator-rendertag-global/input.svelte new file mode 100644 index 0000000000..1e2c6fdc2d --- /dev/null +++ b/packages/svelte/tests/css/samples/general-siblings-combinator-rendertag-global/input.svelte @@ -0,0 +1,23 @@ +
+

before

+ {@render children()} +

+ foo +

+

bar

+
+ + diff --git a/packages/svelte/tests/css/samples/general-siblings-combinator-slot-global/_config.js b/packages/svelte/tests/css/samples/general-siblings-combinator-slot-global/_config.js new file mode 100644 index 0000000000..b595370622 --- /dev/null +++ b/packages/svelte/tests/css/samples/general-siblings-combinator-slot-global/_config.js @@ -0,0 +1,20 @@ +import { test } from '../../test'; + +export default test({ + warnings: [ + { + code: 'css-unused-selector', + end: { + character: 472, + column: 19, + line: 22 + }, + message: 'Unused CSS selector ":global(.x) + .bar"', + start: { + character: 454, + column: 1, + line: 22 + } + } + ] +}); diff --git a/packages/svelte/tests/css/samples/general-siblings-combinator-slot-global/expected.css b/packages/svelte/tests/css/samples/general-siblings-combinator-slot-global/expected.css new file mode 100644 index 0000000000..5495a803ef --- /dev/null +++ b/packages/svelte/tests/css/samples/general-siblings-combinator-slot-global/expected.css @@ -0,0 +1,13 @@ + + .before.svelte-xyz + .foo:where(.svelte-xyz) { color: green; } + .before.svelte-xyz ~ .foo:where(.svelte-xyz) { color: green; } + .before.svelte-xyz ~ .bar:where(.svelte-xyz) { color: green; } + + .x + .foo.svelte-xyz { color: green; } + .x + .foo.svelte-xyz span:where(.svelte-xyz) { color: green; } + .x ~ .foo.svelte-xyz { color: green; } + .x ~ .foo.svelte-xyz span:where(.svelte-xyz) { color: green; } + .x ~ .bar.svelte-xyz { color: green; } + + /* no match */ + /* (unused) :global(.x) + .bar { color: green; }*/ diff --git a/packages/svelte/tests/css/samples/general-siblings-combinator-slot-global/input.svelte b/packages/svelte/tests/css/samples/general-siblings-combinator-slot-global/input.svelte new file mode 100644 index 0000000000..d556ea6b8b --- /dev/null +++ b/packages/svelte/tests/css/samples/general-siblings-combinator-slot-global/input.svelte @@ -0,0 +1,23 @@ +
+

before

+ +

+ foo +

+

bar

+
+ + diff --git a/packages/svelte/tests/css/samples/general-siblings-combinator-slot/_config.js b/packages/svelte/tests/css/samples/general-siblings-combinator-slot/_config.js index 8d9f014856..1de4e6d06d 100644 --- a/packages/svelte/tests/css/samples/general-siblings-combinator-slot/_config.js +++ b/packages/svelte/tests/css/samples/general-siblings-combinator-slot/_config.js @@ -5,38 +5,38 @@ export default test({ { code: 'css-unused-selector', message: 'Unused CSS selector ".a ~ .b"', - start: { character: 111, column: 1, line: 10 }, - end: { character: 118, column: 8, line: 10 } + start: { character: 110, column: 1, line: 10 }, + end: { character: 117, column: 8, line: 10 } }, { code: 'css-unused-selector', message: 'Unused CSS selector ".b ~ .c"', - start: { character: 138, column: 1, line: 11 }, - end: { character: 145, column: 8, line: 11 } + start: { character: 137, column: 1, line: 11 }, + end: { character: 144, column: 8, line: 11 } }, { code: 'css-unused-selector', message: 'Unused CSS selector ".c ~ .f"', - start: { character: 165, column: 1, line: 12 }, - end: { character: 172, column: 8, line: 12 } + start: { character: 164, column: 1, line: 12 }, + end: { character: 171, column: 8, line: 12 } }, { code: 'css-unused-selector', message: 'Unused CSS selector ".f ~ .g"', - start: { character: 192, column: 1, line: 13 }, - end: { character: 199, column: 8, line: 13 } + start: { character: 191, column: 1, line: 13 }, + end: { character: 198, column: 8, line: 13 } }, { code: 'css-unused-selector', message: 'Unused CSS selector ".b ~ .f"', - start: { character: 219, column: 1, line: 14 }, - end: { character: 226, column: 8, line: 14 } + start: { character: 218, column: 1, line: 14 }, + end: { character: 225, column: 8, line: 14 } }, { code: 'css-unused-selector', message: 'Unused CSS selector ".b ~ .g"', - start: { character: 246, column: 1, line: 15 }, - end: { character: 253, column: 8, line: 15 } + start: { character: 245, column: 1, line: 15 }, + end: { character: 252, column: 8, line: 15 } } ] }); diff --git a/packages/svelte/tests/css/samples/general-siblings-combinator-svelteelement/_config.js b/packages/svelte/tests/css/samples/general-siblings-combinator-svelteelement/_config.js new file mode 100644 index 0000000000..d9b40d6462 --- /dev/null +++ b/packages/svelte/tests/css/samples/general-siblings-combinator-svelteelement/_config.js @@ -0,0 +1,20 @@ +import { test } from '../../test'; + +export default test({ + warnings: [ + { + code: 'css-unused-selector', + end: { + character: 496, + column: 10, + line: 26 + }, + message: 'Unused CSS selector ".x + .bar"', + start: { + character: 487, + column: 1, + line: 26 + } + } + ] +}); diff --git a/packages/svelte/tests/css/samples/general-siblings-combinator-svelteelement/expected.css b/packages/svelte/tests/css/samples/general-siblings-combinator-svelteelement/expected.css new file mode 100644 index 0000000000..830d366702 --- /dev/null +++ b/packages/svelte/tests/css/samples/general-siblings-combinator-svelteelement/expected.css @@ -0,0 +1,13 @@ + + .before.svelte-xyz + .foo:where(.svelte-xyz) { color: green; } + .before.svelte-xyz ~ .foo:where(.svelte-xyz) { color: green; } + .before.svelte-xyz ~ .bar:where(.svelte-xyz) { color: green; } + + .x.svelte-xyz + .foo:where(.svelte-xyz) { color: green; } + .x.svelte-xyz + .foo:where(.svelte-xyz) span:where(.svelte-xyz) { color: green; } + .x.svelte-xyz ~ .foo:where(.svelte-xyz) { color: green; } + .x.svelte-xyz ~ .foo:where(.svelte-xyz) span:where(.svelte-xyz) { color: green; } + .x.svelte-xyz ~ .bar:where(.svelte-xyz) { color: green; } + + /* no match */ + /* (unused) .x + .bar { color: green; }*/ diff --git a/packages/svelte/tests/css/samples/general-siblings-combinator-svelteelement/input.svelte b/packages/svelte/tests/css/samples/general-siblings-combinator-svelteelement/input.svelte new file mode 100644 index 0000000000..1c51a2c516 --- /dev/null +++ b/packages/svelte/tests/css/samples/general-siblings-combinator-svelteelement/input.svelte @@ -0,0 +1,27 @@ + + +
+

before

+ +

+ foo +

+

bar

+
+ + diff --git a/packages/svelte/tests/css/samples/host/_config.js b/packages/svelte/tests/css/samples/host/_config.js index 26f4fd6584..e5c8c0fe0e 100644 --- a/packages/svelte/tests/css/samples/host/_config.js +++ b/packages/svelte/tests/css/samples/host/_config.js @@ -6,12 +6,12 @@ export default test({ code: 'css-unused-selector', message: 'Unused CSS selector ":host > span"', start: { - character: 147, + character: 145, column: 1, line: 18 }, end: { - character: 159, + character: 157, column: 13, line: 18 } diff --git a/packages/svelte/tests/css/samples/is/_config.js b/packages/svelte/tests/css/samples/is/_config.js new file mode 100644 index 0000000000..4a3961aaef --- /dev/null +++ b/packages/svelte/tests/css/samples/is/_config.js @@ -0,0 +1,20 @@ +import { test } from '../../test'; + +export default test({ + warnings: [ + { + code: 'css-unused-selector', + end: { + character: 38, + column: 11, + line: 6 + }, + message: 'Unused CSS selector "z"', + start: { + character: 37, + column: 10, + line: 6 + } + } + ] +}); diff --git a/packages/svelte/tests/css/samples/nested-css/_config.js b/packages/svelte/tests/css/samples/nested-css/_config.js new file mode 100644 index 0000000000..e51fbe576c --- /dev/null +++ b/packages/svelte/tests/css/samples/nested-css/_config.js @@ -0,0 +1,104 @@ +import { test } from '../../test'; + +export default test({ + warnings: [ + { + code: 'css-unused-selector', + end: { + character: 239, + column: 13, + line: 20 + }, + message: 'Unused CSS selector ".unused"', + start: { + character: 232, + column: 6, + line: 20 + } + }, + { + code: 'css-unused-selector', + end: { + character: 302, + column: 10, + line: 27 + }, + message: 'Unused CSS selector ".unused"', + start: { + character: 295, + column: 3, + line: 27 + } + }, + { + code: 'css-unused-selector', + end: { + character: 328, + column: 6, + line: 30 + }, + message: 'Unused CSS selector ".c"', + start: { + character: 326, + column: 4, + line: 30 + } + }, + { + code: 'css-unused-selector', + end: { + character: 381, + column: 10, + line: 37 + }, + message: 'Unused CSS selector ".unused"', + start: { + character: 374, + column: 3, + line: 37 + } + }, + { + code: 'css-unused-selector', + end: { + character: 471, + column: 7, + line: 47 + }, + message: 'Unused CSS selector "& &"', + start: { + character: 468, + column: 4, + line: 47 + } + }, + { + code: 'css-unused-selector', + end: { + character: 634, + column: 5, + line: 66 + }, + message: 'Unused CSS selector "&.b"', + start: { + character: 631, + column: 2, + line: 66 + } + }, + { + code: 'css-unused-selector', + end: { + character: 666, + column: 9, + line: 70 + }, + message: 'Unused CSS selector ".unused"', + start: { + character: 659, + column: 2, + line: 70 + } + } + ] +}); diff --git a/packages/svelte/tests/css/samples/siblings-combinator-each-else-nested/_config.js b/packages/svelte/tests/css/samples/siblings-combinator-each-else-nested/_config.js index 47a1c2e253..b7f24a8881 100644 --- a/packages/svelte/tests/css/samples/siblings-combinator-each-else-nested/_config.js +++ b/packages/svelte/tests/css/samples/siblings-combinator-each-else-nested/_config.js @@ -5,62 +5,62 @@ export default test({ { code: 'css-unused-selector', message: 'Unused CSS selector ".a + .c"', - start: { character: 479, column: 1, line: 23 }, - end: { character: 486, column: 8, line: 23 } + start: { character: 478, column: 1, line: 23 }, + end: { character: 485, column: 8, line: 23 } }, { code: 'css-unused-selector', message: 'Unused CSS selector ".a + .g"', - start: { character: 506, column: 1, line: 24 }, - end: { character: 513, column: 8, line: 24 } + start: { character: 505, column: 1, line: 24 }, + end: { character: 512, column: 8, line: 24 } }, { code: 'css-unused-selector', message: 'Unused CSS selector ".b + .e"', - start: { character: 533, column: 1, line: 25 }, - end: { character: 540, column: 8, line: 25 } + start: { character: 532, column: 1, line: 25 }, + end: { character: 539, column: 8, line: 25 } }, { code: 'css-unused-selector', message: 'Unused CSS selector ".c + .g"', - start: { character: 560, column: 1, line: 26 }, - end: { character: 567, column: 8, line: 26 } + start: { character: 559, column: 1, line: 26 }, + end: { character: 566, column: 8, line: 26 } }, { code: 'css-unused-selector', message: 'Unused CSS selector ".c + .k"', - start: { character: 587, column: 1, line: 27 }, - end: { character: 594, column: 8, line: 27 } + start: { character: 586, column: 1, line: 27 }, + end: { character: 593, column: 8, line: 27 } }, { code: 'css-unused-selector', message: 'Unused CSS selector ".d + .d"', - start: { character: 614, column: 1, line: 28 }, - end: { character: 621, column: 8, line: 28 } + start: { character: 613, column: 1, line: 28 }, + end: { character: 620, column: 8, line: 28 } }, { code: 'css-unused-selector', message: 'Unused CSS selector ".e + .f"', - start: { character: 641, column: 1, line: 29 }, - end: { character: 648, column: 8, line: 29 } + start: { character: 640, column: 1, line: 29 }, + end: { character: 647, column: 8, line: 29 } }, { code: 'css-unused-selector', message: 'Unused CSS selector ".f + .f"', - start: { character: 668, column: 1, line: 30 }, - end: { character: 675, column: 8, line: 30 } + start: { character: 667, column: 1, line: 30 }, + end: { character: 674, column: 8, line: 30 } }, { code: 'css-unused-selector', message: 'Unused CSS selector ".g + .j"', - start: { character: 695, column: 1, line: 31 }, - end: { character: 702, column: 8, line: 31 } + start: { character: 694, column: 1, line: 31 }, + end: { character: 701, column: 8, line: 31 } }, { code: 'css-unused-selector', message: 'Unused CSS selector ".g + .h + .i + .j"', - start: { character: 722, column: 1, line: 32 }, - end: { character: 739, column: 18, line: 32 } + start: { character: 721, column: 1, line: 32 }, + end: { character: 738, column: 18, line: 32 } } ] }); diff --git a/packages/svelte/tests/css/samples/siblings-combinator-each-else/_config.js b/packages/svelte/tests/css/samples/siblings-combinator-each-else/_config.js index e0b5ab1557..0014358f74 100644 --- a/packages/svelte/tests/css/samples/siblings-combinator-each-else/_config.js +++ b/packages/svelte/tests/css/samples/siblings-combinator-each-else/_config.js @@ -5,14 +5,14 @@ export default test({ { code: 'css-unused-selector', message: 'Unused CSS selector ".a + .d"', - start: { character: 172, column: 1, line: 12 }, - end: { character: 179, column: 8, line: 12 } + start: { character: 171, column: 1, line: 12 }, + end: { character: 178, column: 8, line: 12 } }, { code: 'css-unused-selector', message: 'Unused CSS selector ".b + .c"', - start: { character: 199, column: 1, line: 13 }, - end: { character: 206, column: 8, line: 13 } + start: { character: 198, column: 1, line: 13 }, + end: { character: 205, column: 8, line: 13 } } ] }); diff --git a/packages/svelte/tests/css/samples/siblings-combinator-slot/_config.js b/packages/svelte/tests/css/samples/siblings-combinator-slot/_config.js index 4d4d486934..b1c2369ab7 100644 --- a/packages/svelte/tests/css/samples/siblings-combinator-slot/_config.js +++ b/packages/svelte/tests/css/samples/siblings-combinator-slot/_config.js @@ -5,20 +5,20 @@ export default test({ { code: 'css-unused-selector', message: 'Unused CSS selector ".a + .b"', - start: { character: 84, column: 1, line: 9 }, - end: { character: 91, column: 8, line: 9 } + start: { character: 83, column: 1, line: 9 }, + end: { character: 90, column: 8, line: 9 } }, { code: 'css-unused-selector', message: 'Unused CSS selector ".b + .c"', - start: { character: 111, column: 1, line: 10 }, - end: { character: 118, column: 8, line: 10 } + start: { character: 110, column: 1, line: 10 }, + end: { character: 117, column: 8, line: 10 } }, { code: 'css-unused-selector', message: 'Unused CSS selector ".c + .f"', - start: { character: 138, column: 1, line: 11 }, - end: { character: 145, column: 8, line: 11 } + start: { character: 137, column: 1, line: 11 }, + end: { character: 144, column: 8, line: 11 } } ] }); diff --git a/packages/svelte/tests/css/samples/unused-selector-child-combinator/_config.js b/packages/svelte/tests/css/samples/unused-selector-child-combinator/_config.js index 5b50fe9c92..cc9927ae0f 100644 --- a/packages/svelte/tests/css/samples/unused-selector-child-combinator/_config.js +++ b/packages/svelte/tests/css/samples/unused-selector-child-combinator/_config.js @@ -5,20 +5,20 @@ export default test({ { code: 'css-unused-selector', message: 'Unused CSS selector "article > *"', - start: { character: 10, column: 1, line: 2 }, - end: { character: 21, column: 12, line: 2 } + start: { character: 9, column: 1, line: 2 }, + end: { character: 20, column: 12, line: 2 } }, { code: 'css-unused-selector', message: 'Unused CSS selector "article *"', - start: { character: 49, column: 1, line: 6 }, - end: { character: 58, column: 10, line: 6 } + start: { character: 47, column: 1, line: 6 }, + end: { character: 56, column: 10, line: 6 } }, { code: 'css-unused-selector', message: 'Unused CSS selector ".article > *"', - start: { character: 86, column: 1, line: 10 }, - end: { character: 98, column: 13, line: 10 } + start: { character: 83, column: 1, line: 10 }, + end: { character: 95, column: 13, line: 10 } } ] }); diff --git a/packages/svelte/tests/css/samples/unused-selector-child-combinator/expected.css b/packages/svelte/tests/css/samples/unused-selector-child-combinator/expected.css index ad93ca631a..fbe005adc9 100644 --- a/packages/svelte/tests/css/samples/unused-selector-child-combinator/expected.css +++ b/packages/svelte/tests/css/samples/unused-selector-child-combinator/expected.css @@ -1,6 +1,6 @@ /* (unused) article > * { font-size: 36px; - }*/ + }*/ /* (unused) article * { font-size: 36px; diff --git a/packages/svelte/tests/css/samples/unused-selector-child-combinator/input.svelte b/packages/svelte/tests/css/samples/unused-selector-child-combinator/input.svelte index 51463bbc9b..b0dc86133c 100644 --- a/packages/svelte/tests/css/samples/unused-selector-child-combinator/input.svelte +++ b/packages/svelte/tests/css/samples/unused-selector-child-combinator/input.svelte @@ -1,7 +1,7 @@