diff --git a/packages/svelte/src/compiler/phases/3-transform/client/utils.js b/packages/svelte/src/compiler/phases/3-transform/client/utils.js index eac059e3b6..710053e3ee 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/utils.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/utils.js @@ -1,5 +1,5 @@ import * as b from '../../../utils/builders.js'; -import { extract_paths, is_simple_expression } from '../../../utils/ast.js'; +import { extract_paths, is_simple_expression, unwrap_ts_expression } from '../../../utils/ast.js'; import { error } from '../../../errors.js'; import { PROPS_IS_LAZY_INITIAL, @@ -7,6 +7,7 @@ import { PROPS_IS_RUNES, PROPS_IS_UPDATED } from '../../../../constants.js'; +import { get_rune } from '../../scope'; /** * @template {import('./types').ClientTransformState} State @@ -535,18 +536,19 @@ export function should_proxy_or_freeze(node) { /** * @param {import('#compiler').Binding | undefined} binding - * @param {import('#compiler').SvelteNode[]} path + * @param {string} name * @returns {boolean} */ -export function is_hoistable_declaration(binding, path) { - const is_top_level = path.at(-1)?.type === 'Program'; +export function is_hoistable_declaration(binding, name) { // TODO: allow object expressions that are not passed to functions or components as props // and expressions as long as they do not reference non-hoistable variables return ( - is_top_level && !!binding && + binding.kind === 'normal' && + binding.scope.is_top_level && !binding.mutated && !binding.reassigned && - binding?.initial?.type === 'Literal' + binding.initial?.type === 'Literal' && + !binding.scope.declared_in_outer_scope(name) ); } diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/javascript-legacy.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/javascript-legacy.js index 7471fb860c..316f788316 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/javascript-legacy.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/javascript-legacy.js @@ -1,7 +1,12 @@ import { is_hoistable_function } from '../../utils.js'; import * as b from '../../../../utils/builders.js'; import { extract_paths } from '../../../../utils/ast.js'; -import { create_state_declarators, get_prop_source, serialize_get_binding } from '../utils.js'; +import { + create_state_declarators, + get_prop_source, + is_hoistable_declaration, + serialize_get_binding +} from '../utils.js'; /** @type {import('../types.js').ComponentVisitors} */ export const javascript_visitors_legacy = { @@ -18,6 +23,19 @@ export const javascript_visitors_legacy = { if (!has_state && !has_props) { const init = declarator.init; + + if (init != null && declarator.id.type === 'Identifier') { + const binding = state.scope + .owner(declarator.id.name) + ?.declarations.get(declarator.id.name); + + if (is_hoistable_declaration(binding, declarator.id.name)) { + state.scope.root.unique; + state.hoisted.push(b.declaration('const', declarator.id, init)); + continue; + } + } + if (init != null && is_hoistable_function(init)) { const hoistable_function = visit(init); state.hoisted.push( 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 de89846f68..67320cce8c 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 @@ -156,21 +156,22 @@ export const javascript_visitors_runes = { return { ...node, body }; }, - VariableDeclaration(node, { path, state, visit }) { + VariableDeclaration(node, { state, visit }) { const declarations = []; for (const declarator of node.declarations) { const init = unwrap_ts_expression(declarator.init); - const rune = get_rune(init, state.scope); - if (!rune && init != null && declarator.id.type === 'Identifier') { + if (init != null && declarator.id.type === 'Identifier') { const binding = state.scope.owner(declarator.id.name)?.declarations.get(declarator.id.name); - if (is_hoistable_declaration(binding, path)) { + if (is_hoistable_declaration(binding, declarator.id.name)) { + state.scope.root.unique; state.hoisted.push(b.declaration('const', declarator.id, init)); continue; } } + const rune = get_rune(init, state.scope); if (!rune || rune === '$effect.active' || rune === '$effect.root' || rune === '$inspect') { if (init != null && is_hoistable_function(init)) { const hoistable_function = visit(init); 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 3f72d2f235..0f6f88ffe1 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 @@ -501,8 +501,6 @@ function serialize_element_attribute_update_assignment(element, node_id, attribu } let grouped_value = value; - /** @type {import('estree').Expression | null} */ - let constant_expression = grouped_value; if (name === 'autofocus') { state.init.push(b.stmt(b.call('$.auto_focus', node_id, value))); @@ -511,7 +509,6 @@ function serialize_element_attribute_update_assignment(element, node_id, attribu if (name === 'class') { grouped_value = b.call('$.to_class', value); - constant_expression = null; } /** @@ -567,18 +564,19 @@ function serialize_element_attribute_update_assignment(element, node_id, attribu } }; - let is_in_hoistable = false; + let hoistable = false; if (Array.isArray(attribute.value)) { for (let value of attribute.value) { if (value.type === 'ExpressionTag' && value.expression.type === 'Identifier') { const binding = context.state.scope .owner(value.expression.name) ?.declarations.get(value.expression.name); - is_in_hoistable ||= is_hoistable_declaration(binding, context.path); + hoistable ||= is_hoistable_declaration(binding, value.expression.name); } } } - if (attribute.metadata.dynamic && !is_in_hoistable) { + + if (attribute.metadata.dynamic && !hoistable) { const id = state.scope.generate(`${node_id.name}_${name}`); serialize_update_assignment( state, @@ -590,12 +588,12 @@ function serialize_element_attribute_update_assignment(element, node_id, attribu ); return true; } else { - if (!is_in_hoistable || DOMBooleanAttributes.includes(name)) { - state.init.push(assign(grouped_value).grouped); - } else if (constant_expression) { + if (hoistable) { push_template_quasi(context.state, ` ${name}="`); push_template_expression(context.state, grouped_value); push_template_quasi(context.state, `"`); + } else { + state.init.push(assign(grouped_value).grouped); } return false; } diff --git a/packages/svelte/src/compiler/phases/constants.js b/packages/svelte/src/compiler/phases/constants.js index b8663e97f0..d31c38af4e 100644 --- a/packages/svelte/src/compiler/phases/constants.js +++ b/packages/svelte/src/compiler/phases/constants.js @@ -70,6 +70,76 @@ export const ElementBindings = [ 'indeterminate' ]; +export const GlobalBindings = new Set([ + 'Map', + 'Set', + 'Array', + 'Uint8Array', + 'Uint8ClampedArray', + 'Int16Array', + 'Uint16Array', + 'Int32Array', + 'Uint32Array', + 'BigInt64Array', + 'BigUint64Array', + 'Float32Array', + 'Float64Array', + 'Int8Array', + 'Function', + 'Error', + 'AggregateError', + 'EvalError', + 'RangeError', + 'ReferenceError', + 'SyntaxError', + 'TypeError', + 'URIError', + 'InternalError', + 'Number', + 'Math', + 'BigInt', + 'String', + 'RegEx', + 'Date', + 'Boolean', + 'Symbol', + 'Object', + 'ArrayBuffer', + 'SharedArrayBuffer', + 'DataView', + 'Atomics', + 'JSON', + 'WeakRef', + 'FinalizationRegistry', + 'Iterator', + 'AsyncIterator', + 'Promise', + 'GeneratorFunction', + 'AsyncGeneratorFunction', + 'Generator', + 'AsyncGenerator', + 'AsyncFunction', + 'Reflect', + 'Proxy', + 'Intl', + 'eval', + 'isFinite', + 'isNaN', + 'parseFloat', + 'parseInt', + 'decodeURI', + 'decodeURIComponent', + 'encodeURI', + 'encodeURIComponent', + 'console', + 'undefined', + 'NaN', + 'Infinity', + 'globalThis', + 'window', + 'document' +]); + export const Runes = /** @type {const} */ ([ '$state', '$state.frozen', diff --git a/packages/svelte/src/compiler/phases/scope.js b/packages/svelte/src/compiler/phases/scope.js index ad3e8fdb34..7e24f271ce 100644 --- a/packages/svelte/src/compiler/phases/scope.js +++ b/packages/svelte/src/compiler/phases/scope.js @@ -4,12 +4,19 @@ import { is_element_node } from './nodes.js'; import * as b from '../utils/builders.js'; import { error } from '../errors.js'; import { extract_identifiers, extract_identifiers_from_expression } from '../utils/ast.js'; -import { JsKeywords, Runes } from './constants.js'; +import { GlobalBindings, JsKeywords, Runes } from './constants.js'; export class Scope { /** @type {ScopeRoot} */ root; + /** + * Whether this is a top-level script scope. + * I.e. `