From a051f96ed607751f502dadcd9cbaff1bf880b15f Mon Sep 17 00:00:00 2001 From: Simon H <5968653+dummdidumm@users.noreply.github.com> Date: Mon, 14 Apr 2025 20:38:02 +0200 Subject: [PATCH 1/3] fix: relax `:global` selector list validation (#15762) We have to allow `:global x, :global y` selector lists because CSS preprocessors might generate that from `:global { x, y {...} }` --- .changeset/green-starfishes-shave.md | 5 ++ .../98-reference/.generated/compile-errors.md | 26 +++++- .../svelte/messages/compile-errors/style.md | 26 +++++- packages/svelte/src/compiler/errors.js | 4 +- .../phases/2-analyze/css/css-analyze.js | 90 +++++++++---------- .../compiler/phases/3-transform/css/index.js | 37 +++++--- .../css-global-block-multiple-1/_config.js | 10 +++ .../css-global-block-multiple-1/main.svelte | 9 ++ .../css-global-block-multiple-2/_config.js | 10 +++ .../css-global-block-multiple-2/main.svelte | 6 ++ .../css-global-block-multiple/_config.js | 9 -- .../css-global-block-multiple/main.svelte | 3 - .../tests/css/samples/global-block/_config.js | 14 +++ .../css/samples/global-block/expected.css | 10 +++ .../css/samples/global-block/input.svelte | 10 +++ 15 files changed, 198 insertions(+), 71 deletions(-) create mode 100644 .changeset/green-starfishes-shave.md create mode 100644 packages/svelte/tests/compiler-errors/samples/css-global-block-multiple-1/_config.js create mode 100644 packages/svelte/tests/compiler-errors/samples/css-global-block-multiple-1/main.svelte create mode 100644 packages/svelte/tests/compiler-errors/samples/css-global-block-multiple-2/_config.js create mode 100644 packages/svelte/tests/compiler-errors/samples/css-global-block-multiple-2/main.svelte delete mode 100644 packages/svelte/tests/compiler-errors/samples/css-global-block-multiple/_config.js delete mode 100644 packages/svelte/tests/compiler-errors/samples/css-global-block-multiple/main.svelte diff --git a/.changeset/green-starfishes-shave.md b/.changeset/green-starfishes-shave.md new file mode 100644 index 000000000000..967bba753c74 --- /dev/null +++ b/.changeset/green-starfishes-shave.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: relax `:global` selector list validation diff --git a/documentation/docs/98-reference/.generated/compile-errors.md b/documentation/docs/98-reference/.generated/compile-errors.md index a8c39aaf9713..6196a85ade62 100644 --- a/documentation/docs/98-reference/.generated/compile-errors.md +++ b/documentation/docs/98-reference/.generated/compile-errors.md @@ -235,7 +235,31 @@ A top-level `:global {...}` block can only contain rules, not declarations ### css_global_block_invalid_list ``` -A `:global` selector cannot be part of a selector list with more than one item +A `:global` selector cannot be part of a selector list with entries that don't contain `:global` +``` + +The following CSS is invalid: + +```css +:global, x { + y { + color: red; + } +} +``` + +This is mixing a `:global` block, which means "everything in here is unscoped", with a scoped selector (`x` in this case). As a result it's not possible to transform the inner selector (`y` in this case) into something that satisfies both requirements. You therefore have to split this up into two selectors: + +```css +:global { + y { + color: red; + } +} + +x y { + color: red; +} ``` ### css_global_block_invalid_modifier diff --git a/packages/svelte/messages/compile-errors/style.md b/packages/svelte/messages/compile-errors/style.md index 1e1ab45e8cfc..f08a2156a35a 100644 --- a/packages/svelte/messages/compile-errors/style.md +++ b/packages/svelte/messages/compile-errors/style.md @@ -16,7 +16,31 @@ ## css_global_block_invalid_list -> A `:global` selector cannot be part of a selector list with more than one item +> A `:global` selector cannot be part of a selector list with entries that don't contain `:global` + +The following CSS is invalid: + +```css +:global, x { + y { + color: red; + } +} +``` + +This is mixing a `:global` block, which means "everything in here is unscoped", with a scoped selector (`x` in this case). As a result it's not possible to transform the inner selector (`y` in this case) into something that satisfies both requirements. You therefore have to split this up into two selectors: + +```css +:global { + y { + color: red; + } +} + +x y { + color: red; +} +``` ## css_global_block_invalid_modifier diff --git a/packages/svelte/src/compiler/errors.js b/packages/svelte/src/compiler/errors.js index 6bf973948b92..aa328764e14a 100644 --- a/packages/svelte/src/compiler/errors.js +++ b/packages/svelte/src/compiler/errors.js @@ -555,12 +555,12 @@ export function css_global_block_invalid_declaration(node) { } /** - * A `:global` selector cannot be part of a selector list with more than one item + * A `:global` selector cannot be part of a selector list with entries that don't contain `:global` * @param {null | number | NodeLike} node * @returns {never} */ export function css_global_block_invalid_list(node) { - e(node, 'css_global_block_invalid_list', `A \`:global\` selector cannot be part of a selector list with more than one item\nhttps://svelte.dev/e/css_global_block_invalid_list`); + e(node, 'css_global_block_invalid_list', `A \`:global\` selector cannot be part of a selector list with entries that don't contain \`:global\`\nhttps://svelte.dev/e/css_global_block_invalid_list`); } /** 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 76cb2f56e995..2dc343564831 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 @@ -193,10 +193,12 @@ const css_visitors = { Rule(node, context) { node.metadata.parent_rule = context.state.rule; - node.metadata.is_global_block = node.prelude.children.some((selector) => { + // We gotta allow :global x, :global y because CSS preprocessors might generate that from :global { x, y {...} } + for (const complex_selector of node.prelude.children) { let is_global_block = false; - for (const child of selector.children) { + for (let selector_idx = 0; selector_idx < complex_selector.children.length; selector_idx++) { + const child = complex_selector.children[selector_idx]; const idx = child.selectors.findIndex(is_global_block_selector); if (is_global_block) { @@ -204,58 +206,56 @@ const css_visitors = { child.metadata.is_global_like = true; } - if (idx !== -1) { - is_global_block = true; + if (idx === 0) { + if ( + child.selectors.length > 1 && + selector_idx === 0 && + node.metadata.parent_rule === null + ) { + e.css_global_block_invalid_modifier_start(child.selectors[1]); + } else { + // `child` starts with `:global` + node.metadata.is_global_block = is_global_block = true; + + for (let i = 1; i < child.selectors.length; i++) { + walk(/** @type {AST.CSS.Node} */ (child.selectors[i]), null, { + ComplexSelector(node) { + node.metadata.used = true; + } + }); + } - for (let i = idx + 1; i < child.selectors.length; i++) { - walk(/** @type {AST.CSS.Node} */ (child.selectors[i]), null, { - ComplexSelector(node) { - node.metadata.used = true; - } - }); - } - } - } + if (child.combinator && child.combinator.name !== ' ') { + e.css_global_block_invalid_combinator(child, child.combinator.name); + } - return is_global_block; - }); + const declaration = node.block.children.find((child) => child.type === 'Declaration'); + const is_lone_global = + complex_selector.children.length === 1 && + complex_selector.children[0].selectors.length === 1; // just `:global`, not e.g. `:global x` - if (node.metadata.is_global_block) { - if (node.prelude.children.length > 1) { - e.css_global_block_invalid_list(node.prelude); - } + if (is_lone_global && node.prelude.children.length > 1) { + // `:global, :global x { z { ... } }` would become `x { z { ... } }` which means `z` is always + // constrained by `x`, which is not what the user intended + e.css_global_block_invalid_list(node.prelude); + } - const complex_selector = node.prelude.children[0]; - const global_selector = complex_selector.children.find((r, selector_idx) => { - const idx = r.selectors.findIndex(is_global_block_selector); - if (idx === 0) { - if (r.selectors.length > 1 && selector_idx === 0 && node.metadata.parent_rule === null) { - e.css_global_block_invalid_modifier_start(r.selectors[1]); + if ( + declaration && + // :global { color: red; } is invalid, but foo :global { color: red; } is valid + node.prelude.children.length === 1 && + is_lone_global + ) { + e.css_global_block_invalid_declaration(declaration); + } } - return true; } else if (idx !== -1) { - e.css_global_block_invalid_modifier(r.selectors[idx]); + e.css_global_block_invalid_modifier(child.selectors[idx]); } - }); - - if (!global_selector) { - throw new Error('Internal error: global block without :global selector'); - } - - if (global_selector.combinator && global_selector.combinator.name !== ' ') { - e.css_global_block_invalid_combinator(global_selector, global_selector.combinator.name); } - const declaration = node.block.children.find((child) => child.type === 'Declaration'); - - if ( - declaration && - // :global { color: red; } is invalid, but foo :global { color: red; } is valid - node.prelude.children.length === 1 && - node.prelude.children[0].children.length === 1 && - node.prelude.children[0].children[0].selectors.length === 1 - ) { - e.css_global_block_invalid_declaration(declaration); + if (node.metadata.is_global_block && !is_global_block) { + e.css_global_block_invalid_list(node.prelude); } } diff --git a/packages/svelte/src/compiler/phases/3-transform/css/index.js b/packages/svelte/src/compiler/phases/3-transform/css/index.js index dff034f8aad6..9f1142cce9a7 100644 --- a/packages/svelte/src/compiler/phases/3-transform/css/index.js +++ b/packages/svelte/src/compiler/phases/3-transform/css/index.js @@ -170,7 +170,11 @@ const visitors = { if (node.metadata.is_global_block) { const selector = node.prelude.children[0]; - if (selector.children.length === 1 && selector.children[0].selectors.length === 1) { + if ( + node.prelude.children.length === 1 && + selector.children.length === 1 && + selector.children[0].selectors.length === 1 + ) { // `:global {...}` if (state.minify) { state.code.remove(node.start, node.block.start + 1); @@ -194,7 +198,7 @@ const visitors = { SelectorList(node, { state, next, path }) { // Only add comments if we're not inside a complex selector that itself is unused or a global block if ( - !is_in_global_block(path) && + (!is_in_global_block(path) || node.children.length > 1) && !path.find((n) => n.type === 'ComplexSelector' && !n.metadata.used) ) { const children = node.children; @@ -282,13 +286,24 @@ const visitors = { const global = /** @type {AST.CSS.PseudoClassSelector} */ (relative_selector.selectors[0]); remove_global_pseudo_class(global, relative_selector.combinator, context.state); - if ( - node.metadata.rule?.metadata.parent_rule && - global.args === null && - relative_selector.combinator === null - ) { - // div { :global.x { ... } } becomes div { &.x { ... } } - context.state.code.prependRight(global.start, '&'); + const parent_rule = node.metadata.rule?.metadata.parent_rule; + if (parent_rule && global.args === null) { + if (relative_selector.combinator === null) { + // div { :global.x { ... } } becomes div { &.x { ... } } + context.state.code.prependRight(global.start, '&'); + } + + // In case of multiple :global selectors in a selector list we gotta delete the comma, too, but only if + // the next selector is used; if it's unused then the comma deletion happens as part of removal of that next selector + if ( + parent_rule.prelude.children.length > 1 && + node.children.length === node.children.findIndex((s) => s === relative_selector) - 1 + ) { + const next_selector = parent_rule.prelude.children.find((s) => s.start > global.end); + if (next_selector && next_selector.metadata.used) { + context.state.code.update(global.end, next_selector.start, ''); + } + } } continue; } else { @@ -380,7 +395,9 @@ function remove_global_pseudo_class(selector, combinator, state) { // div :global.x becomes div.x while (/\s/.test(state.code.original[start - 1])) start--; } - state.code.remove(start, selector.start + ':global'.length); + + // update(...), not remove(...) because there could be a closing unused comment at the end + state.code.update(start, selector.start + ':global'.length, ''); } else { state.code .remove(selector.start, selector.start + ':global('.length) diff --git a/packages/svelte/tests/compiler-errors/samples/css-global-block-multiple-1/_config.js b/packages/svelte/tests/compiler-errors/samples/css-global-block-multiple-1/_config.js new file mode 100644 index 000000000000..85dedc801221 --- /dev/null +++ b/packages/svelte/tests/compiler-errors/samples/css-global-block-multiple-1/_config.js @@ -0,0 +1,10 @@ +import { test } from '../../test'; + +export default test({ + error: { + code: 'css_global_block_invalid_list', + message: + "A `:global` selector cannot be part of a selector list with entries that don't contain `:global`", + position: [232, 246] + } +}); diff --git a/packages/svelte/tests/compiler-errors/samples/css-global-block-multiple-1/main.svelte b/packages/svelte/tests/compiler-errors/samples/css-global-block-multiple-1/main.svelte new file mode 100644 index 000000000000..260921f7048d --- /dev/null +++ b/packages/svelte/tests/compiler-errors/samples/css-global-block-multiple-1/main.svelte @@ -0,0 +1,9 @@ + diff --git a/packages/svelte/tests/compiler-errors/samples/css-global-block-multiple-2/_config.js b/packages/svelte/tests/compiler-errors/samples/css-global-block-multiple-2/_config.js new file mode 100644 index 000000000000..f24095800a6d --- /dev/null +++ b/packages/svelte/tests/compiler-errors/samples/css-global-block-multiple-2/_config.js @@ -0,0 +1,10 @@ +import { test } from '../../test'; + +export default test({ + error: { + code: 'css_global_block_invalid_list', + message: + "A `:global` selector cannot be part of a selector list with entries that don't contain `:global`", + position: [24, 43] + } +}); diff --git a/packages/svelte/tests/compiler-errors/samples/css-global-block-multiple-2/main.svelte b/packages/svelte/tests/compiler-errors/samples/css-global-block-multiple-2/main.svelte new file mode 100644 index 000000000000..2a09ec10cea5 --- /dev/null +++ b/packages/svelte/tests/compiler-errors/samples/css-global-block-multiple-2/main.svelte @@ -0,0 +1,6 @@ + diff --git a/packages/svelte/tests/compiler-errors/samples/css-global-block-multiple/_config.js b/packages/svelte/tests/compiler-errors/samples/css-global-block-multiple/_config.js deleted file mode 100644 index 9ae4e758c46f..000000000000 --- a/packages/svelte/tests/compiler-errors/samples/css-global-block-multiple/_config.js +++ /dev/null @@ -1,9 +0,0 @@ -import { test } from '../../test'; - -export default test({ - error: { - code: 'css_global_block_invalid_list', - message: 'A `:global` selector cannot be part of a selector list with more than one item', - position: [9, 31] - } -}); diff --git a/packages/svelte/tests/compiler-errors/samples/css-global-block-multiple/main.svelte b/packages/svelte/tests/compiler-errors/samples/css-global-block-multiple/main.svelte deleted file mode 100644 index 75178bc664eb..000000000000 --- a/packages/svelte/tests/compiler-errors/samples/css-global-block-multiple/main.svelte +++ /dev/null @@ -1,3 +0,0 @@ - diff --git a/packages/svelte/tests/css/samples/global-block/_config.js b/packages/svelte/tests/css/samples/global-block/_config.js index bee0d7204d00..a8b11a73ec14 100644 --- a/packages/svelte/tests/css/samples/global-block/_config.js +++ b/packages/svelte/tests/css/samples/global-block/_config.js @@ -16,6 +16,20 @@ export default test({ column: 16, character: 932 } + }, + { + code: 'css_unused_selector', + message: 'Unused CSS selector "unused :global"', + start: { + line: 100, + column: 29, + character: 1223 + }, + end: { + line: 100, + column: 43, + character: 1237 + } } ] }); diff --git a/packages/svelte/tests/css/samples/global-block/expected.css b/packages/svelte/tests/css/samples/global-block/expected.css index 438749224ba6..12f9a75032ff 100644 --- a/packages/svelte/tests/css/samples/global-block/expected.css +++ b/packages/svelte/tests/css/samples/global-block/expected.css @@ -90,3 +90,13 @@ opacity: 1; } } + + x, y { + color: green; + } + + div.svelte-xyz, div.svelte-xyz y /* (unused) unused*/ { + z { + color: green; + } + } diff --git a/packages/svelte/tests/css/samples/global-block/input.svelte b/packages/svelte/tests/css/samples/global-block/input.svelte index a1833636a13f..ee05205d67c3 100644 --- a/packages/svelte/tests/css/samples/global-block/input.svelte +++ b/packages/svelte/tests/css/samples/global-block/input.svelte @@ -92,4 +92,14 @@ opacity: 1; } } + + :global x, :global y { + color: green; + } + + div :global, div :global y, unused :global { + z { + color: green; + } + } From 90563e903fd8428db4c087b6fa4f51e0200dbb45 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Mon, 14 Apr 2025 14:38:31 -0400 Subject: [PATCH 2/3] feat: add partial evaluation (#15494) * feat: add partial evaluation * fix * tweak * more * more * evaluate stuff in template * update test * SSR * unused * changeset * remove TODO * Apply suggestions from code review Co-authored-by: Simon H <5968653+dummdidumm@users.noreply.github.com> * allow unknown operators * use blocks and block-scoping in switch statement --------- Co-authored-by: Simon H <5968653+dummdidumm@users.noreply.github.com> --- .changeset/selfish-onions-begin.md | 5 + .../client/visitors/RegularElement.js | 9 +- .../client/visitors/shared/utils.js | 26 +- .../server/visitors/shared/utils.js | 16 +- packages/svelte/src/compiler/phases/scope.js | 322 +++++++++++++++++- .../_expected/client/index.svelte.js | 6 +- .../_expected/server/index.svelte.js | 2 +- .../_expected/client/index.svelte.js | 2 +- .../samples/attached-sourcemap/input.svelte | 2 +- 9 files changed, 357 insertions(+), 33 deletions(-) create mode 100644 .changeset/selfish-onions-begin.md diff --git a/.changeset/selfish-onions-begin.md b/.changeset/selfish-onions-begin.md new file mode 100644 index 000000000000..decf0d5fc6d4 --- /dev/null +++ b/.changeset/selfish-onions-begin.md @@ -0,0 +1,5 @@ +--- +'svelte': minor +--- + +feat: partially evaluate certain expressions diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/RegularElement.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/RegularElement.js index 45a594af1f06..fa4ee9867f8a 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/RegularElement.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/RegularElement.js @@ -685,14 +685,13 @@ function build_element_special_value_attribute(element, node_id, attribute, cont : value ); + const evaluated = context.state.scope.evaluate(value); + const assignment = b.assignment('=', b.member(node_id, '__value'), value); + const inner_assignment = b.assignment( '=', b.member(node_id, 'value'), - b.conditional( - b.binary('==', b.null, b.assignment('=', b.member(node_id, '__value'), value)), - b.literal(''), // render null/undefined values as empty string to support placeholder options - value - ) + evaluated.is_defined ? assignment : b.logical('??', assignment, b.literal('')) ); const update = b.stmt( diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/utils.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/utils.js index af6e56f70c81..55362d75afd1 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/utils.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/utils.js @@ -89,21 +89,21 @@ export function build_template_chunk( } } - const is_defined = - value.type === 'BinaryExpression' || - (value.type === 'UnaryExpression' && value.operator !== 'void') || - (value.type === 'LogicalExpression' && value.right.type === 'Literal') || - (value.type === 'Identifier' && value.name === state.analysis.props_id?.name); - - if (!is_defined) { - // add `?? ''` where necessary (TODO optimise more cases) - value = b.logical('??', value, b.literal('')); - } + const evaluated = state.scope.evaluate(value); - expressions.push(value); + if (evaluated.is_known) { + quasi.value.cooked += evaluated.value + ''; + } else { + if (!evaluated.is_defined) { + // add `?? ''` where necessary + value = b.logical('??', value, b.literal('')); + } - quasi = b.quasi('', i + 1 === values.length); - quasis.push(quasi); + expressions.push(value); + + quasi = b.quasi('', i + 1 === values.length); + quasis.push(quasi); + } } } diff --git a/packages/svelte/src/compiler/phases/3-transform/server/visitors/shared/utils.js b/packages/svelte/src/compiler/phases/3-transform/server/visitors/shared/utils.js index 2c6aa2f316aa..807e12a8fa92 100644 --- a/packages/svelte/src/compiler/phases/3-transform/server/visitors/shared/utils.js +++ b/packages/svelte/src/compiler/phases/3-transform/server/visitors/shared/utils.js @@ -44,15 +44,17 @@ export function process_children(nodes, { visit, state }) { if (node.type === 'Text' || node.type === 'Comment') { quasi.value.cooked += node.type === 'Comment' ? `` : escape_html(node.data); - } else if (node.type === 'ExpressionTag' && node.expression.type === 'Literal') { - if (node.expression.value != null) { - quasi.value.cooked += escape_html(node.expression.value + ''); - } } else { - expressions.push(b.call('$.escape', /** @type {Expression} */ (visit(node.expression)))); + const evaluated = state.scope.evaluate(node.expression); + + if (evaluated.is_known) { + quasi.value.cooked += escape_html((evaluated.value ?? '') + ''); + } else { + expressions.push(b.call('$.escape', /** @type {Expression} */ (visit(node.expression)))); - quasi = b.quasi('', i + 1 === sequence.length); - quasis.push(quasi); + quasi = b.quasi('', i + 1 === sequence.length); + quasis.push(quasi); + } } } diff --git a/packages/svelte/src/compiler/phases/scope.js b/packages/svelte/src/compiler/phases/scope.js index b6063c32343f..73dfeea1d9b0 100644 --- a/packages/svelte/src/compiler/phases/scope.js +++ b/packages/svelte/src/compiler/phases/scope.js @@ -1,4 +1,4 @@ -/** @import { ArrowFunctionExpression, ClassDeclaration, Expression, FunctionDeclaration, FunctionExpression, Identifier, ImportDeclaration, MemberExpression, Node, Pattern, VariableDeclarator } from 'estree' */ +/** @import { ArrowFunctionExpression, BinaryOperator, ClassDeclaration, Expression, FunctionDeclaration, FunctionExpression, Identifier, ImportDeclaration, MemberExpression, LogicalOperator, Node, Pattern, UnaryOperator, VariableDeclarator } from 'estree' */ /** @import { Context, Visitor } from 'zimmerframe' */ /** @import { AST, BindingKind, DeclarationKind } from '#compiler' */ import is_reference from 'is-reference'; @@ -16,6 +16,11 @@ import { is_reserved, is_rune } from '../../utils.js'; import { determine_slot } from '../utils/slot.js'; import { validate_identifier_name } from './2-analyze/visitors/shared/utils.js'; +export const UNKNOWN = Symbol('unknown'); +/** Includes `BigInt` */ +export const NUMBER = Symbol('number'); +export const STRING = Symbol('string'); + export class Binding { /** @type {Scope} */ scope; @@ -34,7 +39,7 @@ export class Binding { * For destructured props such as `let { foo = 'bar' } = $props()` this is `'bar'` and not `$props()` * @type {null | Expression | FunctionDeclaration | ClassDeclaration | ImportDeclaration | AST.EachBlock | AST.SnippetBlock} */ - initial; + initial = null; /** @type {Array<{ node: Identifier; path: AST.SvelteNode[] }>} */ references = []; @@ -100,6 +105,264 @@ export class Binding { } } +class Evaluation { + /** @type {Set} */ + values = new Set(); + + /** + * True if there is exactly one possible value + * @readonly + * @type {boolean} + */ + is_known = true; + + /** + * True if the value is known to not be null/undefined + * @readonly + * @type {boolean} + */ + is_defined = true; + + /** + * True if the value is known to be a string + * @readonly + * @type {boolean} + */ + is_string = true; + + /** + * True if the value is known to be a number + * @readonly + * @type {boolean} + */ + is_number = true; + + /** + * @readonly + * @type {any} + */ + value = undefined; + + /** + * + * @param {Scope} scope + * @param {Expression} expression + */ + constructor(scope, expression) { + switch (expression.type) { + case 'Literal': { + this.values.add(expression.value); + break; + } + + case 'Identifier': { + const binding = scope.get(expression.name); + + if (binding) { + if ( + binding.initial?.type === 'CallExpression' && + get_rune(binding.initial, scope) === '$props.id' + ) { + this.values.add(STRING); + break; + } + + const is_prop = + binding.kind === 'prop' || + binding.kind === 'rest_prop' || + binding.kind === 'bindable_prop'; + + if (!binding.updated && binding.initial !== null && !is_prop) { + const evaluation = binding.scope.evaluate(/** @type {Expression} */ (binding.initial)); + for (const value of evaluation.values) { + this.values.add(value); + } + break; + } + + // TODO each index is always defined + } + + // TODO glean what we can from reassignments + // TODO one day, expose props and imports somehow + + this.values.add(UNKNOWN); + break; + } + + case 'BinaryExpression': { + const a = scope.evaluate(/** @type {Expression} */ (expression.left)); // `left` cannot be `PrivateIdentifier` unless operator is `in` + const b = scope.evaluate(expression.right); + + if (a.is_known && b.is_known) { + this.values.add(binary[expression.operator](a.value, b.value)); + break; + } + + switch (expression.operator) { + case '!=': + case '!==': + case '<': + case '<=': + case '>': + case '>=': + case '==': + case '===': + case 'in': + case 'instanceof': + this.values.add(true); + this.values.add(false); + break; + + case '%': + case '&': + case '*': + case '**': + case '-': + case '/': + case '<<': + case '>>': + case '>>>': + case '^': + case '|': + this.values.add(NUMBER); + break; + + case '+': + if (a.is_string || b.is_string) { + this.values.add(STRING); + } else if (a.is_number && b.is_number) { + this.values.add(NUMBER); + } else { + this.values.add(STRING); + this.values.add(NUMBER); + } + break; + + default: + this.values.add(UNKNOWN); + } + break; + } + + case 'ConditionalExpression': { + const test = scope.evaluate(expression.test); + const consequent = scope.evaluate(expression.consequent); + const alternate = scope.evaluate(expression.alternate); + + if (test.is_known) { + for (const value of (test.value ? consequent : alternate).values) { + this.values.add(value); + } + } else { + for (const value of consequent.values) { + this.values.add(value); + } + + for (const value of alternate.values) { + this.values.add(value); + } + } + break; + } + + case 'LogicalExpression': { + const a = scope.evaluate(expression.left); + const b = scope.evaluate(expression.right); + + if (a.is_known) { + if (b.is_known) { + this.values.add(logical[expression.operator](a.value, b.value)); + break; + } + + if ( + (expression.operator === '&&' && !a.value) || + (expression.operator === '||' && a.value) || + (expression.operator === '??' && a.value != null) + ) { + this.values.add(a.value); + } else { + for (const value of b.values) { + this.values.add(value); + } + } + + break; + } + + for (const value of a.values) { + this.values.add(value); + } + + for (const value of b.values) { + this.values.add(value); + } + break; + } + + case 'UnaryExpression': { + const argument = scope.evaluate(expression.argument); + + if (argument.is_known) { + this.values.add(unary[expression.operator](argument.value)); + break; + } + + switch (expression.operator) { + case '!': + case 'delete': + this.values.add(false); + this.values.add(true); + break; + + case '+': + case '-': + case '~': + this.values.add(NUMBER); + break; + + case 'typeof': + this.values.add(STRING); + break; + + case 'void': + this.values.add(undefined); + break; + + default: + this.values.add(UNKNOWN); + } + break; + } + + default: { + this.values.add(UNKNOWN); + } + } + + for (const value of this.values) { + this.value = value; // saves having special logic for `size === 1` + + if (value !== STRING && typeof value !== 'string') { + this.is_string = false; + } + + if (value !== NUMBER && typeof value !== 'number') { + this.is_number = false; + } + + if (value == null || value === UNKNOWN) { + this.is_defined = false; + } + } + + if (this.values.size > 1 || typeof this.value === 'symbol') { + this.is_known = false; + } + } +} + export class Scope { /** @type {ScopeRoot} */ root; @@ -279,8 +542,63 @@ export class Scope { this.root.conflicts.add(node.name); } } + + /** + * Does partial evaluation to find an exact value or at least the rough type of the expression. + * Only call this once scope has been fully generated in a first pass, + * else this evaluates on incomplete data and may yield wrong results. + * @param {Expression} expression + * @param {Set} values + */ + evaluate(expression, values = new Set()) { + return new Evaluation(this, expression); + } } +/** @type {Record any>} */ +const binary = { + '!=': (left, right) => left != right, + '!==': (left, right) => left !== right, + '<': (left, right) => left < right, + '<=': (left, right) => left <= right, + '>': (left, right) => left > right, + '>=': (left, right) => left >= right, + '==': (left, right) => left == right, + '===': (left, right) => left === right, + in: (left, right) => left in right, + instanceof: (left, right) => left instanceof right, + '%': (left, right) => left % right, + '&': (left, right) => left & right, + '*': (left, right) => left * right, + '**': (left, right) => left ** right, + '+': (left, right) => left + right, + '-': (left, right) => left - right, + '/': (left, right) => left / right, + '<<': (left, right) => left << right, + '>>': (left, right) => left >> right, + '>>>': (left, right) => left >>> right, + '^': (left, right) => left ^ right, + '|': (left, right) => left | right +}; + +/** @type {Record any>} */ +const unary = { + '-': (argument) => -argument, + '+': (argument) => +argument, + '!': (argument) => !argument, + '~': (argument) => ~argument, + typeof: (argument) => typeof argument, + void: () => undefined, + delete: () => true +}; + +/** @type {Record any>} */ +const logical = { + '||': (left, right) => left || right, + '&&': (left, right) => left && right, + '??': (left, right) => left ?? right +}; + export class ScopeRoot { /** @type {Set} */ conflicts = new Set(); diff --git a/packages/svelte/tests/snapshot/samples/nullish-coallescence-omittance/_expected/client/index.svelte.js b/packages/svelte/tests/snapshot/samples/nullish-coallescence-omittance/_expected/client/index.svelte.js index 332c909ebed9..21f6ed9680a9 100644 --- a/packages/svelte/tests/snapshot/samples/nullish-coallescence-omittance/_expected/client/index.svelte.js +++ b/packages/svelte/tests/snapshot/samples/nullish-coallescence-omittance/_expected/client/index.svelte.js @@ -10,11 +10,11 @@ export default function Nullish_coallescence_omittance($$anchor) { var fragment = root(); var h1 = $.first_child(fragment); - h1.textContent = `Hello, ${name ?? ''}!`; + h1.textContent = 'Hello, world!'; var b = $.sibling(h1, 2); - b.textContent = `${1 ?? 'stuff'}${2 ?? 'more stuff'}${3 ?? 'even more stuff'}`; + b.textContent = '123'; var button = $.sibling(b, 2); @@ -26,7 +26,7 @@ export default function Nullish_coallescence_omittance($$anchor) { var h1_1 = $.sibling(button, 2); - h1_1.textContent = `Hello, ${name ?? 'earth' ?? ''}`; + h1_1.textContent = 'Hello, world'; $.template_effect(() => $.set_text(text, `Count is ${$.get(count) ?? ''}`)); $.append($$anchor, fragment); } diff --git a/packages/svelte/tests/snapshot/samples/nullish-coallescence-omittance/_expected/server/index.svelte.js b/packages/svelte/tests/snapshot/samples/nullish-coallescence-omittance/_expected/server/index.svelte.js index 8181bfd98eeb..3b23befcd44e 100644 --- a/packages/svelte/tests/snapshot/samples/nullish-coallescence-omittance/_expected/server/index.svelte.js +++ b/packages/svelte/tests/snapshot/samples/nullish-coallescence-omittance/_expected/server/index.svelte.js @@ -4,5 +4,5 @@ export default function Nullish_coallescence_omittance($$payload) { let name = 'world'; let count = 0; - $$payload.out += `

Hello, ${$.escape(name)}!

${$.escape(1 ?? 'stuff')}${$.escape(2 ?? 'more stuff')}${$.escape(3 ?? 'even more stuff')}

Hello, ${$.escape(name ?? 'earth' ?? null)}

`; + $$payload.out += `

Hello, world!

123

Hello, world

`; } \ No newline at end of file diff --git a/packages/svelte/tests/snapshot/samples/skip-static-subtree/_expected/client/index.svelte.js b/packages/svelte/tests/snapshot/samples/skip-static-subtree/_expected/client/index.svelte.js index 46d376aca2f9..b341d39f28fb 100644 --- a/packages/svelte/tests/snapshot/samples/skip-static-subtree/_expected/client/index.svelte.js +++ b/packages/svelte/tests/snapshot/samples/skip-static-subtree/_expected/client/index.svelte.js @@ -38,7 +38,7 @@ export default function Skip_static_subtree($$anchor, $$props) { var select = $.sibling(div_1, 2); var option = $.child(select); - option.value = null == (option.__value = 'a') ? '' : 'a'; + option.value = option.__value = 'a'; $.reset(select); var img = $.sibling(select, 2); diff --git a/packages/svelte/tests/sourcemaps/samples/attached-sourcemap/input.svelte b/packages/svelte/tests/sourcemaps/samples/attached-sourcemap/input.svelte index 21a47a72a9c9..715bbda8d92e 100644 --- a/packages/svelte/tests/sourcemaps/samples/attached-sourcemap/input.svelte +++ b/packages/svelte/tests/sourcemaps/samples/attached-sourcemap/input.svelte @@ -8,4 +8,4 @@ replace_me_script = 'hello' ; -

{done_replace_script_2}

+

{Math.random() < 1 && done_replace_script_2}

From 6a7e53feaa53425624a47d7ebed98ff8d6fb1d8b Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Mon, 14 Apr 2025 22:23:50 -0400 Subject: [PATCH 3/3] Version Packages (#15764) Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> --- .changeset/green-starfishes-shave.md | 5 ----- .changeset/selfish-onions-begin.md | 5 ----- packages/svelte/CHANGELOG.md | 10 ++++++++++ packages/svelte/package.json | 2 +- packages/svelte/src/version.js | 2 +- 5 files changed, 12 insertions(+), 12 deletions(-) delete mode 100644 .changeset/green-starfishes-shave.md delete mode 100644 .changeset/selfish-onions-begin.md diff --git a/.changeset/green-starfishes-shave.md b/.changeset/green-starfishes-shave.md deleted file mode 100644 index 967bba753c74..000000000000 --- a/.changeset/green-starfishes-shave.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'svelte': patch ---- - -fix: relax `:global` selector list validation diff --git a/.changeset/selfish-onions-begin.md b/.changeset/selfish-onions-begin.md deleted file mode 100644 index decf0d5fc6d4..000000000000 --- a/.changeset/selfish-onions-begin.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'svelte': minor ---- - -feat: partially evaluate certain expressions diff --git a/packages/svelte/CHANGELOG.md b/packages/svelte/CHANGELOG.md index 58f2317796c8..c8f0ad7ed964 100644 --- a/packages/svelte/CHANGELOG.md +++ b/packages/svelte/CHANGELOG.md @@ -1,5 +1,15 @@ # svelte +## 5.27.0 + +### Minor Changes + +- feat: partially evaluate certain expressions ([#15494](https://github.com/sveltejs/svelte/pull/15494)) + +### Patch Changes + +- fix: relax `:global` selector list validation ([#15762](https://github.com/sveltejs/svelte/pull/15762)) + ## 5.26.3 ### Patch Changes diff --git a/packages/svelte/package.json b/packages/svelte/package.json index b8654671ec97..af78d2679ab8 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.26.3", + "version": "5.27.0", "type": "module", "types": "./types/index.d.ts", "engines": { diff --git a/packages/svelte/src/version.js b/packages/svelte/src/version.js index 190958814669..27a39136f8c2 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.26.3'; +export const VERSION = '5.27.0'; export const PUBLIC_VERSION = '5'; pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy