diff --git a/packages/svelte/CHANGELOG.md b/packages/svelte/CHANGELOG.md index 709b829fcd46..8c01c7109957 100644 --- a/packages/svelte/CHANGELOG.md +++ b/packages/svelte/CHANGELOG.md @@ -1,5 +1,11 @@ # svelte +## 5.28.0 + +### Minor Changes + +- feat: partially evaluate more expressions ([#15781](https://github.com/sveltejs/svelte/pull/15781)) + ## 5.27.3 ### Patch Changes diff --git a/packages/svelte/package.json b/packages/svelte/package.json index efead5604c3b..784b14ea26f6 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.27.3", + "version": "5.28.0", "type": "module", "types": "./types/index.d.ts", "engines": { 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 380cf6cd02f9..bc79b760431c 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 @@ -69,11 +69,17 @@ export function build_template_chunk( node.metadata.expression ); - has_state ||= node.metadata.expression.has_state; + const evaluated = state.scope.evaluate(value); + + has_state ||= node.metadata.expression.has_state && !evaluated.is_known; if (values.length === 1) { // If we have a single expression, then pass that in directly to possibly avoid doing // extra work in the template_effect (instead we do the work in set_text). + if (evaluated.is_known) { + value = b.literal(evaluated.value); + } + return { value, has_state }; } @@ -89,8 +95,6 @@ export function build_template_chunk( } } - const evaluated = state.scope.evaluate(value); - if (evaluated.is_known) { quasi.value.cooked += evaluated.value + ''; } else { diff --git a/packages/svelte/src/compiler/phases/scope.js b/packages/svelte/src/compiler/phases/scope.js index 570d5e22d901..8297f174d3de 100644 --- a/packages/svelte/src/compiler/phases/scope.js +++ b/packages/svelte/src/compiler/phases/scope.js @@ -1,4 +1,4 @@ -/** @import { ArrowFunctionExpression, BinaryOperator, ClassDeclaration, Expression, FunctionDeclaration, FunctionExpression, Identifier, ImportDeclaration, MemberExpression, LogicalOperator, Node, Pattern, UnaryOperator, VariableDeclarator } from 'estree' */ +/** @import { ArrowFunctionExpression, BinaryOperator, ClassDeclaration, Expression, FunctionDeclaration, FunctionExpression, Identifier, ImportDeclaration, MemberExpression, LogicalOperator, Node, Pattern, UnaryOperator, VariableDeclarator, Super } from 'estree' */ /** @import { Context, Visitor } from 'zimmerframe' */ /** @import { AST, BindingKind, DeclarationKind } from '#compiler' */ import is_reference from 'is-reference'; @@ -18,8 +18,71 @@ import { validate_identifier_name } from './2-analyze/visitors/shared/utils.js'; const UNKNOWN = Symbol('unknown'); /** Includes `BigInt` */ -const NUMBER = Symbol('number'); -const STRING = Symbol('string'); +export const NUMBER = Symbol('number'); +export const STRING = Symbol('string'); + +/** @type {Record} */ +const globals = { + BigInt: [NUMBER, BigInt], + 'Math.min': [NUMBER, Math.min], + 'Math.max': [NUMBER, Math.max], + 'Math.random': [NUMBER], + 'Math.floor': [NUMBER, Math.floor], + // @ts-expect-error + 'Math.f16round': [NUMBER, Math.f16round], + 'Math.round': [NUMBER, Math.round], + 'Math.abs': [NUMBER, Math.abs], + 'Math.acos': [NUMBER, Math.acos], + 'Math.asin': [NUMBER, Math.asin], + 'Math.atan': [NUMBER, Math.atan], + 'Math.atan2': [NUMBER, Math.atan2], + 'Math.ceil': [NUMBER, Math.ceil], + 'Math.cos': [NUMBER, Math.cos], + 'Math.sin': [NUMBER, Math.sin], + 'Math.tan': [NUMBER, Math.tan], + 'Math.exp': [NUMBER, Math.exp], + 'Math.log': [NUMBER, Math.log], + 'Math.pow': [NUMBER, Math.pow], + 'Math.sqrt': [NUMBER, Math.sqrt], + 'Math.clz32': [NUMBER, Math.clz32], + 'Math.imul': [NUMBER, Math.imul], + 'Math.sign': [NUMBER, Math.sign], + 'Math.log10': [NUMBER, Math.log10], + 'Math.log2': [NUMBER, Math.log2], + 'Math.log1p': [NUMBER, Math.log1p], + 'Math.expm1': [NUMBER, Math.expm1], + 'Math.cosh': [NUMBER, Math.cosh], + 'Math.sinh': [NUMBER, Math.sinh], + 'Math.tanh': [NUMBER, Math.tanh], + 'Math.acosh': [NUMBER, Math.acosh], + 'Math.asinh': [NUMBER, Math.asinh], + 'Math.atanh': [NUMBER, Math.atanh], + 'Math.trunc': [NUMBER, Math.trunc], + 'Math.fround': [NUMBER, Math.fround], + 'Math.cbrt': [NUMBER, Math.cbrt], + Number: [NUMBER, Number], + 'Number.isInteger': [NUMBER, Number.isInteger], + 'Number.isFinite': [NUMBER, Number.isFinite], + 'Number.isNaN': [NUMBER, Number.isNaN], + 'Number.isSafeInteger': [NUMBER, Number.isSafeInteger], + 'Number.parseFloat': [NUMBER, Number.parseFloat], + 'Number.parseInt': [NUMBER, Number.parseInt], + String: [STRING, String], + 'String.fromCharCode': [STRING, String.fromCharCode], + 'String.fromCodePoint': [STRING, String.fromCodePoint] +}; + +/** @type {Record} */ +const global_constants = { + 'Math.PI': Math.PI, + 'Math.E': Math.E, + 'Math.LN10': Math.LN10, + 'Math.LN2': Math.LN2, + 'Math.LOG10E': Math.LOG10E, + 'Math.LOG2E': Math.LOG2E, + 'Math.SQRT2': Math.SQRT2, + 'Math.SQRT1_2': Math.SQRT1_2 +}; export class Binding { /** @type {Scope} */ @@ -107,7 +170,7 @@ export class Binding { class Evaluation { /** @type {Set} */ - values = new Set(); + values; /** * True if there is exactly one possible value @@ -147,8 +210,11 @@ class Evaluation { * * @param {Scope} scope * @param {Expression} expression + * @param {Set} values */ - constructor(scope, expression) { + constructor(scope, expression, values) { + this.values = values; + switch (expression.type) { case 'Literal': { this.values.add(expression.value); @@ -172,15 +238,18 @@ class Evaluation { 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); - } + if (binding.initial?.type === 'EachBlock' && binding.initial.index === expression.name) { + this.values.add(NUMBER); break; } - // TODO each index is always defined + if (!binding.updated && binding.initial !== null && !is_prop) { + binding.scope.evaluate(/** @type {Expression} */ (binding.initial), this.values); + break; + } + } else if (expression.name === 'undefined') { + this.values.add(undefined); + break; } // TODO glean what we can from reassignments @@ -336,6 +405,101 @@ class Evaluation { break; } + case 'CallExpression': { + const keypath = get_global_keypath(expression.callee, scope); + + if (keypath) { + if (is_rune(keypath)) { + const arg = /** @type {Expression | undefined} */ (expression.arguments[0]); + + switch (keypath) { + case '$state': + case '$state.raw': + case '$derived': + if (arg) { + scope.evaluate(arg, this.values); + } else { + this.values.add(undefined); + } + break; + + case '$props.id': + this.values.add(STRING); + break; + + case '$effect.tracking': + this.values.add(false); + this.values.add(true); + break; + + case '$derived.by': + if (arg?.type === 'ArrowFunctionExpression' && arg.body.type !== 'BlockStatement') { + scope.evaluate(arg.body, this.values); + break; + } + + this.values.add(UNKNOWN); + break; + + default: { + this.values.add(UNKNOWN); + } + } + + break; + } + + if ( + Object.hasOwn(globals, keypath) && + expression.arguments.every((arg) => arg.type !== 'SpreadElement') + ) { + const [type, fn] = globals[keypath]; + const values = expression.arguments.map((arg) => scope.evaluate(arg)); + + if (fn && values.every((e) => e.is_known)) { + this.values.add(fn(...values.map((e) => e.value))); + } else { + this.values.add(type); + } + + break; + } + } + + this.values.add(UNKNOWN); + break; + } + + case 'TemplateLiteral': { + let result = expression.quasis[0].value.cooked; + + for (let i = 0; i < expression.expressions.length; i += 1) { + const e = scope.evaluate(expression.expressions[i]); + + if (e.is_known) { + result += e.value + expression.quasis[i + 1].value.cooked; + } else { + this.values.add(STRING); + break; + } + } + + this.values.add(result); + break; + } + + case 'MemberExpression': { + const keypath = get_global_keypath(expression, scope); + + if (keypath && Object.hasOwn(global_constants, keypath)) { + this.values.add(global_constants[keypath]); + break; + } + + this.values.add(UNKNOWN); + break; + } + default: { this.values.add(UNKNOWN); } @@ -548,10 +712,10 @@ export class Scope { * 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 + * @param {Set} [values] */ evaluate(expression, values = new Set()) { - return new Evaluation(this, expression); + return new Evaluation(this, expression, values); } } @@ -1115,7 +1279,19 @@ export function get_rune(node, scope) { if (!node) return null; if (node.type !== 'CallExpression') return null; - let n = node.callee; + const keypath = get_global_keypath(node.callee, scope); + + if (!keypath || !is_rune(keypath)) return null; + return keypath; +} + +/** + * Returns the name of the rune if the given expression is a `CallExpression` using a rune. + * @param {Expression | Super} node + * @param {Scope} scope + */ +function get_global_keypath(node, scope) { + let n = node; let joined = ''; @@ -1133,12 +1309,8 @@ export function get_rune(node, scope) { if (n.type !== 'Identifier') return null; - joined = n.name + joined; - - if (!is_rune(joined)) return null; - const binding = scope.get(n.name); if (binding !== null) return null; // rune name, but references a variable or store - return joined; + return n.name + joined; } diff --git a/packages/svelte/src/version.js b/packages/svelte/src/version.js index c64eded9c4c8..4418204ecab7 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.27.3'; +export const VERSION = '5.28.0'; export const PUBLIC_VERSION = '5'; diff --git a/packages/svelte/tests/snapshot/samples/each-index-non-null/_config.js b/packages/svelte/tests/snapshot/samples/each-index-non-null/_config.js new file mode 100644 index 000000000000..f47bee71df87 --- /dev/null +++ b/packages/svelte/tests/snapshot/samples/each-index-non-null/_config.js @@ -0,0 +1,3 @@ +import { test } from '../../test'; + +export default test({}); diff --git a/packages/svelte/tests/snapshot/samples/each-index-non-null/_expected/client/index.svelte.js b/packages/svelte/tests/snapshot/samples/each-index-non-null/_expected/client/index.svelte.js new file mode 100644 index 000000000000..3d46a679b865 --- /dev/null +++ b/packages/svelte/tests/snapshot/samples/each-index-non-null/_expected/client/index.svelte.js @@ -0,0 +1,19 @@ +import 'svelte/internal/disclose-version'; +import 'svelte/internal/flags/legacy'; +import * as $ from 'svelte/internal/client'; + +var root_1 = $.template(`

`); + +export default function Each_index_non_null($$anchor) { + var fragment = $.comment(); + var node = $.first_child(fragment); + + $.each(node, 0, () => Array(10), $.index, ($$anchor, $$item, i) => { + var p = root_1(); + + p.textContent = `index: ${i}`; + $.append($$anchor, p); + }); + + $.append($$anchor, fragment); +} \ No newline at end of file diff --git a/packages/svelte/tests/snapshot/samples/each-index-non-null/_expected/server/index.svelte.js b/packages/svelte/tests/snapshot/samples/each-index-non-null/_expected/server/index.svelte.js new file mode 100644 index 000000000000..3431e36833b5 --- /dev/null +++ b/packages/svelte/tests/snapshot/samples/each-index-non-null/_expected/server/index.svelte.js @@ -0,0 +1,13 @@ +import * as $ from 'svelte/internal/server'; + +export default function Each_index_non_null($$payload) { + const each_array = $.ensure_array_like(Array(10)); + + $$payload.out += ``; + + for (let i = 0, $$length = each_array.length; i < $$length; i++) { + $$payload.out += `

index: ${$.escape(i)}

`; + } + + $$payload.out += ``; +} \ No newline at end of file diff --git a/packages/svelte/tests/snapshot/samples/each-index-non-null/index.svelte b/packages/svelte/tests/snapshot/samples/each-index-non-null/index.svelte new file mode 100644 index 000000000000..03bfc9e37299 --- /dev/null +++ b/packages/svelte/tests/snapshot/samples/each-index-non-null/index.svelte @@ -0,0 +1,3 @@ +{#each Array(10), i} +

index: {i}

+{/each} diff --git a/packages/svelte/tests/snapshot/samples/purity/_expected/client/index.svelte.js b/packages/svelte/tests/snapshot/samples/purity/_expected/client/index.svelte.js index 940ed8f9e8fa..5bc9766acfd4 100644 --- a/packages/svelte/tests/snapshot/samples/purity/_expected/client/index.svelte.js +++ b/packages/svelte/tests/snapshot/samples/purity/_expected/client/index.svelte.js @@ -8,7 +8,7 @@ export default function Purity($$anchor) { var fragment = root(); var p = $.first_child(fragment); - p.textContent = Math.max(0, Math.min(0, 100)); + p.textContent = 0; var p_1 = $.sibling(p, 2); diff --git a/packages/svelte/tests/snapshot/samples/purity/_expected/server/index.svelte.js b/packages/svelte/tests/snapshot/samples/purity/_expected/server/index.svelte.js index 588332407a63..9457378c0db4 100644 --- a/packages/svelte/tests/snapshot/samples/purity/_expected/server/index.svelte.js +++ b/packages/svelte/tests/snapshot/samples/purity/_expected/server/index.svelte.js @@ -1,7 +1,7 @@ import * as $ from 'svelte/internal/server'; export default function Purity($$payload) { - $$payload.out += `

${$.escape(Math.max(0, Math.min(0, 100)))}

${$.escape(location.href)}

`; + $$payload.out += `

0

${$.escape(location.href)}

`; Child($$payload, { prop: encodeURIComponent('hello') }); $$payload.out += ``; } \ No newline at end of file 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